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 的代码放在脚本最前面,跟导入语句挨着,方便一眼看到网络配置。

小技巧:创建 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 之间的端口,不容易跟系统服务冲突。
注意:端口不能重复绑定。如果你运行了两个脚本都绑定 8888 端口,第二个会报 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 都可能来自不同的客户端。这个特性在日志收集场景下特别有用——多个设备同时发日志,你不需要为每个设备维护一个连接。

核心区别:TCP 的 recv 只收数据,不返回地址;UDP 的 recvfrom 会告诉你「谁发的」。这个差异决定了 UDP 天然适合一对多的场景。

完整示例:一个简单的 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 的核心操作串起来了,你可以对照着理解:

UDP Socket 核心操作流程 socket.socket() bind(('0.0.0.0', 8888)) 发送 or 接收? sendto(data, addr) recvfrom(bufsize) 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
我曾经踩过的坑:有一次写日志收集程序,recvfrom 总是收不到数据,查了半天发现是云服务器的安全组没开放 UDP 端口。UDP 不像 TCP 有握手过程,端口不通时没有任何错误提示,数据直接静默丢弃。所以调试 UDP 程序时,先用 tcpdump 或 Wireshark 抓包确认数据是否到达了本机。