十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

TCP与UDP深度解析:核心机制与工程实战排查

TCP与UDP深度解析:核心机制与工程实战排查 1. 先把传输层这层“皮”剥开TCP与UDP到底在解决什么问题做网络开发这些年我遇到最多的困惑不是应用层怎么调接口而是很多人把TCP、UDP挂在嘴边却说不清传输层在整条网络链路里到底干了一件什么事。传输层是TCP/IP协议栈里承上启下的那一层往上承接HTTP、FTP、DNS这些应用协议往下把数据丢给IP层去路由转发。没有这一层你的浏览器不知道页面数据有没有收全你的视频通话也不知道声音画面该按什么节奏播放。这篇文章想把TCP和UDP的核心原理讲透并且把那些平时文档里不会写的实战细节一并整理出来适合刚接触网络编程的学生、做嵌入式通信的工程师以及后端开发中经常要排查连接问题的同学。先说结论TCP和UDP解决的是同一个问题——如何让数据从一台主机的某个程序准确送到另一台主机的某个程序。但它们的实现哲学完全不同。TCP像打电话要拨号、等待对方接听、确认双方都在线然后一句一句说说完还要确认对方听清楚了才挂断UDP像发快递单把包裹贴上地址扔进快递柜寄出去就不管了对方收没收到快递有没有破损全凭运气和上层应用自行处理。这两种思路没有绝对的高下之分只看你的业务能不能接受丢包或延迟。我在实际项目中见过不少选型错误有人为了图省事全部用UDP结果应用层得自己补一堆重传和排序逻辑代码量比直接用TCP还大也有人不管什么场景都TCP实时音视频卡得没法看。所以开篇先把定位搞清楚后面每个机制的细节才有着落。1.1 传输层的角色给数据包装上“收发地址”如果你抓过包会看到IP层负责的是从一台机器到另一台机器的路由但一台机器上同时跑着浏览器、游戏、微信IP层把数据包送到这台机器之后该交给哪个程序这就是端口号的作用。传输层的核心工作之一就是通过源端口和目的端口把数据精准投递给对应的进程。TCP和UDP头部都包含源端口、目的端口这两个字段各占16位取值范围0到65535。其中0到1023是知名端口比如HTTP用80、HTTPS用443、DNS用53这些端口通常需要管理员权限才能绑定1024到49151是注册端口像MySQL的3306、Redis的6379都在这个区间49152到65535是动态端口客户端发起连接时操作系统会从这段里自动挑一个作为源端口。理解端口这个概念很多问题就迎刃而解了。比如你在CentOS上部署服务明明进程起来了、防火墙也关了但外部就是连不上十有八九是端口没监听对地方。用ss -lntp看一眼监听地址是127.0.0.1还是0.0.0.0差别非常大。前者只接受本机回环请求后者才对外开放。这个坑我踩过不止一次所以先放在前面提醒。1.2 TCP和UDP的分水岭一个打电话一个发快递选协议之前先搞明白“面向连接”和“无连接”的区别。TCP面向连接通信前必须通过三次握手建立会话通信过程中有确认、重传、排序、流量控制结束后还要四次挥手释放连接。好处是可靠坏处是开销大、有延迟。UDP无连接发送前不需要建立会话直接把数据封装成数据报扔出去。好处是快、协议头小、支持广播和组播坏处是不保证送达、不保证顺序、不保证不重复。我见过一个很形象的类比TCP是顺丰快递有回执、有保险、丢了重发UDP是平邮寄明信片寄出去能不能到听天由命。不过这个类比有个不准确的地方UDP并不是“不努力送达”IP层本身会尽最大努力转发只是传输层不负责纠错而已。所以在局域网这种网络质量很好的环境里UDP完全够用反而是跨公网传输时丢包率上来了UDP的短板才暴露出来。判断该用哪个协议我总结了一个简单的三步法第一数据能不能容忍丢失不能容忍就TCP第二实时性要求是不是极高高到宁可丢一帧也不愿意等重传那就考虑UDP第三是否需要一对多通信UDP天然支持组播和广播TCP要实现类似效果得多花不少功夫。2. TCP核心机制拆解三次握手、四次挥手与可靠性保障这一节是重头戏面试考、工作用、排查问题绕不开。我尽量把每个机制背后的“为什么”讲清楚而不是让你死记硬背流程。2.1 三次握手为什么必须是三次而不是两次或四次三次握手的过程大家应该都背得出来客户端发SYN服务端回SYNACK客户端再回ACK然后连接建立。但为什么必须是三次核心原因是为了防止“历史重复连接请求”干扰正常的通信。考虑一种情况客户端发送了一个SYN因为网络拥塞迟迟没到达服务端客户端超时后重发了一个新的SYN。如果第二次SYN先被服务端接收并建立连接通信结束后双方关闭连接这时候第一个迟到的SYN才到达服务端。如果只有两次握手服务端收到这个迟到的SYN会误以为客户端又要建立新连接于是分配资源、返回SYNACK但客户端根本不会理会这个确认因为它压根没想再发连接结果服务端的资源就被白白占用了。三次握手之所以能解决这个问题关键在于客户端收到服务端的SYNACK后有能力判断这个确认对应的到底是哪一次SYN。如果客户端发现自己没有发起新的连接请求就直接发RST把这个历史连接重置掉。而服务端只有在收到客户端的ACK或RST之后才能确认这个连接是有效还是无效。还有一个非常实际的原因TCP需要初始化双方序列号。序列号是TCP可靠传输的基石发送方用序列号标记每一字节数据的编号接收方用确认号告诉发送方“我期望收到哪个序列号”。三次握手的过程正好让双方各自通告自己的初始序列号ISN并确认对方的ISN已经收到。两次握手只能让一方知道另一方的初始序列号对方却无法确认你收到了后面可靠传输就无从谈起。实际抓包时你会发现TCP连接的建立速度比想象中快局域网内通常在毫秒级完成。但如果你在弱网环境看到大量SYN重传就要检查是不是防火墙把SYN包丢了或者服务端的半连接队列backlog满了。2.2 四次挥手的细节与TIME_WAIT状态四次挥手比三次握手复杂因为TCP连接是双向的每一方向都要单独关闭。客户端发送FIN表示“我没有数据要发了”服务端回ACK表示“我收到了”但服务端可能还有数据没发完所以等服务端也发完数据后再发一个FIN客户端再回ACK连接才算彻底关闭。这里面有一个状态容易让人困惑TIME_WAIT。主动关闭连接的一方在发出最后的ACK之后会进入TIME_WAIT状态默认等待2个MSLMaximum Segment Lifetime报文最长存活时间在Linux上通常为60秒。很多人不理解连接都关了为什么还要等这么久原因有二。第一防止最后一个ACK丢失。如果服务端没收到这个ACK它会重发FIN这时候客户端如果已经彻底关闭就收不到这个重发的FIN服务端会一直处于LAST_ACK状态。等2MSL是给可能丢失的ACK一个重发的机会。第二让本连接产生的所有报文在网络中自然消失。因为网络中可能存在延迟到达的重复数据包如果客户端立刻关闭并用同一个四元组源IP、源端口、目的IP、目的端口建立新连接旧连接的残留报文可能会污染新连接的数据。TIME_WAIT太多是高性能服务端常见的困扰。用netstat -ant | grep TIME_WAIT | wc -l看一眼如果数量上万可能说明短连接过于频繁。优化思路有很多比如开启net.ipv4.tcp_tw_reuse让内核在安全条件下复用TIME_WAIT连接或者调整net.ipv4.tcp_fin_timeout缩短等待时间但要注意这些参数不是万能的改了之后得压测验证否则可能引发连接异常。2.3 可靠性保障滑动窗口、确认重传与拥塞控制TCP的可靠性不是一个单一机制完成的而是多个机制协作。最容易理解的是确认与重传发送方发出数据后等待接收方回ACK如果超过重传超时时间还没收到ACK就重新发送这段数据。这个过程看似简单但超时时间怎么定是个技术活。如果定得太小网络稍微慢一点就疯狂重传加剧拥塞如果定得太大丢包后要等很久才能发现用户体验很差。所以TCP引入了自适应重传算法根据历史RTTRound-Trip Time往返时间动态计算超时时间RTO。早期用简单的加权平均后来演进到更精细的算法。内核里都可以通过sysctl查看和调整相关参数比如net.ipv4.tcp_rto_min、net.ipv4.tcp_rto_max。滑动窗口解决的是流量控制问题。接收方在ACK里带上自己还能接收的字节数这个数值就叫窗口大小。发送方不能一口气把数据全发出去必须保证“已发送但未确认的字节数”不超过对方的接收窗口。这个机制避免了发送方太快把接收方缓冲区塞满。如果你看Wireshark的Timeline能看到窗口大小的变化曲线当接收方处理不过来时窗口会逐渐缩小甚至窗口为0这时候发送方就得停下来等待窗口更新。拥塞控制解决的是“不要压垮网络”的问题。经典的四个阶段大家应该都听过慢启动、拥塞避免、快速重传、快速恢复。慢启动时拥塞窗口从初始值指数增长到慢启动阈值后转为线性增长出现丢包时认为网络拥塞阈值减半窗口回退再重新开始。这些机制保证了TCP在网络不稳定时能自己适应不至于因为发送太快导致整个网络雪崩。不过要泼一盆冷水在特定场景下特别是跨运营商网络或者弱网环境TCP的拥塞控制算法可能过于保守导致吞吐量上不去。这也是为什么很多新协议比如QUIC基于UDP会尝试在应用层自带更激进的拥塞控制算法。2.4 粘包/拆包问题TCP“流”特性带来的经典坑这个坑几乎每个写过TCP通信的人都会踩。TCP是流协议它只管把字节流从一个端传到另一个端并不关心你发送的是不是完整的一条“消息”。发送方调一次send()写进去的数据接收方可能要调好几次recv()才能读完反过来发送方连续调好几次send()写入的内容接收方可能一次recv()就全读出来了。这种现象就叫粘包或拆包。很多新手第一反应是“那我在send和recv之间加个sleep不就好了”这是典型的治标不治本。sleep改了时机但TCP的分段行为由内核和网络状况决定不能用业务层的延时去猜。正确做法是给每个消息定义一个边界常见方案有三种。最基础的是定长协议每条消息固定N字节不够就补零收满N字节就解析一条。实现简单但空间浪费大不适合长度变化大的业务。第二种是分隔符协议消息末尾加特定字符如\r\n或\0接收方读到分隔符就认为一条消息结束了。HTTP的头部就是这种思路。第三种最常用也是我推荐在生产环境用的消息头消息体结构头部固定几个字节前4字节存总长度或者消息体长度接收方先读够头部解出长度再按长度读取消息体。用C#写TCP客户端时尤其要注意这个问题。NetworkStream.Read()并不保证一次能读到你期望的字节数必须循环读直到累计字节数满足条件。我见过有人在Read()外面套一层while不做长度判断结果数据一多就错位解析出来的全是乱码。3. UDP实战解析轻量、无连接但绝非“低级”很多教材把UDP描述成“不可靠的传输协议”导致新手对它有一种偏见觉得用UDP就是低级、不专业。实际上UDP在实时音视频、在线游戏、物联网、组播通信这些领域是绝对的主力。理解UDP的设计哲学和应用边界比单纯会背UDP和TCP的区别有价值得多。3.1 UDP头部与无连接特性UDP头部只有8个字节源端口2字节、目的端口2字节、长度2字节、校验和2字节。对比TCP最少20字节的头部UDP省了不少开销。这里的“长度”字段很关键它指UDP数据报的总长度包括头部和数据。因为IP层会把UDP数据报完整地递给上层所以接收方不需要像TCP那样处理粘包每个recvfrom()调用拿到的就是发送方一次sendto()发出去的一整条数据报。UDP的无连接特性体现在API使用上。TCP通信要先connect()建立连接而UDP的sendto()每次都要指定目标地址接收端则用recvfrom()带出源地址信息。有意思的是UDP其实也支持connect()但这里的connect和TCP的connect完全不同它不会真正建立连接只是在内核里把目标地址固定下来后续可以简化为send()/recv()同时内核可以过滤掉非目标地址发来的包减少系统开销。还有一个经常被忽视的地方UDP校验和。IPv4下UDP校验和是可选的如果源端关闭了校验和接收端可能无法检测数据在传输过程中是否被破坏。IPv6则强制要求必须计算校验和。对于关键业务建议在应用层再加一层校验比如每个数据报带一个CRC或者消息摘要防止数据静默损坏。3.2 UDP适合哪些场景实时音视频、组播、IoT先讲实时音视频。WebRTC底层用的就是UDP因为视频通话这种场景偶尔丢掉一帧画面比等一帧数据重传导致后续所有帧都卡住要好得多。用户感知上短暂的马赛克远没有长时间的延迟卡顿那么让人崩溃。语音同理中断100毫秒和延迟300毫秒前者还能忍后者基本就是灾难。组播是UDP的一个独门绝技。TCP是点对点的连接要实现一对多分发必须由发送方维护N个连接资源消耗很大。UDP支持组播组发送方只要往组播地址发一份数据网络中的路由器会负责复制转发给组内所有成员。这在局域网视频分发、股票行情推送、设备发现等场景非常实用。不过要注意组播在跨网段时需要路由器支持IGMP协议很多云服务器默认不支持所以公网组播基本不可行只能局限在可控的局域网内。IoT物联网场景选UDP也很多尤其是功耗敏感的传感器设备。比如低功耗设备通常用CoAP协议CoAP就是基于UDP的请求/响应协议一次通信只交换两个报文关闭连接免了握手的开销设备可以快速进入休眠省电。我自己在嵌入式设备上跑过类似方案用ESP8266通过UDP定时上报传感器数据实测整体功耗比用TCP低一个量级。3.3 调试上手两台电脑用网络调试助手跑通UDP通信很多初学者在开发环境里不知道怎么验证自己的UDP代码其实最简单的方法是用网络调试助手这类工具快速完成两端的联调。准备两台电脑假设A和B在同一个局域网。先在A上打开网络调试助手协议类型选UDP本地IP填A的IP地址比如192.168.1.100本地端口填A要监听的端口比如8000点击“连接”按钮进入监听状态。然后在B上同样打开网络调试助手协议选UDP本地端口空着或者填一个B自己的端口然后在“目标IP”和“目标端口”里填A的IP和8000。B发送一条消息A那边应该立刻就能收到。验证完单向通信再验证双向。A回复消息时目标IP填B的IP端口填B的端口B就能收到。这个过程看着简单但它能帮你确认几件事两台机器网络是否互通防火墙有没有拦截UDP流量你写的UDP绑定逻辑是否正确。如果B发消息A收不到先ping一下A的IP确认网络通不通如果ping通但UDP不通重点查防火墙很多系统默认防火墙会拦截未经允许的UDP端口。UDP有个特点让调试很痛苦包丢了没有任何提示。所以调试时一定要做双向验证并且最好在抓包工具里看数据是否真的发到了线上。我在Windows上用网络调试助手在Linux上更多用nc -u命令一条命令就能起一个UDP收发端点比图形界面还方便。3.4 UDP的“应用层可靠性”补课既然UDP不保证可靠那业务层就得自己想办法。常见做法是应用层实现ACK机制每条消息带一个自增序号接收方收到后回一个ACK发送方超时未收到ACK就重发接收方根据序号处理乱序和去重。这套机制基本就是TCP可靠传输的简化版但是放到应用层之后优势在于你可以完全控制重传策略比如游戏里只重传关键状态不重传高频位置更新。KCP是这方面非常典型的开源项目一个基于UDP的可靠传输库实现了快速重传、选择性确认等机制比TCP在高延迟、高丢包网络上表现更好。很多游戏公司的通信层都在用KCP。如果不想自己造轮子直接集成这类成熟库是性价比很高的选择。不过要提醒一点应用层做可靠性复杂度并不低。序号管理、超时重传、滑动窗口、拥塞控制这些你在TCP里躲掉的坑全部要在应用层重踩一遍。所以做技术选型时先问自己一个问题你的业务对延迟的敏感度真的高到无法接受TCP的开销吗如果不是老老实实用TCP省下来的时间可以做更多有价值的事。4. 工程实践中的协议栈问题抓包、打流、端口与防火墙这一节全是干活时用得上的东西。理论归理论上了生产环境你会遇到各种莫名其妙的现象这里把最常见的问题整理出来。4.1 Wireshark筛选UDP时间间隔与TCP状态分析Wireshark是排查网络问题最趁手的工具但很多人只会打开看一眼不知道怎么高效筛选。先说一个常见需求如何筛选出UDP前后两包的时间间隔。Wireshark本身没有一个直接的“时间间隔”筛选字段但可以用显示过滤器的计算功能。在过滤栏输入udp frame.time_delta是不合法的因为frame.time_delta不是过滤字段只能在列里显示。正确做法是先把frame.time_delta加为一列右键任意包选择Column Preferences添加新列Field填frame.time_delta。这样就能看到每个包和上一个包之间的时间差。如果要筛选出时间间隔大于某值比如大于1秒的包Wireshark在新版本里支持了一些计算字段但不是所有版本都支持直接对列做比较筛选。更通用的一种做法是导出CSV用脚本分析每包的时间戳差。我写过一个简单的思路tshark -r capture.pcap -Y udp -T fields -e frame.time_epoch -e udp.srcport -e udp.dstport导出时间和端口再用awk或Python算前后差值。对于需要精确定位的抖动问题这个办法比肉眼盯着看高效得多。TCP状态分析是另一个高频需求。用tcp.flags.syn1能筛出所有SYN包tcp.flags.fin1筛出FIN包。看三次握手是否成功关注是否有tcp.analysis.retransmission标记这个标记说明有包发生了重传如果大量出现基本可以断定网络有丢包或者延时抖动。tcp.analysis.zero_window则提示接收方窗口为零说明对方处理不过来是性能瓶颈的信号之一。抓包有个关键技巧一定在两端都抓。只在一侧抓看到的现象容易误导你判断问题在哪一段链路。比如客户端抓包看到SYN发出去了但没收到回应可能是服务端根本没收到也可能是服务端回了但路由丢了还可能是服务端回了但被客户端防火墙挡了。两端对比抓包一眼就能看出数据是在哪一段断的。4.2 iPerf3跑TCP/UDP性能测试的正确姿势iperf3是打流测带宽的第一选择好用但容易用错。最常见的错误是不加参数直接跑测出来的数据只能证明TCP能工作无法反映真实性能。TCP测速的标准姿势服务端启动iperf3 -s客户端执行iperf3 -c 服务端IP -t 60 -P 4。-t指定持续时间建议至少30秒太短受慢启动影响测不准-P 4用4个并发流模拟真实应用的并发连接。实测下来单流和多流差距可能很大尤其在跨公网场景单流吞吐量往往不如多流这和TCP拥塞控制的公平性问题有关。UDP打流的参数更讲究。常见误区是直接跑默认参数因为iperf3默认UDP带宽只有1Mbps你看到的吞吐量其实是被限速了。正确的做法是用-b显式指定带宽比如iperf3 -c 服务端IP -u -b 100M -t 60意思是压100Mbps的UDP流。测完终端会报告实际接收带宽和丢包率。这里有个关键点发送端报告的吞吐量和接收端报告的往往不一样接收端显示的才是真正到达对端的速率两个差值就是丢弃的流量。还有几个参数值得一提。-R做反向测试测下行带宽--pkt-size调整UDP包大小默认1470字节可以避开IP分片如果你想测大包对小包的性能差异改成9000字节的巨型帧试试局域网内差距非常明显。-i 1让iperf3每秒打一个报告用来观察带宽的波动曲线对排查间歇性拥塞很有用。4.3 端口冲突、防火墙配置与常见连接错误端口冲突是最常见的一个坑错误提示通常是bind: only one usage of each socket address。这个错误的中文意思是某个socket地址只能被绑定一次你尝试绑定的IP加端口组合已经被占用了。我用Windows开发时遇到过代码里端口写死8080跑起来直接崩用netstat -ano | findstr 8080查一下果然有别的进程在监听。解决办法很简单换端口或者把那个进程处理掉。但如果代码里端口是动态分配的冲突概率会小很多这也是为什么服务端程序经常支持通过配置文件指定端口而不是写死。Linux上查看端口占用用ss -lntp比netstat信息更全直接显示PID和进程名。如果显示的进程名是-说明当前用户没权限查看加sudo即可。防火墙配置是CentOS用户绕不开的坎。CentOS 7以后用的是firewalld不再推荐用iptables命令行直接管理。开放一个端口的命令是firewall-cmd --permanent --add-port8080/tcp然后firewall-cmd --reload生效。注意--permanent决定了规则是否写入永久配置不加的话重启防火墙后就丢了。查端口是否放行用firewall-cmd --query-port8080/tcp。还有个我见过很多次的错误服务已经监听在0.0.0.0了本机能连外部连不上最后发现是云服务商的安全组没放行端口。云服务器和本地环境不一样云平台的安全组规则是独立于系统防火墙的两边都得配好才行。排查网络问题时先看云控制台的安全组再看系统防火墙别一上来就怀疑程序代码。4.4 Socket编程要点与C# TCP客户端实战写TCP/UDP程序时Socket API的细节决定了程序能不能扛住生产环境。先讲TCP服务端的标准骨架socket()创建套接字bind()绑定地址和端口listen()开始监听然后进入accept()循环。这里有个设计要点accept()返回的客户端连接要交给独立线程或异步任务处理否则第一个客户端占住循环后面来的客户端全部排队等待。我在C#里一般用TcpListener加异步AcceptTcpClientAsync()配合Task处理每次接入的连接代码干净且不容易阻塞。C#的TcpClient封装了底层Socket对很多开发者来说是主力工具。一个基础的TCP客户端类核心步骤是构造时传入服务端IP和端口Connect()连接获取NetworkStream然后循环读写。注意读数据时一定要用循环不能指望一次Read()拿到完整消息要声明一个缓冲区循环读到0说明对端关闭了连接读到空说明暂时没有数据但连接还活着。TCP的另一个大坑是接收缓冲区大小。默认缓冲区在Windows上通常是8192字节在Linux上可以更大。如果你要传输大文件缓冲区太小会导致频繁的Read调用性能很差。解决方案是TcpClient.ReceiveBufferSize调大或者干脆用自定义协议把数据分块传输每块控制在合理大小。UDP编程相对简单UdpClient直接绑定端口Receive()返回一个UdpClient注意它返回的是数据报的内容没有流的概念。Send()发送时指定目标IP和端口即可。UDP的Receive和Send天然是一对一的不会出现TCP那种半包问题但也正因为如此UDP应用必须自己处理消息完整性的校验别指望内核帮你拼包。5. 常见故障排查速查表附独家避坑心得做了这么多年网络相关的开发我把高频问题归类整理成了一张速查表你在实际工作中遇到类似现象直接对照查可以省不少排查时间。5.1 高频问题一表搞定现象可能原因解决思路本机连服务正常外部连不上服务监听在127.0.0.1而非0.0.0.0修改监听地址为0.0.0.0重启服务本机连服务正常外部连不上云服务器云安全组未放行端口登录云控制台添加入方向规则开放对应端口端口冲突bind报错目标端口已被其他进程占用netstat -ano或ss -lntp查占用进程换端口或杀进程TCP连接超时抓包无SYN-ACK回包防火墙丢弃了SYN包或服务端对端不可达检查防火墙规则确认目标IP端口可达性大量TIME_WAIT短连接频繁建立关闭开启tcp_tw_reuse或改用长连接复用传输速度上不去抓包有重传网络丢包率高或拥塞窗口受限检查链路丢包率尝试多流并发或调整TCP参数UDP收不到数据防火墙拦截UDP端口或源端没绑定目标地址先ping通再用nc -u双向测试检查防火墙Wireshark看不到预期的包抓包过滤条件写错或接口选错确认抓包接口为实际通信网卡简化过滤条件iperf3 UDP只有1Mbps忘了用-b指定带宽加-b 100M等参数重新打流客户端connect报10061Windows目标端口没有进程在监听确认服务进程已启动且监听地址和端口无误服务端Read一直收不到数据的末尾TCP流没有消息边界对端没关连接应用层定义协议长度或使用消息终止符这张表不可能覆盖所有问题但大部分我遇到的排查场景都在这张表的框架内。如果你遇到的是表外问题我的建议是先抓包再定位不要靠猜。抓包能看到数据包在哪一段丢失和延迟比任何日志都直接。5.2 最后再分享几条实战心得第一写网络程序时日志一定要带上四元组信息源IP、源端口、目的IP、目的端口。出了问题和别人协作排查时没有四元组日志你连问题发生在哪条连接上都说不清楚。这个习惯帮我节省了大量排查时间。第二测试网络通信时不要在同一台机器上起客户端和服务端测试因为回环网络和真实网络的特性差异很大。我第一次写TCP长连接程序时本地测试一切正常部署到跨机房环境就频繁断连折腾了半天才发现是网络中间设备空闲超时把空闲连接掐了。重要提示跨网络的长连接一定要有心跳机制否则中间节点静默丢弃连接你完全察觉不到。第三UDP调试时如果一个包丢了让你怀疑人生先确认对方有没有在监听正确的端口。UDP的ICMP端口不可达消息往往会被防火墙吞掉导致发送方完全无感知。用Wireshark抓包时如果看到ICMP Destination Unreachable就是端口没监听。第四不要轻易修改TCP内核参数。Linux默认的TCP栈经过大量调优适合绝大多数场景。我见过有人把tcp_max_syn_backlog调得很大结果SYN洪水攻击时直接把服务打垮也见过盲目开启tcp_tw_reuse导致连接复用时序号错乱的问题。修改内核参数前一定要理解每个参数的机制和风险最好在测试环境验证。第五多看协议本身的RFC和内核实现少看二手博客。TCP/IP详解卷一是经典但比较老了新一点的可以看《TCP/IP Illustrated Volume 1》的第三版结合现代网络环境更新了很多内容。UDP和TCP的设计哲学吃透一遍能少走很多弯路。如果你正在做一个网络相关的新项目我建议搭建环境的时候就把抓包工具、打流工具和端口排查命令都提前准备好。网络问题排查的黄金法则就一句话先确认数据包在哪一段、哪个方向丢了再问为什么丢。抓包前别猜抓包后别急逐跳分析问题总能查得出来。
返回列表