Skip to main content
  1. Posts/

IPC通信

·840 words·4 mins
# 网络编程核心概念全解析

从浏览器输入 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: 文件服务器、视频流、大文件传输等。


总结 #

网络编程的核心:

  1. Socket 是操作系统对网络的抽象
  2. epoll 实现高效的事件驱动
  3. 多线程/协程 提供并发能力
  4. 零拷贝 减少数据搬运
  5. 长连接 减少握手开销

框架(Flask/FastAPI/Hertz)都是在这些基础上构建的更友好的抽象层。