diff --git a/blog/01-from-zero-userspace-tcp-stack.md b/blog/01-from-zero-userspace-tcp-stack.md
new file mode 100644
index 0000000..c3bde87
--- /dev/null
+++ b/blog/01-from-zero-userspace-tcp-stack.md
@@ -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`(主循环)*
diff --git a/blog/02-websocket-rfc6455-in-practice.md b/blog/02-websocket-rfc6455-in-practice.md
new file mode 100644
index 0000000..58d6a35
--- /dev/null
+++ b/blog/02-websocket-rfc6455-in-practice.md
@@ -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
Upgrade: websocket
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Version: 13
+ Note over S: 校验 key,按 RFC 计算
SHA1(key+magic) 回写 Accept
+ S->>C: HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Sec-WebSocket-Accept: ...
+ Note over C: 检查 101 + Upgrade 头
+ C->>C: 保存 \r\n\r\n 之后的字节
到 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 字节头
(优先消费 leftover)"] --> B{payload_len
== 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
(原样回显负载)"]
+ G -->|PONG 0xA| J[丢弃负载]
+ G -->|TEXT/BINARY| K{负载 > 应用缓冲?}
+ K -->|是| L["分块读掉整个负载
(保解析器同步) → 返回 -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`(内核对比基准)*
diff --git a/blog/03-performance-optimization-microsecond-dataplane.md b/blog/03-performance-optimization-microsecond-dataplane.md
new file mode 100644
index 0000000..cf5acc8
--- /dev/null
+++ b/blog/03-performance-optimization-microsecond-dataplane.md
@@ -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
64KB 栈帧
部分读 memmove
condvar 关闭
256 桶扫描"]
+ end
+ subgraph 优化["五轮迭代"]
+ P0["P0 mempool
128→6.6μs (19×)"]
+ P1["P1 动态栈
64KB→~100B"]
+ P2["P2 read_offset
memmove → 0"]
+ P3["P3 忙轮询关闭"]
+ P4["P4 删死代码
-80 行"]
+ end
+ subgraph 结果["最终: 反超内核 52%"]
+ Z["WS echo RTT 1.5ms
跨核 0.2μs
分配 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`(完整数据)*
diff --git a/blog/04-dpdk-market-data-gateway-productization.md b/blog/04-dpdk-market-data-gateway-productization.md
new file mode 100644
index 0000000..7f8e6e2
--- /dev/null
+++ b/blog/04-dpdk-market-data-gateway-productization.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
每条消息带 timestamp"]
+ end
+ subgraph G["DPDK 行情网关 (app/market_feed.c)"]
+ L0["lcore0: RX burst +
ws_recv_frame"]
+ L0 -->|"ring→in"| L1["lcore1: TCP/WS
状态机处理"]
+ L1 -->|"rcvbuf + cond"| L0
+ subgraph STATS["统计层 (lcore0)"]
+ H["延迟直方图
64 桶 × 1μs 分辨率"]
+ R["per-symbol
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 -p 8080 -l \
+ -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 `(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
active_head 并发"] --> P6["P6 WS 分片发送
header+payload 分离"]
+ P6 --> P7["P7 网卡 checksum offload
省 5% CPU"]
+ P7 --> P8["P8 事件驱动替代忙轮询
空闲降功耗"]
+ P8 --> P9["P9 TSO/GRO 软件实现
大消息吞吐翻倍"]
+
+ M["商业路线: Prometheus 指标
JSON 配置 → 多核 RSS + TLS
→ 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)
diff --git a/blog/README.md b/blog/README.md
new file mode 100644
index 0000000..74300cb
--- /dev/null
+++ b/blog/README.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%)
+```