Python Socket 编程入门:socket 模块、UDP 收发与端口绑定
说实话,很多初学者一上来就被「Socket」这个词唬住了。其实它没那么玄乎——说白了,Socket 就是操作系统提供的一个网络编程接口。你把它想象成一个「管道口」就行:一端连着你的程序,另一端连着网络协议栈。你往管道里扔数据,系统帮你发出去;别人发来的数据,也从这个管道口递到你手里。
我最早接触 Socket 是在大学做课程设计的时候,当时死活搞不懂 bind 和 connect 的区别。后来在项目里写了一个日志采集代理,天天跟 UDP 打交道,才慢慢摸透了这些基础操作。今天咱们就把这块彻底讲清楚。
socket 模块:Python 网络编程的起点
Python 标准库里的 socket 模块,封装了几乎所有常见的网络操作。你不需要关心底层是 TCP 还是 UDP,只需要告诉它「我要用哪种协议」就行。
创建一个 UDP Socket 的代码,简单到让你怀疑人生:
import socket
# 创建一个 UDP Socket
udp_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
这里有两个参数:
- AF_INET:指定使用 IPv4 地址族。如果你要用 IPv6,就换成
AF_INET6。 - SOCK_DGRAM:指定数据报套接字,也就是 UDP。如果是 TCP,这里用
SOCK_STREAM。
嗯,就这么两行,一个 UDP Socket 就活了。我习惯把创建 Socket 的代码放在脚本最前面,跟导入语句挨着,方便一眼看到网络配置。
udp_sock.settimeout(5.0) 设置超时时间。这样 recvfrom 阻塞时不会卡死,5 秒没数据就抛异常。我在写监控脚本时几乎必加这一行。
绑定端口:让数据能找到你的程序
Socket 创建好了,但它还没「落户」。你得告诉操作系统:这个 Socket 要监听哪个 IP 和端口。这个过程就叫绑定(bind)。
# 绑定到本机的 8888 端口
udp_sock.bind(('0.0.0.0', 8888))
bind 接收一个元组,格式是 (IP地址, 端口号)。
- IP 地址:填
'0.0.0.0'表示监听所有网络接口。如果你只想收来自某个网卡的数据,就填对应的 IP,比如'192.168.1.100'。 - 端口号:范围是 0~65535。注意,1024 以下的端口是系统保留的,普通用户没权限绑定。我一般用 8000~9000 之间的端口,不容易跟系统服务冲突。
OSError: [Errno 98] Address already in use。我曾经在调试时忘了关上一个进程,折腾了十分钟才反应过来。
绑定之后,这个 Socket 就「钉」在端口上了。任何发往这个端口的数据,系统都会递交给你的程序。
发送数据:把消息扔到网络上
UDP 发送数据用 sendto 方法。它不需要提前建立连接,直接指定目标地址就能发。
# 发送数据到 192.168.1.10:9999
message = b'Hello, UDP Server!'
udp_sock.sendto(message, ('192.168.1.10', 9999))
注意几个细节:
- 数据必须是字节串:字符串要转成 bytes,用
b'...'或者'...'.encode()。 - 目标地址是元组:格式跟 bind 一样,
(IP, 端口)。 - UDP 不保证送达:sendto 成功返回只代表数据交给了系统,不代表对方收到了。我在日志采集系统里见过太多丢包的情况,所以后来都加了应用层的确认机制。
发送前最好检查一下数据大小。UDP 单包最大能发 65535 字节,但实际网络链路可能限制在 1500 字节左右(MTU)。超过 MTU 的数据会被分片,分片丢失会导致整个包重传。我个人的经验是:单包控制在 1400 字节以内最稳妥。
接收数据:等着别人来找你
接收数据用 recvfrom 方法。它会阻塞当前线程,直到收到数据为止。
# 接收数据,最多接收 1024 字节
data, addr = udp_sock.recvfrom(1024)
print(f'收到来自 {addr} 的消息: {data.decode()}')
recvfrom 返回两个值:
- data:收到的字节串。
- addr:发送方的地址,格式是
(IP, 端口)。
你想想看,UDP 是无连接的,所以每次 recvfrom 都可能来自不同的客户端。这个特性在日志收集场景下特别有用——多个设备同时发日志,你不需要为每个设备维护一个连接。
完整示例:一个简单的 UDP 收发程序
把上面的知识点串起来,写一个能收能发的 UDP 工具:
import socket
def udp_server():
# 创建 UDP Socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 绑定端口
sock.bind(('0.0.0.0', 8888))
print('UDP 服务器已启动,监听端口 8888...')
while True:
# 接收数据
data, addr = sock.recvfrom(1024)
print(f'收到: {data.decode()} 来自 {addr}')
# 回复客户端
response = f'已收到你的消息: {data.decode()}'
sock.sendto(response.encode(), addr)
if __name__ == '__main__':
udp_server()
这个程序启动后会一直运行,收到消息就打印出来,再回复一个确认。你可以用另一个终端发消息测试:
# 用 nc 命令发送 UDP 数据
echo "Hello" | nc -u 127.0.0.1 8888
或者用 Python 写一个简单的发送端:
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.sendto(b'Test message', ('127.0.0.1', 8888))
data, addr = sock.recvfrom(1024)
print(data.decode())
sock.close()
Socket 生命周期与资源管理
Socket 用完之后要记得关闭,释放系统资源。虽然 Python 的垃圾回收会帮你做,但显式关闭是专业习惯。
sock.close()
更好的做法是用 with 语句,自动管理生命周期:
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
sock.bind(('0.0.0.0', 8888))
data, addr = sock.recvfrom(1024)
print(data)
# 退出 with 块后,socket 自动关闭
我在生产环境里都用 with 语句,省心,不会漏掉 close。
本章知识体系总览
下面这张图把 UDP Socket 的核心操作串起来了,你可以对照着理解:
常见问题与避坑指南
| 问题 | 原因 | 解决办法 |
|---|---|---|
| bind 时报 Address already in use | 端口被其他进程占用 | 换端口,或者用 sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 允许端口重用 |
| recvfrom 一直阻塞 | 没有数据到达,或者防火墙拦截了 UDP 包 | 设置超时 sock.settimeout(5),检查防火墙规则 |
| sendto 返回成功但对方没收到 | UDP 本身不保证送达,可能丢包 | 在应用层加确认机制,或者改用 TCP |
| 收到的数据乱码 | 编码不一致,发送方用 GBK,接收方用 UTF-8 | 统一编码,推荐都用 UTF-8 |