目录
客户端 TCP 连接 TIME_WAIT 状态过多,会导致端口资源耗尽而无法建立新的连接吗?
TCP基础
TCP概念
TCP全名是Transmission Control Protocol,传输控制协议
往往八股文中都会说它的三个特性:面向连接、可靠、字节流
1. 面向连接:建立一对一连接,面向这一条连接产生通信,不能像UDP那样一对多
- 用Socket(IP地址+端口号)寻找连接对象
2. 可靠:TCP能够保证报文的可到达、数据完整、数据正确
3. 字节流:数据通过TCP传输时,消息可以被分组成为多个报文,TCP报文有序,当「前一个」TCP 报文没有收到的时候,即使它先收到了后面的 TCP 报文,那么也不能扔给应用层去处理,同时对「重复」的 TCP 报文会自动丢弃。
- 用序列号来解决乱序问题
虽然序列号解决了乱序问题,但是粘包问题还没解决(TCP字节流传输消息没有确定边界,接收方无法判断断点)
- 解决方法有很多:1. 固定消息长度;2. 特殊字符作为边界(注意转义);3. 自定义消息结构,如同TCP IP一样,有头部和body
此外,我还想加入一个重要的概念:流量控制
TCP还有一个重要的特性就是它可以进行流量控制!
- 用窗口大小来做流量控制
TCP头部内容
TCP头部内容有源端口号、目的端口号、序列号Seq、确认应答号Ack、头部长度、保留的、控制位、窗口大小、校验和、紧急指针、选项、数据
其中比较重要的是序列号、确认应答号、控制位
1. 序列号:在建立连接时由计算机生成的随机数作为其初始值,通过 SYN 包传给接收端主机,每发送一次数据,就「累加」一次该「数据字节数」的大小。用来解决网络包乱序问题。
2. 确认应答号:希望接收到的下一个包的序列号,发送端收到这个后认为该序号一千的数据已经被正常接收,可以用来解决丢包问题
3. 控制位:ACK、RST、SYN、FIN
- ACK:if(ACK==1) -> 确认应答字段有效,其实,除了建立连接最初的SYN包,其他的包ACK都是1
- RST:if(RST==1) -> 表示要断开该TCP连接
- SYN:if(SYN==1) -> 请求建立连接
- FIN:if(FIN==1) -> 表示此后不会再有数据发送,当通信结束希望断开连接时,通信双方的主机之间就可以相互交换
FIN
位为 1 的 TCP 段。
TCP的重要性
TCP在传输层,其下的网络层(这里以IP为例)是不可靠的,不能保证网络包的交付以及包内数据的完整性,但是现实应用中往往需要数据的可靠性,那就需要上层的传输层的TCP来完成这个保证数据可靠性的重任!
TCP是一个工作在传输层的可靠数据传输服务,它能确保接收端接收的网络包是无损坏、无间隔、非冗余和按序的
TCP的连接确立前提
TCP确立连接需要源地址IP、源端口、目的地址、目的端口
其中IP在IP包头部中,用于寻找主机,而端口在TCP头部中,用于匹配进程
问:一个服务器监听一个端口,它能够建立最多多少个TCP连接?
- 对于IPv4,IP有32位,端口有16位,能够建立的最多个TCP连接数量是:2^32 * 2^16 = 2^48个
实际应用中不可能能够达到如此之多的TCP连接,因为每一个TCP连接都需要占用内存,内存有限;另外文件描述符也有限,而每一个TCP连接都是一个文件(Linux)
TCP应用
最经典的是FTP、HTTP/HTTPS、SMTP等,TCP用于传输数据量大,可靠性要求高的应用
TCP三次握手过程
1. 服务器通过listen()开始监听某个端口,客户端和服务器都处于 [close] 状态
2. 客户端随机初始化序列号c_isn,将其加入头部,设置SYN=1,发送给服务器,随后客户端处于 [SYN-SENT] 状态
3. 服务端接收到SYN报文后,随机初始化序列号s_isn,将其加入头部,确认应答号为c_isn+1,表示期待收到c_isn+1序号的报文,并且将SYN与ACK=1,发送报文,处于 [SYN-RCVD]
4. 客户端收到SYN+ACK报文后,回复ACK=1 ack=s_isn+1的报文,并且可以直接附上报文内容发送给服务器,处于 [ESTABLISHED] 状态
5. 服务器收到后也进入 [ESTABLISHED] 状态
要进行三次连接而不是两次、四次的原因
1. 避免重复的历史连接
- 一个「旧 SYN 报文」比「最新的 SYN」 报文早到达了服务端,那么此时服务端就会回一个 SYN + ACK 报文给客户端,此报文中的确认号是 91(90+1)。
- 客户端收到后,发现自己期望收到的确认号应该是 100 + 1,而不是 90 + 1,于是就会回 RST 报文。
- 服务端收到 RST 报文后,就会释放连接。
- 后续最新的 SYN 抵达了服务端后,客户端与服务端就可以正常的完成三次握手了- 两次握手在服务器收到SYN后就会建立连接,从而可能会浪费资源!
2. 同步双方序列号
- 客户端第一个SYN有一个SeqNum,服务器接收后将ACK与之同步,并发送自己的SeqNum,客户端收到后将自己的ACK与之同步
3. 避免不必要的资源开销(连接资源)
- 如果只有两次握手,服务器在接收后就会直接创建连接资源,如果服务器的ACK丢失,客户端再次发送SYN则又会创建连接资源
- 「两次握手」:无法防止历史连接的建立,会造成双方资源的浪费,也无法可靠的同步双方序列号;
- 「四次握手」:三次握手就已经理论上最少可靠连接建立,所以不需要使用更多的通信次数。
第一次握手丢失
第一次握手是客户端发送SYN,丢失后会超时重传(序列号相同),如果有内核权限,可以修改超时时限、重传次数等,并且在Linux下每次超时重传时间是前一次的两倍。
如果超过重传次数还未收到第二次握手,则不再尝试并且断开连接
第二次握手丢失
第二次握手是服务器回应SYN+ACK,
第三次握手丢失
客户端收到第二次握手后会进入ESTABLISHED状态,并且向服务器发送ACK报文,如果这个报文丢了,会出发第二次握手的SYN+ACK重传,直到重传达到上限
SYN洪泛攻击
服务器收到SYN后就会处于SYN_RECVD状态,并且维护一些缓存等,这是一个半连接队列,如果攻击者在不同IP下狂发SYN请求,则服务器会维护很多缓存,直到半连接队列爆棚,这样服务器就无法响应正常用户的请求了
如何避免:
1. 调大队列上限
2. 开启syncookie
- 收到SYN后服务器根据特定算法算出一个cookie,放到第二次握手报文的序列号中,再次收到来自客户端的第三次握手时,根据cookie检查其合法性,如果合法直接加入全连接队列
3. 减少SYN+ACK重传次数
TCP保活机制
定义一个时间段,在这个时间段内,如果没有任何连接相关的活动,TCP 保活机制会开始作用,每隔一个时间间隔,发送一个探测报文,该探测报文包含的数据非常少,如果连续几个探测报文都没有得到响应,则认为当前的 TCP 连接已经死亡,系统内核将错误信息通知给上层应用
开发者也可以在应用端自己写一个保活机制
建立连接后服务器进程崩溃
转由内核发送挥手信息,还是能完成四次挥手
为什么每次初始化的序列号不同(随机)
1. 如果每次都从0开始初始化序列号,很有可能在相同四元组情况下,历史报文序列号很有可能在对方的接收窗口内!导致历史报文被接收
2. 避免黑客模拟相同四元组进行攻击
序列号虽然初始化不同,实际上是基于时钟来随机,但是会有回绕问题,这时候难免会有历史报文重复问题,这时候就要加上时间戳机制
四次挥手过程
1. 客户端打算关闭连接,此时会发送一个 TCP 首部 FIN 标志位被置为 1 的报文,也即 FIN 报文,之后客户端进入 FIN_WAIT_1 状态。
此时客户端不再发送数据,但是可以接收数据
2. 服务端收到该报文,向客户端发送 ACK 应答报文,接着服务端进入 CLOSE_WAIT 状态。
此时服务器可能还有没有处理完的数据,会在之后的最后一个报文发出
3. 客户端收到服务端的 ACK 应答报文后,之后进入 FIN_WAIT_2 状态。
4. 等待服务端处理完数据后,向客户端发送 FIN 报文,之后服务端进入 LAST_ACK 状态。
这个FIN报文中可能含有服务端最后要发送的数据
注意!
如果服务器在收到FIN后,没有更多的数据要发送了,并且开启了TCP延迟发送机制,就可以将FIN和ACK合起来发送,这样就变成了三次挥手
5. 客户端收到服务端的 FIN 报文后,回一个 ACK 应答报文,之后进入 TIME_WAIT 状态
6. 服务端收到了 ACK 应答报文后,就进入了 CLOSE 状态,至此服务端已经完成连接的关闭。
7. 客户端在经过 2MSL 一段时间后,自动进入 CLOSE 状态,至此客户端
TIME_WAIT状态
1. 防止历史连接中的数据被后面相同四元组的连接错误接收
- 如果TIME_WAIT不存在或者太短,在上一个连接中服务器发送的数据包延时后可能会在下一次同四元组连接中碰巧被接收
- 因此设置为2MSL足以让两个方向上的历史数据包都被丢弃,使得新建立的连接产生的数据包没有历史的
2. 保证被动关闭方能被正确关闭
- TIME-WAIT 作用是等待足够的时间以确保最后的 ACK 能让被动关闭方接收,从而帮助其正常关闭。
- 如果没有,则第四次挥手丢失后,服务器重传的FIN会碰壁(CLOSE)回一个RST,很难看
如果TIME_WAIT状态过多
1. 客户端过多会导致端口资源被耗尽(端口复用解决)
2. 服务器没关系,因为只监听一个端口,但是会占用系统资源(文件描述符、内存)
解决方法
1. 直接复用处于TIME_WAIT状态的socket
2. 超过限制数量的TIME_WAIT直接重置后面的TW状态连接
服务器出现大量TIME_WAIT怎么办
1. HTTP没有使用长连接
- 在任意一方的Keep-Alive被关闭时,一般默认服务器端主动关闭连接
- 因此出现问题时要检查双方的长连接选项是否打开!
2. HTTP长连接超时
- 当已经建立的长连接TCP中很长一段时间没有进行数据通信,服务器就会主动关闭连接
- 可以排查网络问题,可能是客户端的数据丢失了
3. HTTP长连接数量达到上限
- Web服务器一般会设置最多长连接数量上限
- 可以增大它
服务器出现大量CLOSE_WAIT怎么办
在CLOSE_WAIT状态,服务器要调用close()来关闭连接,出现大量这个状态说明没有close
1. accept连接时没有将其注册到epoll,导致后续收到FIN时服务器无法感知这个事件
2. 最后close()没调用
这些都是因为代码逻辑问题,在close()前卡住或者提前抛出异常
如何恶意关闭一个TCP连接
1. 伪造一个四元组相同的SYN报文
2. 服务器在有连接下收到SYN后回复Challenge ACK(存在连接中的正确的ACK与序列号)
3. 再在这个四元组相同的伪造的连接中发送从ChallengeACK得到的正确序列号的RST
第一次挥手丢失
客户端的FIN丢失后,超时重传,超过重传次数上限就会直接断开连接,也就是直接从FIN_WAIT1进入CLOSE状态
第二次挥手丢失
服务器对第一次挥手的ACK丢失后,客户端重传第一次挥手,直到超过限制
第三次挥手丢失
服务器第三次挥手FIN丢失,服务器会重传,如果服务器达到重传限制,则服务器直接关闭连接进入CLOSE状态;另外,客户端在此期间会一直处于FIN_WAIT2状态,如果超过状态时限,也会直接进入CLOSE状态
第四次挥手丢失
客户端发送出第四次挥手ACK后进入TIME_WAIT,这个ACK丢失后服务器会不断重传FIN直到上限,每次收到服务器的FIN都会重新计算TIME_WAIT状态时间(2MSL),最后服务器达到上限,关闭连接,不再发送,客户端TIME_WAIT状态超时,关闭连接
MSL是报文最大生存时间,设置为2是因为发送一个报文并且等待一个报文一来一回会需要2MSL
TCP分片
当一个数据报文超过MSS则会进行分片
问题是:我们知道IP会基于MTU(1500字节)进行分片,那既然网络层有分片,为什么TCP还要分片呢?
1. IP层如果有一个超过MTU的数据,会分片,然后在目标主机重新组装后上传给TCP
2. 此时如果有一个IP报文丢失了,会无法组装,导致无法上传给TCP
3. 此时由层次关系,TCP并不知道网络层具体发生了什么事,只知道下面没传上来
4. 此时TCP层永远无法接收到网络层上传的报文,就会出发超时重传
5. 这种情况下就要重传整个TCP报文来
因此,TCP需要自己进行分片,进行重发时也是重发MSS大小的数据,不会浪费带宽
UDP和TCP的区别
UDP不提供复杂的控制机制,利用IP提供不面向连接的通信服务,并且发出UDP数据包后就不管这个包在网络中是生是死,撒手不管了!
1. 连接
- TCP在传输数据包前要先建立连接
- UDP不需要建立连接,直接传送数据
2. 服务对象
- 由于TCP是在连接建立的基础上,而连接是1对1建立的,因此TCP只能是一个主机与一个主机传输数据
- UDP无连接,因此可以支持1对1、1对多乃至多对多的通信
3. 可靠性
- TCP传输的数据可靠,能够保证无差错、不丢失、不重复、不乱序
- UDP有一个很有意思的设定:尽最大努力交付。意思是它会尽力传输,不管网络状态有多差或有多好,直接甩出去,但是无法保证数据的可靠性
但是可以基于UDP实现一个可靠传输协议,例如HTTP/3.0的QUIC
4. 拥塞控制、流量控制
- TCP有拥塞控制(慢启动、拥塞控制、快速重传),也有流量控制(window size)
- UDP没有拥塞控制,也没有流量控制,往死里发!
这里有必要提一下拥塞控制和流量控制的区别,拥塞控制是在产生了丢包或者超时的情况下通过减缓发送速率来改善网络环境,而流量控制是控制自己的发送速率,简单来讲就是说流量控制是为了预防拥塞,如:在马路上行车,交警跟红绿灯是流量控制,当发生拥塞时,如何进行疏散,是拥塞控制。流量控制指点对点通信量的控制。
5. 头部
- TCP有很大的头部,在[选项]字段没有使用时是20字节,相当大,同时有可变性
- UDP头部只有8字节(源端口、目的端口、包长度、校验和),固定不变
6. 传输方式
- TCP是字节流传输,没有边界,但是通过内部机制保证了顺序和可靠
- UDP是一个包一个包单独发送,有边界,但是不可靠(丢包、乱序)
7. 分片时机
- TCP数据包大于MSS会在传输层分片,接收方同样也在传输层组装,如果中途丢失了一个分片,则只需要重传这个丢失的即可
- UDP不同,UDP的数据包不会再传输层分片,而是在网络层进行判断,如果大于MTU,则在网络层分片,接收方也在网络层组装(丢包问题)
8. 网络编程
- TCP网络编程如下图: - UDP网络编程如下图:
- TCP有监听过程
- UDP没有监听过程
UDP和TCP可以使用同一个端口吗
要解决这个问题,首先要了解这两种协议在下层是如何传输的
- 在网络层中,通过IP地址寻找互连的主机或路由器
- 在链路层中,通过MAC地址寻找局域网中的主机或交换机
因此传输层的端口号的作用是为了区分同一个主机上不同应用程序的数据包,而在一个进程收到一个数据包后,可以在IP包头知道这个数据包是基于TCP还是UDP的,因此可以将他们发送给不同的内核模块进行处理
简单来说就是,TCP/UDP 各自的端口号也相互独立,如 TCP 有一个 80 号端口,UDP 也可以有一个 80 号端口,二者并不冲突
多个TCP服务进程可以绑定同一个端口吗
1. 在同一个IP地址下:肯定是不可以的,在执行bind()时会提示 "Address already in use"
2. 在不同的IP地址下:肯定是可以的!
这里有一个有趣的点,如果绑定的IP地址是0.0.0.0,则它是绑定了该主机上的所有IP地址,那么此时再去绑定其他的是不可以的
- 可以用SO_REUSEADDR解决问题,绑定的 IP地址 + 端口时,只要 IP 地址不是正好(exactly)相同,那么允许绑定
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
一个客户端的端口可以重复使用吗
先给出答案:可以!
- TCP 是面向连接的,而连接是由四元组(源IP地址,源端口,目的IP地址,目的端口)唯一确认的,那么只要四元组中其中一个元素发生了变化,那么就表示不同的 TCP 连接的
- 所以如果客户端已使用端口 64992 与服务端 A 建立了连接,那么客户端要与服务端 B 建立连接,还是可以使用端口 64992 的
- 因为内核是通过四元祖信息来定位一个 TCP 连接的,并不会因为客户端的端口号相同,而导致连接冲突的问题
注意!这里是一个客户端的端口重复,而服务器的ip+端口不同
如果是多个客户端在同一个主机bind()同一个端口是不行的!
客户端 TCP 连接 TIME_WAIT 状态过多,会导致端口资源耗尽而无法建立新的连接吗?
1. 与相同的服务器连接(ip+port相同),会导致资源被耗尽
2. 与不同的服务器连接则不会,因为不同的四元组可以复用同一个端口号
解决这个问题可以开启net.ipv4.tcp_tw_reuse,内核在判断四元组已经被占用时还会判断处于TIME_WAIT状态的时间,如果超过1s,则会直接重用这个连接,并且用这个四元组
重启TCP服务进程时提示Address in use
当 TCP 服务进程重启之后,总是碰到“Address in use”的报错信息,TCP 服务进程不能很快地重启,而是要过一会才能重启成功
这是因为在TCP服务进程重启时要先结束,而结束则要对所有已经存在的TCP连接的客户端进行四次挥手,挥手有时延,挥手完后也会有一段时间的TIME_WAIT状态,这个时候还是会维护服务器的IP和端口信息,因此此时再次启动该服务进程就会bind()出错
可以通过设置socket的SO_REUSEADDR属性解决。实际上,在所有 TCP 服务器程序中,调用 bind 之前最好对 socket 设置 SO_REUSEADDR 属性,它会帮助在很快时间内重启服务端程序。
UDP的包长度是否必要
首先需要知道包长度是UDP特有的,TCP并没有这个字段,因为TCP下层往往是IP,可以通过 [ IP报文总长度-IP头部长度-TCP头部长度 ]来计算出TCP包的长度
但是UDP也可以这样算啊,为什么要加上这个字段呢?这里有两种解释
1. 为了网络设备硬件设计和处理方便,首部长度需要是 4 字节的整数倍,为了补全 UDP 首部长度是 4 字节的整数倍,才补充了包长度字段
2. 如今的 UDP 协议是基于 IP 协议发展的,而当年可能并非如此,依赖的可能是别的不提供自身报文长度或首部长度的网络层协议,因此 UDP 报文首部需要有长度字段以供计算
UDP应用
UDP简单、高效,常用于一次只传送少量数据,可靠性要求低、传输经济、速率要求高等应用,如DNS、HTTP/3.0(主要是传输效率),以及一些视频音频的实时传输(可以有些许丢包,不会过分影响观众体验)