← 返回Python标准库精讲目录
← 返回学习笔记首页
专题: Python标准库精讲系统学习
关键词: Python, 标准库, socket, 套接字, TCP, UDP, 网络编程, bind, listen, accept, connect, send, recv
一、socket概述 — 网络编程基础与C/S架构
socket (套接字)是网络编程中最基础、最核心的抽象概念。它提供的编程接口允许运行在不同主机(或同一主机)上的两个进程通过网络交换数据。在Python中,socket模块是对BSD Socket API的封装,在标准库中位于Lib/socket.py,历经二十多年发展,已成为Python网络编程的基石。
从操作系统角度看socket
在操作系统层面,socket是网络通信链路的一个抽象句柄(file descriptor)。应用程序通过读写socket来收发网络数据,接口风格遵循UNIX"一切皆文件"的哲学——创建(socket)、绑定(bind)、监听(listen)、接受(accept)、读写(send/recv)、关闭(close)这一完整的生命周期,与文件操作高度一致。
客户端-服务器架构
绝大多数网络应用都采用C/S(Client/Server,客户端-服务器)架构,socket编程正是围绕这种模型设计的。其基本工作流程如下:
服务器端先启动,创建socket后绑定到固定的IP地址和端口,进入监听状态,等待客户端发起连接
客户端由用户操作触发,创建socket后主动向服务器地址发起连接请求
服务器接受客户端的连接请求,建立一条双向通信链路,双方开始数据交换
数据交换完成后,双方各自关闭连接,释放资源
这种模型的上限性能取决于服务器同时处理多个连接的能力。Python提供了多线程、多进程以及异步I/O等多种手段来支撑高并发场景。
socket在协议栈中的位置
在TCP/IP四层网络模型中,socket是应用层与传输层之间的编程接口。应用层程序(如HTTP、FTP、SMTP等)通过socket使用传输层的TCP或UDP服务,而socket则负责将应用数据向下传递给传输层,经过IP层、链路层最终发送到物理网络。这个位置决定了开发者通过socket可以直接操控传输层协议,而无需关心底层IP路由、MTU分片、流量控制等细节。
核心理解: socket是操作系统提供的网络编程接口,它将复杂的网络协议栈封装为简洁的文件式操作。Python的socket模块保持了与C语言Socket API高度一致的接口风格,是学习网络编程原理的最佳工具。
二、套接字创建 — socket()函数详解
所有socket编程的起点都是socket.socket()构造函数,其完整签名如下:
socket(family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None)
三个核心参数分别决定了套接字的协议族、通信类型和具体协议,理解它们之间的关系是正确编程的前提。
地址族(Address Family)
地址族决定了socket使用何种网络协议进行寻址和通信,常用的选项包括:
AF_INET (默认值):IPv4协议族,使用点分十进制IP地址(如"192.168.1.1")加16位端口号进行寻址。这是最广泛使用的地址族,适用于绝大多数互联网通信场景
AF_INET6 :IPv6协议族,使用冒号十六进制地址(如"::1")加端口号。在IPv6逐渐普及的背景下,新项目应同时支持AF_INET和AF_INET6
AF_UNIX (也称AF_LOCAL):同一台机器上的进程间通信(IPC),使用文件系统路径作为地址标识。AF_UNIX比AF_INET的环回地址方式性能更高,因为它绕过了完整的网络协议栈
套接字类型(Socket Type)
套接字类型决定了数据的传输方式和特性,这是socket编程中最关键的选择之一:
SOCK_STREAM :面向连接的字节流套接字,对应TCP协议。它提供可靠的、有序的、无重复的、面向连接的双向数据传输通道。数据被看作连续的字节流,没有边界概念。适合HTTP、SMTP、FTP等绝大多数应用层协议
SOCK_DGRAM :无连接的数据报套接字,对应UDP协议。它提供不可靠的、无序的、面向消息的数据传输。每个sendto调用对应一个独立的数据报,接收方recvfrom获取完整的消息。适合DNS查询、视频直播、在线游戏等对实时性要求高但可容忍少量丢失的场景
SOCK_RAW :原始套接字,允许直接操作IP层数据包。需要管理员/root权限,通常用于实现自定义协议或网络诊断工具(如ping命令)
proto参数与fileno参数
proto参数在大多数场景下不需要显式指定(传0即可),系统会根据family和type的组合自动选择合适的协议。例如socket(AF_INET, SOCK_STREAM)自动选择TCP,socket(AF_INET, SOCK_DGRAM)自动选择UDP。只有当同一family和type组合支持多种协议时才需要指定proto。
fileno参数用于从已有的文件描述符创建socket对象,这在进程间传递socket描述符或与底层系统接口交互时非常有用。指定fileno后,其他三个参数会被忽略。
创建套接字的典型用法
import socket
# TCP套接字(最常用)
tcp_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# UDP套接字
udp_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# IPv6 TCP套接字
tcp6_sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
# Unix域套接字(仅Linux/Unix)
unix_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
# 使用with语句自动关闭
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect(('example.com', 80))
s.sendall(b'GET / HTTP/1.1\r\nHost: example.com\r\n\r\n')
data = s.recv(4096)
print(data.decode())
参数选择原则: 互联网编程绝大多数场景用AF_INET配合SOCK_STREAM(TCP)或SOCK_DGRAM(UDP)。同机高性能IPC通信首选AF_UNIX。创建完socket后务必使用with语句或手动调用close()来释放资源。
三、TCP服务器 — bind-listen-accept通信模型
TCP服务器采用"创建-绑定-监听-接受-收发-关闭"的经典六步流程。每一个步骤都有其特定的作用和注意事项,完整的执行序列是:
socket() → bind() → listen() → accept() → recv()/send() → close()
创建套接字 绑定地址 监听端口 接受连接 数据收发 关闭
bind() — 绑定地址与端口
bind()将socket与一个特定的IP地址和端口号绑定。服务器必须绑定一个固定的端口,客户端才能找到它。地址以元组(host, port)的形式传入:
host参数 :
空字符串''或'0.0.0.0':绑定到所有可用网络接口,允许外部设备通过任何IP访问
'127.0.0.1'(环回地址):仅允许本机访问,外部设备无法连接,适合本地测试
具体的IP地址(如'192.168.1.100'):仅绑定到该特定网络接口
port参数 :0~65535的整数。1~1023为系统保留端口(需要管理员权限),1024~49151为注册端口,49152~65535为动态/私有端口。常见的Web服务端口是80(HTTP)、443(HTTPS)
listen() — 开始监听
listen()将socket从主动模式切换到被动模式,开始监听来自客户端的连接请求。它有一个可选参数backlog,指定操作系统内核为未完成连接(还未被accept处理)保留队列的最大长度。如果backlog设置过小,在高并发场景下客户端的连接请求可能被内核直接拒绝。Python中的典型值是5~128。
注意:backlog不是限制最大连接数,而是限制等待accept处理的"半连接"数量。已经accept的连接不在这个限制内。
accept() — 接受连接
accept()从等待连接队列中取出一个客户端的连接请求,创建一个新的socket对象用于与这个客户端通信,同时返回客户端的地址。原始socket(监听socket)继续监听,等待其他客户端的连接请求——这就是一个服务器能服务多个客户端的基本原理。
accept()默认是阻塞调用——如果没有客户端连接,程序会一直停留在accept()调用处,直到有新的连接到达。
recv()与send() — 数据收发
recv(bufsize) :接收客户端发送的数据。bufsize指定最大接收字节数。recv返回接收到的字节数据(bytes类型),如果客户端关闭连接,recv返回空字节串b''——这是判断客户端是否断开连接的标准方法。recv不保证一次能接收到应用层完整的消息,因此通常需要循环接收。
send(bytes) :发送数据到客户端。send返回实际发送的字节数,可能小于要发送的数据量。对于这种情况,应用程序需要记录已发送的字节数,循环直到所有数据都发送完毕。sendall(bytes) 封装了这一循环过程,确保所有数据要么全部发送成功,要么抛出异常。
完整的多客户端TCP服务器示例
import socket
HOST = '0.0.0.0' # 绑定到所有网络接口
PORT = 8888 # 服务器监听端口
BACKLOG = 5 # 等待队列长度
def handle_client(conn, addr):
"""处理单个客户端连接"""
print(f'[新连接] 客户端 {addr} 已连接')
try:
while True:
data = conn.recv(1024)
if not data:
# 客户端关闭连接
break
print(f'[收到] {addr}: {data.decode()!r}')
# 回显数据(Echo Server)
conn.sendall(b'[服务器回复] ' + data)
except ConnectionResetError:
print(f'[断开] 客户端 {addr} 异常断开')
finally:
conn.close()
print(f'[关闭] 与 {addr} 的连接已释放')
def start_server():
# 1. 创建TCP套接字
server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 2. 设置地址重用(避免"Address already in use"错误)
server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 3. 绑定地址
server_sock.bind((HOST, PORT))
print(f'[启动] 服务器已绑定 {HOST}:{PORT}')
# 4. 开始监听
server_sock.listen(BACKLOG)
print(f'[监听] 正在等待客户端连接...')
try:
while True:
# 5. 接受客户端连接(阻塞)
conn, addr = server_sock.accept()
# 为每个客户端启动一个线程处理
import threading
t = threading.Thread(target=handle_client, args=(conn, addr), daemon=True)
t.start()
print(f'[活动连接数] {threading.active_count() - 1}')
except KeyboardInterrupt:
print('\n[关闭] 服务器正在关闭...')
finally:
server_sock.close()
if __name__ == '__main__':
start_server()
服务器编程要点: ①一定要设置SO_REUSEADDR,否则服务器重启时会因为端口仍被占用而失败;②监听socket只负责接受连接,不负责数据通信;③每个accept()返回的新socket专属于一个客户端;④recv()返回空字节串是客户端断开的信号,必须正确检测;⑤生产环境应使用线程池或异步I/O而非每次创建新线程。
四、TCP客户端 — connect-send-recv通信模型
相比服务器,客户端的编程模型要简单得多,只需三步即可:创建socket、连接服务器、收发数据。客户端不需要bind(系统会自动分配临时端口),也不需要listen和accept。
connect() — 连接服务器
connect(address)将客户端socket与远程服务器建立TCP连接。address是一个(host, port)元组。connect会执行TCP三次握手:SYN → SYN-ACK → ACK。如果服务器不可达、端口未监听或网络异常,connect会抛出异常(如ConnectionRefusedError、TimeoutError等)。
connect默认也是阻塞调用,在网络状况不佳时可能长时间挂起。可以通过settimeout()或非阻塞模式控制等待时间。
数据收发
客户端使用sendall()发送请求数据,使用recv()接收服务器响应。一个常见问题是:recv返回的数据可能比预期的应用层消息短(因为TCP是流协议,没有消息边界)。因此客户端通常需要自行定义应用层的消息分帧(delimiting)策略:
固定长度 :每个消息长度固定,接收方按长度读取
分隔符 :以特殊字符(如换行符\n)标记消息结束,常见于文本协议
长度前缀 :先发送消息长度(如4字节整数),再发送消息体,接收方先读长度再读对应字节数
完整的TCP客户端示例
import socket
def tcp_client(host='127.0.0.1', port=8888):
# 1. 创建TCP套接字
client_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
# 2. 连接服务器
print(f'[连接] 正在连接 {host}:{port}...')
client_sock.connect((host, port))
print(f'[连接成功] 与 {host}:{port} 建立连接')
# 3. 发送数据
message = '你好, 服务器!'
client_sock.sendall(message.encode('utf-8'))
print(f'[发送] {message}')
# 4. 接收响应
response = client_sock.recv(4096)
print(f'[收到] {response.decode("utf-8")}')
except ConnectionRefusedError:
print(f'[错误] 服务器 {host}:{port} 拒绝连接, 请确认服务器已启动')
except TimeoutError:
print(f'[错误] 连接服务器超时')
except Exception as e:
print(f'[错误] 发生异常: {e}')
finally:
# 5. 关闭连接
client_sock.close()
print('[关闭] 连接已释放')
if __name__ == '__main__':
tcp_client()
客户端超时控制
在生产环境中,客户端一定要设置超时,避免程序因网络问题无限期挂起。有三种实现方式:
socket.settimeout(seconds):设置所有阻塞操作的超时时间,超时抛出socket.timeout
socket.setblocking(False):设为非阻塞模式,所有操作立即返回,未完成时抛出BlockingIOError
select.select()结合文件描述符:精细控制等待时间
# 设置5秒超时
client_sock.settimeout(5.0)
# 或使用with语句结合contextlib超时
import contextlib
with contextlib.suppress(socket.timeout):
client_sock.connect(('example.com', 80))
客户端编程要点: ①connect可能抛出多种异常,务必做完善的异常处理;②TCP是流协议,接收方需要处理"粘包"问题,推荐使用长度前缀法分帧;③始终设置connect和recv的超时时间;④使用sendall()而不是send(),前者能确保完整发送。
五、UDP编程 — sendto与recvfrom无连接通信
UDP(User Datagram Protocol,用户数据报协议)是一种无连接的、不可靠的传输层协议。与TCP不同,UDP不保证数据包的到达顺序、不保证不重复、甚至不保证数据包能到达目的地。但UDP具有低延迟、无连接开销、支持广播/组播的优点,在实时音视频、在线游戏、DNS查询等场景得到广泛应用。
UDP的核心特征
无连接 :发送数据前不需要建立连接,直接发送数据报。没有TCP三次握手和四次挥手的开销
消息边界保留 :UDP保留消息边界——一次sendto对应一次recvfrom,接收方收到的数据与发送方发出的大小一致(前提是接收缓冲区足够大)
不可靠 :数据包可能丢失、重复、乱序到达。应用层需要自行处理可靠性问题(如需可靠传输则不应使用UDP)
支持一对多 :UDP天然支持广播和组播,这是TCP无法实现的功能
UDP服务器示例
import socket
HOST = '0.0.0.0'
PORT = 9999
BUFFER_SIZE = 65535 # UDP最大数据报大小
def udp_server():
# 创建UDP套接字
server_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 绑定地址
server_sock.bind((HOST, PORT))
print(f'[UDP服务器] 已启动, 监听 {HOST}:{PORT}')
try:
while True:
# 接收客户端数据
# recvfrom返回 (data, address) 元组
data, client_addr = server_sock.recvfrom(BUFFER_SIZE)
print(f'[收到] 来自 {client_addr}: {data.decode("utf-8")!r}')
# 回复客户端
response = f'服务器已收到 {len(data)} 字节数据'
server_sock.sendto(response.encode('utf-8'), client_addr)
print(f'[回复] 已发送到 {client_addr}')
except KeyboardInterrupt:
print('\n[关闭] UDP服务器关闭')
finally:
server_sock.close()
if __name__ == '__main__':
udp_server()
UDP客户端示例
import socket
def udp_client(host='127.0.0.1', port=9999):
# 创建UDP套接字
client_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
# 发送数据到服务器(不需要connect)
message = 'Hello, UDP Server!'
client_sock.sendto(message.encode('utf-8'), (host, port))
print(f'[发送] 已发送到 {host}:{port}: {message}')
# 接收服务器回复
# 注意:UDP的recvfrom可能永远阻塞(如果服务器没有回复)
client_sock.settimeout(5.0)
data, server_addr = client_sock.recvfrom(4096)
print(f'[收到] 来自 {server_addr}: {data.decode("utf-8")!r}')
except socket.timeout:
print('[超时] 等待服务器回复超时')
finally:
client_sock.close()
if __name__ == '__main__':
udp_client()
UDP与TCP的对比
特性
TCP (SOCK_STREAM)
UDP (SOCK_DGRAM)
连接状态
面向连接(需三次握手)
无连接(直接发送)
可靠性
可靠传输,自动重传丢包
不可靠,不保证送达
消息边界
字节流,无边界
数据报,保留边界
传输顺序
保证有序到达
可能乱序
流量控制
有(滑动窗口)
无
传输速度
相对较慢(有额外开销)
快(低延迟)
数据大小限制
无(基于流)
单个数据报最大65507字节
典型应用
HTTP、FTP、SMTP、SSH
DNS、VoIP、视频直播、在线游戏
UDP编程要点: ①UDP socket不需要listen/accept,直接recvfrom接收数据;②每个recvfrom对应一个完整的UDP数据报,接收缓冲区要足够大(建议65535);③UDP不可靠,如果应用需要可靠性(如文件传输),应在应用层实现确认重传机制或直接改用TCP;④UDP服务器可以同时与多个客户端通信,无需为每个客户端创建新socket。
六、高级I/O — 非阻塞、超时与多路复用
默认情况下,socket的所有I/O操作都是阻塞的:accept()在没有新连接时阻塞、recv()在没有数据时阻塞、connect()在握手完成前阻塞。阻塞模式的优点是编程简单直接,但在高并发场景下效率低下——为每个连接创建一个线程的开销过大。Python提供了多种高级I/O模式来解决这一问题。
非阻塞模式 setblocking(False)
调用setblocking(False)将socket切换为非阻塞模式。在非阻塞模式下,如果操作不能立即完成,不会等待而是直接抛出BlockingIOError异常(Python 3中为OSError的子类)。这使得程序可以在单个线程中轮询多个socket,但"忙等待"轮询会消耗大量CPU资源,不是最佳实践。
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setblocking(False) # 设为非阻塞模式
try:
s.connect(('example.com', 80))
print('连接已发起')
except BlockingIOError:
print('连接正在进行中(非阻塞)')
# 后续手动轮询s的读写状态...
超时模式 settimeout(value)
超时模式是阻塞与非阻塞之间的折中——操作仍然阻塞,但最多等待指定秒数。超时后抛出socket.timeout异常。value为None表示阻塞模式,0.0表示非阻塞模式,正值表示超时秒数。
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(10.0) # 最多等待10秒
try:
s.connect(('8.8.8.8', 53))
data = s.recv(1024)
except socket.timeout:
print('操作超时')
select — I/O多路复用
select模块是Python提供的最经典的I/O多路复用接口。它允许程序同时监视多个socket的可读、可写和异常事件,当至少一个socket就绪时返回,避免了低效的轮询。
import socket
import select
def select_based_server(host='0.0.0.0', port=8888):
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind((host, port))
server.listen(5)
server.setblocking(False) # 非阻塞
inputs = [server] # 监视读事件的socket列表
outputs = [] # 监视写事件的socket列表
message_queues = {} # 每个连接的消息队列
print(f'[select服务器] 已启动 {host}:{port}')
while inputs:
# select 阻塞直到有socket就绪
readable, writable, exceptional = select.select(inputs, outputs, inputs)
# 处理可读的socket
for s in readable:
if s is server:
# 监听socket可读 → 有新连接
conn, addr = s.accept()
conn.setblocking(False)
inputs.append(conn)
message_queues[conn] = []
print(f'[连接] {addr}')
else:
# 客户端socket可读 → 有数据到达
data = s.recv(1024)
if data:
message_queues[s].append(data)
if s not in outputs:
outputs.append(s)
else:
# 客户端断开
print(f'[断开] {s.getpeername()}')
if s in outputs:
outputs.remove(s)
inputs.remove(s)
s.close()
del message_queues[s]
# 处理可写的socket
for s in writable:
if message_queues.get(s):
data = message_queues[s].pop(0)
sent = s.send(data) # 回显数据
else:
outputs.remove(s)
# 处理异常socket
for s in exceptional:
print(f'[异常] {s.getpeername()}')
inputs.remove(s)
if s in outputs:
outputs.remove(s)
s.close()
del message_queues[s]
if __name__ == '__main__':
select_based_server()
poll与epoll
select的局限性在于:①监视的文件描述符数量有限制(通常1024);②每次调用都需要将整个socket列表从用户态拷贝到内核态;③就绪的socket需要线性扫描,效率随数量增加而下降。
poll 使用poll()替代select(),去掉了文件描述符数量的限制,但大数量下的线性扫描问题仍然存在。在Python中使用select.poll()创建poll对象。
epoll 是Linux下的高性能I/O多路复用机制(Windows不支持),使用事件驱动的方式避免了线性扫描——内核只返回就绪的文件描述符列表。在Python中通过select.epoll()使用。它的性能在大量并发连接下远优于select和poll。
# epoll 使用示例(仅Linux)
import select
import socket
epoll = select.epoll()
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('0.0.0.0', 8888))
server.listen(128)
server.setblocking(False)
# 注册监听socket的可读事件
epoll.register(server.fileno(), select.EPOLLIN)
connections = {}
try:
while True:
events = epoll.poll(timeout=-1) # 阻塞等待事件
for fd, event in events:
if fd == server.fileno():
conn, addr = server.accept()
conn.setblocking(False)
epoll.register(conn.fileno(), select.EPOLLIN)
connections[conn.fileno()] = (conn, addr)
elif event & select.EPOLLIN:
conn, addr = connections[fd]
data = conn.recv(1024)
if data:
conn.send(data)
else:
epoll.unregister(fd)
conn.close()
del connections[fd]
finally:
epoll.unregister(server.fileno())
epoll.close()
server.close()
高级I/O选型建议: 对于少量连接(几十个以内),多线程+阻塞socket是最简单直接的做法。对于数百到数千个连接,select(跨平台)或poll是不错的选择。对于数万个并发连接(如高并发服务器),应使用Linux上的epoll或跨平台的asyncio事件循环。Python的asyncio底层正是基于epoll/kqueue/IOCP等高性能多路复用机制构建的。
七、地址与字节序 — 地址解析与网络字节序转换
网络编程中经常需要处理主机名解析、IP地址格式转换、字节序转换等底层操作。Python的socket模块提供了一整套工具函数来完成这些任务。
主机名与地址解析
gethostname() :返回本机的主机名。通常返回的是计算机在网络中的名称。
gethostbyname(hostname) :将主机名解析为IPv4地址。这是最基础也最常用的DNS解析函数,内部调用操作系统的getaddrinfo或gethostbyname系统调用。注意:此函数只返回一个IP地址,对于多个IP地址的域名,只返回其中一个。
gethostbyname_ex(name) :更详细的地址解析,返回(hostname, aliaslist, ipaddrlist)元组,其中ipaddrlist包含所有解析到的IP地址。
getaddrinfo(host, port, family, type, proto, flags) :最通用、最推荐的地址解析函数。它返回一个包含多个地址信息的列表,每个元素是(family, type, proto, canonname, sockaddr)元组。这个函数同时支持IPv4和IPv6,是gethostbyname的现代替代品。
import socket
# 获取本机主机名
hostname = socket.gethostname()
print(f'本机主机名: {hostname}')
# 解析域名到IP
ip = socket.gethostbyname('www.baidu.com')
print(f'www.baidu.com 的IP: {ip}')
# 详细解析
host, aliases, ips = socket.gethostbyname_ex('www.google.com')
print(f'主机名: {host}')
print(f'别名: {aliases}')
print(f'IP地址列表: {ips}')
# 推荐使用的getaddrinfo
info = socket.getaddrinfo('www.python.org', 80,
socket.AF_INET, socket.SOCK_STREAM)
for fam, typ, pro, cn, sa in info:
print(f'地址信息: {sa}')
网络字节序转换
不同的计算机体系结构使用不同的字节序(Byte Order):x86/x86_64使用小端序(Little-Endian,低位字节存储在低地址),而网络协议规定使用大端序(Big-Endian,也称为网络字节序Network Byte Order)。因此,在跨网络传输多字节整数时,必须进行字节序转换。
Python的socket模块提供了四组转换函数:
htons(x) :Host TO Network Short(16位,2字节整数,如端口号),将主机字节序转为网络字节序
htonl(x) :Host TO Network Long(32位,4字节整数,如IP地址),将主机字节序转为网络字节序
ntohs(x) :Network TO Host Short,将网络字节序转回主机字节序
ntohl(x) :Network TO Host Long,将网络字节序转回主机字节序
import socket
# 端口号转换(16位)
port = 8080
print(f'端口 {port} 的网络字节序: {socket.htons(port)}')
print(f'网络字节序转回: {socket.ntohs(socket.htons(port))}')
# IP地址转换示例(32位 IPv4地址)
ip_packed = socket.inet_aton('192.168.1.1')
ip_int = int.from_bytes(ip_packed, byteorder='big')
print(f'192.168.1.1 的整数表示: {ip_int}')
# 使用htonl的场景——自定义协议构造
import struct
# 构造自定义TCP消息头(16位端口 + 32位序列号)
port_net = socket.htons(443)
seq_net = socket.htonl(10001)
header = struct.pack('!H I', port_net, seq_net)
print(f'自定义消息头: {header.hex()}')
IP地址格式转换:inet_ntop与inet_pton
inet_pton(family, ip_string) :将点分十进制字符串(IPv4)或冒号十六进制字符串(IPv6)转换为网络字节序的二进制表示(packed格式,bytes对象)。"pton"代表"presentation to network"。
inet_ntop(family, packed_ip) :与inet_pton相反,将二进制表示的IP地址转换回可读的字符串形式。"ntop"代表"network to presentation"。
这两个函数是inet_aton/inet_ntoa的现代化替代,支持IPv6,是推荐使用的IP地址转换方式。
import socket
# IPv4地址转换
packed_ipv4 = socket.inet_pton(socket.AF_INET, '192.168.1.1')
print(f'IPv4打包后: {packed_ipv4.hex()}') # c0a80101
unpacked_ipv4 = socket.inet_ntop(socket.AF_INET, packed_ipv4)
print(f'IPv4解包后: {unpacked_ipv4}') # 192.168.1.1
# IPv6地址转换
packed_ipv6 = socket.inet_pton(socket.AF_INET6, '::1')
print(f'IPv6 ::1 打包后: {packed_ipv6.hex()}')
unpacked_ipv6 = socket.inet_ntop(socket.AF_INET6, packed_ipv6)
print(f'IPv6解包后: {unpacked_ipv6}') # ::1
struct.pack与自定义协议
在编写自定义网络协议时,struct模块与socket函数配合使用,可以方便地构造和解析二进制网络消息。struct.pack(format, v1, v2, ...)按照格式字符串将Python值打包为字节串。struct.unpack(format, buffer)则反向解析。格式字符串中的!前缀明确指定使用网络字节序(大端序)。
import socket
import struct
# 构造一个自定义协议消息
# 格式: 2字节消息类型 + 2字节数据长度 + N字节负载
msg_type = 1
payload = b'Hello, Network!'
payload_len = len(payload)
# 使用 ! 前缀指定网络字节序
header = struct.pack('!HH', msg_type, payload_len)
packet = header + payload
print(f'协议包: {packet.hex()}')
# 解析接收到的包
recv_type, recv_len = struct.unpack('!HH', packet[:4])
recv_payload = packet[4:4+recv_len]
print(f'类型: {recv_type}, 长度: {recv_len}, 内容: {recv_payload}')
字节序要点: ①网络字节序统一为大端序(Big-Endian);②在x86平台上开发时必须用hton/ntoh系列函数转换多字节数值;③推荐struct.pack('!...')来构造协议消息,'!'明确指定网络字节序;④inet_pton/ntop支持IPv6,是比inet_aton/ntoa更好的选择;⑤getaddrinfo是最通用的地址解析函数,应优先使用。
八、实战案例与总结
通过前面七个章节的系统学习,我们已经掌握了socket编程的核心知识。本章通过三个实战案例将这些知识串联起来,并总结socket编程的最佳实践。
案例一:模拟HTTP请求
HTTP协议运行在TCP之上,通过socket直接发送HTTP请求报文,可以帮助我们深入理解HTTP协议和TCP的关系。
import socket
def http_get(host, path='/'):
"""使用原始socket发送HTTP GET请求"""
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(10)
try:
# 解析域名并连接
ip = socket.gethostbyname(host)
print(f'解析 {host} -> {ip}')
sock.connect((ip, 80))
# 构造HTTP请求报文
request = (
f'GET {path} HTTP/1.1\r\n'
f'Host: {host}\r\n'
f'User-Agent: PythonSocket/1.0\r\n'
f'Connection: close\r\n'
f'\r\n'
)
sock.sendall(request.encode())
# 接收响应
response = b''
while True:
chunk = sock.recv(4096)
if not chunk:
break
response += chunk
# 解析响应头和响应体
header, _, body = response.partition(b'\r\n\r\n')
print(f'响应头:\n{header.decode("utf-8", errors="ignore")}')
print(f'\n响应体大小: {len(body)} 字节')
except socket.timeout:
print('请求超时')
except Exception as e:
print(f'请求失败: {e}')
finally:
sock.close()
if __name__ == '__main__':
http_get('httpbin.org', '/get')
案例二:简易端口扫描器
端口扫描是通过尝试连接目标主机的不同端口来判断哪些端口处于开放状态。这是网络诊断和安全评估中常用的一项技术。
import socket
import threading
from queue import Queue
def scan_port(host, port, timeout=2):
"""扫描单个端口"""
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(timeout)
result = sock.connect_ex((host, port))
sock.close()
return result == 0 # connect_ex返回0表示连接成功
except:
return False
def port_scanner(host, start_port=1, end_port=1024, threads=50):
"""多线程端口扫描器"""
print(f'[扫描] 目标: {host}, 端口范围: {start_port}-{end_port}')
# 先解析IP地址
try:
ip = socket.gethostbyname(host)
print(f'[解析] {host} -> {ip}')
except socket.gaierror:
print(f'[错误] 无法解析主机名: {host}')
return
# 多线程扫描
queue = Queue()
open_ports = []
def worker():
while not queue.empty():
port = queue.get()
if scan_port(ip, port):
try:
service = socket.getservbyport(port)
except:
service = 'unknown'
print(f'[开放] 端口 {port} ({service})')
open_ports.append((port, service))
queue.task_done()
# 填充队列
for port in range(start_port, end_port + 1):
queue.put(port)
# 启动工作线程
thread_list = []
for _ in range(min(threads, end_port - start_port + 1)):
t = threading.Thread(target=worker, daemon=True)
t.start()
thread_list.append(t)
# 等待所有任务完成
queue.join()
# 输出结果
print(f'\n[结果] 扫描完成! 发现 {len(open_ports)} 个开放端口:')
for port, service in sorted(open_ports):
print(f' {port:5d} {service}')
if __name__ == '__main__':
# 扫描本地机器常见端口
port_scanner('127.0.0.1', 1, 1024)
案例三:使用SocketServer搭建简易服务器
Python标准库中的socketserver模块(注意与socket模块的区别)对底层socket进行了更高级的封装,提供了创建TCP/UDP服务器的便捷框架。它可以自动处理多线程、多进程,大大简化了服务器开发。
import socketserver
# 定义请求处理类
class MyTCPHandler(socketserver.BaseRequestHandler):
"""
每次客户端连接都会创建一个新的handler实例
self.request 是已连接的socket对象
self.client_address 是客户端地址元组
"""
def handle(self):
print(f'[连接] 来自 {self.client_address}')
while True:
data = self.request.recv(1024)
if not data:
break
print(f'[收到] {self.client_address}: {data.decode()!r}')
self.request.sendall(data.upper()) # 转为大写并回显
print(f'[断开] {self.client_address}')
if __name__ == '__main__':
HOST, PORT = '0.0.0.0', 7777
# 使用ThreadingTCPServer实现多线程TCP服务器
with socketserver.ThreadingTCPServer((HOST, PORT), MyTCPHandler) as server:
print(f'[SocketServer] 启动 {HOST}:{PORT}')
print('[SocketServer] 按 Ctrl+C 停止服务')
try:
server.serve_forever()
except KeyboardInterrupt:
print('\n[SocketServer] 服务停止')
# 对应客户端测试代码
def socketserver_client(host='127.0.0.1', port=7777):
"""测试SocketServer的客户端"""
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((host, port))
sock.sendall(b'hello server')
response = sock.recv(1024)
print(f'服务器响应: {response.decode()!r}')
sock.close()
socket编程核心总结
通过本章的系统学习,我们覆盖了Python socket编程的完整知识体系。以下是必须掌握的核心要点:
1. 创建socket: socket(family, type)是起点,AF_INET+SOCK_STREAM(TCP)是互联网应用最广泛的选择。
2. TCP服务器六步法: create→bind→listen→accept→recv/send→close,监听socket只负责接受连接,每个accept返回的新socket专用于一个客户端。
3. TCP客户端三步法: create→connect→sendall/recv→close,client不需要bind和listen。
4. UDP无连接通信: 使用sendto/recvfrom,保留消息边界,单个数据报最大65507字节,不保证可靠送达。
5. 高级I/O: setblocking控制阻塞/非阻塞模式,settimeout设置超时,select/poll/epoll实现I/O多路复用。
6. 地址与字节序: getaddrinfo是最推荐的地址解析函数;网络字节序统一为大端序,使用htons/htonl/ntohs/ntohl或struct.pack('!...')进行转换。
7. 错误处理: 总是使用try/except包裹网络操作;recv返回空字节串表示对端关闭;设置超时避免无限期挂起。
8. 资源释放: 每次创建的socket都要确保被close(),推荐使用with语句或try/finally结构。
掌握了上述内容,你已经具备了使用Python进行网络编程的完整能力。后续可以进一步学习asyncio异步网络框架、http.server、urllib等更上层的网络模块,但理解socket底层原理将始终是你调试和优化网络程序的有力工具。