3. Python UDP 编程入门:socket 模块与核心 API
说实话,很多刚接触网络编程的朋友,一上来就被各种协议栈、状态机给吓住了。但 UDP 不一样——它简单、直接,就像你往邮筒里扔一封信,寄不寄得到、对方收不收得到,你都不太关心。这种「发完就不管」的特性,反而让它在实时通信、视频流、游戏同步这些场景里大放异彩。
我个人习惯,学 UDP 编程的第一步,不是去背 RFC 文档,而是先跑通一个能收发数据的程序。Python 的 socket 模块就是最好的入门工具。今天我们就把它拆开揉碎了讲清楚。
3.1 socket 模块:网络通信的「电话机」
Python 的 socket 模块,说白了就是对操作系统网络接口的一层封装。你不需要去管底层的系统调用有多复杂,只需要知道怎么「拨号」和「接听」就行。
我刚开始做网络编程时,总觉得这玩意儿很神秘。后来在项目里用 UDP 做设备日志采集,才发现它其实就是个文件描述符——你往里面写数据,内核帮你发出去;你从里面读数据,内核把收到的包递给你。
核心概念:UDP 套接字是无连接的。你不需要像 TCP 那样先建立连接,直接发数据就行。这既是优点(低延迟),也是缺点(不可靠)。
3.2 socket():创建你的第一个 UDP 套接字
创建套接字,用的是 socket.socket() 函数。它的签名长这样:
import socket
# 创建一个 UDP 套接字
udp_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
这里有两个参数:
- AF_INET:指定使用 IPv4 地址族。如果你要用 IPv6,就换成
AF_INET6。 - SOCK_DGRAM:指定使用数据报套接字,也就是 UDP。如果是 TCP,这里会写
SOCK_STREAM。
我记得有一次,同事把 SOCK_DGRAM 写成了 SOCK_STREAM,结果程序一直报错,查了半天才发现是套接字类型搞错了。嗯,这种低级错误,谁还没犯过呢?
小技巧:创建套接字后,可以用 udp_sock.type 检查类型。如果返回值是 socket.SOCK_DGRAM,说明你创建对了。
3.3 bind():给套接字绑定一个「门牌号」
套接字创建好了,但它还没有地址。就像一部电话机,你得给它分配一个号码,别人才能打进来。bind() 就是干这个的。
# 绑定到本地地址和端口
udp_sock.bind(('0.0.0.0', 8888))
参数是一个元组:(IP地址, 端口号)。
- IP 地址:
'0.0.0.0'表示监听所有网络接口。如果你只想监听某个特定 IP,比如'192.168.1.100',也可以写上去。 - 端口号:范围是 0-65535。注意,1024 以下的端口需要管理员权限才能绑定。
我在项目中遇到过一个问题:程序启动时 bind() 报 OSError: [Errno 98] Address already in use。这是因为上次程序退出后,端口还没释放。解决方案是在 bind() 之前加上 setsockopt():
udp_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
udp_sock.bind(('0.0.0.0', 8888))
注意:如果你只是发送数据(客户端模式),可以不调用 bind()。系统会自动分配一个临时端口。但如果你想接收数据,就必须 bind() 到一个固定端口。
3.4 sendto():把数据扔出去
UDP 发送数据用的是 sendto()。它的参数很简单:数据和目标地址。
message = b'Hello, UDP Server!'
server_addr = ('192.168.1.10', 8888)
sent = udp_sock.sendto(message, server_addr)
这里有几个细节要注意:
- 数据必须是字节串:字符串需要先编码,比如
'你好'.encode('utf-8')。 - 返回值是发送的字节数:但 UDP 是数据报协议,要么全发出去,要么发不出去。所以返回值通常等于数据长度。
- 目标地址格式:和
bind()一样,是一个(IP, 端口)元组。
你想想看,sendto() 其实挺「佛系」的——它把数据交给内核就不管了。至于数据能不能到达对端,它不保证。这也是 UDP 和 TCP 最大的区别。
避坑指南:我曾经在项目中用 sendto() 发送超过 65507 字节的数据(UDP 数据报的理论上限),结果数据被截断了。后来才知道,UDP 单个数据报最大长度是 65535 字节,减去 IP 头(20 字节)和 UDP 头(8 字节),实际可用只有 65507 字节。超过这个值,数据会被静默丢弃或分片,分片后只要丢一片,整个数据报就废了。
3.5 recvfrom():等着收数据
接收数据用的是 recvfrom()。它会阻塞当前线程,直到收到一个数据报。
data, client_addr = udp_sock.recvfrom(1024)
print(f'收到来自 {client_addr} 的消息:{data.decode("utf-8")}')
参数 1024 是缓冲区大小,单位是字节。如果收到的数据报超过这个大小,多余的部分会被丢弃。所以,缓冲区要设得足够大。
返回值有两个:
- data:收到的数据,字节串类型。
- client_addr:发送方的地址,格式是
(IP, 端口)。
我建议在实际项目中,缓冲区至少设到 4096 或 8192。如果你不确定数据大小,可以设成 65507——UDP 数据报的最大值。
小技巧:如果你不想让 recvfrom() 一直阻塞,可以用 settimeout() 设置超时:
udp_sock.settimeout(5.0) # 5 秒超时
try:
data, addr = udp_sock.recvfrom(1024)
except socket.timeout:
print('超时了,没收到数据')
3.6 完整示例:一个简单的 UDP 回显程序
光说不练假把式。我们来写一个完整的 UDP 回显服务器和客户端。服务器收到什么,就原样返回什么。
UDP 服务器端:
import socket
# 创建 UDP 套接字
server_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 允许端口复用
server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 绑定到所有接口的 8888 端口
server_sock.bind(('0.0.0.0', 8888))
print('UDP 服务器已启动,等待数据...')
while True:
# 接收数据,缓冲区 4096 字节
data, client_addr = server_sock.recvfrom(4096)
print(f'收到来自 {client_addr} 的消息:{data.decode("utf-8")}')
# 原样返回
server_sock.sendto(data, client_addr)
UDP 客户端:
import socket
# 创建 UDP 套接字
client_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 服务器地址
server_addr = ('127.0.0.1', 8888)
# 发送数据
message = '你好,UDP 服务器!'
client_sock.sendto(message.encode('utf-8'), server_addr)
print(f'已发送:{message}')
# 接收回显
data, _ = client_sock.recvfrom(4096)
print(f'收到回显:{data.decode("utf-8")}')
# 关闭套接字
client_sock.close()
先启动服务器,再运行客户端。你会看到客户端打印出「收到回显:你好,UDP 服务器!」。嗯,就是这么简单。
3.7 核心 API 对比
为了方便你记忆,我把这几个 API 的关键点整理成了一张表:
| API | 作用 | 关键参数 | 返回值 |
|---|---|---|---|
socket() |
创建套接字 | 地址族、套接字类型 | 套接字对象 |
bind() |
绑定地址和端口 | (IP, 端口) 元组 | 无 |
sendto() |
发送数据报 | 数据(字节串), 目标地址 | 发送的字节数 |
recvfrom() |
接收数据报 | 缓冲区大小(字节) | (数据, 发送方地址) |
3.8 知识体系总览
下面这张图,把 UDP 编程的核心流程和 API 调用关系串起来了。你可以把它当作一张「地图」,写代码时对照着看:
从这张图可以看得很清楚:客户端和服务器都从 socket() 开始,但服务器多了一步 bind()。客户端直接 sendto() 发数据,然后 recvfrom() 等回复;服务器则是先 bind(),再 recvfrom() 等数据,收到后处理并 sendto() 回去。
这个流程,说白了就是 UDP 编程的「骨架」。你把这个骨架记住了,后面再学多线程、超时重传、数据校验这些高级话题,就只是往骨架上添肉而已。
我的建议:初学者可以先把这个回显程序跑通,然后试着改一改——比如改成客户端连续发多条消息,或者服务器把收到的消息广播给所有客户端。动手改代码,比看十遍文档都管用。