计算机网络总结(二)——TCP相关知识
2.TCP相关知识
2.1 TCP短连接和长连接
1.短连接:
Client 向 Server 发送消息,Server 回应 Client,然后一次读写就完成了,这时候双方任何一个都可以发起 close 操作,不过一般都是 Client 先发起 close 操作。短连接一般只会在 Client/Server 间传递一次读写操作。
短连接的优点:管理起来比较简单,建立存在的连接都是有用的连接,不需要额外的控制手段。
2.长连接:
Client 与 Server 完成一次读写之后,它们之间的连接并不会主动关闭,后续的读写操作会继续使用这个连接。
在长连接的应用场景下,Client 端一般不会主动关闭它们之间的连接,Client 与 Server 之间的连接如果一直不关闭的话,随着客户端连接越来越多,Server 压力也越来越大,这时候 Server 端需要采取一些策略,如关闭一些长时间没有读写事件发生的连接,这样可以避免一些恶意连接导致 Server 端服务受损;如果条件再允许可以以客户端为颗粒度,限制每个客户端的最大长连接数,从而避免某个客户端连累后端的服务。
长连接和短连接的产生在于 Client 和 Server 采取的关闭策略,具体的应用场景采用具体的策略。
2.2 TCP重传、滑动窗口、流量控制、拥塞控制
1. 重传机制
TCP实现可靠传输的方式之一,是通过序列号与确认应答。但是在错综复杂的网络环境中,很有可能出现数据包丢失的情况,如果数据包丢失了,TCP会用重传机制解决。
(1)超时重传
TCP在发送数据时,会设置一个定时器,如果一个已经发送的报文段在指定时间内没有收到确认应答报文,那么就重传这个报文段。一个报文段从发送再到接收到确认所经过的时间称为往返时间 RTT,而超时时间称为 RTO,RTO的值应略大于RTT,如图所示:
(2)快速重传
TCP还有一种快速重传机制,它不以时间为驱动,而是以数据驱动重传。
如果发送端连着收到了三个ACK标志位相同的确认,则发送端知道了对应的数据包接收端未收到,就会在定时器过期之前,重传丢失的数据包。
(3)SACK
如果不知道该重传哪些TCP报文,可以用SACK方法,这种方式需要在TCP头部【选项】字段里增加“SACK”,将缓冲区的数据发送给对方,这样发送方就知道哪些数据收到了,哪些没收到。这时,就可以只重传丢失的数据。
2. 滑动窗口
TCP因为每发送一个数据,都要进行一次确认应答,当上一个数据包收到应答了,再发送下一个。但是这样效率非常低,所以TCP引入窗口的概念,窗口是缓存的一部分,用来暂时存放字节流。
窗口可以设置大小,窗口大小就是无需等待确认应答,而可以继续发送数据的最大值。发送方和接收方各有一个窗口,接收方通过 TCP 报文段中的窗口字段告诉发送方自己的窗口大小,发送方根据这个值和其它信息设置自己的窗口大小。发送方发送的数据大小不能超过接收方的窗口大小。假设窗口大小为3个TCP段,那么发送方就可以连续发送3个TCP段,并且如果中途有ACK丢失,可以通过下一个确认应答进行确认。如图所示:
发送方和接收方的窗口示例如下图所示,其中发送方窗口分为4个部分,接收方窗口分为3个部分:
如果发送窗口左部的字节已经发送并且收到了确认,那么就将发送方窗口向右滑动一定距离,直到左部第一个字节不是已发送并且已确认的状态,接收窗口的滑动类似,接收窗口左部字节已经发送确认并交付主机,就向右滑动接收窗口。
上图中,假设发送窗口中的31~33字节的ACK确认应答后,如果发送窗口大小未发生变化,则发送窗口向右滑动3个字节,与之对应的可用窗口也向右滑动3个字节,那么后续可以发送41~43这3个字节的数据。类似的,接收窗口收到数据后,也向右滑动3个字节。
3. 流量控制
如果接收方处理不过来数据,而发送方又一直发送数据的话,就会触发重传机制,从而导致网络流量的浪费。为了解决这种问题,TCP可以进行流量控制,让发送方根据接收方的实际接收能力控制发送的数据量,避免发送方的数据填满接收方的缓存。发送端主机会时不时的发送一个叫做窗口探测的数据段,此数据段仅包含一个字节来获取最新的窗口大小信息。即TCP通过让接收方指明希望从发送方接收的数据大小(窗口大小)来进行流量控制。
发送窗口和接收窗口中所存放的字段,都是放在操作系统内存缓冲区的,而操作系统的缓冲区,会被操作系统调整。如果服务非常繁忙,操作系统可能会减少缓冲区的大小,如果此时应用程序没有读取数据,将收到的数据留在缓冲区中,会收缩窗口,就会出现丢包的现象。例如:发送数据大小超过了接收窗的大小,就会丢包。所以TCP规定不允许同时减少缓冲区又收缩窗口,只能先收缩窗口,过段时间再减少缓存。
如果接收窗口大小为0,就会阻止发送方给接收方传递数据,知道窗口变为0位置,这就是窗口关闭。
4. 拥塞控制
计算机网络时常会出现拥堵现象,这时如果发送方发送大量数据包,就可能会导致数据包时延,丢失等。这时TCP就会重传数据,从而加重网络负担,形成恶性循环。所以当网络发生拥塞时,TCP会进行拥塞控制。
拥塞控制是发送方维护的一个的状态变量,它会根据网络的拥塞程度动态变化。这一点和流量控制很像,但是出发点不同。流量控制是为了让接收方能来得及接收,而拥塞控制是为了降低整个网络的拥塞程度。
引入拥塞控制后,发送窗口的值是 swnd = min(cwnd, rwnd),也就是拥塞窗口和接受窗口的最小值。只要网络中没出现拥塞,cwnd 就会增大,但网络中出现了拥塞,cwnd就会减少。一般来说,如果发送方发送超时重传,就会认为网络中出现了拥塞。
TCP 主要通过四个算法来进行拥塞控制:
慢开始、拥塞避免、快速重传、快速恢复。
(1)慢开始与拥塞避免
TCP在刚建立连接完成后,首先是有个慢开始的过程,一点一点的提高发送数据包的数量。当发送方每收到一个ACK,拥塞窗口cwnd的大小就会加1。例如,开始时,令 cwnd = 1,发送方只能发送 1 个报文段;当收到确认后,将 cwnd 加倍,因此之后发送方能够发送的报文段数量为:2、4、8 ... 。
这里设置一个慢开始门限 ssthresh,当 cwnd >= ssthresh 时,进入拥塞避免,当发送方每收到一个ACK,拥塞窗口cwnd的大小就会加1/cwnd,即每个轮次只将 cwnd 加 1。如果出现了超时,则令 ssthresh = cwnd / 2,然后重新执行慢开始。
(2)快速重传和快速恢复
拥塞发生时,会用到前面讨论过的重传机制中的方法,即超时重传和快速重传。在接收方,要求每次接收到报文段都应该对最后一个已收到的有序报文段进行确认。例如已经接收到 M1 和 M2,此时收到 M4,应当发送对 M2 的确认。在发送方,如果收到三个重复确认,那么可以知道下一个报文段丢失,此时可能会发生超时重传,重新进入慢开始,但是这样会突然减少数据流,反应强烈容易造成网络卡顿,更多的采用快速重传的方式,立即重传下一个报文段。例如收到三个 M2,则 M3 丢失,立即重传 M3。
在快速重传后,只是丢失个别报文段,而不是网络拥塞。因此执行快速恢复,令 ssthresh = cwnd / 2 (也就是设置为原来的一半),cwnd = ssthresh,重传丢失的数据包。如果收到新的ACK,说明ACK确认了新的数据,恢复过程已经结束,可以进入到之前的状态,也就是拥塞避免状态。如图所示:
注意慢开始和快速恢复的快慢指的是 cwnd 的设定值。拥塞控制的整个过程如图所示(图中第2步为超时重传,第3步为快速重传):
2.3 TCP粘包、拆包问题及解决办法
(1)TCP粘包、拆包的概念
TCP有粘包和拆包问题,而UDP没有,UDP 是基于报文发送的,UDP首部采用了 16bit 来指示 UDP 数据报文的长度,因此在应用层能很好的将不同的数据报文区分开,从而避免粘包和拆包的问题。
TCP 是基于字节流的,虽然应用层和 TCP 传输层之间的数据交互是大小不等的数据块,但是 TCP 并没有把这些数据块区分边界,仅仅是一连串没有结构的字节流;另外从 TCP 的帧结构也可以看出,在 TCP 的首部没有表示数据长度的字段,基于上面两点,在使用 TCP 传输数据时,才有粘包或者拆包现象发生的可能。
假设 Client 向 Server 连续发送了两个数据包,用 packet1 和 packet2 来表示,那么服务端收到的数据可以分为三种情况,现列举如下:
1)第一种情况,接收端正常收到两个数据包,即没有发生拆包和粘包的现象。
2)第二种情况,接收端只收到一个数据包,但是这一个数据包中包含了发送端发送的两个数据包的信息,这种现象即为粘包。这种情况由于接收端不知道这两个数据包的界限,所以对于接收端来说很难处理。
3)第三种情况,接收端收到了两个数据包,但是这两个数据包要么是不完整的,要么就是多出来一块,这种情况即发生了拆包和粘包。这两种情况如果不加特殊处理,对于接收端同样是不好处理的。
(2)TCP 粘包、拆包发生原因
-
要发送的数据大于 TCP 发送缓冲区剩余空间大小,将会发生拆包。
-
待发送数据大于 MSS(最大报文长度),TCP 在传输前将进行拆包。
-
要发送的数据小于 TCP 发送缓冲区的大小,TCP 将多次写入缓冲区的数据一次发送出去,将会发生粘包。
-
接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。
(3)TCP粘包、拆包解决办法
由于 TCP 本身是面向字节流的,无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决,根据业界的主流协议的解决方案,归纳如下:
-
消息定长:发送端将每个数据包封装为固定长度(不够的可以通过补 0 填充),这样接收端每次接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。
-
设置消息边界:服务端从网络流中按消息边界分离出消息内容。在包尾增加回车换行符进行分割,例如 FTP 协议。
-
将消息分为消息头和消息体:消息头中包含表示消息总长度(或者消息体长度)的字段。
-
更复杂的应用层协议比如 Netty 中实现的一些协议都对粘包、拆包做了很好的处理。
参考:
- 《TCP-IP网络编程》 韩-尹圣雨
-
-