TCP初解
TCP#
什么是TCP
是面向连接的、可靠的、基于字节流的传输层通信协议。
- 面向连接:一定是一对一才能连接,不能像UDP那样一个主机同时向多个主机发送消息。
- 可靠的:无论网络链路中出现了怎样的链路变化,TCP都可以保证一个报文一定能到达接收端。
- 字节流:TCP报文是有序的,当前一个TCP报文没有收到的时候,即使它先收到了后面的TCP报文,那么也不能扔给应用层去处理,同时对重复的TCP报文自动丢弃。
建立一个TCP连接
连接:用于保证可靠性和流量控制维护的某些状态信息,这些信息的组合,包括socket、序列号和窗口大小。
建立一个TCP连接需要客户端和服务端达成这三个信息的共识。
- socket:由ip地址与端口号构成。
- 序列号:用来解决乱序问题。
- 窗口大小:用来做流量控制。
唯一确定一个TCP连接
TCP四元组可以唯一确定一个连接:
源地址、源端口、目标地址、目标端口。
一个服务端监听了本地的一个端口,理论上可以建立的最大连接数:客户端IP数 * 客户端端口数。
对于IPv4协议,最多是232次方,客户端端口数最多为216次方。但是实际中不可能达到这个数量,会受到一些因素影响:
- 文件描述符限制:每个TCP连接都是一个文件,如果文件描述符被占满了则会发送Too many open files错误。
- 内存限制:每个TCP连接都会占用一定内存,而操作系统的内存是有限的。
与UDP的区别
UDP是面向无连接的通信服务,不保证可靠性。头部格式:源端口号16位,目标端口号16位,包长度16位,校验和16位。
区别:
- TCP是面向连接的传输层协议,传输数据前先要建立连接。UDP是无连接的,即可传输数据。
- TCP是一对一的两点服务。UDP支持一对一、一对多、多对多的交互通信。
- TCP是可靠交付数据的,数据无差错,不丢失,不重复,按序到达。UDP不保证可靠交付数据,但可以基于UDP实现可靠的传输协议,如QUIC协议。
- TCP有拥塞控制和流量控制机制,保证数据传输的安全性。UDP则没有。
- TCP首部长度较长,至少20字节,会有一定开销。UDP首部只有8字节,固定不变的,开销较小。
- TCP是流式传输,没有边界,但保证顺序和可靠。UDP是一个包一个包的发送,有边界,可能会丢包和乱序。
- TCP的数据大小如果大于MSS大小,则会在传输层进行分片,目标主机收到后会在传输层组装TCP数据包,中途丢失一个分片只需要传输丢失的分片。UDP大小如果大于MTU大小,则会在IP层进行分片,在IP层组装,再传给传输层。
应用场景区别:
TCP:面向连接的,保证数据可靠性交付
- FTP文件传输
- HTTP/HTTPS
UDP:面向无连接,可以随时发消息,简单高效
- 包总量较少的通信,DNS、SNMP等。
- 视频、音频等多媒体通信。
- 广播通信。
TCP比UDP多一个首部字段
因为TCP有可变长的字段,而UDP首部长度是不会变化的,因此不需要记录首部长度。
TCP连接
三次握手过程
- 初始,客户端和服务端都处于CLOSE状态,首先是服务端主动监听某个端口,处于LISTEN状态。
- 客户端会随机初始化序号(client_isn),将此序号置于TCP首部的序号字段中,同时将SYN标志置1。然后把第一个SYN报文发送给服务端,表示向服务端发起连接,该报文不包含应用层数据,之后客户端处于SYN-SENT状态。
- 服务端收到SYN报文后,也随机初始化自己的序号(server_isn),将此序号填入TCP首部,然后把TCP首部的确认应答号字段填入 client_isn+1,接着把SYN和ACK都置为1,然后发给客户端,不包含应用层数据。然后服务端处于SYN-RCVD状态。
- 客户端收到服务端报文后,还要向服务端回应最后一个应答报文,首先该应答报文TCP首部ACK置1,然后确认应答序号填入server_isn+1,最后把报文发给服务端,此时可以携带应用层数据。然后客户端处于ESTABLISHED状态。
- 服务端收到客户端的应答报文后也进入ESTABLISHED状态
两次、四次握手不行的原因
- 三次握手可以阻止重复历史连接的初始化
- 三次握手才可以同步双方的初始序列号
- 三次握手才可以避免资源浪费
避免重复历史连接:
两次握手的情况下,服务端没有中间状态给客户端来阻止历史连接,导致服务端可能建立一个历史连接,造成资源浪费。
在两次握手的情况下,服务端在收到SYN报文后就进入ESTABLISHED状态,但客户端还没有进入ESTABLISHED状态,如果这次是历史连接,客户端判断到这是历史连接那么就会回RST报文来断开连接,服务端并不知道这个是历史连接,只有在收到RST报文后才会断开连接。
要解决问题,就是在服务端发送数据之前,要阻止历史连接,这样就不会造成资源浪费。
同步双方初始序列号:
当客户端发送携带初始序列号的SYN报文时,需要服务端回复一个ACK应答报文,当服务端发送初始序列号的SYN报文时要得到客户端的ACK回应。一来一回才能确保双方的初始序列号被可靠同步。
两次握手只保证了一方的初始序列号能被对方成功接收,没办法保证双方的初始序列号都被确认接受。
避免资源浪费:
如果只有两次握手,当客户端发送的SYN报文在网络阻塞,客户端没有收到ACK就会重新发送SYN,由于没有第三次握手,服务端不清楚客户端有没有接收到自己发的ACK,所以服务端每收到一个SYN就只能主动建立一个连接,如果客户端发送的SYN报文在网络中阻塞,多次重复发送SYN,那么服务端在收到请求后会建立多个冗余连接,造成资源浪费。
每次建立TCP连接,初始化的序列号都不一样。
- 为了防止历史报文被下一个相同的四元组连接接收。
- 为了安全性,防止黑客伪造的相同序列号的TCP报文被对方接收。
如果相同会出现的意外情况:
- 客户端与服务端建立好TCP连接后,客户端发送的报文被网络阻塞,此时恰好服务端因为一些原因,TCP断开连接,客户端在超时重传后服务端返回RST要求重新建立连接。
- 客户端与服务端重新建立连接后,服务端接收到被阻塞的报文,而该报文的序列号在服务端的接收窗口范围,所以进行接收。造成了数据错乱。
所以每次初始化序列号不一样能很大程度上避免历史报文被下一个相同四元组的连接接收,但因为序列号会有回绕问题,所以还要有时间戳机制判断历史报文。
TCP层分片
IP层也可以进行分片,但使用TCP协议时会在TCP层就进行分片。原因:
- 当IP层有一个超过MTU大小的要发送时,IP层会进行分片,保证每一个分片都小于MTU,分片后由目标主机的IP层进行组装交付给TCP层。但IP层没有超时重传机制,丢失则会正片重传。
- 为了达到最佳传输效能,TCP在建立连接时会协商双方的MSS值,当TCP层发现数据超过了MSS时则会先进行分片,一个TCP分片丢失后进行重发也是以MSS为单位,不用重传所有分片。
第一次握手丢失
建立TCP连接时,第一个发的是SYN报文,然后客户端进入SYN_SENT状态,之后一直接收不到服务端的 SYN-ACK报文就会触发超时重传,重传SYN报文,且重传的SYN报文序列号都一样。
最大重传次数由 tcp_syn_retries 内核参数控制,每次超时的时间是上次的2倍。
第二次握手丢失
第二次握手的目的:
- 第二次握手的ACK,是对第一次握手的确认报文。
- 第二次握手的SYN,是服务端对客户端发起建立TCP连接的报文。
如果第二次握手丢失:
- 客户端收不到第二次握手的ACK,会认为第一次握手丢失,因此触发超时重传,重传SYN报文。由 tcp_syn_retries参数决定。
- 服务端收到不第三次握手的ACK,会认为第二次握手的SYN-ACK丢失,触发超时重传,由 tcp_synack_retries参数决定。
第三次握手丢失
第三次握手的目的:
- 对第二次握手的SYN的确认报文,让服务端也进入ESTABLISHED状态。
第三次握手丢失:
- 服务端一直接收不到第三次握手,认为第二次握手丢失,触发超时重传,重传SYN-ACK报文,直到收到ACK或者达到最大重传次数断开连接。客户端每收到一次SYN-ACK报文都会发送ACK报文,但ACK报文不会触发超时重传。
ACK如果丢失了,就由对方重传相应的报文。
TCP四次挥手
- 客户端要关闭连接,此时会发送一个TCP首部FIN为1的报文,之后客户端进入FIN_WAIT_1状态。
- 服务端收到该报文后,就向客户端发送ACK应答报文,接着服务端进入CLOSE_WAIT状态。
- 客户端收到服务端的ACK后进入FIN_WAIT_2状态。
- 等待服务端处理完数据后,会向客户端发送一个FIN报文,然后服务端进入LAST_ACK状态。
- 客户端收到服务端的FIN报文后,回一个ACK报文,之后进入TIME_WAIT状态。
- 服务端收到ACK后就进入CLOSE状态,服务端关闭了连接。
- 客户端在经过2MSL的时间后,自动进入CLOSE状态,客户端关闭了连接。
挥手四次的原因:
- 关闭连接时,客户端向服务端发送FIN时,仅仅表示客户端不再发送数据了,但是还能接收数据。
- 服务端收到客户端的FIN报文时,先回一个ACK应答报文,而服务端可能还有数据需要处理和发送,等服务端不再发送数据后,才会发送一个FIN报文给客户端表示同意关闭连接。
但在一些情况下也可以时三次挥手,这是因为TCP的延迟确认机制。因为发送的ACK不携带数据那么传输效率非常低,单个ACK包仍然有40字节。服务端在发送ACK前会先等待一段时间,如果此时有数据要发送会一起立刻发出。如果没有数据要发送且开启了TCP延迟确认机制,那么第二次和第三次挥手就会合并传输,就出现了三次挥手。
第一次挥手丢失
客户端主动调用close函数后会向服务端发送FIN报文,与服务端断开连接,此时客户端进入FIN_WAIT_1状态。若FIN丢失,客户端一直接收不到服务端的ACK,那么会触发超时重传机制,重发次数由 tcp_orphan_retries决定。如果超过了该次数且等待一段时间仍然没接收到,会直接进入CLOSE状态。
第二次挥手丢失
服务端收到第一次挥手的FIN报文后,会发一个ACK报文,然后进入CLOSE——WATI状态。
如果ACK一直丢失,此时服务端不会重传,客户端始终没有收到ACK会认为第一次挥手丢失,触发超时重传FIN报文,直到收到ACK或超过重传次数。
特别的:
- 如果客户端是调用了 close函数进行关闭连接,由于close 会关闭发送和接收数据,所以客户端在收到ACK后处于 FIN_WAIT_2状态, 会受到 tcp_fin_timeout 参数的时间限制。
- 如果是调用 shutdown函数关闭连接,指定了只关闭发送方向,而接收方向没有关闭,那么关闭方如果一直没有收到第三次挥手,将会始终处于 FIN_WAIT_2状态。
第三次挥手丢失
- 当服务端重传第三次挥手报文的次数达到了 3 次后,由于 tcp_orphan_retries 为 3,达到了重传最大次数,于是再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到客户端的第四次挥手(ACK报文),那么服务端就会断开连接。
- 客户端因为是通过 close 函数关闭连接的,处于 FIN_WAIT_2 状态是有时长限制的,如果 tcp_fin_timeout 时间内还是没能收到服务端的第三次挥手(FIN 报文),那么客户端就会断开连接
第四次挥手丢失
在第三次握手后,客户端回复ACK后处于TIME_WAIT状态,持续2MSL后进入关闭状态。但服务端一直没有收到ACK,会触发超时重传FIN报文,仍然处于LAST_ACK状态。客户端收到FIN报文后会重置2MSL定时器,超过重发次数后会断开连接。
TIME_WAIT等待时间2MSL
MSL是报文最大生存时间。等待时间从客户端发送ACK开始计时,如果ACK丢失,那么服务端会重传FIN,客户端收到后会重置计时,两个报文,一去一来需要等待两倍的时间。
因此在2MSL内,至少允许报文丢失一次。
TIME_WAIT状态
需要TIME_WAIT状态的原因:
-
防止历史连接中的数据,被后面相同四元组的连接错误的接收。
整个TIME_WAIT状态持续2MSL,这个时间足以让两个方向上的数据包都被丢弃,使得原来连接的数据包在网络中都自然消失,再出现的新的数据包一定都是新建立连接所产生的。
-
保证被动关闭连接的一方能正确关闭。
如果没有TIME_WAIT状态,客户端在发完最后一次ACK报文就进入CLOSE状态,若该报文丢失,服务端会重传FIN报文,客户端此时已经是关闭状态,会发送RST报文,服务端收到RST后解释为一个错误。为了避免所以客户端要等待足够长的时间,确保ACK传到,这样即使丢失,一去一来刚好2个MSL时间。
作者:墨鱼-yyyl
出处:https://www.cnblogs.com/moyu-yyyl/p/18009695
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义