1.什么是TCP协议
TCP叫做传输控制协议,与之相关的,在传输层还有一个叫做用户数据报的协议,叫做UDP
TCP是面向连接的、可靠的、基于字节流的传输层通信协议
- 所谓面向连接:就是必须通信双方都建立起连接后才能够连接,因此其必须通过三次握手等机制来确保连接正常后才能够进行通信
- 所谓可靠:TCP通信的双方维护了一个发送窗口和接收窗口,这两个窗口说明了当前接收方发送了哪些数据,接收方接收了哪些数据,同时通过
序列号+确认号的机制不断推进传输,从而可以保证传输的有序接收,而且超时重传机制能够使得发送方将未到达的报文重新发送到对方 - 所谓字节流:用户消息通过TCP协议进行传输的时候,这个消息可能是一大段报文,它有可能会被操作系统分组为多个TCP报文,如果接收方的程序不知道消息的边界,是无法读取出一个有效的用户消息的,并且TCP报文是有序的,当前一个TCP报文没有收到的时候,即使它先收到了后面的报文,也不能够传输到应用层进行处理,重复的TCP报文会被自动丢弃
这也就是常说的原生TCP所固有的粘包和半包的症结所在
2. TCP报文段的首部格式
为了实现可靠传输,TCP采用了面向字节流的方式。但TCP在发送数据时,是从发送缓存取出一部分或全部字节并给其添加一个首部使之成为TCP报文段后进行发送。一个TCP报文段由首部和数据载荷两部分构成;TCP的全部功能都体现在它首部中各字段的作用

下面列举几个比较重要的字段
序号(seq):占32比特,取值范围[0,2^32-1],确认号增加到最后一个后,下一个确认号就又回到0。每发送一次数据,就累加一次该数据字节数的大小,它同时也可以用来解决网络包乱序的问题。
确认号(ack):占32比特,取值范围[0,2^32-1],确认号增加到最后一个后,下一个确认号就又回到0.
- 指出期望收到对方下一个TCP报文段的数据载荷的第一个字节的序号,同时也是对之前收到的所有数据的确认
- 若确认号=n,则表明到序号n-1为止的所有数据都已正确接收,期望接收序号为n的数据。
控制位:主要有4个
- 确认标志位ACK:这个位为1时,表示上一次对方发送过来的数据包已经正确接收,是一个应答的信号。TCP规定,在连接建立后所有传送的TCP报文段都必须把ACK置1.
- 同步标志位SYN:当这个位为1时,表示希望与对方建立连接,在此阶段初始化序列号
- 复位标志位RST:当这个位为1时,表示TCP连接出现了异常必须释放连接,然后再重新建立连接。RST置1还用来拒绝一个非法的报文段或拒绝打开一个TCP连接。
- 终止标志位FIN:当这个位为1时,表示希望与对方断开连接,当数据交换完毕后,通信双方的主机就可以相互交换
FIN=1的TCP段
3. 如何确定唯一一个TCP
一个TCP连接的四元组表示如下:{源地址,源端口,目标地址,目标端口}
在网络传输的过程中, 源地址和目的地址的字段是在IP头部中的,而源端口和目的端口的字段是在TCP头部中的
有一个 IP 的服务端监听了一个端口,它的 TCP 的最大连接数是多少?
服务端通常固定在某个本地端口上进行监听,等待客户端的连接请求,它会存在一个监听socket
那么根据TCP的唯一标识,可以看出连接数 = 客户端的IP*客户端的端口数,理论上最多可以到2^48(服务端的IP和端口号是固定的)。
然而,每一个连接代表着一个fd,fd是受系统管控的资源,会受到以下的因素影响
文件描述符的限制,每一个TCP连接都是一个文件,如果文件描述符被占满了,会发生
Too Many open files。Linux对可以打开的文件描述符的数量做了三个方面的限制- 系统级:当前系统可以打开的最大数量,可以通过
cat /proc/sys/fs/file-max进行查看
- 系统级:当前系统可以打开的最大数量,可以通过
用户级:指定用户可打开的最大数量,可以通过
cat /etc/security/limits.conf进行查看- 进程级:单个进程可打开的最大数量,通过
cat /proc/sys/fs/nr_open查看;
- 进程级:单个进程可打开的最大数量,通过
内存限制,每个 TCP 连接都要占用一定内存,操作系统的内存是有限的,如果内存资源被占满后,会发生 OOM
4.UDP和TCP有什么区别?在应用上有什么异同?
UDP和TCP的最重要的区别在于:UDP并不提供复杂的控制机制,它是利用IP提供面向无连接的通信服务
我们从UDP的头部就可见端倪,TCP的首部除了有描述接收双方的源端口号和目的端口号之外,还有序列号、确认号、ACK、SYN、FIN、RST等复杂的控制字段,因此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 报文首部需要有长度字段以供计算。
5.TCP和UDP可以使用同一个端口吗?
首先先来弄明白端口到底是用来干嘛的
在数据链路层中,通过MAC地址来寻找局域网中的主机
在网际层中通过IP地址来寻找网络中互连的主机或者路由器
在传输层中,需要通过端口进行寻址,来识别同一计算机中同时通信的不同应用程序
端口的作用就是用来区分同一个主机上不同应用程序的数据包
因此,如果这个程序同时使用了UDP和TCP就可以使用同一个端口,这是一种理解,从Linux内核和协议头部控制的角度来看,当主机收到一个IP数据包之后,可以根据协议的字段知道这个包是属于UDP还是TCP,而在Linux内核中,它有两个独立的模块分别用来处理TCP包和UDP包,因此就可以将包分发给这两个不同的模块,然后通过这些模块再分发到不同的端口中。这样的话,即使是不同的程序,也可以监听相同的端口,因为内核有能力区分出来这些包应该要发送给哪个用户进程
但是要注意的是,当存在多个TCP连接监听同一个端口的时候,就会导致错误的发生了
问题在于:程序是怎么知道这个包是属于UDP还是TCP的?
当主机收到数据包之后,在IP包头的协议号中就可以知道这个数据包是TCP/UDP,根据信息然后分用,交付到不同的模块进行处理,送给TCP/UDP模块的报文根据端口号送给应用程序进程处理
6.TCP的三次握手过程

为什么是三次握手而不是两次?
这是为了防止客户端迟到的SYN报文又到了服务器,因而产生错误。
我们假设两次握手就可以建立连接。假设目前客户端在发出第一个握手报文,但是这个报文因为网络问题被阻塞在某个路由器中,客户端因为没有收到确认,又重传了一次,后面收到了确认,建立了连接数据传输完就释放了连接。后面这个迟到的报文来了,服务器以为这是一个新的请求连接,然后对它确认,建立了连接,并且一直等客户端发送数据,但是客户端不理睬,就这样服务器的资源被白白浪费了。这些资源可以是序列号,可以是缓存窗口,他们在操作系统中都需要预分配,如果存在大量的无效连接,可能导致内存溢出等问题,从而导致正常的服务无法执行。
那么三次握手是怎么解决这个问题的呢?
基于三次握手,客户端可以确认服务端接收到的连接请求是否已经过期,举个例子:
假设第一个过期的报文,它的序列号是90,而第二个有效的报文,它的序列号是55,然后服务端给回响应的时候就会给出两个91和56,客户端这边是可以检查上下文的,它检查到它应该要收到的是56,然后就会这个序列号为91的回复做出一个重置的操作, 也就是将控制位RST设置为1,最终连接终止。
为什么每次建立 TCP 连接时,初始化的序列号都要求不一样呢?
为了防止历史报文被下一个相同的四元组的连接接受。
假设现在序列号都是固定从0开始的
初始情况:假设客户端和服务端建立好了连接,窗口为10,于是发送了1~11的东西出去,恰好这时候,服务端宕机了但是客户端没有宕机,然后客户端看到服务端迟迟没有发ACK,于是超时重传,网络中有大量的1~11的数据包。然后服务端重启了,之前与客户端建立的连接也消失了,于是收到客户端的历史数据包的时候就会发送RST报文。
在重新连接之后,历史数据包正好抵达了服务端,刚好该数据包的序列号正好在服务端的接收窗口,服务端就这些数据,就会造成数据错乱。
第一次握手丢失了,会发生什么?
第一次握手报文是整个连接建立的起始报文,如果该报文不起作用后面则无从谈起。
握手丢失的实际现象表现为客户端没有收到来自服务端的ACK,当长时间没有收到ACK,与普通的报文一样,都会触发一个超时重传机制,但是这个超时重传的超时时间并不是均匀的,而是以2的指数倍进行翻倍
比如说一开始等待1s,后面就是2s,4s,8s,…,在linux下,当超时重传这个syn的次数达到一定的次数,就会直接断开连接,不再发送syn报文。
第二次握手丢失了,会发生什么?
目前假设第一次握手收到了,但是服务端发送的ACK+SYN丢掉了,会发生什么?
注意,此时的状况是客户端已经发出了第一次握手报文了,它在等待服务端发送的第二次握手报文,如果服务端迟迟不发来这个ACK+SYN,那么就会触发超时重传机制
然后服务端也已经发出了第二次握手报文,但是迟迟没有收到客户端发来的第三次握手报文,于是它也会触发一个超时重传的机制
在这两种情况下,会导致客户端和服务端不断的重发,最终两端同时达到最大的重试次数,连接断开。
第三次握手丢失了,会发生什么?
首先搞明白第三次握手报文的性质是ACK报文,ACK报文并不会重传。
因此当第三次握手丢失了,那么这时候服务端就会不断重发SYN+ACK的报文,直到重试次数全部用完
什么是SYN攻击?
SYN攻击指的是:黑客非法地伪造大量不存在的IP地址向服务端发送第一次的握手SYN报文,此时每一个第一次握手的SYN报文到来,就意味着内核有一个半连接对象的产生,内核会将这个半连接对象放入到半连接队列中,然后发出一个第二次握手报文,等待来自客户端的回复,如果客户端迟迟没有回复,那么这些连接对象就会一直放在半连接队列中,直到重试次数达到上限后被移除。如果等到了回复,那么就可以将这个半连接对象加入到全连接队列,由应用程序进行轮询或者信号通知的方式将这些连接对象通过accept()的方式拿出来。
那么问题就在于,当半连接队列被打满了之后,之后到来的
SYN报文都会被丢弃,从而导致服务器无法正常工作。
那么怎么解决这个问题呢?
- 调大
网络数据包缓存区的大小,可以使得未处理的报文有位置存放而不会丢弃 - 增大
TCP的半连接队列 - 开启cookie,cookie是一种绕开
SYN半连接队列的机制,具体工作如下:这种机制下,不会创建半连接的对象,而是在接收到第一个握手报文之后,计算出一个cookie值,然后发出去,等待来自客户端的第三次握手报文,经过验证之后,当这个cookie值能够对得上,那么就直接创建连接对象,加入到accpet队列中 - 减小重传的次数,加快无效连接的淘汰
7.既然 IP 层会分片,为什么 TCP 层还需要 MSS 呢?

根据以太网的规定,MAC帧的数据部分不能够超过1500个字节,MAC帧如果太长,会影响传输的效率,是人为规定的。因此当上层交付下来的数据太长的话是不合法,一般来说会IP 层进行分片。假设有一份数据,较大,且在TCP层不分段,如果这份数据在发送的过程中出现丢包现象,TCP会发生重传,那么重传的就是这一大份数据这无疑是极大的影响效率的。
因此TCP层为自己设计分片的规则,这个MSS并没有特别的限定,而是由通信双方在连接建立的时候商定的。
经过TCP层的分片之后,重发是以MSS为单位的,而不需要重发整个TCP数据包
8.TCP的四次挥手

TCP的四次挥手预告着TCP连接的释放,由于全双工的特性,双方都可以主动断开连接,断开连接后就可以将主机中关于连接的资源释放掉。
可以看出,每个方向都需要有一个FIN和ACK,主动关闭连接的才有TIME-WAIT的状态
为什么是四次挥手,不是一次挥手?
与不能一次握手的道理相同,一次握手甚至不能够保证结束连接的请求给到对等端,必须采用ACK的机制才能够保证对等端收到停止连接的请求。
为什么是四次挥手,不是两次挥手?
两次挥手,意味着客户端发出请求后,服务端收到了请求。挥手与握手的过程不同,当客户端提出断开连接的时候,只是说明客户端没数据发了,但是服务端可能依然还在发数据,或者有一些收尾数据需要发送到客户机上,因此在确认了客户端想要断开连接的时候,还需要给它一段机动的时间,在这段时间内将数据全部发完。
为什么是四次挥手,不是三次挥手?
注意:四次挥手在一定情况下是可以转换成三次挥手的。
在第二次握手报文和第三次握手报文的中途,实际上是给客户端发送数据,那么如果确认没有数据要发送,并且开启了TCP延迟确认机制,那么第二次和第三次的挥手就会合并。
什么叫TCP的延迟确认机制
一个TCP的首部长度40个字节,还需要经过底层的IP封装和MAC封装,因此发送一个没有数据的空包,是非常浪费的,而ACK包的作用就是用来应答上一次的数据的,其本身的作用不具有携带数据的功能,但是它完全能够顺便携带数据到对方的,因此就提出了TCP的延迟确认机制,其具体运作机制是这样的:
- 当有响应数据要发送的时候,ACK会随着响应数据一起立刻发送
- 当没有响应数据要发送的时候,ACK将会延迟一段时间,以等待是否有数据可以一起发送
- 如果在延迟等待发送ACK期间,对方的第二个数据报文又来了,说明等太久了,此时立即发送ACK
第一次挥手丢失了会发生什么?
假设客户端发出了第一次挥手的FIN报文,但是服务器没有收到。那么接下来:
客户端:会不断重发FIN报文,直到最大的重试次数,当达到最大的重试次数,代表着这个报文无法发出去了,那么就会直接关闭连接
服务器:直接没收到FIN报文,此时就会一直处于一个ESTABLISHED的状态,直到保活计时器启动,然后发现对方已不可达的时候,直接关闭连接
什么是保活计时器?
服务器每收到一次TCP客户进程的数据,就重新设置并启动保活计时器(2小时定时)。
若保活计时器定时周期内未收到TCP客户进程发来的数据,则当保活计时器到时后,TCP服务器进程就向TCP客户进程发送一个探测报文段,以后则每隔75秒钟发送一次。若一连发送10个探测报文段后仍无TCP客户进程响应,TCP服务器进程就认为TCP客户进程所在主机出了故障,接着就关闭这个连接。
第二次挥手丢失了会发生什么?
当服务端收到客户端发送的FIN后,就会回复一个ACK(第二次挥手)。此时服务端就会进入到CLOSE_WAIT状态。
假如这个ACK丢失了,客户端就会以为自己发送的FIN报文丢失了,便会触发超时重传机制,进行重传FIN报文。如果超过重传次数后,还没有收到服务端的第二次挥手,那么客户端就会断开连接。
第三次挥手丢失了会发生什么?
注意,第三次挥手是在服务端发送完数据之后才发送的,因此第三次挥手的时机取决于什么时候发送完数据,那么怎么知道什么时候发送完数据了呢?就是当服务端的数据写入完毕后,调用close()函数就证明完成了。
在这时候,内核就会发出一个FIN报文,然后服务端这边进入了LAST-ACK,等待客户端返回给我ACK来结束连接
如果迟迟收不到这个ACK,那么就会重发这个FIN,然而这个重发的次数也是有限制,当达到上限的时候,那么就直接关闭连接,进入到CLOSED
然后客户端进入了FIN-WAIT-2的时间不能太长,于是根据tcp_fin_timeout的时间的超时时间,提前进入CLOSED
第四次挥手丢失了会发生什么?
第四次挥手报文的作用是用来告诉服务端,可以关闭你的连接了。这个报文的性质属于一个ACK的性质,服务端在没有收到这个ACK之前,都是处于一个LAST-ACK的状态。如果超时就重传FIN报文。
假设第四次挥手一直丢失,那么就会导致这个FIN不断重传,直到超过重试次数为止。
客户端在收到第三次挥手后,就会进入 TIME_WAIT 状态,开启时长为 2MSL 的定时器,如果途中再次收到第三次挥手(FIN 报文)后,就会重置定时器,当等待 2MSL 时长后,客户端就会断开连接。
为什么TIME_WAIT的时间是2MSL
MSL是Maximum Segment Lifetime,叫做报文最大生存时间,它是任何网络上存在的最长时间,超过这个时间报文将会被丢弃。
假设一个包经过最大的TTL所需要的时间为t,那么MSL必须要大于t
首先理解一个报文的生命周期:
从客户端出发->到网络中传输->到达服务端或者被丢弃,因此,一个MSL是单程票,指的是从对等端传送到另一个对等端的最大时长,那么为什么是2MSL呢?
首先理解一下,当收到第三个挥手的时候,这时候就进入了2MSL了,这时候是在等什么,是在确认对方是否收到我这个ACK然后关闭了连接,如果我提前关闭了连接,那么后面那个服务器发来的FIN我就都处理不了了,白白浪费资源
所以的话这个2MSL就是从进入TIME-WAIT开始计时的,然后这时候假如说网络很堵,ACK踩着点到服务端,可以理解为到达MSL的后一刻到达服务端,然后失效了,又正好触发超时重传,发回了一次FIN,然后FIN也踩着点到,于是就恰好等了2MSL才收到这个FIN,此时计时器重新开始计时。
就是说这个过程能够确保:本次连接中产生的所有报文段都从网络中消失了,下一个新的连接就不会出现旧的连接请求报文段了
为什么需要TIME_WAIT的状态
第一:为了保证客户端发出的最后一个ACK报文能够到达服务器。如果没有TIME_WAIT状态,客户端发送完ACK就关闭了。假如这个ACK报文丢失了,服务器就会超时重传FIN+ACK报文,因为这个时候客户端已经关闭了,它不会再发送一个ACK报文,这样服务器就没法正常关闭。
第二:防止已失效的连接请求报文段出现在本连接中。客户端在发送完最后一个ACK报文后,再经过2MSL,就可以使本连接持续时间内所产生的所有报文段都从网络中消失。这样下一个新的连接中不会出现这种旧的连接请求报文段。
9.TIME_WAIT状态过多有什么危害?
客户端和服务端的TIME_WAIT的状态过多,所造成的状态是不一样的。
如果客户端对同一个服务端的相同[IP+PORT]的组合占满了之后,那么就会导致无法再对这个服务端再次发起请求了,这是因为一个TCP连接的标识主要是通过[源IP,源PORT,目标IP,目标PORT],那么如果已经建立了连接,而且这些连接还没有进入closed状态的话,那么就不能够再建立一模一样的连接了,从而导致客户端无法连接上服务端
无法连接上服务端的根本原因是已经存在了相同的连接。
如果服务端对客户端存在太多的TIME-WAIT实际上对服务端的影响并不大,服务端对于新来的客户端请求依然可以接受,只要它的四元组有变化,那么就可以视为新的连接并且创建出来。但是我们说TCP传输数据的核心是fd以及执行相关IO操作的内核线程,如select等,因此有过多的无用连接存在的话,会导致服务端内存在大量的无用垃圾,如果不及时加以清理,轻则服务器性能下降,重则服务器宕机。
10.如何优化TIME_WAIT?
关于如何优化,可以从TIME-WAIT状态的特点入手
- 关于
TIME-WAIT,它被设计出来的原意是为了尽快关闭对等端的连接,避免资源被浪费 - 同时避免历史报文对现存的连接的报文传输造成影响
那么假设,如果这个TIME-WAIT不会造成资源浪费呢?就是说有这样一种场景,如果对等端又发起一个一模一样的TCP连接请求,那么就不会造成资源浪费了,不用关闭该连接,而是直接复用,这是一种优化的思路,在linux下通常用:net.ipv4.tcp_tw_reuse和net.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 状态持续的时间是有一点长,显得很不友好,但是它被设计来就是用来避免发生乱七八糟的事情。
11.服务器出现大量的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 状态。
12.如果已经建立了连接,但是客户端突然出现了故障?
客户端出现了故障,分为两种情况:
- 客户端进程崩溃掉掉了,相当于调用信号函数
kill -9 pid - 客户端主机宕机
首先进程崩溃的情况,进程崩溃在内核中是有一套处理机制的,当进程崩溃退出的时候,如果建立了网络连接,然后就会向对方发送FIN报文,而一个很关键的点是四次挥手的任务是内核完成的,因此在这种情况下,网络连接是能够正常关闭的。
然而,如果是主机宕机的话,那么就意味着没有FIN报文的回复了,服务端无法感知,会一直处于一个ESTABLISH的状态,直到报活计时器超时,断开这个连接
13.如果已经建立了连接,但是服务端突然出现了故障?
TCP 的连接信息是由内核维护的,所以当服务端的进程崩溃后,内核需要回收该进程的所有 TCP 连接资源,于是内核会发送第一次挥手 FIN 报文,后续的挥手过程也都是在内核完成,并不需要进程的参与,所以即使服务端的进程退出了,还是能与客户端完成 TCP 四次挥手的过程。
14.TCP的重传机制是如何工作的?
TCP是如何保证可靠传输的?
要保证可靠传输,首先要明白TCP在传输的时候会产生什么问题
第一个问题就是数据被破坏了,从而导致对方接收到的包是存在错误的
第二个问题就是数据的丢包问题,从而导致TCP报文没有按序到达
第三个问题就是数据的重复,这个通常是出现在确认迟到的情况下,简单来说就是当一个数据报到达了对等端了,但是因为确认ACK包在网络中被阻塞掉了最终导致发送方再次发送了数据,最终就导致了问题的发生
第四个问题就是没有到达对等端的数据混乱的问题,比如说发送端一次性发送了多个包,但是这多个包在网络中的传递速率是不确定的,从而导致这几个包没有按照字节流的顺序到达对等端,那么它的解决办法就是在窗口内的才接受,不在窗口内的就不接受。
什么是超时重传机制?
超时重传机制就是在发送方发送数据到对等端的开始,这时候内部就会开启一个计时器,如果在计时器的期间收到了对方的ACK,那么计时器就会重置,否则的话就会超时重传之前的报文,直到回复了本次的ACK为止。
什么情况下会超时重传?
第一种情况,数据没有被对等端收到,这种情况下因为没有收到数据,所以肯定就不会发送ACK
第二种情况,数据被对等端收到了,但是ACK发送了,但是ACK却丢失了,这时候就会重传
第三种情况,ACK迟到,这种情况下数据和ACK都正常存活,但是会导致了超时重传。
如何设置超时的时间
重传机制的其中一个重要点就是如何来确认超时时间。
理论:从理论上计算,超时的时间应该要略长与RTT,当超时时间远远大于RTT,就会导致响应缓慢,无法及时重传
当超时时间远远小于RTT,就会导致不必要的重传,导致的是资源的浪费
重传超时的时间称为是RTO,这是一个非常复杂的问题,因为底层的IP网络是复杂多变的,时时刻刻都有新的网络接入或者新的网络退出,绝大部分耗时都是因为路由转发所带来的,而TCP是无法控制底层的IP网络的,因此只能够根据每一次的数据传输来确认目前的RTO应该要是多少
简单来说,就是当超时重传的数据,再次超时需要再次重传的时候,TCP的策略就是超时的间隔进行翻倍
为什么呢?这是因为当出现这种迹象的时候,就代表网络中出现用拥塞了,就不宜重传了
超时重传存在的问题是,超时的周期可能相对较长,可以使用快速重传机制来解决超时重发的时间等待
什么是快速重传机制?
快速重传的触发条件有:首先在超时时间之前,连续收到了三个相同的ACK,这时候就不会在超时时间到达之后才进行重传,而是立即重传数据包,这样的话就避免了当网络畅通的时候,还一直在等待超时重传的时间
快速重传机制存在什么问题?存在的问题主要是:在重传报文的时候,到底要重传什么过去?
比如说发送方发送了1 2 3 4 5 6数据报,对方收到了1 4 5 6,因此连发了4个ACK,但是因为没有收到2,因此回答的ACK都是2,那么在这样的情况下,对方并不知道后续的数据包是全部收到的还是全部丢失比如说:
如果只发数据包2,那么后续的数据包3完全可以一起发送过去的,这样的话就导致两次网络传输,浪费
如果将数据包2后面的数据全部一起发送过去,那么后续那些已经被接收到数据包就会被全部丢弃了,造成了不必要传输成本
15.什么是流量控制
一般来说,我们总希望数据传输的快一些。但如果发送方把数据发送的过快,接收方就可能来不及接收,这就会造成数据的丢失。
所谓流量控制(flow control)就是让发送方的发送速率不要太快,要让接收方来得及接收。
利用滑动窗口机制可以很方便地在TCP连接上实现对发送方的流量控制。TCP接收方利用自己的接收窗口的大小来限制发送方的发送窗口的大小。TCP发送方收到接收方的零窗口通知后,应启动持续计时器。持续计时器超时后,向接收方发送零窗口探测报文。
16.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的第一个字节。
17.TCP的拥塞控制机制是如何工作的?
为什么需要拥塞控制?拥塞控制是做什么的?
所谓的拥塞控制就是防止过多的数据注入到网络中,这样可以使得网络中的路由器或者链路不至于过载,拥塞控制所要做的都有一个前提,就是网络能够承受注目前的网络的网络负荷。
流量控制:流量控制是保证了双方在接收和发送的数据速率保持一个相对平衡的阶段,保证发送方不会发送太快导致接收方产生丢包的情况
拥塞控制:则是解决了在双方的接收和发送窗口都充足的情况下,却还是导致不断丢包的情况发生,这种情况下,如果在网络上的主机不对注入网络中的数据包进行限制,就会导致数据包越来越多,最终导致网络的速度奇慢无比,一种方法是在检测到发生拥塞的时候,就将减缓数据注入网络的速度,等网络畅通了之后才继续传输数据
拥塞控制靠什么实现的?
拥塞控制是基于拥塞窗口以及检测到拥塞后启动的那些算法来执行的,首先要搞明白,拥塞出现的原因都基本上是因为数据注入到网络中速度过快导致的,因此拥塞窗口是在发送方进行维护的。其次还有四个非常重要的算法来支持拥塞控制,拥塞控制的数据结构基础是拥塞窗口,那么执行算法就是慢启动、拥塞避免、拥塞发生、快速恢复
拥塞窗口和发送窗口有什么关系?
拥塞窗口cwnd是发送方维护的一个变量,它会根据网络的拥塞程度来进行动态变化,它的变化原则是:只要网络中没有出现拥塞,cwnd就会变大,但是只要网络中出现了拥塞,那么cwnd就会减少
而这个cwnd也是决定发送窗口的一个因素。因此有swnd = min(cwnd,rwnd)
如何检测网络中是否出现拥塞?
网络中出现拥塞的标志通常是网络数据报在网络中被长时间的阻塞,在这种情况下通常就会导致超时重传,这个就是发生拥塞的一个标志
拥塞控制有哪些算法?
慢启动:由于加入网络时,并不知道网络中的情况如何,因此以最小的速率传递数据,在网络情况稳定了之后,因此这个过程可以看成是一个试探性的算法,一旦遇到拥塞就马上降低速率
拥塞避免拥塞发生快速恢复
具体说说这些算法
慢启动
慢启动的规则大概可以这样描述:首先是当初始化的时候,窗口设定为1,然后当正确收到ACK之后,这个窗口就会不断+1,因此在网络畅通的情况下,窗口会呈一个指数级的增长,那么什么时候终止呢
- 当拥塞窗口的大小小于这个门限值的时候,就会开始启动一个慢启动的算法
- 当拥塞窗口的大小大于等于这个门限值的时候,就会使用一个拥塞避免的算法
拥塞避免算法
拥塞避免指的是在慢启动阶段结束以后,就会开始一个线性的算法,这个算法主要就是每经过一个RTT,就会使得拥塞窗口的大小+1,它会不断地增加窗口的大小,然后就这么增长之后,网络就会慢慢进入一个拥塞的情况,于是就会开始出现丢包的现象,这时候就需要对丢失的数据包进行重传,当触发了重传的机制之后,也就进入了拥塞发生算法
拥塞发生算法
当网络出现拥塞,也就是会发生数据包的重传,重传的机制主要有两种
- 超时重传
- 快速重传
超时重传是如何工作的?
当发生了超时重传之后,那么就会使用拥塞发生算法,这时候就会设置sshresh和cwnd
这时候的门限值就设置为cwnd的一半,cwnd重置为1,然后重新开始慢启动算法直到达到那个门限之后就变成了拥塞避免算法
这个算法对于超时现象比较敏感,也就是说当发生了超时的时候,就会导致网络传输的速率大幅度下降。
快速重传是如何工作的?
快重传是让发送方尽早地直到个别报文段发生了丢失,快重传要求接收方不要等待自己发送数据的时候才捎带确认而是应该立即发送确认。
也就是说拿到数据之后就马上发送确认,而不是携带上自己的数据到达一定阈值后才进行确认。
快速重传和快速恢复算法一般是同时使用的,这时因为是个别报文段丢失了,属于一个偶然的现象,因此没有必要说像RTO超时那么剧烈,这时候会将拥塞的窗口设置为原来的一半,然后门限设置为当前的拥塞窗口。
然后这时候并不执行慢开始算法,而是执行拥塞避免算法
为什么快速恢复算法中,cwnd设置回了ssthresh?
首先,快速恢复是拥塞发生后慢启动的优化,其首要目的仍然是降低 cwnd 来减缓拥塞,所以必然会出现 cwnd 从大到小的改变。
其次,过程2(cwnd逐渐加1)的存在是为了尽快将丢失的数据包发给目标,从而解决拥塞的根本问题(三次相同的 ACK 导致的快速重传),所以这一过程中 cwnd 反而是逐渐增大的。
18.断点续传的功能要怎么做?
断点续传的功能可以基于切片进行实现,大概思路就是将文件进行切片,切成若干个小块,这样的话就基于使得文件的上传本来由串行上传变成了并发上传若干个小分片,这样的话可以大大减少上传的时间,另外由于是并发,传输到服务端的顺序可能会发生变化,因此还需要给每个切片记录顺序。
服务端
- 服务端必须要能够合并切片,根据切片头部的文件名,切片的尺寸以及切片的顺序来确定如何将这些切片组装成最终的数据
- 服务端什么时候来合并切片?
首先来说什么时候来合并切片?在每个切片的头部都携带一个当前文件的最大切片数量,当达到了这个数量之后就直接开始合并切片,也可以由客户端发起请求,让客户端来发起确认什么时候来合并切片
如何来合并切片?依据服务端中的切片顺序,可以在服务端中开辟一个链表之类的数据结构,给这些切片的下标进行排序,最终读取这条链表,以I/O流的形式写入到文件中
如何实现秒传功能
所谓秒传,就是指当服务端中能够检索到具有相同内容的文件的时候,直接返回文件的地址,而不需要再次上传
但是问题是:如何来确认这个文件是否已经上传过了,并且能够检测到这个文件的内容是否和之前的数据不同呢?
可以使用哈希的思路,对文件的内容进行哈希运算,生成一个数字签名,那么问题是,大文件要如何计算哈希?
可以采用一个异步线程,这个异步线程在客户端读文件进行扫描,扫描出来计算一个hash码,最终提交到服务端,服务端检查是否有这个哈希码,有这个哈希码的话就执行地址的返回,否则的话就继续执行文件的上传。
19.TCP的粘包问题如何解决?
解决方案一:特殊字符作为边界
我们可以在两个用户消息之间插入一个特殊的字符串,这样接收方在接收数据时,读到了这个特殊字符,就把认为已经读完一个完整的消息。
HTTP 是一个非常好的例子。HTTP 通过设置回车符、换行符作为 HTTP 报文协议的边界。
有一点要注意,这个作为边界点的特殊字符,如果刚好消息内容里有这个特殊字符,我们要对这个字符转义,避免被接收方当作消息的边界点而解析到无效的数据。
解决方案二:自定义消息结构
我们可以自定义一个消息结构,由包头和数据组成,其中包头包是固定大小的,而且包头里有一个字段来说明紧随其后的数据有多大。
比如这个消息结构体,首先 4 个字节大小的变量来表示数据长度,真正的数据则在后面。
struct {
u_int32_t message_length;
char message_data[];
} message;
当接收方接收到包头的大小(比如 4 个字节)后,就解析包头的内容,于是就可以知道数据的长度,然后接下来就继续读取数据,直到读满数据的长度,就可以组装成一个完整到用户消息来处理了。