python socket 编程之 —— UDP 通信
相比于 TCP 的面向连接、可靠传输,UDP 协议是一种无连接、不保证数据可靠性和顺序的传输方式。它更适用于对实时性要求高,但对丢包不敏感的场景,比如视频直播、在线游戏和语音通信等。Python 中的 socket 模块同样封装了 UDP 协议,为程序员提供了简便的接口来实现 UDP 通信。本文将分别介绍 UDP 通信中的客户端与服务端实现方法,并给出相应的代码示例。
客户端(Client)
UDP 客户端无需建立连接,而是直接将数据报发送到指定的服务器地址和端口。下面的示例展示了如何使用 Python 的 socket 模块实现一个简单的 UDP 客户端:
import socket
# 创建一个 UDP socket,AF_INET表示IPv4,SOCK_DGRAM表示使用UDP协议
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 设置超时时长,单位为秒。1s=1000ms
s.settimeout(1)
# 定义服务端的地址与端口。 因为UDP是无连接的服务,所以只需要知道IP地址和端口号,就可以直接发送消息,无需建立连接。
server_addr = ("127.0.0.1", 8888)
# 要发送的消息
message = "hello, UDP"
# 使用sendto方法将消息发送到指定的服务端地址
s.sendto(message.encode(), server_addr)
# 接收来自服务端的响应,recvfrom返回一个元组:(数据, 服务端地址)
# 因为设置了超时,所以使用try,except语句处理超时错误,避免程序崩溃
try:
data, addr = s.recvfrom(1024)
if data:
recv_message = data.decode()
print(f"接收到来自 {addr} 的消息: {recv_message}")
except socket.timeout:
print("timeout")
s.close()
说明:
- UDP 是无连接的,因此无需调用
connect方法,而是直接使用sendto将数据报发送给目标地址。 - 使用
recvfrom接收数据时,同时会返回发送方的地址信息。recvfrom函数返回的是数据和地址的元组(tuple)
recv与recvfrom
读者可能会发现,在TCP编程中我们使用了socket.recv()方法,但是在UDP中我们使用的是socket.recvfrom()方法。二者有什么区别呢?
recv方法返回data,也就是只返回数据,recvfrom方法返回(data, addr),也就是返回数据和发送者的地址两个信息。- UDP
recv和recvfrom方法都可以使用。但是一般来说,需要发送者地址才能够进行回复,所以常用recvfrom - TCP只能使用
recv方法。由于其本身就是建立在连接上的通信,所以本身就知道双方地址。
服务端(Server)
UDP 服务端的编程也十分简单,只需要绑定端口后不断调用 recvfrom 方法即可接收来自各个客户端的数据报。下面是一个基本的 UDP 服务端示例代码:
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 是无连接协议,所以服务端不需要调用
listen或accept方法。 - 每个收到的数据报都包含了发送者的地址信息,可以利用该信息直接回复客户端。
UDP 通信的特点与注意事项
-
无连接性
UDP 不需要建立连接,每个数据报都是独立发送和接收的。这样既降低了通信延迟,又节省了连接资源。 -
不保证可靠性
UDP 不保证数据包一定能到达,也不保证数据包的顺序。如果应用场景要求数据完整可靠,需要在应用层自行实现相应的机制。 -
适用于实时性要求高的场景
由于传输过程中开销较小,UDP 更适用于视频会议、实时语音、在线游戏等对延迟要求较高的场景。 -
数据报大小限制
由于 UDP 数据报需要在 IP 层进行分片与组装,因此数据包一般不宜过大,否则可能引起丢包或传输失败。通常建议数据包大小不超过 1500 字节(考虑到 MTU)。
更优雅的服务端写法
当服务端功能较复杂,需要处理大量不同逻辑时,可以采用面向对象的方式对 UDP 服务端进行封装。下面是一个使用类和多线程的 UDP 服务端示例(注意:UDP 本身是无连接的,多线程仅用于并发处理接收到的数据报):
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()
说明:
- 通过将服务端功能封装在一个类中,可以更方便地扩展和维护代码。
- 多线程处理可以避免在处理某个数据报时阻塞其它数据的接收,不过在高并发场景下,需要注意线程调度和资源竞争问题。