Python 绝技 —— UDP 服务器与客户端
0x00 前言
在上一篇文章「Python 绝技 —— TCP 服务器与客户端」中,介绍了传输层的核心协议 TCP ,并运用 Python 脚本的 socket 模块演示了 TCP 服务器与客户端的通信过程。
本篇将按照同样的套路,先介绍传输层的另一个核心协议 UDP,再比较 TCP 与 UDP 的特点,最后借助 Python 脚本演示 UDP 服务器与客户端的通信过程。
0x01 UDP 协议
UDP(User Datagram Protocol,用户数据报协议)是一种无连接、不可靠、基于数据报的传输层通信协议。
- UDP 的通信过程与 TCP 相比较为简单,不需要复杂的三次握手与四次挥手,体现了无连接;
- UDP 传输速度比 TCP 快,但容易丢包、数据到达顺序无保证、缺乏拥塞控制、秉承尽最大努力交付的原则,体现了不可靠;
- UDP 的无连接与不可靠特性注定无法采用字节流的通信模式,由协议名中的「Datagram」与 socket 类型中的「SOCK_DGRAM」即可体现它基于数据报的通信模式。
为了更直观地比较 TCP 与 UDP 的异同,笔者将其整理成以下表格:
TCP | UDP | |
---|---|---|
连接模式 | 面向连接(单点通信) | 无连接(多点通信) |
传输可靠性 | 可靠 | 不可靠 |
通信模式 | 基于字节流 | 基于数据报 |
报头结构 | 复杂(至少20字节) | 简单(8字节) |
传输速度 | 慢 | 快 |
资源需求 | 多 | 少 |
到达顺序 | 保证 | 不保证 |
流量控制 | 有 | 无 |
拥塞控制 | 有 | 无 |
应用场合 | 大量数据传输 | 少量数据传输 |
支持的应用层协议 | Telnet、FTP、SMTP、HTTP | DNS、DHCP、TFTP、SNMP |
0x02 Network Socket
Network Socket(网络套接字)是计算机网络中进程间通信的数据流端点,广义上也代表操作系统提供的一种进程间通信机制。
进程间通信(Inter-Process Communication,IPC)的根本前提是能够唯一标示每个进程。在本地主机的进程间通信中,可以用 PID(进程 ID)唯一标示每个进程,但 PID 只在本地唯一,在网络中不同主机的 PID 则可能发生冲突,因此采用「IP 地址 + 传输层协议 + 端口号」的方式唯一标示网络中的一个进程。
小贴士:网络层的 IP 地址可以唯一标示主机,传输层的 TCP/UDP 协议和端口号可以唯一标示该主机的一个进程。注意,同一主机中 TCP 协议与 UDP 协议的可以使用相同的端口号。
所有支持网络通信的编程语言都各自提供了一套 socket API,下面以 Python 3 为例,讲解服务器与客户端建立 UDP 通信连接的交互过程:
可见,UDP 的通信过程比 TCP 简单许多,服务器少了监听与接受连接的过程,而客户端也少了请求连接的过程。客户端只需要知道服务器的地址,直接向其发送数据即可,而服务器也敞开大门,接收任何发往自家地址的数据。
小贴士:由于 UDP 采用无连接模式,可知 UDP 服务器在接收到客户端发来的数据之前,是不知道客户端的地址的,因此必须是客户端先发送数据,服务器后响应数据。而 TCP 则不同,TCP 服务器接受了客户端的连接后,既可以先向客户端发送数据,也可以等待客户端发送数据后再响应。
0x03 UDP 服务器
1 | #!/usr/bin/env python3 |
- Line 5:创建 socket 对象,第一个参数为 socket.AF_INET,代表采用 IPv4 协议用于网络通信,第二个参数为 socket.SOCK_DGRAM,代表采用 UDP 协议用于无连接的网络通信。
- Line 6:向 socket 对象绑定服务器主机地址 (“127.0.0.1”, 6000),即本地主机的 UDP 6000 端口。
- Line 9:进入与客户端交互数据的循环阶段。
- Line 10:接收客户端发来的数据,包括 bytes 对象 data,以及客户端的 IP 地址和端口号 addr,其中 addr 为二元组 (host, port)。
- Line 11:打印接收信息,表示从地址为 addr 的客户端接收到数据。
- Line 12:若 bytes 对象为
b"exit"
,则向地址为 addr 的客户端发送结束响应信息b"Good bye!\n"
。发送完毕后,继续等待其他 UDP 客户端发来数据。 - Line 15:若 bytes 对象不为
b"exit"
,则向地址为 addr 的客户端发送问候响应信息b"Hello %s!\n"
,其中%s
是客户端发来的 bytes 对象。发送完毕后,继续等待任意 UDP 客户端发来数据。
与 TCP 服务器相比,UDP 服务器不必使用多线程,因为它无需为每个通信过程创建独立连接,而是采用「即收即发」的模式,又一次体现了 UDP 的无连接特性。
0x04 UDP 客户端
1 | #!/usr/bin/env python3 |
- Line 5:创建 socket 对象,第一个参数为 socket.AF_INET,代表采用 IPv4 协议用于网络通信,第二个参数为 socket.SOCK_DGRAM,代表采用 UDP 协议用于无连接的网络通信。
- Line 6:初始化 UDP 服务器的地址 (“127.0.0.1”, 6000),即本地主机的 UDP 6000 端口。
- Line 8:进入与服务器交互数据的循环阶段。
- Line 9:要求用户输入名字。
- Line 10:当用户的输入为空时,则重新开始循环,要求用户重新输入。
- Line 12:当用户的输入非空时,则将字符串转换为 bytes 对象后,发送至地址为 (“127.0.0.1”, 6000) 的 UDP 服务器。
- Line 13:接收服务器的响应数据,包括 bytes 对象 response,以及服务器的 IP 地址和端口号 addr,其中 addr 为二元组 (host, port)。
- Line 14:将响应的 bytes 对象 response 转换为字符串后打印输出。
- Line 15:当用户的输入为
"exit"
时,则打印会话结束信息,终止与服务器交互数据的循环阶段,即将关闭套接字。 - Line 19:关闭套接字,不再向服务器发送数据。
0x05 UDP 进程间通信
将 UDP 服务器与客户端的脚本分别命名为 udp_server.py
与 udp_client.py
,然后存至桌面,笔者将在 Windows 10 系统下用 PowerShell 进行演示。
小贴士:读者进行复现时,要确保本机已安装 Python 3,注意笔者已将默认的启动路径名
python
改为了python3
。
单服务器 VS 多客户端
- 在其中一个 PowerShell 中运行命令
python3 ./udp_server.py
,服务器绑定本地主机的 UDP 6000 端口,并打印信息UDP bound on port 6000...
,等待客户端发来数据; - 在另两个 PowerShell 中分别运行命令
python3 ./udp_client.py
,并向服务器发送字符串Client1
、Client2
; - 服务器打印接收信息,表示分别从 UDP 63643、63644端口接收到数据,并分别向客户端发送问候响应信息;
- 客户端
Client1
发送空字符串,则被要求重新输入; - 客户端
Client2
先发送字符串Alice
,得到服务器的问候响应信息,再发送字符串exit
,得到服务器的结束响应信息,最后打印会话结束信息,终止与服务器的数据交互; - 客户端
Client1
发送字符串exit
,得到服务器的结束响应信息,并打印会话结束信息,终止与服务器的数据交互; - 服务器按照以上客户端的数据发送顺序打印接收信息,并继续等待任意 UDP 客户端发来数据。
0x06 Python API Reference
socket 模块
本节介绍上述代码中用到的内建模块 socket,是 Python 网络编程的核心模块。
socket() 函数
socket() 函数用于创建网络通信中的套接字对象。函数原型如下:
1 | socket.socket(family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None) |
- family 参数代表地址族(Address Family),默认值为 AF_INET,用于 IPv4 网络通信,常用的还有 AF_INET6,用于 IPv6 网络通信。family 参数的可选值取决于本机操作系统。
- type 参数代表套接字的类型,默认值为 SOCK_STREAM,用于 TCP 协议(面向连接)的网络通信,常用的还有 SOCK_DGRAM,用于 UDP 协议(无连接)的网络通信。
- proto 参数代表套接字的协议,默认值为 0,一般忽略该参数,除非 family 参数为 AF_CAN,则 proto 参数需设置为 CAN_RAW 或 CAN_BCM。
- fileno 参数代表套接字的文件描述符,默认值为 None,若设置了该参数,则其他三个参数将会被忽略。
创建完套接字对象后,需使用对象的内置函数完成网络通信过程。注意,以下函数原型中的「socket」是指 socket 对象,而不是上述的 socket 模块。
bind() 函数
bind() 函数用于向套接字对象绑定 IP 地址与端口号。注意,套接字对象必须未被绑定,并且端口号未被占用,否则会报错。函数原型如下:
1 | socket.bind(address) |
- address 参数代表套接字要绑定的地址,其格式取决于套接字的 family 参数。若 family 参数为 AF_INET,则 address 参数表示为二元组 (host, port),其中 host 是用字符串表示的主机地址,port 是用整型表示的端口号。
sendto() 函数
sendto() 函数用于向远程套接字对象发送数据。注意,该函数用于 UDP 进程间的无连接通信,远程套接字的地址在参数中指定,因此使用前不需要先与远程套接字连接。相对地,TCP 进程间面向连接的通信过程需要用 send() 函数。函数原型如下:
1 | socket.sendto(bytes[, flags], address) |
- bytes 参数代表即将发送的 bytes 对象数据。例如,对于字符串
"hello world!"
而言,需要用 encode() 函数转换为 bytes 对象b"hello world!"
才能进行网络传输。 - flags 可选参数用于设置 sendto() 函数的特殊功能,默认值为 0,也可由一个或多个预定义值组成,用位或操作符
|
隔开。详情可参考 Unix 函数手册中的 sendto(2),flags 参数的常见取值有 MSG_OOB、MSG_EOR、MSG_DONTROUTE 等。 - address 参数代表远程套接字的地址,其格式取决于套接字的 family 参数。若 family 参数为 AF_INET,则 address 参数表示为二元组 (host, port),其中 host 是用字符串表示的主机地址,port 是用整型表示的端口号。
sendto() 函数的返回值是发送数据的字节数。
recvfrom() 函数
recvfrom() 函数用于从远程套接字对象接收数据。注意,与 sendto() 函数不同,recvfrom() 函数既可用于 UDP 进程间通信,也能用于 TCP 进程间通信。函数原型如下:
1 | socket.recvfrom(bufsize[, flags]) |
- bufsize 参数代表套接字可接收数据的最大字节数。注意,为了使硬件设备与网络传输更好地匹配,bufsize 参数的值最好设置为 2 的幂次方,例如 4096。
- flags 可选参数用于设置 recv() 函数的特殊功能,默认值为 0,也可由一个或多个预定义值组成,用位或操作符
|
隔开。详情可参考 Unix 函数手册中的 recvfrom(2),flags 参数的常见取值有 MSG_OOB、MSG_PEEK、MSG_WAITALL 等。
recvfrom() 函数的返回值是二元组 (bytes, address),其中 bytes 是接收到的 bytes 对象数据,address 是发送方的 IP 地址与端口号,用二元组 (host, port) 表示。注意,recv() 函数的返回值只有 bytes 对象数据。
close() 函数
close() 函数用于关闭本地套接字对象,释放与该套接字连接的所有资源。
1 | socket.close() |
0x07 总结
本文介绍了 UDP 协议的基础知识,并与 TCP 协议进行对比,再用 Python 3 实现并演示了 UDP 服务器与客户端的通信过程,最后将脚本中涉及到的 Python API 做成了的参考索引,有助于读者理解实现过程。
感谢各位的阅读,笔者水平有限,若有不足或错误之处请谅解并告知,希望自己对 TCP 和 UDP 的浅薄理解,能帮助读者更好地理解传输层协议。
本文的相关参考请移步至: