1. 传输层到底在解决什么问题很多人学网络协议学到传输层就开始卡壳。前面物理层、数据链路层、网络层还能靠“网线”“交换机”“路由器”这些看得见摸得着的东西撑着理解一到传输层全是端口、序列号、窗口、标志位抽象得让人头皮发麻。我刚开始接触这块的时候也一样书翻了三遍TCP三次握手背得滚瓜烂熟但真要我讲清楚“为什么需要三次而不是两次”“流量控制和拥塞控制到底差在哪”立马露馅。传输层的位置很特殊。它夹在网络层和应用层之间往下要面对网络层“尽力而为、不保证可靠”的IP协议往上要给应用层提供一个“看起来靠谱”的通信通道。你可以把网络层想象成一个不太靠谱的快递公司它只负责把包裹扔出去丢不丢、到不到、顺序乱不乱一概不管。传输层就是那个在快递公司之上做二次保障的角色它要么自己扛起可靠性的大旗TCP要么干脆躺平只做最基本的多路复用UDP。这个“要么扛要么躺”的选择就是传输层最核心的分野。TCP把可靠性、顺序性、流量控制、拥塞控制全揽在自己身上代价是复杂、慢、开销大UDP几乎什么都不管只加了个端口号做区分换来的是快、轻、灵活。理解传输层本质上就是理解这两种设计哲学各自的取舍以及它们分别适合什么样的场景。这篇文章我会从端口和复用讲起把UDP的简单讲透然后重点拆TCP的可靠传输、流量控制、拥塞控制、连接建立与释放这几大块。每一块我都会说清楚“为什么这么设计”“参数怎么算”“实际会遇到什么问题”。适合已经了解网络分层、想真正搞懂传输层底层逻辑的读者也适合面试前想系统梳理一遍的朋友。2. 端口、复用与分用传输层的第一层地基2.1 为什么传输层需要端口网络层用IP地址定位到一台主机但一台主机上同时跑着浏览器、聊天软件、下载工具、后台服务IP数据报到了主机之后到底该交给哪个进程处理这就是端口要解决的问题。端口是一个16位的数字范围0到65535。传输层在IP地址的基础上再加一层端口标识就能精确地把数据交付到某个具体的应用进程。IP地址负责“找到哪台机器”端口负责“找到机器上的哪个程序”两者合起来构成一个套接字Socket地址也就是常说的“IP:端口”二元组。端口分三类这个分类在实际开发和排查问题时很有用熟知端口0到1023分配给系统级或广泛使用的服务比如HTTP的80、HTTPS的443、DNS的53、SSH的22。这些端口在类Unix系统上通常需要管理员权限才能绑定。注册端口1024到49151分配给用户进程或一些注册过的应用服务比如MySQL的3306、Redis的6379。动态端口49152到65535也叫临时端口或私有端口客户端发起连接时由操作系统随机分配用完就回收。提示排查“端口被占用”问题时先确认你用的端口落在哪个区间。绑定1024以下的端口失败很多时候不是端口冲突而是权限不够。2.2 复用与分用的实际含义复用和分用是传输层两个方向相反的动作。发送方多个应用进程共用同一个传输层协议、同一个网络层通道往外发数据这叫复用接收方传输层根据端口号把数据准确分发给对应的应用进程这叫分用。举个生活化的例子。一栋写字楼主机里有很多公司进程快递员网络层只认楼号不认公司。楼里设了一个收发室传输层所有公司往外寄件都交给收发室统一处理复用外面寄来的件由收发室根据收件公司名端口号分发到各公司分用。收发室就是传输层公司名就是端口号。这里有个容易混淆的点TCP和UDP各自维护一套独立的端口空间。也就是说TCP的80端口和UDP的80端口是两个完全不同的东西互不冲突。DNS就同时用了UDP的53和TCP的53前者用于常规查询后者用于区域传送等需要可靠传输的场景。2.3 套接字与连接的唯一标识一个TCP连接由四元组唯一标识源IP、源端口、目的IP、目的端口。只要这四个里有一个不同就是不同的连接。这就是为什么同一台服务器能在80端口上同时服务成千上万个客户端——每个客户端的源IP和源端口组合都不一样四元组各不相同。理解这一点对排查问题很关键。有时候你发现服务器上某个端口“连接数爆了”其实是大量客户端用不同的临时端口连过来每个连接都是独立的四元组。真正限制连接数的往往不是端口数量而是文件描述符上限、内存、CPU这些资源。UDP没有连接的概念它只用二元组目的IP、目的端口来分用源信息不参与连接标识。这也是UDP“无连接”的一个具体体现。3. UDP把简单做到极致3.1 UDP数据报的头部结构UDP的头部只有8个字节四个字段每个2字节源端口、目的端口、长度、校验和。就这么简单没有序列号没有确认号没有窗口没有标志位。源端口可选不需要回复时可以填0。目的端口必填分用的依据。长度UDP头部加数据的总长度最小是8只有头部没有数据。校验和用于检测数据在传输中是否出错计算时会加上一个伪头部包含源IP、目的IP、协议号、UDP长度这样能顺带校验IP地址是否正确送达。校验和是可选的在IPv4里可以填0表示不校验IPv6里则强制要求。实际中大多数实现都会计算校验和因为网络传输出错虽然概率低但一旦出错没有校验就会把脏数据交给应用层后果可能很严重。3.2 UDP为什么“不可靠”却依然重要UDP不保证交付、不保证顺序、不保证不重复也不做流量控制和拥塞控制。发出去就不管了丢包了也不会重传。听起来很糟糕但正是这种“什么都不管”让它有了TCP给不了的优势。第一是快。没有握手没有确认没有等待数据封装好直接扔给网络层。对于DNS查询这种“一问一答、数据量小”的场景TCP三次握手的开销比查询本身还大用UDP就非常合适。第二是轻。头部只有8字节TCP头部最少20字节。对于实时音视频这种每秒要发大量小包的场景头部开销的差异会被放大。第三是灵活。UDP把控制权完全交给应用层。应用层可以根据自己的需求决定要不要重传、要不要做拥塞控制、要不要保证顺序。很多实时协议就是在UDP之上自己实现了一套适合实时场景的机制因为TCP的“可靠”在实时场景下反而是负担——等一个丢失的旧包重传不如直接丢掉它继续播新的。3.3 UDP的典型应用场景DNS查询一问一答数据小要求快。查询失败重发一次就行比建立TCP连接划算。实时音视频丢一两帧画面用户几乎无感但如果因为重传导致卡顿体验会急剧下降。在线游戏玩家位置这种状态数据新的比旧的更重要丢了旧包直接发新包即可。广播和多播TCP只支持一对一UDP天然支持一对多。简单请求响应协议比如某些监控上报、日志采集丢一条无所谓不需要TCP的可靠性开销。注意用UDP做应用时一定要自己考虑丢包、乱序、重复的问题。很多人第一次用UDP写程序发现“偶尔收不到数据”就慌了其实这是UDP的正常行为不是bug。要么在应用层加确认重传要么接受这种不可靠。3.4 UDP校验和的计算细节校验和的计算方法是把伪头部、UDP头部、数据全部按16位分组逐组做二进制反码求和最后取反码。如果数据长度是奇数末尾补一个0字节凑成偶数。接收方收到后做同样的计算如果结果全为1即反码为0说明没有检测到错误。注意校验和只能检测错误不能纠正错误检测到错误后UDP的处理方式是直接丢弃不会通知发送方。这里有个实操中的坑某些老旧的网络设备或虚拟化环境可能会错误地计算或修改校验和导致明明数据没问题却被丢弃。如果你在虚拟化环境里遇到UDP数据莫名丢失可以尝试关闭发送方的UDP校验和填0来验证是不是这个原因。当然生产环境不建议长期关闭校验和。4. TCP可靠传输把不可靠的网络变成可靠的通道4.1 序列号与确认号可靠传输的基石TCP要实现可靠传输首先要解决“怎么知道对方收到了”这个问题。它的方案是给每个字节编号接收方收到后回一个确认告诉发送方“我已经收到了第N号字节之前的所有数据”。TCP的序列号不是按报文段编号而是按字节编号。假设一个报文段携带了1000字节的数据起始序列号是1那么这个报文段覆盖的序列号范围是1到1000下一个报文段的起始序列号就是1001。这种按字节编号的方式让确认可以非常精确。确认号的含义是“期望收到的下一个字节的序列号”。如果接收方回确认号1001意思是“1到1000我都收到了请给我发1001开始的数据”。这种设计叫累积确认一个确认可以确认之前所有连续到达的数据不需要每个报文段单独确认。4.2 超时重传与RTT估算发送方发出数据后启动一个定时器如果在超时时间内没收到确认就重传。超时时间设多长是个技术活设太短网络稍微抖动就触发不必要的重传浪费带宽设太长真丢包了要等很久才重传传输效率低。TCP的做法是动态估算往返时间RTT。它维护一个平滑的RTT估计值和一个RTT偏差估计值超时时间由这两个值计算得出。具体来说每测量到一个新的RTT样本就用加权平均的方式更新平滑RTT同时更新偏差估计最终超时时间等于平滑RTT加上若干倍的偏差。这个机制的好处是能自适应网络状况。网络好的时候超时时间短重传快网络差的时候超时时间长避免误判。实际实现中超时时间还有一个最小值限制防止在极低延迟网络里设得过短。4.3 快速重传不等超时就重传超时重传的问题是等待时间可能很长。TCP还有一个更快的重传机制叫快速重传。原理是如果接收方收到一个失序的报文段它会立即重复发送上一个确认号。发送方如果连续收到三个重复的确认就判断这个确认号对应的报文段丢失了不等超时定时器到期就直接重传。为什么是三个而不是一个因为网络传输中偶尔出现乱序是正常的一个重复确认可能是乱序造成的。连续三个重复确认说明大概率是真丢包了而不是乱序。这个阈值是经验值在大多数网络环境下能很好地平衡误判和响应速度。4.4 选择确认只重传真正丢失的部分累积确认有个缺点如果发送方发了1到1000、1001到2000、2001到3000三个报文段中间那个丢了接收方只能确认到1000。发送方不知道2001到3000其实已经到了可能会把1001到3000全部重传浪费带宽。选择确认SACK解决了这个问题。接收方可以在确认里附加一个选项告诉发送方“我额外收到了哪些不连续的块”。这样发送方就能精确地只重传真正丢失的部分。SACK在现代网络里广泛支持对高带宽高延迟网络的性能提升非常明显。实操心得如果你在做性能调优发现TCP重传率偏高但带宽利用率上不去可以检查一下SACK是否启用。在某些老旧的中间设备上SACK选项可能被剥离导致退化成累积确认重传效率大幅下降。5. 流量控制别把接收方撑爆5.1 滑动窗口的基本原理可靠传输解决了“数据会不会丢”的问题但还有一个问题发送方发得太快接收方处理不过来怎么办接收方的缓冲区是有限的如果发送方不管不顾地猛发缓冲区满了之后新来的数据只能丢弃然后触发重传形成恶性循环。流量控制就是让接收方告诉发送方“我还能接收多少数据”。TCP用窗口字段来实现这个机制。接收方在每个确认里带上自己的接收窗口大小表示“从确认号开始我还能接收这么多字节”。发送方根据这个窗口调整自己的发送速率保证不会超出接收方的处理能力。这个窗口是动态变化的。接收方应用层读取数据越快缓冲区空得越多窗口就越大应用层处理慢缓冲区堆积窗口就缩小甚至变成0。窗口为0时发送方停止发送但会定期发送窗口探测报文询问接收方窗口是否恢复。5.2 零窗口与窗口探测当接收方缓冲区满时它会通告一个零窗口。发送方收到零窗口后停止发送数据但不会无限期等待。它会启动一个持续定时器每隔一段时间发送一个窗口探测报文这个报文只带一个字节的数据或者不带数据目的是触发接收方回复当前的窗口大小。如果接收方窗口恢复了会回复一个非零窗口发送方继续发送。如果还是零窗口发送方继续等待并逐渐延长探测间隔。这个机制保证了即使窗口曾经为零连接也不会死锁。注意零窗口在实际中可能引发死锁。如果接收方窗口恢复的通知丢失了发送方可能一直以为窗口是零。窗口探测就是为了打破这种僵局。如果你在排查“连接卡住不动”的问题可以抓包看看是不是卡在零窗口状态。5.3 糊涂窗口综合症与Nagle算法糊涂窗口综合症是指发送方每次只发很少的数据或者接收方每次只通告很小的窗口导致大量小报文在网络里传输头部开销远大于数据本身效率极低。解决这个问题有两个方向。接收方这边不要一有空间就通告小窗口而是等窗口达到一定大小比如缓冲区的一半或者缓冲区从满变空时再通告。发送方这边Nagle算法规定如果还有未确认的数据就先把小数据攒起来等收到确认或者攒够一个最大报文段再发。Nagle算法能有效减少小报文但和延迟确认一起用时可能产生额外延迟。延迟确认是指接收方不立即回确认而是等一小段时间看能不能捎带数据一起发。如果发送方在等确认才发下一批数据接收方在等数据才发确认就会互相等待造成延迟。在对延迟敏感的场景比如交互式应用通常会关闭Nagle算法。6. 拥塞控制别把网络压垮6.1 流量控制与拥塞控制的区别流量控制关心的是接收方的处理能力拥塞控制关心的是整个网络的承载能力。这是两个完全不同的问题。接收方可能处理得过来但中间链路已经堵死了这时候继续猛发只会让情况更糟。拥塞控制的核心思想是发送方维护一个拥塞窗口这个窗口的大小根据网络状况动态调整。实际发送窗口取拥塞窗口和接收窗口的较小值。网络顺畅时拥塞窗口增大检测到拥塞时拥塞窗口减小。6.2 慢启动从零开始试探慢启动不是“慢”而是“从小开始”。连接刚建立时发送方不知道网络能承受多少所以从一个较小的拥塞窗口开始通常是几个最大报文段每收到一个确认就把拥塞窗口翻倍。这样窗口呈指数增长很快就能探到网络的承载上限。指数增长很快但也很危险容易一下子把网络压垮。所以慢启动设了一个阈值叫慢启动门限。当拥塞窗口超过这个门限时就进入拥塞避免阶段增长速度从指数降为线性。6.3 拥塞避免线性增长谨慎试探拥塞避免阶段每经过一个往返时间拥塞窗口只增加一个最大报文段。这种线性增长比指数增长温和得多让发送方能更谨慎地接近网络容量。当检测到拥塞时TCP会大幅减小拥塞窗口。早期的实现直接把窗口降到1然后重新慢启动这种反应过于激烈。后来的快速恢复机制让窗口降到门限值附近然后直接进入拥塞避免而不是回到起点这样能更快恢复传输效率。6.4 不同拥塞控制算法的取舍TCP拥塞控制算法经过多年演进出现了很多变种各有侧重算法核心思路适用场景Tahoe检测到拥塞就降到1重新慢启动早期实现现在很少用Reno快速重传加快速恢复窗口减半经典算法低延迟网络表现好CUBIC用三次函数控制窗口增长高带宽高延迟网络Linux默认BBR基于带宽和延迟测量主动控制高丢包网络Google提出CUBIC是Linux的默认算法它在高带宽长距离网络里比Reno表现好很多。BBR则完全换了个思路不把丢包当作拥塞信号而是主动测量瓶颈带宽和最小往返时间据此控制发送速率。在有一定丢包率的网络里BBR往往能跑出比CUBIC更高的吞吐。实操心得如果你在跨地域传输大量数据发现TCP吞吐上不去可以试试切换拥塞控制算法。Linux下用sysctl net.ipv4.tcp_congestion_control查看当前算法用sysctl -w net.ipv4.tcp_congestion_controlbbr切换。不同网络环境下最优算法不同建议实测对比。7. 连接建立三次握手的每一个细节7.1 为什么是三次而不是两次三次握手的目的是让双方都确认对方的收发能力正常并且同步初始序列号。第一次握手客户端发一个同步标志位置1的报文带上自己的初始序列号。服务端收到后知道客户端的发送能力正常服务端的接收能力正常。第二次握手服务端回一个同步标志位和确认标志位都置1的报文带上自己的初始序列号并确认客户端的序列号。客户端收到后知道服务端的收发能力都正常自己的发送能力正常。第三次握手客户端再发一个确认报文确认服务端的序列号。服务端收到后知道客户端的接收能力正常。如果只有两次握手服务端在第二次握手后就认为连接建立了但此时它还不知道客户端是否收到了自己的同步报文。如果客户端的确认丢失或者客户端根本没收到服务端会白白维护一个连接浪费资源。三次握手就是为了让双方都确认对方的收发能力避免这种情况。7.2 初始序列号的选择初始序列号不是随便选的也不是从0开始。如果每次连接都从0开始旧连接的延迟报文可能被新连接误认为是有效数据造成混乱。所以初始序列号要随机化并且随时间变化让旧报文很难恰好落在新连接的序列号范围内。现代实现里初始序列号的生成会结合一个随时间递增的计数器和一个随机偏移既保证不可预测性又保证唯一性。不可预测性还有一个安全考虑防止攻击者猜测序列号来伪造报文。7.3 半连接队列与全连接队列服务端收到第一次握手后会把这个连接放入半连接队列此时连接处于同步已接收状态。收到第三次握手后连接移入全连接队列等待应用层接受。这两个队列的长度都是有限的。如果半连接队列满了新的第一次握手会被丢弃客户端会超时重传。如果全连接队列满了服务端通常不会丢弃第三次握手而是忽略它让客户端重传或者根据配置直接重置连接。注意在高并发场景下这两个队列的长度需要根据实际情况调整。队列太短会导致连接建立失败率上升队列太长会占用过多内存。排查“连接超时”问题时可以看看是不是队列溢出导致的。8. 连接释放四次挥手的完整流程8.1 为什么是四次挥手TCP是全双工的两个方向的数据流独立。关闭连接时一方停止发送数据但另一方可能还有数据要发所以不能一次性关闭两个方向。第一次挥手主动关闭方发送结束标志位置1的报文表示自己没有数据要发了但仍然可以接收数据。第二次挥手被动关闭方回一个确认表示知道了。此时主动关闭方进入半关闭状态被动关闭方还可以继续发送数据。第三次挥手被动关闭方数据发完了也发送一个结束标志位置1的报文。第四次挥手主动关闭方回一个确认。至此连接完全关闭。8.2 时间等待状态的作用主动关闭方在发送最后一个确认后不会立即关闭连接而是进入时间等待状态等待两倍的最大报文段生存时间。为什么要等两个原因。第一如果最后一个确认丢失了被动关闭方会重传它的结束报文主动关闭方需要还在这个状态才能收到并重发确认。如果直接关闭被动关闭方会一直重传直到超时无法正常关闭。第二让本次连接的所有报文在网络中彻底消失避免它们干扰后续使用相同四元组的新连接。等待时间设为两倍最大报文段生存时间是因为一个报文在网络中最多存活一个最大报文段生存时间确认它到达对端并触发对端关闭也需要一个最大报文段生存时间加起来就是两倍。8.3 连接重置的触发条件除了正常的四次挥手TCP还有一种粗暴的关闭方式叫连接重置。当一方收到不期望的报文时会发送一个重置标志位置1的报文立即终止连接。常见触发场景包括向一个没有监听的端口发起连接收到同步报文后回重置连接已经关闭但收到数据回重置应用层主动设置重置选项关闭连接。连接重置不经过四次挥手数据可能丢失所以正常关闭应该用四次挥手重置只用于异常情况。实操心得抓包时看到重置报文不要慌先看是谁发的、在什么状态下发的。如果是服务端在收到同步后回重置说明端口没监听如果是数据传输中突然重置可能是中间设备干预或者应用层异常退出。结合上下文判断不要一看到重置就以为是攻击。9. 常见问题与排查技巧实录9.1 传输层问题速查表现象可能原因排查方向连接建立超时半连接队列满、防火墙拦截、路由不通检查队列长度、抓包看同步报文是否到达连接建立后立即断开全连接队列满、应用层拒绝检查队列溢出计数、应用日志传输中卡住不动零窗口、拥塞窗口降为1、中间设备丢包抓包看窗口字段、检查重传率吞吐量远低于带宽拥塞控制算法不合适、窗口太小、RTT高切换算法、调整窗口、检查带宽延迟积大量重复确认乱序或丢包区分是乱序还是真丢包看是否触发快速重传连接无法关闭时间等待状态堆积、结束报文丢失检查时间等待连接数、抓包看挥手流程9.2 抓包分析的基本思路排查传输层问题抓包是最直接的手段。但抓包容易陷入“数据太多不知道看什么”的困境。我的经验是先明确目标是连接建立问题、传输问题还是关闭问题然后过滤出相关的报文按时间顺序看状态变化。看三次握手时重点关注序列号和窗口字段。看数据传输时重点关注确认号的变化、窗口大小的变化、有没有重复确认和重传。看四次挥手时重点关注结束报文和确认的配对情况。如果重传率很高先判断是超时重传还是快速重传。超时重传说明超时时间设置可能不合理或者网络延迟波动大。快速重传说明有丢包但后续报文到达了网络存在一定程度的拥塞或乱序。9.3 几个容易踩的坑第一个坑是把UDP的丢包当成bug。UDP本来就不保证可靠丢包是正常行为。如果你的应用需要可靠传输要么用TCP要么在UDP之上自己实现确认重传机制。第二个坑是忽略窗口字段。很多人调优时只盯着带宽和延迟忘了窗口才是决定实际吞吐的关键因素之一。带宽延迟积大的网络里窗口不够大带宽根本跑不满。第三个坑是混淆流量控制和拥塞控制。接收方窗口大不代表网络能承受网络能承受也不代表接收方能处理。实际发送窗口取两者较小值任何一个成为瓶颈都会限制吞吐。第四个坑是认为三次握手一定安全。三次握手能确认双方收发能力但不能防止所有问题。比如同步洪水攻击就是利用半连接队列有限来消耗服务端资源。实际部署中需要配合其他防护手段。9.4 参数调优的实操建议Linux下有一些传输层相关的内核参数值得关注。接收缓冲区和发送缓冲区的大小会影响窗口的最大值在高带宽延迟积网络里需要适当调大。时间等待状态的连接数过多时可以调整相关参数来加快回收但要注意不要影响可靠性。拥塞控制算法的选择要根据实际网络环境来定。低延迟局域网里Reno和CUBIC差别不大高带宽长距离网络里CUBIC优势明显有一定丢包率的网络里BBR可能更合适。没有万能的最优算法只有最适合当前场景的算法。提示调优前先测量调优后再测量用数据说话。不要凭感觉改参数也不要照搬别人的配置。网络环境千差万别适合别人的不一定适合你。10. 我在实际工作中的几点体会传输层这块内容书本上讲的和实际遇到的往往有差距。书本上假设网络是理想的丢包是偶发的但真实网络里中间设备可能做各种奇怪的事情比如修改窗口字段、剥离选项、甚至伪造确认。我遇到过中间设备把窗口字段改成固定值导致吞吐上不去的情况排查了很久才定位到。另一个体会是理解传输层最好的方式是自己写代码实现一个简化版。不用实现完整的TCP就实现一个带确认重传和滑动窗口的可靠传输协议跑一遍就能深刻理解为什么需要序列号、为什么需要窗口、为什么需要超时重传。纸上得来终觉浅这句话在传输层学习上特别适用。最后说一个实用技巧。如果你在排查传输层问题先看统计信息再看抓包。操作系统提供了很多TCP统计计数器比如重传数、错误数、连接数等。这些计数器能快速告诉你问题的大致方向避免一上来就抓包陷入海量数据。方向明确了再抓包看细节效率会高很多。