socket模块 — 底层网络接口

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编程正是围绕这种模型设计的。其基本工作流程如下:

这种模型的上限性能取决于服务器同时处理多个连接的能力。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使用何种网络协议进行寻址和通信,常用的选项包括:

套接字类型(Socket Type)

套接字类型决定了数据的传输方式和特性,这是socket编程中最关键的选择之一:

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)的形式传入:

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会抛出异常(如ConnectionRefusedErrorTimeoutError等)。

connect默认也是阻塞调用,在网络状况不佳时可能长时间挂起。可以通过settimeout()或非阻塞模式控制等待时间。

数据收发

客户端使用sendall()发送请求数据,使用recv()接收服务器响应。一个常见问题是:recv返回的数据可能比预期的应用层消息短(因为TCP是流协议,没有消息边界)。因此客户端通常需要自行定义应用层的消息分帧(delimiting)策略:

完整的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()

客户端超时控制

在生产环境中,客户端一定要设置超时,避免程序因网络问题无限期挂起。有三种实现方式:

# 设置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的核心特征

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解析函数,内部调用操作系统的getaddrinfogethostbyname系统调用。注意:此函数只返回一个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模块提供了四组转换函数:

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.serverurllib等更上层的网络模块,但理解socket底层原理将始终是你调试和优化网络程序的有力工具。