标签

TCP

TCP(Transmission Control Protocol,传输控制协议)是一种广泛使用的网络通信协议,属于互联网协议套件的核心协议之一。它位于 OSI 模型的传输层,提供了一种可靠的、面向连接的通信方式,确保数据包在两个网络设备之间正确无误地传递。

TCP
查看更多相关内容
计算机基础5月29日 00:12
TCP 可靠传输靠哪些机制保证?RTO 和 SACK 是怎么工作的?TCP 可靠传输靠六个机制保证:序列号+确认应答(保证有序、检测丢包)、超时重传+快重传(丢包恢复)、校验和(检测数据损坏)、流量控制(防接收方溢出)、拥塞控制(防网络过载)。其中序列号和重传是核心,其他都是辅助。 ## 追问 ### 超时重传的 RTO 怎么算?为什么不能固定值? 网络延迟波动大,固定值不靠谱——设小了正常延迟就触发多余重传,设大了真丢包要等很久才恢复。RTO 基于实测 RTT 动态计算:RTO = SRTT + 4 × RTTVAR,SRTT 是平滑 RTT(历史加权平均),RTTVAR 是 RTT 波动幅度。首次测量 SRTT = RTT,后续每次 SRTT = 7/8 × 旧SRTT + 1/8 × 新RTT。Linux 还有个下限(tcp_rto_min,默认 200ms),防止 RTO 过小导致过度重传。 ### 累积确认有什么问题?SACK 怎么解决的? 累积确认只告诉你"到这个序号之前的都收到了",中间丢了哪个说不清。比如发了 1-10 号包,5 号丢了,ACK 会一直回 5(表示期望收到 5),6-10 号收到了但不确认。发送方不知道 6-10 号到底到了没有,可能重传 5-10 全部。SACK(Selective ACK)在 TCP 选项里额外报告已收到的非连续块,发送方就知道只重传 5 号就行。Linux 默认开启 SACK(tcp_sack=1),对高延迟高丢包链路很重要。 ### 序列号为什么是按字节编号而不是按包编号? 因为 TCP 是字节流协议,应用层不关心包的边界——你写 1000 字节,TCP 可能拆成两个 500 字节的段,也可能合成一个。按字节编号让接收方能精确知道每个字节的位置,不管怎么拆分合成都能正确重组和确认。也方便滑动窗口按字节数控制流量。面试追问:ISN(初始序列号)为什么不用 0 或 1?防止旧连接的报文段被新连接误接收——ISN 基于时钟递增,每隔 4 微秒加 1,不同连接的序列号空间错开。 ### 校验和能检测所有错误吗? 不能。校验和是 16 位反码求和,只能检测单比特错误和大部分多比特错误,但两个错误恰好互相抵消时校验和不变。以太网的 CRC 能检测更多错误,但端到端路径上中间设备可能修改数据(如 NAT 改 IP 地址),TCP 校验和是端到端验证的最后防线。如果需要更强的完整性保证,要用 TLS 的 MAC 或应用层哈希。 ## 写段代码 ```python # 模拟 TCP 序列号和累积确认 def simulate_seq(): sent = 0 # 下一个要发的字节号(ISN=0 简化) acked = 0 # 已确认的字节号 segments = [(0, 100), (100, 200), (200, 300)] # (起始, 结束) for start, end in segments: print(f"发送: seq={start}, 数据长度={end-start}") # 模拟中间段丢失 print(f"收到: ACK={100} (第1段确认)") print(f"收到: ACK={100} (第2段丢失,重复ACK)") print(f"收到: ACK={100} (第3个重复ACK→快重传)") print(f"重传: seq={100}, 数据长度=100") print(f"收到: ACK={300} (累积确认到第3段)") simulate_seq() ```
计算机基础5月29日 00:11
TCP 四次挥手的过程是什么?为什么不能三次挥手?TCP 四次挥手是断开连接的过程:主动方发 FIN,被动方回 ACK,被动方再发 FIN,主动方回 ACK。之所以要四次而不是三次,是因为 TCP 全双工——每个方向必须单独关闭。第二次挥手时被动方可能还有数据没发完,不能把 FIN 和 ACK 合并,只能先确认,等数据发完再关自己的方向。 ## 追问 ### TIME_WAIT 为什么要等 2MSL? 两个原因。第一,确保最后的 ACK 能到——如果 ACK 丢了,被动方会重传 FIN,TIME_WAIT 状态的主动方可以重新回 ACK。如果主动方直接 CLOSED,收到重传的 FIN 只能回 RST,被动方会报连接异常。第二,让网络中属于这个连接的旧报文段全部消亡——MSL 是报文最大生存时间(RFC 793 建议 2 分钟,Linux 实际是 60 秒),等 2MSL 确保来回两个方向的旧包都过期,不会干扰后续新连接。 ### TIME_WAIT 过多怎么办? 短连接场景下(比如 HTTP/1.0 每个请求一个连接),主动关闭方会积累大量 TIME_WAIT,每个占一个五元组(源IP、源端口、目的IP、目的端口、协议),端口耗尽后新连接无法建立。解决方法:启用 `tcp_tw_reuse`(允许 TIME_WAIT 端口给新连接用,依赖时间戳判断旧包)、改用长连接(HTTP/1.1 Keep-Alive)、让客户端主动关闭(服务端被动关闭不产生 TIME_WAIT)。注意 `tcp_tw_recycle` 在 NAT 环境下会导致连接失败,Linux 4.12 后已移除此选项。 ### 为什么不能三次挥手? 假设第二次和第三次合并——被动方收到 FIN 后立刻发 FIN+ACK。问题是被动方可能还有数据没发完:它收到主动方的 FIN 只是说"我不发了",不代表"你也不能发了"。被动方需要继续发送剩余数据,发完才能关自己的方向。如果提前发 FIN,这些数据就丢了。只有在被动方恰好也没有数据要发时,才能合并成三次挥手——这就是延迟 ACK 场景下偶尔观察到三次挥手的原因。 ### CLOSE_WAIT 很多是什么问题? CLOSE_WAIT 是被动方收到 FIN 后、还没发 FIN 之前的状态。大量 CLOSE_WAIT 说明应用层没有调用 close()——通常是因为代码 bug:忘了关 socket,或者线程池满了来不及处理。一个典型的坑是:HTTP 服务端在请求处理异常时没关连接,客户端超时断开后,服务端停留在 CLOSE_WAIT。排查方法:`netstat` 统计 CLOSE_WAIT 数量,配合 `lsof` 找到对应进程,检查代码里的 socket 关闭逻辑。 ## 写段代码 ```python # 查看系统 TCP 连接状态分布 import subprocess result = subprocess.run(['netstat', '-tan'], capture_output=True, text=True) states = {} for line in result.stdout.splitlines()[2:]: parts = line.split() if len(parts) >= 6: state = parts[5] states[state] = states.get(state, 0) + 1 for state, count in sorted(states.items(), key=lambda x: -x[1]): print(f"{state:20s} {count}") ```
计算机基础5月29日 00:10
TCP 拥塞控制的四个算法是什么?超时重传和快重传怎么区分?TCP 拥塞控制有四个算法:慢启动、拥塞避免、快重传、快恢复。核心思想是"先试探再加码"——不清楚网络能承受多少时就少发,确认没问题再加速;一旦发现拥塞就立刻降速。控制对象是 cwnd(拥塞窗口),和 rwnd 取较小值作为实际发送窗口。 ## 追问 ### 慢启动为什么叫"慢"?明明指数增长很快 "慢"是相对没有拥塞控制的情况——TCP 早期一上来就发满窗口,容易打爆网络。慢启动从 cwnd=1 MSS 开始,每收到一个 ACK 就 +1 MSS,一个 RTT 内翻倍(1→2→4→8→16)。看起来快,但初始只有 1 MSS,比直接发满窗口保守得多。cwnd 到达 ssthresh 后切换到拥塞避免的线性增长(每 RTT +1 MSS),不再翻倍。ssthresh 的初始值通常很大,第一次丢包后才设为 cwnd/2。 ### 3 个重复 ACK 为什么就重传?不等超时吗? 等超时太慢了。RTO 通常是几百毫秒到几秒,而 3 个重复 ACK 说明后续包已经到了,只是中间那个丢了——网络还在工作,只是丢了一个包。快重传收到第 3 个重复 ACK 就立即重传,不用等 RTO,恢复速度差了一个数量级。为什么是 3 个不是 1 个?因为偶尔乱序也会产生重复 ACK,1 个就重传太激进,3 个基本确认是丢包。 ### 超时重传和快重传的处理有什么不同? 超时重传意味着网络可能严重拥塞——连 ACK 都回不来了,所以处理更激进:ssthresh = cwnd/2,cwnd 直接降到 1 MSS,重新慢启动。快重传说明网络还通着(后续包到了),只是丢了一个包,所以处理温和:ssthresh = cwnd/2,cwnd 降到 ssthresh 而不是 1,进入快恢复。这就是为什么快重传后不回慢启动——丢一个包不代表网络崩溃。 ### BBR 和传统拥塞控制有什么不同? 传统算法(Reno/Cubic)基于丢包判断拥塞——丢包就降速,不丢就加速。问题是在高延迟高丢包的网络(无线、跨国)里,丢包不一定是拥塞,可能是链路本身的特性,传统算法会白白降速。BBR(Google 2016 年提出)不看病包,直接测量带宽和 RTT,把发送速率调整到带宽-延迟积(BDP),目标是把管道填满但不溢出。实测:YouTube 启用 BBR 后跨洲链路吞吐量提升 10-100 倍。 ## 写段代码 ```python # 模拟慢启动和拥塞避免的 cwnd 变化 def simulate_cwnd(ssthresh=16, max_rtt=20): cwnd = 1 # 初始 1 MSS for rtt in range(1, max_rtt + 1): if cwnd < ssthresh: cwnd *= 2 # 慢启动:指数增长 phase = "慢启动" else: cwnd += 1 # 拥塞避免:线性增长 phase = "拥塞避免" print(f"RTT {rtt:2d}: cwnd={cwnd:3d} MSS [{phase}]") simulate_cwnd() ```
计算机基础5月29日 00:09
TCP 滑动窗口机制是怎么工作的?零窗口和糊涂窗口是什么?TCP 流量控制靠滑动窗口:接收方在 ACK 里通告自己还能接收多少数据(rwnd),发送方据此控制发送量,不让接收方缓冲区溢出。发送窗口 = min(rwnd, cwnd),rwnd 是接收方给的流量控制信号,cwnd 是发送方自己根据拥塞状况算的。这里只说 rwnd 的部分。 ## 追问 ### 零窗口是怎么回事?怎么恢复? 接收方缓冲区满了,在 ACK 里通告 rwnd=0,发送方就必须停。但接收方应用层读完数据释放缓冲区后,会发一个窗口更新报文告诉发送方可以继续了。问题在于:这个窗口更新报文丢了怎么办?发送方会一直等,死锁。解决方法是发送方启动持续计时器,定期发零窗口探测报文(只含 1 字节数据),迫使接收方回 ACK 并带上最新窗口值。这就是为什么零窗口不会永远卡住。 ### 糊涂窗口综合征是什么? 接收方应用层每次只从缓冲区读 1 字节,就通告 rwnd=1;发送方就发 1 字节数据——一个 41 字节的包只装了 1 字节有效载荷,带宽利用率 2.4%。这就是糊涂窗口综合征(Silly Window Syndrome)。接收端的解决方法叫 Clark 方案:缓冲区空闲不到 MSS 或总缓冲区的 35% 时不通告新窗口;发送端配合 Nagle 算法,攒够数据再发。两端一起防才能根治。 ### rwnd 和 cwnd 有什么区别? rwnd 是接收方告诉发送方"我还能收多少",防止接收方溢出;cwnd 是发送方自己算的"网络还能承受多少",防止网络拥塞。流量控制(rwnd)是端到端的保护,拥塞控制(cwnd)是全局性的保护。实际发送窗口取两者最小值——任何一个窗口满了都不能多发。面试中经常把这两个混在一起考,要分清楚哪个是对方给的,哪个是自己算的。 ### 滑动窗口的"滑动"体现在哪? 窗口把发送缓冲区的数据分成四段:已确认 ← 窗口左边界 | 已发送未确认 | 可发送未发送 | 窗口右边界 → 不可发送。收到 ACK 后左边界右移(滑过去),接收方通告新窗口后右边界右移(窗口打开)——这就是"滑动"。如果右边界不变就是窗口关闭,左边界追上右边界就是零窗口。 ## 写段代码 ```python # 模拟滑动窗口发送 def sliding_window_send(data, window_size): sent = 0 # 窗口左边界 acked = 0 # 已确认 while acked < len(data): # 发送窗口内的数据 while sent < min(acked + window_size, len(data)): print(f"发送 data[{sent}]") sent += 1 # 模拟收到 ACK print(f"收到 ACK={acked+1}") acked += 1 # 左边界滑动 sliding_window_send(list(range(8)), window_size=3) ```
计算机基础5月29日 00:02
TCP Keep-Alive 机制是什么?为什么还需要应用层心跳?TCP Keep-Alive 是操作系统提供的连接存活检测机制:连接空闲一段时间后,内核自动发探测包,根据对端响应判断连接是否还活着。三个核心参数控制行为——空闲多久开始探测(tcp_keepalive_time,默认 7200 秒)、探测间隔(tcp_keepalive_intvl,默认 75 秒)、探测几次放弃(tcp_keepalive_probes,默认 9 次)。最差情况下,从连接断开到被检测出来要 7200 + 75×9 = 7875 秒,超过 2 小时。 ## 追问 ### 为什么默认 2 小时这么长? RFC 1122 建议至少 2 小时,是出于对网络风暴的担忧——如果全网所有连接都以短间隔发探测包,本身就是一场 DDoS。2 小时在服务器间稳定网络里够用了,问题出在移动端:运营商 NAT 设备的连接跟踪表有限,空闲 5 分钟(移动 2/3G)到 28 分钟(电信 3G)就淘汰条目,连接就被静默丢弃了,2 小时探测根本来不及救。 ### 既然有 Keep-Alive,为什么还要应用层心跳? 三个原因。第一,Keep-Alive 只能检测连接是否可达,不能检测对端进程是否卡死——进程死锁时 TCP 连接还活着,Keep-Alive 照样通过。第二,Keep-Alive 的探测包不带业务数据,服务端对探测无感知,无法在应用层做状态同步。第三,参数是系统级的,改了影响所有连接,不如应用层心跳可以按业务精细控制间隔。微信的心跳从 30 秒到 300 秒动态调整,Keep-Alive 做不到。 ### Keep-Alive 探测包长什么样? 一个不包含数据的 ACK 包,序列号设为对端期望的序列号减 1,这样对端会发现序列号不匹配,回一个 ACK 带上正确的期望序列号——探测就成功了。如果对端回复 RST,说明进程已崩溃重启;如果连续 9 次无响应,内核判定连接死亡,关闭 socket 并返回 ETIMEDOUT。 ### 什么场景下 Keep-Alive 就够了? 服务器之间的稳定内网连接。比如微服务间 gRPC 长连接、数据库连接池,网络环境可控,不存在 NAT 超时问题,Keep-Alive 配合较短的探测间隔(比如 60 秒)就能及时清理僵死连接。这些场景不需要应用层心跳的灵活性。 ## 写段代码 ```python import socket sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 60) # 60秒后开始探测 sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 10) # 每10秒探测一次 sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 3) # 探测3次放弃 ```
计算机基础5月29日 00:00
什么是 TCP Nagle 算法?为什么会造成 40ms 延迟?Nagle 算法的核心规则只有一条:连接上有未被确认的小包时,不再发新的小包,等 ACK 回来再把缓冲区里的数据攒一起发。目的是减少小包数量——Telnet 按一个键就产生一个 41 字节的包,其中 40 字节是 TCP+IP 头部,有效载荷只有 1 字节,带宽利用率不到 3%。1984 年 John Nagle 在 RFC 896 里提出这个方案,解决的就是交互式应用疯狂发小包导致的广域网拥塞。 算法默认开启(RFC 1122 推荐),通过 TCP_NODELAY 选项关闭。 ## 追问 ### Nagle 和延迟 ACK 怎么会互相卡死? Nagle 在发送端等 ACK,延迟 ACK 在接收端等更多数据再确认,两者同时启用就形成僵持:发送方写了一个小包,等 ACK;接收方收到后不马上回 ACK,等 40ms(Linux 默认)或 200ms(Windows 默认)看还有没有后续数据。典型场景是 write-write-read 模式:第一次写直接发出,第二次写被 Nagle 挡住,接收端延迟 ACK 等 40ms,发送端就卡在这 40ms 上。腾讯云有实际案例,营销平台 10% 的请求耗时稳定卡在 38-42ms,根因就是这对组合。 ### 什么时候必须关掉 Nagle? 实时交互场景:游戏同步、远程桌面、WebSocket 推送。这些场景宁可多发几个小包也不能容忍额外延迟。Redis 3.x 的主从同步曾因 Nagle 导致从库延迟飙升,后来在源码里给同步 socket 加了 TCP_NODELAY 才解决。 ### Nagle 和 TCP_CORK 有什么区别? Nagle 是"有小包没确认就不发",侧重减少小包数量;CORK 是"攒够一个 MSS 再发",侧重提高吞吐量。CORK 更激进——把数据一直攒到 MSS 或超时(通常 200ms)才放行,适合 HTTP 流水线这类一次要发大量数据的场景。Nginx 在发送响应头时用 CORK,等响应体也凑齐了一起发,减少系统调用次数。 ### 怎么确认线上问题是 Nagle 引起的? 抓包看时序:发送端有小包发出后,超过 40ms 才收到 ACK 或才发下一个包,大概率是 Nagle + 延迟 ACK。也可以临时在客户端设 TCP_NODELAY 做对比——如果 40ms 档消失了就确认了。注意要区分是 Nagle 的问题还是单纯的网络延迟,对比开关前后的延迟分布比看绝对值更可靠。 ## 写段代码 ```c // 禁用 Nagle 算法 int flag = 1; setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(flag)); // Linux 上启用快速 ACK(接收端) setsockopt(fd, IPPROTO_TCP, TCP_QUICKACK, &flag, sizeof(flag)); ```
计算机基础5月27日 22:51
TCP 和 UDP 的主要区别是什么?## 答案速览 TCP 面向连接、可靠传输、一对一通信,代价是延迟高、开销大;UDP 无连接、不保证可靠、支持一对多,优势是快。面试中一句话总结:**要可靠选 TCP,要速度选 UDP**。 ## 核心区别 | 维度 | TCP | UDP | |------|-----|-----| | 连接 | 三次握手建立,四次挥手断开 | 无连接,直接发 | | 可靠性 | 确认应答+重传+校验,保证不丢不重不乱 | 尽力交付,可能丢包乱序 | | 传输方式 | 面向字节流 | 面向报文,保留边界 | | 流量/拥塞控制 | 滑动窗口+慢启动+拥塞避免 | 无 | | 通信模式 | 仅一对一 | 一对一/一对多/多对多 | | 首部开销 | 最少 20 字节 | 固定 8 字节 | 理解的关键不在背表,而在**为什么**:TCP 的每一个"可靠"特性(确认、重传、序号、窗口)都是有代价的——更多握手、更大首部、更低效率。UDP 丢掉这些,换来的是简单和快速。 ## 适用场景怎么选 - **TCP**:HTTP/HTTPS、FTP、SSH、数据库连接——数据不能丢的场景 - **UDP**:视频会议、直播、在线游戏、DNS 查询——延迟比完整性更重要的场景 面试常见的陷阱题:**DNS 既用 UDP 又用 TCP,为什么?** 普通查询用 UDP(快),响应超过 512 字节或区域传输时切 TCP(可靠)。这说明选协议不是非此即彼,而是按场景取舍。 ## 面试追问 - 三次握手为什么不能两次?——两次无法确认客户端接收能力,可能产生死连接 - 为什么视频通话用 UDP 而不重传丢包?——重传到达时画面已经过了,不如跳过 - QUIC 为什么基于 UDP 而不是 TCP?——TCP 的握手和拥塞控制内核实现,无法快速迭代;UDP 在用户态可实现同等可靠性和更快的连接建立
计算机基础5月27日 22:48
TCP SYN Flood 攻击的原理和防御方法是什么?## 攻击原理:三次握手的致命缺陷 TCP 建立连接需要三次握手:客户端发 SYN,服务器回 SYN+ACK 并分配资源等待 ACK,客户端确认后连接建立。SYN Flood 攻击正是利用第二步——攻击者发送大量伪造源 IP 的 SYN 包,服务器为每个请求分配半连接资源并等待永远不会到来的 ACK,直到半连接队列被填满,正常请求无法处理。 核心危害:半连接队列(SYN Queue)被占满 → 新连接被丢弃 → 服务不可用。每个半连接约占 200 字节内存,攻击者用极低成本即可耗尽服务器资源。 ## 防御方法(按实战优先级排列) ### SYN Cookie——最核心的防御 服务器收到 SYN 时不分配半连接资源,而是将连接信息编码到 SYN+ACK 的初始序列号(Cookie)中。收到合法 ACK 后,通过验证 Cookie 还原连接状态。 ```bash sysctl -w net.ipv4.tcp_syncookies=1 ``` **追问:SYN Cookie 的局限?** 会禁用 TCP 窗口缩放和 SACK 等选项,影响高延迟链路性能;Cookie 可被暴力猜解,不适用于超高带宽攻击。 ### 调整内核参数——配合 Cookie 的辅助手段 ```bash # 增大半连接队列 sysctl -w net.ipv4.tcp_max_syn_backlog=8192 # 减少重试次数,加速释放 sysctl -w net.ipv4.tcp_synack_retries=2 ``` 增大 backlog 只是延缓耗尽,不能根治;缩短超时可能影响高延迟正常连接,需根据业务权衡。 ### 网络层限速与过滤 ```bash iptables -A INPUT -p tcp --syn -m limit --limit 1/s --limit-burst 3 -j ACCEPT iptables -A INPUT -p tcp --syn -j DROP ``` 单 IP 限速可防小规模攻击,但分布式攻击下效果有限,且可能误伤 NAT 出口用户。生产环境建议使用专业 DDoS 防护服务(Cloudflare、AWS Shield 等)做流量清洗。 ## 如何检测 SYN Flood? - `netstat -n | grep SYN_RCVD | wc -l` 大量 SYN_RCVD 状态连接 - `ss -s` 观察 sockets 统计异常 - 监控系统 SYN 收包速率突增 检测到攻击后,优先启用 SYN Cookie,再结合限速和外部清洗逐步缓解。 与 UDP Flood、HTTP Flood 相比,SYN Flood 靶向传输层,防御手段更成熟,但仍是互联网上最经典的 DDoS 攻击方式之一,1996 年至今未被根治。
计算机基础5月27日 22:48
TCP TIME_WAIT 状态的作用和问题是什么?## 答案:TIME_WAIT 是主动关闭方在四次挥手最后发送 ACK 后进入的等待状态,持续 2MSL,核心作用是保证连接可靠终止和防止旧报文干扰新连接 主动关闭方发送最后一个 ACK 后不能直接关闭,必须等待 2MSL(最大报文生存时间,Linux 默认 60 秒)。这段等待有两个目的: **第一,兜底最后的 ACK。** 如果这个 ACK 丢了,对端会重传 FIN,TIME_WAIT 状态下还能重发 ACK 响应。没有这个等待,ACK 丢失后对端永远收不到确认,连接无法正常关闭。 **第二,让网络中的残留报文过期。** 同一个四元组(源IP、源端口、目的IP、目的端口)可能很快建立新连接,旧连接的延迟报文如果还没消失,会被新连接误收。等 2MSL 后这些报文必然被丢弃,不会串扰。 ## TIME_WAIT 带来的实际问题 高并发短连接场景下,大量连接同时处于 TIME_WAIT,会导致端口耗尽。客户端可用端口约 6 万个,每个 TIME_WAIT 占一个,当并发远超这个数,新连接报 "address already in use"。 服务端主动关闭连接时问题更突出。HTTP/1.0 默认 connection: close,服务端每次响应后主动关连接,短时间积累大量 TIME_WAIT。 ## 怎么解决 实际工程中常用三种手段配合: **开启 tcp_tw_reuse:** 允许将 TIME_WAIT 状态的连接端口分配给新连接,前提是开启了 TCP 时间戳(tcp_timestamps=1),用时间戳区分新旧连接,比单纯缩短等待更安全。 **用长连接和连接池:** 根本思路是减少连接的频繁创建和销毁。HTTP/1.1 默认 keep-alive,数据库连接池复用连接,都是这个逻辑。 **扩大端口范围:** 调整 ip_local_port_range 到 "1024 65535",治标不治本但能缓解。 ```bash # Linux 常用配置 sysctl -w net.ipv4.tcp_tw_reuse=1 sysctl -w net.ipv4.tcp_timestamps=1 sysctl -w net.ipv4.ip_local_port_range="1024 65535" ``` 注意 `tcp_tw_recycle` 在 NAT 环境下会导致连接失败,Linux 4.12 后已移除该参数,不要用。 ## 面试追问 **为什么是 2MSL 而不是 1MSL?** ACK 最多存活 1MSL 到达对端,对端重传的 FIN 也最多存活 1MSL 回来,加起来恰好 2MSL,覆盖了两个方向的最坏情况。 **服务端出现大量 TIME_WAIT 说明什么?** 说明服务端在主动关闭连接。检查是否 HTTP 响应头缺了 keep-alive,或者业务逻辑在用完连接后主动 close 而非复用。 **客户端出现大量 TIME_WAIT 呢?** 客户端用短连接高频请求服务端,每次自己主动关连接。排查是否可以用连接池或长连接替代。
计算机基础5月27日 22:44
TCP 粘包问题是什么?如何解决?## TCP 粘包问题是什么?如何解决? TCP 粘包是指发送方多次 send 的数据,在接收方被一次性 read 出来,多个消息"粘"在了一起。根本原因是 TCP 是字节流协议,不维护消息边界——它只保证数据可靠、按序到达,但不关心你这条消息从哪开始到哪结束。 ## 粘包是怎么产生的? **发送端:Nagle 算法**。多个小包攒成一个大包再发,减少网络开销。这意味着你连续调用两次 send,数据可能被合并成一个 TCP 段发出。 **接收端:缓冲区读取时机**。应用层 read 的速度慢于数据到达速度,缓冲区里攒了好几条消息,一次 read 全取出来了。 注意:粘包不是 TCP 的 bug,而是字节流协议的设计特性。UDP 就不会有这个问题,因为 UDP 保留消息边界,每次 sendto 对应一次 recvfrom。 ## 怎么解决? 核心思路:**在应用层定义消息边界**。 **1. 固定长度**:每条消息固定 N 字节,不够补齐。简单但浪费带宽,实际很少用。 **2. 分隔符**:消息之间用特殊字符(如 `\n`)分隔。HTTP/1.1 就是用 `\r\n\r\n` 分隔头部和 body。缺点是消息内容本身包含分隔符时要转义,处理麻烦。 **3. 长度前缀(最常用)**:消息头加一个字段表示 body 长度,接收方先读长度再读对应字节数。绝大多数二进制协议都用这种方式。 ```python import struct def send_msg(sock, data: bytes): # 前4字节表示消息长度,大端序 sock.sendall(struct.pack('!I', len(data)) + data) def recv_msg(sock): # 先读4字节拿到长度 raw = _recv_exact(sock, 4) length = struct.unpack('!I', raw)[0] # 再读对应长度的body return _recv_exact(sock, length) def _recv_exact(sock, n): buf = b'' while len(buf) < n: chunk = sock.recv(n - len(buf)) if not chunk: raise ConnectionError('连接断开') buf += chunk return buf ``` ## 面试追问 **Q: 关掉 Nagle 算法能解决粘包吗?** 不能。设置 `TCP_NODELAY` 只解决发送端的合并问题,接收端缓冲区仍然可能一次读出多条消息。粘包的本质是缺乏消息边界,必须由应用层协议解决。 **Q: 什么时候不需要处理粘包?** 如果连续发送的数据本身就是一个整体(比如传文件),那接收端粘在一起反而是正确的,不需要额外处理。只有当每条消息是独立的、需要分别处理时,才必须定义边界。 **Q: 长度前缀方案,长度字段本身被拆包了怎么办?** 这正是 `_recv_exact` 函数存在的意义——用循环确保读满指定字节数。这是处理拆包的标准做法。