3、Python基础UDP通信实现:socket模块基础、sendto/recvfrom方法、简单回显服务器
好,咱们正式开始动手写代码了。
前面两章我们把UDP的原理和风险聊透了。这一章,我带你亲手实现一个最基础的UDP通信。说白了,就是让两台机器(或者本机两个程序)通过UDP协议互相扔数据包。
你可能会觉得,不就是调几个API吗?嗯,确实不难。但我在项目里见过太多人栽在细节上——端口没绑对、数据没编解码、缓冲区设太小……这些坑,咱们今天一次性踩明白。
3.1 socket模块:网络通信的"门把手"
Python里做网络通信,绕不开socket模块。它封装了操作系统底层的socket接口。你可以把它想象成一扇门——你要跟别人通信,总得先有一扇门吧?
创建一个UDP socket,代码极其简单:
import socket
# 创建一个UDP套接字
# AF_INET 表示使用IPv4地址
# SOCK_DGRAM 表示使用UDP协议(数据报套接字)
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
就这一行,门就打开了。
用完记得关。虽然Python的垃圾回收会帮你收,但显式关闭是个好习惯:
udp_socket.close()
或者用with语句,更省心:
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
# 在这里使用sock
pass # 退出with块时自动关闭
3.2 sendto 和 recvfrom:UDP通信的"扔"与"接"
UDP没有连接的概念。你想发数据,直接扔过去就行。对方想收数据,得先准备好一个"篮子"接着。
这两个方法就是干这个的。
3.2.1 sendto:把数据扔出去
# sendto(data, address)
# data 必须是 bytes 类型
# address 是一个 (ip, port) 元组
udp_socket.sendto(b"Hello, UDP!", ("127.0.0.1", 8888))
注意三个要点:
- 数据必须是bytes——字符串要先用
.encode()转一下 - 地址是元组——(IP字符串, 端口号)
- 没有"对方是否收到"的确认——扔出去就不管了
TypeError: a bytes-like object is required。排查了半天,就少了个.encode()。所以,发送前一定确认数据类型。
3.2.2 recvfrom:接住飞来的数据
# recvfrom(bufsize)
# bufsize 指定最大接收字节数
# 返回 (data, address) 元组
data, addr = udp_socket.recvfrom(1024)
print(f"收到来自 {addr} 的消息:{data.decode()}")
recvfrom会阻塞,直到收到数据。返回的addr是发送方的地址和端口,这个信息很重要——你要回消息就得靠它。
缓冲区大小怎么设?我一般用1024(1KB)。UDP数据报最大理论值是65535字节,但实际网络链路层限制通常是1500字节左右。如果你要传大文件,别用UDP,或者自己实现分片和重组——那又是另一门课了。
| 方法 | 作用 | 参数 | 返回值 |
|---|---|---|---|
sendto(data, addr) |
发送数据报 | data: bytes, addr: (ip, port) | 发送的字节数 |
recvfrom(bufsize) |
接收数据报 | bufsize: int | (data: bytes, addr: (ip, port)) |
3.3 简单回显服务器:让数据"转一圈"
理论讲完了,咱们写个能跑的东西。
回显服务器(Echo Server)是网络编程里的"Hello World"。它收到什么,就原样返回什么。虽然简单,但能完整展示UDP通信的全流程。
3.3.1 服务器端代码
import socket
def udp_echo_server(host="0.0.0.0", port=8888):
"""
UDP回显服务器
监听所有网卡,收到消息后原样返回
"""
# 1. 创建socket
server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 2. 绑定地址和端口
server_socket.bind((host, port))
print(f"[服务器] 已启动,监听 {host}:{port}")
try:
while True:
# 3. 接收数据
data, client_addr = server_socket.recvfrom(1024)
message = data.decode("utf-8")
print(f"[收到] 来自 {client_addr}:{message}")
# 4. 原样返回
server_socket.sendto(data, client_addr)
print(f"[回显] 已发送回 {client_addr}")
except KeyboardInterrupt:
print("\n[服务器] 正在关闭...")
finally:
server_socket.close()
print("[服务器] 已关闭")
if __name__ == "__main__":
udp_echo_server()
3.3.2 客户端代码
import socket
def udp_echo_client(server_host="127.0.0.1", server_port=8888):
"""
UDP回显客户端
发送用户输入,接收服务器回显
"""
client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
while True:
message = input("请输入要发送的消息(输入exit退出):")
if message.lower() == "exit":
break
# 1. 发送数据
client_socket.sendto(message.encode("utf-8"), (server_host, server_port))
print(f"[发送] 已发送到 {server_host}:{server_port}")
# 2. 接收回显
# 注意:这里设置了超时,防止服务器没响应时卡死
client_socket.settimeout(5.0)
try:
data, server_addr = client_socket.recvfrom(1024)
print(f"[收到] 来自服务器 {server_addr} 的回显:{data.decode('utf-8')}")
except socket.timeout:
print("[超时] 服务器没有响应")
finally:
client_socket.close()
print("[客户端] 已关闭")
if __name__ == "__main__":
udp_echo_client()
- 服务器先
bind绑定端口,然后recvfrom等着 - 客户端
sendto发数据,不需要提前建立连接 - 服务器收到后,用
sendto把数据原路返回 - 客户端
recvfrom收回复
整个过程,服务器和客户端之间没有"握手",没有"连接状态"。这就是UDP——简单、直接、但不可靠。
3.4 图解UDP通信流程
光看代码可能还不够直观。我画了一张图,把整个流程串起来。你一看就明白了。
3.5 运行与测试
先启动服务器,再启动客户端。顺序无所谓——UDP不要求服务器先启动,但你先启动客户端的话,它发出去的数据没人收,就丢了。
运行效果大概是这样:
# 终端1:服务器
$ python udp_echo_server.py
[服务器] 已启动,监听 0.0.0.0:8888
[收到] 来自 ('127.0.0.1', 54321):你好
[回显] 已发送回 ('127.0.0.1', 54321)
[收到] 来自 ('127.0.0.1', 54321):hello
[回显] 已发送回 ('127.0.0.1', 54321)
# 终端2:客户端
$ python udp_echo_client.py
请输入要发送的消息(输入exit退出):你好
[发送] 已发送到 127.0.0.1:8888
[收到] 来自服务器 ('127.0.0.1', 8888) 的回显:你好
请输入要发送的消息(输入exit退出):hello
[发送] 已发送到 127.0.0.1:8888
[收到] 来自服务器 ('127.0.0.1', 8888) 的回显:hello
sendto后,操作系统会自动分配一个临时端口。所以你在服务器端看到的客户端端口(比如54321)每次运行都可能不一样。这是正常的。
3.6 常见问题与避坑
- 端口被占用: 如果
bind时报OSError: [Errno 98] Address already in use,说明端口被占用了。换一个端口,或者等一会儿再试。我一般用netstat -tuln | grep 8888查一下谁在用。 - 数据没收到: 先检查防火墙。Linux下用
ufw status,Windows检查防火墙规则。UDP没有重传,丢了就是丢了。 - 中文乱码: 确保发送和接收都用同样的编码。我统一用
utf-8,省心。 - recvfrom阻塞不返回: 检查客户端是不是真的发了数据。可以用
tcpdump或Wireshark抓包确认。
嗯,这一章的内容就这些。代码不多,但都是基本功。你把这个回显服务器跑通了,后面讲超时控制、数据校验、多线程并发,你才有底气继续往下走。