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
小提示:文件描述符是操作系统用来管理打开文件的整数标识。在Linux系统中,一切皆文件,Socket也不例外。0、1、2分别对应标准输入、标准输出、标准错误,所以新创建的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, 端口)
核心要点:UDP的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 Socket编程核心流程 服务端 客户端 1. socket() 创建UDP Socket 2. bind() 绑定IP和端口 3. recvfrom() 接收数据 4. sendto() 发送响应 5. close() 关闭Socket 1. socket() 创建UDP Socket 2. sendto() 发送数据 3. recvfrom() 接收响应 4. close() 关闭Socket 客户端可以不bind() 系统自动分配临时端口

这张图展示了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编程的基石,后面的章节我们会在这个基础上,逐步构建更复杂的应用。