2026/10/20 5:38:58

运输层核心机制解析:端口、UDP、TCP可靠传输与拥塞控制

运输层核心机制解析:端口、UDP、TCP可靠传输与拥塞控制 1. 运输层到底在整条网络链路里扮演什么角色很多人学计算机网络学到运输层就开始犯迷糊。前面物理层、数据链路层、网络层好歹还能对应到网线、网卡、IP地址这些看得见摸得着的东西一到运输层感觉突然就飘了——什么端口、什么复用分用、什么可靠传输好像都懂一点但串不起来。我当年第一次学这一章的时候也是这样背了一堆名词考试能过但真让我说清楚运输层到底解决了什么问题我答不上来。后来做了几年开发抓过无数次包写过socket程序回头再看这一章才发现谢希仁老师这本书的运输层章节其实讲得非常扎实只是当年我没有带着问题去读。运输层的核心定位用一句话概括网络层负责把数据送到正确的主机运输层负责把数据送到正确的进程。这个进程到进程的视角是理解整章内容的钥匙。你想想一台服务器上同时跑着Web服务、数据库服务、SSH服务数据包从网络上过来IP层只知道它属于这台机器但到底该交给哪个程序处理这就是运输层要干的事。谢希仁第七版这一章的结构其实很清晰从运输层协议概述讲起然后是UDP、TCP重点在TCP的可靠传输、流量控制、拥塞控制最后是TCP的连接管理。这个编排逻辑是先建立框架再逐个拆解但很多同学读的时候是线性读的读完UDP读TCP读完TCP读拥塞控制每个知识点单独看都懂合在一起就散了。我的建议是读这一章之前先建立一个问题链运输层要解决进程寻址问题端口要解决数据交给谁的问题复用与分用要解决传输可不可靠的问题UDP vs TCP要解决发太快把对方压垮的问题流量控制要解决发太快把网络压垮的问题拥塞控制要解决连接怎么建立和释放的问题三次握手四次挥手。你带着这六个问题去读每个知识点都能找到它的位置。这一章还有一个特点就是概念之间的因果关系特别强。比如为什么TCP需要滑动窗口因为要解决可靠传输和流量控制。为什么有了流量控制还需要拥塞控制因为流量控制只管接收方管不了中间网络。为什么TCP建立连接是三次握手而不是两次因为要防止失效的连接请求突然到达服务端造成资源浪费。这些为什么如果不想清楚光背结论考试一变形就废了。下面我按照自己的理解把这一章的重点知识重新梳理一遍。不是照搬书上的目录而是按照实际使用中你会遇到什么问题这个逻辑来组织。每个知识点我都会说清楚它解决什么问题、怎么解决的、容易在哪里踩坑。2. 端口与复用分用运输层最基础也最容易轻视的机制2.1 端口不是硬件接口是软件层面的进程标识很多初学者第一次听到端口这个词会下意识地把它和路由器上的网口、交换机上的端口混淆。这两个是完全不同的概念。路由器上的端口是物理接口运输层的端口是软件端口是操作系统用来标识进程的一个16位数字范围从0到65535。这个16位的设计不是随便定的。你算一下2的16次方等于65536也就是说理论上最多能同时标识六万多个进程。对于一台普通主机来说这个数量远远够用。但如果端口号设计成8位最多256个那服务器上稍微多跑几个服务就不够了。设计成32位呢又浪费因为一台机器上不可能同时跑几十亿个网络进程。所以16位是一个工程上的平衡点。端口号分为三类这个分类要记清楚熟知端口0到1023分配给最重要的系统服务。比如FTP用21SSH用22HTTP用80HTTPS用443DNS用53。这些端口在大多数操作系统上需要管理员权限才能绑定。登记端口1024到49151分配给一些注册过的应用比如MySQL默认3306Redis默认6379。动态端口49152到65535客户端发起连接时由操作系统临时分配用完就释放。这里有个实际开发中经常遇到的坑你写了一个服务端程序绑定了一个1024以下的端口在本地测试没问题因为你是管理员权限部署到服务器上用普通用户启动就报Permission denied。这不是代码问题是操作系统的安全策略。解决办法要么用sudo启动要么换一个1024以上的端口。2.2 复用与分用一台机器同时跑多个网络程序的秘密复用和分用是运输层最核心的机制之一但书上的定义比较抽象。我用一个生活场景来解释。假设你住在一栋公寓楼里楼里有很多住户。快递员送快递到楼下他只知道这栋楼的地址IP地址但不知道具体送给哪一户。这时候楼下的快递柜就起作用了每个住户有一个专属的柜子编号端口号快递员把包裹放进对应编号的柜子住户自己去取。复用就是多个住户的包裹都送到同一个快递柜多个进程的数据都交给运输层运输层加上端口信息后交给网络层。分用就是快递柜根据柜子编号把包裹分给不同住户运输层根据端口号把数据交给对应的进程。具体到技术层面发送方这边多个应用进程的数据通过各自的端口交给运输层运输层加上TCP或UDP头里面包含源端口和目的端口然后交给网络层。接收方这边网络层把数据交给运输层运输层根据目的端口号把数据交给对应的进程。这里要注意一个细节分用时运输层是根据目的端口来分发的但一个完整的TCP连接是由四元组唯一标识的源IP、源端口、目的IP、目的端口。这意味着同一个目的端口可以同时接受来自不同客户端的连接。比如你的Web服务器监听80端口同时有一万个客户端连过来服务器怎么区分靠的就是每个连接的四元组不同。这个知识点在实际开发中非常重要。很多人写服务器程序时想不通我一个端口怎么能同时处理这么多请求答案就是四元组。服务器accept之后操作系统会为每个连接维护一个独立的socket虽然它们的目的端口都是80但源IP和源端口不同所以不会混淆。2.3 端口号的实际查看与调试方法学理论不结合实际操作很容易忘。我建议你在自己电脑上跑几个命令亲眼看看端口是怎么回事。在Linux或macOS上查看当前监听的端口netstat -tlnp或者用更现代的ss命令ss -tlnp在Windows上netstat -ano你运行之后会看到一堆输出每一行代表一个正在监听的端口。重点关注Local Address那一列格式是IP:端口。如果显示0.0.0.0:80说明监听所有网卡的80端口如果显示127.0.0.1:3306说明只监听本地回环地址的3306端口外部访问不了。这里有一个非常实用的排查技巧如果你的服务部署到服务器上本地能访问但外部访问不了第一件事就是检查监听地址。很多程序默认绑定127.0.0.1只有改成0.0.0.0才能接受外部连接。这不是网络问题是绑定地址配置问题。我见过太多人在这上面浪费几个小时以为是防火墙问题其实是程序配置问题。还有一个命令值得记住lsof -i :8080这个命令可以查看是哪个进程占用了8080端口。当你启动服务报Address already in use的时候用这个命令一查就知道是谁占着。3. UDP被低估的简单协议3.1 UDP只做两件事复用分用和差错检测谢希仁书里对UDP的描述很精炼UDP只在IP的数据报服务之上增加了很少一点功能就是复用分用和差错检测。很多人看到这里就觉得UDP很简单不重要然后草草翻过。这是非常错误的。UDP的简单恰恰是它的优势。它不建立连接不保证可靠交付不进行流量控制和拥塞控制头部只有8个字节。这意味着什么意味着快。没有握手开销没有确认重传没有窗口管理发送方想发就发。UDP头部结构是这样的字段长度说明源端口2字节发送方端口可选目的端口2字节接收方端口必须长度2字节UDP数据报总长度检验和2字节检测传输错误就这四个字段一共8字节。对比TCP的20字节固定头部不含选项UDP的开销小得多。3.2 什么时候该用UDP什么时候必须用TCP这个问题在实际开发中经常被讨论。我的经验是看你的应用对可靠性和实时性的权衡。适合UDP的场景实时音视频视频通话、直播。丢一两帧画面用户可能察觉不到但如果用TCP丢包后重传导致的延迟会让画面卡顿体验更差。DNS查询一次查询就一个请求一个响应用TCP建立连接的开销比查询本身还大。游戏尤其是FPS游戏位置信息更新频率高旧的位置信息丢了就丢了重传过来也没用因为玩家已经移动到新位置了。广播和多播TCP只支持一对一UDP支持一对多。必须用TCP的场景文件传输丢一个字节文件就损坏了必须可靠。网页浏览HTML、CSS、JS任何一个文件不完整页面就乱了。邮件内容必须完整准确。数据库连接数据一致性要求极高。这里有一个常见的误区很多人以为UDP不可靠所以不能用在对可靠性有要求的场景。但实际上你可以在应用层自己实现可靠性机制。比如很多游戏用UDP传输但在应用层加了序列号、确认、重传逻辑。这样做的好处是你可以根据业务特点定制可靠性策略比TCP的通用策略更高效。3.3 UDP检验和的计算与验证UDP检验和的计算方法书上有详细说明但很多人看一遍就忘了。我用自己的话重新讲一遍。UDP检验和计算时需要加上一个伪头部。伪头部包含源IP地址、目的IP地址、协议号、UDP长度。为什么要加伪头部因为UDP检验和不仅要检验UDP数据报本身还要检验IP地址是否正确。虽然IP层有自己的检验和但那只检验IP头部不检验数据部分。UDP加上伪头部相当于多了一层保险。计算步骤把伪头部、UDP头部、UDP数据部分拼在一起把检验和字段置为0按16位分组如果总长度不是偶数补一个全0字节所有16位字进行二进制反码求和把结果取反码填入检验和字段接收方收到后把包括检验和在内的所有16位字再求一次反码和如果结果是全1说明没有差错。这个计算过程听起来繁琐但实际抓包的时候Wireshark会自动帮你验证。你只需要知道如果检验和错了UDP会直接丢弃这个数据报不会通知发送方。这是UDP不可靠的一个具体体现。4. TCP可靠传输滑动窗口不只是窗口4.1 停止等待协议为什么效率低要理解TCP的滑动窗口得先理解它要解决什么问题。最朴素的可靠传输方案是停止等待协议发送方发一个数据包然后停下来等确认收到确认后再发下一个。这个方案的问题太明显了假设发送方和接收方之间的往返时间RTT是100毫秒那么每100毫秒才能发一个数据包。如果每个数据包1KB那传输速率就是10KB/s。这在现代网络里慢得离谱。而且这还没考虑网络带宽。假设链路带宽是1Mbps理论上每秒能传125KB但因为停止等待实际只用了10KB/s利用率只有8%。这就是停止等待协议的根本缺陷信道利用率太低。书上有信道利用率的公式U Td / (Td RTT Ta)其中Td是发送数据的时间RTT是往返时间Ta是发送确认的时间。从这个公式可以看出当RTT远大于Td时利用率极低。4.2 连续ARQ与滑动窗口的工作机制解决停止等待效率低的方法就是流水线传输不等确认连续发送多个数据包。这就是连续ARQ协议也是TCP滑动窗口的基础。滑动窗口的核心思想是发送方维护一个窗口窗口内的数据可以连续发送不需要等确认。窗口的大小决定了能连续发多少数据。我用一个具体例子来说明。假设窗口大小是5个报文段发送方可以连续发送序号1、2、3、4、5不需要等确认收到1的确认后窗口向前滑动可以发送6收到2的确认后窗口再滑动可以发送7以此类推这里有一个关键点TCP的确认是累积确认。也就是说收到对序号5的确认表示5之前的所有数据都收到了。这个机制的好处是即使中间的确认丢了后面的确认也能覆盖。但累积确认也有缺点。假设发送方发了1到5接收方收到了1、2、4、5没收到3。接收方只能确认到2因为3没到累积确认不能确认3之后的。发送方收到对2的确认后窗口滑动但3、4、5还在窗口里需要重传。这时候发送方怎么知道该重传3靠的是超时重传或者快速重传。4.3 超时重传时间的选择一个容易被忽略的工程问题超时重传时间RTO的选择是TCP实现中最复杂的部分之一。选得太短会导致不必要的重传浪费带宽选得太长丢包后恢复慢影响体验。书上的公式是RTO RTTs 4 * RTTd其中RTTs是加权平均往返时间RTTd是往返时间的偏差。RTTs的计算新的RTTs (1 - α) * 旧的RTTs α * 新的RTT样本α通常取0.125。RTTd的计算新的RTTd (1 - β) * 旧的RTTd β * |RTTs - 新的RTT样本|β通常取0.25。这个公式看起来复杂但逻辑很简单RTO要略大于平均RTT并且要考虑RTT的波动。如果RTT很稳定RTTd就小RTO就接近RTTs如果RTT波动大RTTd就大RTO就相应增大避免误判超时。这里有一个实际中容易踩的坑Karn算法。当发生重传时收到的确认到底是对哪次发送的确认分不清。所以Karn算法规定重传时不更新RTT样本。但这样又会导致RTT样本不足所以后来又加了修正每重传一次RTO就增大一倍。这个细节在实际网络编程中很重要。如果你在做一个对延迟敏感的应用发现偶尔有卡顿可能就是RTO计算导致的。不过大多数情况下操作系统已经帮你处理好了你不需要自己实现。4.4 快速重传不等超时就重传超时重传的问题是等待时间太长。如果RTO是200毫秒丢包后要等200毫秒才重传这期间接收方一直在等体验很差。快速重传的思路是如果接收方连续收到三个相同的确认说明有一个报文段丢了发送方立即重传不等超时。举个例子发送方发了1、2、3、4、5。接收方收到1确认1收到2确认2收到4发现3没到还是确认2收到5还是确认2。发送方收到三个对2的重复确认立即重传3。这个机制大大加快了丢包恢复速度。实测下来快速重传能把恢复时间从几百毫秒降到几十毫秒。5. 流量控制与拥塞控制两个容易混淆的概念5.1 流量控制别把接收方压垮流量控制要解决的问题是发送方发得太快接收方处理不过来。TCP通过滑动窗口实现流量控制。接收方在确认报文里带上自己的接收窗口rwnd告诉发送方我还能接收这么多数据。发送方的发送窗口不能超过接收方的接收窗口。这里有一个死锁问题如果接收方发送的窗口更新报文丢了发送方还在等窗口更新接收方在等数据双方就僵住了。解决办法是持续计时器发送方收到零窗口通知后启动一个计时器时间到了就发一个探测报文接收方收到后回复当前的窗口大小。这个机制在实际中很少出问题但如果你在做一个高并发的服务器接收方处理慢窗口经常变成0就会触发持续计时器。这时候你会看到连接还在但数据传输停了过一会儿又恢复。这不是bug是TCP的正常行为。5.2 拥塞控制别把网络压垮拥塞控制和流量控制容易混淆但它们的关注点完全不同。流量控制关注的是接收方的处理能力拥塞控制关注的是中间网络的承载能力。为什么需要拥塞控制因为如果所有发送方都拼命发数据中间路由器的缓冲区会满然后开始丢包。丢包后发送方重传网络更堵形成拥塞崩溃。TCP的拥塞控制有四个核心算法慢开始刚开始发送时拥塞窗口cwnd设为1个报文段然后每收到一个确认cwnd加1。这样cwnd呈指数增长1、2、4、8、16……直到达到慢开始门限ssthresh。拥塞避免当cwnd达到ssthresh后改为每经过一个RTTcwnd加1。这样cwnd呈线性增长避免增长过快导致拥塞。快重传收到三个重复确认立即重传丢失的报文段。快恢复快重传后不回到慢开始而是把ssthresh设为当前cwnd的一半cwnd设为新的ssthresh然后进入拥塞避免。这四个算法配合起来形成了一个完整的拥塞控制机制。我用一个实际场景来说明假设ssthresh初始值是16。发送方从cwnd1开始慢开始cwnd增长到16后进入拥塞避免。cwnd慢慢增长到24时发生超时。这时候ssthresh设为1224的一半cwnd回到1重新慢开始。cwnd增长到12后进入拥塞避免。如果是快重传触发ssthresh设为12cwnd设为12直接进入拥塞避免不回到1。这就是快恢复的优势不用从头开始。5.3 发送窗口到底取哪个值这是一个考试常考、实际也重要的点发送方的发送窗口取接收窗口和拥塞窗口中的较小值。发送窗口 min(rwnd, cwnd)为什么因为要同时满足两个约束不能超过接收方的处理能力rwnd也不能超过网络的承载能力cwnd。这个公式解释了一个现象为什么有时候网络带宽很足但传输速度上不去可能是接收方处理慢rwnd小也可能是网络路径上有拥塞cwnd小。排查的时候要两边都看。6. TCP连接管理三次握手和四次挥手的细节6.1 三次握手为什么不是两次或四次三次握手的流程大家都背过SYN、SYNACK、ACK。但为什么是三次核心原因是要让双方都确认对方的发送和接收能力都正常。第一次握手客户端发SYN服务端收到。服务端确认客户端的发送能力正常服务端的接收能力正常。第二次握手服务端发SYNACK客户端收到。客户端确认服务端的发送能力正常客户端的接收能力正常。但服务端还不知道客户端能不能收到。第三次握手客户端发ACK服务端收到。服务端确认客户端的接收能力正常。所以三次握手是保证双方都确认自己和对方的收发能力都正常的最小次数。为什么不是两次因为如果只有两次服务端发完SYNACK就认为连接建立了但万一客户端没收到呢客户端会认为连接没建立服务端却认为建立了资源就浪费了。更严重的是如果有一个失效的连接请求突然到达服务端服务端以为是新连接发SYNACK客户端不理会服务端就一直等着白白占用资源。三次握手可以避免这个问题。6.2 四次挥手与TIME_WAIT状态四次挥手比三次握手多一次因为TCP连接是全双工的每个方向都要单独关闭。第一次挥手客户端发FIN表示我没数据要发了。第二次挥手服务端发ACK表示我知道了。但服务端可能还有数据要发所以不能立即关闭。第三次挥手服务端发FIN表示我也没数据要发了。第四次挥手客户端发ACK表示我知道了。这里最容易被问到的是TIME_WAIT状态。客户端发完最后一个ACK后不立即关闭而是等2MSL最长报文段寿命才关闭。为什么两个原因确保最后一个ACK能到达服务端。如果ACK丢了服务端会重发FIN客户端还能响应。让本次连接的所有报文段从网络中消失避免影响后续的新连接。TIME_WAIT在实际运维中经常遇到。如果你用netstat看到大量TIME_WAIT连接说明短连接很多。这不是bug是TCP的正常行为。但如果TIME_WAIT太多导致端口耗尽就需要优化比如启用tcp_tw_reuse注意这个选项有争议不是所有场景都适用。6.3 抓包验证用Wireshark看三次握手理论讲再多不如自己抓一次包。我建议你用Wireshark抓一次本地的HTTP请求看看三次握手和四次挥手的过程。步骤打开Wireshark选择回环接口lo0或Loopback在过滤器里输入tcp.port 80或者你访问的端口用浏览器访问一个HTTP网站注意现在很多网站是HTTPS端口443停止抓包找到三次握手的包你会看到第一个包Flags是SYNSeq0第二个包Flags是SYNACKSeq0Ack1第三个包Flags是ACKSeq1Ack1然后你可以看到HTTP请求和响应最后是四次挥手。抓包的时候注意Seq和Ack是相对值Wireshark默认显示相对序号方便阅读。你可以在Wireshark设置里改成显示绝对序号。这个实操经验比背十遍书都有用。我当年就是抓了一次包之后才真正理解三次握手是怎么回事。7. 这一章学完之后实际工作中怎么用学完运输层很多人会觉得这些理论工作中用不到。其实不是。我举几个实际场景。场景一服务端大量TIME_WAIT。前面说过短连接多会导致TIME_WAIT堆积。如果你做的是一个高并发短连接服务比如HTTP API就需要关注这个。解决方案包括用长连接Keep-Alive、调整内核参数、或者用连接池。场景二传输大文件速度上不去。可能的原因有很多接收窗口太小、拥塞窗口太小、RTT太大。排查的时候先用ping看RTT再用抓包看窗口大小。如果是接收窗口小可能是接收方处理慢如果是拥塞窗口小可能是网络有丢包。场景三UDP丢包严重。UDP本身不保证可靠丢包是正常的。但如果丢包率太高就要考虑是不是网络拥塞、MTU设置不对、或者应用层发送速率太快。可以在应用层加一些简单的控制比如根据丢包率动态调整发送速率。场景四连接建立慢。三次握手需要1.5个RTT。如果RTT是100毫秒建立连接就要150毫秒。对于延迟敏感的应用这个开销不能忽略。解决方案包括TCP Fast Open但支持度有限、连接复用、或者用QUIC基于UDP的可靠传输。这一章的知识点看起来多但核心逻辑就一条运输层要在不可靠的网络层之上为应用层提供进程到进程的通信服务。UDP选择简单把可靠性交给应用层TCP选择复杂在运输层实现可靠传输、流量控制、拥塞控制。理解了这条主线各个知识点就能串起来了。最后分享一个我自己的学习方法每学完一个协议就想象自己是一个数据包从发送方出发经过运输层、网络层、数据链路层到达接收方再一层层上去。每经过一层问自己这一层对我做了什么加了什么头改了什么这样走几遍整个协议栈就活了。