背景

最近有个业务场景需要服务端(简称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

参考

https://www.cnblogs.com/xiaolincoding/p/12571184.html

https://juejin.cn/post/6993853593476399140

https://www.cnblogs.com/acac-t/p/udp_message_and_icmp.html

https://www.zhihu.com/question/31002474

C/S UDP通信实践踩坑记录与对于ICMP的进一步认识的更多相关文章

  1. DevOps落地实践点滴和踩坑记录-(2) -聊聊平台建设

    很久没有写文章记录了,上一篇文章像流水账一样,把所见所闻一个个记录下来.这次专门聊聊DevOps平台的建设吧,有些新的体会和思考,希望给正在做这个事情的同学们一些启发吧. DevOps落地实践点滴和踩 ...

  2. 你真的了解字典(Dictionary)吗? C# Memory Cache 踩坑记录 .net 泛型 结构化CSS设计思维 WinForm POST上传与后台接收 高效实用的.NET开源项目 .net 笔试面试总结(3) .net 笔试面试总结(2) 依赖注入 C# RSA 加密 C#与Java AES 加密解密

    你真的了解字典(Dictionary)吗?   从一道亲身经历的面试题说起 半年前,我参加我现在所在公司的面试,面试官给了一道题,说有一个Y形的链表,知道起始节点,找出交叉节点.为了便于描述,我把上面 ...

  3. ABP框架踩坑记录

    ABP框架踩坑记录 ASP.NET Boilerplate是一个专用于现代Web应用程序的通用应用程序框架. 它使用了你已经熟悉的工具,并根据它们实现最佳实践. 文章目录 使用MySQL 配置User ...

  4. python发布包到pypi的踩坑记录

    前言 突然想玩玩python了^_^ 这篇博文记录了我打算发布包到pypi的踩坑经历.python更新太快了,甚至连这种发布上传机制都在不断的更新,这导致网上的一些关于python发布上传到pypi的 ...

  5. unionId突然不能获取的踩坑记录

    昨天(2016-2-2日),突然发现系统的一个微信接口使用不了了.后来经查发现,是在网页授权获取用户基本信息的时候,unionid获取失败导致的. 在网页授权获取用户基本信息的介绍中(http://m ...

  6. CentOS7.4安装MySQL踩坑记录

    CentOS7.4安装MySQL踩坑记录 time: 2018.3.19 CentOS7.4安装MySQL时网上的文档虽然多但是不靠谱的也多, 可能因为版本与时间的问题, 所以记录下自己踩坑的过程, ...

  7. ubuntu 下安装docker 踩坑记录

    ubuntu 下安装docker 踩坑记录 # Setp : 移除旧版本Docker sudo apt-get remove docker docker-engine docker.io # Step ...

  8. SpringBoot + Shiro + shiro.ini 的踩坑记录

    0.写在前面的话 好久没写博客了,诶,好多时候偷懒直接就抓网上的资料丢笔记里了,也就没有自己提炼,偷懒偷懒.然后最近参加了一个网络课程,要交作业的那种,为了能方便看下其他同学的作业,就写了个爬虫把作业 ...

  9. google nmt 实验踩坑记录

       最近因为要做一个title压缩的任务,所以调研了一些text summary的方法.    text summary 一般分为抽取式和生成式两种.前者一般是从原始的文本中抽取出重要的word o ...

  10. SpringBoot+SpringSecurity+Thymeleaf认证失败返回错误信息踩坑记录

    Spring boot +Spring Security + Thymeleaf认证失败返回错误信息踩坑记录 步入8102年,现在企业开发追求快速,Springboot以多种优秀特性引领潮流,在众多使 ...

随机推荐

  1. 开源WindivertDotnet

    0 前言 Hi,好久没有写博客,因为近段时间没有新的开源项目给大家.现在终于又写了一篇,是关于网络方向的内容,希望对部分读者有帮助. 1 WinDivert介绍 WinDivert是windows下为 ...

  2. JUC(5)BlockingQueue四组API

    1.读写锁ReadWriteLock package com.readlock; import java.util.HashMap; import java.util.Map; /** * ReadW ...

  3. 齐博x1如果把万能表单直接插入到内容中去

    很多时候,你创建了一个万能表单可能像下面这个情况,在文章中加一个链接叫别人点击填表,其实这个很不人性化,用户也容易忽略. 其实你完全可以像下面这样,把表单直接引用到文章中来.给用户更直观的感觉 那是如 ...

  4. Linux--多线程(二)

    线程的同步和互斥 基本概念 概述:现在操作系统基本都是多任务的操作系统,同时有大量可以调度的实体在运行.在多任务操作系统当中,同时运行的多个任务可能: 都需要访问/使用同一种资源 多个任务之间有依赖关 ...

  5. webRTC demo

    准备: 信令服务 前端页面用于视频通话 demo github 地址. 前端页面 为了使 demo 尽量简单,功能页面如下,即包含登录.通过对方手机号拨打电话的功能.在实际生成过程中,未必使用的手机号 ...

  6. 支持JDK19虚拟线程的web框架之四:看源码,了解quarkus如何支持虚拟线程

    欢迎访问我的GitHub 这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos 前文链接 支持JDK19虚拟线程的web框架,之一:体 ...

  7. AtCoder Beginner Contest 277 题解

    掉大分力(悲 A - ^{-1} 直接模拟. #include<bits/stdc++.h> #define IOS ios::sync_with_stdio(false) #define ...

  8. [zoj] 4178. Killing the Brute-force

    题目 Chenjb is the task author of the 71-st Zhejiang Provincial Collegiate Programming Contest. He cre ...

  9. Windows Server 2019 安装 Oracle 19C RAC(VMWare虚拟机环境)

    软件 Windows Server 2019 Standard Oracle 19C Oracle Grid 19 VMware Workstation 16 规划 共享存储,使用Windows Se ...

  10. 23、有一个字符串,包含n个字符,编写一函数,将此字符串中从第m个字符开始的全部字符串复制成另一个字符串

    /* 有一个字符串,包含n个字符,编写一函数,将此字符串中从第m个字符开始的全部字符串复制成另一个字符串 */ #include <stdio.h> #include <stdlib ...