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。
我的习惯:创建 socket 时最好用 with 语句,或者至少记住要 close()。我在早期项目里吃过亏,socket 没关导致端口一直被占用,调试了半天才发现。

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 客户端的完整流程画了张图,你一看就明白:

UDP 客户端核心流程 1. import socket 2. socket(AF_INET, SOCK_DGRAM) 3. sendto(data, address) 4. recvfrom(buffer_size) 5. close() 关键点 无连接,直接发 每次都要指定目标 记得 close() 返回值 sendto → 发送字节数 recvfrom → (data, addr) data 是 bytes 类型

4.7 常见问题与避坑

问题 原因 解决办法
sendto 报错 TypeError 数据不是 bytes 类型 用 .encode() 或 b'' 转成 bytes
recvfrom 一直卡住 服务器没发数据回来 检查服务器是否在运行,或者设置超时
Address already in use 上次的 socket 没关 确保调用了 close(),或者用 SO_REUSEADDR
数据乱码 编码不一致 发送和接收用同一种编码,推荐 UTF-8
设置超时的小技巧:如果你不想让 recvfrom 无限期阻塞,可以设置超时:
client_socket.settimeout(5.0) # 5 秒后超时,抛出 socket.timeout 异常

好了,UDP 客户端的四个核心 API 就这些。说白了就是:开门、扔数据、收数据、关门。你把这个流程记熟了,后面写 UDP 服务器的时候会发现,其实服务器端也是差不多的套路。