python socket编程之---UDP通信

相比于 TCP 的面向连接、可靠传输,UDP 协议是一种无连接、不保证数据可靠性和顺序的传输方式。它更适用于对实时性要求高,但对丢包不敏感的场景,比如视频直播、在线游戏和语音通信等。Python 中的 socket 模块同样封装了 UDP 协议,为程序员提供了简便的接口来实现 UDP 通信。本文将分别介绍 UDP 通信中的客户端与服务端实现方法,并给出相应的代码示例。

客户端

UDP 客户端无需建立连接,而是直接将数据报发送到指定的服务器地址和端口。下面的示例展示了如何使用 Python 的 socket 模块实现一个简单的 UDP 客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import socket

# 创建一个 UDP socket,AF_INET表示IPv4,SOCK_DGRAM表示使用UDP协议
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

# 定义服务端的地址与端口
server_addr = ("127.0.0.1", 8888)

# 要发送的消息
message = "hello, UDP"

# 使用sendto方法将消息发送到指定的服务端地址
s.sendto(message.encode(), server_addr)

# 接收来自服务端的响应,recvfrom返回一个元组:(数据, 服务端地址)
data, addr = s.recvfrom(1024)
if data:
recv_message = data.decode()
print(f"接收到来自 {addr} 的消息: {recv_message}")

s.close()

说明:

  • UDP 是无连接的,因此无需调用 connect 方法,而是直接使用 sendto 将数据报发送给目标地址。
  • 使用 recvfrom 接收数据时,同时会返回发送方的地址信息。

服务端

UDP 服务端的编程也十分简单,只需要绑定端口后不断调用 recvfrom 方法即可接收来自各个客户端的数据报。下面是一个基本的 UDP 服务端示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import socket

# 创建一个 UDP socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

# 绑定地址与端口,"0.0.0.0"表示监听所有可用的网络接口
server_addr = ("0.0.0.0", 8888)
s.bind(server_addr)

print(f"UDP 服务端已启动,监听地址:{server_addr}")

while True:
# 接收数据和客户端地址
data, client_addr = s.recvfrom(1024)
message = data.decode()
if message:
print(f"接收到来自 {client_addr} 的消息: {message}")
# 处理完消息后,可发送响应给客户端
response = "receive"
s.sendto(response.encode(), client_addr)

说明:

  • 服务端使用 bind 绑定到指定的 IP 地址和端口,其中 "0.0.0.0" 表示监听所有网络接口。如果使用 "127.0.0.1" 则仅能接收本机发送的数据报。
  • 因为 UDP 是无连接协议,所以服务端不需要调用 listenaccept 方法。
  • 每个收到的数据报都包含了发送者的地址信息,可以利用该信息直接回复客户端。

UDP 通信的特点与注意事项

  1. 无连接性
    UDP 不需要建立连接,每个数据报都是独立发送和接收的。这样既降低了通信延迟,又节省了连接资源。

  2. 不保证可靠性
    UDP 不保证数据包一定能到达,也不保证数据包的顺序。如果应用场景要求数据完整可靠,需要在应用层自行实现相应的机制。

  3. 适用于实时性要求高的场景
    由于传输过程中开销较小,UDP 更适用于视频会议、实时语音、在线游戏等对延迟要求较高的场景。

  4. 数据报大小限制
    由于 UDP 数据报需要在 IP 层进行分片与组装,因此数据包一般不宜过大,否则可能引起丢包或传输失败。通常建议数据包大小不超过 1500 字节(考虑到 MTU)。


更优雅的服务端写法

当服务端功能较复杂,需要处理大量不同逻辑时,可以采用面向对象的方式对 UDP 服务端进行封装。下面是一个使用类和多线程的 UDP 服务端示例(注意:UDP 本身是无连接的,多线程仅用于并发处理接收到的数据报):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import socket
from threading import Thread

class UDPServer:
def __init__(self, host="0.0.0.0", port=8888):
self.server_addr = (host, port)
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.sock.bind(self.server_addr)
print(f"UDP 服务端启动,监听地址:{self.server_addr}")

def start(self):
while True:
# 接收数据和客户端地址
data, client_addr = self.sock.recvfrom(1024)
if data:
# 启动新线程处理每个数据报
Thread(target=self.handle_client, args=(data, client_addr)).start()

def handle_client(self, data, client_addr):
message = data.decode()
print(f"收到来自 {client_addr} 的消息: {message}")
# 在此处加入处理逻辑
response = "receive"
self.sock.sendto(response.encode(), client_addr)

if __name__ == "__main__":
server = UDPServer(port=8888)
server.start()

说明:

  • 通过将服务端功能封装在一个类中,可以更方便地扩展和维护代码。
  • 多线程处理可以避免在处理某个数据报时阻塞其它数据的接收,不过在高并发场景下,需要注意线程调度和资源竞争问题。