计算机网络运输层核心:TCP与UDP可靠传输、连接管理及排查指南 很多人学计算机网络到了运输层这一章就开始发怵前面刚搞明白IP地址、路由协议结果突然冒出来一堆端口、序号、确认号、滑动窗口还有三次握手、四次挥手信息量一下子翻了好几倍。这篇《计算机网络》知识梳理系列写到第五篇轮到运输层我不打算把教材目录再念一遍而是想按照“这东西到底解决什么问题、为什么这么设计、实际排查时怎么用”这条线把这章真正串起来。运输层在整个网络体系里很特殊。网络层负责把数据送到目的主机但一台主机上同时跑着几十上百个应用数据到了之后该交给谁这件事网络层不负责得靠运输层解决。它向上提供进程到进程的通信向下衔接网络层手里握着两个差异极大的协议一个是什么都不管的UDP一个是把可靠、有序、流量控制、拥塞控制全扛在肩上的TCP。这两个家伙可以说是设计哲学的极端对照理解了它们为什么长得完全不同运输层这一章也就吃透了一大半。不论你是正在备考的学生还是工作中排查连接问题需要补课的开发者这篇梳理都值得花二十分钟读完。1. 运输层到底在解决什么问题端口、进程与端到端1.1 网络层和运输层有什么分工把视角拉高一点。网络层里的IP地址解决的是“用哪条路把数据送到哪台机器”它看到的单位是主机。但真正收发数据的是主机里的进程比如浏览器、游戏客户端、远端登录服务。如果只有IP地址数据到了主机门口没人知道该进哪扇门这就很尴尬。运输层就是补上这段距离的。它把数据从一台主机的某个进程交付到另一台主机的某个进程这就是教材里常说的端到端通信。发送方进程把数据交给运输层运输层给它加上一层头部信息再交给网络层发出去接收方的网络层把包交给运输层运输层再根据头部里的端口号把数据分发给正确的进程。这个过程里有两个关键词复用和分用。一堆应用同时用运输层发数据叫复用运输层收到数据后按端口分发到不同应用叫分用。讲个容易理解的类比。IP像是快递要送到哪个小区运输层则是小区门口的收发室。快递到了门口收发室得看清楚包裹上写的哪一栋几号房再交给对应的人。包裹上那个门牌号在传输层里就是端口号。1.2 端口定位到进程的“门牌号”端口号是个16位整数范围从0到65535。这个范围听起来很大但拆开看分了三段0到1023是周知端口给HTTP80、HTTPS443、DNS53、SSH22、FTP21这类系统服务用1024到49151是注册端口给常见的应用程序用比如MySQL默认的3306、Redis的637949152到65535是动态端口客户端发起连接时用操作系统临时分配用完了就还回去。这里有一个特别重要的细节一个TCP连接或者一个UDP通信靠的是四元组唯一确定——源IP、源端口、目的IP、目的端口。很多初学者会忽略源端口觉得只要目标端口对就行。其实不对服务器收到请求后要回包回包的目的地址就是请求里的源IP加源端口。如果两个客户端恰好用了同一个源端口服务器也能靠IP区分但如果连IP也一样四元组就完全冲突了系统就会尝试复用或丢弃这是实际开发中偶发连接错乱的根源之一。讲到端口还得提醒一句不要把所有端口号都背下来但几个常用的一定要熟比如HTTP的80、HTTPS的443、DNS的53。笔试面试喜欢在这里挖坑问别人SSH端口是多少答不上来就很尴尬。1.3 两个协议家族TCP 和 UDP 的选型逻辑运输层把协议分成两个方向有点像快递行业的两种服务。UDP是平邮便宜、快、不保证不丢、不保证顺序TCP是挂号件有回执、有编号、丢件自动补发、按顺序签收但响应慢一点、开销大一点。选谁不选谁取决于应用到底想要什么。需要传输文件、网页、数据库操作数据一个字节都不能丢顺序也不能乱那就用TCP。反过来语音通话、视频会议、实时游戏操作、DNS查询这类场景更在意“别让我等太久”偶尔丢一帧、丢一个小包反而影响不大重传旧数据还不如直接发新数据于是UDP更合适。这么一想运输层看似只提供了两个协议但已经覆盖了绝大多数应用场景。接下来两章我会把UDP和TCP分开拆开讲特别是TCP那些容易让人头痛的机制争取每个点都说清楚它存在的理由。2. UDP8 字节首部撑起实时应用半边天2.1 UDP 报文段格式轻量到几乎没有存在感UDP首部固定8字节一共四个字段源端口2字节、目的端口2字节、长度2字节、检验和2字节。没有序号、没有确认号、没有各种标志位也没有窗口。这就是它“轻”的来源。这里的“长度”指的是UDP报文总长度也就是首部加数据最小是8字节最长是65535字节。实际传输时下面还有IP层的最大传输单元MTU限制所以一个UDP报文不能太大否则会被IP层分片分片后只要一片丢了整个数据报都作废这也是大UDP包在公网上容易丢的原因之一。检验和字段这里多说一句。UDP校验的是伪首部、UDP首部和数据。伪首部不是真的出现在报文里而是把IP层的源地址、目的地址和协议类型临时拼进去一起算目的是防止数据被路由器送错了主机。这个设计比较迂回但能有效发现地址错误的问题。计算时如果校验失败处理方式很干脆丢弃。UDP没有重传丢了就是丢了由上层应用自己判断要不要处理。2.2 UDP 无连接与面向报文这两个特性怎么理解教材上描述UDP写“无连接、不可靠、面向报文”每个词背后都有实际含义。无连接指的是发送方不需要先和接收方建立会话直接把数据丢出去就行。我往某个地址发UDP包对方可能在线可能不在可能过五分钟才收到发送方都不关心。这就像寄明信片写好地址往邮筒一扔不挂号、不签收、不返回回执。面向报文的意思是应用层给UDP多大的数据块UDP就照原样封装成一个报文发出去接收方还是按那个边界把数据都交给上层。UDP不拆分、不合并。这个特点在编码上反而是福气收发双方天然是对齐的不需要自己解析边界。代价是如果应用发的数据太大或太小都不好太大容易触发分片太小浪费带宽所以上层得自己控制好数据块大小。“不可靠”这事要换个角度看。很多人一听不可靠就觉得是缺点但在实时通信里可靠性反而拖后腿。视频会议中偶尔丢几帧画面会轻微卡顿但重传一帧旧画面没有任何意义游戏里玩家每秒要同步几十次位置连续重传旧坐标不如直接送最新坐标。所以宁可让UDP在网络上什么都不做把“要不要保证可靠”的决定权留给应用层。2.3 UDP 的典型场景从 DNS 到音视频到 QUIC最能体现UDP价值的经典场景是DNS。域名解析通常只是一个一问一答的交互用一个UDP包发回去查询收到回应就完事根本不需要提高三次握手。即便中间丢了一个包客户端等超时会自动重发一次查询应用层的重试成本非常低。实时音视频也是UDP的忠实用户。语音和画面本身带有时间敏感属性接受少量丢包不能接受延迟和抖动。这类系统通常会在应用层自己加序号、加FEC前向纠错或者做弱网丢包补偿但底层传输还是交给UDP。还有一类容易被忽略的是局域网里的服务发现和广播类协议比如DHCP动态分配IP、设备自动发现工具等。因为UDP支持广播和组播TCP做不到这些场景就天然选择UDP。以及近几年的新星QUIC协议它本质上是把TCP的可靠传输逻辑搬到UDP上再在应用层加密搞成了一套更适合移动网络的可靠传输方案。这说明UDP并不是老古董它作为一块地基被现代工程用出了新高度。2.4 用 UDP 做应用时要自己盯着四件事如果决定用UDP需要自己处理的事情立刻变多。第一丢包检测收发双方要协商一个心跳或编号机制一段时间没收到就判定丢失第二乱序整理发送顺序和到达顺序未必一致需要在应用层按序号缓存排序第三接收端流量控制UDP没有接收窗口应用如果忙不过来包会在缓冲区溢出然后被内核丢弃第四连接状态管理UDP本身无连接如果业务需要判断对端是否存活就要自己维护会话表定期发心跳。这不是为了劝退谁而是想说明UDP的“简单”只是协议简单使用它的人不简单。很多高并发实时系统的关键代码就是在这一层之上把可靠性、时序性一点点补出来。3. TCP 的可靠传输可靠不是一句空话3.1 TCP 报文段格式一次看懂冗长的首部TCP首部比UDP复杂得多最短也有20字节。字段多不是为了炫技每一个都是为了支撑可靠传输和连接管理这里按重要性逐个说。源端口和目的端口各占2字节与UDP一样。序号4字节用来给字节流编号。确认号4字节表示期望收到的下一个字节序号也意味着之前的数据都收到了。数据偏移4位指出段首部有多长用4字节为单位表示所以TCP首部最大60字节。紧接着6位保留字段目前基本不用。然后是6个标志位URG、ACK、PSH、RST、SYN、FIN。窗口2字节用来告诉对方自己的接收能力是流量控制的主战场。检验和2字节覆盖范围同样是伪首部加段内容。再往后是紧急指针只在URG置位时生效还有可变长的选项字段如MSS、SACK、时间戳、窗口扩大因子这些选项在性能调优时才会深入讨论。看着20字节的头很多初学者会想“为了传输几个字节的数据要带这么多头真浪费”。这账不能这么算。没有序号和确认号就无法处理丢包没有窗口就无法避免接收方崩溃没有标志位就无法建立和释放连接。这20字节是可靠传输的保险单。3.2 停止等待 ARQ 到滑动窗口重传策略的演进逻辑TCP可靠传输的基础是确认与重传。早期教材从最简单的情况讲起发送方发一个包等接收方回一个确认确认回来了再发下一个超时没等到就重传。这就是停止等待ARQ。它的优点是逻辑清楚缺点是效率极低网络延迟一大链路大部分时间都在等待。于是一个自然的改进是连续发送多个段再统一接收确认这就是滑动窗口机制。发送方可以一次把窗口内多个段发出去接收方每收到一个段就按序号组装并返回累计确认。所谓累计确认就是说“你的1到100字节我收到了请从101发”这意味着只要确认号是101即使中间的某个段迟到了也照样代表前面的都处理好了。丢包怎么办分两种情况。如果发送方超时没收到确认就把超时的段重传如果接收方收到了乱序的数据理论上可以立刻回一个重复确认告诉对方“我还在等第X个字节”。TCP连续收到三个重复确认时就会推断对方丢了包不等超时就重传这叫做快速重传。这个机制大大缩短了感知丢包的时间是TCP能适应长距离、高延迟链路的重要设计。超时重传的等待时间不是拍脑袋设的而是基于RTT动态估算出来的。发送方每收到一个确认就会刷新一下当前RTT的采样再用加权平均算出平滑RTT最后加上安全余量得到RTO。网络波动大时RTO跟着变大网络顺畅时RTO变小这样既避免频繁误判超时也不会等太久。看抓包时如果发现大量重复ACK和重传通常说明链路真在丢包而不是TCP参数有问题。3.3 流量控制接收缓冲区说了算可靠传输解决的是“数据能不能完整到达”的问题流量控制则解决“发送太快会不会把对方压垮”的问题。接收方的处理能力是有限的缓冲区也就那么大如果发送方拼命塞数据缓冲区满了之后新到的包只能被丢掉反而触发重传风暴。流量控制的办法是让接收方在每个回复的报文里带上自己的窗口大小字段。这个窗口叫接收窗口rwnd意思是“我还能收下这么多字节”。发送方的发送窗口被限制在rwnd范围内rwnd变成0就暂时停下来等到对方重新发一个窗口更新的段再继续。在实际开发里遇到“连接建立成功但数据进度卡住”的问题首先就该看一眼是不是发送窗口变成0了。用抓包工具过滤窗口字段如果看到大量零窗口通知和窗口更新交替出现基本就是接收方程序处理不过来缓冲区被占满。这时候问题往往不在网络而在应用代码的消费速度上。3.4 拥塞控制全局网络的大闸门流量控制是从单个接收方的角度做限制拥塞控制则是从整个网络链路的角度做限制。想象一条高速公路路上车多到一定量谁也别想开快还容易连环追尾。TCP的拥塞控制就是给每个发送方装一个“路况限速器”这个限速器叫拥塞窗口cwnd。TCP大致按四种算法运作。建立连接初期发送方不知道路况先从一个MSS开始每收到一个确认窗口就翻倍这叫慢开始。指数增长看着猛但起点低本质是试探性起步。当cwnd长到一个阈值ssthresh后担心把网络快速灌满就切换到拥塞避免让窗口每个往返周期只加一个MSS线性增长。一旦发生超时TCP认为路况很差把ssthresh降到当前窗口的一半cwnd变成1重新慢开始这叫超时后的处理。如果只是收到三个重复ACK路况可能没那么糟就走快重传加快恢复ssthresh降到一半cwnd也降到一半然后直接线性增长不再从头开始。这四个算法合起来构成了TCP一个非常重要的哲学“先小心翼翼地试探再稳步加码一旦路况恶化就快速降速。” 理解这个逻辑后再看服务器性能参数里的TCP重传率、RTT曲线会变得清晰很多。可以说拥塞控制是TCP最像一门工程艺术的部分。4. 三次握手和四次挥手连接从建立到释放的完整生命周期4.1 三次握手为什么偏偏是三次TCP是面向连接的发送数据前要先建立连接。建立过程大家背得滚瓜烂熟客户端发SYN服务器回SYNACK客户端再回ACK。但我更想说的是这个流程背后有个关键的防护意义避免历史重复SYN造成的错误连接。举个例子。客户端因为网络原因先发了一个旧SYN又发了一个新SYN两个请求到达服务器的顺序如果反了服务器先后收到新和旧两个请求。如果两次握手就建立连接服务器对旧SYN也会立刻分配资源并等待数据但客户端压根没打算用旧连接于是服务器傻等白占一堆资源。三次握手的情况下客户端收到服务器的SYNACK后会根据里面的确认号判断这是不是自己想要的连接不是就发RST强制终止是才回ACK正式建立。这就是“双方确认对方状态”的实际含义。还有一个角度也值得理解。三次握手完成了两件事的验证客户端确认自己能发、服务器能收服务器确认自己能发、客户端能收。只有双方都确认了通信能力连接才敢开工。这叫“二次握手知半、三次握手知全”。4.2 SYN 洪泛与半连接队列的防护思路握手过程里服务器在收到SYN、还没收到最终ACK时会把这个连接放到半连接队列里等第三次握手完成后移到全连接队列。这个队列是有限度的如果大量伪造的SYN请求打过来队列被填满正常用户就握不上手了这就是SYN洪泛。工程上对付SYN洪泛有几种思路。一是缩短半连接超时时间让无用连接尽快清出去二是用SYN Cookie技术服务器不立刻分配资源而是把连接信息加密成一个Cookie塞在SYNACK里等客户端三次握手时带回来再验证后建立连接。SYN Cookie本质上是把资源分配从第一次握手推迟到了第三次握手让服务器能扛住扫描式攻击。我做安全相关工作后明显感受到懂一点半连接队列和SYN Cookie的细节排查“服务端口不通但进程在运行”这类问题会快很多。4.3 四次挥手优雅离场与 TIME_WAIT 的讲究建立连接需要三次断开却要四次原因在于TCP支持半关闭。主动方要断开先发一个FIN表示“我的数据发完了”被动方收到FIN后回ACK表示“知道了”但被动方可能还有数据没发完所以此时连接只会关闭主动方到被动方的方向。等被动方把数据也发完了才发FIN主动方再回ACK。这就是为什么有两次独立的说再见。四次挥手里最容易被人忽略的是主动关闭方的TIME_WAIT状态。主动方发出最后一个ACK后并不会立即释放连接而是要等两个MSL最大报文段寿命时间。这个设计出于两个考虑第一万一最后一个ACK丢失被动方会重发FIN主动方在TIME_WAIT里还能回应否则被动方会一直等不来陷入无限重发第二让连接里可能残留的旧报文在网络中自然消亡避免这些老数据被误当成新连接的数据。TIME_WAIT这个状态在实际运维中常被讨论。大量TIME_WAIT出现在高并发短连接的服务器上有时会占满临时端口影响新连接建立。针对短连接场景有人会调整参数允许TIME_WAIT复用。做这个操作前要先想清楚服务是否受影响因为复用TIME_WAIT有可能让旧数据混进新连接虽然概率很低但需要评估业务容忍度。多数情况下该等还是得等那不叫性能问题叫协议正确性。5. 状态查看与问题排查把运输层知识落到真实网络5.1 用 netstat 和 ss 看懂连接状态学了一堆运输层概念最终要能落到命令行。最常用的查看连接工具是netstat新版系统推荐用ss它更快。查看当前所有TCP连接可以执行netstat -ant ss -tnp输出里每行代表一条连接重点是State列。常见状态有LISTEN、ESTABLISHED、SYN_SENT、SYN_RECV、FIN_WAIT1、FIN_WAIT2、CLOSE_WAIT、LAST_ACK、TIME_WAIT。看到某一种状态大批量堆积往往就意味着有状况。比如一堆SYN_RECV说明服务器发完SYNACK后在等对方的最后一个ACK迟迟等不到一般有两种原因服务器回包被防火墙丢掉或者遭遇了SYN洪泛。再比如一堆CLOSE_WAIT说明对端已经发FIN来关闭连接但本端应用一直没有调用close基本可以断定是代码状态泄漏连接对象没被正确关闭。这里分享一个我查问题的小习惯先用ss数状态再抓包看具体报文。从状态分布能猜到八九成方向再从报文里确认哪些包重传、哪些包超时、窗口变化是否异常。状态是表象报文是证据两个结合起来才能下结论。5.2 用 tcpdump 亲手看一次握手和挥手抓包验证运输层的理解是最直观的学习方式。抓包工具选tcpdump在Linux环境下一行命令就能抓到你想要的交互流程tcpdump -i any host 192.0.2.10 and port 443抓包时盯几个关键点。第一次握手是客户端发SYN标志位显示S序号随便一个初始值第二次服务器返回S和.的组合也就是SYNACK确认号正好是客户端初始序号加一第三次客户端回纯ACK确认号是服务器初始序号加一。整个握手在抓包里就是依次递增的三种包。挥手时更清晰先是两个方向各来一个FIN然后是确认FIN的ACK。如果你能看到完整过程再把RST、重传、重复ACK补充进来基本就可以把状态机的每个跳转和抓包对应起来。看抓包还有一个好处就是能把抽象的参数变成具体数字。比如能看到初始窗口大小、MSS值、时间戳选项这些东西在教材里是干巴巴的字段在抓包里有血有肉对比几次就能牢牢记住。5.3 线上连接故障速查表把常见问题和排查方向整理成一个速查表遇到问题先查表再决定深入的方向。现象可能原因排查方向客户端连接超时半连接队列满、服务器防火墙丢弃SYN抓包看SYN是否有响应数SYN_RECV数量大量SYN_RECV遭遇SYN洪泛或回包被丢做SYN Cookie防护检查防火墙策略大量TIME_WAIT默认行为大多正常短连接业务可评估端口复用先看端口是否耗尽大量CLOSE_WAIT应用未正确关闭连接查代码看连接对象释放逻辑连接被RST端口未监听、应用主动拒绝、冲突连接抓包看RST方向补充检查本端监听端口数据窗口为0接收缓冲区满消费速度慢检查接收端进程负载、应用消费代码频繁重传网络丢包严重或网卡/驱动问题看RTT、重传包数量用ping和抓包定位丢包点这张表不能代替深入分析但能帮你在茫茫指标里找到入口。5.4 一个真实的排查片段说个我自己遇到过的场景。某次模拟项目里一个服务端应用偶尔出现外部请求无响应进程在跑端口也在监听但就是请求卡住。第一反应是看连接状态发现一堆CLOSE_WAIT堆积正常情况下这个值应该是零。顺着CLOSE_WAIT往上找代码里确实有个分支在异常路径上没有调用close连接对象被用户态持有内核等不到FIN就一直吊着。修完那个资源释放问题后CLOSE_WAIT降为零服务恢复正常。这件事给我的印象很深。TCP状态不是网络设备的内部资料它是应用程序与内核协作留下的脚印。学会了数状态、抓包、看清字段之间的关系运输层这些知识就不再只停留在考卷上而是真正能帮你摆平线上问题的手艺。最后说一点个人体会学运输层不要急着把所有细节一把抓。先把三次握手、四次挥手、序号和确认号的关系搞通透再逐步延伸到滑动窗口和拥塞控制。每一块知识都试着用抓包去验证一次比自己死记一倍参数有用得多。等你哪天对着ss输出里的状态一眼就有判断再回头复习这一章会发现自己已经站在另一层高度了。