Java八股文——计算机网络「传输层篇」
说一下 TCP 的头部
本文图片来自小林 coding
面试官您好,TCP头部是TCP协议实现其可靠、有序、面向连接等特性的基石。它包含了非常丰富和精巧的控制信息。一个标准的TCP头部,在不包含选项的情况下,长度是20个字节。
我们可以把它分为几个部分来看:
1. 端口号 (Port Numbers) —— “找到正确的应用程序”
- 源端口号 (Source Port, 16位) 和 目标端口号 (Destination Port, 16位)
- 这是TCP头部的头两个字段。它们的作用,是与IP地址一起,唯一地标识一个网络连接中的两个通信端点(即
IP地址:端口号
)。 - 它保证了数据包能够被正确地交付给服务器和客户端上对应的应用程序进程(比如,HTTP服务通常在80端口,SSH在22端口)。
- 这是TCP头部的头两个字段。它们的作用,是与IP地址一起,唯一地标识一个网络连接中的两个通信端点(即
2. 序列号与确认号 (Sequence & Acknowledgment Numbers) —— “保证可靠与有序的核心”
这绝对是TCP头部中最重要的两个字段,它们共同构成了TCP可靠传输的基石。
-
序列号 (Sequence Number, 32位)
- 作用:它主要用来解决网络包的乱序问题。
- 工作原理:TCP是面向字节流的,它会为每一个发送出去的字节都进行编号。这个“序列号”字段,记录的就是当前这个TCP报文段中,第一个字节数据在整个数据流中的编号。
- 通过这个序列号,接收方就能对收到的、可能乱序的数据包,进行正确的排序和重组。
-
确认应答号 (Acknowledgment Number, 32位)
- 作用:主要用来解决丢包问题,并告知对方数据已成功接收。
- 工作原理:这个字段的值,是 “我期望下一次收到的你的数据的序列号”。
- 比如,我收到了你序列号为1到1000的数据,那么我就会在回给你的ACK报文中,将确认应答号设置为1001。这传达了两个信息:“1001之前的所有数据,我都已经安全收到了”,以及“请你下一次从1001这个位置开始发”。
3. 控制位 (Control Flags) —— “控制连接状态的开关”
这是一个6位的字段,其中有几个非常关键的标志位,它们就像是控制TCP连接状态的“开关”。
ACK
(Acknowledgment):当这个位被设为1
时,上面的“确认应答号”字段才有效。在TCP连接建立后,几乎所有的数据包,这个位都会被设为1。SYN
(Synchronization):当这个位被设为1
时,表示这是一个 “建立连接” 的请求。在TCP三次握手的第一个和第二个包中,这个位都会被置为1。FIN
(Finish):当这个位被设为1
时,表示发送方的数据已经全部发送完毕,请求 “关闭连接”。在TCP四次挥手的过程中,会交换FIN包。RST
(Reset):当这个位被设为1
时,表示连接出现了严重错误,需要强制、异常地断开连接。它不会走正常的四次挥手流程。
4. 其他重要字段
- 窗口大小 (Window Size, 16位):用于TCP的流量控制。它告诉对方:“我本地的接收缓冲区,现在还能容纳多少字节的数据,你悠着点发,别把我撑爆了。”
- 校验和 (Checksum, 16位):用于校验TCP头部和数据部分在传输过程中,是否发生了数据损坏。
- 选项 (Options):一个可变长的字段,用于支持一些高级功能,比如MSS(最大报文段长度)、SACK(选择性确认)、时间戳等。
总结一下,TCP头部通过端口号实现了应用寻址,通过序列号和确认号实现了数据的可靠与有序,通过控制位管理着连接的生命周期(建立、关闭、重置),再通过窗口大小等字段进行流量控制。正是这个设计精巧的头部,才使得TCP协议能够在复杂的互联网环境中,提供如此稳定、可靠的传输服务。
TCP 三次握手过程
面试官您好,TCP的三次握手,是客户端和服务器在正式传输数据之前,为了建立一个可靠的、双向的TCP连接而进行的一个交互过程。
这个过程,就像是两个人打电话,在开始正式通话前,需要互相确认一下“喂,能听到我吗?”和“嗯,能听到,你也能听到我吗?”。
整个过程,涉及到TCP头部的两个关键标志位:SYN
(同步位) 和 ACK
(确认位)。
下面我来详细描述一下这“三步”都发生了什么:
第一次握手:客户端的“连接请求”
- 谁发起:客户端 (Client)
- 做什么:
- 客户端首先会创建一个TCP报文段,将TCP头部的
SYN
标志位置为1
。这表示:“我请求建立一个连接。” - 同时,它会随机生成一个初始的序列号
x
(Sequence Number = x
),并将其放入TCP头部。
- 客户端首先会创建一个TCP报文段,将TCP头部的
- 状态变化:发送完这个
SYN
包后,客户端就进入了SYN-SENT
状态,开始等待服务器的回应。 - 一个生动的比喻:客户端(A)向服务器(B)大喊:“B,B,你听得到我说话吗?我的编号是x!”
第二次握手:服务器的“确认并请求”
- 谁发起:服务器 (Server)
- 做什么:
- 服务器在接收到客户端的
SYN
包后,如果同意建立连接,它会构造一个回应的报文。 - 在这个报文中,它会同时将
SYN
和ACK
这两个标志位都置为1
。SYN=1
表示:“我也请求建立一个连接。”ACK=1
表示:“我收到了你的请求。”
- 它会确认客户端的序列号,将确认应答号(
Acknowledgment Number
)设置为x + 1
。这表示:“我已经收到了你编号为x的包,我期望你下一个包的编号是x+1。” - 同时,它也会为自己的数据传输,随机生成一个初始的序列号
y
(Sequence Number = y
)。
- 服务器在接收到客户端的
- 状态变化:发送完这个
SYN+ACK
包后,服务器进入了SYN-RCVD
状态。 - 比喻升级:服务器(B)听到了,回应道:“A,我听到了!你的编号x我收到了,你下句话从x+1开始说。另外,你也能听到我说话吗?我的编号是y。”
第三次握手:客户端的“最终确认”
- 谁发起:客户端 (Client)
- 做什么:
- 客户端在收到服务器的
SYN+ACK
包后,就确认了服务器能收到自己的消息,也能向自己发消息。现在,它需要向服务器发送最后一个确认包。 - 在这个报文中,它会将
ACK
标志位置为1
。 - 它会确认服务器的序列号,将确认应答号设置为
y + 1
。这表示:“我也收到了你编号为y的包,我期望你下一个包的编号是y+1。” - 这次通信,通常可以携带实际的应用数据了。
- 客户端在收到服务器的
- 状态变化:
- 客户端发送完这个
ACK
包后,连接就建立成功了,客户端进入ESTABLISHED
状态,可以开始发送数据。 - 服务器在接收到这个
ACK
包后,也确认了客户端能收到自己的消息,连接也建立成功,进入ESTABLISHED
状态。
- 客户端发送完这个
- 比喻收尾:客户端(A)回应道:“好的,B,我也能听到你!你的编号y我收到了,你下句话从y+1开始说吧。我们现在可以正常通话了。”
为什么必须是三次握手,而不是两次或四次?
-
为什么不能是两次?
- 两次握手无法保证客户端能确认服务器的接收和发送能力都是正常的。在第二次握手时,服务器虽然发出了
SYN+ACK
,但它不知道客户端是否收到了。如果这个包丢失了,服务器就会一直等待,浪费资源。 - 更重要的是,两次握手无法防止已失效的连接请求报文突然又传到服务器,导致服务器错误地建立连接。
- 两次握手无法保证客户端能确认服务器的接收和发送能力都是正常的。在第二次握手时,服务器虽然发出了
-
为什么不需要四次?
- 三次握手,已经足以让双方都确认了彼此的“收”和“发”能力都是正常的,已经达到了建立一个可靠连接的最小且完备的步骤。再增加一次,就显得多余和浪费了。服务器的
SYN
和ACK
,是完全可以合并在一个报文中发送的。
- 三次握手,已经足以让双方都确认了彼此的“收”和“发”能力都是正常的,已经达到了建立一个可靠连接的最小且完备的步骤。再增加一次,就显得多余和浪费了。服务器的
总结一下,TCP三次握手,就是通过SYN
和ACK
标志位的交换,以及序列号的确认,来确保通信双方都同步了初始序列号,并验证了对方的接收和发送能力都正常,从而为后续可靠的数据传输,铺平了道路。
TCP 为什么需要三次握手建立连接?
面试官您好,TCP之所以必须采用三次握手来建立连接,而不能简化为两次,其背后是经过深思熟虑的设计,主要是为了解决几个在不可靠网络中可能出现的致命问题。
三大原因,层层递进,共同构成了三次握手的必要性:
1. 最核心的原因:防止已失效的“历史连接请求”被错误地初始化
这是理解为什么“两次握手不够”的关键所在。
-
问题场景:
- 客户端A向服务器B发送了一个
SYN
连接请求包(我们称之为SYN_1
)。 - 但是,这个
SYN_1
因为网络拥堵,在某个路由器上滞留了很长时间,没有及时到达服务器B。 - 客户端A因为迟迟收不到确认,超时重传,又发送了一个新的
SYN
请求包(SYN_2
)。 - 这次,
SYN_2
很顺利地到达了服务器B,双方通过两次握手(假设可以),成功建立了连接,传输数据,然后正常关闭了连接。
- 客户端A向服务器B发送了一个
-
如果只有两次握手,会发生什么?
- 在上述连接关闭后,那个被滞留在网络中的、早已失效的
SYN_1
,突然又到达了服务器B。 - 对于服务器B来说,它收到的就是一个看似完全正常的
SYN
请求。如果采用两次握手,那么服务器B会立即分配资源,并向客户端A发送一个SYN+ACK
确认包,然后单方面地进入ESTABLISHED
状态,认为连接已建立。 - 但是,客户端A此时根本没有在发起连接请求,它会直接忽略或丢弃这个来自服务器B的
SYN+ACK
包。 - 后果:服务器B错误地建立了一个“僵尸连接”,并傻傻地等待一个永远不会到来的客户端数据,这白白地浪费了服务器的宝贵资源。
- 在上述连接关闭后,那个被滞留在网络中的、早已失效的
-
三次握手如何解决?
- 在三次握手的模型下,即使服务器B收到了那个失效的
SYN_1
并回复了SYN+ACK
,只要它收不到客户端A的第三次握手的ACK
确认,它就知道这可能是一个历史连接,就不会真正地进入ESTABLISHED
状态,从而避免了资源浪费。
- 在三次握手的模型下,即使服务器B收到了那个失效的
2. 确保双方都能同步初始序列号 (ISN)
- TCP的可靠传输,是建立在序列号和确认号之上的。
- 三次握手的过程,不仅仅是在“打招呼”,更是一个双方交换并确认彼此初始序列号(ISN) 的过程。
- 第一次握手:客户端告诉服务器:“我的初始序列号是x”。
- 第二次握手:服务器告诉客户端:“我收到了你的x,我的初始序列号是y”。
- 第三次握手:客户端告诉服务器:“我收到了你的y”。
- 只有经过这三步,双方才能确信,对方都已经准确地收到了自己的ISN,后续的数据包才能基于正确的序列号进行收发和确认,从而保证了数据传输的有序和不重复。
3. 避免资源浪费(与第一点紧密相关)
这一点是第一点“防止历史连接”的直接结果。
- 如果采用两次握手,服务器在收到一个(可能是失效的)
SYN
包后,就会立即分配资源(如内存缓冲区、TCP控制块等)并建立连接。 - 如果这种失效的连接请求频繁发生,服务器就会因为创建大量无用的“半连接”而耗尽资源,无法为正常的请求提供服务。
- 三次握手,通过增加一次客户端的最终确认,确保了服务器只为那些真正有效的、双向确认的连接请求,才去分配系统资源。
总结一下,TCP的三次握手,看似比两次多了一步,但正是这关键的第三步,才使得TCP协议能够在复杂的、不可靠的网络环境中,可靠地、健壮地建立连接,既能同步好双方的“通信密码”(序列号),又能抵御来自网络中的“历史幽灵包”的攻击,从而避免了资源的浪费。
服务端发送第二个报文后连接的状态进入什么状态
面试官您好,当服务器接收到客户端的第一次握手(SYN
包),并成功发送出第二次握手的响应(SYN+ACK
包)之后,服务器端的TCP连接状态就会进入 SYN-RCVD
状态。
对 SYN-RCVD
状态的进一步解释
- 含义:这个状态的字面意思是 “已接收到SYN” (SYN Received)。它表示服务器已经收到了客户端的连接请求,并且已经将自己的确认和同步请求发送了出去。
- 状态特性:这是一个半连接(Half-Open Connection) 的中间状态。
- 此时,服务器已经为这个连接分配了部分资源(如TCP控制块TCB),并正在等待客户端的最后一次确认(第三次握手的ACK包)。
- 如果服务器能成功收到第三次握手的ACK,它就会从
SYN-RCVD
状态,转换到ESTABLISHED
(已建立) 状态,连接正式建立成功。 - 如果服务器在一定时间内没有收到客户端的ACK,它会重传
SYN+ACK
包。如果重传几次后依然失败,这个半连接最终会超时并被丢弃。
SYN-RCVD
状态与SYN Flood攻击
理解SYN-RCVD
这个状态,对于理解一种常见的网络攻击——SYN Flood攻击——非常有帮助。
- 攻击原理:攻击者会伪造大量的、虚假的源IP地址,然后向服务器疯狂地发送第一次握手的
SYN
包。 - 服务器的反应:服务器在收到这些
SYN
包后,会为每一个请求都创建一个SYN-RCVD
状态的半连接,并向那些伪造的IP地址发送SYN+ACK
包。 - 后果:由于源IP是假的,服务器永远也收不到第三次握手的ACK。这会导致服务器上积压了海量的
SYN-RCVD
状态的半连接,最终耗尽其半连接队列的资源,使其无法再响应任何正常的连接请求,从而达到拒绝服务(DoS)的目的。
所以,SYN-RCVD
是TCP连接建立过程中的一个关键但又脆弱的中间状态。
三次握手和 accept 是什么关系? accept 做了哪些事情?
面试官您好,您提出的这个问题非常好,它揭示了TCP连接建立过程中的两个不同层面:内核层面和应用层面。
简单来说,它们的关系是:TCP的三次握手,完全是由操作系统内核的网络协议栈来完成的。而accept()
方法,则是应用程序从内核中“取走”一个已经完成握手的连接的动作。
一个生动的比喻:餐厅的“迎宾”与“服务员”
我们可以把服务器的监听Socket想象成一家餐厅:
- 操作系统内核:就像是餐厅的 “迎宾和领位员”,他站在门口负责接待所有客人。
- 三次握手:就是“迎宾员”与一位新到客人之间完整的“欢迎-确认”流程。
- 客人(客户端)向迎宾员招手(发送SYN)。
- 迎宾员看到后,回应一个微笑并准备好菜单(发送SYN+ACK)。
- 客人点头确认(发送ACK)。
- 全连接队列(Accept Queue):当“欢迎-确认”流程结束后,这位客人就被领到了一个“等候区”,准备好随时可以点餐了。这个等候区,就是内核的全连接队列。
- 应用程序调用
accept()
:就像是一个 “服务员”(应用程序的工作线程),当他忙完手里的活,发现有客人在等候区时,他就会走过去,把这位客人领到餐桌上,并开始为他点餐服务。这个“领走客人”的动作,就是accept()
。
详细的交互流程
-
服务器准备:服务器应用程序首先调用
socket()
,bind()
,listen()
这三个方法。listen(backlog)
这一步非常关键,它告诉内核:“我已经准备好接收新连接了,请帮我维护两个队列:一个半连接队列(SYN Queue)和一个全连接队列(Accept Queue)。”
-
内核完成三次握手:
- 第一次握手:当客户端的
SYN
包到达,内核会创建一个“半连接”对象,并将其放入半连接队列中。 - 第二次握手:内核回复
SYN+ACK
。 - 第三次握手:当客户端的
ACK
包到达,内核会在半连接队列中找到对应的条目,将其移出,并创建一个“全连接”对象,然后将其放入全连接队列中。 - 至此,TCP的三次握手已经由内核完全独立地完成了! 整个过程,用户态的应用程序是完全无感知的。
- 第一次握手:当客户端的
-
应用程序调用
accept()
:- 应用程序的工作线程,在其逻辑需要处理一个新连接时,会调用
accept()
方法。 accept()
做了哪些事情?- 检查全连接队列:它会首先检查内核的全连接队列是否为空。
- 如果队列不为空:它会从队列的头部,取出一个已经建立好的连接。然后,它会为这个连接创建一个新的、已连接的Socket文件描述符(connected socket),并将其返回给应用程序。应用程序后续与这个特定客户端的所有数据交互,都将通过这个新的Socket来进行。
- 如果队列为空:那么调用
accept()
的这个线程,就会被阻塞,进入睡眠状态,直到有新的连接完成了三次握手,被放入了全连接队列,它才会被唤醒。
- 应用程序的工作线程,在其逻辑需要处理一个新连接时,会调用
总结
- 关系:三次握手是因,
accept
是果。内核负责完成“因”(建立连接),应用程序通过accept
来收获“果”(获取已建好的连接)。 - 分工:
- 内核:负责处理所有底层的、复杂的TCP协议细节,包括握手、挥手、数据包排序、重传等。
accept()
:是应用层与内核之间的一个同步点和数据交接点。它为应用程序提供了一个简单、阻塞式的接口,来安全、有序地从内核中获取新连接。
理解这个分工,对于我们排查像“连接超时”、“SYN Flood攻击”、“全连接队列溢出”等网络问题,非常有帮助。
客户端发送的第一个 SYN 报文,服务器没有收到怎么办?
面试官您好,您提出的这个问题,直击了TCP协议可靠性的一个核心体现。
当客户端发送的第一个SYN
报文,因为网络问题(比如被路由器丢弃)而导致服务器没有收到时,客户端并不会无限期地傻等。它会启动一套健壮的 “超时重传”机制来应对。
1. 核心机制:超时重传 (Timeout Retransmission)
- 工作原理:
- 客户端在发送完
SYN
报文并进入SYN_SENT
状态后,会启动一个定时器。 - 如果在这个定时器超时之前,没有收到服务器的
SYN+ACK
响应,客户端就会认为它发送的第一个SYN
包丢失了。 - 此时,它会重新发送一个完全相同的
SYN
报文(序列号也一样)。
- 客户端在发送完
2. 重传的策略:指数退避 (Exponential Backoff)
为了避免因为网络持续拥堵而导致频繁的无效重传,TCP的重传策略并不是固定间隔的。它采用了一种被称为 “指数退避” 的智能策略。
- 具体表现:每一次重传的超时等待时间,都会比上一次翻倍。
- 第一次重传,可能在1秒后。
- 如果还没收到响应,第二次重传,就会在2秒后。
- 第三次,在4秒后。
- 第四次,在8秒后…以此类推。
- 目的:这种策略,既能在网络状况良好时快速重传,又能在网络拥堵时,自动地拉长重传间隔,给网络留出恢复的时间,避免进一步加剧拥堵。
3. 重传的次数与总时长
-
重传次数:一个客户端到底会重传多少次,这个行为是由操作系统的内核参数来控制的。
- 在Linux系统中,这个参数就是
tcp_syn_retries
。 - 它的默认值通常是5或6。这意味着,除了第一次发送,还会额外重传5次或6次。
- 在Linux系统中,这个参数就是
-
总的超时时间:
- 假设
tcp_syn_retries
为5,那么整个尝试建立连接的过程,正如您精确计算的,会经历1 + 2 + 4 + 8 + 16
秒的等待,在最后一次重传后,还会再等待32
秒。 - 总的超时放弃时间大约是
1+2+4+8+16+32 = 63
秒。 - 如果在这大约一分钟的时间内,客户端始终没有收到服务器的任何回应,它就会最终放弃这次连接尝试,并向上层应用程序返回一个“连接超时”的错误。
- 假设
总结一下,当客户端的第一个SYN
包丢失时,TCP协议会通过一个带“指数退避”策略的、有固定重传次数上限的“超时重传”机制,来努力地、智能地尝试重新建立连接。这套健壮的机制,是TCP之所以能够在不可靠的互联网上,提供可靠连接服务的关键一环。
服务器收到第一个 SYN 报文,回复的 SYN + ACK 报文丢失了怎么办?
面试官您好,您提出的这个问题非常好,它揭示了TCP三次握手过程中,一个非常有趣的、双向的可靠性保障机制。
当服务器发送的第二次握手(SYN+ACK
包)在网络中丢失时,客户端和服务器会从各自的角度,得出不同的“猜测”,并因此各自启动自己的超时重传机制。
1. 客户端的行为:“我的请求是不是丢了?”
- 客户端的视角:客户端在发送完第一次握手的
SYN
包后,它唯一期待的,就是收到服务器的SYN+ACK
。 - 触发行为:如果它在指定的超时时间内,没有收到这个
SYN+ACK
,它无法区分是自己的SYN
包在去程路上丢了,还是服务器的SYN+ACK
在回程路上丢了。 - 采取的行动:出于可靠性的考虑,它会做出最保守的假设——“我的第一个
SYN
包可能丢了”。因此,它会触发超时重传,重新发送一次第一次握手的SYN
包。 - 重传策略:这个重传的次数和间隔,正如您所分析的,是由Linux内核的
tcp_syn_retries
参数来控制的。
2. 服务器的行为:“我的回应对方收到了吗?”
- 服务器的视角:服务器在发送完第二次握手的
SYN+ACK
包后,它的状态进入了SYN-RCVD
。它唯一期待的,就是收到客户端的第三次握手的ACK
确认包。 - 触发行为:如果它在指定的超时时间内,没有收到这个
ACK
,它就会认为——“我刚才发送的那个SYN+ACK
包,可能在路上丢了,对方没收到”。 - 采取的行动:因此,它也会触发超时重传,重新发送一次第二次握手的
SYN+ACK
包。 - 重传策略:这个重传的次数和间隔,是由Linux内核的
tcp_synack_retries
参数来控制的。
3. 最终会发生什么?
- 在这个场景下,网络中就会同时存在客户端重传的
SYN
和服务器重传的SYN+ACK
。 - 最终,只要网络恢复,总有一方的数据包会先到达对端,从而打破僵局,让握手流程继续下去。
- 比如,服务器先收到了客户端重传的
SYN
,它可能会重新发送一个SYN+ACK
。 - 或者,客户端先收到了服务器重传的
SYN+ACK
,它就会发送第三次握手的ACK
,连接建立。
- 比如,服务器先收到了客户端重传的
总结一下,第二次握手的丢失,会触发一个非常有趣的 “双向重传” 场景:
- 客户端因为没收到对它
SYN
的确认,所以会重传SYN
。 - 服务器因为没收到对它
SYN+ACK
的确认,所以会重传SYN+ACK
。
这个机制,完美地体现了TCP协议在设计上的严谨和鲁棒性。它确保了在任何一方的报文可能丢失的情况下,通信双方都有能力通过各自的重传机制,来努力地将连接建立起来,而不是陷入无限的等待。
第一次握手,客户端发送SYN报后,服务端回复ACK包,那这个过程中服务端内部做了哪些工作?
面试官您好,您提出的这个问题非常好,它揭示了从收到第一个SYN
包,到连接最终被应用程序accept
,服务器内核内部一个非常精巧的、基于两个队列的工作流程。
这个过程,我们可以把它比喻成一个餐厅的“排队系统”:
- 半连接队列 (SYN Queue):就像是餐厅门口的 “取号等位区”。
- 全连接队列 (Accept Queue):就像是已经安排好座位、但服务员还没来点餐的 “就座等候区”。
现在,我们来看一下服务器内核具体做了哪些工作:
第一步:收到第一次握手(SYN
包)—— “客人取号”
- 当服务器的网卡接收到客户端发来的
SYN
请求包后,内核会进行基本的校验。 - 如果校验通过,内核会为这个潜在的连接,创建一个“半连接”状态的内核对象(TCP控制块TCB)。
- 然后,它会将这个半连接对象,放入半连接队列(SYN Queue) 中。
- 此时,服务器进入
SYN-RCVD
状态。 - 最后,内核会构造一个
SYN+ACK
报文,并将其发送回客户端。
- 比喻:一个客人(客户端)来到餐厅门口,迎宾员(内核)给了他一个排队号,并让他去 “取号等位区”(半连接队列) 坐着,同时告诉他:“您的号我记下了,请稍等。”
第二步:收到第三次握手(ACK
包)—— “从等位区到座位区”
- 当服务器接收到来自客户端的、用于完成第三次握手的
ACK
确认包后,内核会根据ACK
包中的信息,去半连接队列中查找对应的那个半连接对象。 - 如果找到了,说明这是一个合法的、完成了三次握手的连接。
- 内核会将这个连接对象,从半连接队列中移除。
- 然后,内核会创建一个 “全连接”状态的内核对象,并将其放入全连接队列(Accept Queue) 中。
- 此时,对于内核来说,这个TCP连接已经完全建立成功,进入
ESTABLISHED
状态。
- 比喻:迎宾员听到客人回应后,确认了他的排队号,于是把他从 “取号等位区”(半连接队列),领到了 “就座等候区”(全连接队列) 的一张空桌子上,并告诉他:“您请坐,服务员马上就来。”
第三步:应用程序调用accept()
—— “服务员开始服务”
- 此时,这个已经建立好的连接,正在全连接队列中“排队”,等待被上层的应用程序来“取走”和处理。
- 当我们的服务器应用程序(比如一个Tomcat或Nginx的工作线程)调用
accept()
系统调用时,内核就会从全连接队列的队头,取出一个连接,并为它创建一个新的文件描述符,然后返回给应用程序。 - 至此,这个连接的控制权,才真正地从内核交到了应用程序的手中。
总结一下,从收到第一个SYN
到回复ACK
的这个过程中,服务器内核的核心工作,就是通过半连接队列来管理那些**“正在握手”的连接,再通过全连接队列来存放那些“已经握手成功,但还没被应用程序处理”**的连接。
这两个队列的大小,分别由内核参数tcp_max_syn_backlog
和listen()
系统调用中的backlog
参数来控制。理解这两个队列,对于我们排查像SYN Flood攻击(耗尽半连接队列)或连接超时(全连接队列满了)等问题,至关重要。
大量SYN包发送给服务端服务端会发生什么事情?
面试官您好,您提出的这个场景,正是一种非常经典的、被称为SYN Flood(SYN洪水)的DDoS(分布式拒绝服务)攻击。
当服务端在短时间内收到海量的、伪造源IP的SYN
包时,它的TCP半连接队列(SYN Queue)会面临巨大的压力,并最终可能导致正常的客户端无法建立连接。
1. 攻击原理:耗尽半连接队列
我们先回顾一下服务器处理SYN
包的正常流程:
- 服务器内核收到一个
SYN
包后,会创建一个“半连接”对象。 - 将这个半连接对象放入半连接队列(SYN Queue) 中。
- 向客户端(源IP)回复一个
SYN+ACK
包。 - 等待客户端的第三次握手
ACK
包,收到后,才将连接从半连接队列中移出。
SYN Flood攻击就是利用了这个过程:
- 攻击者会伪造大量的、虚假的源IP地址,然后用这些假IP,向目标服务器疯狂地发送
SYN
包。 - 服务器在收到这些
SYN
包后,会忠实地为每一个请求都创建一个半连接,并放入半连接队列,然后向那些根本不存在的假IP回复SYN+ACK
。 - 由于源IP是假的,服务器永远也等不到第三次握手的
ACK
。 - 后果:半连接队列会被这些“僵尸连接”迅速地占满。当队列满了之后,所有后续到达的、包括正常用户的合法
SYN
请求,都会被内核直接丢弃。从用户的角度看,就是网站无法访问了。
2. 如何防御?—— 多层次的应对策略
防御SYN Flood攻击,需要从多个层面入手,对Linux内核的网络参数进行调优。主要有以下几种有效的方法:
-
方法一:增大半连接队列的容量
- 做法:通过调大内核参数
net.ipv4.tcp_max_syn_backlog
的值(比如从默认的1024调整到2048或更高),来直接增加半连接队列的容量。 - 效果:这是一种简单直接的“扩容”方法,可以提高服务器抵抗攻击的“血量”,但它不能从根本上解决问题,如果攻击流量足够大,队列依然会被打满。
- 做法:通过调大内核参数
-
方法二:开启
tcp_syncookies
机制- 做法:通过设置
net.ipv4.tcp_syncookies = 1
来开启此功能。 - 原理:这是一种非常巧妙的机制。当半连接队列满了之后,服务器不再将新的半连接放入队列。取而代之的是,它会根据收到的
SYN
包的源IP、端口等信息,通过一个特殊的算法,计算出一个 “Cookie”值。然后,它会将这个Cookie,编码到回复的SYN+ACK
包的初始序列号中。 - 验证:如果这是一个合法的客户端,它在收到
SYN+ACK
后,会回复第三次握手的ACK
。当服务器收到这个ACK
时,它会从ACK
包的确认号中,反解出之前的那个Cookie,并验证其合法性。如果验证通过,服务器就直接创建一个全连接,而无需再依赖半连接队列。 - 效果:
syncookies
机制使得服务器在半连接队列溢出后,依然有能力处理正常的连接请求,极大地增强了防御能力。
- 做法:通过设置
-
方法三:减少
SYN+ACK
的重传次数- 做法:通过调小内核参数
net.ipv4.tcp_synack_retries
的值(比如从默认的5次减少到2或3次)。 - 效果:这个参数决定了服务器在未收到客户端
ACK
时,会重传SYN+ACK
多少次。减少重传次数,可以让那些“僵尸半连接”更快地从队列中超时并被清除,从而更快地为正常连接腾出空间。
- 做法:通过调小内核参数
-
方法四:增大
netdev_max_backlog
- 做法:调大
net.core.netdev_max_backlog
参数。 - 效果:这个参数定义了当网卡接收数据包的速度大于内核处理速度时,允许在网卡的“输入队列”中缓存多少个数据包。在遭受SYN Flood攻击时,这个队列也可能被瞬间打满。适当增大它可以提高系统的应对能力。
- 做法:调大
总结一下,应对SYN Flood攻击,我会采用一套组合拳:
- 首先,通过调大
tcp_max_syn_backlog
来提升基础防御能力。 - 然后,开启
tcp_syncookies
作为最核心的、在队列溢出后的“救生筏”。 - 同时,通过减少
tcp_synack_retries
来加速“僵尸连接”的清理。
通过这些内核层面的精细化调优,可以有效地缓解SYN Flood攻击带来的影响。当然,对于超大规模的DDoS攻击,最终还需要依赖专业的硬件防火墙和云服务商的流量清洗服务。
TCP 四次挥手过程说一下?
面试官您好,TCP的四次挥手,是通信双方为了优雅地、完全地断开一个TCP连接而进行的、一个双向的告别过程。
它的核心思想是 “半关闭”(Half-Close),即TCP连接是全双工的,因此连接的关闭也必须是双向的,每一方都需要独立地关闭自己的“发送”通道。
我们可以把它比喻成一次礼貌的电话挂断过程。
第一次挥手:客户端的“告别”请求
- 谁发起:主动关闭方,通常是客户端(Client)。
- 做什么:
- 客户端的数据已经全部发送完毕,它决定关闭连接。
- 它会向服务器发送一个TCP报文,将头部的
FIN
标志位置为1
。
- 含义:“我的话说完了(我不会再给你发数据了),我请求关闭连接。”
- 状态变化:客户端进入
FIN_WAIT_1
状态。 - 比喻:客户端(A)说:“B,我的事说完了,我这边准备挂了啊。”
第二次挥手:服务器的“收到”确认
- 谁发起:被动关闭方,服务器(Server)。
- 做什么:
- 服务器在收到客户端的
FIN
包后,知道对方已经不会再发数据了。 - 它会立刻回复一个
ACK
确认包。
- 服务器在收到客户端的
- 含义:“好的,收到了,我知道你话说完了。”
- 状态变化:
- 服务器进入
CLOSE_WAIT
状态。此时,服务器的 “接收”通道关闭了,但它的 “发送”通道可能还没关闭。 - 客户端在收到这个
ACK
后,进入FIN_WAIT_2
状态,等待服务器的“告别”。
- 服务器进入
- 为什么需要这一步(半关闭的体现)?
- 此时服务器可能还有一些尚未发送完毕的数据需要发送给客户端。TCP协议允许服务器在进入
CLOSE_WAIT
状态后,继续发送这些剩余的数据。客户端在FIN_WAIT_2
状态下,依然可以接收这些数据。
- 此时服务器可能还有一些尚未发送完毕的数据需要发送给客户端。TCP协议允许服务器在进入
第三次挥手:服务器的“告别”请求
- 谁发起:服务器(Server)。
- 做什么:
- 当服务器这边也确认所有数据都已发送完毕后,它也准备关闭连接了。
- 它会向客户端发送一个TCP报文,同样将头部的
FIN
标志位置为1
。
- 含义:“好了,我的话也说完了,我这边也准备挂了。”
- 状态变化:服务器发送完后,进入
LAST_ACK
状态,等待客户端的最后确认。 - 比喻:服务器(B)说:“A,我这边也处理完了,我也可以挂了。”
第四次挥手:客户端的“最终确认”
-
谁发起:客户端(Client)。
-
做什么:
- 客户端在收到服务器的
FIN
包后,知道对方也准备关闭了。 - 它会向服务器发送最后一个
ACK
确认包。
- 客户端在收到服务器的
-
含义:“好的,收到,那我们都挂了吧。”
-
状态变化:
- 客户端发送完这个
ACK
后,并不会立即关闭,而是进入了非常关键的TIME_WAIT
状态。它会在这里等待2个MSL(Maximum Segment Lifetime,报文最大生存时间)的时长。 - 服务器在收到这个最后的
ACK
后,就认为连接已完全关闭,立即进入CLOSED
状态。
- 客户端发送完这个
-
客户端为什么需要等待
2MSL
?- 保证第四次握手的
ACK
能可靠地到达服务器:如果这个最后的ACK
在路上丢失了,服务器在LAST_ACK
状态下会收不到确认,它就会超时重传第三次握手的FIN
包。客户端在TIME_WAIT
状态下,还能接收到这个重传的FIN
,并再次发送ACK
,从而保证服务器能正常关闭。 - 防止已失效的“历史报文”:等待2MSL,可以确保本次连接中,所有在网络中滞留的报文段都已自然消失,不会在新建立的连接中(如果使用了相同的端口号)造成数据混淆。
- 保证第四次握手的
-
最终关闭:客户端在等待完
2MSL
后,也进入CLOSED
状态。至此,整个TCP连接才算完全、干净地关闭。
总结一下,TCP四次挥手通过一个双向的、各自独立的关闭请求和确认过程,优雅地实现了全双工连接的可靠关闭,并用巧妙的TIME_WAIT
状态,保证了整个关闭过程的健壮性。
为什么4次握手中间两次不能变成一次?
面试官您好,您提出的这个问题非常好,它直击了TCP四次挥手和三次握手在设计上的一个根本性差异。
第二次和第三次挥手之所以不能像三次握手那样合并成一次,其核心原因在于:TCP连接是全双工的,关闭连接时,需要处理“半关闭”(Half-Close)的状态。
1. 回顾三次握手:为什么可以合并?
- 在三次握手时,当服务器收到客户端的
SYN
请求后,它处于LISTEN
状态,并没有任何应用数据需要发送。 - 此时,它既需要确认对方的请求(发送
ACK
),又需要发起自己的同步请求(发送SYN
)。这两个动作在逻辑上是紧密耦合的,完全可以放在一个报文中同时完成。所以,SYN
和ACK
可以合并,使得握手只需要三次。
2. 四次挥手:为什么不能合并?—— “半关闭”状态的需要
正如您所分析的,当服务器收到客户端发来的第一次挥手的FIN
报文时,情况就完全不同了。
-
FIN
报文的含义:它只代表客户端这个方向的“发送”通道已经关闭了。即,客户端告诉服务器:“我不会再给你发新的业务数据了。” -
服务器此时的处境:
- 内核的“立即响应”:当服务器的TCP协议栈收到这个
FIN
后,它作为一个可靠的协议,必须立即回复一个ACK
,来告知客户端:“好的,我收到了你的关闭请求。” 这个动作是由内核自动、立刻完成的。此时,服务器进入CLOSE_WAIT
状态。 - 应用程序的“未尽事宜”:然而,此时服务器的应用程序可能还有数据没有发送完毕。比如,客户端请求了一个大文件,客户端这边决定提前关闭(不再接收),但服务器这边可能还有几十MB的数据在发送缓冲区里,正准备发给客户端。
- 内核的“立即响应”:当服务器的TCP协议栈收到这个
-
控制权的解耦:
- 因此,TCP协议将“回复ACK”和“发送自己的FIN”这两个动作的控制权分离开了。
- 回复ACK:是内核的职责,目的是为了协议的可靠性,必须立即执行。
- 发送FIN:则是应用程序的职责。应用程序需要继续处理完自己剩余的业务(比如发送完所有数据),直到它认为自己也无事可做了,才会去调用
close()
方法。只有这时,内核才会发送第二次挥手的FIN
包。
一个生动的比喻:结束一场对话
- 客户端(A)对服务器(B) 说:“我的话说完了。” (第一次挥手, FIN)
- 服务器(B) 立刻回答:“好的,收到了。” (第二次挥手, ACK)
- 然后,服务器(B) 可能还需要一些时间,来把自己想说的最后一句话讲完。比如,他又继续讲了10秒钟。(
CLOSE_WAIT
状态下继续发送数据) - 当服务器(B) 也把所有话都讲完后,他才说:“好了,现在我的话也说完了。” (第三次挥手, FIN)
- 客户端(A) 最后确认:“好的,那我们都挂了吧。” (第四次挥手, ACK)
总结一下,第二次和第三次挥手之间,可能存在一个时间差,这个时间差就是留给服务器应用程序处理剩余数据的。正是因为这个潜在的时间差,以及ACK由内核控制和FIN由应用控制这种职责的分离,导致了这两个报文在绝大多数情况下,都必须分开发送,从而构成了完整的四次挥手。
断开连接时客户端 FIN 包丢失,服务端的状态是什么?
面试官您好,您提出的这个问题非常好,它揭示了TCP四次挥手过程中,如果发生丢包,通信双方状态可能出现不一致的情况。
当客户端发送的第一次挥手(FIN
包)在网络中丢失,导致服务端完全没有收到时:
1. 服务端的状态:始终是ESTABLISHED
- 核心原因:对于服务端来说,它完全不知道客户端有任何想要关闭连接的意图。
- 具体表现:
- 在服务端的视角里,这个TCP连接依然是一个完全正常的、已建立的连接。它的状态会始终保持在
ESTABLISHED
。 - 它会继续占用着系统为这个连接分配的所有资源(如内存缓冲区、文件描述符等)。
- 如果此时服务端有数据要发送给客户端,它会正常地发送出去。
- 在服务端的视角里,这个TCP连接依然是一个完全正常的、已建立的连接。它的状态会始终保持在
2. 客户端的行为与最终结果
与此同时,在客户端这边,它会启动一套超时重传机制:
-
超时重传
FIN
包:客户端在发送FIN
包并进入FIN_WAIT_1
状态后,如果迟迟收不到服务端的ACK
确认,它会认为自己的FIN
包丢了,于是会多次重传FIN
包。- 在Linux系统中,这个重传的次数由内核参数
tcp_orphan_retries
来控制。
- 在Linux系统中,这个重传的次数由内核参数
-
客户端单方面关闭:
- 如果客户端在重传了
tcp_orphan_retries
次之后,依然没有收到服务端的任何回应。 - 此时,客户端会认为服务器已经不可达或彻底崩溃了。它会放弃这次关闭尝试,在等待最后一个超时周期结束后,单方面地关闭连接,进入
CLOSED
状态,并释放本地的连接资源。
- 如果客户端在重传了
3. 最终导致的“半连接”状态
- 结果:现在,就出现了一个不一致的状态:
- 客户端:认为连接已经关闭了。
- 服务端:认为连接还好好地处于
ESTABLISHED
状态。
- 这个只在服务器端单方面存在的连接,就成了一个 “半连接(Half-Open)” 或 “孤儿连接(Orphaned Connection)”。
4. 对服务器的影响与解决方案
- 影响:这种“半连接”会持续地、无效地占用服务器的资源。如果因为网络问题,导致大量这样的半连接在服务器端堆积,最终可能会耗尽服务器的连接资源。
- 如何解决?—— TCP
Keepalive
机制- 为了清理这些“僵尸连接”,TCP协议自身提供了一个
Keepalive
(保活)机制。 - 当一个连接长时间(默认是2小时)没有任何数据交互时,服务器的TCP协议栈会主动发送一个“保活探测包”给客户端。
- 由于客户端此时已经关闭了连接,它收到这个探测包后,会回复一个
RST
(重置) 报文。 - 当服务器收到这个
RST
报文后,它就知道这个连接其实早就失效了,于是就会立即关闭连接,并回收资源。
- 为了清理这些“僵尸连接”,TCP协议自身提供了一个
总结一下,当客户端的第一个FIN
包丢失时,服务端会一直保持在ESTABLISHED
状态,完全无感知。直到客户端重传多次失败、单方面放弃后,这个连接在服务端就成了一个“孤儿连接”。最终,这个孤儿连接需要依靠TCP的 Keepalive
机制来被动地发现和清理。
为什么四次挥手之后要等2MSL?
面试官您好,您提出的这个问题,触及了TCP四次挥手设计中,一个非常关键、也是为了保证连接 “干净”关闭的鲁棒性(Robustness) 设计。
客户端在发送完第四次挥手的最后一个ACK
确认包后,之所以不立即关闭,而是要进入一个 TIME_WAIT
状态并等待2MSL,其背后有两个核心目的。
首先,我们需要理解什么是MSL。
- MSL (Maximum Segment Lifetime,报文最大生存时间):它指的是一个TCP报文段,在复杂的互联网中,能够存活的最长时间。超过这个时间,就可以认为这个报文段已经在网络中自然消失了。在Linux中,这个值通常被设定为30秒或60秒。
现在,我们来看TIME_WAIT
状态等待2MSL
的两大原因:
1. 原因一:保证最后一次ACK
能可靠地到达,让对方能正常关闭(主要原因)
这是TIME_WAIT
状态最核心、最重要的作用。
-
问题场景:四次挥手的最后一步,是客户端(主动关闭方)向服务器(被动关闭方)发送一个
ACK
。但这个ACK
报文,是有可能在网络中丢失的。 -
如果没有
TIME_WAIT
会发生什么?- 客户端发送完
ACK
后,立刻进入CLOSED
状态,释放了所有连接资源。 - 服务器因为没有收到这个
ACK
,它会一直停留在LAST_ACK
状态。在等待一个超时周期后,它会重传FIN
报文给客户端。 - 但此时,客户端已经“关门大吉”了,它不认得这个连接。收到这个重传的
FIN
后,它的TCP协议栈可能会回复一个RST
(重置) 报文。 - 服务器收到
RST
后,会认为这是一个错误,而不是一次正常的关闭,可能会导致一些应用层面的异常。
- 客户端发送完
-
有了
TIME_WAIT
状态如何解决?- 正如您精辟的分析,
TIME_WAIT
状态就像是客户端在说:“我再等一会儿,以防万一。” - 如果最后的
ACK
丢失了,服务器重传的FIN
包,会在2MSL
这个时间窗口内,到达还处于TIME_WAIT
状态的客户端。 - 客户端发现自己收到了一个重复的
FIN
,就知道是自己的ACK
丢了。于是,它会重新发送一次ACK
。 - 这样,就保证了服务器最终一定能收到确认,从而正常、优雅地进入
CLOSED
状态。 - 为什么是
2MSL
? 正如您所说,这个时间,正好覆盖了一个 “一来一回” 的最大耗时:一个ACK
丢失的最长路程(1MSL),加上对方FIN
重传回来的最长路程(1MSL)。
- 正如您精辟的分析,
2. 原因二:防止已失效的“历史连接报文”干扰新连接
- 问题场景:假设没有
TIME_WAIT
状态,一个连接(比如源IP:源端口 - 目标IP:目标端口
)刚被关闭,客户端立刻又用完全相同的四元组,与服务器建立了一个新的连接。 - 风险:此时,网络中可能还“漂浮”着上一个旧连接的、因为网络延迟而迟迟未到达的数据包。
- 后果:这个“历史遗留”的数据包,可能会在新的连接建立后,才到达服务器。服务器会误以为这是新连接发来的合法数据,从而造成数据混淆和逻辑错误。
TIME_WAIT
如何解决?- 通过等待
2MSL
这个足够长的时间,可以确保上一个连接在网络中产生的所有报文段,都已经自然地、彻底地消失了。 - 之后再建立新的连接,就保证了新连接的“纯洁性”,不会受到任何历史报文的干扰。
- 通过等待
总结一下,TIME_WAIT
状态等待2MSL
,是TCP协议为了应对不可靠网络而设计的一个 “保险”机制。它通过“多等一会儿”,既保证了本次连接的可靠关闭,又保证了未来新连接的纯净,是TCP鲁棒性设计的一个典范。
服务端出现大量的 TIME_WAIT 有哪些原因?
面试官您好,您提出的这个问题非常好。通常我们都认为TIME_WAIT
状态主要出现在客户端,但在特定场景下,服务端确实也可能出现大量的TIME_WAIT
状态。
要理解这个问题,我们首先要抓住一个核心原则:在TCP四次挥手中,哪一方主动发起关闭(即先发送第一个FIN
包),哪一方在挥手结束后,就会进入TIME_WAIT
状态。
所以,“服务端出现大量TIME_WAIT
”这个现象,其根本原因只有一个:在这些TCP连接中,是服务端主动发起了连接关闭。
那么,在常见的Web服务(如Nginx)中,有哪些典型的场景会导致服务端主动关闭连接呢?
1. 场景一:客户端使用了HTTP/1.0或明确指定了短连接
- HTTP/1.0:在早期的HTTP/1.0协议中,默认的行为是短连接。即服务器在发送完一次HTTP响应后,就会主动关闭TCP连接。
- HTTP/1.1的
Connection: close
:即使在使用HTTP/1.1时,如果客户端在请求头中明确发送了Connection: close
,它就是在告诉服务器:“处理完我这个请求就断开吧。” 服务器在收到这个指令后,也会在响应完毕后,主动关闭连接。 - 后果:在这种模式下,每一次请求都会导致服务端在处理完后,进入一次
TIME_WAIT
状态。如果QPS很高,就会在短时间内累积大量的TIME_WAIT
连接。
2. 场景二:HTTP长连接空闲超时 (Keep-Alive Timeout)
这是最常见、最主要的原因。
- HTTP/1.1长连接(Keep-Alive):为了提升性能,HTTP/1.1默认使用长连接,即一个TCP连接可以复用,处理多个HTTP请求。
- 服务器的保护机制:但是,为了防止客户端一直占着连接不放,导致服务器资源被耗尽,Web服务器(如Nginx)都会配置一个空闲超时时间(比如Nginx的
keepalive_timeout
,默认可能是65秒)。 - 触发关闭:如果一个客户端在一个已经建立的长连接上,超过了这个设定的超时时间,都没有再发送任何新的请求,那么服务器就会认为这个客户端已经“失联”或不再需要服务了。此时,服务器会主动发起四次挥手,来关闭这个空闲的连接,以回收资源。
- 后果:在一个高并发、客户端来了又走的系统中,会有大量的连接因为空闲而达到超时阈值,从而导致服务端产生大量的
TIME_WAIT
状态。
3. 场景三:HTTP长连接请求数量达到上限
- 服务器的另一个保护机制:一些Web服务器还允许配置一个连接上可以处理的最大请求数(比如Nginx的
keepalive_requests
,默认是100)。 - 触发关闭:当一个长连接上处理的请求数量,达到了这个预设的上限时,即使客户端马上又发来了新的请求,服务器在处理完这个请求后,也会主动关闭这个连接。
- 目的:这是一种“强制换班”的机制,可以防止某个连接被长期占用,有助于负载均衡和资源的定期回收。
- 后果:同样,这也会导致服务端在达到请求数上限后,进入
TIME_WAIT
状态。
如何应对服务端大量的TIME_WAIT
?
虽然TIME_WAIT
是TCP协议的正常状态,但如果数量过多(比如几十万个),会占用大量的端口号和内存资源。在Linux系统上,我们可以通过调整内核参数来进行优化:
- 开启
net.ipv4.tcp_tw_reuse = 1
:允许将处于TIME_WAIT
状态的socket,用于新的TCP连接。这是最推荐的优化方式,能非常有效地复用端口。 - 开启
net.ipv4.tcp_tw_recycle = 1
(Linux 4.12版本后已废弃):这个参数比reuse
更激进,它会快速回收TIME_WAIT
连接。但因为它在NAT环境下存在严重的兼容性问题,所以现在已经不推荐使用。 - 调小
net.ipv4.tcp_fin_timeout
:缩短TIME_WAIT
状态的等待时间(虽然MSL是固定的,但这个参数可以影响最终的超时行为),但这可能会降低连接的可靠性,需要谨慎调整。
总结一下,服务端出现大量TIME_WAIT
,其根源是服务端在特定HTTP策略下(短连接、空闲超时、请求数超限)主动关闭了连接。我们可以通过理解这些场景,并合理配置服务器的keep-alive
参数和Linux内核的TCP参数,来对其进行有效的管理和优化。
TCP和UDP区别是什么?
面试官您好,TCP和UDP是传输层两个最核心、但设计哲学完全不同的协议。它们没有绝对的优劣之分,而是分别为了不同的应用场景,在 “可靠性” 和 “效率” 之间做出了不同的极致选择。
我们可以用一个生动的比喻来理解它们:
- TCP (传输控制协议):就像是打电话。
- UDP (用户数据报协议):就像是寄平信。
下面我将从几个核心维度,来详细对比它们的区别:
1. 连接性与服务对象
- TCP (打电话):是面向连接的。在正式通话(传输数据)前,必须先通过 “三次握手” 拨通电话、建立连接。并且,一次通话只能是一对一的。
- UDP (寄平信):是无连接的。写好信直接扔进邮筒就行,不需要先跟对方确认“我要给你寄信了”。因此,它可以支持一对一、一对多、多对多的通信(广播、多播)。
2. 可靠性 (最核心的区别)
- TCP (打电话):提供极其可靠的传输。
- 如何保证? 它通过序列号、确认应答(ACK)、超时重传、数据校验等一系列复杂机制,来保证数据能够不重、不丢、不乱地、按顺序地到达对方。通话中如果有一句没听清,你会说“麻烦再说一遍”,这就是重传。
- UDP (寄平信):提供不可靠的、“尽力而为” 的传输。
- 如何工作? 它只负责把数据包“扔”出去,但不保证这个包能否到达、什么时候到达、是否按顺序到达。信寄出去,可能会寄丢,也可能几封信的到达顺序和寄出顺序不一致。
3. 流量控制与拥塞控制
- TCP (打电话):有精密的流量控制和拥塞控制机制。
- 流量控制:通过“滑动窗口”机制,接收方可以告诉发送方:“我这边的缓冲区快满了,你慢点发。”
- 拥塞控制:通过慢启动、拥塞避免等算法,TCP能够感知到网络整体的拥堵状况,并主动地调整自己的发送速率,避免加剧网络拥堵。
- UDP (寄平信):完全没有。它会按照应用层给定的速度,持续地、不管不顾地向网络中发送数据。即使网络已经堵得一塌糊涂了,它也“我行我素”。
4. 传输方式与头部开销
- TCP (打电话):是面向字节流的。它没有消息边界的概念,数据像水流一样传输。应用程序需要自己处理“粘包”和“半包”问题。其头部较大(基础20字节),包含了序列号、确认号、窗口大小等大量控制信息。
- UDP (寄平信):是面向数据报的。发送方发一个包,接收方就收一个完整的包,它保留了消息的边界。其头部极小(固定8字节),开销非常低。
总结与应用场景
特性 | TCP (打电话) | UDP (寄平信) |
---|---|---|
连接性 | 面向连接 | 无连接 |
可靠性 | 可靠 | 不可靠 |
流控/拥塞控制 | 有 | 无 |
头部开销 | 大 (≥20B) | 小 (8B) |
传输模式 | 流式 | 数据报式 |
速度 | 慢 | 快 |
基于这些区别,它们的适用场景非常明确:
-
选择TCP的场景:所有对数据可靠性要求极高的应用。
- HTTP/HTTPS:网页浏览,一个字节都不能错。
- FTP:文件传输,必须保证文件完整。
- SMTP/POP3:邮件收发。
-
选择UDP的场景:所有对实时性要求高、能容忍少量丢包的应用。
- 在线视频/语音通话:丢掉一两帧画面或一个音节,影响不大,但卡顿是不能接受的。
- 实时游戏:要求玩家的操作能以最快速度同步。
- DNS查询:一次简单的查询,追求极致的速度。
- 现代的一些协议,比如Google的QUIC(HTTP/3的基础),就是尝试在UDP之上,自己实现一套可靠传输和拥塞控制机制,以期结合TCP的可靠性和UDP的低延迟优势。
TCP为什么可靠传输
面试官您好,TCP之所以能够在不可靠的、复杂的互联网上,提供一个看似“可靠”的传输通道,是因为它在内部实现了一整套极其精巧和完备的、应对各种网络异常的机制。
这个“可靠性”,并不是单一的技术,而是一个由多个机制协同工作,共同达成的目标。正如您所分析的,主要包含以下几个方面:
1. 基础保障:面向连接的管理
- 机制:三次握手和四次挥手。
- 解决了什么问题?
- 在传输数据之前,通过三次握手,确保了通信双方的收发能力都正常,并同步好了初始序列号,为后续的可靠传输奠定了基础。
- 在传输数据之后,通过四次挥手,确保了双方的数据都能完整地发送完毕,并优雅地关闭连接,避免了数据丢失。
2. 核心机制:序列号、确认应答与超时重传
这三者共同构成了一个 “数据确认与重发” 的闭环,是TCP可靠性的核心。
-
机制一:序列号 (Sequence Number)
- 做什么? TCP会将发送出去的每一个字节都进行编号。这个序列号,就是数据包中第一个字节的编号。
- 解决了什么问题?
- 保证有序性:接收方可以根据序列号,对可能乱序到达的数据包,进行正确的排序和重组。
- 去重:如果接收方收到了重复的序列号,就知道这是一个重传的包,可以直接丢弃。
-
机制二:确认应答 (Acknowledgment, ACK)
- 做什么? 接收方每收到一个数据包,都需要向发送方回复一个ACK确认包。这个包里的确认号,表示 “这个序号之前的所有数据,我都已经安全收到了”。
- 解决了什么问题? 它为发送方提供了一个明确的反馈,让发送方知道哪些数据已经被对方成功接收。
-
机制三:超时重传 (Timeout Retransmission)
- 做什么? 发送方在发送数据后,会启动一个定时器。如果在指定时间内,没有收到对方对这个数据包的ACK确认。
- 解决了什么问题?
- 数据包丢失:发送方会认为这个数据包在路上丢了,于是会重新发送一次。
- ACK包丢失:即使是ACK包丢了,发送方同样会因为超时而重传数据。接收方收到重复数据后,会丢弃数据,但会重新发送一次ACK。
- 通过这个机制,TCP保证了数据“不丢失”。
3. 性能与网络适应性保障
-
机制四:流量控制 (Flow Control)
- 做什么? 通过TCP头部的 “窗口大小(Window Size)” 字段来实现。接收方通过这个字段,告诉发送方:“我本地的接收缓冲区,现在还能容纳多少数据。”
- 解决了什么问题? 防止了发送方发送速度过快,导致接收方缓冲区溢出而被动丢包的问题。它实现了端到端的速率匹配。
-
机制五:拥塞控制 (Congestion Control)
- 做什么? 这是TCP最高级、最智能的机制。它通过慢开始、拥塞避免、快重传、快恢复等一系列算法,来感知整个网络的拥堵状况。
- 解决了什么问题?
- 当TCP判断网络发生拥堵时(比如发生了超时重传),它会主动地、智能地降低自己的发送速率。
- 这不仅是为了保证自己的数据能成功送达,更是为了避免加剧整个网络的拥堵,保证了互联网的整体稳定性。它让TCP成为了一个“有社会责任感”的协议。
总结一下,TCP的可靠性,是一个层层递进的保障体系:
- 连接管理是前提。
- 序列号+确认应答+超时重传的组合,是保证数据不丢、不重、不乱的核心。
- 而流量控制和拥塞控制,则是在此基础上,为了适应不同的接收能力和网络状况,而进行的高级动态调整,保证了传输的高效和稳定。
TCP粘包怎么解决?
面试官您好,您提出的这个问题非常好,它直击了HTTP/3协议设计的核心思想。
直接在UDP之上实现HTTP是不可行的,因为UDP是不可靠的。但我们可以在UDP之上,自己实现一套可靠传输的逻辑。正如您所说,Google主导开发的QUIC协议,正是这个问题的完美答案,它也成为了HTTP/3的基石。
1. 为什么会有这个“疯狂”的想法?—— TCP的“队头阻塞”之痛
在回答“如何实现”之前,我们先要理解“为什么这么做”。其根本动机,是为了解决TCP协议自身难以逾越的“队头阻塞(Head-of-Line Blocking)”问题。
- HTTP/2的困境:虽然HTTP/2通过多路复用,解决了应用层(HTTP层面)的队头阻塞,但它依然运行在TCP之上。
- TCP层面的队头阻塞:TCP是一个严格保证字节流有序的协议。如果在一个TCP连接中,承载了多个HTTP/2的Stream,其中任何一个数据包丢失了,TCP就必须暂停后续所有数据包的处理,等待那个丢失的包被重传回来。即使其他Stream的数据已经到达了,也只能在接收缓冲区里干等着。
- 后果:一个小的网络抖动,就可能导致整个TCP连接上的所有HTTP/2通信被“卡住”。
2. QUIC如何实现?—— 在UDP上“重建”一个更智能的TCP
QUIC的思路是:既然无法改造TCP(因为它在操作系统内核中,升级缓慢),那我们就干脆绕过它,在一个“白板”一样的UDP协议之上,用应用层的代码,重新实现一套更灵活、更智能的可靠传输机制。
正如您所分析的,QUIC主要通过以下几种方式,在UDP之上实现了可靠性:
-
a. 自定义、可插拔的拥塞控制
- 做什么? QUIC内置了一套与TCP类似的、但更先进的拥塞控制机制(如Cubic算法)。它能根据网络状况,智能地调整发送速率。
- 优势:因为是在应用层实现的,所以可以非常灵活地迭代和优化拥塞控制算法,而无需等待操作系统内核的更新。
-
b. 精准的重传与前向纠错(FEC)
- 重传机制:QUIC在自己的协议层,实现了基于Stream和Packet的、更精细的重传。一个Stream的数据包丢失,只会影响到这个Stream本身,而不会阻塞其他并行的Stream。这就从根本上解决了TCP的队头阻塞问题。
- 前向纠错 (Forward Error Correction):正如您提到的,QUIC还可以通过发送少量冗余数据(纠错码)的方式,让接收端在丢失少量数据包时,能够直接自行修复,而无需等待重传,进一步降低了延迟。
-
c. 强大的连接迁移 (Connection Migration)
- TCP的痛点:TCP连接是通过四元组(源IP:源端口, 目的IP:目的端口) 来唯一标识的。一旦你的网络发生变化(比如手机从WiFi切换到4G),IP地址变了,TCP连接就必须中断并重新建立。
- QUIC的解决方案:QUIC的连接,不再由四元组标识,而是由一个64位的、全局唯一的“连接ID”(Connection ID) 来标识。
- 优势:只要这个连接ID不变,无论你的IP地址和端口如何变化,QUIC连接都可以无缝地、快速地迁移,而无需重新握手,极大地提升了移动网络下的用户体验。
3. 握手与加密
- QUIC还将传输层的握手(类似TCP)和安全层的握手(类似TLS)进行了合并。
- 一个典型的QUIC连接,通常只需要1个RTT(网络往返时延) 就可以建立,而TCP+TLS通常需要2-3个RTT。这使得连接建立的速度也更快。
总结一下,要在UDP上实现HTTP,我们不能直接做,而是需要像QUIC那样,在UDP之上,用应用层的代码,重新实现一套更现代、更灵活的可靠传输、拥塞控制和安全加密的逻辑。
QUIC通过解决TCP的队头阻塞、实现快速的连接迁移、以及更高效的握手,最终构建了一个比“TCP+TLS+HTTP/2”性能更优的传输层,成为了HTTP/3的坚实基础。
TCP 粘包怎么解决?
面试官您好,TCP粘包和半包问题,是在进行基于TCP的Socket编程时,必须面对和处理的一个经典问题。
1. 首先,为什么TCP会产生粘包/半包问题?
这个问题的根源,在于TCP协议自身的特性:
-
TCP是面向字节流的(Stream-oriented):
- 在TCP的视角里,它看到的是一串没有边界的、连续的字节数据,就像一条源源不断的河流。它完全不关心上层应用发送的是多少条“消息”。
- 应用程序调用了三次
send
,发送了三条消息,但在TCP看来,这可能只是一大块字节数据。
-
TCP的优化机制:
- Nagle算法:为了提高网络效率,TCP默认会开启Nagle算法。它会把应用层发送下来的多个小的数据包,攒在一起“打包”,凑成一个大的数据包再发送出去。这就很容易导致粘包。
- 发送/接收缓冲区:TCP的发送和接收都有缓冲区。发送方的数据可能在缓冲区里被合并发送;接收方也可能因为处理不及时,导致多个数据包在接收缓冲区里“粘”在了一起。
- MSS/MTU限制:如果应用层发送的一条消息过大,超过了最大报文段长度(MSS),TCP会自动将其拆分成多个小的数据包发送。这就可能导致半包(或称拆包)。
2. 如何解决?—— 在应用层“定义消息边界”
既然TCP本身不提供消息边界,那么解决这个问题的责任,就落在了我们应用层协议的设计上。我们必须自己定义一套规则,来告诉接收方:“一条完整的消息,到哪里结束”。
正如您所分析的,主要有以下三种解决方案:
-
方案一:固定长度的消息 (Fixed-Length Framing)
- 做法:发送方和接收方,事先约定好,每一条消息的长度都是固定的。比如,约定每条消息都是256个字节。
- 接收方处理:接收方每次都从TCP流中,不多不少,正好读取256个字节,作为一个完整的消息来处理。
- 优点:实现最简单。
- 缺点:缺乏灵活性,浪费带宽。如果实际要发送的数据很小(比如只有10个字节),也必须用空余的字节填充到256字节,造成了大量的空间浪费。
-
方案二:使用特殊分隔符 (Delimiter-based Framing)
- 做法:发送方和接收方,事先约定好一个特殊的、不会在消息正文中出现的分隔符(比如回车换行符
\r\n
,或者其他特殊字符组合)。发送方在每条消息的末尾,都加上这个分隔符。 - 接收方处理:接收方不断地从TCP流中读取数据,直到读到了那个特殊的分隔符,就知道一条消息已经完整接收。
- 优点:解决了固定长度的浪费问题,比较灵活。
- 缺点:
- 需要对消息内容进行转义。如果消息正文中恰好包含了这个分隔符,就需要进行转义处理,增加了复杂性。
- 需要逐字节地扫描来寻找分隔符,在消息很大时,性能可能不高。
- 应用:很多基于文本的协议,如HTTP的头部,就是用
\r\n
来分隔的。
- 做法:发送方和接收方,事先约定好一个特殊的、不会在消息正文中出现的分隔符(比如回车换行符
-
方案三:自定义消息结构 —— “长度+内容” (Length-based Framing)
- 做法:这是最常用、最通用、也最推荐的解决方案。我们在每条消息的前面,都加上一个固定长度的头部,这个头部里明确地记录了紧跟其后的消息体的长度。
- 一个典型的协议:
[4字节的消息长度(length)] + [消息体(content)]
- 接收方处理:
- 先接收固定长度的头部(比如4个字节)。
- 从头部中,解析出即将到来的消息体的确切长度
L
。 - 然后,再继续从TCP流中,不多不少,正好读取
L
个字节,作为完整的消息体。
- 优点:极其灵活和高效。它既没有空间浪费,也无需进行内容扫描,是性能和灵活性的最佳结合。
实践中的应用
在现代网络编程中,我们通常不会自己去手动处理这些粘包/半包问题。像Netty这样的高性能网络框架,已经为我们内置了非常完善的解码器(Decoder):
FixedLengthFrameDecoder
DelimiterBasedFrameDecoder
LengthFieldBasedFrameDecoder
我们只需要在构建Netty的ChannelPipeline
时,加入相应的解码器,并配置好参数(如长度、分隔符等),Netty就会自动地、高效地为我们处理好粘包和半包问题。
TCP的拥塞控制介绍一下?
面试官您好,TCP的拥塞控制,是TCP协议中 最高级、也最能体现其“智能”和“社会责任感” 的机制。
如果说流量控制(Flow Control)是为了解决“点对点”之间,发送方不要撑爆接收方的问题,那么拥塞控制(Congestion Control)就是为了解决“点对网”之间,发送方不要撑爆整个网络的问题。
1. 为什么需要拥塞控制?—— 避免“公地悲剧”
正如您所分析的,互联网是一个共享资源。如果每一个TCP连接都只顾自己,以最快的速度疯狂地发送数据,那么很快就会导致网络中的路由器不堪重负,发生拥塞。
- 拥塞的后果:大量的数据包丢失、时延剧增。
- 恶性循环:发送方发现丢包,就会触发超时重传,这又进一步加剧了网络的拥堵,最终可能导致整个网络陷入瘫痪。
为了避免这种“公地悲剧”,TCP被设计成了一个“无私”的协议,它会主动地感知网络的拥堵状况,并自我约束,动态地调整自己的发送速率。
2. 如何实现拥塞控制?—— 拥塞窗口 (cwnd
)
- 核心工具:TCP在发送端,维护了一个名为 “拥塞窗口”(
cwnd
) 的状态变量。 - 它的作用:它代表了在当前网络状况下,发送方被“允许”一次性发送多少个未经确认的数据包。
- 与发送窗口的关系:真正的发送窗口大小,是拥塞窗口(
cwnd
)和接收方通告的滑动窗口(rwnd
)之间的最小值。即:发送窗口 = min(cwnd, rwnd)
。
3. cwnd
如何动态变化?—— 四大核心算法
cwnd
的大小,不是固定的,它会根据网络状况,在一个 “慢启动-拥塞避免-拥塞发生-快速恢复” 的循环中,进行动态调整。
-
a. 慢启动 (Slow Start)
- 何时发生? 在连接刚刚建立时。
- 做什么?
cwnd
的初始值非常小(通常是1到10个MSS)。然后,每收到一个ACK,cwnd
的大小就翻倍(指数级增长)。 - 目的:以一种非常“谨慎”的方式,快速地去试探网络的承载能力,找到一个拥塞的“临界点”。
-
b. 拥塞避免 (Congestion Avoidance)
- 何时发生? 当
cwnd
的大小,增长到一个预设的 “慢启动阈值”(ssthresh
) 之后。 - 做什么? 指数级增长太快了,容易导致拥塞。进入这个阶段后,
cwnd
的增长方式,会从“指数级”变为 “线性、加法式” 的增长。比如,每经过一个RTT(网络往返时延),cwnd
只增加1个MSS。 - 目的:以一种更平缓、更“温柔”的方式,继续慢慢地增加发送速率,逼近网络的极限。
- 何时发生? 当
-
c. 拥塞发生 (Congestion Detection)
- 如何判断? 当发送方检测到数据包丢失时(最典型的就是超时重传的发生),它就认为网络已经发生了拥塞。
- 做什么? 这是对网络拥堵的“惩罚”和“紧急刹车”。
- 它会立刻将慢启动阈值
ssthresh
,设置为当前cwnd
的一半。 - 然后,将
cwnd
直接重置回初始值1。 - 最后,重新进入慢启动阶段,开始新一轮的试探。
- 它会立刻将慢启动阈值
-
d. 快速恢复 (Fast Recovery) & 快重传 (Fast Retransmit)
- 何时发生? 这是对“超时重传”这种严重拥塞信号的一种优化。当发送方连续收到3个重复的ACK时,它会认为“只是有个别包丢了,但网络整体状况还不算太糟”。
- 做什么(快重传)? 不用等到超时,立即重传那个丢失的数据包。
- 做什么(快速恢复)?
- 它同样会将
ssthresh
设置为当前cwnd
的一半。 - 但它不会像超时重传那样,把
cwnd
降到1,而是直接将cwnd
也设置为新的ssthresh
。 - 然后,直接跳过慢启动阶段,进入拥塞避免阶段,开始线性增长。
- 它同样会将
- 目的:在网络只是轻微拥堵时,进行一次更“温和”的速率调整,而不是一竿子打死,从而更快地恢复到较高的传输速率。
总结一下,TCP的拥塞控制,就是通过 cwnd
这个窗口,以及慢启动、拥夕避免、拥塞发生、快速恢复这四大算法的动态循环,实现了一套自我感知、自我调节的智能发送机制。它让每一个TCP连接,都成为了一个负责任的“网络公民”,共同维护了整个互联网的稳定和高效。
参考小林 coding