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)

就这一行,门就打开了。

小提示: 我习惯把socket创建放在函数外面,作为模块级变量。但如果你写多线程程序,记得每个线程用独立的socket,别共享——UDP虽然无连接,但socket对象本身不是线程安全的。

用完记得关。虽然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字符串, 端口号)
  • 没有"对方是否收到"的确认——扔出去就不管了
我曾经踩过的坑: 有一次我忘了把字符串转bytes,直接传了个str进去,结果Python报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()
核心流程总结:
  1. 服务器先bind绑定端口,然后recvfrom等着
  2. 客户端sendto发数据,不需要提前建立连接
  3. 服务器收到后,用sendto把数据原路返回
  4. 客户端recvfrom收回复

整个过程,服务器和客户端之间没有"握手",没有"连接状态"。这就是UDP——简单、直接、但不可靠。

3.4 图解UDP通信流程

光看代码可能还不够直观。我画了一张图,把整个流程串起来。你一看就明白了。

UDP回显服务器通信流程 客户端 服务器 1. 创建socket 2. sendto(数据, 服务器地址) 3. recvfrom(1024) 等待回复 4. 处理收到的回显数据 1. 创建socket 2. bind(0.0.0.0, 8888) 3. recvfrom(1024) 阻塞等待 4. sendto(数据, 客户端地址) 数据报 回显数据报 注意: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抓包确认。

嗯,这一章的内容就这些。代码不多,但都是基本功。你把这个回显服务器跑通了,后面讲超时控制、数据校验、多线程并发,你才有底气继续往下走。