4. UDP 客户端实现:socket()、sendto()、recvfrom()、close()
好,咱们今天来动手写 UDP 客户端。说实话,UDP 客户端比 TCP 简单太多了。你想想看,TCP 要三次握手、要维护连接状态、还要处理各种拥塞控制——UDP 呢?直接扔数据包就完事了。
我刚开始学网络编程那会儿,总觉得 UDP 太「简陋」了,心里不踏实。后来在项目中用 UDP 做实时视频传输,才发现这种「简陋」恰恰是它的优势。嗯,咱们今天就把这四个核心 API 一个一个掰开揉碎了讲。
4.1 socket()——创建通信的「门」
任何网络通信的第一步,都是创建 socket。你可以把它理解成在操作系统里开了一扇门,数据就从这扇门进出。
import socket
# 创建一个 UDP socket
client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
这里有两个参数,我解释一下:
- AF_INET:指定使用 IPv4 地址。你如果要用 IPv6,就换成 AF_INET6。
- SOCK_DGRAM:指定使用数据报协议,说白了就是 UDP。如果是 TCP,这里就是 SOCK_STREAM。
4.2 sendto()——把数据扔出去
UDP 的发送函数叫 sendto(),它跟 TCP 的 send() 最大的区别是——你得告诉它「发给谁」。
server_address = ('127.0.0.1', 8888)
message = b'Hello, UDP Server!'
# 发送数据
client_socket.sendto(message, server_address)
注意看,sendto() 的第一个参数是数据,必须是 bytes 类型。第二个参数是一个元组,包含目标 IP 和端口。
为什么会这样?因为 UDP 是无连接的。你每次发数据都要指定目的地,不像 TCP 那样建立连接后只管发就行。
.encode() 转成 bytes,或者像我上面那样在字符串前面加个 b。
4.3 recvfrom()——等着收数据
发完数据,客户端通常要等服务器回个话。recvfrom() 就是干这个的。
# 接收数据,缓冲区大小设为 1024 字节
data, server = client_socket.recvfrom(1024)
print(f'收到来自 {server} 的消息:{data.decode()}')
recvfrom() 返回两个值:
- data:接收到的数据,还是 bytes 类型
- server:发送方的地址,是一个 (ip, port) 元组
你想想看,为什么 UDP 要返回发送方地址?因为 UDP 是无连接的,同一时刻可能有多台机器给你发数据,你得知道是谁发的。
缓冲区大小怎么选?
我一般用 1024 或 4096。UDP 数据包最大也就 65535 字节,但实际网络环境里超过 1500 字节就可能被分片。如果你不确定数据大小,可以设大一点,比如 65535。
4.4 close()——用完记得关门
这个最简单,但也最容易忘。
client_socket.close()
关闭 socket 会释放系统资源,包括端口号。如果你不关,程序退出后端口可能还被占用一会儿。
我记得有一次写测试脚本,循环创建 socket 但不关闭,结果跑了几百次之后系统报「Address already in use」。嗯,从那以后我每次写完都检查有没有 close()。
4.5 完整客户端代码
来,咱们把上面这些拼起来,写一个完整的 UDP 客户端:
import socket
def udp_client():
# 1. 创建 socket
client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
server_address = ('127.0.0.1', 8888)
try:
# 2. 发送数据
message = b'Hello, UDP Server!'
print(f'发送:{message}')
client_socket.sendto(message, server_address)
# 3. 接收响应
data, server = client_socket.recvfrom(1024)
print(f'收到:{data.decode()} 来自 {server}')
finally:
# 4. 关闭 socket
client_socket.close()
print('客户端已关闭')
if __name__ == '__main__':
udp_client()
这段代码的逻辑很清楚:创建 socket → 发数据 → 收响应 → 关闭。你把这个跑起来,前提是服务器已经在 8888 端口等着了。
4.6 核心流程一览
我把 UDP 客户端的完整流程画了张图,你一看就明白:
4.7 常见问题与避坑
| 问题 | 原因 | 解决办法 |
|---|---|---|
| sendto 报错 TypeError | 数据不是 bytes 类型 | 用 .encode() 或 b'' 转成 bytes |
| recvfrom 一直卡住 | 服务器没发数据回来 | 检查服务器是否在运行,或者设置超时 |
| Address already in use | 上次的 socket 没关 | 确保调用了 close(),或者用 SO_REUSEADDR |
| 数据乱码 | 编码不一致 | 发送和接收用同一种编码,推荐 UTF-8 |
client_socket.settimeout(5.0) # 5 秒后超时,抛出 socket.timeout 异常
好了,UDP 客户端的四个核心 API 就这些。说白了就是:开门、扔数据、收数据、关门。你把这个流程记熟了,后面写 UDP 服务器的时候会发现,其实服务器端也是差不多的套路。