系列文章目录
JAVAEE初阶第七节(中)------物理原理与TCP_IP
文章目录
- 系列文章目录
- 一.应用层重点协议)
-
- [1. DNS](#1. DNS)
- [2 .NAT](#2 .NAT)
- [3. NAT IP转换过程](#3. NAT IP转换过程)
- [4 .NAPT](#4 .NAPT)
- [5. NAT技术的缺陷](#5. NAT技术的缺陷)
- [6. HTTP/HTTPS](#6. HTTP/HTTPS)
- [7. 自定义协议](#7. 自定义协议)
- [二. 传输层重点协议](#二. 传输层重点协议)
-
- [1 .UDP协议](#1 .UDP协议)
-
- [2.1.1 UDP协议端格式](#2.1.1 UDP协议端格式)
- [2.1.2 UDP的特点](#2.1.2 UDP的特点)
- [2.1.3 UDP的构成](#2.1.3 UDP的构成)
- [2.1.4 基于UDP的应用层协议](#2.1.4 基于UDP的应用层协议)
- 2.TCP协议
-
- [2.2.1 TCP协议段格式](#2.2.1 TCP协议段格式)
- [2.2.2 TCP原理](#2.2.2 TCP原理)
-
- [1. 确认应答特性(可靠传输的核心机制)](#1. 确认应答特性(可靠传输的核心机制))
- 2.超时重传特性(安全机制)
- [3. TCP的连接管理特性(重点)](#3. TCP的连接管理特性(重点))
- [4. 连接管理过程中涉及到的TCP状态转换](#4. 连接管理过程中涉及到的TCP状态转换)
- [4. 滑动窗口特性(效率机制)](#4. 滑动窗口特性(效率机制))
- [5. 流量控制机制(安全机制)](#5. 流量控制机制(安全机制))
- [6. 拥塞控制特性(安全机制)](#6. 拥塞控制特性(安全机制))
- [7. 延时应答特性(效率机制)](#7. 延时应答特性(效率机制))
- [8. 捎带应答机制(效率机制)](#8. 捎带应答机制(效率机制))
- [9. 面向字节流特性](#9. 面向字节流特性)
- [10. TCP异常情况](#10. TCP异常情况)
- [11. TCP小结](#11. TCP小结)
物理原理与TCP_IP(上)
- 应用层重点协议
- 传输层重点协议
一.应用层重点协议)
1. DNS
DNS,即Domain Name System,域名系统。DNS是一整套从域名映射到IP的系统。
TCP/IP中使用IP地址来确定网络上的一台主机,但是IP地址不方便记忆,且不能表达地址组织信息,于是人们发明了域名 ,并通过域名系统来映射域名和IP地址。
域名是一个字符串,如 www.baidu.com , hr.nowcoder.com域名系统为一个树形结构的系统,包含多个根节点。其中:
- 根节点即为根域名服务器,最早IPv4的根域名服务器全球只有13台,IPv6在此基础上扩充了数量。
- 子节点主要由各级DNS服务器,或DNS缓存构成。
- DNS域名服务器,即提供域名转换为IP地址的服务器。
- 浏览器、主机系统、路由器中都保存有DNS缓存。
- Windows系统的DNS缓存在 C:\Windows\System32\drivers\etc\hosts 文件中,Mac/Linux系统的DNS缓存在 /etc/hosts 文件中。
网络通信发送数据时,如果使用目的主机的域名,需要先通过域名解析查找到对应的IP地址:
- 域名解析的过程,可以简单的理解为:发送端主机作为域名系统树形结构的一个子节点,通过域名信息,从下到上查找对应IP地址的过程。如果到根节点(根域名服务器)还找不到,即找不到该主机。
- 域名解析使用DNS协议 来传输数据。DNS协议是应用层协议,基于传输层UDP或TCP协议来实现。
2 .NAT
技术背景
之前讨论了,IPv4协议中,IP地址数量不充足的问题
NAT技术当前解决IP地址不够用的主要手段,是路由器的一个重要功能;
- NAT能够将私有IP对外通信时转为全局IP。也就是就是一种将私有IP和全局IP相互转化的技术方法:
- 很多学校,家庭,公司内部采用每个终端设置私有IP,而在路由器或必要的服务器上设置全局IP;
- 全局IP要求唯一,但是私有IP不需要;在不同的局域网中出现相同的私有IP是完全不影响的;
3. NAT IP转换过程
- NAT路由器将源地址从10.0.0.10替换成全局的IP 202.244.174.37;
- NAT路由器收到外部的数据时,又会把目标IP从202.244.174.37替换回10.0.0.10;
- 在NAT路由器内部,有一张自动生成的,用于地址转换的表;
- 当 10.0.0.10 第一次向 163.221.120.9 发送数据时就会生成表中的映射关系;
4 .NAPT
那么问题来了,如果局域网内,有多个主机都访问同一个外网服务器,那么对于服务器返回的数据中,目的IP都是相同的。那么NAT路由器如何判定将这个数据包转发给哪个局域网的主机?
这时候NAPT来解决这个问题了。使用IP+port来建立这个关联关系
这种关联关系也是由NAT路由器自动维护的。例如在TCP的情况下,建立连接时,就会生成这个表项;在断开连接后,就会删除这个表项
5. NAT技术的缺陷
由于NAT依赖这个转换表,所以有诸多限制:
- 无法从NAT外部向内部服务器建立连接;
- 转换表的生成和销毁都需要额外开销;
- 通信过程中一旦NAT设备异常,即使存在热备,所有的TCP连接也都会断开;
6. HTTP/HTTPS
HTTP及HTTPS是应用层重点协议,会在Web开发中学习。
7. 自定义协议
自定义协议:约定网络传输的数据要怎么使用,数据是什么样的格式,里面包含哪些内容。
自定义协议的内容:
服务器和客户端之间要交互哪些信息,客户端按照自定义的约定发送请求,服务器按照自定义的约定来解析请求。
数据的具体格式。服务器按照自定义约定构造响应,客户端也按照自定义的约定解析响应。
自定义协议的过程:(点外卖)请求,约定使用行文本的格式来表示,一个请求以为结尾。多个字段之间使用,来分割。
格式:userld,position \ n
1000,【经纬度】\n
2.响应,也是使用行文本来表示。一个响应中可能会包含多个商家。每个商家占一行。每个商家要返回Id,名称,图片,评分,简介。若干行最后,使用空行作为所有数据的结束标记.
格式:Id,名称,图片,评分,简介
1001,扬州炒饭,[logo图片地址],4.8,非常好吃的炒饭\n
1002,柳州螺蛳粉,[logo图片地址],4.7,很好吃的螺蛳粉\n
客户端和服务器之间往往要进行交互的是"结构化数据"(数据是一个结构体/类包含很多个属性)网络传输的数据其实是"字符串""二进制bt流"。把结构化数据,转成字符串/二进制比特流这个操作,称为"序列化"
把字符串/二进制比特流还原成结构化数据,这个操作,称为"反序列化"
序列化/反序列化具体要组织成什么样的格式,这里包含哪些信息,约定这两件事的过程就是自定义协议的过程.
上述给的例子里,约定的这个格式太过于简单粗暴了。虽然能解决问题,真实情况下,很少会真的这么约定。这样的约定,不太适合扩展,可读性也不高。为了让程序员更方便的去约定这里的协议格式,业界也给出了几个比较好用的方案,可以拿过来直接套进来:
xml
大概的模样形如:1000
【经纬度】响应:
xml
请求:
<request>
<userld>1000</userld>
<position>【经纬度】</position>
</request>
响应:
<response>
<shops>
<shop>
<id>1001</id>
<name>扬州炒饭</name>
<image>图片地址</image>
<rank>4.8</rank>
<description>非常好吃的炒饭</description>
</shop>
</shops
</response>
<request>
这种带有尖括号的说明性文字叫做标签(tag)标签往往是成对出现的
这种称为开始标签
xml
</userld>
这种称为结束标签
开始标签和结束标签之间夹着的就是标签的值,
标签可以嵌套,标签的名字/标签的值/标签的嵌套关系,都是程序员自定义的·
这样的话数据的可读性和扩展性都提升了很多了,标签的名字能够对数据起到描述说明效果,后续要增加一些属性,就新增一个标签即可,对于已有代码影响就不大,代码中按照标签名字获取到标签的值,新增新的标签对于已有代码都没啥影响。缺点:整个数据,冗余信息就非常多了,标签(描述性信息)占据的空间反而比数据本身更多了。尤其是网络传输的时候,这些数据都是要通过网络传输的(消耗带宽)(国内,最贵的硬件资源,是网络带宽)
前端这里有个东西叫做html.也是这种标签的格式来组织的数据,非常像xml.
xml里面的标签/格式/值都是自定义的。。
html里面的标签/格式/值都是有大佬们约定好的,没啥可操作空间,只能遵守人家的约定。
- json: 当下一种非常主流/非常常用的数据组织格式了。
大概的模样形如:
json
请求:
{
userld:1000,
position:[经纬度]
}
响应
[
{
id:1001,
name:"扬州炒饭'
},
{
id:1002,
name:"柳州螺蛳粉"
}
]
json:键值对结构键和值之间使用:分割键值对之间使用,分割
把若干个键值对使用{}括起来。此时就形成了一个json对象
还可以把多个json对象放到一起,使用,分隔开,并且使用【】整体括起来。
就形成了一个json数组
这种json格式的数据可读性很好,扩展性也很好。通过key来对数据起到解释说明。对于xml来说解释说明是通过标签,需要有开始和结束两个标签,比较占用空间,相比之下json只使用一个key就能描述,占用的空间就比xml更少。更节省带宽了。虽然json比xml是节省了带宽但是,很明显,当前这里的带宽仍然是有浪费的部分。尤其是这种数组格式的json.这种情况下往往传输的数据字段都是相同的。使刚才这里的key名字被重复传输了。
- protobuffer: 更节省带宽的方式,效率最高的方式。
只是开发阶段(代码)定义出这里都有哪些资源,描述每个字段的含义,程序真正运行的时候,实际传输的数据是不包含这样的描述信息。所以这样的设定,是最高效的做法(只是程序运行的效率高)并不太有利于程序员阅读。这样的数据是按照二进制的方式来组织的。虽然protobuffer运行效率更高,但是使用并没有比json更广泛。只是哪些对于性能要求非常高的场景,才会使用protobuffer。
二. 传输层重点协议
传输层虽然是系统内核已经实现好了的,但是也需要重点关注。使用的socket api都是传输层提供。
端口号。端口号是一个2个字节的整数。使用端口号的时候,1-1024都属于是系统保留自用的端口。(知名端口号)把这些端口保留给比较知名的服务器实用,例如:
HTTP服务器,80
HTTPS服务器,443
1 .UDP协议
2.1.1 UDP协议端格式
16位UDP长度,表示整个数据报(UDP首部+UDP数据)的最大长度;
如果校验和出错,就会直接丢弃;
2.1.2 UDP的特点
- 无连接
知道对端的IP和端口号就直接进行传输,不需要建立连接;
- 不可靠
没有任何安全机制,发送端发送数据报以后,如果因为网络故障该段无法发到对方,UDP协议层也不会给应用层返回任何错误信息;
- 面向数据报
应用层交给UDP多长的报文,UDP原样发送,既不会拆分,也不会合并;
用UDP传输100个字节的数据:如果发送端一次发送100个字节,那么接收端也必须一次接收100个字节;而不能循环接收10次, 每次接收10个字节。
- 全双工
UDP的socket既能读,也能写.
- 缓冲区
UDP只有接收缓冲区,没有发送缓冲区:
UDP没有真正意义上的 发送缓冲区 。发送的数据会直接交给内核,由内核将数据传给网络层协议
进行后续的传输动作;
UDP具有接收缓冲区,但是这个接收缓冲区不能保证收到的UDP报的顺序和发送UDP报的顺序一
致;如果缓冲区满了,再到达的UDP数据就会被丢弃;
- 大小受限
UDP协议首部中有一个16位的最大长度。也就是说一个UDP能传输的数据最大长度是64K(包含UDP首部)。
2.1.3 UDP的构成
UDP数据报=报头(重点)+载荷(应用层数据包)
UDP报头中一共有4个字段,每个字段2个字节(一共8个字节)
由于协议报头中使用2个字节表示端口号,端口号的取值范围就是0-65535,这里最大值就是64KB。因此,一个UDP数据报最大长度,就是64KB,无法更长了!一旦整个数据报的长度超出64kb,此时就可能会导致数据出现截断(本来数据是完整的,后面的部分没了)
总的UDP数据报最大长度是64KB,载荷部分实际能承担的最大长度,应该是64KB-8,64KB-8约等于64KB.使用语言表述的时候,会采取约数近似值。
UDP的校验和UDP的校验和的作用是用来验证数据在传输过程中是否正确(前提:数据在网络传输过程中,可能坏掉)
网络数据传输,本质上是光信号/电信号/电磁波,这些信号很可能会受到干扰的。
使用高电平低电平表示0,1。在信号传播的过程中如果外界加上一个磁场就可能把其中低电平变成高点平。此时出现了0->1或者1->0。这种情况被比特翻转,这样就会出现问题。
校验和的作用就是用来识别出当前的数据是否出现了比特翻转(检验是否是正确的数据)
UDP校验和的计算包括UDP数据报文本身以及伪首部(源IP地址、目的IP地址、协议类型、UDP长度)。这个伪首部只在计算校验和时使用,不会在实际传输中发送。
- 计算伪首部校验和: 首先,UDP发送端会计算UDP数据报的校验和,这是通过将UDP数据报的头部和数据内容进行校验和计算得到的。
- 设置校验和字段: 在UDP数据报首部,校验和字段是16位的,如果校验和可选,那么校验和字段就被设为全0。
- 检验: 在UDP接收端,接收端会再次计算收到的UDP数据报的校验和。 如果接收端计算出的校验和与UDP数据报首部中的校验和不一致,那么接收端会丢弃这个数据包。
校验和是拿着原始信息的一部分内容参与计算的。有可能会出现,内容虽然错了,但是算出的校验和还是和之前一致的。但是这种情况的概率很小,可以忽略不计。严格的说,校验和只能用来"证伪"证明数据是出错了,无法确保这个数据100%正确。但是实践中可以近似的认为校验和一致,原来的数据就一致。
UDP中,校验和,使用比较简单的方式,CRC算法(循环冗余校验)来完成校验。
UDP数据报发送方,在发送之前,先计算一遍CRC,把算好的CRC值放到UDP数据报中。(设这个CRC值为vlue1)接下来这个数据包通过网络传输到达接收端。接收端收到这个数据之后,也会按照同样的算法,再算一遍CRC的值,得到的结果是value2.比较自己计算的value2和收到的value1是否一致。
如果是一致的,就说明的数据是ok的。
如果不一致,传输过程中发生了比特翻转。
上述CRC算法中,如果只有一个bite位发生翻转,此时100%能够发现问题.
如果有两个/多个bite位发生翻转,有可能恰好校验和和之前一样!这样的情况概率比较低,可以忽略不计如果希望这里有更高的检查精度,就需要使用其他的更严格的校验和算法了。
2.1.4 基于UDP的应用层协议
- NFS:网络文件系统
- TFTP:简单文件传输协议
- DHCP:动态主机配置协议
- BOOTP:启动协议(用于无盘设备启动)
- DNS:域名解析协议
2.TCP协议
TCP,即Transmission Control Protocol,传输控制协议。人如其名,要对数据的传输进行一个详细的控制。
2.2.1 TCP协议段格式
TCP的报头就要比UDP复杂很多了
- 源/目的端口号:表示数据是从哪个进程来,到哪个进程去;
- 32位序号/32位确认号:后面详细讲;
- 4位TCP报头长度:表示该TCP头部有多少个32位bit(有多少个4字节);所以TCP头部最大长度是15 * 4 = 60
- 6位标志位:
(1)URG:紧急指针是否有效
(2)ACK:确认号是否有效
(3)PSH:提示接收端应用程序立刻从TCP缓冲区把数据读走
(4)RST:对方要求重新建立连接;我们把携带RST标识的称为复位报文段
(5)SYN:请求建立连接;我们把携带SYN标识的称为同步报文段
(6)FIN:通知对方,本端要关闭了,我们称携带FIN标识的为结束报文段- 16位窗口大小:后面再说
- 16位校验和:发送端填充,CRC校验。接收端校验不通过,则认为数据有问题。此处的检验和不光包含TCP首部,也包含TCP数据部分。
- 16位紧急指针:标识哪部分数据是紧急数据;
- 选项:可选的/可有可无的,TCP报头中的前20个字节是固定长度的,后面这里包含了"选项"(optional)部分。(选项部分可以有,也可以没有,可以有一个也可以有多个...)
- 保留(6位):reserved保留位。
UDP这个协议,长度受到2个字节的限制。想要进行扩展,发现扩展不了。一旦你改变了这里的报头长度,就会使机器发送的UDP数据报和其他机器不兼容。无法通信了。
TCP就在设定报头的时候,提前准备了几个保留位。(现在虽然不用,但是也先占个位置)后面一旦需要用了,咱们再把这些保留位给使用起来。后续一旦需要扩展功能,使用保留位就可以实现,就可以避免tcp的扩展引起不兼容的问题。
2.2.2 TCP原理
TCP对数据传输提供的管控机制,主要体现在两个方面:安全和效率。
这些机制和多线程的设计原则类似:保证数据传输安全的前提下,尽可能的提高传输效率。
网络通信过程是复杂的。无法确保发送方发出去的数据,100%能够到达接收方。此处的可靠性,只能"退而求其次",只要尽可能的去进行发送了,发送方能够知道对方是否收到,就认为是可靠传输了。
1. 确认应答特性(可靠传输的核心机制)
在网上聊天通常都是下面这种时序情况:
在这种时序下,不会出现任何问题。但是实际上网络在传输过程中经常会出现"后发先至"的情况
这种情况下B对第一个问题的回答是后发送的,但是先到达了A,这时A就会对B的回答的接收就会产生歧义。会把第二个问题的答案当做是第一个问题的了。
为了解决上述问题,引入了序号和确认序号。对于数据进行编号。应答报文里就告诉发送方说,这次应答的是哪个数据.形容下面的样子:
上面的例子是简化版本的模型,真实的TCP的情况要更复杂一些,TCP是面向字节流的,以字节为单位进行传输的。没有"一条两条"的概念。实际上,TCP的序号和确认序号都是以字节来进行编号的。
(TCP将每个字节的数据都进行了编号。即为序列号)
- TCP的序号
每一个ACK都带有对应的确认序列号,意思是告诉发送者,我已经收到了哪些数据;下一次你从哪里开始发。
由于序号是连续的。只需要在报头中保存第一个字节的序号,即可。后续字节的序号都是很容易计算得到的。
- 应答报文中的确认序号
应答报文中的确认序号,是按照发送过去的最后一个字节的序号再加1来进行设定的
上图中,主机A第一次发生的TCP数据报中,数据报头中的序号就是1。
主机B收到了1-1000这些字节数据之后,反馈一个应答报文。应答报文中的确认序号的值就是1001。(刚才收到的最后一个字节的序号的基础上加一)
1001的含义:
- <1001的数据,主机B都已经收到了。
- 发送方(主机A)接下来要给主机B发1001开始的数据了。
主机A接收到1001这样的确认序号后,下一步开始向主机B发送1001-2000的数据...
TCP的确认应答是确保TCP可靠性的最核心机制
TCP之所以能够保证可靠性,是因为"进行了三次握手------------错误的
确认应答中,通过应答报文来反馈给发送方,当前的数据正确收到了。
应答报文,也叫做ack报文,acknowledge单词的缩写。
6位标志位中的第二位就是ACK
平时这个位是0.,如果当前报文是应答报文,此时报头中的这一位就是1.
2.超时重传特性(安全机制)
超时重传是确认应答的补充。
如果一切顺利,通过应答报文就可以告诉发送方,当前数据是不是成功收到。但是,网络上可能存在"丢包"情况。如果数据包丢了,没有到达对方,对方自然也没有ACK报文了。这个情况下,就需妻超时重传了。
网络上会出现丢包的原因:
通信双方的这两个设备之间并不是一根网线直接连接的,而是由很多通讯设备(路由器/交换机)间接起来的。(形成了错综复杂的网络结构)
这个网络中的路由器/交换机,不仅仅是给你这一次通信提供服务,还要能支持千干万万的主机之间的通信
整个网络中,就可能存在,某个路由器/交换机,某个时刻,突然负载量很高。短时间内可能有大量的数据包要
经过这个设备转发.
但是,一台设备能够处理的数据量是有限的!很可能瞬间的高负载超出了这个设备能转发的数据量的极限.此时多出来的部分,就被设备丢包了。
如果在传输过程中,碰巧某个数据包遇上了上述的情况,此时就会出现丢包。丢包情况客观存在。啥时候丢包,难以预测.
TCP可靠性就是在对抗丢包,期望在丢包客观存在的背景下,也能够尽可能的把包给传过去。超时重传:发送方发了个数据之后,要等待一段时间,等的时间里,收到了ack(数据报在网络上传输,需要时间的)。如果等了好久,ack还没等到,此时发送方就认为数据的传输出现丢包了。当认为丢包之后,就会把刚才的数据包再传输一次。"重传"等待的过程有一个时间的阈值(上线),就是"超时"
不同情况的丢包
上面的过程中,是认为没收到ack就是丢包,其实这样的结论有点小问题。
丢包,不一定是发的数据丢了,还可能是,ack丢了。数据丢了,还是ack丢了,发送方角度看起来,就是区分不了,都是ack没收到。
- 数据丢了
这种情况接收方本身就没收到数据,此时你重传理所应当,没有任何问题。
- ACK丢了
数据已经被B收到了,再传输一次,同一份数据,B就会收到两次。这时如果发的请求是扣款请求,可能就会存在问题。
这样的问题又是怎么解决的呢?
解决ACK丢包引起的问题
TCP socket在内核中存在接收缓冲区(一块内存空间)。发送方发来的数据,是要先放到接收缓冲区中的。然后应用程序调用read/scanner.next才能读到数据。这里的读操作其实是读接收缓冲区。
当数据到达接收缓冲区的时候,接收方首先会先判定一下看当前缓冲区中是否已经有这个数据了,或者这个数据曾经在接收缓冲区中存在过。
如果已经存在或者存在过,就直接把重复发来的数据就丢弃了。这样就能确保应用程序,调用read/scanner.next的时候,不会出现重复数据了。(毕竟当前是靠应用程序来进行"扣款")
那接收方如何判定这个数据是否是"重复数据呢?核心就在于判定依据数据的序号(具体有两种情况:)
数据还在接受缓冲区里,还没被read走
此时,就拿着新收到的数据的序号,和缓冲区中的所有数据的序号对一下,看看有没有一样的。有一样的就是重复了。就可以把新收到的数据丢弃了。
数据在接受缓冲区中,已经被应用程序给read走了,此时新来的数据序号直接无法再接受缓冲区查到。
注意!应用程序读取数据的时候是按照序号的先后顺序,连续读取的!先读1-1000 ->1001-2000 -> 2001-3000
一定是先读序号小的数据后读序号大的数据的(可以把接收缓冲区这个队列想象成带有优先级的阻塞队列)此时socket api中就可以记录上次读的最后一个字节的序号是多少。
比如上次读的最后一个字节的序号是3000,新收到一个数据包的序号是1001。这个1001一定是之前已经读过的了。这个时候同样可以把这个新的数据包判定为"重复的包""直接丢弃了。
接收缓冲区,除了能够帮助我们进行去重之外,还能够进行排序,对收到的数据进行排序.按照序号来排序确保应用程序读到的数据和发送的数据顺序是一致的!!
上述谈到的,ack,重传,保证顺序,自动去重,都是 TCP 内置的.咱们使用 TCP 的api 的时候outputStream.write()只需要调用一个这样的简单代码,上述功能就都自动生效了。如果使用UDP,上述这些问题就都得好好考虑考虑.
超时重传的策略
1.重传次数是有上限的。重传到一定程度,还没有ack,就尝试重置连接,如果重置也失败,就直接放弃连接。
2.重传的超时时间阈值也不是固定不变的,随着重传次数的增加,而增大(重传频率越来越低)
假设,一次网络通信过程中,丢包的概率是10%(这个数字其实已经很大很夸张了,实际使用网络过程中,如果出现这种情况,这就是非常严重的故障)
这时包顺利到达的概率是90%。如果你重传了一次。两次都丢包的概率是10% * 10%=>1%。两次传输包至少有一次能到达的慨率,就是99%.
随着重传次数的增加,包到达对方的概率也会大大增加。如果连续重传三四次还丢包,只能说明当前丢包的概率太大了,远远不止10%了。这个时候意味着网络已经出现非常严重的故障了,再重传也意义不大。
经历了重传之后还丢包,大概率是网络出现严重问题了。再怎么重传也是白费劲。重传还是要重传的,但是可以省点力气,少传两次。
3. TCP的连接管理特性(重点)
TCP的连接管理可以分为建立连接和断开连接两个部分。
3.1 建立连接 ------ 三次握手
TCP是有连接的。客户端执行socket new Socket(serverlp,serverPort);这个操作就是在建立连接!
上述只是调用socket api,真正连接建立的过程,是在操作系统内核完成的。
内核是怎样完成上述的"建立连接"过程的呢?内核建立连接的过程称为"三次握手"。此次处谈到的连接"虚拟的,抽象的"连接。目的是让通信双方都能保存对方的相关信息。
三次握手的流程
- 所谓的SYN(同步报文段)是一个特殊的TCP数据报。表达的语义:我想和你建立连接(抽象)
- 没有载荷。不会携带应用层数据。
- 六个标志位中的第五位,SYN为1
虽然SYN不带有应用层载荷,但是也是会带有IP报头/以太网数据帧帧头...更会有TCP报头。
TCP报头中就包含了客户端自己的端口。IP报头中就包含了客户端自己的IP.
(这个过程,也是客户端在告诉服务器,我是谁)SYN传递到服务器就会出现两种可能:
- 服务器同意了,服务器表示我也愿意和你建立连接
- 服务器没同意。(一般来说这种情况比较少见,就是出现服务器负载极高的情况下,服务器完全无法响应。客户端太多了)
只要服务器有空闲都会得到一个肯定的响应!(服务器本身角色就是要提供服务的,它的职业就是干这个的!)
- 服务器收到syn之后,会返回ack(确认应答)。语义就是:收到
接下来服务器还会再返回SYN。这个SYN意思就是我接收你的连接(我也愿意和你建立连接)- 客户端接收到服务器发来的ACK和SYN后,就会返回一个ACK给服务器
所谓的建立连接过程,本质上就是通信双方各自给对方发起一个SYN,各自给对方回应一个ACK,这里客户端的信息告知服务器这个操作确实在第一次握手的时候就完成了,但是最终确立出这个连接要建立,确立出后续要进行通信,还是得所有的流程都走完。虽然第一次握手,客户端已经把自己的信息告诉服务器了,但是服务器具体是否要确定存储这个信息还是要等到所有的握手环节完成,服务器才会最终确定保存客户端的相关信息。
上述流程上讲,是有四次交互,但是实际过程中,其中的两次交互,能够合二为一(服务器发给客户端的交互)!最终就形成了"三次握手"。SYN就是第五位为1,ACK就是第二位为1。完全可以有一个数据包,第五位和第二位都是1。此时这个数据包就同时起到了两个作用,既能够应答上个请求,也能发起SYN。
这种传输方法也是最推荐使用的,因为网络传输过程中要涉及到多次的封装和分用两个包就封装分用两次。合并成一个包,就可以减少一次封装分用的过程,整体的效率就提升了,成本就降低了。
详细流程:
三次握手的作用
- 三次握手,可以先针对通信路径,进行投石问路,初步的确认一下通信链路是否畅通。(可靠性的前提条件)【关注点在中间过程】
因此,三次握手,对于"可靠传输"这个件事情,是有意义的,确实起到一定作用。但是它的作用比较有限,关键的可靠传输还是通过确认应答以及超时重传来保证的。
毕竟三次握手,只是通信最开始的时候,起到了一定的作用,后面数据开始传输了,就和三次握手无关了。
- 三次握手,也是在验证通信双方,发送能力和接收能力是否正常。【关注点在两端】
握手过程中,确认应答/超时重传,确实存在,但是当握手完成之后,确认应答/超时重传还会继续存在。
三次握手中确实包含了确认应答的报文,但是不能认为"确认应答"机制是"三次握手"机制中的一部分。
- 三次握手的过程中也会协商一些必要的参数
通信是客户端服务器两方的事情。要配合。其中的有些内容要保持一致。
TCP中也是有很多参数要进行协商的。往往是以"选项"部分来体现的
选项最少有0字节,最多40字节(TCP报头总长度最多60,去掉前面固定的20,还剩40)。其中有一个信息是挺关键的,TCP通信的序号,起始值。
TCP一次通信过程中,序号不是从0或者从1开始计算的,而是先选择一个比较大的数字,以这个数字开头来继续计算。即使是同一个客户端和服务器,每次连接,开始的序号都不同。
TCP之所以怎么设定是为了避免下面这种情况:
由于互联网中广泛存在"后发先至"的情况,第一次连接的过程中,传输的有一个数据包,在路上堵车了。迟迟没有到达对端。等到终于到了对端的时候。之前的连接早都没了,现在是新的连接了!
由于数据报是按照IP+端口进行识别的。第一个连接,是用客户端A来连的。第二个连接用客户端B来连的。如果这是恰好是同一个端口的话,(客户端概率是比较低,服务器概率很大)就会出现就会出现混乱或者错误的情况。
具体表现为:
(1) 新的连接可能会接收到之前未完全发送的数据包,这样可能导致解析错误或者逻辑问题。
(2) 对服务器而言,可能会出现难以重现的异常,比如收到重复的请求、请求处理逻辑出现混乱、数据不一致等问题。
此时数据到达这一边,早已变了。这个时候的话,再来进行处理这个数据就不合适了。
此时,这份数据,就应该被丢弃!
如何识别出,当前的数据是上一次连接的数据包呢?可以通过序号来区分!每次建立连接就会协商一个序号,每次的序号都不一样。(序号是不同,也不是随机的,背后还有一系列分配策略)
正常的数据包的序号都是从开始序号往后依次的。就算偶尔丢包,偶尔数据不连续,差异不会很大。上一次连接的包,序号就会和新的连接的包序号差异非常大,很容易一眼识别出来。
3. 2 断开连接 ------ 四次挥手
断开连接的本质目的,就是为了把对端的信息,从数据结构中给删除掉/释放掉。
此处谈到的四次挥手,指的是连接正常这种情况。对于这种单方面断开连接的情况,四次挥手不一定适用就会有其他方式会释放这里的连接。不意味着说,断开连接就一定是四次挥手。
四次挥手,不一定非得是客户端先发FIN.服务器也可能先发FIN,它和三次握手不一样,三次握手,一定是客户端主动。四次挥手中,谁先发都可以都行,具体看代码咋写。
(1)调用socket.close0就会触发FIN(FIN也是内核负责完成)
(2)如果进程直接结束,也会触发FIN
上面两种情况的本质都是关闭socket文件。 (结束进程,也会关闭文件)
四次挥手的流程:
假设客户端先发FIN
- 客户端发送一个FIN(结束报文段)给服务器
- 服务器接收到后返回一个ACK
- 服务器也给客户端发送一个FIN(结束报文段)
- 客户端也返回一个ACK
通信双方各自给对方发起FIN,再各自给对方反馈ACK
四次挥手,能否像三次握手一样,把中间两次交互,合二为一呢?有的时候能合并,有的时候不能合并
不像三次握手,100%会合并。
在三次握手中,握手过程中服务器发送SYN和ACK这俩个数据的触发时机是完全一致的。都是内核再收到SYN之后立即触发。这两个数据报的触发,和应用程序代码无关。
如果实际通信过程中,ACK和第二个FIN时间间隔比较长,此时就无法进行合并了。就分成两次来传输。
如果当前时间间隔很小,是有可能会合并的。TCP还有机制,延时应答和捎带应答。(后面再说)
如果服务器挂了(挂了不一定是出bug才挂了的)确实会触发主动的四次挥手但是更多的情况是,服务器代码中主动调用close来结束连接。
3. 3 四次挥手和三次握手的区别
相似之处:
- 都是通信双方各自给对方发起一个SYN/fin,各自给对方返回ack
- 数据传输的顺序类似。三次握手:syn/ack/syn/ack (中间的两个数据报都是同一个机器发起的)四次挥手:fin/ack/fin/ack(中间的两个数据报都是同一个机器发起的)
不同之处:
- 三次握手中间两次一定能合并。四次挥手则不一定
- 三次握手,必须是客户端主动。而四次挥手,客户端/服务器都可以主动。
4. 连接管理过程中涉及到的TCP状态转换
TCP状态和之前的"线程"状态类似的概念"。状态描述的是某个实体,现在正在干啥。
TCP状态:TCP服务器和客户端都要有一定的数据结构来保存这个连接的信息。在这个数据结构中其中就有一个属性叫做"状态"操作系统内核根据状态的不同,决定了当前应该干什么。
TCP状态转换过程:
服务端状态转化:
[CLOSED -> LISTEN] : 服务器端调用listen后进入LISTEN状态,等待客户端连接;
[LISTEN -> SYN_RCVD] :一旦监听到连接请求(同步报文段),就将该连接放入内核等待队列中,并向客户端发送SYN确认报文。
[SYN_RCVD -> ESTABLISHED] :服务端一旦收到客户端的确认报文,就进入ESTABLISHED状态,可以进行读写数据了。
[ESTABLISHED -> CLOSE_WAIT] :当客户端主动关闭连接(调用close),服务器会收到结束
[CLOSE_WAIT -> LAST_ACK] : 进入CLOSE_WAIT后说明服务器准备关闭连接(需要处理完之前的数据);当服务器真正调用close关闭连接时,会向客户端发送FIN,此时服务器进入LAST_ACK状态,等待最后一个ACK到来(这个ACK是客户端确认收到了FIN)
[LAST_ACK -> CLOSED] : 服务器收到了对FIN的ACK,彻底关闭连接。
客户端状态转化:[CLOSED -> SYN_SENT] 客户端调用connect,发送同步报文段;
[SYN_SENT -> ESTABLISHED] :connect调用成功,则进入ESTABLISHED状态,开始读写数据。
[ESTABLISHED -> FIN_WAIT_1] :客户端主动调用close时,向服务器发送结束报文段,同时进入FIN_WAIT_1;
[FIN_WAIT_1 -> FIN_WAIT_2] 客户端收到服务器对结束报文段的确认,则进入FIN_WAIT_2,
开始等待服务器的结束报文段;[FIN_WAIT_2 -> TIME_WAIT] :客户端收到服务器发来的结束报文段,进入TIME_WAIT,并发
出LAST_ACK;[TIME_WAIT -> CLOSED] :客户端要等待一个2MSL(Max Segment Life,报文最大生存时
间)的时间,才会进入CLOSED状态。
几个比较重要的状态(重点):
-
LISTEN状态,表示服务器这边,创建好serverSocket了,并且绑定端口号完成。(手机开机了,信号良好,可以随时有人给我打电话了)
-
ESTABLISHED已确立的。客户端和服务器连接已经建立完毕(三次握手握完了)(有人给我打电话,我接通了。此时我们就可以给对方说话了。)
可以认为是几乎同时进入的ESTABLISHED,通过肉眼观察的方式是看不出来时间差除非可以精确的查看内核日志的时间戳(ms级别的差别)
-
CLOSE WAIT就表示,接下来代码中需要调用close来主动发起FIN(谁被动断开连接,谁进入CLOSE WAIT)收到对方的FIN之后进入这个状态的。
-
TIME WAIT就是表示本端给对方发起FIN之后,对端也给我发FIN.此时本端进入TIME WAIT。(谁主动断开连接,谁进入TIME WAIT)给最后一个ACK的重传留有一定的时间。
TIME WAIT存在的意义,主要是防止,最后一个ACK丢包。
在四次挥手的过程中,会涉及到确认应答和超时重传。如果没有收到ACK就视为丢包。
服务器如果没有收到最后一个ACK,就会重传FIN
客户端如果在TIME_WAIT这个环节,把TCP连接释放掉此时意味着重传的FIN就无法被返回ACK了。
(保存对端信息的数据结构存在,才能给这个连接提供各种操作,才能返回ACK)
此处的TIME WAIT等待也不是无休止的等待,最多等2MSL(MSL是一个系统内核的配置项,表示客户端到服务器之间,消耗的最长时间。这个时间一般都是拍脑门出来的一个非常大的时间,比如常见的设置值是1min,一般1s就够了)
客户端等一个很长很长的时间,这么长时间你都不重传,意思是就再不会重传了
4. 滑动窗口特性(效率机制)
可靠传输,其实付出了代价。传输效率低了!单位时间,能传输的数据量变少了。
长连接:
确认应答机制之下,每次发送方收到一个ack才会发下一个数据。导致大量的时间都消耗在等待ack上了。此处等待消耗的时间成本是非常多的。因此就希望保证可靠传输的前提下,能够让效率尽量高点,让消耗的时间成本尽可能少点。
滑动窗口提出就是为了解决上述问题的。滑动窗口就可以在保证可靠传输的基础上,提高效率(这里的提高效率其实是降低损失,而不是增加速度)虽然通过这个机制,提高了效率,效率不可能高于UDP这种不需要可靠性的。但是比啥都不做还是有所提升的。
引入滑动窗口后:
此时采取的传输策略是批量传输。之前发了一个数据,等待ACK,再发下一条数据。现在先发一个数据,不等ACK,再发下一个。继续再往下发连续发了一定数据之后,统一等一波ACK。这样就把多次请求的等待时间,使用同一份时间来等了。减少了总的等待时间。
滑动窗口的概念
滑动窗口的这四份数据是已经批量传输出去了。传输出这四份数据之后,就等待ack,暂时先不传了。就把白色的区域(不等待ACK,能够批量传输的数据量)称为"窗口大小"。
批量发了四个数据,就会对应四个ack。此时四个ack也不一定是同时到达,而是有先有后。
什么时候能继续发下一个数据(5001这个数据呢)是等到所有ack回来了,再往后批量发四组数据,还是等回来一个ack就往后发一组呢??如果等到所有ack回来了,再往后批量发四组数据。这样的效率不太理想。所以应该等回来一个ack就往后发一组。
上述的发送/返回ack的过程都很快,窗口往后移动的就很快,直观上就感觉是一个"滑动"效果。所以上述的过程就称为滑动窗口了。
滑动窗口,要保证可靠性是前提。如果出现丢包,滑动窗口会咋祥呢?
滑动窗口出现丢包
- 数据包已经抵达,ACK被丢了。
这种情况,无需进行任何处理!对于可靠性没有影响!!也不需要进行重传。
虽然1001的ACK(1001之前的数据都收到了)丢了,但是2001的ACK成功传给服务器了,而2001的ACK的意思是: 2001之前的数据都收到了(这里涵盖了1001之前的数据也收到了这样的信息)
所以最后一个ACK之前的任意一个ACK丢包都不会对数据的传输产生影响。
如果是2001先到,效果和1001丢包是一样的,发送方仍然会认为1-1000以及1001-2000都是已经到达的,接下来滑动窗口直接往后滑动两个格子。
- 数据包直接丢了
主机B的第二个ACK的内容是解决这个问题的关键要点!此处返回的应答报文,确认序号是1001(意思是1001之前的数据收到了)而不是3001。如果是3001,就是在告诉发送方,1001-2000也收到了。实际上,1001-2000丢包了没收到。
此处明确的告诉了对方,主机B想要的是1001这个数据。不仅是这一条数据,而是接下来若干个数据的确认序号都是1001,反复再向发送方索要1001这个数据。
反复索要,就是在给1001的到达留有等待时间。连续多次索要,发现1001还没到,应该就是丢了,就相当于"超时时间"判定了。
发送方在多次收到索要1001ACK的确认报文之后,认为是1001丢包了,于是就重传了1001。
重传1001之后,此时的确认序号直接就是7001,而不是2001。
确认序号也表示主机B接下来向发送方,索要哪个数据,7000之前的数据都已经收到了。接下来要索要7001这个数据了。
在上述重传的过程中,整体的效率是非常高的。这里的重传做到了"针对性"的重传。哪个丢了就重传哪个。已经收到的数据,是不必重复发送的。整体的效率没有额外损失的,就把这种重传成为"快速重传"。
可以认为,接收方的接收缓冲区里,这些数据就是按照顺序排序好了,从前往后看,缺哪个数据,就通过确认序号要哪个数据!!
主机B可以做到一边索要1001(已经丢包的数据),同时又接收主机A发来的1001后的数据。
这是因为当前TCP有个接收缓冲区(生产者消费者模型)。这边发送的数据到了接收方都是先放到接收缓冲区里排队的。一排队就发现队伍里缺数据,缺谁就喊谁等缺的数据到了,队伍补齐再继续收后面的数据。
小结
确认应答 超时重传,滑动窗口 快速重传。这几个概念并不冲突,而且是同时存在的!!
滑动窗口中当然也有确认应答,只不过,把等待策略稍作调整,转成批量的了。批量的前提是,你短时间发了很多数据。但是如果你发的数据很少,此时滑动窗口滑不起来,退化成了确认应答。如果当前传输过程是按照滑动窗口(短时间传输大量数据)就按照快速重传保证可靠性,此时判定丢包的标准就是看是否连续有多个ACK索要同一个数据。(快速重传)
如果当前传输过程不是按照滑动窗口(没有传很多数据)此时仍然按照之前的超时重传。保证可靠性,此时判定丢包标准就是是否达到超时时间还没有ACK到达。(超时重传)
5. 流量控制机制(安全机制)
上面学习到了通过滑动窗口可以提高传输效率,而窗口大小越大,更多的数据复用同一块时间等待,效率就更高。(批量传多少数据不需要等待ack,此时数据的量就称为"窗口大小")
但是窗口大小是不能无限大的,因为实现TCP的可靠传输是前提。任何提升效率的行为都不应该影响到可靠性。
如果数据发送得太快而接收方处理能力跟不上,就有可能发生数据丢失的情况。这通常发生在接收方的缓冲区已经满了,但发送方仍然继续发送数据的场景中。在这种情况下,由于接收方的缓冲区没有足够的空间来存储新的数据,任何进一步的数据都会被丢弃。
接收方的处理速度不仅取决于其网络层的能力,还取决于应用程序的效率。如果应用程序在处理接收到的数据方面做得不够快(例如,通过调用
read
方法从缓冲区中提取数据的速度慢),那么缓冲区可能会在新数据到达之前就已经满了。这种情况下,新的数据无法被正确接收和处理,导致数据丢失。为了避免这种情况,就需要让发送方提前就感知到接收方快满了,就减慢速度。通过让发送方发送速度和接收方的处理速度,能够保持步调一致。(让接收方反过来影响发送方的速度)这就是流量控制机制,通过这个流量控制机制就能确保数据以接收方能够有效处理的速度发送。
流量控制具体的控制方法是通过TCP中的报头中的对应字段------窗口大小来控制的。
通过这个字段来给发送方反馈发送速度。这个字段在普通报文中无意义,在ACK报文中才有意义。通过这个大小反馈给发送方接下来要发送的窗口设置成多少合适。
接收方就会按照自己接收缓冲区剩余空间的大小,作为ACK中的窗口大小的数值。下一步发送方就会根据这个数值来调整自己的窗口大小。
发送方何时恢复发送呢?发送方会周期性的触发"窗口探测包"它并不携带载荷。这样的包对于业务不产生影响,只是为了触发ACK。一旦查询出来的结果,是已经非0了,说明缓冲区可以存数据了!发送方就继续发送。
实际的窗口大小的设定也需要考虑到窗口扩展因子的应用。在TCP协议的报文头部选项中,还存在一个特定的参数称为窗口扩展因子。这意味着,实际的窗口大小是实际上真实的要设置的窗口大小,是16位窗口大小* 2^ 窗口扩展因子。这样的设计允许TCP协议动态调整窗口大小,以适应不同网络条件下的传输效率需要。
负载因子是根据接收缓冲器的剩余空间的数值来确定的。如果,接收缓冲区特别大-128KB。实际上TCP报头的窗口大小最多表示64KB,此时,就在选项里加上扩展因子,设为1即可。相当于,窗口大小的数值,填成64kb,扩展因子填成1,表示的实际的窗口大小就是128KB了。
6. 拥塞控制特性(安全机制)
拥塞控制与流量控制都旨在调节发送方的数据发送速率,但它们的出发点和目标存在显著差异。流量控制主要是从接收方的角度出发,通过限制发送方的速率来防止接收方的缓冲区溢出,确保接收方有足够的时间和资源来正确处理到达的数据。
拥塞控制,相对而言,关注的是网络中的情况。它旨在防止过多的数据包同时在网络中传输而引起的网络拥塞。网络拥塞发生时,数据包可能会在网络中滞留、丢失或遭遇延迟,降低整个网络的传输效率。拥塞控制机制通过动态调整发送方的数据发送速率来响应网络当前的拥塞程度,以此来优化网络资源的使用,减少数据包延迟和丢失的可能性,从而维护整个网络的稳定性和效率。
假设当前接收方处理速度很快,但是中间的通信路径出现问题了,某个地方的数据"堵车"了发送的速度就算再快,也没用。
针对这种情况,解决问题的核心思路,把中间路径经过的所有设备,视为是一个整体,然后通过"实验"的方式,找到一个比较合适的传输速率。
如果按照某个窗口大小发送数据之后,出现丢包,就视为中间路径存在拥堵,就减小窗口大小。
如果没出现丢包,就视为中间路径不存在拥堵,就增大窗口大小。
按照这种方法就可以逐渐地找到比较合适的传输数据的窗户大小
上述方案,一方面简化了问题,另一方面,也能够很好的适应,当前网络环境的复杂性。因为中间这些节点,啥时候出现拥堵,啥时候不拥堵,就都是"随机"的。此时按照上述策略,就可以让发送速率,动态变化。
拥塞控制的具体过程
- 慢启动。刚开始传输的数据,速率是比较小的。采用的窗口(拥塞窗口)大小也就比较小
因为此时,网络的拥堵情况未知。如果一上来就窗口大小很大,可能就让本来不宽敞的网络带宽,更加拥堵。
- 如果上述传输的数据,没有出现丢包,说明网络还是畅通的。就要增大窗口大小
此时,增大方式是按照指数来增长。
因为开始使用的是慢启动,开始的时候窗口大小非常小。也有可能网络上就是很畅通,通过指数增长可以让上述的窗口大小快速变大。这样就可以保证传输的效率。
- 指数增长,不会一直持续保持的。可能会增长太快,一下就导致网络拥堵,
这里引入了一个指数增长的"阈值",当拥塞窗口达到阈值之后,此时,指数增长就成了线性增长。线性增长能够使当下的窗口持久的保持在一个比较高的速率,并且也不容易一下就造成丢包。
- 线性增长也是一直在增长,积累一段时间之后,传输的速度可能太快,此时还是会引起丢包,于是就回到最初的慢启动过程(又要重新指数增长)。
一旦出现丢包,就把拥塞窗口重置成较小的值。
并且这里也会根据刚才丢包时的窗口大小,重新设置指数增长到线性增长的阈值。
虚线的部分是经典版,拥塞窗口过大后回到非常小的值,重新指数增长,重新线性增长。
全新版本(Reno版本)中,后续就没有指数增长过程了,就只是刚开始的时候,会指数增长。
7. 延时应答特性(效率机制)
延时应答是基于滑动窗口的前提下,尽可能的再提高一点效率。
延时应答结合滑动窗口以及流量控制,能够通过延时应答ACK的方式,把反馈的窗口大小,搞大一点。其核心就在于在允许的范围内,让窗口尽可能的大。
接收方收到数据之后,不会立即返回ACK而是稍等一下。等一会再返回ACK.等了这一会,相当于给接收方的应用程序这里,腾出来更多的时间,来处理这里的数据。
假设发送方不停的发,接收方也不停的再取。如果接收方在接收数据后立即返回ACK,此时ACK报文中显示的缓冲区的大小相对会比较小。(新收到的数据肯定会占用一块空间)
如果不是立即返回,比如延时20 ms,在20 ms之内,接收方的应用程序就能再多消费掉一些数据。缓冲区中剩余的空间就更大了。返回的ACK报文中的窗口大小就是一个相对更大的值了
之前学习了,在滑动窗口下,如果ACK丢了,没啥影响。延时应答就可以根据这个机制,来少传几个ACK从而让接收方的接收缓冲区的数据可以被消费一部分。正常接收方在接收到每个数据的同时都会返回一个ACK,此时就可以每隔几个数据再返回一个ACK了。(每隔几个数据,就能起到延时应答的效果)另外也能减少ACK传输的数量,也能起到节省开销的效果。
延时应答不仅仅是和数据的数量,也是和时间有关的,这个情况如果延时时间达到一定程度了,即使个数没够,也会返回ACK了。一般N(间隔的数据数量)取2,超时时间取200ms。这些数值(参数)都是可以调整的。
8. 捎带应答机制(效率机制)
捎带应答是基于延时应答,引入的机制,能够提升传输效率。
修改窗口大小,确实是提升效率的有效途径,而捎带应答,就是走另一条路,尽可能的把能合并的数据包进行合并,从而起到提高效率的效果。
正常情况下服务器发送的第一个ACK和Response之间有一定的时间间隔,此时的ACK和Response是分开发送的。
引入延时应答后,服务器发送的第一个ACK的应答时间有可能会推迟。
ACK延时的这段时间里,响应数据刚好准备好了。此时就可以把ACK和应答的响应数据合并成一个TCP数据报。
因为本身ack也不携带载荷,只是把报头中的ack标志位设为1,并且设置确认序号以及窗口大小这几个属性,本身一个正常Response报文也用不到。也不会冲突。
注意!很多时候客户端和服务器之间是长连接,是要进行若干次请求的!在捎带应答的加持之下,后续每次传输请求响应,都可能触发捎带应答,都可能把接下来要传输的业务数据和上次的ACK合二为一。
捎带应答也不是一定触发!具体是否能触发,取决于代码咋写的。
取决于下一个数据来的快不快。如果下一个数据来的很快,在延时应答的延时时间之内,就可以触发合并。如果下一个数据来的慢,就无法触发了。
因为延时应答+捎带应答,使后续的四次挥手,可能合并成三次
9. 面向字节流特性
此处重点讨论面向字节流中的一个非常重要的问题,"粘包问题"。此处的包是"TCP的载荷中的应用数据包"。
这里需要结合之前网络编程套接字的内容来进行理解。TCP传输的数据到了接收方之后,接收方要根据socket api来read出来。read出来的结果就是应用层数据包,由于整个read过程非常灵活,可能会使代码中无法区分出当前的数据从哪到哪是一个完整的应用数据包.
此处假定,这三个TCP数据报都是携带的完整的应用层数据包(实际上,一个TCP数据报也可以携 带多个应用层数据包/半个应用层数据包)
应用程序,调用read读取接收缓冲区的数据时,由于此处是字节流的读取,读取过程非常灵活。
因此可以一次读出一个 I
也可以一次读出I li
也可以一次读出I like
还可以一次读出I like B
读出数据之后,应用程序就需要把这里的数据转成应用层数据包(应用程序要做的事情)然后这个数据才能正确被使用。但是由于"粘包",不知道究竟哪种读法,读出来的才是完整的应用层数据包。
这种多个应用层数据包混淆不清了,称为"粘包"。
粘包问题,不是TCP独有的问题,只要你是面向字节流的,都是有同样的问题的。
解决粘包问题的方法
解决问题的关键,就是"明确包之间的边界"
- 通过特殊符号,作为分隔符。见到分隔符,就视为是一个包结束了。
前面网络编程套接字写的echo server,就是使用了分隔符作为边界。(使用空白符+scanner)
这里使用任意字符作为分隔符都可以。需要确保当前这个分隔符不会在正式的数据中存在。
- 指定出包的长度。比如在包开始的位置,加上一个特殊的空间来表示整个数据的长度。
对应的,UDP没有这个问题。UDP传输的基本单位是UDP数据报。在UDP这一层,就已经分开了。只要约定好,每个UDP数据报都只承载一个应用层数据包。就不需要额外的手段来进行区分了。
因为UDP的接收缓冲区就不是一个队列这样的结构,而是类似于链表。每个链表的节点都是一个UDP数据报。通过代码来读取的时候,一次取一个,也就是一个应用层数据包了。
10. TCP异常情况
考虑一种丢包更严重的情况,甚至说网络直接出现故障的情况,这时该如何处理?
- 其中有一方出现了进程崩溃
进程无论是正常结束,还是异常崩溃,都会触发到回收文件资源,关闭文件这样的效果(系统自动完成的)就会触发四次挥手。
TCP连接的生命周期,可以比进程更长一些。虽然进程已经退出了,但是TCP连接还在,仍然可以继续进行四次挥手虽然说是异常崩溃,实际上和正常的四次挥手结束,没啥区别。进程不在了,是通过系统中仍然持有的连接信息,完成后续的挥手过程的。
- 其中有一方出现了关机(按照正常流程关机)
当有个主机,触发关机操作,就会先强制终止所有的进程。(类似于上述的强杀进程)终止进程自然就会触发4次挥手。
点了关机之后,此时,四次挥手不一定能挥完,系统马上就关闭了。
如果挥的快,能够顺利挥完四次挥手,此时,本端和对端都能正确的删除掉保存的连接信息。(四次挥手的核心任务)
如果挥的不快,至少也能把第一个FIN发给对端。这样就能告诉对方,我这边要结束了。对端收到FIN之后,对端也就要进入释放连接的流程了,返回ACK,并且也发FIN. 但是这里发的FIN就不会有ACK了。
对端发送FIN后没有收到ACK,势必要进行重传。(超时重传的流程)当重传几次之后,发现还是没有ACK,这个时候,对端就会单方面的释放连接信息。
正常来说的四次挥手,是确认双方都删除干净了。但是如果四次挥手没这么顺利,没挥完,对端就挂了,没ACK,也就挥不下去了。重试几次也就会直接单方面删除连接信息。
- 其中一方出现了断电(直接拔电源。也是关机,更突然性的关机)
如果直接断电,机器瞬间关机。此时,肯定来不及进行发送FIN。
(1)断电的是接收方
发送方就会突然发现,没有ACK了。就要重传重传几次之后,还是不行TCP就会尝试"复位"连接。(相当于清除原来的TCP中的各种临时数据,重新开始(重新开始并不是重新进行四挥手再重新三次握手 ))这时需要用到一个TCP中的"复位报文段"RST。通过这个报文直接复位。
此时的RST也不会有ACK。如果重置了还不行,就会单方面放弃连接!
( 2 )断电的是发送方
这个情况下,接收方需要区分出,发送方是挂了,还是好着但是暂时没发,TCP中也是如此,接收方一段时间之后,如果没有收到对方的消息,就会触发"心跳包"来询问对方的情况。"心跳包"也是一种不携带应用层数据的特殊数据包,其特点是:1.周期性的 2.没有心跳,视为是对端挂了。 如果对端没心跳了,此时本端也就会尝试复位并且单方面释放连接了。
一般情况下即使主机之间使用TCP通信,仍然会再应用层自己设计"心跳包"。而不是使用TCP的"心跳包",这是因为TCP的心跳机制周期比较长,是s (秒)级甚至分钟级的心跳。
编写分布式系统的程序的时候,服务器之间相互调用,也是需要心跳包来确认其存活状态的。
- 网线断开
这个情况本质上就是3)中的(1) 和(2)的结合了。
这个时间并没有哪个主机挂了,但是中间的通信链路断了,此时通信的双方都会进行对应操作。
发送方就会根据超时重传来进行复位连接后再强制单方面地断开连接(删除连接信息)
接收方则会根据"心跳包"来判断发送方是否正常,然后再强制单方面地断开连接(删除连接信息)
11. TCP小结
为什么TCP这么复杂?因为要保证可靠性,同时又尽可能的提高性能。
可靠性:
- 校验和
- 序列号(按序到达)
- 确认应答(特性)
- 超时重传(特性)
- 连接管理(三次握手,四次挥手)(特性)
- 流量控制 (特性)
- 拥塞控制 (特性)
- 面向字节流。粘包问题(特性)
- 异常情况(特性)
提高性能: - 滑动窗口 (特性)
- 延时应答(特性)
- 捎带应答 (特性)
- 快速重传