chore: english filenames for blog posts
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
# 从零实现一个用户态 TCP/IP 协议栈:用 DPDK 把内核 TCP 甩在身后
|
||||
|
||||
> 6000 行 C 代码,11 状态 TCP 状态机,RFC 5681 拥塞控制,零拷贝零系统调用。
|
||||
> 实测:TCP 连接 1.5ms(内核 2.5ms,**快 40%**),WS echo RTT 1.5ms(内核 3.1ms,**快 52%**)。
|
||||
|
||||
---
|
||||
|
||||
## 一、为什么要绕过内核 TCP
|
||||
|
||||
对于绝大多数应用,Linux 内核 TCP 是完美的:30 年打磨、各种边界场景都处理过。但在**低延迟数据面**(金融行情、交易执行、高频网关)场景下,内核协议栈有三笔"固定开销":
|
||||
|
||||
| 开销 | 内核 TCP | 用户态协议栈(本项目) |
|
||||
|------|----------|----------------------|
|
||||
| 内存拷贝(收+发) | 2 次(skb → 用户缓冲区) | **0 次**(mbuf 全程零拷贝) |
|
||||
| 系统调用 | 每次 I/O 都要进入内核 | **0 次**(纯用户态函数调用) |
|
||||
| 协议栈可定制性 | 改内核模块,复杂且高风险 | **任意修改**,改完 `make` 即可 |
|
||||
| 可观测性 | `/proc/net/tcp` 有限 | **每个 TCB 可挂任意指标** |
|
||||
|
||||
本项目用 **DPDK 24.11** 接管网卡,在用户态从二层(以太网)一路实现到七层(WebSocket),数据包全程不经过内核。
|
||||
|
||||
> 测试拓扑:Intel X540 10G 网卡 → 千兆交换机 → C WebSocket 服务端,`rdtsc` 硬件时间戳测量。
|
||||
|
||||
## 二、整体架构:双核流水线 + 无锁队列
|
||||
|
||||
核心思路:**用两个 lcore 组成流水线**,核间只通过 `rte_ring` SP/SC 无锁队列传递指针,数据本体零拷贝。
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph NIC["物理网卡 (DPDK PMD)"]
|
||||
RX["RX 队列 (128 desc)"]
|
||||
TX["TX 队列 (512 desc)"]
|
||||
end
|
||||
|
||||
subgraph LC0["lcore 0 — 主线程 (main.c)"]
|
||||
B1["rte_eth_rx_burst 收包"]
|
||||
B2["ring→out 出队 → rte_eth_tx_burst 发包"]
|
||||
B3["WS 帧接收轮询 + 应用逻辑"]
|
||||
end
|
||||
|
||||
subgraph LC1["lcore 1 — pkt_process (tcp_core.c)"]
|
||||
C1["分发: ARP / ICMP / TCP"]
|
||||
C2["TCP 11 状态机 + 拥塞控制"]
|
||||
C3["定时器: RTO / delACK / keepalive / ZWP"]
|
||||
C4["TX 输出引擎 ng_tcp_out"]
|
||||
end
|
||||
|
||||
RX -->|"ring→in 只入队指针"| C1
|
||||
C4 -->|"ring→out 只出队指针"| B2
|
||||
|
||||
subgraph TCB["每连接 (TCB)"]
|
||||
D1["rcvbuf rte_ring (1024)"]
|
||||
D2["sndbuf rte_ring (1024)"]
|
||||
D3["pthread_cond_t + mutex"]
|
||||
end
|
||||
|
||||
C2 -->|"数据入队 + cond signal"| D1
|
||||
D2 --> C4
|
||||
D1 --> B3
|
||||
B3 -->|"nsend 分段入队"| D2
|
||||
```
|
||||
|
||||
**为什么这样分工?**
|
||||
|
||||
- **lcore0** 只做两件事:从网卡收包、向网卡发包,以及跑应用逻辑(WS 轮询)。它不碰任何协议状态,忙轮询即可。
|
||||
- **lcore1** 是协议处理核心:TCP 状态机、拥塞控制、定时器都在这里。单线程无锁,天然避免竞态。
|
||||
- 跨核传递只传 `rte_mbuf *` 指针(8 字节入 ring),实测 **0.2 μs**,接近 CPU 三级缓存延迟(~15ns)的量级。
|
||||
|
||||
## 三、TCB 查找:O(1) 的两级设计
|
||||
|
||||
每个 TCP 连接一个 `ng_tcp_stream`(TCB)。数据包到达后,第一步就是找到它属于哪条连接。这里做了两级 O(1) 设计:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ 256 桶哈希表 (4-tuple: sip, dip, sport, dport) │ ← 包路径: 收到段 → 查哈希 → 定位 TCB
|
||||
│ ht_next 链表解决冲突 │
|
||||
└─────────────────────────────────────────────────┘
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ fd_to_tcb[] 直接数组 (fd → TCB 指针) │ ← 应用路径: nsend(fd) 直接取 TCB
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
- **包路径**用哈希表:`pkt_process` 收到一个段,用四元组哈希直接命中 TCB,平均 2.1 ns/次(微基准实测 4.81 亿次/秒)。
|
||||
- **应用路径**用直查数组:`nsend(fd)` 无需遍历,`fd_to_tcb[fd]` 一条指令完成。
|
||||
- 另有一条 `active_head` 双向链表维护活跃连接,定时器遍历时**不用扫全部 256 个桶**(这是 P4 优化的成果,详见第三篇)。
|
||||
|
||||
## 四、TCP 状态机:完整的 11 状态
|
||||
|
||||
`src/tcp_core.h` 中定义了与 RFC 793 一致的 11 个状态。作为客户端,我们主要走右侧这条路径:
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> CLOSED
|
||||
CLOSED --> SYN_SENT: nconnect() 发 SYN
|
||||
SYN_SENT --> ESTABLISHED: 收 SYN+ACK,回 ACK
|
||||
ESTABLISHED --> FIN_WAIT_1: nclose() 发 FIN
|
||||
FIN_WAIT_1 --> FIN_WAIT_2: 收 ACK
|
||||
FIN_WAIT_2 --> TIME_WAIT: 收 FIN,回 ACK
|
||||
TIME_WAIT --> CLOSED: 2MSL 超时
|
||||
FIN_WAIT_1 --> CLOSING: 同时收到 FIN
|
||||
CLOSING --> TIME_WAIT: 收 ACK
|
||||
ESTABLISHED --> CLOSE_WAIT: 收到对方 FIN
|
||||
CLOSE_WAIT --> LAST_ACK: 本方发 FIN
|
||||
LAST_ACK --> CLOSED: 收 ACK
|
||||
```
|
||||
|
||||
```c
|
||||
/* src/tcp_core.h — 状态枚举 */
|
||||
typedef enum _NG_TCP_STATUS {
|
||||
NG_TCP_STATUS_CLOSED = 0, NG_TCP_STATUS_LISTEN,
|
||||
NG_TCP_STATUS_SYN_RCVD, NG_TCP_STATUS_SYN_SENT,
|
||||
NG_TCP_STATUS_ESTABLISHED, NG_TCP_STATUS_FIN_WAIT_1,
|
||||
NG_TCP_STATUS_FIN_WAIT_2, NG_TCP_STATUS_CLOSING,
|
||||
NG_TCP_STATUS_TIME_WAIT, NG_TCP_STATUS_CLOSE_WAIT,
|
||||
NG_TCP_STATUS_LAST_ACK
|
||||
} NG_TCP_STATUS;
|
||||
```
|
||||
|
||||
状态机入口 `ng_tcp_process()`(`src/tcp_input.c`,~690 行)按状态 + 段标志(SYN/ACK/FIN/RST)驱动迁移,每个迁移附带动作(发 ACK、唤醒等待线程、回收资源)。
|
||||
|
||||
## 五、拥塞控制:RFC 5681 全套
|
||||
|
||||
`src/tcp_output.c` 实现了完整的拥塞控制闭环,参数直接对应 RFC:
|
||||
|
||||
| 机制 | 实现 |
|
||||
|------|------|
|
||||
| 初始窗口 | `cwnd = 2 × MSS`(~2920 字节,RFC 6928) |
|
||||
| 慢启动 | 每收一个 ACK,`cwnd += MSS`,指数增长 |
|
||||
| 拥塞避免 | 超过 `ssthresh` 后按 `MSS²/cwnd` 线性增长 |
|
||||
| 快速重传 | 收到 **3 个重复 ACK** 立即重传,不等 RTO |
|
||||
| 快速恢复 | 快重传后 `cwnd` 减半,进入恢复阶段 |
|
||||
| RTO | RFC 6298 RTT 估计(`srtt + 4×rttvar`)+ 指数退避 |
|
||||
|
||||
配套的定时器系统(`src/tcp_timer.c`)在 `pkt_process` 每次迭代中检查:
|
||||
|
||||
| 定时器 | 间隔 | 用途 |
|
||||
|--------|------|------|
|
||||
| RTO 重传 | 动态(指数退避) | 丢包恢复 |
|
||||
| 延迟 ACK | 200 ms | 合并 ACK,减少小包 |
|
||||
| Keepalive | 2 小时空闲 | 探测对端存活 |
|
||||
| 零窗口探测 (ZWP) | 1 s,最多 10 次 | 对端窗口恢复后继续发 |
|
||||
|
||||
## 六、乱序处理:排序链表 + 有界队列
|
||||
|
||||
乱序段(序号超前于接收窗口左缘)进入 `ofo_head` 排序链表(`src/tcp_input.c`),最大 64 个条目:
|
||||
|
||||
```
|
||||
rcv_nxt = 1000 (期望下一个字节)
|
||||
|
||||
收到 seq=1460 的段 → 乱序,挂入 ofo_head(按 seq 排序)
|
||||
收到 seq=2460 的段 → 乱序,插入 ofo_head 第二个位置
|
||||
收到 seq=1000 的段 → 命中期望!立刻交付,然后沿 ofo_head 逐条取出:
|
||||
seq=1460 ✓ 交付
|
||||
seq=2460 ✓ 交付 → ofo_head 清空
|
||||
```
|
||||
|
||||
每个 OFO 段同样从 mempool 分配(见第三篇 P0 优化),上限 256 个,超过即丢弃,防止内存被恶意乱序攻击耗尽。
|
||||
|
||||
## 七、阻塞式 Socket API:用条件变量骗过应用层
|
||||
|
||||
这是最有意思的设计。用户态协议栈的难点之一:应用层习惯**阻塞式**调用(`connect()` 卡住直到连上),但协议处理在另一个 lcore 上跑。解决方案是**条件变量 + 内联轮询**:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant APP as 应用线程 (lcore0)
|
||||
participant PKT as pkt_process (lcore1)
|
||||
|
||||
APP->>APP: nconnect(): 发 SYN, 置状态 SYN_SENT
|
||||
loop 等待握手期间
|
||||
APP->>APP: 内联执行 rte_eth_rx_burst + rte_eth_tx_burst
|
||||
APP->>APP: pthread_cond_timedwait(stream->cond, 1ms)
|
||||
end
|
||||
PKT->>PKT: 收到 SYN+ACK → 状态迁移 ESTABLISHED
|
||||
PKT->>APP: pthread_cond_signal(stream->cond)
|
||||
APP->>APP: 醒来检查 state == ESTABLISHED → 返回成功
|
||||
```
|
||||
|
||||
关键点(`src/tcp_socket.c`):
|
||||
|
||||
- `nrecv()` 阻塞在 `stream->cond` 上;`pkt_process` 收到数据入 `rcvbuf` 后 `signal` 唤醒。
|
||||
- `nconnect()` 在等握手的间隙**顺手帮 pkt_process 干收发包的活**——反正核闲着也是闲着,这段内联轮询把连接建立延迟从"跨核通知"压到了理论下限。
|
||||
- `nclose()`(P3 优化后)用 `rte_pause()` 忙轮询替代条件变量定时等待,关闭路径无阻塞抖动。
|
||||
|
||||
应用层看到的 API 与 POSIX 几乎一致:
|
||||
|
||||
```c
|
||||
int fd = nsocket(); // 创建 socket
|
||||
nconnect(fd, &servaddr, sizeof(servaddr)); // TCP 三次握手
|
||||
ws_connect(fd, host, port, "/ws", NULL); // WS 握手
|
||||
nsend(fd, buf, len, 0); // 发送(TCP 分段 + 拥塞控制)
|
||||
nrecv(fd, buf, len, 0); // 阻塞接收
|
||||
nclose(fd); // 四次挥手
|
||||
```
|
||||
|
||||
## 八、发送路径:从 nsend 到网线
|
||||
|
||||
```
|
||||
nsend(fd, 1MB)
|
||||
→ 按 MSS 1460 切成 ~700 个 ng_tcp_fragment(从 g_frag_pool 分配)
|
||||
→ 逐个入 stream->sndbuf ring(不拷贝,指针传递)
|
||||
└─ pkt_process 每轮迭代:
|
||||
ng_tcp_out(): 从 sndbuf 出队 → 组以太网/IP/TCP 头
|
||||
→ 查 ARP 表解析 MAC(未命中: 发 ARP 请求 + 段重新入队等待)
|
||||
→ 受 cwnd 窗口限制,未发出的段留在 sndbuf
|
||||
→ 入 ring->out → lcore0 出队 → rte_eth_tx_burst 发出
|
||||
```
|
||||
|
||||
ARP 请求做了 **10 次/秒/目的地址** 的限速(`src/arp.h` 链表实现),避免对端无响应时刷爆网络。
|
||||
|
||||
## 九、实测结果
|
||||
|
||||
同一物理链路,`rdtsc` 时间戳测量(完整数据见 `PUBLISH.md`):
|
||||
|
||||
| 指标 | DPDK(本项目) | Linux 内核 TCP | 优势 |
|
||||
|------|---------------|----------------|------|
|
||||
| TCP 连接 | **1.5 ms** | 2.5 ms | 快 40% |
|
||||
| WS 握手 | **3.0 ms** | 5.4 ms | 快 45% |
|
||||
| WS echo RTT | **1.5 ms** | 3.1 ms | 快 52% |
|
||||
| 跨核包传递 | **0.2 μs** | N/A | 硬件极限 |
|
||||
|
||||
代码规模(纯 C,仅依赖 DPDK):
|
||||
|
||||
| 模块 | 行数 |
|
||||
|------|------|
|
||||
| TCP 核心(状态机 + 拥塞控制) | ~3500 |
|
||||
| SACK + 乱序队列 | ~700 |
|
||||
| 拥塞控制 | ~600 |
|
||||
| 定时器 | ~200 |
|
||||
| Socket API | ~500 |
|
||||
| WebSocket | ~400 |
|
||||
| **合计** | **~6000** |
|
||||
|
||||
## 十、总结
|
||||
|
||||
一个"能用"的 TCP 客户端栈其实不需要几万行代码:**11 个状态、4 个定时器、2 条无锁队列、1 张哈希表**,就是全部骨架。真正拉开性能差距的,是绕开内核后的**零拷贝**与**零系统调用**,以及后续一系列数据面优化(P0-P4,见第三篇)。
|
||||
|
||||
下一篇:[在自研 TCP 栈上跑通 WebSocket:RFC 6455 帧编解码实战](./02-WebSocket协议RFC6455实战.md)
|
||||
|
||||
---
|
||||
|
||||
*相关源码:`src/tcp_core.h`(结构体与常量)、`src/tcp_input.c`(状态机)、`src/tcp_output.c`(拥塞控制)、`src/tcp_timer.c`(定时器)、`src/tcp_socket.c`(Socket API)、`src/main.c`(主循环)*
|
||||
@@ -0,0 +1,184 @@
|
||||
# 在自研 TCP 栈上跑通 WebSocket:RFC 6455 帧编解码实战
|
||||
|
||||
> 上一篇讲了自研 TCP 协议栈。这一篇往上走一层:如何在它上面实现 WebSocket 客户端,
|
||||
> 包括 HTTP Upgrade 握手、帧编解码、掩码、粘包半包、控制帧——以及掉进去的坑。
|
||||
|
||||
---
|
||||
|
||||
## 一、为什么是 WebSocket
|
||||
|
||||
金融行情推送(股票、期货行情源)普遍用 WebSocket 对外提供实时数据流:HTTP 完成连接升级后,就是一个双向、低开销的帧协议。相比裸 TCP,它有标准化的握手、心跳(Ping/Pong)和关闭语义;相比 HTTP 轮询,它没有每秒数百次请求的开销。
|
||||
|
||||
在自研 TCP 栈上实现 WS 客户端,整个链路是:
|
||||
|
||||
```
|
||||
DPDK 收包 → 自研 TCP 栈 → stream->rcvbuf → nrecv()
|
||||
→ ws_recv_frame() 解帧 → 应用拿到完整 JSON 行情消息
|
||||
```
|
||||
|
||||
## 二、握手:一次普通的 HTTP 请求,加上点仪式感
|
||||
|
||||
RFC 6455 的握手本质是一个 HTTP/1.1 Upgrade 请求。`ws_connect()`(`src/ws_client.c`)的实现步骤:
|
||||
|
||||
1. 生成 **16 字节随机数**,Base64 编码成 `Sec-WebSocket-Key`;
|
||||
2. 发送 Upgrade 请求;
|
||||
3. 收到响应后检查 `101` 状态码和 `Upgrade: websocket` 头;
|
||||
4. 把响应中 `\r\n\r\n` **之后的字节**(可能是服务端抢先发的第一帧)存入 leftover 缓冲。
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as DPDK WS 客户端
|
||||
participant S as WS 服务端
|
||||
|
||||
C->>S: GET /ws HTTP/1.1<br/>Upgrade: websocket<br/>Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==<br/>Sec-WebSocket-Version: 13
|
||||
Note over S: 校验 key,按 RFC 计算<br/>SHA1(key+magic) 回写 Accept
|
||||
S->>C: HTTP/1.1 101 Switching Protocols<br/>Upgrade: websocket<br/>Sec-WebSocket-Accept: ...
|
||||
Note over C: 检查 101 + Upgrade 头
|
||||
C->>C: 保存 \r\n\r\n 之后的字节<br/>到 g_ws_leftover(防丢首帧)
|
||||
C->>S: 应用消息(TEXT 帧,带掩码)
|
||||
```
|
||||
|
||||
真实的握手代码(节选):
|
||||
|
||||
```c
|
||||
/* 生成随机 16 字节 key,base64 编码 */
|
||||
uint8_t key_bytes[16];
|
||||
ws_random_bytes(key_bytes, 16);
|
||||
base64_encode(key_bytes, 16, ws_key);
|
||||
|
||||
/* 构建 HTTP Upgrade 请求 */
|
||||
snprintf(handshake, sizeof(handshake),
|
||||
"GET %s HTTP/1.1\r\n"
|
||||
"Host: %s:%d\r\n"
|
||||
"Upgrade: websocket\r\n"
|
||||
"Connection: Upgrade\r\n"
|
||||
"Sec-WebSocket-Key: %s\r\n"
|
||||
"Sec-WebSocket-Version: 13\r\n",
|
||||
path, host, port, ws_key);
|
||||
```
|
||||
|
||||
两个值得注意的实现细节:
|
||||
|
||||
- **IPv4 only**:当前用 `inet_pton(AF_INET, ...)` 解析主机,不支持域名(协议栈没有 DNS)。生产场景需要预先解析或加 DNS 模块。
|
||||
- **leftover 缓冲**(`g_ws_leftover[4096]`):服务端可以把手握响应和第一帧数据**写进同一个 TCP 段**。如果解析器只读到 `\r\n\r\n` 就返回,后面的帧字节就丢了。这里把多读的字节存起来,`ws_recv_frame()` 优先从 leftover 消费——这是 WS-over-TCP 最经典的坑之一。
|
||||
|
||||
## 三、帧格式:把 RFC 的图搬进代码注释
|
||||
|
||||
`ws_send_frame()` 的文件注释里直接画了帧结构(RFC 6455 §5.2):
|
||||
|
||||
```
|
||||
0 1 2 3
|
||||
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
+-+-+-+-+-------+-+-------------+-------------------------------+
|
||||
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|
||||
|I|S|S|S| (4) |A| (7) | (16/64) |
|
||||
|N|V|V|V| |S| | (if payload len==126/127) |
|
||||
| |1|2|3| |K| | |
|
||||
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - -+
|
||||
| Masking-key (4 bytes) | Payload Data |
|
||||
+--------------------------------+ - - - - - - - - - - - - - - -+
|
||||
```
|
||||
|
||||
要点:
|
||||
|
||||
| 字段 | 含义 |
|
||||
|------|------|
|
||||
| FIN (1bit) | 是否最后一帧(本项目暂未实现分片发送,但解析器兼容) |
|
||||
| opcode (4bit) | 帧类型:TEXT=0x1 / BINARY=0x2 / CLOSE=0x8 / PING=0x9 / PONG=0xA |
|
||||
| MASK (1bit) | 负载是否掩码。**客户端→服务端必须为 1**,服务端→客户端为 0 |
|
||||
| Payload len (7bit) | 长度三档:≤125 直接存;=126 用后续 2 字节;=127 用后续 8 字节 |
|
||||
|
||||
### 长度三档编码
|
||||
|
||||
```c
|
||||
if (payload_len < 126) {
|
||||
/* 短帧: 7bit 直接存 */
|
||||
} else if (payload_len <= 0xFFFF) {
|
||||
/* 126: 后续 2 字节大端 */
|
||||
} else {
|
||||
/* 127: 后续 8 字节大端 */
|
||||
}
|
||||
```
|
||||
|
||||
## 四、掩码:4 字节 XOR,安全与性能兼得
|
||||
|
||||
客户端发出的每一帧都必须带 4 字节随机掩码,负载逐字节 XOR:
|
||||
|
||||
```c
|
||||
/* ws_send_frame: 生成掩码并逐字节 XOR */
|
||||
for (int i = 0; i < len; i++)
|
||||
masked[i] = payload[i] ^ mask_key[i & 3];
|
||||
```
|
||||
|
||||
掩码的初衷是防止"缓存投毒"(恶意代理场景),实现代价极低——XOR 一条 SIMD 指令可以并行处理 16 字节。微基准实测**帧编解码吞吐 18 Gbps**(50 万次编码基准,`test/bench_tcp.c`)。
|
||||
|
||||
接收侧对应地解掩码:按规范,**服务端发来的帧不应带掩码**,但解析器做了兼容——只要 MASK 位为 1 就解。
|
||||
|
||||
## 五、接收路径:一个健壮的帧解析器长什么样
|
||||
|
||||
`ws_recv_frame()` 的完整决策流程:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["ws_read 读 2 字节头<br/>(优先消费 leftover)"] --> B{payload_len<br/>== 126 / 127?}
|
||||
B -->|126| C[再读 2 字节扩展长度]
|
||||
B -->|127| D[再读 8 字节扩展长度]
|
||||
B -->|≤125| E{masked?}
|
||||
C --> E
|
||||
D --> E
|
||||
E -->|是| F[读 4 字节掩码]
|
||||
E -->|否| G{opcode?}
|
||||
F --> G
|
||||
G -->|CLOSE 0x8| H[消费负载, 返回 0]
|
||||
G -->|PING 0x9| I["读负载 → 自动回 PONG<br/>(原样回显负载)"]
|
||||
G -->|PONG 0xA| J[丢弃负载]
|
||||
G -->|TEXT/BINARY| K{负载 > 应用缓冲?}
|
||||
K -->|是| L["分块读掉整个负载<br/>(保解析器同步) → 返回 -1"]
|
||||
K -->|否| M[读负载 → 解掩码 → 返回]
|
||||
```
|
||||
|
||||
三个容易做错的点,这里都处理了:
|
||||
|
||||
1. **Ping → Pong 自动应答**:协议层自动回 PONG 并原样回显负载,应用层完全无感。配合 `main.c` 的 `-k 30` 参数(每 30 秒主动发一次 Ping),断线能被及时发现。
|
||||
2. **超大帧跳过保持同步**:如果服务端发来一个比应用缓冲区还大的帧(本项目上限 `WS_MAX_FRAME_SIZE = 64KB`),**不能只丢弃返回**——否则后续字节会被当成下一个帧头解析,整个流全部错位。正确做法是分块把负载读完再返回错误(这是 commit `9a596e6` 修复的坑)。
|
||||
3. **CLOSE 帧的负载也要消费**:否则 close 的 payload 会污染下一条连接的首帧。
|
||||
|
||||
## 六、测试体系:不需要 DPDK 也能测协议
|
||||
|
||||
协议层与 DPDK 解耦的关键收益是**可测试性**——用内核 TCP socket 就能完整验证 WS 协议逻辑:
|
||||
|
||||
```bash
|
||||
# 1. 协议单测(内核 TCP 跑 WS 协议,不碰 DPDK)
|
||||
gcc -O2 -o test/test_ws_protocol test/test_ws_protocol.c -lpthread
|
||||
python3 test/ws_test_server.py 8080 # 起 Python 服务端(依赖 websockets)
|
||||
./test_ws_protocol 127.0.0.1 8080 # 跑握手/echo/订阅/推送全流程
|
||||
|
||||
# 2. C 服务端(零依赖,通常跑在远端机器)
|
||||
gcc -O2 -o test/ws_echo_server test/ws_echo_server.c
|
||||
./ws_echo_server 8080
|
||||
|
||||
# 3. 内核 TCP 对比基准(10 轮连接+握手+echo 延迟)
|
||||
python3 test/kern_c_test.py
|
||||
```
|
||||
|
||||
三层测试各司其职:协议单测快速迭代、C 服务端保证低延迟基准、内核对比脚本给出"反超内核"的证据。
|
||||
|
||||
## 七、实测数据
|
||||
|
||||
WS 协议栈在自研 TCP 上的端到端表现(`PUBLISH.md`):
|
||||
|
||||
| 指标 | DPDK WS | 内核 TCP WS | 优势 |
|
||||
|------|---------|-------------|------|
|
||||
| WS 握手 | **3.0 ms** | 5.4 ms | 快 45% |
|
||||
| WS echo RTT | **1.5 ms** | 3.1 ms | 快 52% |
|
||||
| 帧编解码吞吐(CPU 微基准) | **18 Gbps** | N/A | 50 万次编码 |
|
||||
|
||||
## 八、总结
|
||||
|
||||
WebSocket 协议本身很小:一个 HTTP 握手、一个 2-14 字节的帧头、一个 XOR 掩码。真正的工程量在**边界情况**——握手响应里夹带首帧、超大帧保同步、控制帧负载必须消费、粘包半包。把这些坑填平后,WS 层约 400 行代码,跑在自研 TCP 栈上,端到端延迟反超内核 52%。
|
||||
|
||||
下一篇:[微秒级性能优化实战:如何让自研 TCP 栈反超内核 52%](./03-性能优化实战-微秒级数据面.md)
|
||||
|
||||
---
|
||||
|
||||
*相关源码:`src/ws_client.c`(握手 + 帧编解码)、`src/ws_client.h`(opcode 与 API)、`test/test_ws_protocol.c`(协议单测)、`test/ws_echo_server.c`(C 服务端)、`test/kern_c_test.py`(内核对比基准)*
|
||||
@@ -0,0 +1,194 @@
|
||||
# 微秒级性能优化实战:自研 TCP 栈如何反超内核 52%
|
||||
|
||||
> 五轮迭代(P0-P4),把 WS echo RTT 从"落后内核"打到 **1.5ms vs 3.1ms,快 52%**。
|
||||
> 本文记录每一轮优化的测量方法、改动内容和量化收益——包括一次 19 倍的分配性能提升。
|
||||
|
||||
---
|
||||
|
||||
## 一、方法论:先测量,再动手
|
||||
|
||||
优化最忌讳"我觉得这里慢"。本项目建立了一套三层测量体系:
|
||||
|
||||
| 手段 | 测什么 | 工具 |
|
||||
|------|--------|------|
|
||||
| `rdtsc` 打点 | 数据路径各环节的 CPU 周期级延迟 | 代码内埋点,`rte_get_timer_cycles()` |
|
||||
| tcpdump 抓包 | 协议行为正确性(重传、乱序、窗口) | `tcpdump -i eno1 -nn -e 'arp or tcp port 8080'` |
|
||||
| 对照基准 | 同一链路内核 TCP 的表现 | `test/kern_c_test.py`(10 轮连接+握手+echo) |
|
||||
|
||||
基线数据先摸清楚。首版跑通后,跨核数据路径上发现了第一个大问题:
|
||||
|
||||
```
|
||||
rx_enq → pkt_deq (跨核 ring) 0.2 μs ✓ 达标
|
||||
pkt_deq → rcvbuf (协议处理) 0.4 μs ✓ 达标
|
||||
rcvbuf → nrecv_seen (通知) 0.1 μs ✓ 达标
|
||||
pkt_deq → fragment alloc 128 μs ✗✗✗ 超标 20 倍!
|
||||
```
|
||||
|
||||
**热路径上的 `rte_malloc` 一次 128 μs**——这是全部延迟的地板,也是 P0 的靶子。
|
||||
|
||||
## 二、P0:Fragment Mempool——消除热路径 malloc(19× 提升)
|
||||
|
||||
### 问题
|
||||
|
||||
每个 TCP 段到达时,都要 `rte_malloc` 一个 `ng_tcp_fragment` 结构来装数据。通用分配器的开销(锁、元数据、碎片管理)在万兆流量下被放大成主要瓶颈。
|
||||
|
||||
### 改造
|
||||
|
||||
把结构体里的 `*data` 指针改成 **flexible array `data[]`**,让"结构体 + 负载"成为一块连续内存,然后用 `rte_mempool` 预分配池管理:
|
||||
|
||||
```
|
||||
改造前: 改造后:
|
||||
struct ng_tcp_fragment { struct ng_tcp_fragment {
|
||||
... ...
|
||||
unsigned char *data; ← 移除 int read_offset; ← 新增
|
||||
→ rte_malloc(sizeof+len) unsigned char data[]; ← 内嵌
|
||||
→ rte_mempool_get() 一次搞定
|
||||
```
|
||||
|
||||
```c
|
||||
/* src/tcp_core.h — mempool 配置 */
|
||||
#define FRAG_POOL_SIZE 4096 /* 在途 fragment 上限 */
|
||||
#define OFO_POOL_SIZE 256 /* 乱序段上限 */
|
||||
#define FRAG_ELT_SIZE (sizeof(struct ng_tcp_fragment) + TCP_MSS_DEFAULT + 1)
|
||||
|
||||
/* O(1) 无锁分配 */
|
||||
static inline struct ng_tcp_fragment *frag_alloc(int data_len) {
|
||||
struct ng_tcp_fragment *f = NULL;
|
||||
if (rte_mempool_get(g_frag_pool, (void **)&f) < 0) return NULL;
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
`rte_mempool_get` 走 per-lcore 缓存,**无锁、O(1)、缓存友好**。
|
||||
|
||||
### 收益
|
||||
|
||||
```
|
||||
fragment 分配: 128 μs → 6.6 μs (19 倍)
|
||||
```
|
||||
|
||||
一次改动把数据路径地板从 128μs 打到 6.6μs,端到端延迟立刻反超内核。
|
||||
|
||||
## 三、P1:WS 帧动态栈分配——64KB 栈占用降到 ~100 字节
|
||||
|
||||
### 问题
|
||||
|
||||
首版 `ws_recv_frame` 在栈上固定开了一个 64KB 的接收缓冲。应用线程栈占用暴涨,也增加了 cache 污染。
|
||||
|
||||
### 改造
|
||||
|
||||
改为按需 `alloca()` 动态分配——小帧只占几十字节,大帧才多占:
|
||||
|
||||
```
|
||||
改造前: uint8_t buf[WS_MAX_FRAME_SIZE]; // 恒 64KB
|
||||
改造后: 按帧头解析出的 payload_len alloca() // 20B ~ 1500B(典型行情消息)
|
||||
```
|
||||
|
||||
**收益**:典型消息的栈占用 **64KB → 20~1500 字节**。
|
||||
|
||||
## 四、P2:read_offset——消灭 memmove
|
||||
|
||||
### 问题
|
||||
|
||||
`nrecv(buf, n)` 要求读 n 字节,但一个 fragment 里可能有 1500 字节。应用只取 200 字节时,首版把剩余 1300 字节 `memmove` 到头部,等待下次读取——**每次部分读取都平移一次数据**。
|
||||
|
||||
### 改造
|
||||
|
||||
加一个 `read_offset` 字段记录"已经读到哪里",读取时直接偏移,零平移:
|
||||
|
||||
```c
|
||||
// 优化前: // 优化后:
|
||||
rte_memcpy(buf, f->data, n); rte_memcpy(buf, f->data + f->read_offset, n);
|
||||
memmove(f->data, f->data + n, f->length - n); f->read_offset += n;
|
||||
f->length -= n; f->length -= n;
|
||||
```
|
||||
|
||||
**收益**:部分读取场景下数据平移 **1 次 → 0 次**。WS 行情消息常常是"先读 2 字节帧头、再读 4 字节掩码、再读负载"的多次小读模式,这一改动直接命中了高频路径。
|
||||
|
||||
## 五、P3:nclose 忙轮询——关闭路径零阻塞
|
||||
|
||||
### 问题
|
||||
|
||||
首版 `nclose()` 用 `pthread_cond_timedwait` 等待 FIN 确认,关闭路径引入了不可控的唤醒延迟。
|
||||
|
||||
### 改造
|
||||
|
||||
改为 `rte_pause()` 忙轮询:关闭是短时操作(毫秒级),自旋等待的代价远小于上下文切换:
|
||||
|
||||
```c
|
||||
while (state != CLOSED) rte_pause(); // PAUSE 指令,低功耗自旋
|
||||
```
|
||||
|
||||
**收益**:关闭操作无阻塞抖动,配合 P0 让连接生命周期全程低延迟。
|
||||
|
||||
## 六、P4:删除死代码——少 80 行 O(n) 扫描
|
||||
|
||||
### 问题
|
||||
|
||||
旧版 `ng_tcp_out()` 每轮迭代**扫描全部 256 个哈希桶**找待发送的 TCB。后来引入了 `active_head` 活跃连接链表,旧路径成了纯粹的死代码。
|
||||
|
||||
### 改造
|
||||
|
||||
删掉旧扫描路径(-80 行),`pkt_process` 定时器与发送统一走 `active_head` 链表遍历——只碰活跃连接。
|
||||
|
||||
**收益**:空闲连接再多,每轮迭代的开销也恒定;代码更简单,`make` 后行为不变(这是删除代码的最高境界:删除而不改变行为)。
|
||||
|
||||
## 七、五轮优化全景
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph 基线["基线: WS echo RTT > 内核"]
|
||||
A["rte_malloc 128μs<br/>64KB 栈帧<br/>部分读 memmove<br/>condvar 关闭<br/>256 桶扫描"]
|
||||
end
|
||||
subgraph 优化["五轮迭代"]
|
||||
P0["P0 mempool<br/>128→6.6μs (19×)"]
|
||||
P1["P1 动态栈<br/>64KB→~100B"]
|
||||
P2["P2 read_offset<br/>memmove → 0"]
|
||||
P3["P3 忙轮询关闭"]
|
||||
P4["P4 删死代码<br/>-80 行"]
|
||||
end
|
||||
subgraph 结果["最终: 反超内核 52%"]
|
||||
Z["WS echo RTT 1.5ms<br/>跨核 0.2μs<br/>分配 6.6μs"]
|
||||
end
|
||||
A --> P0 --> P1 --> P2 --> P3 --> P4 --> Z
|
||||
```
|
||||
|
||||
最终成绩单(同一物理链路实测):
|
||||
|
||||
| 指标 | DPDK(优化后) | 内核 TCP | 优势 |
|
||||
|------|---------------|----------|------|
|
||||
| TCP 连接 | **1.5 ms** | 2.5 ms | 快 40% |
|
||||
| WS 握手 | **3.0 ms** | 5.4 ms | 快 45% |
|
||||
| WS echo RTT | **1.5 ms** | 3.1 ms | **快 52%** |
|
||||
| 跨核传递 | **0.2 μs** | N/A | 硬件级 |
|
||||
| Fragment 分配 | **6.6 μs** | N/A | 19× 于 malloc |
|
||||
|
||||
CPU 数据面微基准(无网卡依赖,i7-12700 / DDR5 / GCC -O2):
|
||||
|
||||
| 测试项 | 性能 |
|
||||
|--------|------|
|
||||
| 哈希表查找(1000 条目) | **2.1 ns/次** → 4.81 亿次/秒 |
|
||||
| Ring Buffer SP/SC(1M fragments) | **26.87 Gbps** → 230 万 pps |
|
||||
| WS 帧编解码(50 万次) | **18.00 Gbps** |
|
||||
|
||||
rdtsc 延迟追踪(最新实测):
|
||||
|
||||
| 路径 | 延迟 | 说明 |
|
||||
|------|------|------|
|
||||
| rx_enq → pkt_deq | **0.2 μs** | rte_ring 无锁跨核传递 |
|
||||
| pkt_deq → rcvbuf | **0.4 μs** | TCP 11 状态机处理 |
|
||||
| rcvbuf → nrecv_seen | **0.1 μs** | cond 唤醒 |
|
||||
| fragment 分配 | **6.6 μs** | mempool O(1) |
|
||||
|
||||
## 八、复盘:这五轮优化教给我的事
|
||||
|
||||
1. **热路径上的通用设施是最大的敌人**。`rte_malloc` 是"正确"的,但它在每包路径上就是慢。P0 的 19 倍提升来自一次"专用化":把通用分配器换成专用 mempool。
|
||||
2. **优化顺序 = 数据说话**。没有 rdtsc 打点,谁也不会先想到 128μs 的 malloc;有了数据,P0 是唯一的答案。
|
||||
3. **小改动大收益往往藏在"看似无害"的地方**。P2 的 read_offset 只加了一个字段,消灭的却是每次部分读取的内存平移。
|
||||
4. **删除代码也是优化**。P4 没有让任何操作变快,但让每一轮迭代都变短、让系统更可理解——为后续 feature 扫清道路。
|
||||
|
||||
下一篇:[从实验代码到产品:DPDK 行情接入网关的产品化实践](./04-DPDK行情接入网关产品化.md)
|
||||
|
||||
---
|
||||
|
||||
*相关源码:`src/tcp_core.h`(mempool 与 fragment)、`src/tcp_socket.c`(P2/P3)、`src/ws_client.c`(P1)、`src/tcp_core.c`(P4)、`test/bench_tcp.c`(微基准)、`PERF_REPORT.md`(完整数据)*
|
||||
@@ -0,0 +1,171 @@
|
||||
# 从实验代码到产品:DPDK 行情接入网关的产品化实践
|
||||
|
||||
> 前三篇讲完了协议栈、WebSocket 和性能优化。这一篇把视角切到产品:
|
||||
> 如何把这套技术栈包装成一个**金融行情接入网关**(`app/market_feed.c`),
|
||||
> 以及它的部署方式、商用评估和演进路线。
|
||||
|
||||
---
|
||||
|
||||
## 一、产品定位:金融行业最典型的低延迟场景
|
||||
|
||||
在量化交易链路中,行情数据(股票、期货的实时报价)的到达延迟直接影响策略的成交质量。典型的接入形态:
|
||||
|
||||
```
|
||||
交易所 / 行情提供商 → [WebSocket 行情流] → 行情接入网关 → 下游交易系统
|
||||
↑
|
||||
本项目占据的位置
|
||||
```
|
||||
|
||||
网关要做的事:**订阅多品种行情 → 以微秒级延迟接收 → 解析 → 转发/统计**。用自研 TCP 栈的收益在这里被放大——行情消息高频、小包、长连接,正是内核协议栈开销占比最大的流量形态。
|
||||
|
||||
## 二、产品功能(`app/market_feed.c`,约 420 行)
|
||||
|
||||
| 功能 | 说明 |
|
||||
|------|------|
|
||||
| 多品种订阅 | 通过 `-s '{"action":"subscribe","symbols":[...]}'` 订阅任意品种 |
|
||||
| **延迟直方图统计** | 每品种 min / avg / P50 / P95 / P99 / max,微秒级 |
|
||||
| 消息速率跟踪 | 每品种 msg/s 实时计算 |
|
||||
| 定时统计输出 | `-t` 秒可配置刷新间隔(默认 5s) |
|
||||
| 优雅退出 | SIGINT/SIGTERM 触发有序关闭 |
|
||||
| 断线检测 | 心跳 Ping + 断线统计 |
|
||||
|
||||
架构与基础客户端同构(lcore0 RX/TX + lcore1 协议处理),产品层增加的是**延迟测量**和**统计展示**:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph S["行情服务端 (demo/market_data_server.py)"]
|
||||
S1["AAPL / GOOGL / MSFT / TSLA<br/>每条消息带 timestamp"]
|
||||
end
|
||||
subgraph G["DPDK 行情网关 (app/market_feed.c)"]
|
||||
L0["lcore0: RX burst +<br/>ws_recv_frame"]
|
||||
L0 -->|"ring→in"| L1["lcore1: TCP/WS<br/>状态机处理"]
|
||||
L1 -->|"rcvbuf + cond"| L0
|
||||
subgraph STATS["统计层 (lcore0)"]
|
||||
H["延迟直方图<br/>64 桶 × 1μs 分辨率"]
|
||||
R["per-symbol<br/>msg/s 速率"]
|
||||
end
|
||||
L0 --> STATS
|
||||
STATS -->|"每 5s"| OUT["终端统计表输出"]
|
||||
end
|
||||
S1 -->|"WS 行情流 (TCP)"| G
|
||||
```
|
||||
|
||||
## 三、延迟直方图:64 桶 × 1μs 分辨率的极简实现
|
||||
|
||||
延迟 = 消息携带的服务端 `timestamp` 到网关收到时刻的差值。测量与统计实现非常轻量(`app/market_feed.c`):
|
||||
|
||||
```c
|
||||
#define LAT_BUCKETS 64 /* 直方图桶数 (1us 分辨率) */
|
||||
|
||||
typedef struct {
|
||||
uint64_t buckets[LAT_BUCKETS]; /* 延迟分布直方图 */
|
||||
uint64_t min_us, max_us, sum_us; /* 累计值 */
|
||||
uint64_t count;
|
||||
} symbol_stats_t;
|
||||
```
|
||||
|
||||
- 每收一条行情,算 `now - msg_timestamp`,`buckets[min(lat, 63)]++`;
|
||||
- 百分位直接在累计分布上找分位点(P50/P95/P99);
|
||||
- min/max/avg 用累计值 O(1) 维护;
|
||||
- 输出时打印 ASCII 表格(见下文),无需外部依赖。
|
||||
|
||||
这套实现约 100 行 C,换来了产品的核心卖点:**可量化的延迟证据**。
|
||||
|
||||
## 四、端到端演示
|
||||
|
||||
```bash
|
||||
# 1. 编译产品
|
||||
make app # → build/market_feed
|
||||
|
||||
# 2. 启动模拟行情服务端(零依赖 Python,100 msg/s)
|
||||
python3 demo/market_data_server.py 8080 100
|
||||
|
||||
# 3. 启动 DPDK 行情网关
|
||||
./build/market_feed -l 0-1 -n 2 \
|
||||
-d $DPDK_DRV/librte_mempool_ring.so \
|
||||
-d $DPDK_DRV/librte_net_ixgbe.so \
|
||||
-- -a <SERVER_IP> -p 8080 -l <LOCAL_IP> \
|
||||
-s '{"action":"subscribe","symbols":["AAPL","GOOGL"]}' -t 5
|
||||
```
|
||||
|
||||
每 5 秒输出一次延迟统计表:
|
||||
|
||||
```
|
||||
┌────────┬──────────┬────────┬────────┬────────┬────────┬──────┐
|
||||
│ Symbol │ Msgs │ Rate │ Min │ Avg │ P50 │ P99 │
|
||||
├────────┼──────────┼────────┼────────┼────────┼────────┼──────┤
|
||||
│ AAPL │ 498 │ 99 │ 598 │ 821 │ 780 │ 1245 │
|
||||
│ GOOGL │ 497 │ 99 │ 587 │ 815 │ 772 │ 1230 │
|
||||
└────────┴──────────┴────────┴────────┴────────┴────────┴──────┘
|
||||
```
|
||||
|
||||
> 延迟口径:服务端 `timestamp` → 网关 `rte_get_timer_cycles` 的端到端时间,
|
||||
> 包含服务端序列化 + 内核 TCP 发送 + 网络传输 + DPDK 协议栈 + WS 解码全链路。
|
||||
|
||||
## 五、部署清单
|
||||
|
||||
生产部署需要三样东西(详见仓库 README / CLAUDE.md):
|
||||
|
||||
1. **Hugepages**:`echo 1024 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages`
|
||||
2. **网卡绑定**:`dpdk-devbind --bind=vfio-pci <pci_addr>`(DPDK 24.11 usertools)
|
||||
3. **PMD 插件**:运行参数 `-d` 指定 mempool/NIC 驱动(Makefile 按共享库链接,PMD 运行时加载)
|
||||
|
||||
云上部署:AWS `c5n.large`+(ENA PMD)或 `c5n.metal`(SR-IOV),安全组放行行情端口。
|
||||
|
||||
## 六、商用评估:什么能卖,什么不能
|
||||
|
||||
### 就绪度分级
|
||||
|
||||
| 梯队 | 场景 | 就绪度 |
|
||||
|------|------|--------|
|
||||
| 第一梯队(当前可商用) | **金融行情推送网关**(券商/量化/交易所) | ★★★★★ |
|
||||
| | 微服务 RPC 网关(长连接复用,10ms→1-2ms) | ★★★★☆ |
|
||||
| 第二梯队(3-6 月加固) | 5G 核心网 NRF、FIX 协议引擎 | ★★★★☆ |
|
||||
| 第三梯队(6-12 月) | CDN 边缘推送、工业 IoT 控制面 | ★★★☆☆ |
|
||||
|
||||
### 竞品对比
|
||||
|
||||
| 维度 | 本项目 | Solarflare Onload | 内核 TCP |
|
||||
|------|--------|-------------------|----------|
|
||||
| 部署成本 | 免费 + 2 天 | $2000+/lic | 免费 |
|
||||
| WS echo 延迟 | 1.5 ms | <1 ms | 3.1 ms |
|
||||
| 零拷贝 | ✅ | ✅ | ❌ |
|
||||
| 可定制性 | 高(开箱即改) | 中(闭源) | 低 |
|
||||
| 成熟度 | 教学/验证级 | 生产级 | 30 年生产级 |
|
||||
|
||||
**推荐切入路径**:金融行情网关(1-2 月 MVP)→ 微服务 RPC(3-4 月)→ 5G 边缘(6 月+)→ CDN 推送(12 月+)。
|
||||
|
||||
### 不适合的场景(要诚实)
|
||||
|
||||
| 场景 | 原因 |
|
||||
|------|------|
|
||||
| 公网 WebSocket 服务 | 无 SYN flood / DDoS 防护 |
|
||||
| 浏览器客户端 | 需要 TLS (WSS),当前无 TLS 集成 |
|
||||
| 百万并发连接 | 无 epoll 模型、无连接池管理 |
|
||||
| 跨平台部署 | 依赖 DPDK + hugepages + 专用 NIC |
|
||||
|
||||
## 七、演进路线图
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
P5["P5 修复多核 RSS<br/>active_head 并发"] --> P6["P6 WS 分片发送<br/>header+payload 分离"]
|
||||
P6 --> P7["P7 网卡 checksum offload<br/>省 5% CPU"]
|
||||
P7 --> P8["P8 事件驱动替代忙轮询<br/>空闲降功耗"]
|
||||
P8 --> P9["P9 TSO/GRO 软件实现<br/>大消息吞吐翻倍"]
|
||||
|
||||
M["商业路线: Prometheus 指标<br/>JSON 配置 → 多核 RSS + TLS<br/>→ HA 双活 + 热升级"] --> P5
|
||||
```
|
||||
|
||||
其中 P5 已有实验骨架:`feature/multi-core` 分支实现了 per-worker TCB 表、ring 数组与 RX 哈希分流,当前被主线程与 worker 对 `active_head` 链表的并发竞争阻塞(表现为 TCP 连接超时)。修好后吞吐可望随核数线性扩展。
|
||||
|
||||
## 八、总结
|
||||
|
||||
从 6000 行实验代码到可演示产品,中间只隔了 **100 行直方图 + 1 张统计表 + 1 个演示脚本**——但这一步把"技术能力"变成了"客户能看懂的东西"。低延迟基础设施类产品的说服力,永远来自一张带 P99 的延迟表。
|
||||
|
||||
---
|
||||
|
||||
*相关文件:`app/market_feed.c`(产品实现)、`app/README.md`(产品文档)、`demo/market_data_server.py`(模拟行情源)、`PRODUCT_ANALYSIS.md`(商用场景分析)、`PERF_REPORT.md`(性能数据)*
|
||||
|
||||
---
|
||||
|
||||
**系列目录**:[① 用户态 TCP 协议栈](./01-从零实现用户态TCP协议栈.md) · [② WebSocket 实战](./02-WebSocket协议RFC6455实战.md) · [③ 性能优化实战](./03-性能优化实战-微秒级数据面.md) · [④ 产品化实践](./04-DPDK行情接入网关产品化.md)
|
||||
@@ -0,0 +1,29 @@
|
||||
# 项目博客系列
|
||||
|
||||
基于本仓库(DPDK 用户态 TCP/IP 协议栈 + WebSocket 客户端)的技术博客,共 4 篇。
|
||||
|
||||
## 目录
|
||||
|
||||
| # | 文章 | 主题 | 适合读者 |
|
||||
|---|------|------|----------|
|
||||
| 01 | [从零实现一个用户态 TCP/IP 协议栈](01-从零实现用户态TCP协议栈.md) | 架构设计、11 状态机、拥塞控制、阻塞式 Socket API | 想理解整体设计的读者 |
|
||||
| 02 | [在自研 TCP 栈上跑通 WebSocket](02-WebSocket协议RFC6455实战.md) | RFC 6455 握手、帧编解码、掩码、粘包半包 | 对协议实现细节感兴趣的读者 |
|
||||
| 03 | [微秒级性能优化实战](03-性能优化实战-微秒级数据面.md) | P0-P4 优化历程、测量方法、反超内核 52% 的全过程 | 性能工程师、C 开发者 |
|
||||
| 04 | [从实验代码到产品:DPDK 行情接入网关](04-DPDK行情接入网关产品化.md) | 延迟直方图、产品定位、部署与商用评估 | 产品经理、架构师 |
|
||||
|
||||
## 渲染说明
|
||||
|
||||
- 所有文档为 **Markdown + Mermaid 图表 + ASCII 图**,推荐在 GitHub / Gitea / Typora / VS Code(装 Markdown Preview Mermaid Support 插件)中阅读,图表会自动渲染。
|
||||
- 文中的数据均来自仓库实测:见 `PERF_REPORT.md`、`PUBLISH.md`、`README.md`。
|
||||
- 文中标注的源码位置(`src/tcp_input.c` 等)可直接跳转到仓库对应文件核对。
|
||||
|
||||
## 一句话速览
|
||||
|
||||
```
|
||||
用 6000 行 C 语言 + DPDK 24.11 从零实现用户态 TCP/IP 协议栈与 WebSocket 客户端:
|
||||
· 零内存拷贝(收发各 0 次)
|
||||
· 零系统调用(收发各 0 次)
|
||||
· 跨核数据传递 0.2 μs
|
||||
· TCP 连接延迟 1.5 ms(内核 2.5 ms,快 40%)
|
||||
· WS echo RTT 1.5 ms(内核 3.1 ms,快 52%)
|
||||
```
|
||||
Reference in New Issue
Block a user