传输层及其协议(TCP、UDP)实现总览

本文记录传输层及其协议的实现与细节。
本文记录TCP协议的发送与接收,探讨TCP的粘包、讨论基于TCP协议设计应用层协议并尝试解释目前应用协议对TCP粘包的解决。
本文记录UDP协议的发送与接收。(待更新)
本文较长,请耐心进行翻阅。

传输层功能

传输层为应用层提供通信服务,他属于面向通信的最高层;从用户角度来看,他是用户功能的最底层。
image.png
传输层的功能:

  1. 传输层为不同主机的进程之间提供逻辑通信,网络层为不同主机之间提供逻辑通信。

image.png

应用进程之间的通信被称为端到端的逻辑通信

  1. 传输层进行复用和分用。(网络层也有这方面的功能)复用指的是发送方不同的应用进程都能使用同一个传输层协议发送数据(在TCP中,这与流式传输有一定的联系),分用指的是接收方的传输层能够将数据正确交付给目的应用进程。(这与TCP头部格式设计有关)
  2. 差错检测。传输层提供可靠的传输服务,因此需要对数据(首部+传输数据)进行差错检测,并通过机制来保证数据的正确接收。(网络层提供不可靠的连接,其只检测IP数据报的头部)

IP数据报每经过一个路由器都要重新计算校验和,为了提高传输效率,IP首部中的首部校验和字段只检测首部是否出现差错而不检查数据部分。传输层TCP和UDP的校验和既要校验首部也要校验数据部分,并且只在发送端进行一次校验和计算,在接受端进行一次校验
在通俗意义上的解释,IP协议携带的数据部分即为TCP报文,因此其可靠性由TCP负责。IP协议只解决在通信子网中两个主机之间的逻辑通信,因此只计算首部校验和。

  1. 提供两种不同的传输协议,面向连接的TCP协议和无连接的UDP协议。(网络层只能在面向连接和无连接二选一而不能同时实现两种协议)

TCP协议由于其协议特性(下文提出),因此相当于一条可靠的全双工通路,UDP协议由于其是无连接的,因此其通信依旧是不可靠信道。

面向连接和无连接

传输层提供面向连接和无连接两类不同的协议。
面向连接要求在数据通信时,必须要建立连接之后才能发送数据,其建立连接的证据保存在协议的首部,这条连接一直被实时的监控和管理。通信结束后,应当释放这个连接。(一对套接字socket之间只能建立一条连接)
TCP协议由于其面向连接的特性,需要为此增加很多功能,比如确认、流量控制、计时器和连接管理等等,这使得TCP协议头部过于“臃肿”。
UDP协议是无连接的,它仅在IP协议基础上增加对数据的校验和多路复用等传输层必须要实现的功能,他不需要确认机制,执行速度快。UDP的主要应用方面为DNS、SNMP和RTP等等。

UDP可实现组播功能,但在目前国内的RTMP和HLS等协议是基于TCP进行的。
google使用QUIC实现了对UDP的可靠性保证。

总结

应当明确,面向连接与可靠性从不是传输层必须的属性和功能,传输层只提供端到端的逻辑通信,传输层的功能由如上所述概括表示。传输层提供不同的协议选择满足用户对不同功能的需求,因此产生面向连接的TCP协议和无连接的UDP协议。

TCP协议概述

TCP协议,传输控制协议,是一种面向连接的、可靠的、基于字节流的传输层通信协议,它能够进行端到端的流量控制。
文档定义:RFC 793

https://datatracker.ietf.org/doc/html/rfc793

TCP通信需要经过创建连接、数据传送、终止连接三个步骤。
TCP 通信模型中,在通信开始之前,一定要先建立相关连接,才能发送数据。
TCP解决传输过程中的可靠、有序、无丢失和不重复问题
TCP是一个全双工的通信,在逻辑层面通信双方可以进行同时的数据发送。

虽然在逻辑上TCP可以进行全双工通信,但在下层通信子网中,比如数据链路层,同一时间线路上只能存在一种电信号。所以也就不会存在两种电信号同时存在的场景了,即有线传输物理层面上没有双工一说,要么发送数据,要么接收数据。
TCP的全双工是逻辑层面上,通信两端随时都可以发送和接收数据,不需要像 HTTP/1.1 那样采用请求应答模式。

面向连接

TCP提供面向连接的服务,TCP的实现方式主要是三次握手和四次挥手。
在TCP连接的建立过程中,要解决

  1. 双方能够相互确认对方的存在
  2. 双方对连接的参数的协商(最大窗口值、时间戳选项等等)
  3. 对运输实体资源进行分配

TCP使用C/S模式进行连接
image.png
在三次握手的第三次传输中,已经可以进行数据的携带以及对第二次握手的捎带确认。
image.png

为什么要进行三次握手进行连接?

三次握手的目的,是为了防止A端已经失效的连接请求突然又传到B端,被误认为是A端再次发出的一个新的连接请求,如果B端这时又再次向A发出确认报文,表示同意建立连接,就会产生错误。
第一次是A端向B端发送请求,如果是只有一次握手的话,A端不知道B端是不是收到了这个请求。
第二次是B端确认收到A端请求,如果只有两次的话,B端不确定A端是否收到了确认消息,这个确认消息有可能会在半路丢了。
第三次是A端确认收到了B的确认消息,A和B双方都是通的,然后AB就可以建立连接相互通信了。
通俗来说,TCP通信是双向的,双方都应该知道对方持有并保存了TCP连接。

为什么要进行四次挥手释放连接?

我们假设三次挥手的情况。我们假设客户端向服务器发送了断开请求,服务器在收到断开请求后也向客户端发送断开请求(FIN=1,ACK=1,seq=w,ack=u+1),客户端收到此消息后向服务器发送断开连接(ACK=1,seq=u+1,ack=w+1)。
显然,这种方式没有考虑到服务器是否有数据发送给客户端,只考虑了单向关闭的情况,所以为了保证服务器端正常传输完数据,服务器端在收到客户端发送的断开请求后先发送一个ACK(ACK=1,seq=v,ack=u+1)给客户端,当服务器端数据传输完后发送断开请求(FIN=1,ACK=1,seq=w,ack=u+1)。

客户端最后等待了2MSL的时间才关闭连接,为什么不直接关闭呢?

如果客户端直接关闭连接,而此时客户端最后发送的ACK又在网络中丢失,从而可能导致服务器端的连接无法正常关闭。
那为什么又要设置为2MSL呢?
1MSL表示一个IP数据报在网络中的最多存活时间。
假设客户端最后发送的ACK经过将近1MSL快要到达服务器端的时候丢失了,那么服务器端在规定的时间内未收到最后客户端发送的ACK,则服务器端重新发送最后的FIN给客户端,请求客户端重发ACK,该FIN经过1MSL到达客户端。
所以如上最坏情况,如果客户端在2MSL内没有收到FIN请求,则表明服务器端已经断开连接(客户端主观认为)。

对比TTL、MSL和RTT

MSL

MSL(Maximum Segment Lifetime):报文段最大生存时间,表示任何报文在网络上存在的最长时间,超过这个时间,报文将被丢弃;反之,就要接收和承认报文。
RFC 973指定 MSL 为 2 分钟,然而,在实现中的常用值是 30s,1分钟 和 2分钟。

Maximum Segment Lifetime, the time a TCP segment can exist in the internetwork system. Arbitrarily defined to be 2 minutes.
最大报文段生存时间TCP报文段在网络系统中存在的时间。任意定义为2分钟。

在 2MSL 的时间内,该地址上的连接不能被使用(客户端的地址、端口和服务器端的地址、端口)。比如在建立一个连接后关闭连接然后迅速重启连接,那么就会出现端口不可用的情况。
这是一个主观的时间记录方式,取决于主机双方。

TTL

网络中路由器数量巨大,很难做到时间同步。因此,在实际工程实现中,TTL 并不是直接保存存活时间,而是保存 IP 包失效前可以经过的路由跳数。IP 包每经过一跳路由,TTL 都会减一;当 TTL 减到 0 ,路由便将它丢弃。
有了 TTL 机制后,就算 IP 包陷入路由环路,循环若干次后,最终将自行消失,不会造成更大的影响。
RFC 791定义了TTL。

This field indicates the maximum time the datagram is allowed to remain in the internet system. If this field contains the value zero, then the datagram must be destroyed. This field is modified in internet header processing. The time is measured in units of seconds, but since every module that processes a datagram must decrease the TTL by at least one even if it process the datagram in less than a second, the TTL must be thought of only as an upper bound on the time a datagram may exist. The intention is to cause undeliverable datagrams to be discarded, and to bound the maximum datagram lifetime.
该字段指示数据报允许在互联网系统中保留的最长时间。如果该字段包含值零,则必须销毁数据报。该字段在互联网标头处理中被修改。时间以秒为单位进行测量,但由于每个处理数据报的模块都必须将 TTL 至少减少 1,即使它在不到一秒的时间内处理数据报,因此 TTL 必须仅被视为数据报可能存在的时间。 目的是导致无法传送的数据报被丢弃,并限制最大数据报生存期

RTT

RTT(Round-Trip Time),往返时延。表示从发送端发送数据开始,到发送端收到来自接收端的确认(接收端收到数据后便立即发送确认),总共经历的时延。
往返延时(RTT)由三个部分决定:即链路的传播时间、末端系统的处理时间以及路由器的缓存中的排队和处理时间。其中,前面两个部分的值作为一个TCP连接相对固定,路由器的缓存中的排队和处理时间会随着整个网络拥塞程度的变化而变化。所以RTT的变化在一定程度上反映了网络拥塞程度的变化。

有些人可能疑惑MSL和TTL是否一致的问题,他们看起来都像是超时丢弃。
我们来看RFC6864 3.2中的定义

Network delays are incurred in other ways, e.g., satellite links, which can add seconds of delay even though the Time to Live (TTL) is not decremented by a corresponding amount. There is thus no enforcement mechanism to ensure that datagrams older than 120
seconds are discarded.
网络延迟是由其他方式引起的,例如卫星链路,即使生存时间(TTL)没有减少相应的量,也会增加数秒的延迟。因此,没有强制机制来确保丢弃超过 120 秒的数据报。

再看RFC1122 3.3.2对此的表述

DISCUSSION: The IP specification says that the reassembly timeout should be the remaining TTL from the IP header, but this does not work well because gateways generally treat TTL as a simple hop count rather than an elapsed time. If the reassembly timeout is too small, datagrams will be discarded unnecessarily, and communication may fail. The timeout needs to be at least as large as the typical maximum delay across the Internet. A realistic minimum reassembly timeout would be 60 seconds.
It has been suggested that a cache might be kept of round-trip times measured by transport protocols for various destinations, and that these values might be used to dynamically determine a reasonable reassembly timeout value. Further investigation of this approach is required.
If the reassembly timeout is set too high, buffer resources in the receiving host will be tied up too long, and the MSL (Maximum Segment Lifetime) [TCP:1] will be larger than necessary. The MSL controls the maximum rate at which fragmented datagrams can be sent using distinct values of the 16-bit Ident field; a larger MSL lowers the maximum rate. The TCP specification [TCP:1] arbitrarily assumes a value of 2 minutes for MSL. This sets an upper limit on a reasonable reassembly timeout value.
讨论:IP 规范规定重组超时应该是 IP 标头中剩余的 TTL,但这效果不佳,因为网关通常将 TTL 视为简单的跳数而不是经过的时间。如果重组超时太小,数据报将被不必要地丢弃,并且通信可能会失败。超时至少需要与 Internet 上的典型最大延迟一样大。实际的最小重组超时为 60 秒。
有人建议,缓存可以保存由传输协议测量的各个目的地的往返时间,并且这些值可以用于动态确定合理的重组超时值。需要对这种方法进行进一步研究。
如果重组超时设置得太高,接收主机中的缓冲区资源将占用太长的时间,并且 MSL(最大段寿命)[TCP:1] 将大于必要的值。MSL 使用 16 位 Ident 字段的不同值控制分段数据报发送的最大速率;较大的 MSL 会降低最大速率。TCP 规范 [TCP:1] 任意假定 MSL 值为 2 分钟。这设置了合理的重组超时值的上限。

虽然这两者有联系,但绝不能对此划等号。

同时打开和同时关闭

同时打开

image.png
TCP是特意设计为了可以处理同时打开,对于同时打开它仅建立一条连接而不是两条连接。
出现同时打开的情况时,两端几乎在同时发送SYN,并进入 SYN_SENT状态。当每一端收到SYN时,状态变为SYN_RCVD,同时它们都再发SYN并对收到的SYN进行确认。当双方都收到SYN及相应的ACK时,状态都变迁为ESTABLISHED。
同时打开时间,TCP的两端一共进行了四次握手,比正常的三次握手多一个,这是十分罕见的情况,但TCP的设计允许这么做。

同时关闭

image.png
同时关闭也属于TCP允许的情况。
当应用层发出关闭命令时,两端均从ESTABLISHED变为FIN_WAIT_1。
这将导致双方各发送一个 FIN,两个FIN经过网络传送后分别到达另一端。收到FIN后,状态由FIN_WAIT_1变迁到 CLOSING,并发送最后的ACK。当收到最后的 ACK时,状态变化为TIME_WAIT。

CLOSING 状态是一个新状态,之前我们没有遇到过,只在 TCP 状态机图里看到过。CLOSING 状态是由于同时关闭导致的

SYN攻击问题(对三次握手的攻击)

image.png
在三次握手过程中,服务器发送 SYN ACK 之后,收到客户端的 ACK 之前的 TCP 连接称为半连接(half-open connect)(第二次之后,第三次之前)。此时服务器处于 SYN_RCVD 状态。当收到 ACK 后,服务器才能转入 ESTABLISHED 状态。
SYN 攻击指的是,攻击客户端在短时间内伪造大量不存在的IP地址,向服务器不断地发送SYN包,服务器回复确认包,并等待客户的确认。
由于源地址是不存在的,服务器需要不断的重发直至超时,这些伪造的SYN包将长时间占用未连接队列,正常的SYN请求被丢弃,导致目标系统运行缓慢,严重者会引起网络堵塞甚至系统瘫痪。

TCP KeepAlive 以及 TCP长连接的讨论

TCP KeepAlive 的基本原理是,隔一段时间给连接对端发送一个探测包,如果收到对方回应的 ACK,则认为连接还是存活的,在超过一定重试次数之后还是没有收到对方的回应,则丢弃该 TCP 连接。
利用keepAlive可以实现TCP全双工信道的长期保持。
在linux中,TCP KeepAlive机制主要涉及3个参数:

  1. 在TCP保活打开的情况下,最后一次数据交换到TCP发送第一个保活探测包的间隔,即允许的持续空闲时长,或者说每次正常发送心跳的周期,默认值为7200s(2h)。
  2. 在tcp_keepalive_time之后,最大允许发送保活探测包的次数,到达此次数后直接放弃尝试,并关闭连接,默认值为9(次)。
    3.在tcp_keepalive_time之后,没有接收到对方确认,继续发送保活探测包的发送频率,默认值为75s。
    这是TCP协议的保活机制,与HTTP/2的保活要区别开来。
HTTP的KeepAlive和TCP的KeepAlive的关系
  • HTTP的KeepAlive和TCP的KeepAlive是两个完全不同的概念
  • TCP的KeepAlive是由操作系统内核来控制,通过 keep-alive 报文来防止TCP连接被对端、防火墙或其他中间设备意外中断,和上层应用没有任何关系,只负责维护单个TCP连接的状态,其上层应用可以复用该TCP长连接,也可以关闭该TCP长连接
  • HTTP的KeepAlive机制则是和自己的业务密切相关的,浏览器通过头部告知服务器要复用这个TCP连接,请不要随意关闭。只有到了 keepalive 头部规定的 timeout 才会关闭该TCP连接,不过这具体依赖应用服务器,应用服务器也可以根据自己的设置在响应后主动关闭这个TCP连接,只要在响应的时候携带 Connection: Close 告知对方
  • 所以很多时候我们可以把HTTP连接理解为TCP连接,但HTTP KeepAlive则不能当成TCP的KeepAlive看待
  • 假设我们不开启TCP长连接而只开启HTTP长连接,是不是HTTP的KeepAlive就不起作用了?并不是的,此时HTTP的KeepAlive还会正常起作用,TCP连接还会被复用,但被复用的TCP连接出现故障的概率就高很多。由于没有开启TCP的KeepAlive,防火墙或负载转发服务等中间设备可能因为该TCP空闲太长而悄悄关闭该连接,当HTTP从自己的连接池拿出该TCP连接时,可能并不知道该连接被关闭,继续使用就会出现错误
  • 为了减少错误,一般来说开启HTTP的KeepAlive的应用都会开启TCP的KeepAlive
NAT与TCP长连接的关系

NAT协议通常工作在网络层(第三层)和传输层(第四层)之间。
按照RFC的定义属于网络层。
NAT机制与TCP的长连接其实是有关系的,总结就是,TCP的心跳/保活机制的间隔时间应当小于NAT释放空闲映射的时间。
运营商分配给手机终端的 IP 是运营商内网的 IP,手机要连接 Internet,就需要通过运营商的网关做一个网络地址转换(Network Address Translation,NAT)。简单的说运营商的网关需要维护一个外网 IP、端口到内网 IP、端口的对应关系,以确保内网的手机可以跟 Internet 的服务器通讯。
大部分移动无线网络运营商都在链路一段时间没有数据通讯时,会淘汰 NAT 表中的对应项,造成链路中断。

TCP传输的可靠性

TCP使用了校验、序号、确认和重传等机制来实现可靠性。
TCP与UDP的校验属于传输层的必要特性,因此不做过多赘述。

序号

序号(TCP首部字段)建立在TCP字节流之上,准确来说就是从本次发送数据的第一个字节的编号。

确认

TCP首部的确认字段是期望收到的对方的下一个报文段的数据的第一个字节的序号。TCP默认使用积累确认,TCP只确认数据流中至第一个丢失为止的字节。
即使是乱序传输,TCP也只会确认第一个丢失的字节,不管后续是否传输完成。

重传机制

有两种事件会导致TCP对报文段的重传:超时和冗余ACK

超时

如果客户端在RTT时间周期内未收到服务器端的确认号,则引发超时重传。因此TCP协议中需要计时器。
理论情况下,408教材中,我们的做法是,TCP每发送一个报文段,就对这个报文段设置一次计时器,计时器设置的重传时间到期但还未收到确认时,就要重传这一报文段。
但在工程实践中,TCP的报文段过多,给每个报文段设置超时时间不太现实。
在实际中采取给每个TCP连接生成一个计时器。
设计原则可以参考如下所述:

  1. 发送TCP分段时,如果没有开启重传定时器,则开启;
  2. 发送TCP分段时,如果有重传定时器开启,则不再开启;
  3. 收到一个非冗余的ACK时,如果有数据在传输,重新启动重传定时器;
  4. 收到一个非冗余的ACK时,如果没有数据在传输,则关闭重传定时器;
  5. 如果连续收到3个冗余ACK时,则不用等到重传定时器超时,直接重传。
冗余ACK

发送方可以通过冗余ACK机制较好的检测丢包状态,以A方发送,B方接收报文为例。观察其中第N-1个报文到达B方后,B方的答复。B收到第N-1报文后,就会答复1个ACK号为N的ACK报文,此时如果收不到序列号为N的报文,则会继续发送第2个ACK号为N的答复报文,此为重复ACK。如果一直收不到对方序列号为N的报文,则会出现三次冗余的ACK,则意味着很大可能出现丢包了,这时A立即对2号报文重传,这被称之为快速重传。
快重传
快速重传机制只解决了一个问题,就是超时时间的问题,但是它依然面临着另外一个问题。就是重传的时候,是重传一个,还是重传所有的问题。
假设在一串连续的报文段中有多个超时现象,接收方只会回复第一个缺失的报文段,这就导致了发送方无法确认是那是选择重传一个报文,还是重传之后已发送的所有报文。

  • 如果只选择重传一个报文,那么重传的效率很低。因为对于丢失的其他报文,还得在后续收到三个重复的 ACK3 才能触发重传。
  • 如果选择重传之后已发送的所有报文,虽然能同时重传已丢失的所有的报文,但是后续的某些报文是已经被接收过了,对于重传部分数据相当于做了一次无用功,浪费资源。

还有一种实现重传机制的方式叫:SACK( Selective Acknowledgment), 选择性确认
这种方式需要在 TCP 头部「选项」字段里加一个 SACK 的东西,它可以将已收到的数据的信息发送给「发送方」,这样发送方就可以知道哪些数据收到了,哪些数据没收到,知道了这些信息,就可以只重传丢失的数据
如果要支持 SACK,必须双方都要支持。在 Linux 下,可以通过 net.ipv4.tcp_sack 参数打开这个功能(Linux 2.4 后默认打开)。

TCP流量控制

image.png
从图中可以看出,B进行了三次流量控制。第一次把窗口减少到 rwnd = 300 ,第二次又减到了 rwnd = 100 ,最后减到 rwnd = 0 ,即不允许发送方再发送数据了。这种使发送方暂停发送的状态将持续到主机B重新发出一个新的窗口值为止。B向A发送的三个报文段都设置了 ACK = 1 ,只有在ACK=1时确认号字段才有意义。
TCP使用窗口机制来实现流量控制的。原理是:在通信过程中,接收方根据自己接收缓存的大小,动态调整发送方的发送窗口大小,即TCP报文段首部中的“窗口”字段rwnd,来限制发送方向网络注入报文的速率。同时,发送方根据其对当前网络拥塞程序的估计确定一个拥塞窗口cwnd,最终A发送的窗口的实际大小是min(rwnd,cwnd)值。
TCP为每一个连接设有一个持续计时器(persistence timer)。只要TCP连接的一方收到对方的零窗口通知,就启动持续计时器。若持续计时器设置的时间到期,就发送一个零窗口控测报文段(携1字节的数据),那么收到这个报文段的一方就重新设置持续计时器。

TCP拥塞控制

拥塞:即对资源的需求超过了可用的资源。若网络中许多资源同时供应不足,网络的性能就要明显变坏,整个网络的吞吐量随之负荷的增大而下降。
拥塞控制:防止过多的数据注入到网络中,这样可以使网络中的路由器或链路不致过载。拥塞控制所要做的都有一个前提:网络能够承受现有的网络负荷。拥塞控制是一个全局性的过程,涉及到所有的主机、路由器,以及与降低网络传输性能有关的所有因素。

几种拥塞控制的方法

慢开始( slow-start )、拥塞避免( congestion avoidance )、快重传( fast retransmit )和快恢复( fast recovery )。

慢开始和拥塞避免

发送方维持一个拥塞窗口 cwnd ( congestion window )的状态变量。拥塞窗口的大小取决于网络的拥塞程度,并且动态地在变化。发送方让自己的发送窗口等于拥塞。
发送方控制拥塞窗口的原则是:只要网络没有出现拥塞,拥塞窗口就再增大一些,以便把更多的分组发送出去。但只要网络出现拥塞,拥塞窗口就减小一些,以减少注入到网络中的分组数。
慢开始算法:当主机开始发送数据时,如果立即所大量数据字节注入到网络,那么就有可能引起网络拥塞,因为现在并不清楚网络的负荷情况。因此,较好的方法是先探测一下,即由小到大逐渐增大发送窗口,也就是说,由小到大逐渐增大拥塞窗口数值。通常在刚刚开始发送报文段时,先把拥塞窗口 cwnd 设置为一个最大报文段MSS的数值。而在每收到一个对新的报文段的确认后,把拥塞窗口增加至多一个MSS的数值。用这样的方法逐步增大发送方的拥塞窗口 cwnd ,可以使分组注入到网络的速率更加合理。

MSS(Maximum Segment Size,最大报文长度),是TCP协议定义的一个选项,MSS选项用于在TCP连接建立时,收发双方协商通信时每一个报文段所能承载的最大数据长度


每经过一个传输轮次,拥塞窗口 cwnd 就加倍。一个传输轮次所经历的时间其实就是往返时间RTT。不过“传输轮次”更加强调:把拥塞窗口cwnd所允许发送的报文段都连续发送出去,并收到了对已发送的最后一个字节的确认。
为了防止拥塞窗口cwnd增长过大引起网络拥塞,还需要设置一个慢开始门限ssthresh状态变量(如何设置ssthresh)。慢开始门限ssthresh的用法如下:
当 cwnd < ssthresh 时,使用上述的慢开始算法。
当 cwnd > ssthresh 时,停止使用慢开始算法而改用拥塞避免算法。
当 cwnd = ssthresh 时,既可使用慢开始算法,也可使用拥塞控制避免算法。
拥塞避免算法:线性的将cwnd依次相加。
image.png
初始化时,cwnd=1。慢开始门限的初始值设置为16个报文段,即 cwnd = 16 。
在执行慢开始算法时,拥塞窗口 cwnd 的初始值为1。以后发送方每收到一个对新报文段的确认ACK,就把拥塞窗口值另1,然后开始下一轮的传输(图中横坐标为传输轮次)。因此拥塞窗口cwnd随着传输轮次按指数规律增长。当拥塞窗口cwnd增长到慢开始门限值ssthresh时(即当cwnd=16时),就改为执行拥塞控制算法,拥塞窗口按线性规律增长。
假定拥塞窗口的数值增长到24时,网络出现超时。更新后的ssthresh值变为12(即变为出现超时时的拥塞窗口数值24的一半),拥塞窗口再重新设置为1,并执行慢开始算法。当cwnd=ssthresh=12时改为执行拥塞避免算法,拥塞窗口按线性规律增长,每经过一个往返时间增加一个MSS的大小。
“拥塞避免”并非指完全能够避免了拥塞。利用以上的措施要完全避免网络拥塞还是不可能的。“拥塞避免”是说在拥塞避免阶段将拥塞窗口控制为按线性规律增长,使网络比较不容易出现拥塞。

快重传和快恢复

快重传算法首先要求接收方每收到一个失序的报文段后就立即发出重复确认而不要等到自己发送数据时才进行捎带确认。

与快重传配合使用的还有快恢复算法。
当发送方连续收到三个重复确认,就执行“乘法减小”算法,把慢开始门限ssthresh减半。这是为了预防网络发生拥塞。请注意:接下去不执行慢开始算法。
由于发送方现在认为网络很可能没有发生拥塞,因此与慢开始不同之处是现在不执行慢开始算法(即拥塞窗口cwnd现在不设置为1),而是把cwnd值设置为慢开始门限ssthresh减半后的数值,然后开始执行拥塞避免算法(“加法增大”),使拥塞窗口缓慢地线性增大。

cwnd与rwnd

接收方根据自己的接收能力设定了接收窗口rwnd,并把这个窗口值写入TCP首部中的窗口字段,传送给发送方。因此,接收窗口又称为通知窗口。因此,从接收方对发送方的流量控制的角度考虑,发送方的发送窗口一定不能超过对方给出的接收窗口rwnd 。
发送方窗口的上限值 = Min [ rwnd, cwnd ]
当rwnd < cwnd 时,是接收方的接收能力限制发送方窗口的最大值。
当cwnd < rwnd 时,则是网络的拥塞限制发送方窗口的最大值。

TCP报文段设计



TCP之报文首部格式 - Jummyer - 博客园

TCP的计时器及其作用

TCP中的四个计时器包括重传计时器、坚持计时器、保活计时器、时间等待计时器。
重传计时器在上述报文重传时提到。
坚持计时器在上述TCP流量控制时提到。
保活计时器在上述KeepAlive时提到。
时间等待计时器在上述四次挥手时提到。

重传计时器(Retransmission Timer):

  • 为了控制丢失的报文段或者丢弃的报文段。这段时间为对报文段的等待确认时间。
  • 创建时间:在TCP发送报文段时,会创建对次特定报文段的重传计时器。
  • 可能发生的两种情况:在截止时间(通常为60秒)到之前,已经收到了对此特定报文段的确认,则撤销计时器;当计时器超时后,TCP会重传发送队列最前端的报文段,并且将计时器复位。
  • 当一个或者多个报文段被累计确认后,这个或者这些报文段会被清除出队列。
  • 重传时间:2*RTT

坚持计时器(Persistent Timer):

  • 主要解决零窗口大小通知可能导致的死锁问题
  • 死锁问题的产生:当接收端的窗口大小为0时,接收端向发送端发送一个零窗口报文段,发送端即停止向对端发送数据。此后,如果接收端缓存区有空间则会重新给发送端发送一个窗口大小,即窗口更新。但接收端发送的这个确认报文段有可能会丢失,而此时接收端不知道已经丢失并认为自己已经发送成功,则一直处于等待数据的状态;而发送端由于没有收到该确认报文段,就会一直等待对方发来新的窗口大小,这样一来,双方都处在等待对方的状态,这样就形成了一种死锁问题。如果没有应对措施,这种局面是不会被打破的。为了解决这种问题,TCP为每一个连接设置了坚持计时器。
  • 工作原理:当发送端TCP收到接收端发来的零窗口通知时,就会启动坚持计时器。当计时器的期限到达时,发送端就会主动发送一个特殊的报文段告诉对方确认已经丢失,必须重新发送。【这个特殊的报文段就称为探测报文段,探测报文段只有1个字节的大小,里边只有一个序号,该序号不需要被确认,甚至在计算其他部分数据的确认时该序号会被忽略。】
  • 截止期的设置:设置为重传时间的值。但如果没有收到接收端的响应,则会发送另一个探测报文段,并将计时器的值加倍并复位,直到大于门限值(一般为60秒)。在此之后,发送端会每隔60秒发送一个探测报文段,直到窗口重新打开。

保活计时器(Keeplive Timer):

  • 主要是为了防止两个TCP连接出现长时间的空闲。当客户端与服务器端建立TCP连接后,很长时间内客户端都没有向服务器端发送数据,此时很有可能是客户端出现故障,而服务器端会一直处于等待状态。
  • 工作原理:每当服务器端收到客户端的数据时,都将保活计时器重新设置(通常设置为2小时)。过了2小时后,服务器端如果没有收到客户端的数据,会发送探测报文段给客户端,并且每隔75秒发送一个,当连续发送10次以后,仍没有收到对端的来信,则服务器端认为客户端出现故障,并会终止连接。

时间等待计时器(Time_Wait Timer):

  • 时间等待计时器是在连接终止期间使用的。
  • 当TCP关闭连接时并不是立即关闭的,在等待期间,连接还处于过渡状态。这样就可以使重复的FIN报文段在到达终点之后被丢弃。
  • 时间设置:一般为报文段寿命期望值的2倍。

分包和粘包

TCP是流式的,接收大的数据包时,该数据包可能被拆分成多份,多次发送,底层可能会合并一次性发送。这就会导致:
分包:收到的多个数据包,需要拆分。
合包:收到数据包只是一部分,需要缓存数据,合并成原包。
它其实是TCP的 Nagle 算法优化,目的是为了避免发送小的数据包。
在 Nagle 算法开启的状态下,数据包在以下两个情况会被发送:

  • 如果包长度达到MSS(或含有Fin包),立刻发送,否则等待下一个包到来;如果下一包到来后两个包的总长度超过MSS的话,就会进行拆分发送;
  • 等待超时(一般为200ms),第一个包没到MSS长度,但是又迟迟等不到第二个包的到来,则立即发送。

粘包出现的根本原因是不确定消息的边界。接收端在面对"无边无际"的二进制流的时候,根本不知道收了多少 01 才算一个消息。一不小心拿多了就说是粘包。其实粘包根本不是 TCP 的问题,是使用者对于 TCP 的理解有误导致的一个问题。
只要在发送端每次发送消息的时候给消息带上识别消息边界的信息,接收端就可以根据这些信息识别出消息的边界,从而区分出每个消息
在实际的应用中,至少在JAVA课程中,我们学习过使用java实现一个C/S的聊天室系统。在数据的发送中,我们使用流的形式进行发送,这样仅仅只能发送数据,更进一步的,我们想要发送具有特定数据结构的更为复杂的信息,就需要我们基于TCP来进行应用层协议的设计,这样程序才能够对流式数据进行边界的确认和解析。
所以,TCP通信时需要制定通信协议。例如,HTTP、HTTPS、FTP、SMTP、POP3、IMAP、SSH等。
一个常用的方式是采用EOF协议分包:每个数据包的结尾加上特殊字符表示包结束,如 Memcache、FTP、SMTP 都使用 \r\n 作为结束符。发送数据时,则需要在末尾加上\r\n即可。但也这样意味着数据包中间不能有同样的EOF字符。这样我们就能通过换行符作为新的数据的开始,实现了一个简单的应用层协议。

由衷的感谢

  1. https://networkengineering.stackexchange.com/questions/68468/is-tcps-msl-value-equivalent-to-ips-ttl-value
  2. https://github.com/steveLauwh/TCP-IP/blob/master/TCP/Open%20and%20Closed%20at%20the%20same%20time.md
  3. https://www.cnblogs.com/zhanglei93/p/6574714.html
  4. https://cloud.tencent.com/developer/article/1873188
  5. https://hit-alibaba.github.io/interview/basic/network/TCP.html
  6. https://fasionchan.com/network/ip/ttl/
  7. https://www.jianshu.com/p/aea9ae5c8a3c
  8. https://blog.csdn.net/clearLB/article/details/106166339
  9. https://www.cnblogs.com/sjjg/p/5830009.html
  10. https://github.com/cuijiji/article-collection/blob/master/TCP%E5%BA%8F%E5%88%97%E5%8F%B7%E5%9B%9E%E7%BB%95%E4%B8%8E%E8%A7%A3%E5%86%B3.md
  11. https://www.zhihu.com/question/21789252
  12. https://juanha.github.io/2018/05/05/tcp/
  13. https://www.cnblogs.com/549294286/p/3861391.html
  14. https://blog.csdn.net/hyman_yx/article/details/52086389
  15. https://blog.csdn.net/qq_33951180/article/details/60468267
  16. https://www.cnblogs.com/Jummyer/p/11026966.html
  17. https://blog.csdn.net/thegoodboy1234/article/details/120812341
  18. https://segmentfault.com/a/1190000039691657
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值