IPC通信
·840 words·4 mins
Table of Contents
# 网络编程核心概念全解析
从浏览器输入 URL 到服务器响应的完整流程
---
## 目录
1. [核心概念](#核心概念)
2. [完整流程演示](#完整流程演示)
3. [epoll 详解](#epoll-详解)
4. [并发模型](#并发模型)
5. [性能优化技术](#性能优化技术)
---
## 核心概念
### Socket 是什么?
**Socket = 操作系统提供的"网络通信接口"**
物理层:网卡 + 电缆(电信号传输) ↓ 网络层:TCP/IP 协议栈(可靠传输) ↓ 抽象层:Socket(文件描述符) ↓ 应用层:HTTP 框架(业务逻辑)
### 本机通信需要 IP 吗?
**需要,但走特殊的 Loopback 接口**
```python
# 即使是 localhost,依然需要 IP 地址
sock.connect(('127.0.0.1', 5000))
# 区别:
# - 数据不经过物理网卡
# - 在内核内存中直接传递
# - 速度极快,但仍走完整的 TCP/IP 协议栈
技术栈层次 #
┌─────────────────────────────────────┐
│ 业务代码: @app.get("/user") │ 你写的代码
├─────────────────────────────────────┤
│ 框架层: Flask / FastAPI / Hertz │ 更好用的 API
├─────────────────────────────────────┤
│ Socket 层: socket.recv() / send() │ 操作系统抽象
├─────────────────────────────────────┤
│ 内核层: TCP/IP + epoll │ 协议处理
├─────────────────────────────────────┤
│ 硬件层: 网卡 + 电缆 │ 物理传输
└─────────────────────────────────────┘
完整流程演示 #
场景:用户访问 http://localhost:5000/api/user #
第一步:建立连接(0-3ms) #
# 浏览器创建 Socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('127.0.0.1', 5000))
# 内核处理:
# 1. 查路由表:127.0.0.1 是本机
# 2. TCP 三次握手(全在内存中)
# 3. 返回文件描述符 fd=3
第二步:发送 HTTP 请求(3-5ms) #
# 浏览器构造请求
http_request = """GET /api/user HTTP/1.1\r
Host: localhost:5000\r
Connection: keep-alive\r
\r
"""
# 发送数据
sock.send(http_request.encode())
数据流转:
浏览器用户态缓冲区
↓ (拷贝1)
内核发送缓冲区
↓ (加TCP头、IP头)
Loopback接口
↓ (内存传递)
服务端接收缓冲区
↓ (epoll 唤醒)
Gunicorn 进程
第三步:服务端处理(5-50ms) #
# Gunicorn 的 epoll 被唤醒
events = epoll.poll() # [(4, EPOLLIN)]
# 读取数据
data = os.read(fd=4, size=4096)
# Flask 处理
@app.route("/api/user")
def get_user():
return {"name": "张三", "age": 25}
第四步:返回响应(50-55ms) #
# Flask 生成响应
response = """HTTP/1.1 200 OK\r
Content-Type: application/json\r
Content-Length: 27\r
\r
{"name":"张三","age":25}"""
# 发送回浏览器
sock.send(response.encode())
epoll 详解 #
问题:传统方式的瓶颈 #
# 低效:逐个检查 10000 个连接
while True:
for sock in all_sockets: # 遍历 10000 次
data = sock.recv(1024) # 大部分时间阻塞
if data:
process(data)
解决:epoll 事件驱动 #
import select
# 创建 epoll 对象
epoll = select.epoll()
# 注册所有关心的 Socket
for sock in all_sockets:
epoll.register(sock.fileno(), select.EPOLLIN)
while True:
# 内核通知:"这些 Socket 有数据了"
events = epoll.poll() # 只返回就绪的 fd
for fd, event in events:
data = os.read(fd, 4096) # 保证不阻塞
process(data)
epoll 的工作原理 #
┌─ 应用层 ─────────────────────┐
│ epoll.register(fd5, fd6, ...) │
│ epoll.poll() ← 阻塞等待 │
└───────────┬───────────────────┘
│
┌─ 内核层 ──▼──────────────────────────┐
│ 维护"关注列表": │
│ [fd5, fd6, fd7, ..., fd10000] │
│ │
│ 当数据到达: │
│ fd5 收到数据 → 加入"就绪队列" │
│ fd6 收到数据 → 加入"就绪队列" │
│ │
│ 唤醒 epoll.poll() │
│ 返回 [(5, EPOLLIN), (6, EPOLLIN)] │
└──────────────────────────────────────┘
消息不会丢失的保证 #
关键:内核缓冲区独立存储
客户端 A (fd=5) ──→ [缓冲区 A: "GET /api/user"]
客户端 B (fd=6) ──→ [缓冲区 B: "POST /login"]
客户端 C (fd=7) ──→ [缓冲区 C: "GET /data"]
即使你慢慢处理:
# 时刻 T1: 三个连接同时有数据
events = epoll.poll() # [(5,IN), (6,IN), (7,IN)]
# 时刻 T2: 先处理 fd=5,耗时 10 秒
data5 = os.read(5, 4096)
time.sleep(10) # 模拟慢处理
# fd=6 和 fd=7 的数据依然在内核缓冲区等着
# 不会丢失!
TCP 保证单连接顺序 #
# 客户端连续发送
sock.send(b"消息1")
sock.send(b"消息2")
sock.send(b"消息3")
# 服务端接收
data = os.read(fd, 4096)
# 保证收到顺序:消息1 → 消息2 → 消息3
# 不会出现 消息3 先于 消息1 到达
并发模型 #
能并发处理吗? #
可以!但有规则:
✅ 不同 fd 可以并发处理
❌ 同一个 fd 不能并发读取
模型 1:单线程串行(慢) #
events = epoll.poll() # [(5,IN), (6,IN), (7,IN)]
for fd, event in events:
data = os.read(fd, 4096)
handle(data) # 处理 1 秒
# 总耗时:3 秒
模型 2:多线程并发(快) #
from concurrent.futures import ThreadPoolExecutor
pool = ThreadPoolExecutor(max_workers=4)
while True:
events = epoll.poll()
for fd, event in events:
pool.submit(handle_connection, fd)
def handle_connection(fd):
data = os.read(fd, 4096)
handle(data) # 各自独立执行
# 总耗时:1 秒(并发处理)
Gunicorn 配置示例 #
# gunicorn.conf.py
workers = 3 # 3 个进程
threads = 4 # 每个进程 4 个线程
worker_class = "gthread"
# 并发能力:3 × 4 = 12 个请求同时处理
防止同一 fd 并发的方法 #
processing_fds = set()
lock = threading.Lock()
while True:
events = epoll.poll()
for fd, event in events:
with lock:
if fd in processing_fds:
continue # 正在处理,跳过
processing_fds.add(fd)
pool.submit(handle_and_cleanup, fd)
def handle_and_cleanup(fd):
try:
data = os.read(fd, 4096)
process(data)
finally:
with lock:
processing_fds.remove(fd)
不同并发模型对比 #
| 模型 | 配置 | QPS | 内存 | 适用场景 |
|---|---|---|---|---|
| 单线程 | 1进程×1线程 | ~1,000 | 50MB | 简单脚本 |
| 多线程 | 3进程×4线程 | ~10,000 | 200MB | Flask+Gunicorn |
| 异步协程 | asyncio | ~20,000 | 100MB | FastAPI |
| Go协程 | Goroutine | ~50,000+ | 150MB | Hertz+Netpoll |
性能优化技术 #
零拷贝(Zero-Copy) #
传统方式:4 次拷贝
磁盘 ─①DMA→ 内核缓冲区 ─②CPU→ 用户态
↓
③CPU
↓
网卡 ←④DMA─ Socket缓冲区 ←─────┘
零拷贝:2 次拷贝
import os
# 使用 sendfile 系统调用
os.sendfile(
sock.fileno(), # 目标 Socket
file_fd, # 源文件
offset=0,
count=file_size
)
# 数据流:磁盘 → 内核 → 网卡
# 完全不经过用户态!
磁盘 ─①DMA→ 内核页缓存 ─②DMA→ 网卡
(不经过用户态)
长连接(Keep-Alive) #
短连接的代价:
请求1: 三次握手(50ms) → 传输 → 四次挥手(50ms)
请求2: 三次握手(50ms) → 传输 → 四次挥手(50ms)
请求3: 三次握手(50ms) → 传输 → 四次挥手(50ms)
长连接:
三次握手(50ms)
├→ 请求1
├→ 请求2
├→ 请求3
└→ 空闲 10 秒后关闭
# gunicorn.conf.py
keepalive = 10 # 连接空闲 10 秒后再关闭
连接池 #
import requests
# requests 内部维护连接池
# 多次请求同一服务器时自动复用连接
session = requests.Session()
for i in range(100):
resp = session.get("http://api.example.com/data")
# 底层复用同一个 TCP 连接
形象比喻总结 #
Socket = 快递站的"收发窗口" #
- 你不需要知道快递车怎么走
- 只需要在窗口寄信(send)和收信(recv)
- 操作系统负责物流
epoll = 智能门卫 #
- 传统方式:挨家挨户敲门问"有快递吗?"
- epoll 方式:门卫主动通知"5号和7号有快递"
- 你只处理有快递的,不浪费时间
内核缓冲区 = 快递柜 #
- 每个连接有独立的柜子
- 快递放进柜子就安全了,不会丢
- 你慢慢取,快递在柜子里等着
多线程 = 多个工人 #
- 工人A取5号柜,工人B取6号柜(允许)
- 工人A和B同时取5号柜(禁止,会打架)
零拷贝 = 直达快递 #
- 传统:仓库 → 你手里 → 同事
- 零拷贝:仓库 → 同事(不经过你)
快速参考 #
关键系统调用 #
# 创建 Socket
fd = socket.socket(AF_INET, SOCK_STREAM)
# 连接
socket.connect(('127.0.0.1', 5000))
# 发送数据
socket.send(data)
# 接收数据
data = socket.recv(4096)
# epoll 操作
epoll = select.epoll()
epoll.register(fd, select.EPOLLIN)
events = epoll.poll()
# 零拷贝
os.sendfile(out_fd, in_fd, offset, count)
性能优化清单 #
- 使用 epoll(事件驱动)
- 启用多线程/多进程
- 开启 Keep-Alive
- 使用连接池
- 大文件传输用 sendfile
- 及时读取数据,避免缓冲区满
- 监控并发数,合理配置 workers
常见问题 #
Q: 本机通信需要走 TCP/IP 吗?
A: 需要,但走 Loopback 接口,不经过物理网卡。
Q: epoll 会丢消息吗?
A: 不会。数据在内核缓冲区安全存储。
Q: 可以并发处理请求吗?
A: 可以。不同连接可以并发,同一连接不能。
Q: 零拷贝适用哪些场景?
A: 文件服务器、视频流、大文件传输等。
总结 #
网络编程的核心:
- Socket 是操作系统对网络的抽象
- epoll 实现高效的事件驱动
- 多线程/协程 提供并发能力
- 零拷贝 减少数据搬运
- 长连接 减少握手开销
框架(Flask/FastAPI/Hertz)都是在这些基础上构建的更友好的抽象层。