Python Socket编程入门:socket模块、创建UDP Socket、绑定端口、收发数据
上一章我们聊了UDP协议的基本原理。说白了,UDP就是“发出去就不管了”的传输方式。但光懂理论没用,得动手写代码才行。
这一章,我们就用Python把UDP通信跑起来。我当年第一次写UDP程序时,以为要搞一堆底层配置,结果发现Python的socket模块把大部分细节都封装好了。你想想看,几行代码就能让两台机器互相发数据,是不是挺爽?
socket模块:Python网络编程的瑞士军刀
Python标准库里的socket模块,说白了就是对操作系统Socket API的封装。它提供了创建套接字、绑定地址、收发数据等核心功能。
我个人习惯,写网络程序第一件事就是:
import socket
就这么一行。但别小看它,背后涉及的东西可不少。
创建UDP Socket
创建UDP套接字,核心就是调用socket.socket()函数。你需要指定两个参数:
- 地址族:
socket.AF_INET表示IPv4,socket.AF_INET6表示IPv6 - 套接字类型:
socket.SOCK_DGRAM表示UDP,socket.SOCK_STREAM表示TCP
代码很简单:
import socket
# 创建一个IPv4的UDP套接字
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
print("UDP Socket创建成功!")
print(f"文件描述符: {udp_socket.fileno()}")
运行后你会看到类似输出:
UDP Socket创建成功!
文件描述符: 3
绑定端口:让数据能找到你
创建了Socket之后,它就像一个没有门牌号的邮箱。别人想给你发数据,得知道你的地址和端口号。绑定端口就是给这个邮箱贴上地址标签。
绑定使用bind()方法,参数是一个元组(IP地址, 端口号):
import socket
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 绑定到本机的8888端口
local_addr = ('0.0.0.0', 8888)
udp_socket.bind(local_addr)
print(f"已绑定到 {local_addr[0]}:{local_addr[1]}")
这里有几个关键点:
- IP地址填
'0.0.0.0':表示监听本机所有网络接口。如果你只想监听某个特定IP,比如内网地址'192.168.1.100',也可以指定。 - 端口号建议大于1024:0-1023是系统保留端口,需要root权限才能绑定。我建议用8000-9000之间的端口,不容易冲突。
- 端口号不能重复:同一个端口在同一台机器上只能被一个程序绑定。如果提示
Address already in use,说明端口被占用了。
OSError: [Errno 98] Address already in use。这是因为操作系统在Socket关闭后,会进入TIME_WAIT状态,持续一段时间。解决方案是在bind()之前设置SO_REUSEADDR选项:udp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)。
发送数据:把消息扔进网络
UDP发送数据用sendto()方法。它需要两个参数:要发送的数据(bytes类型)和目标地址。
import socket
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 目标地址:本机的9999端口
dest_addr = ('127.0.0.1', 9999)
message = "Hello, UDP!".encode('utf-8')
# 发送数据
sent_len = udp_socket.sendto(message, dest_addr)
print(f"发送了 {sent_len} 字节数据到 {dest_addr}")
注意几个细节:
- 数据必须是bytes类型:字符串需要
.encode(),接收时用.decode()还原。 - sendto()返回发送的字节数:UDP不保证数据一定到达,所以这个返回值只是告诉你“本地发送了多少”,不代表对方收到了。
- 目标地址可以是任意IP:不限于本机,只要网络可达就行。
接收数据:等着消息上门
接收数据用recvfrom()方法。它会阻塞程序,直到收到数据为止。参数是缓冲区大小,单位是字节。
import socket
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
udp_socket.bind(('0.0.0.0', 9999))
print("等待接收数据...")
# 接收数据,缓冲区设为1024字节
data, client_addr = udp_socket.recvfrom(1024)
print(f"收到来自 {client_addr} 的数据:{data.decode('utf-8')}")
recvfrom()返回一个元组:
- data:接收到的原始字节数据
- client_addr:发送方的地址,格式为
(IP, 端口)
recvfrom()是一次性的。它只接收一个数据报,不会像TCP那样处理流式数据。如果发送方发了1000字节,你设的缓冲区是512字节,那recvfrom()只会返回前512字节,剩下的数据就丢了。所以缓冲区大小要足够大,我一般设65535(UDP数据报最大长度)。
完整示例:UDP回显程序
把上面的知识点串起来,写一个简单的UDP回显程序。客户端发什么,服务端就原样返回什么。
服务端代码
import socket
# 创建UDP Socket
server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 绑定到本机8888端口
server_socket.bind(('0.0.0.0', 8888))
print("UDP服务端已启动,监听端口8888...")
while True:
# 接收数据
data, client_addr = server_socket.recvfrom(65535)
message = data.decode('utf-8')
print(f"收到来自 {client_addr} 的消息:{message}")
# 原样返回
server_socket.sendto(data, client_addr)
print(f"已回显给 {client_addr}")
客户端代码
import socket
# 创建UDP Socket
client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 服务端地址
server_addr = ('127.0.0.1', 8888)
while True:
# 输入消息
message = input("请输入要发送的消息(输入exit退出):")
if message == 'exit':
break
# 发送数据
client_socket.sendto(message.encode('utf-8'), server_addr)
print(f"已发送:{message}")
# 接收回显
data, _ = client_socket.recvfrom(65535)
print(f"收到回显:{data.decode('utf-8')}")
运行步骤:先启动服务端,再启动客户端。客户端输入内容,服务端收到后原样返回。
核心流程总结
UDP Socket编程的流程,我用一张图帮你理清楚:
这张图展示了UDP通信的完整流程。注意看,服务端必须先bind(),客户端可以不绑定,系统会自动分配一个临时端口。数据流向是双向的,但UDP本身没有连接的概念,每次sendto()都是独立的。
常见问题与避坑
| 问题 | 原因 | 解决方案 |
|---|---|---|
Address already in use |
端口被占用或处于TIME_WAIT状态 | 设置SO_REUSEADDR选项,或更换端口 |
OSError: [Errno 9] Bad file descriptor |
Socket已被关闭,但还在使用 | 检查代码逻辑,确保Socket在使用期间保持打开 |
| 接收数据为空或乱码 | 编码/解码不一致,或缓冲区太小 | 统一使用utf-8编码,缓冲区设为65535 |
| 数据丢失 | UDP不保证可靠传输 | 应用层添加确认机制,或改用TCP |
sendto()和recvfrom()前后加打印日志,记录发送/接收的字节数和地址。这样一旦出问题,能快速定位是发送端还是接收端的锅。另外,netstat -an | grep 8888可以查看端口是否在监听。
嗯,这一章的内容就到这里。你学会了创建UDP Socket、绑定端口、发送和接收数据。这些是UDP编程的基石,后面的章节我们会在这个基础上,逐步构建更复杂的应用。