背景
最近有个业务场景需要服务端(简称S)与客户端(简称C)设计一套基于UDP的通信协议--要求尽可能快的前提下可容忍一定丢包率,得以比较深入地学习和了解UDP通信和实践,在开发调试期间先后碰到了C端UDP发包无响应、响应Host Unreachable、响应Port Unreachable、再次C端UDP发包无响应这四种错误情况,不同于以往连接调试成功后万事大吉不再细究,这次有了好奇心想刨根问底的弄清楚造成不同错误的原因与错误通知的原理,并最终进一步了解了ICMP这个熟悉又陌生的协议。
错误问题与原因分析
为了便于更清晰、方便的阐明问题,以下对问题的顺序和出现场景进行了艺术加工--和实际发生的情况并不一致,毕竟实际问题并不会讲道理的一个一个顺序给你出现,而是经常多个问题混在在一起形成所谓的bug渐欲迷人眼==!
C端UDP发包无响应
sudo hping -2 -k -s 3000 -p 9999 test.demoabc.com -d 2 # hping参数含义:-2表示UDP模式, -s表示源端口固定3000, -p表示目的端口9999, test.demoabc.com为目的主机, -d 2表示数据包payload为2字节 HPING test.demoabc.com (en0 119.x.x.100): udp mode set, 28 headers + 2 data bytes ^C --- test.demoabc.com hping statistic --- 11 packets tramitted, 0 packets received, 100% packet loss round-trip min/avg/max = 0.0/0.0/0.0 ms
如上,使用hping向test.demoabc.com:9999 发送了11个UDP包,但是没有得到任何回应,S上的监听进程也没有接收到这11个UDP包中的任意一个,如果说UDP本身不可靠会导致可能丢包的话,在网络链路质量正常的情况下这11个包理论上是不可能100%丢包的。略一思考很快想到应该是由于防火墙没有开放UDP端口,于是防火墙既不会将UDP包转给后面正在监听9999端口的S进程,也不会给C端回包,而是直接丢弃处理。
解决方案:防火墙开放UDP对应端口即可。
Host Unreachable
防火墙放开端口后,自测联调C、S的发包、回包已经调通,于是交付客户端,结果客户端反馈UDP发包有问题,并且ping 目标host会报Host Unreachable,这就奇怪了,自测已经调通了,监听进程已经在运行且能够收到使用hping命令发包的UDP包了,客户端怎么就有问题呢?
仔细一看:嗯,C端host写错了--之前还没有配置测试域名的时候,直接给了C端一个公网ip想快速测试,结果由于种种原因最终实际使用的是另外一台服务器,旧IP对应的服务器回收了,所以客户端ping会报Host Unreachable。
ping命令大概是这么个效果:
ping 119.x.x.90 PING 119.x.x.90 (119.x.x.90) 56(84) bytes of data. From 192.168.0.105 icmp_seq=1 Destination Host Unreachable From 192.168.0.105 icmp_seq=2 Destination Host Unreachable From 192.168.0.105 icmp_seq=3 Destination Host Unreachable From 192.168.0.105 icmp_seq=4 Destination Host Unreachable From 192.168.0.105 icmp_seq=5 Destination Host Unreachable ^C --- 119.x.x.90 ping statistics --- 8 packets transmitted, 0 received, +5 errors, 100% packet loss, time 7167ms
解决方案: 客户端更改为正确host请求即可。
Port Unreachable
解决了上面ping 结果Host Unreachable的问题后,客户端表示ping是OK的,但是UDP通信还是会报 Port Unreachable错误,真是怪事天天有,今天特别多,继续排查。
嗯,经过排查,客户端的UDP端口写错了,简单来说给客户端的连接地址是test.demoabc.com:9999, 但是客户端实际使用的时候用的域名是test.demoabc.com,但是端口却使用了默认的HTTP 80端口,TCP的80端口确实起着nginx在监听着,但是UDP的80端口可是没有任何进程监听的,于是就会导致Port Unreachable,大概类似于以下hping的请求
sudo hping -2 -k -s 3000 -p 80 test.demoabc.com -d 2 HPING test.demoabc.com (en0 119.x.x.100): udp mode set, 28 headers + 2 data bytes ICMP Port Unreachable from ip=119.x.x.100 name=test.demoabc.com ICMP Port Unreachable from ip=119.x.x.100 name=test.demoabc.com ICMP Port Unreachable from ip=119.x.x.100 name=test.demoabc.com ICMP Port Unreachable from ip=119.x.x.100 name=test.demoabc.com ICMP Port Unreachable from ip=119.x.x.100 name=test.demoabc.com ICMP Port Unreachable from ip=119.x.x.100 name=test.demoabc.com ^C --- test.demoabc.com hping statistic --- 6 packets tramitted, 6 packets received, 0% packet loss
解决方案:客户端更改为正确host+port请求即可。
再次C端UDP发包无响应
解决了上面三个问题后,客户端、服务端UDP通信终于是调通了,可以继续快乐的开发后续逻辑了,结果某天客户端突然反馈客户端发包正常,但是收不到任何回包,重新自己用hping进行测试确实也是类似的结果,hping结果如下:
sudo hping -2 -k -s 3000 -p 9999 test.demoabc.com -d 2 HPING test.demoabc.com (en0 119.x.x.100): udp mode set, 28 headers + 2 data bytes ^C --- test.demoabc.com hping statistic --- 19 packets tramitted, 0 packets received, 100% packet loss round-trip min/avg/max = 0.0/0.0/0.0 ms
看上去和刚开始防火墙导致的问题确实毫无区别,但是又check了防火墙规则确认并不存在问题,使用tcpdump抓包也确认收到了来自C端的UDP包,最后还在server代码中添加了UDP 收包后直接打印原始内容的log,也能够确认数据已经被交付到了监听进程,可是C端为什么收不到任何响应呢?
仔细思考UDP的原理,UDP本身是无连接、不可靠的,它不像TCP那样协议保证每个发包都会保证送达,协议会保证有对应的ack回包--即便业务代码不给C端回包,协议本身也会保证有ack的回包,所以理论上如果S端收到了C端的UDP包,本身却不做任何回应的话,对于发包的C端来说其实并不能知道数据包是在发送途中默默丢失了、被目标防火墙拒收丢弃了还是最终被S收到了但未做任何回应。
前面已经确认了防火墙配置正确,tcpdump抓包和业务log也验证了监听进程确实已经收到了C端数据包,那么问题就只可能出在S端给C端的回包逻辑上了,代码中为了测试对于C端的发包是有一次固定回包的,S端固定1s间隔还会给C端发送心跳包,这在之前几天其实无论自测还是C端使用上都是正常的,结果现在突然就不work了。依据丰富的bug经验--推断应该是最近的业务代码改动出bug了--很合理的解释,仔细一查服务端近期并没有代码改动,但是代码中会有一些异常条件判断提前return的逻辑,在相应地方添加详细错误log后重新测试,终于真相大白--客户端使用的序列化协议错了,C端最近一次改动序列化生成的二进制数据S端会解析出错,于是提前return,不会走后面的回包逻辑,而固定心跳包机制未生效的原因也破解了--只有成功走到S回包流程的C端ip/port才会被加入S的活跃C端列表,才会发送心跳包,这里由于C端的数据全部错误提前返回了,未能加入活跃C端列表,也就不会发送心跳包了。而自己使用hping测试之前正常现在却是失败的原因也是由于刚开始并没有加这块解析错误提前return的逻辑,只是简单验证发包回包连通性,现在已经有这块解析校验return的逻辑的情况下使用hping当然也一样收不到回包了。
解决方案:C端fix错误的序列化代码,S端增加更全面、详细的错误log方便后续更快排查问题。
对于ICMP的进一步认识
到这一步UDP通信联调碰到的4个问题已经阐述完毕,似乎已经可以结束整篇blog了,但是好像标题中的ICMP到目前为止还没有丝毫提及的样子?
对于有一定网络基础、经验的小伙伴,其实应该都能意识到之前虽然一直没有提到ICMP的名字,但ICMP本身的使用其实已经被多次提及,从ping命令执行的响应Host Unreachable,hping命令和C端发UDP包得到的响应 Port Unreachable这些其实都是依赖的ICMP协议。
但是对于网络基础较弱的小伙伴,这一点就并不那么明显了,包括自己在内其实之前从来没有把 Unreachable这些报错响应和ICMP直接联系起来过--换句话说,课堂上学习过ICMP,知道其全称是Internet Control Message Protocol,也大概知道ping命令和ICMP有关系,但是更进一步:ICMP到底是干啥的?什么场景下会有ICMP响应?为什么有时候响应是Host Unreachable,有时候是Port Unreacable,有时候又直接是timeout而没有任何响应呢?ICMP是哪一层的协议?UDP、TCP和ICMP又有什么关系?
更详细的ICMP介绍网上已经有很多的资料了,这里仅简单讲述一下自己对以上问题的理解--有错漏欢迎大家指正,想进一步了解的小伙伴推荐阅读小林coding的20 张图解: ping 的工作原理,图文并茂讲的非常之赞。
ICMP到底是干啥的
顾名思义,ICMP是用于控制报文传输的协议,主要分为两类:查询报文和差错报文。
什么场景下会有ICMP响应
可以先思考一个问题,当源主机发送一个IP包、UDP包或者TCP包给目标主机时,如果在送达过程中出现了某些而被丢弃--比如目标主机关机了,源主机怎么能知道某个包被丢弃了/主机不可达呢?如果没有一种机制负责通知源主机的话,源主机可能只能傻等到timeout了,这就是ICMP的通知机制的一种使用场景,节点会在丢弃数据包的同时向源主机发送ICMP报文通知,明确告知其目标主机unreachable。
非常常用的ping命令就是基于查询报文实现,查询报文可用于测试到目的主机链路是否可达,目的主机在收到查询报文时默认会回复一个响应报文给源主机--除非目的主机禁止了策略回送,在简单网络延迟测试、链路连通故障检测方面使用ping命令大家应该都已经十分熟悉了。
而差错报文类型就是在IP数据包送达目的主机过程中出错时,出错节点给源主机回送的具体差错信息,比如数据包到达了目的主机前一跳路由器,目的主机已关机,路由器无法送达数据包就会给告知源主机 Host Unreachable, 又比如源主机发送UDP包到目标主机的9999端口但是9999端口的监听进程挂了没起来,目标主机发现收到了UDP包但是却没有可交付的进程,就会告知源主机Port Unreachable。
为什么有时候响应是Host Unreachable,有时候是Port Unreacable,有时候又直接是timeout而没有任何响应呢?
如果最终发现数据包无法送达只能丢弃的节点(可能是中间路由器、最终主机等)没有禁止对应ICMP消息响应,那就会给源主机发送Unreachable响应,简单来说如果直接是目的host都找不到无法交付会响应Host Unreachable, 如果已经交付到了目标host,但是目标host发现这是个传输层TCP、UDP包却无法找到对应port的接收进程, 响应就是 Port Unreachable。而如果节点禁止对应ICMP消息响应,那么节点只是简单丢弃无法送达的数据包,不会有响应操作,此时源主机等待超过一定时间也就只能判定timeout了。
ICMP是哪一层的协议
ICMP本身是网络层协议。
UDP、TCP和ICMP又有什么关系
UDP、TCP都是传输层协议,其和网络层ICMP并没有直接关系,但是当UDP、TCP数据包在送达目的主机的过程中出现问题--如Host UnReachable、Port Unreachable、拒绝分片导致被丢弃时,节点会通过向源主机发送ICMP报文帮助源主机了解发送失败原因以进一步处理,其他还有提示数据发送方更优路径的Redirect Message和缓解拥堵的Source Quench Message等。
总结
亲自实践了基于UDP的网络编程可谓把之前以死记硬背为主的UDP知识进行了一番试炼与提纯,真正的加深了理解,同时对于看似熟悉实则陌生的ICMP协议有了直观得多、深入的多的学习。真正是:纸上得来终觉浅,绝知此事要躬行。与大家共勉。
转载请注明出处,原文地址: https://www.cnblogs.com/AcAc-t/p/udp_message_and_icmp.html