tcp

简述:

Tcp是

面向链接的
面向字节流的
可靠的(几个点)

保证可靠的手段:

  • 数据分块——握手时协商确定MSS,大于MSS的tcp数据包分段(也就是拆包);序列号;校验和;确认ack包

  • 超时重传——发送方使用一个保守估计的时间作为收到数据包的确认的超时上限RTO。如果超过这个上限仍未收到确认包,发送方将重传这个数据包。每当发送方收到确认包后,会重置这个重传定时器。

    超时重传会触发拥塞控制之重置拥塞窗口为1个MSS,阈值减为当前cwnd一半,执行慢启动每轮往返拥塞倍增

  • 滑动窗口实现的流量控制;接收方在ack包中设置rwnd控制发送方发送速度。

  • 拥塞控制算法——小于阈值之前从1开始每轮往返拥塞窗口cwnd倍增(慢启动),拥塞窗口大于阈值后步长为一的递增(拥塞避免)。接收方收到失序报文段后立即发出重复确认ack包,发送方连续三次重复确认则直接发送缺乏ack的丢包(快速重传),同时把阈值减为cwnd/2并调整拥塞窗口为新阈值而后执行拥塞避免算法(快速恢复);

Ps:

超时重传会触发拥塞控制之重置拥塞窗口为1个MSS,阈值减为当前cwnd一半,执行慢启动每轮往返拥塞倍增;

三次重复确认会执行快速重传快速恢复,阈值减为当前cwnd一半,拥塞窗口为新阈值值,执行拥塞避免,每轮往返递增;

First of All

废话少说,首先,我们需要知道TCP在网络OSI的七层模型中的第四层——Transport层,IP在第三层——Network层,ARP在第二层——Data Link层,在第二层上的数据,我们叫Frame,在第三层上的数据叫Packet,第四层的数据叫Segment。

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

TCP segment(段), IP packet(包)

UDP 和 TCP 的特点与区别

用户数据报协议 UDP(User Datagram Protocol)

是无连接的,尽最大可能交付,没有拥塞控制,面向报文(对于应用程序传下来的报文不合并也不拆分,只是添加 UDP 首部),支持一对一、一对多、多对一和多对多的交互通信。效率高。

传输控制协议 TCP(Transmission Control Protocol)

是面向连接的,提供可靠交付,有流量控制,拥塞控制,提供全双工通信,面向字节流(把应用层传下来的报文看成字节流,把字节流组织成大小不等的数据块),每一条 TCP 连接只能是点对点的(一对一)。效率低。

Tcp协议是怎么保证可靠传输

  1. 数据分块:应用数据被分割成 TCP 认为最适合发送的数据块(MTU=1500= MSS1460 + TCP头20+IP头20)。

    大于一个MSS的会被拆包,小于MSS的相连发出的包会粘包。

  2. 序列号:TCP 给发送的每一个包进行编号,接收方对数据包进行排序,把有序数据传送给应用层。

    首部中的seq字段32位,如果SYN标记位打开,则为握手时初始化随机序列号。保证每个包的有序

  3. 校验和:发送的数据包的二进制相加然后取反,目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错,TCP将丢弃这个报文段和不确认收到此报文段。

  4. 确认应答:TCP 传输的过程中,每次接收方收到数据后,都会对传输方进行确认应答。也就是发送 ACK 报文。这个 ACK 报文当中带有对应的确认序列号,告诉发送方,接收到了哪些数据,下一次的数据从哪里发。

    ACK包=ack标记位置1接受到的seq+1

  5. 流量控制: TCP 连接的每一方都有固定大小的缓冲空间,TCP的接收端只允许发送端发送接收端缓冲区能接纳的数据。当接收方来不及处理发送方的数据,能提示发送方降低发送的速率,防止包丢失。TCP 使用的流量控制协议是可变大小的滑动窗口协议。 (TCP 利用滑动窗口实现流量控制)

    首部中的16位WIN字段,由接收方返回给发送端的ack中携带,通知发送端自身缓冲区还能接纳的数据。

  6. 拥塞控制: 当网络拥塞时,减少数据的发送。

    一为慢启动和拥塞避免算法(小于阈值倍增大于阈值加一递增,超时重传触发重置cwnd为1并慢启动)

    二为快速重传和快速回复(连续三次重复确认会触发快速恢复,即阈值减半,cwnd=新阈值,拥塞避免)

  7. 超时重传: 当 TCP 发出一个段后,它启动一个定时器(一个超时时间RTO),等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段。

    如果一个已经发送的报文段在超时时间RTO内没有收到确认,那么就重传这个报文段,阈值减半,重置cwnd为1后慢启动

Tcp的连接和断开过程

三次握手和四次挥手

tcp握手挥手和状态机

图片

三次握手:

即:client发起一个Syn包包含client序列号 -> server收到后返回ack(client序列号+1)包和Syn包包含server序列号 -> client回馈一个ack包(server序列号+1),三次之后client和server都确认了对方的读写能力

TCP 进行握手初始化一个连接的目标是:**分配资源、初始化序列号(通知 peer 对端我的初始序列号是多少)**,知道初始化连接的目标,那么要达成这个目标的过程就简单了,握手过程可以简化为下面的四次交互:

第一次握手:

1)client 端首先发送一个 SYN 包告诉 Server 端我的初始序列号是 X;

第二次握手:

2)Server 端收到 SYN 包后回复给 client 一个 ACK 确认包,告诉 client 说我收到了;

3)接着 Server 端也需要告诉 client 端自己的初始序列号,于是 Server 也发送一个 SYN 包告诉 client 我的初始序列号是 Y;

第三次握手:

4)Client 收到后,回复 Server 一个 ACK 确认包说我知道了。

其中2)3)合并在一起形成第二次握手,也就是server回一个SYN+ACK包,序列号为Y,ack为X+1;

Question:

  1. 一定是三次吗?

大部分情况下建立连接需要三次握手,也不一定都是三次,有可能出现四次握手来建立连接的。如下图,当 Peer 两端同时发起 SYN 来建立连接的时候,就出现了四次握手来建立连接(对于有些 TCP/IP 的实现,可能不支持这种同时打开的情况)。

图片

  1. 初始化序列号 X、Y 是可以是写死固定的吗,为什么不能呢?

不能,假如序列号固定的话,假设为1,如果刚开始client给server发送的10个包被缓存住了,又恰好client掉了。过了一会,client又用同样的端口重连回来了,序列号重新从1开始发送,连发5个包。这时候又恰好之前被路由器缓存住的10个包全部被路由到server端了,然后server给client回ack=10的包。那么client就乱了,才发了5个(重连后的)server就返回了确认号为10的?故显然不行。

  • 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。
  1. 如 Client 发送一个 SYN 包给 Server 后就挂了或是不管了,这个时候这个连接处于什么状态呢?会超时吗?为什么呢?

重试5次,从1s开始,每次是之前的2倍,1s + 2s +4s+ 8s+ 16s + 32s =63s

Linux 下默认会进行 5 次重发 SYN-ACK 包,重试的间隔时间从 1s 开始,下次的重试间隔时间是前一次的双倍,5 次的重试时间间隔为 1s,2s, 4s, 8s,16s,总共 31s,第 5 次发出后还要等 32s 都知道第 5 次也超时了,所以,总共需要 1s + 2s +4s+ 8s+ 16s + 32s =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 处理不过来干脆就直接拒绝连接了。

四次挥手

即:client发起FIN包 -> server回一个ack包 -> server数据发完了就也发一个FIN包 -> client回一个ack包同时进入TIME_WAIT防止ack包丢失

TCP 进行断开连接的目标是:回收资源、终止数据传输。由于 TCP 是全双工的,需要 Peer 两端分别各自拆除自己通向 Peer 对端的方向的通信信道。这样需要四次挥手来分别拆除通信信道,就比较清晰明了了。

第一次挥手:

1)Client 发送一个 FIN 包来告诉 Server 我已经没数据需要发给 Server 了;

第二次挥手:

2)Server 收到后回复一个 ACK 确认包说我知道了;

第三次挥手:

3)然后 server 在自己也没数据发送给 client 后,Server 也发送一个 FIN 包给 Client 告诉 Client 我也已经没数据发给 client 了;

第四次挥手:

4)Client 收到后,就会回复一个 ACK 确认包说我知道了。

TCP 主动关闭连接的那一方(Client)会最后进入 TIME_WAIT(超时设置是 2*MS,即Linux为30s)

Question:

  1. 为什么建立连接是三次握手,关闭连接确是四次挥手呢?

简单来讲就是第二三次挥手不能像第二三次握手的时候一样合并成一个,因为可能server端还有数据没有发完。所以需要在可能的数据发完之后再进行第三次挥手

  1. 为什么主动关闭连接的一方要进入TIME_WAIT?为什么是2MS

主动关闭方需要进入 TIME_WAIT 以便能够重发丢掉的被动关闭方 FIN 包的 ACK。

TIME_WAIT状态也成为2MSL等待状态。每个具体TCP实现必须选择一个报文段最大生存时间MSL(Maximum Segment Lifetime),它是任何报文段被丢弃前在网络内的最长时间。也就是一来一回的时间。

第四次挥手时,客户端发送给服务器的ACK有可能丢失,TIME_WAIT状态就是用来重发可能丢失的ACK报文。如果Server没有收到ACK,就会重发FIN,如果Client在2*MSL的时间内收到了FIN,就会重新发送ACK并再次等待2MSL,防止Server没有收到ACK而不断重发FIN。 MSL(Maximum Segment Lifetime),指一个片段在网络中最大的存活时间,2MSL就是一个发送和一个回复所需的最大时间。如果直到2MSL,Client都没有再次收到FIN,那么Client推断ACK已经被成功接收,则结束TCP连接。

数据传输中的Sequence Number

下图是我从Wireshark中截了个我在访问coolshell.cn时的有数据传输的图给你看一下,SeqNum是怎么变的。(使用Wireshark菜单中的Statistics ->Flow Graph… )

img

你可以看到,SeqNum的增加是和传输的字节数相关的。上图中,三次握手后,来了两个Len:1440的包,而第二个包的SeqNum就成了1441。然后第一个ACK回的是1441,表示第一个1440收到了。

注意:如果你用Wireshark抓包程序看3次握手,你会发现SeqNum总是为0,不是这样的,Wireshark为了显示更友好,使用了Relative SeqNum——相对序号,你只要在右键菜单中的protocol preference 中取消掉就可以看到“Absolute SeqNum”了

数据分块&面向字节流

分块传输

我们可以发现,运输层在传输数据的时候,并不是把整个数据包加个首部直接发送过去,而是会拆分成多个报文分开发送;那他这样做原因是什么?

有读者可能会想到:数据链路层限制了数据长度只能有1460(MSS)。那数据链路层为什么要这么限制?他的本质原因就是:网络是不稳定的。如果报文太长,那么极有可能在传输一般的时候突然中断了,这个时候就要整个数据重传,效率就降低了。把数据拆分成多个数据报,那么当某个数据报丢失,只需要重传该数据报即可。

那是不是拆分得越细越好?报文中数据字段长度太低,会使得首部的占比太大,这样首部就会成为网络传输最大的负担了。例如1000字节,每个报文首部是40字节,如果拆分成10个报文,那么只需要传输400字节的首部;而如果拆分成1000个,那么需要传输40000字节的首部,效率就极大地降低了。

面向字节流

TCP并不是把应用层传输过来的数据直接加上首部然后发送给目标,而是把数据看成一个字节 流,给他们标上序号之后分部分发送。这就是TCP的 面向字节流 特性:

img

  • TCP会以流的形式从应用层读取数据并存放在自己的发送缓存区中,同时为这些字节标上序号
  • TCP会从发送方缓冲区选择适量的字节组成TCP报文,通过网络层发送给目标
  • 目标会读取字节并存放在自己的接收方缓冲区中,并在合适的时候交付给应用层

TCP的粘包和拆包

程序需要发送的数据大小和TCP报文段能发送MSS(Maximum Segment Size,最大报文长度)是不一样的
大于MSS时,而需要把程序数据拆分为多个TCP报文段,称之为拆包;小于时,则会考虑合并多个程序数据为一个TCP报文段,则是粘包;
在IP协议层或者链路层、物理层,都存在拆包、粘包现象

解决粘包和拆包的方法都有哪些?

1)在数据尾部增加特殊字符进行分割
2)将数据定为固定大小
3)将数据分为两部分,

流量控制:

滑动窗口

TCP头里有一个字段叫Window,这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来

TCP 利用滑动窗口实现流量控制。流量控制是为了控制发送方发送速率,保证接收方来得及接收。将窗口字段设置为 0,则发送方不能发送数据。

由接收方 向 发送方通知自己还有多少缓冲区接受数据

设A向B发送数据。在连接建立时,B告诉了A:“我的接收窗口是 rwnd = 400 ”(这里的 rwnd 表示 receiver window) 。因此,发送方的发送窗口不能超过接收方给出的接收窗口的数值。请注意,TCP的窗口单位是字节,不是报文段。假设每一个报文段为100字节长,而数据报文段序号的初始值设为1。大写ACK表示首部中的确认位ACK,小写ack表示确认字段的值ack。

图片

从图中可以看出,B进行了三次流量控制。第一次把窗口减少到 rwnd = 300 ,第二次又减到了 rwnd = 100 ,最后减到 rwnd = 0 ,即不允许发送方再发送数据了。这种使发送方暂停发送的状态将持续到主机B重新发出一个新的窗口值为止。B向A发送的三个报文段都设置了 ACK = 1 ,只有在ACK=1时确认号字段才有意义。

零窗口(Zero Window)

如果Window变成0了,TCP会怎么样?是不是发送端就不发数据了?是的,发送端就不发数据了,你可以想像成“Window Closed”,那你一定还会问,如果发送端不发数据了,接收方一会儿Window size 可用了,怎么通知发送端呢?

解决这个问题,TCP使用了零窗口探测Zero Window Probe(ZWP)技术,也就是说,发送端在窗口变成0后,会发ZWP的包给接收方,让接收方来ack他的Window尺寸,一般这个值会设置成3次,每次大约30-60秒(不同的实现可能会不一样)。如果3次过后还是0的话,有的TCP实现就会发RST把链接断了。

image-20230907175049737

糊涂窗口综合症(Silly Window Syndrome)

如果我们的接收方太忙了,来不及取走Receive Windows里的数据,那么,就会导致发送方越来越小。到最后,如果接收方腾出几个字节并告诉发送方现在有几个字节的window,而我们的发送方会义无反顾地发送这几个字节。
要知道,我们的TCP+IP头有40个字节,为了几个字节,要达上这么大的开销,这太不经济了。
要解决这个问题也不难,就是避免对小的window size做出响应,直到有足够大的window size再响应,这个思路可以同时实现在sender和receiver两端。

  • 如果这个问题是由接收端引起的(比如接收端处理缓冲区数据处理不过来,太忙了),那么就会使用 Clark 方案。在receiver端,如果收到的数据导致window size小于某个值,可以直接ack(0)回sender,这样就把window给关闭了,也阻止了sender再发数据过来,等到receiver端处理了一些数据后windows size 大于等于了MSS,或者,receiver buffer有一半为空,就可以把window打开让send 发送数据过来。

    (简述:接收端引起的SWS,Clark方案:接收到的数据导致滑窗过小,ack(window=0)让发送端先暂停发送,直到接收端处理完一些数据后window大于MSS了或者缓冲区一半空了,就可以ack(新值)了)

  • 如果这个问题是由发送端引起的(比如发送端的内容是一个字节一个字节产生的),那么就会使用著名的 Nagle’s algorithm。这个算法的思路也是延时处理,他有两个主要的条件:1)要等到 Window Size>=MSS 或是 Data Size >=MSS,2)收到之前发送数据的ack回包,他才会发数据,否则就是在攒数据。

    (简述:发送端引起的SWS,nagle算法:攒数据直到WindowSize >= MSS或者Data Szie >= MSS,同时要收到之前发送数据的ack回包)

另外,Nagle算法默认是打开的,所以,对于一些需要小包场景的程序——比如像telnet或ssh这样的交互性比较强的程序,你需要关闭这个算法。你可以在Socket设置TCP_NODELAY选项来关闭这个算法(关闭Nagle算法没有全局参数,需要根据每个应用自己的特点来关闭)

拥塞控制:

慢开始门限ssthresh状态变量(假设为8个MSS)

swnd = min(rwnd, cwnd)

发送方的窗口上限,是取值滑动窗口和拥塞窗口两者的最小值

滑动窗口和拥塞窗口区别:
相同点都是控制丢包现象,实现机制都是让发送方发得慢一点

不同点在于控制的对象不同
1)流量控制的对象是接收方,怕发送方发的太快,使得接收方来不及处理
2)拥塞控制的对象是网络拥塞,怕发送方发的太快,造成网络拥塞,使得网络来不及处理

慢启动

即:拥塞窗口从1MSS开始,每轮往返(即发送的TCP段都收到了ack)加倍直到ssthresh值为止,切换拥塞避免

当主机开始发送数据时,如果立即所大量数据字节注入到网络,那么就有可能引起网络拥塞,因为现在并不清楚网络的负荷情况。 因此,较好的方法是 先探测一下,即由小到大逐渐增大发送窗口,也就是说,由小到大逐渐增大拥塞窗口数值。

通常在刚刚开始发送报文段时,先把拥塞窗口 cwnd 设置为一个最大报文段MSS的数值。而在每收到一个对新的报文段的确认后,把拥塞窗口增加至多一个MSS的数值。用这样的方法逐步增大发送方的拥塞窗口 cwnd ,可以使分组注入到网络的速率更加合理。

图片

cwnd窗口 1 -> 2 -> 4 -> 8

每经过一个传输轮次,拥塞窗口 cwnd 就加倍。一个传输轮次所经历的时间其实就是往返时间RTT。不过“传输轮次”更加强调:把拥塞窗口cwnd所允许发送的报文段都连续发送出去,并收到了对已发送的最后一个字节的确认。 另,慢开始的“慢”并不是指cwnd的增长速率慢,而是指在TCP开始发送报文段时先设置cwnd=1,使得发送方在开始时只发送一个报文段(目的是试探一下网络的拥塞情况),然后再逐渐增大cwnd。

为了防止拥塞窗口cwnd增长过大引起网络拥塞,还需要设置一个慢开始门限ssthresh状态变量。慢开始门限ssthresh的用法如下:

  • 当 cwnd < ssthresh 时,使用上述的慢开始算法。
  • 当 cwnd > ssthresh 时,停止使用慢开始算法而改用拥塞避免算法。
  • 当 cwnd = ssthresh 时,既可使用慢开始算法,也可使用拥塞控制避免算法。

拥塞避免算法

即:大于ssthresh值时拥塞窗口每轮往返加一

让拥塞窗口cwnd缓慢地增大,即每经过一个往返时间RTT就把发送方的拥塞窗口cwnd加1,而不是加倍。这样拥塞窗口cwnd按线性规律缓慢增长,比慢开始算法的拥塞窗口增长速率缓慢得多。

​ (假装有图)

cwnd窗口 8 -> 9 -> 10 -> 11

慢热启动算法 – Slow Start

首先,我们来看一下TCP的慢热启动。慢启动的意思是,刚刚加入网络的连接,一点一点地提速,不要一上来就像那些特权车一样霸道地把路占满。新同学上高速还是要慢一点,不要把已经在高速上的秩序给搞乱了。

慢启动的算法如下(cwnd全称Congestion Window):

1)连接建好的开始先初始化cwnd = 1,表明可以传一个MSS大小的数据。

2)每当收到一个ACK,cwnd++; 呈线性上升

3)每当过了一个RTT,cwnd = cwnd*2; 呈指数让升

4)还有一个ssthresh(slow start threshold),是一个上限,当cwnd >= ssthresh时,就会进入“拥塞避免算法”(后面会说这个算法)

所以,我们可以看到,如果网速很快的话,ACK也会返回得快,RTT也会短,那么,这个慢启动就一点也不慢。下图说明了这个过程。

img

这里,我需要提一下的是一篇Google的论文《An Argument for Increasing TCP’s Initial Congestion Window》Linux 3.0后采用了这篇论文的建议——把cwnd 初始化成了 10个MSS。 而Linux 3.0以前,比如2.6,Linux采用了RFC3390,cwnd是跟MSS的值来变的,如果MSS< 1095,则cwnd = 4;如果MSS>2190,则cwnd=2;其它情况下,则是3。

拥塞避免算法 – Congestion Avoidance

前面说过,还有一个ssthresh(slow start threshold),是一个上限,当cwnd >= ssthresh时,就会进入“拥塞避免算法”。一般来说ssthresh的值是65535,单位是字节,当cwnd达到这个值时后,算法如下:

1)收到一个ACK时,cwnd = cwnd + 1/cwnd

2)当每过一个RTT时,cwnd = cwnd + 1

这样就可以避免增长过快导致网络拥塞,慢慢的增加调整到网络的最佳值。很明显,是一个线性上升的算法。

快速重传和快速恢复

接收方每收到一个失序报文段就立即发出重复确认,发送方如果一连三次收到重复确认立即重传,ssthresh值减半,并cwnd窗口变更为新ssthresh,接下来执行拥塞避免算法加法增大

快速重传

即:接收方每收到一个失序报文段就立即发出重复确认,发送方如果一连三次收到重复确认立即重传

  • 首先要求接收方每收到一个失序的报文段后就立即发出重复确认(为的是使发送方及早知道有报文段没有到达对方)而不要等到自己发送数据时才进行捎带确认。
  • 发送方只要一连收到三个重复确认M2就应当立即重传对方尚未收到的报文段M3

图片

快速恢复(Reno算法)

即发送方接受到三个重复确认时,ssthresh值减半,并cwnd窗口变更为新ssthresh,接下来执行加法增大

  • 当发送方连续收到三个重复确认,就执行“乘法减小”算法,把慢开始门限ssthresh减半,cwnd窗口变更为新ssthresh值(旧值的一半)。(即不执行“慢开始指数增大”,窗口不重置为1,而是设置为ssthresh二分之一,后面执行拥塞避免算法“加法增大”)

图片

超时重传

简述:如果一个已经发送的报文段在超时时间RTO内没有收到确认,那么就重传这个报文段。

往返时间Round Trip Time——RTT:客户到服务器往返所花时间,常说的延迟

超时时间Retransmission TimeOut——RTO:由RTT加权计算出来的超时时间,超出RTO没有ack则重传

等到RTO超时,重传数据包。TCP认为这种情况太糟糕,反应也很强烈。

    • sshthresh = cwnd /2
    • cwnd 重置为 1
    • 进入慢启动过程

一个报文段从发送再到接收到确认所经过的时间称为往返时间 RTT,加权平均往返时间 RTTs 计算如下:

img

其中,0 ≤ a < 1,RTTs 随着 a 的增加更容易受到 RTT 的影响。超时时间 RTO 应该略大于 RTTs,TCP 使用的超时时间计算如下:

img

其中 RTTd 为偏差的加权平均值。

快速重传和超时重传区别

超时重传:如果一个已经发送的报文段在超时时间RTO内没有收到确认,那么就重传这个报文段,阈值减半,重置cwnd为1后慢启动

快速重传:客户端连续收到三次重复确认(即服务器连续三次收到乱序的包),立即重传对应丢失包,阈值减半,cwnd设为新阈值后拥塞避免

快速重传机制「RFC5681」基于接收端的反馈信息来引发重传,而非重传计时器超时。基于计时器的重传往往要等待很长时间,而快速重传使用了很巧妙的方法来解决这个问题:服务器如果收到乱序的包,也给客户端回复 ACK,只不过是重复的 ACK。就拿刚刚的例子来说,收到乱序的包 6,7,8,9 时,服务器全都发 ACK = 5。这样,客户端就知道 5 发生了空缺。一般来说,如果客户端连续三次收到重复的 ACK,就会重传对应包,而不需要等到计时器超时。

附录一: MSL、ttl及RTT&RTO的区别

MSL(30s)最大报文生存时间

Maximum Segment Lifetime

简述:最大报文生存时间,一般是30s,用于关闭链接TIME_WAIT状态停留2MSL

每个TCP实现必须选择一个MSL。它是任何报文段被丢弃前在网络内的最长时间。这个时间是有限的,因为TCP报文段以IP数据报在网络内传输,而IP数据报则有限制其生存时间的TTL时间。RFC 793指出MSL为2分钟,Linux为30s

2MSL(2*30s)

当TCP执行主动关闭,并发出最后一个ACK,该链接必须在TIME_WAIT状态下停留的时间为2MSL。这样可以(1)让TCP再次发送最后的ACK以防这个ACK丢失(被动关闭的一方超时并重发最后的FIN);保证TCP的可靠的全双工连接的终止。(2)允许老的重复分节在网络中消失。参考文章《unix网络编程》(3)TCP连接的建立和终止

RTT往返时间,RTO超时时间

round-trip-time

简述:RTT客户到服务器往返所花时间,RTO由RTT加权计算出的用于超时重传的超时时间用于

往返时间Round Trip Time——RTT:客户到服务器往返所花时间,常说的延迟,Ping命令出来的

超时时间Retransmission TimeOut——RTO:由RTT加权计算出来的超时时间,超出RTO没有ack则重传

TCP超时重传中最重要的部分就是对一个给定连接的往返时间RTT的测量。由于路由器和网络流量均会变化,因此这个时间可能经常会变化,TCP应该跟踪这些变化并相应地改变其超时时间。

TTL生存时间字段

time-to-live

IP首部中的8位字段。该字段不是存的具体时间,而是设置了数据报可以经过的最多路由器数。它制定了数据报的生存时间。TTL的初始值由源主机设置(通常为32或64),一旦经过一个处理它的路由器,它的值就减去1.当该字段值为0时,数据报就被丢弃,并发送ICMP报文通知源主机。

附录二:MSS、MTU

你需要知道网络上有个MTU,对于以太网来说,MTU是1500字节,除去TCP+IP头的40个字节,真正的数据传输可以有1460,这就是所谓的MSS(Max Segment Size)注意,TCP的RFC定义这个MSS的默认值是536,这是因为 RFC 791里说了任何一个IP设备都得最少接收576尺寸的大小(实际上来说576是拨号的网络的MTU,而576减去IP头的20个字节就是536)。

附录三:UDP 、TCP 首部格式

UDP首部:

8个字节

img

UDP 首部字段只有 8 个字节,包括源端口、目的端口、长度、检验和。12 字节的伪首部是为了计算检验和临时添加的。

TCP首部:

20个字节以上

img

img

TCP 首部格式比 UDP 复杂。

序号:用于对字节流进行编号,例如序号为 301,表示第一个字节的编号为 301,如果携带的数据长度为 100 字节,那么下一个报文段的序号应为 401。

确认号:期望收到的下一个报文段的序号。例如 B 正确收到 A 发送来的一个报文段,序号为 501,携带的数据长度为 200 字节,因此 B 期望下一个报文段的序号为 701,B 发送给 A 的确认报文段中确认号就为 701。

数据偏移:指的是数据部分距离报文段起始处的偏移量,实际上指的是首部的长度。

控制位:八位从左到右分别是 CWR,ECE,URG,ACK,PSH,RST,SYN,FIN。

CWR:CWR 标志与后面的 ECE 标志都用于 IP 首部的 ECN 字段,ECE 标志为 1 时,则通知对方已将拥塞窗口缩小;

ECE:若其值为 1 则会通知对方,从对方到这边的网络有阻塞。在收到数据包的 IP 首部中 ECN 为 1 时将 TCP 首部中的 ECE 设为 1;

URG:该位设为 1,表示包中有需要紧急处理的数据,对于需要紧急处理的数据,与后面的紧急指针有关;

ACK:该位设为 1,确认应答的字段有效,TCP规定除了最初建立连接时的 SYN 包之外该位必须设为 1;

PSH:该位设为 1,表示需要将收到的数据立刻传给上层应用协议,若设为 0,则先将数据进行缓存;

RST:该位设为 1,表示 TCP 连接出现异常必须强制断开连接;

SYN:用于建立连接,该位设为 1,表示希望建立连接,并在其序列号的字段进行序列号初值设定;

FIN:该位设为 1,表示今后不再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换 FIN 位置为 1 的 TCP 段。

每个主机又对对方的 FIN 包进行确认应答之后可以断开连接。不过,主机收到 FIN 设置为 1 的 TCP 段之后不必马上回复一个 FIN 包,而是可以等到缓冲区中的所有数据都因为已成功发送而被自动删除之后再发 FIN 包;

窗口:窗口值作为接收方让发送方设置其发送窗口的依据。之所以要有这个限制,是因为接收方的数据缓存空间是有限的。

TCP头格式

接下来,我们来看一下TCP头的格式

imgTCP头格式(图片来源

你需要注意这么几点:

  • TCP的包是没有IP地址的,那是IP层上的事。但是有源端口和目标端口。
  • 一个TCP连接需要四个元组来表示是同一个连接(src_ip, src_port, dst_ip, dst_port)准确说是五元组,还有一个是协议。但因为这里只是说TCP协议,所以,这里我只说四元组。
  • 注意上图中的四个非常重要的东西:
    • Sequence Number是包的序号,用来解决网络包乱序(reordering)问题。
    • Acknowledgement Number就是ACK——用于确认收到,用来解决不丢包的问题
    • Window又叫Advertised-Window,也就是著名的滑动窗口(Sliding Window),用于解决流控的
    • TCP Flag ,也就是包的类型,主要是用于操控TCP的状态机的

关于其它的东西,可以参看下面的图示

img

图片来源

IP首部:

20个字节以上

1、第一个4字节(也就是第一行):
(1)版本号(Version),4位;用于标识IP协议版本,IPv4是0100,IPv6是0110,也就是二进制的4和6。
(2)首部长度(Internet Header Length),4位;用于标识首部的长度,单位为4字节,所以首部长度最大值为:(2^4 - 1) * 4 = 60字节,但一般只推荐使用20字节的固定长度。
(3)服务类型(Type Of Service),8位;用于标识IP包的优先级,但现在并未使用。
(4)总长度(Total Length),16位;标识IP数据报的总长度,最大为:2^16 -1 = 65535字节。
2、第二个四字节:
(1)标识(Identification),16位;用于标识IP数据报,如果因为数据链路层帧数据段长度限制(也就是MTU,支持的最大传输单元),IP数据报需要进行分片发送,则每个分片的IP数据报标识都是一致的。
(2)标识(Flag),3位,但目前只有2位有意义;最低位为MF,MF=1代表后面还有分片的数据报,MF=0代表当前数据报已是最后的数据报。次低位为DF,DF=1代表不能分片,DF=0代表可以分片。
(3)片偏移(Fragment Offset),13位;代表某个分片在原始数据中的相对位置。
3、第三个四字节:
(1)生存时间(TTL),8位;以前代表IP数据报最大的生存时间,现在标识IP数据报可以经过的路由器数。
(2)协议(Protocol),8位;代表上层传输层协议的类型,1代表ICMP,2代表IGMP,6代表TCP,17代表UDP。
(3)校验和(Header Checksum),16位;用于验证数据完整性,计算方法为,首先将校验和位置零,然后将每16位二进制反码求和即为校验和,最后写入校验和位置。
4、第四个四字节:源IP地址
5、第五个四字节:目的IP地址

附录:参考QA

网络篇:朋友面试之TCP/IP,回去等通知吧 - 掘金 (juejin.cn)

TCP 的那些事儿(上) | 酷 壳 - CoolShell

TCP 的那些事儿(下) | 酷 壳 - CoolShell

Author

white crow

Posted on

2021-11-11

Updated on

2024-04-24

Licensed under