手头有个服务突然连不上,第一反应查防火墙,第二反应怀疑代码改挂了,折腾了大半天,最后发现就是一行报错:listen tcp 127.0.0.1:11434: bind: only one usage of each socket addre。端口被占了。这种问题在我这几年的网络调试里碰到过太多次——TCP和UDP这两个端到端传输协议,考试的时候人人都能默写出"TCP可靠、UDP不可靠",可真到线上排错、性能压测、工控联调的时候,这些基础概念才显出真正的分量。
这篇就把TCP和UDP从头到尾掰开揉碎讲一遍。不绕弯子,直接用实际场景说话:三次握手到底在握什么、四次挥手为什么是四次、拥塞控制是怎么让网络"既快又不崩溃"的、端口占用怎么排、iperf3怎么打UDP流、选型的时候到底该闭眼选TCP还是UDP。适合刚入门想建立完整认知的开发者,也适合被线上网络问题折磨过的运维和工控工程师。
1. "端到端"到底在端什么:传输层在协议栈里的真实位置
1.1 从主机到进程:IP只解决"送到哪台机器",传输层解决"交给哪个程序"
很多人背七层模型背得滚瓜烂熟,但真要问一句"传输层和网络层到底谁干活",容易卡壳。我习惯用快递来类比:IP协议解决的是"这个包裹从上海寄到北京",它关心的是城市、街道、楼栋,也就是IP地址。但包裹到了楼栋之后,收件人是谁?是302的王先生还是501的李女士?这个"具体交给谁"的问题,就是端口号在解决的。
一个IP地址加一个端口号,才构成一个完整的"端点"。而传输层做的事情,就是在这些端点之间建立逻辑通信通道。所以教科书上叫它"端到端"协议,这个"端"指的不是电脑,而是电脑上跑着的某个进程、某个服务。比如你同时开着浏览器(用着443端口)、挂着SSH(用着22端口)、跑着数据库(用着3306端口),数据包到了你机器这个IP之后,是靠TCP/UDP头里的端口号来分发的。
实际排查的时候,我们常说的"四元组"就是这套逻辑的具象化:源IP、源端口、目的IP、目的端口。TCP连接其实就是靠这个四元组来唯一标识的。你在netstat里看到的一行ESTABLISHED连接,背后就是一个四元组在维护着状态。
1.2 网络层"尽力而为",可靠性的活全在传输层
这里有个常被误解的点:IP协议是"尽力而为"的,它不保证不丢包、不保证顺序、不保证不重复。丢包了谁来管?乱序了谁来排?就是传输层。
TCP把"可靠"这件事扛了下来:确认机制、超时重传、序列号排序、滑动窗口限速,这是TCP的全部家当。UDP则选择"裸奔":我把数据报发出去,能不能到、什么时候到、对不对,我都不管,交给上层去处理。所以"端到端"这个词的另一层意思,就是复杂度和可靠性都在端系统上实现,网络中间的路由器、交换机只管转发,不承担TCP的状态维护。
理解了这点,你就能解释很多现象:为什么局域网内UDP延迟这么低,因为中间设备不维护连接状态,数据包进来就转发;为什么TCP在有线网络里表现稳定,因为它把可靠性都做在了两端。这也是为什么我们要单独学习传输层——它才是应用层和网络层之间的翻译官和质检员。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. TCP的可靠性,是拿三次握手换来的
2.1 三次握手:不只是"打招呼",是在互相确认序列号起点
面试必考的三次握手,很多人理解为"你好""你好""好的",这么说没错,但不够本质。三次握手真正干的事情,是同步双方的初始序列号(ISN)。
TCP是面向字节流的,每个字节都有一个序列号,接收方靠序列号来排序、去重、确认。如果两端不从同一个起点对齐,后面的数据传输全乱套。所以三次握手的过程实际上是:
- 客户端发送
SYN报文,初始序列号seq=x,同时表示"我要建立连接,我的起始序号是x"。 - 服务端回复
SYN+ACK,报文的seq=y,确认号ack=x+1,表示"收到你的x,我的起始序号是y"。 - 客户端再回一个
ACK,seq=x+1,ack=y+1,表示"收到你的y,连接建立"。
我见过很多人在理解上纠结"握手是三次还是两次",一个非常实用的判断标准:至少需要一来一回确认双方的收发能力,且要同步双向的ISN,两个方向各需要一次SYN交换,再加上对第一次SYN的确认,最少就是三次。两次握手最大的问题在于,如果客户端第一个SYN在网络里滞留超时重传,服务端可能会建立一个已经过期的旧连接,浪费资源。三次握手让双方都确认了"对方确实活着、序列号确实对上了",再开始传数据。
用Wireshark抓一次包就能看得非常清楚。我建议每个做网络开发的人都亲自抓一次:启动抓包,然后telnet或者nc连一个端口,看那三个包的seq/ack变化,比背十遍教科书都有效。
2.2 四次挥手:TCP是全双工的,断开也得两边分别说再见
握手要三次,挥手却要四次,很多人不理解。关键在于TCP连接是全双工的——数据可以同时双向流动。断开的时候,每一端都必须单独关闭自己的发送通道,所以整个断开过程是四个动作:
- 主动关闭方发送
FIN,seq=m,表示"我的数据发完了"。 - 被动关闭方回复
ACK,ack=m+1,表示"知道了,但我这边可能还有数据要发"。 - 被动关闭方发完剩余数据后,也发送
FIN,seq=n。 - 主动关闭方回复
ACK,ack=n+1,连接彻底关闭。
这里最容易出问题的状态有两个:CLOSE_WAIT和TIME_WAIT。
CLOSE_WAIT
