网络协议TCP

 

145919yh0x5j58qohs22sa.png

在第二层上的数据,我们叫Frame,在第三层上的数据叫Packet(数据包),第四层的数据叫Segment(报文段)。

我们程序的数据首先会打到TCP的Segment中,然后TCP的Segment会打到IP的Packet中,然后再打到以太网Ethernet的Frame中,传到对端后,各个层解析自己的协议,然后把数据交给更高层的协议处理。

 

网络协议:

1.物理层:

2. 链路层(ARP在第二层——Data Link层):
也称作数据链路层或者网络接口层,通常包括操作系统中的设备驱动程序和计算机中对应的网络接口卡。它们一起处理与电缆(或其他任何传输媒介)的物理接口细节,负责将网络层交下来的 IP 数据报封装成帧,并在链路的两个相邻节点间传送帧,每一帧都包含数据和必要的控制信息(如同步信息、地址信息、差错控制等)。

3. 网络层(internet layer层):
也称作互联网层,处理分组在网络中的活动,例如分组的选路。网络层协议包括IP协议(网际协议)、ICMP协议(Internet互联网控制报文协议),以及IGMP协议(Internet组管理协议),它负责为两台主机提供通信服务,并通过选择合适的路由将数据传递到目标主机。

4. 运输层主要为两台主机上的应用程序提供端到端的通信(传输层Transport):
在TCP/IP协议族中,有两个互不相同的传输协议:TCP(传输控制协议)和UDP(用户数据报协议)。TCP为两台主机提供高可靠性的数据通信。他所作的工作包括把应用程序交给它的数据分成合适的小块交给下面的网络层,确认接收到的分组,设置发送最后确认分组的超时时钟等。由于运输层提供了高可靠性的端到端通信,因此应用层可以忽略所有这些细节。而另一方面,UDP则为应用层提供一种非常简单的服务。它只是把称作数据报的分组从一台主机发送到另一台主机,但并不保证该数据报能到达另一端。任何必须的可靠性必须由应用层来提供。

5. 应用层负责处理特定的应用程序细节
包括Telnet(远程登录)、FTP(文件传输协议)、SMTP(简单邮件传送协议)以及SNMP(简单网络管理协议)等。

 

wireshark抓到的包与对应的协议层如下图所示:

ç论èç³»å®éï¼WiresharkæååæTCP 3次æ¡æã4次æ¥æè¿ç¨_2.jpg

1. Frame:   物理层的数据帧概况
2. Ethernet II: 数据链路层以太网帧头部信息
3. Internet Protocol Version 4: 互联网层IP包头部信息
4. Transmission Control Protocol:  传输层的数据段头部信息,此处是TCP
5. Hypertext Transfer Protocol:  应用层的信息,此处是HTTP协议。
ç论èç³»å®éï¼WiresharkæååæTCP 3次æ¡æã4次æ¥æè¿ç¨_4.jpg

 

传输层 (transport layer):有时也译为运输层,它负责为两台主机中的进程提供通信服务。该层主要有以下两种协议:

  • 用户数据报协议 (User Datagram Protocol,UDP):提供无连接的、尽最大努力的数据传输服务,但不保证数据传输的可靠性,数据传输的基本单位是用户数据报。

 

  • 传输控制协议 (Transmission Control Protocol,TCP):提供面向连接的、可靠的数据传输服务,数据传输的基本单位是报文段(segment);

TCP:

  • Sequence Number:是包的序号,用来解决网络包乱序(reordering)问题。
  • Acknowledgement Number:就是ACK——用于确认收到,用来解决不丢包的问题。
  • Window:又叫Advertised-Window,也就是著名的滑动窗口(Sliding Window),用于解决流控的。
  • TCP Flag :也就是包的类型,主要是用于操控TCP的状态机的。

(1)序号:Seq序号,占32位,用来标识从TCP源端向目的端发送的字节流,发起方发送数据时对此进行标记。
(2)确认序号:Ack序号,占32位,只有ACK标志位为1时,确认序号字段才有效,Ack=Seq+1。

       (A)URG:紧急指针(urgent pointer)有效。
       (B)ACK:确认序号有效。
       (C)PSH:接收方应该尽快将这个报文交给应用层。
       (D)RST:重置连接。
       (E)SYN:发起一个新连接。
       (F)FIN:释放一个连接。

 

各个字段含义:https://www.lagou.com/lgeduarticle/93429.html

需要注意:

  • TCP的包是没有IP地址的,那是IP层上的事,但是有源端口和目标端口。
  • 一个TCP连接需要四个元组来表示是同一个连接(src_ip, src_port, dst_ip, dst_port)准确说是五元组,还有一个是协议。但因为这里只是说TCP协议,所以,这里说四元组。

 

三次握手机制:

  1. 服务器进程 B 首先创建传输控制模块 TCB,然后进入 LISTEN(收听)状态,准备接受客户端的连接请求;

  2. 客户端进程 A 首先创建传输控制模块 TCB,然后发出连接请求报文段,此时同步位 SYN = 1 ,同时选择一个初始序号    seq = x ,之后进入 SYN-SENT(同步已发送)状态;

  3. B 收到连接请求报文段后,如果同意建立连接,则发送确认报文段,此时 SYN 和 ACK 都置为 1,确认号 ack = x + 1 ,并为自己选择一个初始序号 seq =y ,之后进入 SYN-RCVD(同步收到)状态;

  4. A 收到来自 B 的确认后,发出最后的确认,确认报文段的 ACK 为 1,确认号 ack = y + 1,序号 seq = x + 1。TCP 标准规定,ACK 报文段可以携带数据也可以不携带,如果不携带则该序号不被消耗,下一个数据报文段的序号仍然是 seq = x + 1。之后 A 进入 ESTABLISHED(已连接)状态;

  5. 当 B 收到 A 的确认后,也进入 ESTABLISHED 状态。

  • 对于建链接的3次握手:主要是要初始化Sequence Number 的初始值。通信的双方要互相通知对方自己的初始化的Sequence Number(缩写为ISN:Inital Sequence Number)——所以叫SYN,全称Synchronize Sequence Numbers。也就上图中的 x 和 y。这个号要作为以后的数据通信的序号,以保证应用层接收到的数据不会因为网络上的传输的问题而乱序(TCP会用这个序号来拼接数据)。

 

四次挥手:

  1. 假设应用进程 A 先主动关闭连接,此时需要发送连接释放报文段:首部终止控制位 FIN 为 1,序号 seq = u,其中 u 等于前面传送过的数据的最后一个字节的序号加 1 。之后 A 进入 FIN-WAIT-1(终止等待 1)状态;

  2. 应用进程 B 收到连接释放报文段后立即发出确认,确认号 ack = u + 1,序号 seq = v ,其中 v 等于前面传送过的数据的最后一个字节的序号加 1 。之后 B 进入 CLOSE-WAIT(关闭等待)状态,并通知高层应用进程。此时 TCP 连接处于半关闭状态,即 A 已经没有数据需要发送,但如果 B 发送数据,A 仍要接收;

  3. A 收到来自 B 的确认后就进入 FIN-WAIT-2(终止等待 2)状态,等待 B 发出连接释放报文段;

  4. 若高层应用进程已经没有数据要发送,则通知 B 释放 TCP 连接。此时 B 发出释放连接报文段:首部终止控制位 FIN 为 1,序号 seq = w(在半关闭状态下 B 可能又发送了一些数据),另外还需要重复上次已经发送过的确认号 ack = u + 1。之后 B 进入 LAST-ACK(最后确认)状态;

  5. A 收到 B 的连接释放报文段后,发出最后确认:ACK 为 1,确认号 ack = w + 1,序号 seq = u + 1,然后进入 TIME-WAIT(有时间限制的等待)状态;

  6. B 收到来自 A 的最后确认后进入 CLOSED(关闭)状态;

  7. A 经过 2 倍的 MSL(Maximum Segment Lifetime,报文最大生存时间)后,才进入 CLOSED 状态。

RFC 793 建议 MSL 设置为 2 分钟,现在的网络环境已经有了质的提升,该值可以按需缩短。A 之所以要等待两倍的 MSL 时间后才进入 CLOSED 状态,主要基于以下两个原因:

  • 为了保证 A 发送的最后一个 ACK 报文段能够到达 B。如果 B 没有收到该最后确认,则会进行超时重发 FIN+ACK 报文段,A 在 2MSL 等待时间内会响应该报文段并重发最后确认;
  • 确保本次连接内产生的所有报文段都从网络消失,进而确保下一个新的连接中不会出现旧的连接请求报文段。

对于4次挥手:其实你仔细看是2次,因为TCP是全双工的,所以,发送方和接收方都需要Fin和Ack。只不过,有一方是被动的,所以看上去就成了所谓的4次挥手。如果两边同时断连接,那就会就进入到CLOSING状态,然后到达TIME_WAIT状态。

有几个事情需要注意一下:

  • 关于建连接时SYN超时:
    试想一下,如果server端接到了clien发的SYN后回了SYN-ACK后client掉线了,server端没有收到client回来的ACK,那么,这个连接处于一个中间状态,即没成功,也没失败。于是,server端如果在一定时间内没有收到的TCP会重发SYN-ACK。在Linux下,默认重试次数为5次,重试的间隔时间从1s开始每次都翻售,5次的重试时间间隔为1s, 2s, 4s, 8s, 16s,总共31s,第5次发出后还要等32s都知道第5次也超时了,所以,总共需要 1s + 2s + 4s+ 8s+ 16s + 32s = 2^6 -1 = 63s,TCP才会把断开这个连接。
  • 关于SYN Flood攻击:
    一些恶意的人就为此制造了SYN Flood攻击——给服务器发了一个SYN后,就下线了,于是服务器需要默认等63s才会断开连接,这样,攻击者就可以把服务器的syn连接的队列耗尽,让正常的连接请求不能处理。于是,Linux下给了一个叫tcp_syncookies的参数来应对这个事——当SYN队列满了后,TCP会通过源地址端口、目标地址端口和时间戳打造出一个特别的Sequence Number发回去(又叫cookie),如果是攻击者则不会有响应,如果是正常连接,则会把这个 SYN Cookie发回来,然后服务端可以通过cookie建连接(即使你不在SYN队列中)。请注意,请先千万别用tcp_syncookies来处理正常的大负载的连接的情况。因为,synccookies是妥协版的TCP协议,并不严谨。对于正常的请求,你应该调整三个TCP参数可供你选择,第一个是:tcp_synack_retries 可以用他来减少重试次数;第二个是:tcp_max_syn_backlog,可以增大SYN连接数;第三个是:tcp_abort_on_overflow 处理不过来干脆就直接拒绝连接了。
  • 补充:
  • 在三次握手过程中,Server发送SYN-ACK之后,收到Client的ACK之前的TCP连接称为半连接(half-open connect),此时Server处于SYN_RCVD状态,当收到ACK后,Server转入ESTABLISHED状态。SYN攻击就是Client在短时间内伪造大量不存在的IP地址,并向Server不断地发送SYN包,Server回复确认包,并等待Client的确认,由于源地址是不存在的,因此,Server需要不断重发直至超时,这些伪造的SYN包将产时间占用未连接队列,导致正常的SYN请求因为队列满而被丢弃,从而引起网络堵塞甚至系统瘫痪。SYN攻击时一种典型的DDOS攻击,检测SYN攻击的方式非常简单,即当Server上有大量半连接状态且源IP地址是随机的,则可以断定遭到SYN攻击了,使用如下命令可以让之现行: #netstat -nap | grep SYN_RECV
  • 关于ISN的初始化:
    ISN是不能hard code的,不然会出问题的——比如:如果连接建好后始终用1来做ISN,如果client发了30个segment过去,但是网络断了,于是 client重连,又用了1做ISN,但是之前连接的那些包到了,于是就被当成了新连接的包,此时,client的Sequence Number 可能是3,而Server端认为client端的这个号是30了。全乱了。RFC793中说,ISN会和一个假的时钟绑在一起,这个时钟会在每4微秒对ISN做加一操作,直到超过2^32,又从0开始。这样,一个ISN的周期大约是4.55个小时。因为,我们假设我们的TCP Segment在网络上的存活时间不会超过Maximum Segment Lifetime(缩写为MSL – Wikipedia语条),所以,只要MSL的值小于4.55小时,那么,我们就不会重用到ISN。
  • 关于 MSL 和 TIME_WAIT:
    通过上面的ISN的描述,相信你也知道MSL是怎么来的了。我们注意到,在TCP的状态图中,从TIME_WAIT状态到CLOSED状态,有一个超时设置,这个超时设置是 2*MSL(RFC793定义了MSL为2分钟,Linux设置成了30s)为什么要这有TIME_WAIT?为什么不直接给转成CLOSED状态呢?主要有两个原因:1)TIME_WAIT确保有足够的时间让对端收到了ACK,如果被动关闭的那方没有收到Ack,就会触发被动端重发Fin,一来一去正好2个MSL,2)有足够的时间让这个连接不会跟后面的连接混在一起(你要知道,有些自做主张的路由器会缓存IP数据包,如果连接被重用了,那么这些延迟收到的包就有可能会跟新连接混在一起)。你可以看看这篇文章《TIME_WAIT and its design implications for protocols and scalable client server systems
  • 关于TIME_WAIT数量太多:
    从上面的描述我们可以知道,TIME_WAIT是个很重要的状态,但是如果在大并发的短链接下,TIME_WAIT 就会太多,这也会消耗很多系统资源。只要搜一下,你就会发现,十有八九的处理方式都是教你设置两个参数,一个叫tcp_tw_reuse,另一个叫tcp_tw_recycle的参数,这两个参数默认值都是被关闭的,后者recyle比前者resue更为激进,resue要温柔一些。另外,如果使用tcp_tw_reuse,必需设置tcp_timestamps=1,否则无效。这里,你一定要注意,打开这两个参数会有比较大的坑——可能会让TCP连接出一些诡异的问题(因为如上述一样,如果不等待超时重用连接的话,新的连接可能会建不上。正如官方文档上说的一样“It should not be changed without advice/request of technical experts”)。


关于tcp_tw_reuse:
官方文档上说tcp_tw_reuse 加上tcp_timestamps(又叫PAWS, for Protection Against Wrapped Sequence Numbers)可以保证协议的角度上的安全,但是你需要tcp_timestamps在两边都被打开(你可以读一下tcp_twsk_unique的源码 )。我个人估计还是有一些场景会有问题。

关于tcp_tw_recycle:
如果是tcp_tw_recycle被打开了话,会假设对端开启了tcp_timestamps,然后会去比较时间戳,如果时间戳变大了,就可以重用。但是,如果对端是一个NAT网络的话(如:一个公司只用一个IP出公网)或是对端的IP被另一台重用了,这个事就复杂了。建链接的SYN可能就被直接丢掉了(你可能会看到connection time out的错误)(如果你想观摩一下Linux的内核代码,请参看源码 tcp_timewait_state_process)。

关于tcp_max_tw_buckets:
这个是控制并发的TIME_WAIT的数量,默认值是180000,如果超限,那么,系统会把多的给destory掉,然后在日志里打一个警告(如:time wait bucket table overflow),官网文档说这个参数是用来对抗DDoS攻击的。也说的默认值180000并不小。这个还是需要根据实际情况考虑。

Again,使用tcp_tw_reuse和tcp_tw_recycle来解决TIME_WAIT的问题是非常非常危险的,因为这两个参数违反了TCP协议(RFC 1122) 

其实,TIME_WAIT表示的是你主动断连接,所以,这就是所谓的“不作死不会死”。试想,如果让对端断连接,那么这个破问题就是对方的了,呵呵。另外,如果你的服务器是于HTTP服务器,那么设置一个HTTP的KeepAlive有多重要(浏览器会重用一个TCP连接来处理多个HTTP请求),然后让客户端去断链接(你要小心,浏览器可能会非常贪婪,他们不到万不得已不会主动断连接)。

 

概念

MTU:maximum transmission unit最大传输单元

每种网络都不一样,以太网是1500。最小46字节。当数据块大于MTU时,将在发送端IP层进行分片,接收端IP层进行重组。IP分组在网络中传输中出现丢包时,由于IP层没有重传机制,TCP将重传整个报文段而不是丢失的IP分组

PS: 以太网最小数据帧长度为,最小64字节,其中6字节目的地址 、字节6源地址、2字节类型、46字节数据、4字节校验和.

MSS:maximum segment size最大分段大小

MSS是TCP数据包每次能够传输的最大数据分段。为了达到最佳的传输效能TCP协议在建立连接的时候通常要协商双方的MSS值,这个值TCP协议在实现的时候往往用MTU值代替(需要减去IP数据包包头的大小20Bytes和TCP数据段的包头20Bytes)所以往往MSS为1460。通讯双方会根据双方提供的MSS值得最小值确定为这次连接的最大MSS值。

MSL:Maximum Segment Lifetime报文最大生存时间

报文在网络上存在的最长时间,超过这个时间报文将被丢弃。在RFC793指出MSL为2分钟,实际应用中常用的是30秒(linux),1分钟和2分钟等。

TTL:Time To Live生存时间

生存时间是由源主机设置初始值但不是存的具体时间,而是存储了一个ip数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减1,当此值为0则数据报将被丢弃,同时发送ICMP报文通知源主机。

RTT:round-trip time客户到服务器往返所花时间

RTT由三部分组成:链路的传播时间(propagation delay)、末端系统的处理时间、 路由器缓存中的排队和处理时间(queuing delay)。 其中,前两个部分的值对于一个TCP连接相对固定,路由器缓存中的排队和处理时间会随着整个网络拥塞程度 的变化而变化。所以RTT的变化在一定程度上反应了网络的拥塞程度。

RTO:Retransmission TimeOut重传超时时间

重传机制依赖于RTT(Round Trip Time)的测量,从而计算RTO(Retransmission Timeout)。

TSO:TCP Segment Offload

是一种利用网卡的处理能力,降低CPU发送数据包负载的技术。对于支持TSO的网卡,TCP协议栈在封包的时候会逐渐尝试增大MSS,网卡接收到TCP向下递交的数据后,按照MTU进行分片、复制TCP头且重新计算校验和,这样在网卡上完成了对大块数据的TCP分段,缓解了CPU的计算压力。

查看是否开启TSO

  • sudo ethtool -k eth0

关闭和打开TSO

  • $ sudo ethtool -K eth0 tso off // 关闭tso

  • $ sudo ethtool -K eth0 tso on // 开启tso

TCP协议结构

 

  • source port destination port:源端口和目标端口,注意不包括IP是因为这是上一层IP层的事情,这一层(传输层)只负责找到对应的端口,即应用程序。
  • sequence number:用来标记包的顺序。一个TCP包最多能传1460字节,对于超出的包,需要分片,为了保证对端收到的包是顺序完整的,需要通过seq num来重新组装数据包。值是本报文段所发送的数据的第一个字节的序号。
  • acknowledgment number:由于tcp是可靠的传输,发送端需要确认对端是否收到包,所以需要ack num来确认,如果没有收到,则会启动重传机制。ack num是期望收到对方的下一个报文段的数据的第一个字节的序号
  • header:由于tcp header有可选字段,所以长度不定。所以需要这个值作为offset来表示tcp头有多大。
  • reserved:保留字段,应填为0.
  • Tag位:
  • URG:为1表示有应急指针
  • ACK:除SYN包外都为1,确认应答字段有效
  • PSH:为1表明将接受的数据立即传给上层协议,0则先进行缓存
  • RST:为1表示强制断开连接要求对方重置连接
  • SYN:为1表示建立连接,发送方向对方发送建立连接的请求
  • FIN:为1表示结束连接,告知对方要中断连接
  • Window:滑动窗口值,告知对端,当前能接收的最大字节数,进行流量控制
  • CheckSum:校验和,由发送端填充,接收端对TCP报文段执行CRC算法以检验TCP报文段在传输过程中是否被篡改。注意,这个校验不仅包括TCP头部,也包括数据部分。
  • Urgent Pointer:紧急数据指针,紧急数据指的是发送端告诉接收端,这个数据是非常紧急的,请优先读取,多用于中断。
  • Options:扩展使用,不定长,但最长不超过40字节

 

滑动窗口

我们为了增加网络的吞吐量,想将数据包一起发送过去,这时候便产生了“滑动窗口”这种协议。有了“滑动窗口”这个概念

TCP滑动窗口主要是提供TCP的流控特性,解决接收端处理数据不及时导致接收缓冲区被填满后丢失数据的问题。 TCP头中的window字段,是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来

 

案例:

问题一:如何保证次序

提出问题:在我们滑动窗口协议之前,我们如何来保证发送方与接收方之间,每个包都能被收到。并且是按次序的呢?

问题1

发送方发送一个包1,这时候接收方确认包1。发送包2,确认包2。就这样一直下去,知道把数据完全发送完毕,这样就结束了。那么就解决了丢包,出错,乱序等一些情况!同时也存在一些问题。问题:吞吐量非常的低。我们发完包1,一定要等确认包1.我们才能发送第二个包。

问题二:如何提高吞吐量:

提出问题:那么我们就不能先连发几个包等他一起确认吗?这样的话,我们的速度会不会更快,吞吐量更高些呢?

æ¹è¿æ¹æ¡

 

如图,这个就是我们把两个包一起发送,然后一起确认。可以看出我们改进的方案比之前的好很多,所花的时间只是一个来回的时间。接下来,我们还有一个问题:改善了吞吐量的问题

问题三:如何实现最优解?

问题:我们每次需要发多少个包过去呢?发送多少包是最优解呢?

我们能不能把第一个和第二个包发过去后,收到第一个确认包就把第三个包发过去呢?而不是去等到第二个包的确认包才去发第三个包。这样就很自然的产生了我们"滑动窗口"的实现。

实现

在图中,我们可看出灰色1号2号3号包已经发送完毕,并且已经收到Ack。这些包就已经是过去式。4、5、6、7号包是黄色的,表示已经发送了。但是并没有收到对方的Ack,所以也不知道接收方有没有收到。8、9、10号包是绿色的。是我们还没有发送的。这些绿色也就是我们接下来马上要发送的包。 可以看出我们的窗口正好是11格。后面的11-16还没有被读进内存。要等4号-10号包有接下来的动作后,我们的包才会继续往下发送。

正常情况

正常情况

可以看到4号包对方已经被接收到,所以被涂成了灰色。“窗口”就往右移一格,这里只要保证“窗口”是7格的。 我们就把11号包读进了我们的缓存。进入了“待发送”的状态。8、9号包已经变成了黄色,表示已经发送出去了。接下来的操作就是一样的了,确认包后,窗口往后移继续将未发送的包读进缓存,把“待发送“状态的包变为”已发送“。

丢包情况

有可能我们包发过去,对方的Ack丢了。也有可能我们的包并没有发送过去。从发送方角度看就是我们没有收到Ack。

丢包

发生的情况:一直在等Ack。如果一直等不到的话,我们也会把读进缓存的待发送的包也一起发过去。但是,这个时候我们的窗口已经发满了。所以并不能把12号包读进来,而是始终在等待5号包的Ack。

如果我们这个Ack始终不来怎么办呢?  ------>后边的TCP重传机制有介绍

 

Alt text

从上图中看出,把发送数据横拍做长列状,发送方一但有数据收到ACK,那么滑动窗口左侧边就进行左移。同样,一旦接收方有数据被应用层消费,那么,滑动窗口的右侧边就进行右移。

  • 发送窗口及发送方数据状态
    1. 已发送并得到对方ACK;
    2. 已发送未收到ACK;
    3. 未发送但对方允许发送;
    4. 未发送,对方不允许发送。

    其中已发送未收到ACK和未发送但对方允许发送的数据称为发送窗口

  • 接收窗口接收方数据状态
    1. 已接收;
    2. 未接收,准备接收;
    3. 未接收,并不准备接收。

    其中未接收,准备接收的数据称为接收窗口

一旦滑动窗口大小缩小为0,发送端将停止发送数据,等待接收端新接收窗口值(大于0)的到来以移动滑动窗口的右边沿。上述滑动窗口机制自然能抑制发送端的发包速率,但同时引入了糊涂窗口综合症。

糊涂窗口综合症(Silly Window Syndrome)是指接收方通告一个较小的窗口,而发送方发送少量的数据的现象。要解决这个问题也不难,就是避免对小的window size做出响应,直到有足够大的window size再响应。

拥塞处理

如果网络不佳的情况下,接收端可能会因为网络包拥塞而无法接收到,而根据重传的特性发送端会在RTO时间后重传数据,这样更加加剧数据拥塞。解决办法有慢启动、拥塞避免、快速重传与快速恢复

发送方维护了两个窗口:拥塞窗口和滑动窗口。两者都是试图对发送窗口大小进行控制的,自然发送窗口大小=min{滑动窗口大小,拥塞窗口大小}。当无网络拥塞发生时,滑动窗口大小一般小于拥塞窗口大小。

慢启动

慢启动的意思是,刚刚加入网络的连接,一点一点地提速,其实并不慢,拥塞窗口大小呈指数上升。

拥塞避免算法

慢启动使得cwnd是呈指数增长。一定不可能是无限增长的,这里就有个阀值,超过这个阀值,就进入拥塞避免算法。 拥塞避免算法说的是拥塞窗口的增加不再是“每收到一个ACK,拥塞窗口就增加一个报文段”。 而是“每收到一个ACK,cwnd = cwnd + 1/cwnd”

判断拥塞

  1. 超时重传

    发出去一个包,超时定时器就开始计时,当超时定时器到时间之后,没有收到ACK,那么这个时候就判断为拥堵了,需要进行重传。TCP会直接把cwnd调整为1,sshthread 调整为cwnd/2,重新进入到慢启动流程。

  2. 快速重传

    比如5个请求,但是第2个请求丢失了,第1、3、4、5请求到了接收端,3、4、5请求触发了三个ACK返回,但是由于接收端没有收到请求1,返回的三个ACK都是ACK1的,所以发送方就表现为收到重复ACK。,当连续收到三条重复ACK的时候就进行重传,不需要等待重传计时器。这个时候TCP会觉得网络还是可以的,反应不会那么激烈,cwnd调整为cwnd/2, sshthresh调整为cwnd大小,进入快速恢复算法。

快速恢复

快速恢复算法是为了不要有一个重传就那么大响应。能尽快恢复到网络流畅时候稳定的状态。

 

TCP重传机制:

TCP要保证所有的数据包都可以到达,所以,必需要有重传机制。

注意:接收端给发送端的Ack确认只会确认最后一个连续的包,比如,发送端发了1,2,3,4,5一共五份数据,接收端收到了1,2,于是回ack 3,然后收到了4(注意此时3没收到),此时的TCP会怎么办?我们要知道,因为正如前面所说的,SeqNum和Ack是以字节数为单位,所以ack的时候,不能跳着确认,只能确认最大的连续收到的包,不然,发送端就以为之前的都收到了。

 

1.超时重传机制


一种是不回ack,死等3,当发送方发现收不到3的ack超时后,会重传3。一旦接收方收到3后,会ack 回 4——意味着3和4都收到了。

但是,这种方式会有比较严重的问题,那就是因为要死等3,所以会导致4和5即便已经收到了,而发送方也完全不知道发生了什么事,因为没有收到Ack,所以,发送方可能会悲观地认为也丢了,所以有可能也会导致4和5的重传。

对此有两种选择:

  • 一种是仅重传timeout的包。也就是第3份数据。
  • 另一种是重传timeout后所有的数据,也就是第3,4,5这三份数据。


这两种方式有好也有不好。第一种会节省带宽,但是慢,第二种会快一点,但是会浪费带宽,也可能会有无用功。但总体来说都不好。因为都在等timeout,timeout可能会很长(在下篇会说TCP是怎么动态地计算出timeout的)。
 

2.快速重传机制


于是,TCP引入了一种叫Fast Retransmit 的算法,不以时间驱动,而以数据驱动重传。也就是说,如果,包没有连续到达,就ack最后那个可能被丢了的包,如果发送方连续收到3次相同的ack,就重传。Fast Retransmit的好处是不用等timeout了再重传。

比如:如果发送方发出了1,2,3,4,5份数据,第一份先到送了,于是就ack回2,结果2因为某些原因没收到,3到达了,于是还是ack回2,后面的4和5都到了,但是还是ack回2,因为2还是没有收到,于是发送端收到了三个ack=2的确认,知道了2还没有到,于是就马上重转2。然后,接收端收到了2,此时因为3,4,5都收到了,于是ack回6。

Fast Retransmit只解决了一个问题,就是timeout的问题,它依然面临一个艰难的选择,就是,是重传之前的一个还是重传所有的问题。对于上面的示例来说,是重传#2呢还是重传#2,#3,#4,#5呢?因为发送端并不清楚这连续的3个ack(2)是谁传回来的?也许发送端发了20份数据,是#6,#10,#20传来的呢。这样,发送端很有可能要重传从2到20的这堆数据(这就是某些TCP的实际的实现)。可见,这是一把双刃剑。

 

3.SACK 方法


另外一种更好的方式叫:Selective Acknowledgment (SACK)(参看RFC 2018),这种方式需要在TCP头里加一个SACK的东西,ACK还是Fast Retransmit的ACK,SACK则是汇报收到的数据碎版。参看下图:

[通俗易懂]深入理解TCP协议(上):理论基础_7.jpg

这样,在发送端就可以根据回传的SACK来知道哪些数据到了,哪些没有到。于是就优化了Fast Retransmit的算法。当然,这个协议需要两边都支持。在 Linux下,可以通过tcp_sack参数打开这个功能(Linux 2.4后默认打开)。

这里还需要注意一个问题——接收方Reneging,所谓Reneging的意思就是接收方有权把已经报给发送端SACK里的数据给丢了。这样干是不被鼓励的,因为这个事会把问题复杂化了,但是,接收方这么做可能会有些极端情况,比如要把内存给别的更重要的东西。所以,发送方也不能完全依赖SACK,还是要依赖ACK,并维护Time-Out,如果后续的ACK没有增长,那么还是要把SACK的东西重传,另外,接收端这边永远不能把SACK的包标记为Ack。

注意:SACK会消费发送方的资源,试想,如果一个攻击者给数据发送方发一堆SACK的选项,这会导致发送方开始要重传甚至遍历已经发出的数据,这会消耗很多发送端的资源。(详细的东西请参看《TCP SACK的性能权衡》)

 

4.Duplicate SACK – 重复收到数据的问题


Duplicate SACK又称D-SACK,其主要使用了SACK来告诉发送方有哪些数据被重复接收了。RFC-2883 里有详细描述和示例。下面举几个例子(来源于RFC-2883).

D-SACK使用了SACK的第一个段来做标志:
 

  • 如果SACK的第一个段的范围被ACK所覆盖,那么就是D-SACK
  • 如果SACK的第一个段的范围被SACK的第二个段覆盖,那么就是D-SACK


示例一:ACK丢包
下面的示例中,丢了两个ACK,所以,发送端重传了第一个数据包(3000-3499),于是接收端发现重复收到,于是回了一个SACK=3000-3500,因为ACK都到了4000意味着收到了4000之前的所有数据,所以这个SACK就是D-SACK——旨在告诉发送端我收到了重复的数据,而且我们的发送端还知道,数据包没有丢,丢的是ACK包。

1

2

3

4

5

6

7

Transmitted  Received    ACK Sent

Segment      Segment     (Including SACK Blocks)

  

3000-3499    3000-3499   3500 (ACK dropped)

3500-3999    3500-3999   4000 (ACK dropped)

3000-3499    3000-3499   4000, SACK=3000-3500

                                    ---------


示例二:网络延误
下面的示例中,网络包(1000-1499)被网络给延误了,导致发送方没有收到ACK,而后面到达的三个包触发了“Fast Retransmit算法”,所以重传,但重传时,被延误的包又到了,所以,回了一个SACK=1000-1500,因为ACK已到了3000,所以,这个SACK是D-SACK——标识收到了重复的包。这个案例下,发送端知道之前因为“Fast Retransmit算法”触发的重传不是因为发出去的包丢了,也不是因为回应的ACK包丢了,而是因为网络延时了。

01

02

03

04

05

06

07

08

09

10

11

Transmitted    Received    ACK Sent

Segment        Segment     (Including SACK Blocks)

  

500-999        500-999     1000

1000-1499      (delayed)

1500-1999      1500-1999   1000, SACK=1500-2000

2000-2499      2000-2499   1000, SACK=1500-2500

2500-2999      2500-2999   1000, SACK=1500-3000

1000-1499      1000-1499   3000

                       1000-1499   3000, SACK=1000-1500

                                      ---------


可见,引入了D-SACK,有这么几个好处:

  • 1)可以让发送方知道,是发出去的包丢了,还是回来的ACK包丢了。
  • 2)是不是自己的timeout太小了,导致重传。
  • 3)网络上出现了先发的包后到的情况(又称reordering)
  • 4)网络上是不是把我的数据包给复制了。


知道这些东西可以很好得帮助TCP了解网络情况,从而可以更好的做网络上的流控。Linux下的tcp_dsack参数用于开启这个功能(Linux 2.4后默认打开)

 

 

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值