深入理解TCP


1. TCP的首部都有哪些字段?

TCP本身来说比较复杂,因此其控制协议传输的首部也就比较复杂,因此下面就列举几个比较重要的字段:

序列号:TCP是面向字节流的协议,因此为了说明接收双方的数据流动情况,就必须用标号来标识当前的传输进度,而序列号就是描述数据传输情况的一种手段,每发送一次数据,就累加一次该数据字节数的大小,它同时也可以用来解决网络包乱序的问题

确认应答号:指的是下一次期望收到的数据的序列号,发送端收到这个确认应答之后可以认为在这个ack之前所有的字节都被正确收到了

控制位主要有四个

  • ACK:这个位为1的时候,表示上一次对方发送过来的数据包已经正确接收,是一个应答的信号
  • RST:当这个位为1的时候,表示TCP连接出现了异常必须强制断开连接
  • SYN:当这个位为1的时候,表示希望与对方建立连接,在此阶段初始化序列号
  • FIN:当这个位为1的时候,表示希望与对方断开连接,当数据交换完毕后,通信双方的主机就可以相互交换FIN=1的TCP段

2. 为什么需要TCP协议?直接使用IP协议不行吗?

这是因为IP协议是不可靠的,它不能够保证数据包的可靠传输,IP协议的作用主要是为上层连接提供一个分组转发、路由选择的作用,在网络传输的过程中,由于路由选择情况的不同,可能导致后发送的包先到达目标主机,在这样的情况下,可能导致乱序接收,因此为了双方接收的数据是正常的,必须有一套可靠传输的控制机制来控制数据包的交互。

TCP是一个工作在传输层的可靠的赵金数据传输服务,能够确保接收端接收的网络包是没有损坏、没有间隔、非冗余和按序的。

3. 什么是TCP协议

TCP叫做传输控制协议,与之相关的,在传输层还有一个叫做用户数据报的协议,叫做UDP

TCP是面向连接的、可靠的、基于字节流的传输层通信协议

  • 所谓面向连接:就是必须通信双方都建立起连接后才能够连接,因此其必须通过三次握手等机制来确保连接正常后才能够进行通信
  • 所谓可靠:TCP通信的双方维护了一个发送窗口和接收窗口,这两个窗口说明了当前接收方发送了哪些数据,接收方接收了哪些数据,同时通过序列号+确认号的机制不断推进传输,从而可以保证传输的有序接收,而且超时重传机制能够使得发送方将未到达的报文重新发送到对方
  • 所谓字节流:用户消息通过TCP协议进行传输的时候,这个消息可能是一大段报文,它有可能会被操作系统分组为多个TCP报文,如果接收方的程序不知道消息的边界,是无法读取出一个有效的用户消息的,并且TCP报文是有序的,当前一个TCP报文没有收到的时候,即使它先收到了后面的报文,也不能够传输到应用层进行处理,重复的TCP报文会被自动丢弃

这也就是常说的原生TCP所固有的粘包和半包的症结所在。

4. 什么是TCP连接?

对于TCP来说,怎么样来描述一个连接的基本状态?

  • 收发双方的地址:这个通常使用socket进行表示,一个socket就包含了了{Ip,port}
  • 序列号:收发双方获取数据的进度
  • 窗口大小:用来表示收发双方的接收和发送能力,用来做流量控制

5. 如何唯一确定一个TCP连接?

一个TCP连接的四元组表示如下:{源地址,源端口,目标地址,目标端口}

在网络传输的过程中, 源地址和目的地址的字段是在IP头部中的,而源端口和目的端口的字段是在TCP头部中的

有一个 IP 的服务端监听了一个端口,它的 TCP 的最大连接数是多少?

服务端通常固定在某个本地端口上进行监听,等待客户端的连接请求,它会存在一个监听socket

那么根据TCP的唯一标识,可以看出连接数 = 客户端的IP*客户端的端口数,最多可以到2^48

然而,每一个连接代表着一个fd,fd是受系统管控的资源,会受到以下的因素影响

  • 文件描述符的限制,每一个TCP连接都是一个文件,如果文件描述符被占满了,会发生 Too Many open filesLinux对可以打开的文件描述符的数量做了三个方面的限制
    • 系统级:当前系统可以打开的最大数量,可以通过cat /proc/sys/fs/file-max进行查看
    • 用户级:指定用户可打开的最大数量,可以通过cat /etc/security/limits.conf进行查看
    • 进程级:单个进程可打开的最大数量,通过 cat /proc/sys/fs/nr_open 查看;
  • 内存限制,每个 TCP 连接都要占用一定内存,操作系统的内存是有限的,如果内存资源被占满后,会发生 OOM。

6. UDP和TCP有什么区别?在应用上有什么异同?

UDPTCP的最重要的区别在于:UDP并不提供复杂的控制机制,它是利用IP提供面向无连接的通信服务

我们从UDP的头部就可见端倪,TCP的首部除了有描述接收双方的源端口号和目的端口号之外,还有序列号确认号ACKSYNFINRST等复杂的控制字段,因此UDP相对于TCP而言,其使用更加简单,但是是不可靠的传输

UDP的头部格式有源端口号目标端口号包长度检验和

  • 目标和源端口:主要是告诉 UDP 协议应该把报文发给哪个进程。
  • 包长度:该字段保存了 UDP 首部的长度跟数据的长度之和。
  • 校验和:校验和是为了提供可靠的 UDP 首部和数据而设计,防止收到在网络传输中受损的 UDP 包。

下面具体来讲讲两者的异同:

  • 关于连接方面

TCP是面向连接的协议,必须要建立连接后才能够收发数据

UDP是面向无连接的协议,不需要复杂的握手的连接建立和回收的连接释放,即刻传送数据即可

  • 关于服务对象

TCP是一对一的两点服务,也就是说一条连接只有两个端点

UDP支持一对一,一对多,多对多的通信

  • 关于可靠性

TCP实现的是可靠传输,数据的传输可以实现无差错不丢失不重复按序到达

UDP是尽最大努力交付的,不保障具体的交付过程,但是可以在UDP之上应用层中添加相关协议,实现UDP的可靠传输

  • 拥塞控制和流量控制

TCP有拥塞控制和流量控制的极值,保证了数据传输的安全性

而UDP并没有此套机制,当网络极度拥堵的时候也不会调整网络包的发送速率,导致网络更加拥堵

  • 关于首部的开销

TCP的首部由于携带大量的字段,因此其首部会很长,基础的长度是20个字节,当使用了选项中的字段,最大可能增长到40个字节

UDP首部只有8个字节

  • 传输方式

TCP是面向字节流的,按照流式进行传输,没有边界,但是保证顺序和可靠

UDP是一个包一个包地发送的,是有边界的,但是可能会丢包和乱序

  • 分片的不同

TCP的数据长度如果大于了MSS的大小,那么就直接在传输层进行分片,目标主机收到之后,也同样会在传输层组装TCP的数据包,如果中途丢失了一个分片,那么就只需要丢失这个分片

UDP的长度如果大于了MTU大小,那么就会在IP层进行分片,目标主机收到之后,在IP层组装完数据之后,才会交付到传输层

关于应用场景

由于TCP实现的是可靠传输,可以保证数据的可靠传输,因此通常用于

  • FTP的文件传输
  • HTTPs/HTTP传输

由于UDP面向的是无连接的,它随时可以发送数据,因此使用的场景大多数是实时的场景

  • 包总量较小的通信,如DNS,SNMP
  • 视频,音频等多媒体通信
  • 广播通信

为什么UDP头部没有首部长度字段,而TCP的首部具有首部长度字段呢?

原因是 TCP 有可变长的「选项」字段,而 UDP 头部长度则是不会变化的,无需多一个字段去记录 UDP 的首部长度。

为什么 UDP 头部有「包长度」字段,而 TCP 头部则没有「包长度」字段呢?

TCP数据的长度 = IP总长度 - IP首部长度 - TCP首部长度

  • 第一种说法:因为为了网络设备硬件设计和处理方便,首部长度需要是 4 字节的整数倍。如果去掉 UDP 的「包长度」字段,那 UDP 首部长度就不是 4 字节的整数倍了,所以我觉得这可能是为了补全 UDP 首部长度是 4 字节的整数倍,才补充了「包长度」字段。
  • 第二种说法:如今的 UDP 协议是基于 IP 协议发展的,而当年可能并非如此,依赖的可能是别的不提供自身报文长度或首部长度的网络层协议,因此 UDP 报文首部需要有长度字段以供计算。

7. TCP和UDP可以使用同一个端口吗?

首先先来弄明白端口到底是用来干嘛的

在数据链路层中,通过MAC地址来寻找局域网中的主机

在网际层中通过IP地址来寻找网络中互连的主机或者路由器

在传输层中,需要通过端口进行寻址,来识别同一计算机中同时通信的不同应用程序

端口的作用就是用来区分同一个主机上不同应用程序的数据包

因此,如果这个程序同时使用了UDPTCP就可以使用同一个端口,这是一种理解,从Linux内核和协议头部控制的角度来看,当主机收到一个IP数据包之后,可以根据协议的字段知道这个包是属于UDP还是TCP,而在Linux内核中,它有两个独立的模块分别用来处理TCP包和UDP包,因此就可以将包分发给这两个不同的模块,然后通过这些模块再分发到不同的端口中。这样的话,即使是不同的程序,也可以监听相同的端口,因为内核有能力区分出来这些包应该要发送给哪个用户进程

但是要注意的是,当存在多个TCP连接监听同一个端口的时候,就会导致错误的发生了

问题在于:程序是怎么知道这个包是属于UDP还是TCP的?

当主机收到数据包之后,在IP包头的协议号中就可以知道这个数据包是TCP/UDP,根据信息然后分用,交付到不同的模块进行处理,送给TCP/UDP模块的报文根据端口号送给应用程序进程处理

如何查看端口占用的情况?

netstat -napt | grep [port]

8. TCP三次握手过程是怎么样的?

TCP的三次握手概述如下:

首先clientserver都处于一个close的状态,然后服务端被动打开,进入listen状态,监听指定端口中是否有数据发送过来。

假设现在客户端请求了一次连接,于是发送第一个SYN同步报文,这个报文的产生过程是这样的:首先客户端初始化自己的序列号,然后将报文头部的SYN设置为1,然后发送给服务端,这是第一个握手报文,客户端进入了SYN-SENT阶段,意为进入了同步报文已发送阶段

然后现在服务端正确接收到了第一个握手报文,经过解析是SYN同步报文,于是初始化服务器自己的序列号,然后发送应答报文,这个报文的产生过程是这样的:服务端初始化自己的序列号,记录为s_seq,然后做出应答,也就是将ACK设置为1,SYN设置为1,然后ack设置为c_seq +1,这是第二个握手报文,然后服务端进入了SYN_RCVD阶段,意为收到了第一个同步报文

接着第二个握手应答报文被客户端收到,经过解析是同步应答报文,此时,客户端就进入了ESTABLISHED的状态,还需要发送第三个握手报文,这个报文响应的是服务端的同步应答报文,表示服务端的同步报文已经接收到了,这个报文此时就可以携带数据了,产生过程如下:将s_seq+1,设置ACK=1,发送出去

服务端接收完毕后,也同样进入ESTABLISHED

第三个报文可以携带数据,细节问题在下面展开

9. 如何在Linux下查看TCP的状态

可以通过netstat -napt进行查看

10. 为什么是三次握手而不是一次握手?

一次握手不能保证消息的可靠性,因为从客户端发送出去的消息可能在底层产生丢包,从而导致服务端没有收到请求。同时,我们知道描述一个TCP连接的必要字段有:

  • 收发双方的地址
  • 序列号
  • 窗口大小

一次握手显然无法知道服务端的序列号和窗口大小,无法正确描述一个TCP连接。

11. 为什么是三次握手而不是两次握手?

同样的原因,我们对TCP的连接的必要字段进行描述:其中收发双方的地址在客户端就可以知道,而两次握手的情况下,如果客户端接收到来自客户端的请求,就知道了它的地址,因此首先第一个条件收发双方的地址是可以确定的了

序列号问题:然而序列号呢?服务端的序列号在客户端初始时是并不知道的,因此在这样的情况下,需要服务端发送到客户端上,如果是两次握手,那么服务端的初始序列号是否被客户端收到就不知道了,因此客户端在收到服务端的序列号之后,并且应答被正确接收之后,才可以说序列号被确定,因此两次握手是肯定不行的

窗口大小的问题与序列号相同,两次握手无法保证可靠性

除此之外,还有一个非常经典的问题:

迟到的SYN报文问题

这个问题是这样描述的

假设目前客户端请求与远程服务器连接,在发出第一个握手报文后,客户端就宕机掉了,这个握手报文又因为网络问题被阻塞在某个路由器中,然后客户端恢复了执行,再次请求连接,此时网络中就有两个请求连接的报文了

如果是两次握手,那么服务端就会全盘接收这些请求,这些的话就会导致服务端同时监听多个客户端的连接,但是只有一个是有效的,从而导致资源被浪费。这些资源可以是序列号,可以是缓存窗口,他们在操作系统中都需要预分配,如果存在大量的无效连接,可能导致内存溢出等问题,从而导致正常的服务无法执行

那么三次握手是怎么解决这个问题的呢?

基于三次握手,客户端可以确认服务端接收到的连接请求是否已经过期,举个例子:

假设第一个过期的报文,它的序列号是90,而第二个有效的报文,它的序列号是55,然后服务端给回响应的时候就会给出两个9156,客户端这边是可以检查上下文的,它检查到它应该要收到的是56,然后就会这个序列号为91的回复做出一个重置的操作, 也就是将控制位RST设置为1,最终连接终止。

12. 为什么是三次握手而不是四次握手?

这是因为三次已经能够确保对等端的TCP连接被完全初始化,因此完全不需要四次握手了。

13. 为什么每次建立 TCP 连接时,初始化的序列号都要求不一样呢?

要证明这个问题,最好的方法就是进行反证法:

假设现在序列号都是固定从0开始的

初始情况:假设客户端和服务端建立好了连接,窗口为10,于是发送了1~11的东西出去,恰好这时候,服务端宕机了但是客户端没有宕机,然后客户端服务端迟迟没有发ACK,于是超时重传,网络中有大量的1~11的数据包,然后服务端重启了,然后服务端收到这个历史数据包发现压根就没有这个连接,于是直接RST

在重新连接之后,这时候,网络滞留的那些1~11数据包恢复传送,最终导致服务端收到了错误的数据。

如果每次建立连接的客户端和服务端的初始化序号都不一样,就有大概率因为历史报文的序列号不在对方的接收窗口从而导致包被丢弃,不会接收这些错误的数据包。

并不是完全避免了(因为序列号会有回绕的问题,所以需要用时间戳的机制来判断历史报文,所谓回绕,就是说unsigned int

14. 初始序列号ISN是如何产生的?

起始 ISN 是基于时钟的,每 4 微秒 + 1,转一圈要 4.55 个小时。

RFC793 提到初始化序列号 ISN 随机生成算法:ISN = M + F(localhost, localport, remotehost, remoteport)。

  • M 是一个计时器,这个计时器每隔 4 微秒加 1。
  • F 是一个 Hash 算法,根据源 IP、目的 IP、源端口、目的端口生成一个随机数值。要保证 Hash 算法不能被外部轻易推算得出,用 MD5 算法是一个比较好的选择。

可以看到,随机数是会基于时钟计时器递增的,基本不可能会随机成一样的初始化序列号。

15. 既然 IP 层会分片,为什么 TCP 层还需要 MSS 呢?

根据以太网的规定,MAC帧的数据部分不能够超过1500个字节,MAC帧如果太长,会影响传输的效率,是人为规定的。因此当上层交付下来的数据太长的话是不合法,一般来说会IP 层进行分片,假设TCP的数据报进行分片,那么也就是一个数据报被分成了若干片,假设在IP层传输的时候,有一个分片丢失了,那么就意味着整个TCP数据报都需要重新发送,这无疑是极大的影响效率的。

因此TCP层为自己设计分片的规则,这个MSS并没有特别的限定,而是由通信双方在连接建立的时候商定的。

经过TCP层的分片之后,重发是以MSS为单位的,而不需要重发整个TCP数据包

16. 第一次握手丢失了,会发生什么?

第一次握手报文是整个连接建立的起始报文,如果该报文不起作用后面则无从谈起。

握手丢失的实际现象表现为客户端没有收到来自服务端的ACK,当长时间没有收到ACK,与普通的报文一样,都会触发一个超时重传机制,但是这个超时重传的超时时间并不是均匀的,而是以2的指数倍进行翻倍

比如说一开始等待1s,后面就是2s,4s,8s,…,在linux下,当超时重传这个syn的次数达到一定的次数,就会直接断开连接,不再发送syn报文。

17. 第二次握手丢失了,会发生什么?

目前假设第一次握手收到了,但是服务端发送的ACK+SYN丢掉了,会发生什么?

注意,此时的状况是客户端已经发出了第一次握手报文了,它在等待服务端发送的第二次握手报文,如果服务端迟迟不发来这个ACK+SYN,那么就会触发超时重传机制

然后服务端也已经发出了第二次握手报文,但是迟迟没有收到客户端发来的第三次握手报文,于是它也会触发一个超时重传的机制

在这两种情况下,会导致客户端和服务端不断的重发,最终两端同时达到最大的重试次数,连接断开。

18. 第三次握手丢失了,会发生什么?

首先搞明白第三次握手报文的性质是ACK报文,ACK报文并不会重传。

因此当第三次握手丢失了,那么这时候服务端就会不断重发SYN+ACK的报文,直到重试次数全部用完

19. 什么是SYN攻击?

SYN攻击指的是:黑客非法地伪造大量不存在的IP地址向服务端发送第一次的握手SYN报文,此时每一个第一次握手的SYN报文到来,就意味着内核有一个半连接对象的产生,内核会将这个半连接对象放入到半连接队列中,然后发出一个第二次握手报文,等待来自客户端的回复,如果客户端迟迟没有回复,那么这些连接对象就会一直放在半连接队列中,直到重试次数达到上限后被移除。如果等到了回复,那么就可以将这个半连接对象加入到全连接队列,由应用程序进行轮询或者信号通知的方式将这些连接对象通过accept()的方式拿出来。

那么问题就在于,当半连接队列被打满了之后,之后到来的SYN报文都会被丢弃,从而导致服务器无法正常工作。

那么怎么解决这个问题呢?

  • 调大网络数据包缓存区的大小,可以使得未处理的报文有位置存放而不会丢弃
  • 增大TCP的半连接队列
  • 开启cookie,cookie是一种绕开SYN半连接队列的机制,具体工作如下:这种机制下,不会创建半连接的对象,而是在接收到第一个握手报文之后,计算出一个cookie值,然后发出去,等待来自客户端的第三次握手报文,经过验证之后,当这个cookie值能够对得上,那么就直接创建连接对象,加入到accpet队列中
  • 减小重传的次数,加快无效连接的淘汰

20. TCP的四次挥手过程是怎么样的?

TCP的四次挥手预告着TCP连接的释放,由于全双工的特性,双方都可以主动断开连接,断开连接后就可以将主机中关于连接的资源释放掉,挥手的过程如下:

首先,对等端都处于ESTABLISHED的状态,然后客户端数据发送完毕的信号,于是进入了第一次挥手,这第一次挥手的报文:此时发送一个TCP首部FIN标志为1的报文,也就是第一个FIN报文,用来告诉对等端说自己希望释放连接,进入了等待状态,等啥?等待的的是服务端给客户端一个响应,进入了FIN-WAIT-1

然后服务端接收到这个请求之后,发送第二个挥手报文,这个报文用来告知客户端接收到对方想要结束连接的请求,准备开始发送收尾的数据,此时的报文格式就是ACK标志为1,代表这上一次的告知收到了,然后开始发送收尾数据。此时服务端就进入了等待状态,等啥,等待的是服务端将收尾数据发送完毕,进入了CLOSE-WAIT状态

客户端接收到这个ACK之后,就开始接收服务端的收尾数据,然后进入了等待状态,等啥?等待的是服务端的收尾数据接收完毕,进入了第二阶段的等待FIN-WAIT-2

服务端收尾数据接收完毕,然后发送断开连接的请求,此时会发送一个FIN为1的报文,进入了LAST-ACK,叫做最终的响应阶段

然后当客户端收到这个FIN为1的报文后,就发送了ACK,进入了time_wait阶段,等待2MSL后关闭连接

而服务端在收到这个ACK之后就可以关闭这个连接了

可以看出,每个方向都需要有一个FIN和ACK,主动关闭连接的才有TIME-WAIT的状态

21. 为什么是四次挥手?不是一次挥手?

与不能一次握手的道理相同,一次握手甚至不能够保证结束连接的请求给到对等端,必须采用ACK的机制才能够保证对等端收到停止连接的请求。

22. 为什么是四次挥手?不是两次挥手?

两次挥手,意味着客户端发出请求后,服务端收到了请求,那么这时候是否就可以关闭客户端了呢?

挥手与握手的过程不同,当客户端提出断开连接的时候,只是说明客户端没数据发了,但是服务端可能依然还在发数据,或者有一些收尾数据需要发送到客户机上,因此还需要在确认了客户端想要断开连接的时候,给它一段机动的时间,在这段时间内将数据全部发完。

24. 为什么是四次挥手?不是三次挥手?

注意:四次挥手在一定情况下是可以转换成三次挥手的。

在第二次握手报文和第三次握手报文的中途,实际上是给它发送数据,那么如果没有确认没有数据要发送,并且开启了TCP延迟确认机制,那么第二次和第三次的挥手就会合并。

什么叫TCP的延迟确认机制

一个TCP的首部可以长度40个字节,还需要经过底层的IP封装和MAC封装,因此发送一个没有数据的空包,是非常浪费的,而ACK包的作用就是用来应答上一次的数据的,其本身的作用不具有携带数据的功能,但是它完全能够顺便携带数据到对方的,因此就提出了TCP的延迟确认机制,其具体运作机制是这样的:

  • 当有响应数据要发送的时候,ACK会随着响应数据一起立刻发送
  • 当没有响应数据要发送的时候,ACK将会延迟一段时间,以等待是否有数据可以一起发送
  • 如果在延迟等待发送ACK期间,对方的第二个数据报文又来了,说明等太久了,此时立即发送ACK

怎么来关闭延迟确认机制

// 1 表示开启 TCP_QUICKACK,即关闭 TCP 延迟确认机制
int value = 1;
setsockopt(socketfd, IPPROTO_TCP, TCP_QUICKACK, (char*)& value, sizeof(int));

25. 为什么是四次挥手?不是五次挥手?

与不是四次握手的原因类似,因为四次挥手就已经完全能够解决连接关闭的问题了,那么就不需要更高的通信量了

26. 第一次挥手丢失了,会发生什么?

假设现在出现一种情况,第一次挥手的FIN报文因为网络问题发不出去,迟迟无法收到来自对等端的第一个ACK,因此无法进入FIN-WAIT-2的情况,在这时候,会不断重发FIN报文,直到最大的重试次数,当达到最大的重试次数,代表着这个报文无法发出去了,那么就会直接关闭连接(客户端),但是服务端有两种情况,第一种情况就是直接没收到FIN报文,此时就会一直处于一个ESTABLISHED的状态,直到保活计时器启动,然后发现对方已不可达的时候,直接关闭连接。第二种情况就是第二次挥手报文丢失了。

27. 第二次挥手丢失了,会发生什么?

当收到第一个FIN报文后,此时就会开始等待服务端的数据发送回去,此时进入了一个CLOSED-WAIT的状态,因此就会发送一个ACK回去,但是如果这个ACK丢包丢掉了,是不会触发超时重传的,重传的只会是那个FIN报文,重传这个FIN报文也有一个次数的限制,当达到这个次数之后就会直接断开与服务端的连接。

28. 第三次挥手丢失了,会发生什么?

注意,第三次挥手是在服务端发送完数据之后才发送的,因此第三次挥手的时机取决于什么时候发送完数据,那么怎么知道什么时候发送完数据了呢?就是当服务端的数据写入完毕后,调用close()函数就证明完成了。

在这时候,内核就会发出一个FIN报文,然后服务端这边进入了LAST-ACK,等待客户端返回给我ACK来结束连接

如果迟迟收不到这个ACK,那么就会重发这个FIN,然而这个重发的次数也是有限制,当达到上限的时候,那么就直接关闭连接,进入到CLOSE

然后客户端进入了FIN-WAIT-2的时间不能太长,于是根据tcp_fin_timeout的时间的超时时间,提前进入CLOSE

29. 第四次挥手丢失了,会发生什么?

第四次挥手报文的作用是用来告诉服务端,客户端关于服务端的数据发送完毕的信息已经收到,可以关闭你的连接了。这个报文的性质属于一个ACK的性质,服务端在没有收到这个ACK之前,都是处于一个LAST-ACK的状态。如果超时重传,那么重传的结果就是服务端给客户端发送一个FIN报文。

假设第四次挥手一直丢失,那么就会导致这个FIN不断重传,直到超时为止。

客户端在收到第三次挥手后,就会进入 TIME_WAIT 状态,开启时长为 2MSL 的定时器,如果途中再次收到第三次挥手(FIN 报文)后,就会重置定时器,当等待 2MSL 时长后,客户端就会断开连接。

30. 为什么TIME_WAIT的时间是2MSL

MSLMaximum Segment Lifetime,叫做报文最大生存时间,它是任何网络上存在的最长时间,超过这个时间报文将会被丢弃。

假设一个包经过最大的TTL所需要的时间为t,那么MSL必须要大于t

首先理解一个报文的生命周期:

从客户端出发->到网络中传输->到达服务端或者被丢弃,因此,一个MSL是单程票,指的是从对等端传送到另一个对等端的最大时长,那么为什么是2MSL呢?

首先理解一下,当收到第三个挥手的时候,这时候就进入了2MSL了,这时候是在等什么,是在确认对方是否收到我这个ACK然后关闭了连接,如果我提前关闭了连接,那么后面那个服务器发来的FIN我就都处理不了了,白白浪费资源

所以的话这个2MSL就是从进入TIME-WAIT开始计时的,然后这时候假如说网络很堵,ACK踩着点到服务端,可以理解为到达MSL的后一刻到达服务端,然后失效了,又正好触发超时重传,发回了一次FIN,然后FIN也踩着点到,于是就恰好等了2MSL才收到这个FIN,此时计时器重新开始计时。

那么这个过程发送了什么?就是说这个过程能够确保:本次连接中产生的所有报文段都从网络中消失了,下一个新的连接就不会出现旧的连接请求报文段了

31. 为什么需要TIME_WAIT的状态

主动发起连接关闭的一方才会有TIME-WAIT的状态,需要TIME-WAIT的状态,主要是两个原因

防止历史连接中的数据被后面相同的四元组的连接错误的接收

  • 序列号,是 TCP 一个头部字段,标识了 TCP 发送端到 TCP 接收端的数据流的一个字节,因为 TCP 是面向字节流的可靠协议,为了保证消息的顺序性和可靠性,TCP 为每个传输方向上的每个字节都赋予了一个编号,以便于传输成功后确认、丢失后重传以及在接收端保证不会乱序。序列号是一个 32 位的无符号数,因此在到达 4G 之后再循环回到 0
  • 初始序列号,在 TCP 建立连接的时候,客户端和服务端都会各自生成一个初始序列号,它是基于时钟生成的一个随机数,来保证每个连接都拥有不同的初始序列号。初始化序列号可被视为一个 32 位的计数器,该计数器的数值每 4 微秒加 1,循环一次需要 4.55 小时

  • 服务端在关闭连接之前发送的 SEQ = 301 报文,被网络延迟了。
  • 接着,服务端以相同的四元组重新打开了新连接,前面被延迟的 SEQ = 301 这时抵达了客户端,而且该数据报文的序列号刚好在客户端接收窗口内,因此客户端会正常接收这个数据报文,但是这个数据报文是上一个连接残留下来的,这样就产生数据错乱等严重的问题。

为了防止历史连接中的数据,被后面相同四元组的连接错误的接收,因此 TCP 设计了 TIME_WAIT 状态,状态会持续 2MSL 时长,这个时间足以让两个方向上的数据包都被丢弃,使得原来连接的数据包在网络中都自然消失,再出现的数据包一定都是新建立连接所产生的。

保证「被动关闭连接」的一方,能被正确的关闭

也就是说,TIME-WAIT 作用是等待足够的时间以确保最后的 ACK 能让被动关闭方接收,从而帮助其正常关闭。

如果客户端(主动关闭方)最后一次 ACK 报文(第四次挥手)在网络中丢失了,那么按照 TCP 可靠性原则,服务端(被动关闭方)会重发 FIN 报文。

假设客户端没有 TIME_WAIT 状态,而是在发完最后一次回 ACK 报文就直接进入 CLOSE 状态,如果该 ACK 报文丢失了,服务端则重传的 FIN 报文,而这时客户端已经进入到关闭状态了,在收到服务端重传的 FIN 报文后,就会回 RST 报文。

服务端收到这个 RST 并将其解释为一个错误(Connection reset by peer),这对于一个可靠的协议来说不是一个优雅的终止方式。

为了防止这种情况出现,客户端必须等待足够长的时间,确保服务端能够收到 ACK,如果服务端没有收到 ACK,那么就会触发 TCP 重传机制,服务端会重新发送一个 FIN,这样一去一来刚好两个 MSL 的时间。

32. TIME_WAIT状态过多有什么危害?

客户端和服务端的TIME_WAIT的状态过多,所造成的状态是不一样的。

如果客户端对同一个服务端的相同[IP+PORT]的组合占满了之后,那么就会导致无法再对这个服务端再次发起请求了,这是因为一个TCP连接的标识主要是通过[源IP,源PORT,目标IP,目标PORT],那么如果已经建立了连接,而且这些连接还没有进入closed状态的话,那么就不能够再建立一模一样的连接了,从而导致客户端无法连接上服务端

无法连接上服务端的根本原因是已经存在了相同的连接。

如果服务端对客户端存在太多的TIME-WAIT实际上对服务端的影响并不大,服务端对于新来的客户端请求依然可以接受,只要它的四元组有变化,那么就可以视为新的连接并且创建出来。但是我们说TCP传输数据的核心是fd以及执行相关IO操作的内核线程,如select等,因此有过多的无用连接存在的话,会导致服务端内存在大量的无用垃圾,如果不及时加以清理,轻则服务器性能下降,重则服务器宕机。

33. 如何优化TIME_WAIT?

关于如何优化,可以从TIME-WAIT状态的特点入手

  • 关于TIME-WAIT,它被设计出来的原意是为了尽快关闭对等端的连接,避免资源被浪费
  • 同时避免历史报文对现存的连接的报文传输造成影响

那么假设,如果这个TIME-WAIT不会造成资源浪费呢?就是说有这样一种场景,如果对等端又发起一个一模一样的TCP连接请求,那么就不会造成资源浪费了,不用关闭该连接,而是直接复用,这是一种优化的思路,在linux下通常用:net.ipv4.tcp_tw_reusenet.ipv4.tcp_timestamps的选项进行设置

当开启了这两个参数之后,当有新的连接请求conncet()来的时候,那么就会在内核中的TIME-WAIT的连接中,看是否有连接可以复用的,如果可以复用,那么就直接用它就行了

还有一种方式,一种基于限流的思想,当TIME-WAIT的状态严重影响系统的使用的时候,那么这时候就需要对TIME-WAIT加以限制,比如说超过一定的阈值,直接将连接给重置掉,不要再等了。在linux下通常用net.ipv4.tcp_max_tw_buckets

还有一种方式,称为SO_LINGER,那么调用close后,会立该发送一个RST标志给对端,该 TCP 连接将跳过四次挥手,也就跳过了TIME_WAIT状态,直接关闭。

但这为跨越TIME_WAIT状态提供了一个可能,不过是一个非常危险的行为,不值得提倡。

前面介绍的方法都是试图越过 TIME_WAIT状态的,这样其实不太好。虽然 TIME_WAIT 状态持续的时间是有一点长,显得很不友好,但是它被设计来就是用来避免发生乱七八糟的事情。

34. 服务器出现大量的TIME_WAIT的原因有什么?

首先TIME-WAIT是只有在主动断开连接的一方才会出现的,当服务器出现了大量的TIME-WAIT,也就是说服务器主动地断开了大量的与客户端的连接。

  • HTTP没有使用长连接
  • HTTP长连接超时掉了
  • HTTP长连接的请求数量达到上限

当HTTP没有使用长连接

目前来说,大部分的浏览器使用的HTTP协议都是HTTP/1.1,这与HTTP/1.0的区别在于,它改进了使用了长连接的技术,而不像HTTP/1.0一样,一个请求就对应的一个TCP连接,这样的消耗是很大的

先来看看长连接是如何打开的,在HTTP/1.0的时候,如果要开启长连接,那么就需要在请求头的header中添加:

Conncetion: Keep-alive

然后当服务器收到请求做出回应的时候,这个header也被添加到响应header

Connection: Keep-alive

这样的话TCP就不会产生中断,当客户端发起了另一个请求的时候,就会复用这一条连接

那么如何来关闭长连接的机制呢?

Connection: close

只要客户端和服务端任意一方的header中有这个字段,那么就无法使用这个机制了。

无论是哪一方禁用长连接,主动关闭连接的都是服务端

客户端禁用了长连接,服务端开启了长连接

此时是服务端主动关闭连接的,为什么是服务端?这是因为是请求-响应模型,请求-响应复用同一条连接的初衷是为用户的下一次请求能够减少连接的次数,那么客户端都关闭了长连接,就说明下一次请求就肯定不是复用这一条连接了呀

那客户端的行为应该是这样的,发起了一次请求,然后等待响应后直接关闭,不会再给服务端发送信息,因此关闭连接的时机就是在服务端了。

客户端开启了长连接,而服务端关闭了长连接

这个问题可以从linux内核的角度进行分析,如果客户端要求关闭,那么也就是说需要这样的过程:

客户端发起请求->服务端发送响应->等待客户端再发一次请求,发送断开连接的信号->发送信号->四次挥手结束

如果是服务端主动要求关闭

客户端发起请求->服务端发送响应->发送完毕发送断开连接的信号->四次挥手结束

从上面的例子可以看出,如果是客户端主动要求关闭,那么就意味着服务端有一段时间内是什么都没干,而且还占用了一定的资源的,但是结果却和服务端主动断开连接后的一样,那么当然选择消耗更小的方式了。

HTTP长连接的超时问题

首先超时时间的概念就是:如果双方在完成一个请求-响应之后,如果在一定的时间之内没有发送新的请求-响应,那么系统就会回收掉这条连接,它的具体实现是基于一个计时器+回调函数的模型来关闭连接的,那么此时服务端上就会出现TIME-WAIT状态的连接。

这种往往是客户端在某一个特定的时间点内涌入系统,向服务端发送HTTP请求,然后只执行了几步操作后就完成了,那么这时候服务端就会有大量的长连接产生,并且过一段时间后超时。这种属于是正常情况,还有一种情况是服务端的接收产生问题了,客户端的请求没有被服务端接收到,从而导致无法触发自动续期的机制,导致过期。

HTTP长连接的请求数量达到了上限

Web服务器通常会有一个参数来定义一条HTTP长连接上最大能够处理的请求数量,当超过了最大的限制之后,就会主动关闭连接,比如 nginx 的 keepalive_requests 这个参数,这个参数是指一个 HTTP 长连接建立之后,nginx 就会为这个连接设置一个计数器,记录这个 HTTP 长连接上已经接收并处理的客户端请求的数量。如果达到这个参数设置的最大值时,则 nginx 会主动关闭这个长连接,那么此时服务端上就会出现 TIME_WAIT 状态的连接。

keepalive_requests 参数的默认值是 100 ,意味着每个 HTTP 长连接最多只能跑 100 次请求,这个参数往往被大多数人忽略,因为当 QPS (每秒请求数) 不是很高时,默认值 100 凑合够用。

但是,对于一些 QPS 比较高的场景,比如超过 10000 QPS,甚至达到 30000 , 50000 甚至更高,如果 keepalive_requests 参数值是 100,这时候就 nginx 就会很频繁地关闭连接,那么此时服务端上就会出大量的 TIME_WAIT 状态

35. 如果已经建立了连接,但是客户端突然出现了故障?

客户端出现了故障,分为两种情况:

  • 客户端进程崩溃掉掉了,相当于调用信号函数kill -9 pid
  • 客户端主机宕机

首先进程崩溃的情况,进程崩溃在内核中是有一套处理机制的,当进程崩溃退出的时候,如果建立了网络连接,然后就会向对方发送FIN报文,而一个很关键的点是四次挥手的任务是内核完成的,因此在这种情况下,网络连接是能够正常关闭的。

然而,如果是主机宕机的话,那么就意味着没有FIN报文的回复了,服务端无法感知,会一直处于一个ESTABLISH的状态,因此在这种情况下,这条无用的连接就会一直占据着对等端的主机资源,从而导致浪费

为了避免这种情况,于是开发了 保活机制,在一段时间内,如果没有任何连接相关的活动,TCP的保活机制就会起作用,每隔一段时间,就会发送一个探测的报文,如果连续几个的报文都没有给到答复,那么就认为这条连接无用了,直接回收相应的资源

net.ipv4.tcp_keepalive_time=7200//表示保活时间是7200s 也就是2小时,如果2小时内没有任何连接相关的活动就回收
net.ipv4.tcp_keepalive_intvl=75  //每次检测间隔75s
net.ipv4.tcp_keepalive_probes=9 // 表示检测9次没有响应,就会中断本次的连接

如果开启了TCP的保活机制,那么需要考虑如下的机制:

  • 对等端是正常工作的,那么就像经典的心跳机制一样,给一个ping-pong的响应,然后保活的机制就会被重置,等待下一次的探测
  • 对等端宕机,连接对象从内存中丢失,但是能够正常处理TCP的报文,当探测报文在这时候到来的时候,它能够识别到这个报文,并且根据上下文判断是否是有效的连接,然后根据是否有效,给出不同的响应,而对于探测报文来说,它会给出一个头部控制位RST=1的报文,让对等端重置连接
  • 对等端宕机,而且没有重启,那么这时候发送的探测报文就会石沉大海,到达最大探测报文发送次数之后,连接资源就会回收掉了

比如,web 服务软件一般都会提供 keepalive_timeout 参数,用来指定 HTTP 长连接的超时时间。如果设置了 HTTP 长连接的超时时间是 60 秒,web 服务软件就会启动一个定时器,如果客户端在完成一个 HTTP 请求后,在 60 秒内都没有再发起新的请求,定时器的时间一到,就会触发回调函数来释放该连接。

36. 如果已经建立了连接,但是服务端进程崩溃会发生什么?

TCP 的连接信息是由内核维护的,所以当服务端的进程崩溃后,内核需要回收该进程的所有 TCP 连接资源,于是内核会发送第一次挥手 FIN 报文,后续的挥手过程也都是在内核完成,并不需要进程的参与,所以即使服务端的进程退出了,还是能与客户端完成 TCP 四次挥手的过程。

37. 服务器出现大量 CLOSE_WAIT 状态的原因有哪些?

CLOSE_WAIT 状态是「被动关闭方」才会有的状态,而且如果「被动关闭方」没有调用 close 函数关闭连接,那么就无法发出 FIN 报文,从而无法使得 CLOSE_WAIT 状态的连接转变为 LAST_ACK 状态。

所以,当服务端出现大量 CLOSE_WAIT 状态的连接的时候,说明服务端的程序没有调用 close 函数关闭连接

我们先来分析一个普通的 TCP 服务端的流程:

  1. 创建服务端 socket,bind 绑定端口、listen 监听端口
  2. 将服务端 socket 注册到 epoll
  3. epoll_wait 等待连接到来,连接到来时,调用 accpet 获取已连接的 socket
  4. 将已连接的 socket 注册到 epoll
  5. epoll_wait 等待事件发生
  6. 对方连接关闭时,我方调用 close

第一个原因:第 2 步没有做,没有将服务端 socket 注册到 epoll,这样有新连接到来时,服务端没办法感知这个事件,也就无法获取到已连接的 socket,那服务端自然就没机会对 socket 调用 close 函数了。

不过这种原因发生的概率比较小,这种属于明显的代码逻辑 bug,在前期 read view 阶段就能发现的了。

第二个原因: 第 3 步没有做,有新连接到来时没有调用 accpet 获取该连接的 socket,导致当有大量的客户端主动断开了连接,而服务端没机会对这些 socket 调用 close 函数,从而导致服务端出现大量 CLOSE_WAIT 状态的连接。

发生这种情况可能是因为服务端在执行 accpet 函数之前,代码卡在某一个逻辑或者提前抛出了异常。

第三个原因:第 4 步没有做,通过 accpet 获取已连接的 socket 后,没有将其注册到 epoll,导致后续收到 FIN 报文的时候,服务端没办法感知这个事件,那服务端就没机会调用 close 函数了。

发生这种情况可能是因为服务端在将已连接的 socket 注册到 epoll 之前,代码卡在某一个逻辑或者提前抛出了异常。之前看到过别人解决 close_wait 问题的实践文章,感兴趣的可以看看:一次 Netty 代码不健壮导致的大量 CLOSE_WAIT 连接原因分析(opens new window)

第四个原因:第 6 步没有做,当发现客户端关闭连接后,服务端没有执行 close 函数,可能是因为代码漏处理,或者是在执行 close 函数之前,代码卡在某一个逻辑,比如发生死锁等等。

可以发现,当服务端出现大量 CLOSE_WAIT 状态的连接的时候,通常都是代码的问题,这时候我们需要针对具体的代码一步一步的进行排查和定位,主要分析的方向就是服务端为什么没有调用 close

38. TCP的重传机制是如何工作的?

问题:TCP是如何保证可靠传输的?

要保证可靠传输,首先要明白TCP在传输的时候会产生什么问题

第一个问题就是数据被破坏了,从而导致对方接收到的包是存在错误的

第二个问题就是数据的丢包问题,从而导致TCP报文没有按序到达

第三个问题就是数据的重复,这个通常是出现在确认迟到的情况下,简单来说就是当一个数据报到达了对等端了,但是因为确认ACK包在网络中被阻塞掉了最终导致发送方再次发送了数据,最终就导致了问题的发生

第四个问题就是没有到达对等端的数据混乱的问题,比如说发送端一次性发送了多个包,但是这多个包在网络中的传递速率是不确定的,从而导致这几个包没有按照字节流的顺序到达对等端,那么它的解决办法就是在窗口内的才接受,不在窗口内的就不接受

什么是超时重传机制?

超时重传机制就是在发送方发送数据到对等端的开始,这时候内部就会开启一个计时器,如果在计时器的期间收到了对方的ACK,那么计时器就会重置,否则的话就会超时重传之前的报文,直到回复了本次的ACK为止。

什么情况下会超时重传?

第一种情况,数据没有被对等端收到,这种情况下因为没有收到数据,所以肯定就不会发送ACK

第二种情况,数据被对等端收到了,但是ACK发送了,但是ACK却丢失了,这时候就会重传

第三种情况,ACK迟到,这种情况下数据和ACK都正常存活,但是会导致了超时重传。

如何设置超时的时间

重传机制的其中一个重要点就是如何来确认超时时间。

理论:从理论上计算,超时的时间应该要略长与RTT,当超时时间远远大于RTT,就会导致响应缓慢,无法及时重传

当超时时间远远小于RTT,就会导致不必要的重传,导致的是资源的浪费

重传超时的时间称为是RTO,这是一个非常复杂的问题,因为底层的IP网络是复杂多变的,时时刻刻都有新的网络接入或者新的网络退出,绝大部分耗时都是因为路由转发所带来的,而TCP是无法控制底层的IP网络的,因此只能够根据每一次的数据传输来确认目前的RTO应该要是多少

Linux操作:

简单来说,就是当超时重传的数据,再次超时需要再次重传的时候,TCP的策略就是超时的间隔进行翻倍

为什么呢?这是因为当出现这种迹象的时候,就代表网络中出现用拥塞了,就不宜重传了

超时重传存在的问题是,超时的周期可能相对较长,可以使用快速重传机制来解决超时重发的时间等待

什么是快速重传机制?

快速重传的触发条件有:首先在超时时间之前,连续收到了三个相同的ACK,这时候就不会在超时时间到达之后才进行重传,而是立即重传数据包,这样的话就避免了当网络畅通的时候,还一直在等待超时重传的时间

快速重传机制存在什么问题?存在的问题主要是:在重传报文的时候,到底要重传什么过去?

比如说发送方发送了1 2 3 4 5 6数据报,对方收到了1 4 5 6,因此连发了4个ACK,但是因为没有收到2,因此回答的ACK都是2,那么在这样的情况下,对方并不知道后续的数据包是全部收到的还是全部丢失比如说:

如果只发数据包2,那么后续的数据包3完全可以一起发送过去的,这样的话就导致两次网络传输,浪费

如果将数据包2后面的数据全部一起发送过去,那么后续那些已经被接收到数据包就会被全部丢弃了,造成了不必要传输成本

什么是SACK方法?

有一种实现重传的机制叫做SACK,这种方式需要在TCP的首部字段添加一个SACK,它可以将已经收到的数据的信息发送给发送方,这样的话发送方就可以知道哪些数据是已经收到的了,那些数据是没有收到的了

可以看成是这样:就是一个数组,通过哪一段数组是被标记为空的,从而就可以知道哪里没有接收到了

39. TCP的滑动窗口机制是如何工作的?

为什么需要滑动窗口?没有滑动窗口会发生什么?

TCP是每发送一个数据,都要进行一次ACK,当上一个数据包收到了应答之后,就会发送下一个数据包,这种方式叫做是停止等待协议,这样的传输方式有一个缺点,就是数据的往复时间越长,就会导致通信的效率越低。

窗口:它的实现实际上是操作系统开辟的一个缓存空间,它指的是不需要等待应答,而可以继续发送数据的最大值。

窗口的实现实际上是操作系统中开辟的一个缓存空间,发送方主机在等到应答返回之前,必须在缓冲区中保留已经发送的数据,如果按期收到了应答,那么就可以将数据从缓冲区中删除。

什么叫做累计确认?

累计确认是说当接收方收到一个数据报时,并不立即发送ACK,而是等到累计到一定的数量的时候才发送ACK,这样的话一次性可以确认多个报文,这个就叫做累计确认或者累计应答的模式,同时这个模式可以有效解决确认丢失的问题,比如说当发送方没有接收到序列号为500的ACK,但是如果在超时时间之内收到了500以上的ACK的,那么发送方就不需要重传序列号为500的ACK了。

窗口大小由哪一方来决定?

TCP的头部字段中有一个字段叫做windows,也就是窗口的大小,这个字段是接收端用来告诉发送自己的缓冲区还能够接收多少数据,从而发送方就可以根据这个字段来调整。

综合来说,发送的报文的大小取决于双方窗口的最小值,当发送方的窗口较小而接收方的窗口较大的时候,此时发送数据量的瓶颈在发送方,当接收方窗口较小的时候,而发送方的窗口较大的时候,由于接收方的接收能力有限,此时发送数据量的瓶颈在接收方,最终就导致了这样一个公式windows = min{sentWindows,getWindows}

发送方的窗口是如何工作的?

对于发送的窗口而言,可以分成三个部分,已经确认的数据,已经发送但还没有发送的数据,还没有发送的数据,其中还没有发送的数据可以分为可以发送但是还没有发送的数据,不在窗口内,不能发送的数据。

它是这样工作的,有一个窗口的前沿指针,这个指针指向的是窗口的起点,有一个窗口的后沿,这个后沿决定了能够发送的最后一个字节的序号,然后后面的就都是不能够发送的字节序号。

当发送方发送完了一批数据之后,必须等待接收方的ACK,当ACK没有到来的时候,都必须要等待,不能够删除缓冲区中的数据,当接收到ACK之后,窗口开始滑动,将接收到ACK-1的最后一个字节的后面一个字节序号设置为窗口的前沿,然后窗口开始扩张,可以这意味着有新的数据可以发送了。

接收方的窗口是如何工作的?

接收方的窗口由接收方自己决定,windows,同样分为前沿后沿,只有在前沿和后沿中规定的数据才能接收,其他数据都会被忽略,也就是直接丢弃,当接收到了一部分的数据报之后,窗口就会向后滑动。

就可以分成三个部分:

  • 已经成功接收的数据,这些数据提交到上层应用等待读取
  • 还没有成功接收的数据,当这些数据到来的时候,就会触发ACK并且使得接收窗口向右滑动
  • 还没有能力接收的数据,必须等待还没有成功的数据接收到才能轮到它们

程序是如何表示发送方的四个部分的呢?

TCP的滑动窗口方案使用的是三个指针在四个传输类别中的每一个类别的指针,其中两个指针是绝对指针,它指的是特定的序列号,还有一个相对指针,需要做一个偏移

SND.WND:表示发送窗口的大小(大小是由接收方指定的)

SND.UNA:是一个绝对指针,它指向的是已经发送但是还没有收到确认的第一个字节的序列号

SND.NXT:是一个绝对指针,指向的是没有发送但是可发送范围的第一个字节的序列号。

还不能够发送的数据的字节指针:SND.UNA+SND.WND

程序是如何表示接收方的三个部分的呢

RCV.WND:表示的是接收窗口的大小,它会通告给发送方

RCV.NXT:是一个指针,表示的是期望的下一个数据字节的序列号

RCV.UNR:是一个指针,表示的是指向#4的第一个字节。

40. TCP的流量控制机制是如何工作的?

流量控制:实际上就是接收方和发送方协商窗口大小的过程,比如说当接收方的缓冲区大小不足的时候,就会及时收缩窗口,然后通告对方当前的窗口大小,使得对方能够及时调整自己发送窗口大小

例子:

首先,假设没有外界的干扰,制约的因素只有双方的缓冲区大小,初始化,客户端作为接收方,服务器作为发送方

初始化窗口为200,服务器收到请求后,发送请求确认顺便捎带上80个字节的数据,这时候这80个字节还没有收到确认,因此留在窗口中,然后接收方收到80个字节的数据后,就发送ACK报文,然后窗口向右滑动,可以接受更多的数据

在服务器还没有收到ACK之前,可以继续发送120个字节的数据

当服务器收到80个字节的数据之后,窗口向右滑动80个字节,这时候可以继续发送80个字节的新数据

当再收到120个字节的确认报文后,再向右滑动80个字节

然后这时候服务端组装好了一个160个字节的数据,发送到客户端,客户端发送ACK

操作系统的缓冲区和窗口的大小关系?

如果应用程序没有及时读取缓存,会发生什么?

简单来理解一下这个图

  • 首先是客户端和服务端的初始窗口都是360
  • 然后客户端发送了一个140字节的数据包到服务端,服务端在接收到数据之后,发送了ACK,但是因为生成上层应用程序只读取了40个字节的数据,100个字节的数据残留在缓冲区中,于是需要将这100个字节维持在窗口中,因此告诉对方:我目前可接收的窗口数据有260个字节
  • 客户端收到对方的通告,然后就知道了窗口收缩了,所以也收缩窗口为260,然后发送了180个字节的数据
  • 服务端收到这180个字节的数据, 由于上层依然没有读取数据,因此通告对方目前的窗口只有80个字节
  • 然后客户端收到这80个通告之后,就将剩余的80个字节的数据发送出去,此时还没有发送的字节数变为0
  • 服务端接收到这80个字节的数据之后,就会导致它的窗口被彻底填满,最终导致双方都不能够发送数据

  • 首先双方的窗口大小都是360,然后客户端发送140个字节的数,于是窗口减少到了220,等待对方的ACK
  • 然后收到了140个字节之后,此时有操作系统资源的紧张,操作系统强制回收了120个字节的缓冲区资源,于是此时的可用的窗口就变成了100,然后通告对方窗口变成了100
  • 当收到这个通告之前,如果发送方发送了一个新的数据包,比如说是180个字节的数据包,然后这时候就会导致这个数据包被丢弃了,本来的可用窗口大小是40,指针回滚,产生了一个负数。

为了防止这种情况的发生,TCP是不允许同时减少缓存又收缩窗口的,而是采用先收缩窗口,过段时间再减少缓存

什么叫窗口关闭?

窗口关闭就是说当接收方的接收窗口变为0了之后,就会导致发送数据的终止,从表现上来看就是窗口关闭了,不再发送数据了,这就叫做窗口的关闭

窗口关闭有什么弊端?

窗口关闭本质上流量控制出现的一种中间结果,但是如果当接收方的窗口有了更多的空间之后,那么就会向发送方通告一个窗口非0的ACK报文,来告诉对方现在的窗口不是0了,可以发送数据了,但是如果这个ACK丢掉了,那么就会导致一种死锁状态

发送方等着接收方给它一个非零窗口的ACK,接收方等着发送方给它发数据

什么是零窗口探测报文?

为了解决这样的死锁问题,TCP为每个连接设定有一个持续的计时器,只要TCP连接一方收到对方的零窗口通知,就会启动持续计时器,对方在确认这个探测报文的时候,就会给出自己现在的窗口大小,并且如果没有给出确认,就会一直重传。如果3次过后这个接收窗口还是0的话,考虑连接资源的问题,就会发起RST报文来中断连接。

糊涂窗口综合征是什么?

糊涂窗口综合征说的大概就是接收方要求一定数量的数据的时候,就发送这一定数量的数据,比如说需要5字节的,那么就发送这5字节的,主要的问题就是出现在接收方可以通告一个小的窗口,发送方可以发送小数据

怎么让接收方不通告小窗口呢?

当窗口大小小于min(MSS,缓存空间/2),就会向发送方通告窗口为0,不要再发数据了,等待当前的缓冲数据被读取之后才继续接收

怎么让发送方来避免发送小数据呢?

使用Nagle算法,主要的思路就还是延时处理

  • 等到窗口的大小>=MSS,并且数据的大小要>=MSS
  • 收到之前发送数据的ACK回包

注意,如果接收方不能满足「不通告小窗口给发送方」,那么即使开了 Nagle 算法,也无法避免糊涂窗口综合症,因为如果对端 ACK 回复很快的话(达到 Nagle 算法的条件二),Nagle 算法就不会拼接太多的数据包,这种情况下依然会有小数据包的传输,网络总体的利用率依然很低。

Nagle算法:若发送应用进程把要发送的数据逐个字节地送到TCP的发送缓存,则发送方就把第一个数据字节先发送出去,把后面到达的数据字节都缓存起来,直到对方给我发来关于第一个数据字节的ACK之后,我才把缓存中的所有数据字节组成成一个TCP报文段发送出去,当到达的数据达到发送窗口的大小的一半或者达到报文段的最大长度的时候,就马上发送一个报文段,这样做就可以提高网络的吞吐量

所以,接收方得满足「不通告小窗口给发送方」+ 发送方开启 Nagle 算法,才能避免糊涂窗口综合症

另外,Nagle 算法默认是打开的,如果对于一些需要小数据包交互的场景的程序,比如,telnet 或 ssh 这样的交互性比较强的程序,则需要关闭 Nagle 算法。

可以在 Socket 设置 TCP_NODELAY 选项来关闭这个算法(关闭 Nagle 算法没有全局参数,需要根据每个应用自己的特点来关闭)

41. TCP的拥塞控制机制是如何工作的?

为什么需要拥塞控制?拥塞控制是做什么的?

所谓的拥塞控制就是防止过多的数据注入到网络中,这样可以使得网络中的路由器或者链路不至于过载,拥塞控制所要做的都有一个前提,就是网络能够承受注目前的网络的网络负荷。

流量控制:流量控制是保证了双方在接收和发送的数据速率保持一个相对平衡的阶段,保证发送方不会发送太快导致接收方产生丢包的情况

拥塞控制:则是解决了在双方的接收和发送窗口都充足的情况下,却还是导致不断丢包的情况发生,这种情况下,如果在网络上的主机不对注入网络中的数据包进行限制,就会导致数据包越来越多,最终导致网络的速度奇慢无比,一种方法是在检测到发生拥塞的时候,就将减缓数据注入网络的速度,等网络畅通了之后才继续传输数据

拥塞控制靠什么实现的?

拥塞控制是基于拥塞窗口以及检测到拥塞后启动的那些算法来执行的,首先要搞明白,拥塞出现的原因都基本上是因为数据注入到网络中速度过快导致的,因此拥塞窗口是在发送方进行维护的。其次还有四个非常重要的算法来支持拥塞控制,拥塞控制的数据结构基础是拥塞窗口,那么执行算法就是慢启动拥塞避免拥塞发生快速恢复

拥塞窗口和发送窗口有什么关系?

拥塞窗口cwnd是发送方维护的一个变量,它会根据网络的拥塞程度来进行动态变化,它的变化原则是:只要网络中没有出现拥塞,cwnd就会变大,但是只要网络中出现了拥塞,那么cwnd就会减少

而这个cwnd也是决定发送窗口的一个因素。因此有swnd = min(cwnd,rwnd)

如何检测网络中是否出现拥塞?

网络中出现拥塞的标志通常是网络数据报在网络中被长时间的阻塞,在这种情况下通常就会导致超时重传,这个就是发生拥塞的一个标志

拥塞控制有哪些算法?

慢启动:由于加入网络时,并不知道网络中的情况如何,因此以最小的速率传递数据,在网络情况稳定了之后,因此这个过程可以看成是一个试探性的算法,一旦遇到拥塞就马上降低速率

拥塞避免

拥塞发生

快速恢复

具体说说这些算法

慢启动

慢启动的规则大概可以这样描述:首先是当初始化的时候,窗口设定为1,然后当正确收到ACK之后,这个窗口就会不断+1,因此在网络畅通的情况下,窗口会呈一个指数级的增长,那么什么时候终止呢

  • 当拥塞窗口的大小小于这个门限值的时候,就会开始启动一个慢启动的算法
  • 当拥塞窗口的大小大于等于这个门限值的时候,就会使用一个拥塞避免的算法

拥塞避免算法

拥塞避免指的是在慢启动阶段结束以后,就会开始一个线性的算法,这个算法主要就是每经过一个RTT,就会使得拥塞窗口的大小+1,它会不断地增加窗口的大小,然后就这么增长之后,网络就会慢慢进入一个拥塞的情况,于是就会开始出现丢包的现象,这时候就需要对丢失的数据包进行重传,当触发了重传的机制之后,也就进入了拥塞发生算法

拥塞发生算法

当网络出现拥塞,也就是会发生数据包的重传,重传的机制主要有两种

  • 超时重传
  • 快速重传

超时重传是如何工作的?

当发生了超时重传之后,那么就会使用拥塞发生算法,这时候就会设置sshreshcwnd

这时候的门限值就设置为cwnd的一半,cwnd重置为1,然后重新开始慢启动算法直到达到那个门限之后就变成了拥塞避免算法

这个算法对于超时现象比较敏感,也就是说当发生了超时的时候,就会导致网络传输的速率大幅度下降。

快速重传是如何工作的?

快重传是让发送方尽早地直到个别报文段发生了丢失,快重传要求接收方不要等待自己发送数据的时候才捎带确认而是应该立即发送确认。

也就是说拿到数据之后就马上发送确认,而不是携带上自己的数据到达一定阈值后才进行确认。

快速重传和快速恢复算法一般是同时使用的,这时因为是个别报文段丢失了,属于一个偶然的现象,因此没有必要说像RTO超时那么剧烈,这时候会将拥塞的窗口设置为原来的一半,然后门限设置为当前的拥塞窗口。

然后这时候并不执行慢开始算法,而是执行拥塞避免算法

为什么快速恢复算法中,cwnd设置回了ssthresh?

首先,快速恢复是拥塞发生后慢启动的优化,其首要目的仍然是降低 cwnd 来减缓拥塞,所以必然会出现 cwnd 从大到小的改变。

其次,过程2(cwnd逐渐加1)的存在是为了尽快将丢失的数据包发给目标,从而解决拥塞的根本问题(三次相同的 ACK 导致的快速重传),所以这一过程中 cwnd 反而是逐渐增大的。

42. 断点续传的功能要怎么做?

断点续传的功能可以基于切片进行实现,大概思路就是将文件进行切片,切成若干个小块,这样的话就基于使得文件的上传本来由串行上传变成了并发上传若干个小分片,这样的话可以大大减少上传的时间,另外由于是并发,传输到服务端的顺序可能会发生变化,因此还需要给每个切片记录顺序。

服务端

  • 服务端必须要能够合并切片,根据切片头部的文件名,切片的尺寸以及切片的顺序来确定如何将这些切片组装成最终的数据
  • 服务端什么时候来合并切片?

首先来说什么时候来合并切片?在每个切片的头部都携带一个当前文件的最大切片数量,当达到了这个数量之后就直接开始合并切片,也可以由客户端发起请求,让客户端来发起确认什么时候来合并切片

如何来合并切片?依据服务端中的切片顺序,可以在服务端中开辟一个链表之类的数据结构,给这些切片的下标进行排序,最终读取这条链表,以I/O流的形式写入到文件中

如何实现秒传功能

所谓秒传,就是指当服务端中能够检索到具有相同内容的文件的时候,直接返回文件的地址,而不需要再次上传

但是问题是:如何来确认这个文件是否已经上传过了,并且能够检测到这个文件的内容是否和之前的数据不同呢?

可以使用哈希的思路,对文件的内容进行哈希运算,生成一个数字签名,那么问题是,大文件要如何计算哈希?

可以采用一个异步线程,这个异步线程在客户端读文件进行扫描,扫描出来计算一个hash码,最终提交到服务端,服务端检查是否有这个哈希码,有这个哈希码的话就执行地址的返回,否则的话就继续执行文件的上传。


文章作者: 穿山甲
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 穿山甲 !
  目录