目录
[一、Socket 编程概述](#一、Socket 编程概述)
[1.1 什么是 Socket](#1.1 什么是 Socket)
[1.2 Python 中的 Socket 模块](#1.2 Python 中的 Socket 模块)
[1.3 两种主流 Socket 类型](#1.3 两种主流 Socket 类型)
[二、Socket 编程基础](#二、Socket 编程基础)
[2.1 地址族](#2.1 地址族)
[2.2 创建 Socket 对象](#2.2 创建 Socket 对象)
[2.3 地址表示](#2.3 地址表示)
[2.4 字节序问题](#2.4 字节序问题)
[三、TCP Socket 编程详解](#三、TCP Socket 编程详解)
[3.1 TCP 通信流程](#3.1 TCP 通信流程)
[3.2 服务端核心步骤](#3.2 服务端核心步骤)
[3.2.1 创建 Socket](#3.2.1 创建 Socket)
[3.2.2 绑定地址与端口](#3.2.2 绑定地址与端口)
[3.2.3 开始监听](#3.2.3 开始监听)
[3.2.4 接受客户端连接](#3.2.4 接受客户端连接)
[3.2.5 收发数据](#3.2.5 收发数据)
[3.2.6 关闭连接](#3.2.6 关闭连接)
[3.3 客户端核心步骤](#3.3 客户端核心步骤)
[3.4 完整示例:回显服务器](#3.4 完整示例:回显服务器)
[3.5 TCP 常见问题与解决方案](#3.5 TCP 常见问题与解决方案)
[3.5.1 粘包问题](#3.5.1 粘包问题)
[3.5.2 端口占用错误](#3.5.2 端口占用错误)
[3.5.3 并发处理多个客户端](#3.5.3 并发处理多个客户端)
[四、UDP Socket 编程详解](#四、UDP Socket 编程详解)
[4.1 UDP 特点](#4.1 UDP 特点)
[4.2 服务端流程](#4.2 服务端流程)
[4.3 客户端流程](#4.3 客户端流程)
[4.4 UDP 注意事项](#4.4 UDP 注意事项)
[5.1 非阻塞 Socket](#5.1 非阻塞 Socket)
[5.2 I/O 多路复用](#5.2 I/O 多路复用)
[5.3 超时设置](#5.3 超时设置)
[5.4 广播与多播(UDP)](#5.4 广播与多播(UDP))
[5.5 使用 SSL/TLS 加密](#5.5 使用 SSL/TLS 加密)
[6.1 常见异常](#6.1 常见异常)
[6.2 调试工具](#6.2 调试工具)
[6.3 优雅关闭连接](#6.3 优雅关闭连接)
[7.1 服务器端](#7.1 服务器端)
[7.2 客户端](#7.2 客户端)
一、Socket 编程概述
1.1 什么是 Socket
Socket(套接字)是网络通信的端点,可以理解为不同进程(本地或远程)之间进行数据交换的"虚拟通道"。它屏蔽了 TCP/IP 协议栈的复杂细节,为应用层提供了一套标准的文件描述符接口 ------ 打开、读写、关闭。
1.2 Python 中的 Socket 模块
Python 标准库中的 socket 模块是对底层操作系统 Socket API 的封装,语法简洁、跨平台,适合快速开发网络应用。无论是 Web 服务器、聊天程序、文件传输工具,还是网络爬虫,都离不开 Socket。
1.3 两种主流 Socket 类型
|--------------------|-----|---------------------|------------------|
| 类型 | 类型 | 核心特点 | 典型应用 |
| socket.SOCK_STREAM | TCP | 面向连接、可靠、按序、无边界、 有重传 | HTTP、FTP、SSH、数据库 |
| socket.SOCK_DGRAM | UDP | 无连接、不可靠、有消息边界、 效率高 | DNS、音视频、游戏 |
还有 SOCK_RAW(原始套接字),可以操作 IP 层,常用于实现 ping、traceroute 等工具,但需要 root 权限。
二、Socket 编程基础
2.1 地址族
socket.AF_INET:IPv4
socket.AF_INET6:IPv6
socket.AF_UNIX:Unix 域套接字(仅本机进程间通信)
2.2 创建 Socket 对象
python
python
import socket
# TCP 套接字
tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# UDP 套接字
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
2.3 地址表示
Python 中用元组 (ip, port) 表示网络地址:
IPv4:('127.0.0.1', 8080)
IPv6:('::1', 8080, 0, 0)(第四个元素为流标识)
特殊 IP:
0.0.0.0 或 ' ':绑定到本机所有可用网络接口
127.0.0.1:本地地址,只能本机访问
2.4 字节序问题
网络协议规定使用大端序(网络字节序),而多数 CPU(x86/ARM)使用小端序。Python 提供了转换函数:
python
python
# 端口转换
port = 8080
net_port = socket.htons(port) # host to network short
host_port = socket.ntohs(net_port)
# IP 转换(字符串 → 32位整数)
ip_str = '192.168.1.1'
ip_int = socket.htonl(int.from_bytes(socket.inet_aton(ip_str), 'big'))
不过在日常编程中,直接使用字符串和元组即可,底层函数会自动处理字节序。
三、TCP Socket 编程详解
3.1 TCP 通信流程
python
mermaid
sequenceDiagram
participant Client
participant Server
Server->>Server: socket() → bind() → listen()
Client->>Client: socket()
Client->>Server: connect()
Server-->>Client: accept() 返回新socket
Client->>Server: send() 请求数据
Server->>Client: send() 响应数据
Client->>Server: close()
Server->>Server: close()
3.2 服务端核心步骤
3.2.1 创建 Socket
python
python
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
3.2.2 绑定地址与端口
python
python
server.bind(('0.0.0.0', 8888))
端口号范围 0~65535,1024 以下需要管理员权限。
绑定 `0.0.0.0` 表示监听所有网卡(包括 127.0.0.1 和局域网 IP)。
3.2.3 开始监听
python
python
server.listen(5) # 最大等待连接数(backlog)
backlog 是已完成三次握手但尚未被 accept() 取走的连接队列长度。
3.2.4 接受客户端连接
python
python
client_socket, client_addr = server.accept()
accept() 会阻塞,直到有客户端连接,返回一个新的 Socket 对象(用于与该客户端通信)和客户端地址。
3.2.5 收发数据
python
python
data = client_socket.recv(1024) # 最多接收 1024 字节
if not data:
break # 对方关闭连接
client_socket.sendall(b'Hello') # 发送所有数据
recv 返回空字节串 b' ' 表示对方已正常关闭连接。
sendall 会循环发送直到所有数据发出,比 send 更安全。
3.2.6 关闭连接
python
python
client_socket.close()
server.close()
3.3 客户端核心步骤
python
python
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('127.0.0.1', 8888)) # 阻塞连接
client.sendall(b'Hello Server')
response = client.recv(1024)
client.close()
3.4 完整示例:回显服务器
服务端(echo_server.py)
python
python
import socket
def run_server():
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 端口复用
server.bind(('0.0.0.0', 8888))
server.listen(5)
print('Echo server listening on port 8888...')
while True:
client, addr = server.accept()
print(f'New connection from {addr}')
with client:
while True:
data = client.recv(1024)
if not data:
break
client.sendall(data) # 原样返回
print(f'Connection from {addr} closed')
if __name__ == '__main__':
run_server()
客户端(echo_client.py)
python
python
import socket
def run_client():
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('127.0.0.1', 8888))
client.sendall(b'Hello, TCP server!')
response = client.recv(1024)
print(f'Server replied: {response.decode()}')
client.close()
if __name__ == '__main__':
run_client()
3.5 TCP 常见问题与解决方案
3.5.1 粘包问题
现象:连续多次 send 的小数据可能被一次 recv 全部读走,导致无法区分消息边界。
原因:TCP 是流式协议,没有消息边界,发送方有Nagle 算法会合并小包,接收方缓冲区可能一次性取出多个包。
解决方案:
-
固定长度消息:每个消息固定为 N 字节,不足补空格或 \0。
-
特殊分隔符:如 HTTP 的 `\r\n\r\n`,但消息体本身不能包含分隔符。
-
长度前缀法(最常用):每个消息前 4 字节表示消息体长度。
示例(长度前缀):
python
python
# 发送端
def send_msg(sock, msg_bytes):
length = len(msg_bytes)
sock.sendall(length.to_bytes(4, 'big')) # 发送长度
sock.sendall(msg_bytes) # 发送内容
# 接收端
def recv_msg(sock):
raw_len = sock.recv(4)
if not raw_len:
return None
length = int.from_bytes(raw_len, 'big')
data = b''
while len(data) < length:
chunk = sock.recv(min(1024, length - len(data)))
if not chunk:
raise ConnectionError('Connection broken')
data += chunk
return data
3.5.2 端口占用错误
原因:Socket 关闭后进入 TIME_WAIT 状态(默认 2MSL,约 1~4 分钟),此时重新绑定同一端口会失败。
解决:设置 SO_REUSEADDR 选项
python
python
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
3.5.3 并发处理多个客户端
上面示例只能依次处理客户端(一个结束后才能处理下一个)。要实现并发,常见方案:
多线、多进程、异步 I/O
多线程示例:
python
python
import threading
def handle_client(client_sock, addr):
print(f'Handling {addr}')
with client_sock:
while True:
data = client_sock.recv(1024)
if not data:
break
client_sock.sendall(data)
print(f'{addr} done')
def run_concurrent_server():
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(('0.0.0.0', 8888))
server.listen(5)
print('Concurrent server running...')
while True:
client, addr = server.accept()
t = threading.Thread(target=handle_client, args=(client, addr))
t.start()
注意:Python 的 GIL 对 I/O 密集型任务影响不大,但线程数过多会增加上下文切换开销。对于高并发场景,建议使用 asyncio 或 epoll。
四、UDP Socket 编程详解
4.1 UDP 特点
- 无连接,不需要 listen 和 accept。
- 每个数据报独立发送,可能丢失、乱序。
- 有消息边界:一次 recvfrom 对应一个 sendto。
- 效率高,适合实时通信。
4.2 服务端流程
python
python
import socket
udp_server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
udp_server.bind(('0.0.0.0', 9999))
print('UDP server ready')
while True:
data, addr = udp_server.recvfrom(1024) # 阻塞等待数据报
print(f'Received from {addr}: {data.decode()}')
udp_server.sendto(b'ACK', addr) # 回复
4.3 客户端流程
python
python
client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
client.sendto(b'Hello UDP', ('127.0.0.1', 9999))
response, server_addr = client.recvfrom(1024)
print(response.decode())
client.close()
4.4 UDP 注意事项
数据报大小限制:IPv4 下最大 65507 字节(包含头部),一般建议不超过 1472 字节(避免分片)。
丢包处理:应用层需要实现超时重传、序列号等机制。
广播与多播:UDP 天然支持。
五、高级主题
5.1 非阻塞 Socket
默认情况下,recv、accept、connect 都是阻塞的。可以设置为非阻塞模式:
python
python
sock.setblocking(False)
此时若没有数据可读,recv 会立即抛出 BlockingIOError。通常与 I/O 多路复用配合使用。
5.2 I/O 多路复用
用一个线程同时监控多个 Socket 的可读/可写事件,避免为每个客户端创建一个线程。
select / poll / epoll 对比
|-------|-------|------------------|------------|
| 方法 | 最大描述符 | 效率 | 平台支持 |
| elect | 1024 | O(n) 遍历,每次重新复制集合 | 全平台 |
| poll | 无限制 | O(n) 遍历 | Unix/Linux |
| epoll | 无限制 | O(1) 事件驱动,只返回活跃的 | Linux(推荐) |
使用 selectors 模块(推荐)
Python 3.4+ 提供了 selectors 模块,自动选择最高效的 I/O 多路复用机制。
python
python
import selectors
import socket
sel = selectors.DefaultSelector()
def accept(sock, mask):
conn, addr = sock.accept()
conn.setblocking(False)
sel.register(conn, selectors.EVENT_READ, read)
def read(conn, mask):
data = conn.recv(1024)
if not data:
print('Closing connection')
sel.unregister(conn)
conn.close()
else:
conn.sendall(data)
server = socket.socket()
server.bind(('0.0.0.0', 8888))
server.listen()
server.setblocking(False)
sel.register(server, selectors.EVENT_READ, accept)
while True:
events = sel.select() # 阻塞等待事件
for key, mask in events:
callback = key.data
callback(key.fileobj, mask)
5.3 超时设置
python
python
sock.settimeout(5.0) # 秒
try:
data = sock.recv(1024)
except socket.timeout:
print('Recv timeout')
或者使用 select 实现更精细的超时控制。
5.4 广播与多播(UDP)
广播:向网络内所有主机发送数据,需要设置 SO_BROADCAST 选项。
python
python
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
sock.sendto(b'Broadcast message', ('255.255.255.255', 9999))
多播(组播):加入一个多播组(如 224.0.0.1),只有加入该组的主机才能收到。
python
python
import struct
multicast_group = '224.0.0.1'
multicast_port = 10000
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2) # TTL=2
# 发送多播
sock.sendto(b'Hello multicast', (multicast_group, multicast_port))
# 接收端需要加入多播组
sock.bind(('0.0.0.0', multicast_port))
mreq = struct.pack('4sl', socket.inet_aton(multicast_group), socket.INADDR_ANY)
sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
5.5 使用 SSL/TLS 加密
Python 的 ssl 模块可以为 Socket 添加加密层,实现 HTTPS 等安全通信。
python
python
import ssl
# 服务端
context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
context.load_cert_chain(certfile='server.crt', keyfile='server.key')
secure_sock = context.wrap_socket(sock, server_side=True)
# 客户端
context = ssl.create_default_context()
secure_sock = context.wrap_socket(sock, server_hostname='example.com')
六、错误处理与调试
6.1 常见异常
|------------------------|-----------------------|
| 异常类 | 典型原因 |
| socket.error | 通用错误(被更具体的异常替代 |
| socket.gaierror | DNS 解析失败(getaddrinfo) |
| socket.timeout | 操作超时 |
| ConnectionRefusedError | 目标端口未监听或防火墙拒绝 |
| ConnectionResetError | 对方异常关闭(发送 RST) |
| ConnectionAbortedError | 本地软件中止连接 |
| TimeoutError | 连接超时 |
6.2 调试工具
netstat、lsof -i:查看端口监听状态
tcpdump、Wireshark:抓包分析网络包
Python日志:记录收发数据和时间戳
python
python
import logging
logging.basicConfig(level=logging.DEBUG)
logging.debug(f'Sent {len(data)} bytes')
6.3 优雅关闭连接
python
python
# 先调用 shutdown 表示不再发送数据,让对方知晓
sock.shutdown(socket.SHUT_WR) # 关闭写方向
sock.recv(1024) # 等待对方关闭读方向(可选)
sock.close()
shutdown 会立即发送 FIN 包,而 close 只是减少引用计数,可能不会立即关闭。
七、实战案例:简易聊天室
结合多线程和 TCP,实现一个群聊服务器。
7.1 服务器端
python
python
import socket
import threading
clients = []
def broadcast(message, sender_sock=None):
for client in clients:
if client != sender_sock:
try:
client.send(message)
except:
client.close()
clients.remove(client)
def handle_client(client_sock, addr):
print(f'{addr} connected')
while True:
try:
data = client_sock.recv(1024)
if not data:
break
broadcast(data, client_sock)
except:
break
clients.remove(client_sock)
client_sock.close()
print(f'{addr} disconnected')
def run_chat_server():
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(('0.0.0.0', 5555))
server.listen(5)
print('Chat server started on port 5555')
while True:
client, addr = server.accept()
clients.append(client)
t = threading.Thread(target=handle_client, args=(client, addr))
t.start()
if __name__ == '__main__':
run_chat_server()
7.2 客户端
python
python
import socket
import threading
def receive_messages(sock):
while True:
try:
data = sock.recv(1024)
if not data:
break
print(data.decode())
except:
break
def run_chat_client():
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('127.0.0.1', 5555))
threading.Thread(target=receive_messages, args=(client,), daemon=True).start()
while True:
msg = input()
if msg.lower() == '/quit':
break
client.send(msg.encode())
client.close()
if __name__ == '__main__':
run_chat_client()
八、总结
-
TCP 适用于需要可靠传输的场景:如文件传输、HTTP、数据库连接。注意处理粘包和并发。
-
UDP 适用于实时性要求高、可容忍丢包的场景:如音视频、游戏、DNS。注意应用层实现丢包恢复。
-
高并发首选 asyncio 或 epoll:避免创建大量线程带来的开销。
-
始终处理异常:网络环境不可控,必须捕获 socket.error并合理重试或关闭连接。
-
使用 `SO_REUSEADDR`:开发调试时非常有用,避免 `Address already in use`。
-
生产环境务必加安全层:使用 SSL/TLS 或 SSH 隧道,防止数据被窃听。