1. 重新认识:TCP 和 UDP 的区别不是“可靠”和“不可靠”
先问一个问题:如果你的视频通话一直卡顿,你会觉得是网络问题,还是协议选型问题?
大多数人第一反应是“带宽不够”“Wi-Fi 信号差”。但实际上,很多实时场景卡顿的根源,恰恰是开发者老老实实地选择了 TCP——因为它“可靠”。视频通话里,一帧画面已经过时了,TCP 还在执着地重传那一帧旧数据,后面的新画面只能排队等待。用户看到的,就是延迟越积越高的画面和越来越频繁的卡顿。
这件事暴露了一个关键认知:TCP 的可靠性不是免费的,它是用延迟、吞吐和管理成本换来的。而 UDP 之所以被长期低估,是因为大多数教材只告诉了你“UDP 不可靠”,却没有告诉你“UDP 用不可靠换来了什么”。
在 CSDN 和各大技术社区里,“tcp和udp的区别”一直是搜索量最高的计算机基础问题之一。这说明每个接触网络开发的工程师,迟早都要面对这个选择。但网上很多文章停留在表格对比的层面:TCP 有连接、可靠、有序;UDP 无连接、不可靠、无序。这没有错,但对于指导真实项目远远不够。
本文想做的,是把 TCP 和 UDP 的区别讲透:不仅要说明它们在工作原理上差在哪,还要解释这些差异在实际工程中带来的选型后果。你会看到,为什么有些场景必须用 TCP,为什么有些场景选 TCP 反而是错的,以及当你自己动手写一个通信模块时,应该如何在两者之间做决策。
读完这篇文章,你能带走三样东西:一套完整的协议对比认知、一组可运行的 Python TCP/UDP 示例代码、一份实战排错清单。无论你是刚入门的新手,还是被线上通信问题折磨过的老手,都能在这里找到用得上的内容。
2. 基础概念与核心原理
2.1 传输层:TCP 和 UDP 在协议栈中的位置
在理解 TCP 和 UDP 之前,先看清楚它们在整个网络体系中的位置。
通常我们说的 TCP/IP 协议族,是一个分层模型。应用层(HTTP、FTP、DNS 等)负责业务数据;传输层负责端到端的通信;网络层负责跨设备寻址;链路层负责物理介质传输。TCP 和 UDP 都工作在传输层,它们从上层拿到应用数据,再打包交给网络层发送。
传输层的核心价值之一,是引入了端口号。IP 地址解决了“数据包去哪台机器”的问题,端口号解决的是“到了这台机器,交给哪个进程”的问题。类比来说,IP 地址是公司地址,端口号是部门门牌号。TCP 和 UDP 都使用 16 位端口号,范围从 0 到 65535,其中 0 到 1023 是知名端口,比如 HTTP 的 80、HTTPS 的 443、DNS 的 53。
从开发角度看,不管底层技术多复杂,你通过 Socket API 打交道最多的就是 TCP 和 UDP 这两种传输层协议。不同语言(C#、Java、Python、Qt C++、LabVIEW)的 Socket API 形态不同,但核心模型非常一致:TCP 是面向连接的字节流,UDP 是无连接的数据报。
2.2 TCP 是如何做到“可靠”的
TCP 的可靠,是通过一整套复杂的机制实现的。理解这些机制,就能理解为什么 TCP 比 UDP 处理能力强,但也更“重”。
三次握手。建立 TCP 连接前,客户端和服务端需要先完成三次握手:客户端发 SYN(请求同步)、服务端回 SYN+ACK(同意并同步)、客户端再回 ACK(确认收到)。这个流程的直观目的,是让双方都确认“对方能收到我的消息,我也能收到对方的消息”。更底层的原因是防止失效的连接请求突然到达服务端,导致服务端建立无意义的连接。
序列号与确认应答。TCP 会把应用层数据切分成一段一段的字节流,每个字节都有一个序列号。接收方收到数据后,会回复 ACK,告诉发送方“我收到了哪些字节”。发送方收到 ACK,才知道数据确实到达了对端。
超时重传。如果发送方在一定时间内没有收到 ACK,就会重新发送数据。数据在网络上可能丢失,这是 TCP 能主动应对丢包的基础。
流量控制。TCP 用滑动窗口机制控制发送速度,避免发送方太快、接收方处理不过来。接收方在 ACK 里带上自己的可用窗口大小,发送方根据这个值调整发送节奏。
拥塞控制。这是 TCP 的另一个重要特性。网络出现过载时,TCP 会主动降低发送速度,比如通过慢启动、拥塞避免、快重传、快恢复等算法。这保证了 TCP 在共享网络上不会压垮链路,但也意味着 TCP 的传输速率是动态调整的,不保证“稳定”。
这些机制层层叠加,最终呈现出我们感知到的“可靠”。但每一次确认、重传、窗口调整,都在消耗额外的网络往返和计算资源。
2.3 UDP 的“尽力而为”到底指什么
UDP 的设计思路和 TCP 完全相反。它只做一件事:把应用层数据封装成数据报,加上源端口、目的端口、长度和校验和,然后扔给网络层发送。没有握手,没有确认,没有重传,没有拥塞控制,没有任何“连接状态”。
这里有一个容易误解的点:UDP 的校验和(Checksum)只覆盖首部和数据的一个简单校验,用于检测传输中的损坏,但不负责恢复。如果数据包在网络上被丢弃了,UDP 不会重发;如果多个包乱序到达,UDP 也不会重新排序。这些工作完全交给应用层自己处理。
从 API 角度来看,TCP 的行为像“打电话”:先拨号(connect),接通后才能持续说话,挂断(close)后连接结束。UDP 的行为像“寄明信片”或“广播”:不需要提前建立关系,直接把写着地址和内容的封装发出去就行,对方能不能收到、什么时候收到,寄件者并不确认。
值得注意的是,UDP 也有自己的“边界感”。TCP 是流式协议,应用层发送的多个消息在网络上可能被组合或拆分;UDP 是数据报协议,一次sendto对应一次recvfrom,接收方拿到的数据报大小与发送方发送的粒度一致。这个差异在后续写代码时特别重要。
2.4 TCP 与 UDP 核心对比
| 对比维度 | TCP | UDP |
|---|---|---|
| 连接状态 | 面向连接,需三次握手 | 无连接,直接发送 |
| 可靠性 | 可靠传输,确认与重传 | 尽力而为,不保证到达 |
| 数据顺序 | 保证字节流有序 | 不保证数据报顺序 |
| 传输单位 | 字节流,无消息边界 | 数据报,有消息边界 |
| 首部开销 | 固定 20 字节以上 | 固定 8 字节 |
| 流量控制 | 滑动窗口机制 | 无 |
| 拥塞控制 | 有 | 无 |
| 通信模式 | 点对点 | 支持一对多、多对一、组播广播 |
| 典型应用 | HTTP、文件传输、数据库、消息队列 | DNS、音视频通话、实时游戏、IoT |
这个表格是“背诵版”,但真正指导工程实践的,是表格背后那些权衡逻辑。
3. 为什么 UDP 在很多场景不可替代
很长一段时间里,UDP 被错误地当成“TCP 的低配版”。实际上,UDP 不是 TCP 的退化,而是另一种极端场景下的正确选择。它的价值,恰恰体现在 TCP 无法胜任的实时场景中。
实时音视频通话、直播、VoIP。语音和视频数据具有强时效性。一方说完一句话,如果这包数据丢了,TCP 会触发重传。但等重传的数据到达时,对话已经过去了 300 毫秒,重传的内容没有任何收听价值。接收方宁可跳过这一小段,也希望画面和声音持续更新。这就是为什么大量实时音视频系统选择 UDP 或基于 UDP 的协议(如 RTP、SRT),并在应用层做抗丢包、前向纠错(FEC)等处理。
实时对战游戏。在 FPS 射击游戏或 MOBA 游戏中,服务器持续向所有玩家广播位置、血量、技能状态。玩家需要的是“此刻所有人状态的最新快照”,而不是“过去某个时刻的完整历史”。如果状态同步走 TCP,一个旧数据包的重传可能导致所有后续状态在客户端排队,玩家的操作反馈延迟急升。因此很多游戏引擎默认用 UDP 做高频状态同步,丢失的旧状态直接跳过。
DNS 域名解析。这是被大多数人忽略的 UDP 经典场景。客户端向 DNS 服务器发起域名查询时,默认使用 UDP 的 53 端口。一次请求对应一个响应,查询逻辑简单,不适合为每一次查询都建立 TCP 连接。当 UDP 响应过大或需要区域传输时,DNS 才会切换到 TCP。这个设计从几十年前延续至今,足以证明 UDP 的高效率在小请求场景下有多重要。
物联网和工业控制。在资源受限的嵌入式设备上(比如带网络模组的 MCU 设备),TCP 协议栈占用的内存和计算资源远高于 UDP。工业控制领域的一些实时协议栈,会直接基于 UDP 构建,因为控制指令要求在毫秒级内到达,不能等待 TCP 的握手或拥塞反馈。当然,Modbus TCP 这类工控协议走的是 TCP,因为工业数据要求强可靠;但实时运动控制类系统,UDP 反而更常见。选择哪种协议,完全取决于业务对实时性和可靠性的优先级。
广播与组播。TCP 是点对点连接,无法一对多发送。UDP 天然支持广播和组播,这在局域网设备发现、多媒体分发、集群节点信息同步等场景中非常有用。你能在同一个局域网里“喊一嗓子”让所有设备响应,唯一的实现基础就是 UDP。
归根结底,判断一个场景是否适合 UDP,只看一个问题:这个场景需要的是“旧数据补传”,还是“新数据及时到达”?如果需要的是后者,UDP 就不是“不可靠”,而是“不够可靠但刚好够用”。
4. TCP 的真实成本:不仅仅是三次握手
既然 TCP 如此可靠,那是不是所有业务都无脑用 TCP 就行?在实际开发中,TCP 的可靠性往往伴随着一系列工程负担。很多新手在第一次用 TCP 做长连接通信时,都会惊讶地发现:表面可靠的 TCP,背后有一堆需要你自己解决的问题。
4.1 连接管理的复杂度
TCP 是有状态的协议,每个连接都要在两端维护发送缓冲区、接收缓冲区、序列号、窗口大小等状态。海量连接会占用大量内存。同时,连接的建立和关闭也都要时间。
建立连接需要一次三次握手,通常消耗 1.5 个 RTT(网络往返时间)。关闭连接时,主动关闭方还要经历 TIME_WAIT 状态,端口在短时间内不能立即复用。如果你写的服务端短连接请求量极大,比如大量线程频繁 connect、close,很容易看到端口被 TIME_WAIT 堆积占满,或者出现only one usage of each socket address这类绑定错误。这里真正容易踩坑的地方在于:很多人以为程序挂了重启就行,结果服务起来直接报端口被占用,就是 TIME_WAIT 或残留进程惹的祸。
4.2 粘包与半包问题
TCP 是字节流协议,它只保证发送的字节按顺序到达,不保证应用层的消息边界。应用层发送了两次send,接收方可能一次recv就把两份数据全读走了;也可能一次recv只读到第一份消息的一半。
这就是著名的粘包(多份消息合并)和半包(一条消息被拆开)问题。解决方式通常有三种:固定长度消息、分隔符分割、以及最常用的“长度字段 + 内容体”的 TLV 格式。也就是说,即使你用 TCP,也必须自己在应用层设计一套帧协议,否则业务数据解析一定会出错。
举一个很实际的例子:服务端向客户端连续发了两条 JSON 消息{"a":1}和{"b":2}。接收方一次recv可能读到完整的{"a":1}{"b":2},如果直接做 JSON 解析必然失败。这个问题的根因不在代码,而在 TCP 的流式设计。很多做 Modbus TCP、自定义长连接协议的开发者,第一周都在和这个问题搏斗。
4.3 队头阻塞:TCP 在弱网下的最大软肋
TCP 为了保证字节流顺序,规定接收方必须按序列号提交数据给应用层。如果第 3 个数据包丢了,即使第 4、5 个数据包已经到达,接收方也会先把它们缓存起来,直到第 3 个包重传成功。这就叫队头阻塞(Head-of-Line Blocking)。
在链路质量好、丢包率极低的内网环境下,队头阻塞的影响几乎可以忽略。但在跨运营商、跨国、Wi-Fi 信号波动等弱网环境下,丢包率一旦上升,TCP 的吞吐量和延迟会显著恶化。这也是为什么 HTTP/3 选择了基于 UDP 的 QUIC 协议,而不是继续生扛 TCP:QUIC 可以在应用层实现多路复用和独立重传,避免某一条数据流的丢包拖垮所有数据流。如果你在评估一套弱网通信方案,千万不要忽略队头阻塞这个因素。
4.4 协议开销
TCP 首部固定部分至少 20 字节,UDP 首部只有 8 字节。单看一个包,差异不大。但 TCP 的每次握手、ACK、窗口通告都要占用带宽。在大流量、高并发场景下,控制报文带来的额外带宽开销是一笔不可忽视的成本。对于 64 字节左右的小包业务,TCP 的确认机制可能让链路利用率大幅下降。
4.5 小结论
TCP 的可靠,不是一种“白送”的属性。它需要你支付连接管理的复杂度、消息边界的自定义工作、弱网下的队头阻塞风险,以及额外的协议开销。选型时,必须把这些成本一并放上谈判桌。
5. 协议选型决策框架:什么时候用 TCP,什么时候用 UDP
现在进入最有工程价值的环节:如何判断一个业务该用 TCP 还是 UDP。
我建议不要凭感觉,也不要只看一两个对比维度,而是按下面四个问题依次过一遍。
第一问:数据丢了之后,重传还有价值吗?
如果是文件传输、数据库事务、支付请求、订单状态,数据必须完整到达,重传当然有价值——选 TCP。如果是视频帧、实时状态快照,丢失一帧可以靠下一帧补偿,重传只会增加延迟——考虑 UDP。
第二问:实时性要求有多高?
控制指令、游戏状态同步、音视频通话这类业务,延迟超过某个阈值就会带来明显体验恶化。UDP 不保证可靠,但它的延迟可预测、没有重传排队。TCP 在弱网下可能出现“为了可靠而牺牲实时”的行为,这在某些业务里是不可接受的。
第三问:是否需要“连接”语义?
如果你需要追踪“某个客户端现在是否在线”,需要做登录鉴权、会话保持,TCP 的连接状态模型是天然的助手。连接建立后,双方可以随时随地通信,断开时也能感知到。UDP 是无连接的,你需要自己在应用层维护会话关系,工作量会明显上升。
第四问:团队能在应用层实现多少可靠性补偿?
选 UDP 不代表放弃可靠性。你可以在 UDP 之上自己设计序列号、ACK、超时重传、乱序缓存——也就是自研一个简化版可靠协议。很多实时系统就是这么做的。如果你没有足够的协议设计和测试能力,选 UDP 可能会被各种丢包问题折磨;如果你有把握,UDP 能给你更大的灵活性和性能上限。
按照这个框架,典型业务的大致结论如下:
| 业务类型 | 推荐协议 | 选型原因 |
|---|---|---|
| 网页浏览、API 接口 | TCP | 数据必须完整,HTTP 本身基于 TCP |
| 文件传输、日志上报 | TCP | 不允许丢数据,可靠性优先 |
| 数据库连接 | TCP | 强一致性和事务语义 |
| 消息推送(IM) | TCP | 需要长连接、在线状态和可靠送达 |
| DNS 查询 | UDP | 一问一答,追求最小延迟和最小开销 |
| 视频直播/通话 | UDP | 丢帧可接受,延迟优先 |
| 实时对战游戏 | UDP | 状态同步要最新,不等待重传 |
| IoT 高频传感上报 | UDP | 控制开销小,可容忍偶发丢包 |
| 局域网设备发现 | UDP(广播) | 一对多通信,TCP 无法实现 |
| 工业实时控制 | UDP(或实时协议栈) | 毫秒级控制延迟,不容忍拥塞等待 |
| Modbus TCP 等工控数据采集 | TCP | 数据采集可靠优先,链路一般稳定 |
这里有一个反直觉的提醒:选择 TCP 不等于安全,选择 UDP 也不等于冒险。工程上更常见的做法,是在 TCP 之上做好协议设计和心跳保活,或者基于 UDP 构建适合业务场景的轻量可靠机制。优秀的方案不是简单二选一,而是在两者之间找到平衡点。
6. 动手实践:用 Python 实现 TCP 与 UDP 通信
理论讲再多,不如跑一遍代码。这一节我们用 Python 内置的socket库,分别实现 TCP 和 UDP 的服务端与客户端,体验两者的核心差异。
6.1 环境准备
- Python 3.6 及以上版本(本文使用通用 socket 标准库,不依赖第三方包)
- 一个能运行 Python 的终端,Windows、Linux、macOS 均可
- 建议准备两个终端,一个跑服务端,一个跑客户端
不同语言的 Socket API 形态不同,但 C#、Java、Go、Qt C++ 中的模型和这里的 Python 版本高度一致:TCP 需要listen、accept、connect;UDP 则是bind、sendto、recvfrom。所以即使你不用 Python,也能通过这份示例理解两种协议的 API 风格差异。
6.2 TCP 服务端实现
# 文件路径:tcp_server.py import socket HOST = '0.0.0.0' PORT = 9001 server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind((HOST, PORT)) server.listen(5) print(f"[TCP] 服务端监听 {HOST}:{PORT}") while True: conn, addr = server.accept() print(f"[TCP] 连接建立,客户端地址:{addr}") with conn: data = conn.recv(1024) print(f"[TCP] 收到数据:{data.decode('utf-8', errors='ignore')}") conn.sendall(b"hello from tcp server") print(f"[TCP] 连接关闭,客户端地址:{addr}")关键点:
SOCK_STREAM表示使用 TCP。listen(5)表示最大等待连接队列长度,不是最大连接数。accept()会阻塞等待客户端接入,返回一个新的conn套接字,后续收发都用它。SO_REUSEADDR用于快速复用处于 TIME_WAIT 状态的端口,在开发调试时很实用。
6.3 TCP 客户端实现
# 文件路径:tcp_client.py import socket HOST = '127.0.0.1' PORT = 9001 client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.connect((HOST, PORT)) client.sendall(b"hello from tcp client") resp = client.recv(1024) print(f"[TCP] 收到服务端响应:{resp.decode('utf-8', errors='ignore')}") client.close()关键点:
connect()会触发三次握手,握手完成后函数才返回。- 如果服务端没有启动,这里会直接抛出
ConnectionRefusedError(连接被拒绝)或等待一段时间后超时。 recv(1024)是字节流式读取,读取长度并不代表消息边界。
6.4 UDP 服务端实现
# 文件路径:udp_server.py import socket HOST = '0.0.0.0' PORT = 9002 server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) server.bind((HOST, PORT)) print(f"[UDP] 服务端监听 {HOST}:{PORT}") while True: data, addr = server.recvfrom(1024) print(f"[UDP] 收到数据:{data.decode('utf-8', errors='ignore')},来源:{addr}") server.sendto(b"hello from udp server", addr)关键点:
SOCK_DGRAM表示使用 UDP。- 不需要
listen()和accept(),bind()之后直接可以收数据。 recvfrom()返回数据和发送方地址,每次调用只接收一个完整数据报。
6.5 UDP 客户端实现
# 文件路径:udp_client.py import socket HOST = '127.0.0.1' PORT = 9002 client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) client.sendto(b"hello from udp client", (HOST, PORT)) data, addr = client.recvfrom(1024) print(f"[UDP] 收到服务端响应:{data.decode('utf-8', errors='ignore')},来源:{addr}") client.close()关键点:
- 客户端甚至不需要调用
connect(),直接sendto()就可以发包。 sendto()没有确认机制,调用成功只代表数据交给了操作系统网络栈,不代表对端收到了。- 如果服务端没启动,客户端的
sendto()通常不会立刻报错。
6.6 运行与验证
打开两个终端,先启动服务端,再启动客户端:
# 终端 1 python tcp_server.py # 终端 2 python tcp_client.py预期输出:
# 终端 1 [TCP] 服务端监听 0.0.0.0:9001 [TCP] 连接建立,客户端地址:('127.0.0.1', 54321) [TCP] 收到数据:hello from tcp client [TCP] 连接关闭,客户端地址:('127.0.0.1', 54321)# 终端 2 [TCP] 收到服务端响应:hello from tcp serverUDP 同理:
# 终端 1 python udp_server.py # 终端 2 python udp_client.py如果你在 UDP 客户端运行时杀掉了服务端进程,再执行客户端代码,大概率会看到sendto()成功返回,但recvfrom()一直阻塞。这就是“无连接、无确认”的典型表现——发送方无法感知接收方是否存在,只能等一个永远不来的响应。
这一个细节,就能让你直观理解 TCP 和 UDP 工作方式的差异。
6.7 顺带做一个粘包验证
为了验证 TCP 是字节流、UDP 是数据报,可以在 TCP 客户端连续发送多条消息:
# 文件路径:tcp_client_multi.py import socket HOST = '127.0.0.1' PORT = 9001 client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.connect((HOST, PORT)) client.sendall(b"msg1") client.sendall(b"msg2") client.sendall(b"msg3") resp = client.recv(1024) print(f"[TCP] 收到响应:{resp.decode('utf-8', errors='ignore')}") client.close()如果服务端只调用一次recv(1024),你很可能一次性收到msg1msg2msg3拼在一起的内容。这不是 bug,而是 TCP 字节流特性的正常表现。UDP 则不会出现这个现象,因为每个sendto都是一个独立数据报,recvfrom一次只取一个。
7. 用 Wireshark 验证三次握手与 UDP 无握手
写完代码,再看网络层行为会更直观。用 Wireshark 抓包,观察两种协议在底层是如何工作的。
7.1 抓包步骤
- 打开 Wireshark,选择回环接口(通常叫 Loopback 或 lo)。
- 设置显示过滤器:
tcp.port == 9001 || udp.port == 9002。 - 重新运行上面的 TCP 示例和 UDP 示例。
- 停止抓包,观察报文序列。
7.2 TCP 报文:三次握手一目了然
抓到的 TCP 会话开头,一定是这三条报文:
1. 客户端 -> 服务端: TCP [SYN] Seq=0 2. 服务端 -> 客户端: TCP [SYN, ACK] Seq=0 Ack=1 3. 客户端 -> 服务端: TCP [ACK] Seq=1 Ack=1这三条报文的顺序和内容,就是理论教材里的三次握手。可以看到,在第一条应用数据(hello from tcp client)发送之前,双方已经耗费了 1.5 个 RTT 做连接建立。如果的网络对端在另一个城市,这个握手延迟会明显感知到。
连接关闭时,还会看到 FIN 和 ACK 组合的四次挥手过程。这些状态都可以在 Wireshark 的 Follow TCP Stream 功能里更直观地看到。
7.3 UDP 报文:直接发送应用数据
UDP 会话的抓包结果则完全不同。过滤条件匹配到的第一个包,就是hello from udp client的数据报文,没有任何握手包。整个通信过程就是两个数据报:
1. 客户端 -> 服务端: UDP Source port: 客户端端口 Destination port: 9002 2. 服务端 -> 客户端: UDP Source port: 9002 Destination port: 客户端端口UDP 报文里没有序列号、没有 ACK、没有窗口字段,只有源端口、目的端口、长度、校验和。谁的“重量”更大,一看便知。
7.4 一个常见困惑:为什么设置了 UDP 过滤还看到 ICMP
有读者问过:在 Wireshark 里设置了udp过滤条件,为什么还能抓到 ICMP 包?
这通常有两种原因。第一,Wireshark 的“显示过滤器”会实时过滤列表中的包,如果你之前用了捕获过滤器,或者在命令行用 tcpdump 抓包后导入文件,那文件里本来就有 ICMP 包;第二,UDP 通信失败时,网络协议栈会回 ICMP 端口不可达(Port Unreachable)报文,这个报文虽然不是 UDP,却是对 UDP 行为的反馈。你启动 Wireshark 时如果不小心用了udp or icmp类似的组合过滤,自然也会看到 ICMP。实际排查时,建议把过滤条件写成udp.port == 9002,避免误看。
8. 常见问题与排查思路
协议开发中遇到的问题,往往和协议本身密切相关。下面列出一份高频问题排查表,覆盖了 TCP 和 UDP 最常见的坑。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
服务启动报错only one usage of each socket address | 端口被其他进程占用,或残留 TIME_WAIT | Windows 用netstat -ano | findstr 端口;Linux 用lsof -i:端口或ss -tlnp | 杀掉占用进程,或更换端口;开发环境可开启 SO_REUSEADDR |
客户端 connect 报Connection refused | 服务端未启动,或服务端绑定地址不是客户端访问的地址 | 确认服务端监听地址,用telnet 目标IP 端口测试 | 启动服务端,确认监听 0.0.0.0 或客户端可达的网卡地址 |
curl: (35) tcp connection reset by peer | 服务端主动 RST,通常是服务未监听、连接队列满、TLS 版本不兼容或防火墙干预 | 抓包看 RST 从哪端发出;检查服务端口和日志 | 修复服务监听,调整连接队列,检查防火墙策略 |
| TCP connect 超时 | 对端 IP 不可达、防火墙上丢弃 SYN 包、对端负载过高不响应 | 先ping验证可达性,再用telnet验证端口;抓包看 SYN 是否有响应 | 检查路由和防火墙,确认服务端健康状态 |
| UDP 客户端收不到服务端响应 | 服务端绑定了 127.0.0.1,客户端访问局域网 IP;防火墙禁 UDP;NAT 未映射;服务端没写回包 | 同一台机器先用 127.0.0.1 测试;抓包看 UDP 包是否到达服务端 | 统一监听地址,配置防火墙放行,检查 NAT 映射,确认逻辑 |
| TCP 消息粘包/半包导致解析失败 | TCP 是字节流,没有消息边界,多条消息被合并或拆分 | 打印每次 recv 的原始长度和内容,观察边界 | 应用层协议统一采用“4 字节长度 + 内容体”等方式定义帧边界 |
| UDP 丢包严重 | 接收缓冲区太小、应用层处理慢、网络拥塞 | Linux 用netstat -su查看 UDP 丢包统计;Windows 查看性能计数器 | 加大接收缓冲区;优化处理逻辑;升级链路;必要时前向纠错 |
| 大量 TIME_WAIT 导致新连接建立变慢 | 高并发短连接频繁关闭,端口处于回收状态 | ss -tan state time-wait(Linux)统计状态 | 改用长连接或连接池;调优系统端口回收参数需谨慎,先在测试环境验证 |
Docker 容器访问外部服务报dial tcp ... i/o timeout | 容器网关、宿主机防火墙、NAT、DNS 解析或目标服务不可达 | 在宿主机先 curl 目标地址;再在容器内测试;对比结果 | 排查防火墙和 NAT 规则,确认 DNS 解析正常 |
排查网络问题时,一条最重要的原则是:先确认自己处于哪一层,再抓包看证据。不要凭日志里的“报错”猜测可能原因,直接看 Wireshark 或 tcpdump 里的报文,很多问题会立刻变得清晰。
9. 工程最佳实践与延伸学习
9.1 使用 TCP 的最佳实践
消息边界必须自己设计。TCP 没有消息边界,团队必须统一约定一套帧格式。最简单的方案是“4 字节长度 + 内容体”:先收 4 字节获得长度,再读取指定长度的内容。无论使用 C#、Java、Python 还是 Qt C++,这个思路都适用。
长连接必须有心跳。网络断开不会总是立刻触发 TCP 的关闭事件。中间设备掉电、路由器重启等情况,会让连接变成“半开连接”。建议在应用层定期发送心跳包或采用 TCP KeepAlive 机制,并在连续多次心跳无响应时主动重连。生产环境建议把心跳周期、超时次数做成配置项。
连接和端口管理要敬畏。服务端要考虑文件描述符上限、连接句柄释放、异常断开后的清理。客户端要设置 connect 超时,不要无限等待。如果你想评估一个进程能建立多少 TCP 连接,首先看系统文件描述符或句柄上限,同时关注内存占用。
依赖 TCP 不等于不需要业务重试。TCP 只保证传输层可靠,不保证业务逻辑正确。比如消息到达了对端但对端处理失败,TCP 不会感知。业务层的确认、幂等和补偿机制依然必要。
9.2 使用 UDP 的最佳实践
包大小要有控制。以太网链路常见 MTU 是 1500 字节,扣除 IP 头和 UDP 头后,建议单个 UDP 数据报的载荷控制在 1200 字节左右,避免 IP 分片带来的额外丢包风险。如果数据超过这个量,要么在应用层分包,要么考虑改用 TCP、或设计支持分片重组的应用层协议。
自建可靠机制是可选路径。如果业务需要 UDP 的实时性,又要一定可靠性,可以在 UDP 之上设计一个轻量可靠协议:给每个包加序列号,接收方回 ACK,发送方超时重传,接收方用缓存处理乱序。这个方案比直接用 TCP 复杂,但能保留对重传策略的完全控制。RTP、QUIC 都是类似思想的成熟产物。特别要注意,CRC32 这类校验和验证是应用层数据完整性校验的工作,UDP 首部的校验和只做基础检测,不能替代业务校验。
系统缓冲区要监控。UDP 流量突发时,如果接收缓冲区不足,内核会直接丢包。Linux 下可以查看/proc/sys/net/core/rmem_max等参数,按需调整;Windows 系统也有 UDP 接收缓冲区参数,但修改注册表前一定要备份,并在测试环境充分验证,避免影响线上服务。更稳妥的做法是先优化应用层处理速度,再考虑改系统参数。
安全边界要考虑。UDP 无连接的特性和容易被伪造源地址,历史上曾被用于反射放大攻击。生产环境不要将敏感 UDP 服务直接暴露到公网,应配合防火墙、身份校验、请求频率限制等安全手段。
9.3 性能测试与协议栈深入了解
如果你使用 iperf3 测试 UDP 打流,有一点要特别注意:发送端报告的是发送速率,接收端报告的是接收速率和丢包率。判断链路是否可靠,不能只看发送端数字,要对比两端的统计结果。接收端丢包率上升,说明链路或接收端处理能力存在瓶颈。
在此基础上,继续深入可以从三个方向展开:
- 协议规范层面:阅读 RFC 793(TCP)、RFC 768(UDP)、RFC 9000(QUIC/HTTP3),掌握协议设计意图。
- 内核实现层面:走读 Linux 内核 TCP/UDP 协议栈的收发路径,理解缓冲、分片、重传等细节在实际系统中如何处理。
- 应用协议层面:理解 WebSocket 和 TCP 的区别与联系。WebSocket 本质上是基于 TCP 的应用层协议,它在 TCP 之上增加了帧和消息边界,解决的是 TCP 应用层“没有边界”的问题。因此它不等于 TCP,更不能替代 UDP 在实时低延迟场景中的角色。
10. 总结
TCP 和 UDP 的区别,本质上不是“谁更高级”,而是“确定性与效率”的取舍。TCP 用连接管理、确认重传、流控拥塞控制,换来了可靠的字节流;UDP 用放弃这些机制,换来了低延迟、低开销和报文边界。产品选型时,不要问“哪个协议更可靠”,要问“我们的业务能否接受偶发丢包,是否能接受重传带来的延迟,团队的协议设计能力能不能兜底”。
动手验证是最好的学习方式。建议你把第六章的代码跑一遍,用 Wireshark 亲眼看一次三次握手,再用netstat或ss观察一次端口状态变化。把这些基础动作内化成习惯之后,再遇到线上连接超时、UDP 丢包、粘包错乱等问题,你就不会再凭感觉瞎猜,而是能快速定位到协议层,找到真正的原因。
这套内容值得收藏备用。下一次做通信方案评审时,再翻出来对照一次,你会发现自己对 TCP 和 UDP 的认知,已经远超“TCP 可靠、UDP 不可靠”这八个字。