1. 初始概述
数据重传
要明白的一点:网络传输本身就是不可靠的;可能丢包或者数据重复.
案例:
客户端发送了3个数据包, 经过传输到达服务器端,但是只收到了3和1的包;
- 数据包2
丢包了
- 发送和接收的次序出现问题, 客户端先发的数据包1,但服务器先收到的是数据包3;
网络中的路由节点较多,难免发生节点故障;数据包发送到网络中,使用了不同的网络链路,前后次序可能不一致.
(1)首先网络丢包
难以避免,那么要保证不丢包就可用重发机制,服务器每次收到数据包,跟客户端确认,回复已收到. 若客户端在约定时间后还没有收到ACK确认编号
,就认为是丢包了,重新发送数据包即可.
(2)但是如果每个数据包都得这样一个一个地确认,从效率上来看不可观.
那么就让客户端在发送数据包的时候,给每个数据包编号
,由小到大排序递增,那么根据编号即可知道是哪个数据包.
比如服务器收到数据包1,2,3之后,回复给客户端
ACK:3
即可表示数据包1,2,3都收到了.
判断重复数据包
由于超过了通信双方约定的时间,客户端还没有收到服务器的确认,客户端就进行重复,但是此时服务器之前的确认收到ACK
已经进入网络,准备在响应了,还没到客户端,此时服务器就可能收到重复数据.
那么就需要数据包判重,个已经收到的逐个比较,显然比较麻烦.
具体做法:采用顺序的ACK
,比如服务器给客户端回复ACK:6,告知客户端所有小于等于6的数据包已经全部收到了,之后要再收到小于等于6的数据包,就说明重复,服务端丢弃该数据即可.
如何保证传输的顺序不乱
若服务器收到数据包1,2,3, 给客户端回复了ACK=3
,然后收到了数据包5,6,7,但是4号数据包不见了,那么服务器就会把数据包5,6,7这三个先暂时存放,直到4号数据包发过来,然后给客户端回复ACK=7
;
可是如果4号数据包一直不来, 那么等待到超时时间后,客户端重新发送数据包4,5,6,7,然后此时服务器收到数据包4之后,就回复ACK=7
,对于数据包5,6,7判断去重丢弃即可.
服务器虽然是并发接收数据包,但数据包的ACK按照编号从小到大进行确认.
到最后的阶段,会通过消息顺序编号+客户端重复机制+服务器顺序ACK号
,实现客户端到服务器的数据发送.
TCP的链接实际是逻辑层面的,当客户端与服务端交互时,其中的数据可能会走不同的网络链路;不过这个逻辑上的链接保证了数据的不重复性和有序性.
链接≈>状态机
- 其中的而每一个TCP链接都采用
4元组
(客户端IP,客户端端口Port,服务器IP,服务器端口Port)来进行唯一确认. TCP的链接作为逻辑层面存在,那么就具有生命周期:创建连接–>传输数据–>关闭连接. - 每个连接作为状态机,客户端/服务端都得维护状态
state
. - (1)创建连接时,客户端/服务器处于关闭状态
CLOSED
.因为连接复用原则
,所以每次都从最初的关闭状态开始. - (2)连接后双方达到通信状态:
ESTABLSHED
. - (3)使用完成后管理连接,通信双方回到关闭状态.
2. 三次握手 四次挥手
三次握手
最开始的时候客户端与服务端都处理关闭的CLOSED
状态;
首先服务器启动进入到监听状态LISTEN
,
客户端启动后发送SYN
同步序列编号与 序列号seq=x
; 进入同步已发送状态;
服务端监听接收到消息后,达到同步已收到状态, 且回复 SYN
同步序列编号,ACK=x+1
确认序列编号与序列号seq=y
,
客户端收到回复后, 为服务端发出 确认序列编号ACK=y+1
,
双方达到连接状态.
- 这里的
ACK = x+ 1
,实际意思是:表示已经确认收到小于等于x
序号的数据包,接下来需要x+1
的数据包.seq=y
表示发送的数据包编号为y
;TCP具有全双工特征
; 这里就是seq = y
和ACK =x+1
一块发送出去了;即发送自己的数据包y,也回复确认自己收到了数据包x.
为什么是三次握手???
C : 服务器:我需要创建连接
S:OK
那么这样的两次握手,可靠吗? 服务器回复的OK,客户端是否可以成功接收到呢?服务器不确定.
而同样换为三次,四次握手,也无法搞定最后一次通信的ACK是否可以成功接收的问题.
这么一分析的话,2次,3次,4次都是这样的,但为什么TCP选择了3次握手
呢?
三次握手就可以保证客户端与服务器对于自身的发送接收能力进行了认可;
- (1)客户端发出数据包
seq=x
,客户端的发送能力已确认; - (2)服务端回复
seq = y
ACK = x+1
,此时客户端确认自身的发送与接收能力, 服务端确认自己的接收能力; - (3)客户端发送
ACK = y+1
,服务器接收后,即可确认自己的发送能力.
且是否成功接收的问题,可以采用超时重传机制与心跳时间来控制.
网络两将军的问题:客户端向服务端发送一个http请求,例如客户端给数据库写入一个记录;超时没有回复,那么无法确定服务器是否成功接收到.
可假设几种不同的原因:
(1) 由于网络问题,服务器就是没有收到请求.
(2)服务器虽然成功收到请求,也处理了,但是回复客户端时网络
出现问题;
(3)服务器虽然成功收到请求,也处理了,但是回复客户端时服务器
出现问题;
四次挥手
一般情况下通信双方中任意一方主动发起关闭请求
, 普遍来说由客户端来主动发起关闭.
(1)首先客户端发出断开连接的请求,携带FIN
终止序列编号与seq=x+2
,以及ACK=y+1
确认序列编号,进入到终止等待1状态FIN_WAIT_1
;
(2)服务器收到消息后,可能服务端还有一些后序的操作,此时发出ACK=x+3
的确认序列编号,先确认收到消息了;进入 关闭等待状态CLOSE_WAIT
;
(3)客户端收到回复到,进入终止等待2状态FIN_WAIT_2
;
(4)服务端此时可以向客户端发出数据,进行自己的操作, 结束后,可以向 客户端发出关闭终止请求;包含终止序列编号FIN
以及seq=y+1
;自己进入最终确认状态LAST_ACK
.
(5)客户端收到请求后,给与回复,包含确认序列编号ACK=y+2
,自己进入超时等待状态TIME_WAIT
.
当然也有特殊的案例,通信双方同时发出关闭请求;双方都处于终止等待1FIN_WAIT_1
状态,此时收到对方的ACK,双方又进入到CLOSING
关闭中状态, 之后一起到达超时等待状态TIME_WAIT
,经过一段时间后进入到关闭状态.
为什么客户端在最后一次确认之后,要进入超时等待状态TIME_WAIT
,而不是直接close关闭呢?
- 当客户端与服务端都进入到CLOSE状态,可能还有某些数据包在网络上走动,
- 注意:
一个连接是由4元组唯一标识[客户端IP, 客户端Port,服务端IP,服务端Port]
, 这个连接关闭之后再重开,这就应该是一个新连接,可是4元组无法区分新连接和旧连接
, - 这样导致:之前走动的数据包就可能跑到这个新的连接上;
所以在TCP的四次握手时,客户端最后一次发完确认消息后就会进入到TIME_WAIT
状态,避免旧的数据包跑到新连接上面.
任何一个Ip的数据包在网络上存活的时间为 MSL(Maximum Segment LifeTime)
默认为120S.;也就是说如果超出了这个时间,中间的路由节点就会把这个数据丢弃.
而TIME_WAIT
状态会持续2倍的MSL 时间,足够防止其他数据包串到通信双方的连接处.
为什么设定为2倍的时间
由于网络2将军问题,第四次发送的数据包,服务器不确定是否可以收到,那么如果服务器没有收到,就需要向客户端重新发送第三次数据包,然后客户端收到之后就明白服务器没收到第四次的数据包,最终客户端就会重新发出第四个的数据包.
分析之后,那么 客户端发送第四次数据包和服务器重新发送第三次数据包时,两次的最长时间为2MSL,所以设定为2倍的MSL等待时间.
客户端有TIME_WAIT状态,为何服务器不设定呢?
一方面考虑到服务端的利用问题,服务端本来收到最后一次挥手确认后就关闭了,要是还等2MSL时间,可能会让服务器耗费时间在这里.
方面二: 每个连接都是一个4元组,同时关联客户端与服务端. 当客户端处于TIME_WAIT
状态之后,这个连接就需要2倍的MSL时间后才可以重新启用.
如何防止大量的连接处于TIME_WAIT超时等待状态
如果频繁地创建出连接,就可能导致大量连接处于超时等待状态, 消耗过多的连接资源;
(1)避免服务器主动关闭连接, 那么服务端就不会处于超时等待状态;
(2)在客户端进行连接使用时,可以复用连接,不要频繁创建关闭,消耗资源.
3.TCP的粘包与分包 , 数据分段
Socket编程:
粘包案例:
- 客户端write写操作两次, 产生2个数据包;假设
"abc"
,"def"
- 服务端read读一次,可能读到"abcdef" 或许是"abcd",后面的"ef"在下次被读取到.
分包案例:
- 客户端write写操作1次,产生1个数据包,假设为
"abcdef"
- 服务端read读操作2次, 读到2个数据包, 即"ab" , “cdef”.
由于TCP是一个面向流的协议,并不管理应用层的数据包之间的分界.
- 可能发送一次, N次接收才能完成;
- 可能发送N次,一次就给接收完毕;
如何解决粘包/分包 , 具体还是需要逻辑业务完成;
方式(1)可以在每个数据包前,加一个 长度为4字节的字段.
案例:
- 发送方:数据包1,2,3 .大小不固定,变长. 每次发送数据包时,在头部先加长度为4字节的字段.
- 接收方: 每次接受数据时,先固定读4个字节,解析后得到数据包1的长度,然后再读取,当读取的数据达到这个长度时,就说明数据包1已经接收完成. 重复后面的操作…
方式(2):在每个数据包的前后部分,加一个固定长度的特殊标记. 在接收时根据前后的标志确定当前的数据包是否读取完成; 但是如果两个数据包以及特殊标记一样了,就无法区分了.
数据分段问题MSS
在理想情况下,数据经过一次写操作即可进入TCP,封装IP,然后传到以太网.
但如果要发送的业务数据,超出以太网报文最大值MTU[Maximum Transmission Unit]
, 通常为1500字节.
比如数据传到TCP时,TCP头占用20字节,在包装到IP层时,IP头又占用20字节,层层叠加,最后到以太网时就可能
超出最大报文值
.
- 那么就得
在IP层对数据进行分包
: 在IP层中,若数据包大小已经超出了MTU,那么就需要做IP分片,将一个数据包分为多个数据片. - 怎么避免大量的IP分片产生呢? 在TCP进行连接时,通信双方就需要协商
MSS值[TCP最大报文段值]
. - 或者直接计算MSS = MTU - tcp头部20字节 - IP头部20字节 = 1460字节.
需要注意的一点是,即使预先协商了可行的MSS[TCP最大报文段值],也可能会出现IP数据分片;
由于这个IP层可通信不同结构的局域网,有可能某个局域网的MTU小于1500呢