网络实验:观察UDP协议通信过程

天行健,君子以自强不息。地势坤,君子以厚德载物。

—— 西周·姬昌·《周易》

为更好地理解 UDP 协议的工作原理,本节我们安排一次实验,来观察 UDP 应用的通信过程。

Client-Server 模型

开始之前,我们先来讨论 客户端-服务端模型 ( client-server model ),简称 CS模型

网络应用一般可以分为两种角色,一种角色负责提供服务或资源,称为 服务端 ( server );另一种角色则是服务和资源的访问者,一般称为 客户端 ( client )。

如下图,这是一个典型的例子:右边是一台 服务器 ,它负责提供数据服务;左边的手机、PC等设备则是 客户端 ,它们可以通过互联网访问服务器提供的数据:

像这样由客户端和服务端组成的通信模型就叫做 CS模型 ,这种架构在分布式系统中非常常见。

典型应用

我们提供了一个简单的网络时间查询应用,它分为服务端和客户端两部分:

  • 服务端 udp-time-server 监听某个端口上的 UDP 请求报文,并提供时间查询服务;
  • 客户端 udp-time-client 负责向服务端发起时间查询请求,并输出结果;

客户端和服务端之间采用 UDP 作为通信协议:

  1. udp-time-client 先发出一个时间查询请求,该请求被封装成 UDP 数据报发给服务端;
  2. udp-time-server 对请求进行处理,应答数据同样被封装成 UDP 数据报发给客户端;

客户端和服务端双方使用 UDP 协议进行通信,它们必须对 UDP 数据的格式达成一致:

这个应用非常简单,通信数据只分为 请求 ( request )和 应答 ( reply )两种。

其中,请求由客户端发起,它需要提供时间格式化字符串,将期望的时间格式告诉服务端。请求数据分为两个字段:

  • 时间格式化字符串长度
  • 时间格式化字符串

应答由服务端发出,它需要将时间按照期望的格式,发给客户端。应答数据也分为两个字段:

  • 时间字符串长度
  • 时间字符串

请求数据和应答数据,都要符合这样的格式才能被通信双方所接受。关于这部分数据格式的约定,其实就构成了 应用层 协议。理论上,我们定的这个数据格式,就构成了一个应用层协议,虽然它还非常简陋。

最后,我们回过头来看:怎么将时间查询应用跑起来。

服务端

在 Linux 终端中执行 udp-time-server 命令,即可启动时间查询应用的服务端:

1
udp-time-server -p [服务端端口]
  • -p 选项指定监听端口。

客户端

同理,在 Linux 终端中执行 udp-time-client 命令,即可运行时间查询客户端:

1
udp-time-client -i [服务端IP] -p [服务端端口] -f [时间格式化字符串]
  • -i 选项指定服务端的 IP 地址;
  • -p 选项指定服务端的端口;
  • -f 选项指定期望的时间格式,可使用形如 %Y-%m-%d %H:%M:%S 的时间格式化串;

实验步骤

我们需要准备一台服务器和一台客户端主机,之前介绍 traceroute 的演示环境,就是一个不错的选择:

实验环境同样由 Docker 提供,只需执行这个 docker 命令即可一键启动:

1
docker run --name udp-time-service-lab --rm -it --privileged --cap-add=NET_ADMIN --cap-add=SYS_ADMIN -v /data -h switch fasionchan/netbox:0.6 bash /script/udp-time-service-lab.sh

实验环境启动后,我们将看到 3 个窗口。其中,有两个窗口属于主机 ant ,另一个窗口属于主机 apple 。

如何在不同窗口间进行切换呢? 我们来复习一下 tmux 窗口切换操作:

  1. 按下 tmux 功能键 Ctrl-B
  2. 再按下 Q ,这时每个窗口会出现一个数字;
  3. 按下窗口上的数字,即可切到对应的窗口;

我们在主机 apple 上运行服务端 udp-time-server ,-p 选项指定监听端口为 12345 :

1
root@apple [ ~ ]  ➜ udp-time-server -p 12345

运行客户端之前,我们先使用 tcpdump 命令进行抓包,以便观察双方的通信过程:

1
2
3
root@ant [ ~ ]  ➜ tcpdump -nXi eth0 udp
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes

请注意,tcpdump 命令最后的参数是过滤规则,规则 udp 表示只抓 UDP 协议报文。

准备完毕后,我们在主机 ant 上执行客户端 udp-time-client ,向位于主机 apple 的服务端请求时间:

1
2
3
root@ant [ ~ ]  ➜ udp-time-client -i 10.0.2.2 -p 12345 -f '%Y-%m-%d %H:%M:%S'
Receive 20 bytes
2021-03-16 20:30:52
  • -i 指定服务端的地址,即 10.0.2.2 ;
  • -p 指定服务端监听的端口,即 12345 ;
  • -f 指定想要的时间格式;

udp-time-client 命令的输出表明,它收到了服务端回复的应答,当前时间是 2021-03-16 20:30:52

服务端 udp-time-server 的输出表明,它收到了来自 10.0.1.2 ,也就是 ant 的请求:

1
2
root@apple [ ~ ]  ➜ udp-time-server -p 12345
10.0.1.2:53018 request with format: %Y-%m-%d %H:%M:%S

主机 ant 上的 tcpdump 程序输出也表明,它向 10.0.2.2( apple )发出了一个 UDP 数据报,报文中带着时间查询请求;随后收到 10.0.2.2 回复的 UDP 数据报,报文中是一个时间查询应答。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
root@ant [ ~ ]  ➜ tcpdump -nXi eth0 udp
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
20:30:52.034026 IP 10.0.1.2.53018 > 10.0.2.2.12345: UDP, length 22
        0x0000:  4500 0032 e63a 4000 4011 3d7d 0a00 0102  E..2.:@.@.=}....
        0x0010:  0a00 0202 cf1a 3039 001e 1733 0000 0012  ......09...3....
        0x0020:  2559 2d25 6d2d 2564 2025 483a 254d 3a25  %Y-%m-%d.%H:%M:%
        0x0030:  5300                                     S.
20:30:52.034250 IP 10.0.2.2.12345 > 10.0.1.2.53018: UDP, length 24
        0x0000:  4500 0034 f809 4000 3d11 2eac 0a00 0202  E..4..@.=.......
        0x0010:  0a00 0102 3039 cf1a 0020 1735 0000 0014  ....09.....5....
        0x0020:  3230 3231 2d30 332d 3136 2032 303a 3330  2021-03-16.20:30
        0x0030:  3a35 3200                                :52.

tcpdump 输出看着有点费劲,但我们可以用 Wireshark 来解读,pcap 文件同样可以从 Github 上获取。

WireShark 显示了两个 UDP 报文,其中一个是时间查询请求,另一个是时间查询应答。点击 UDP 协议部分,可以查看 UDP 报文的头部信息;点击 Data ,可以查看 UDP 报文中的数据。

图中高亮的部分就是 UDP 报文中的数据,也就是时间查询请求:前四个字节 00000012 就是时间格式化字符串的长度,十六进制 12 换算成十进制是 18 。后面紧跟着时间格式化字符串%Y-%m-%d %H:%M:%S\0 ,包含 \0 在内,正好就是 18 字节。

小菜学网络】系列文章首发于公众号【小菜学编程】,敬请关注:

【小菜学网络】系列文章首发于公众号【小菜学编程】,敬请关注: