简介:这份资源面向FPGA与网络加速方向的学习者及工程师,提供一套用Verilog硬件描述语言实现的TCP代理程序,用于在客户端与服务器之间透明转发TCP连接,可服务于负载均衡、访问控制与性能优化等场景。压缩包为zip格式,整体约57.72MB,文件总数与类型明细上游暂未提供,从描述看应包含Verilog源码及工程相关文件。已有568人学习下载,具备一定参考热度。资源价值在于完整呈现硬件级TCP代理的实现思路:涵盖SYN/SYN+ACK/ACK握手、序列号与确认号处理、四次挥手连接释放,以及超时重传、连接重置等错误恢复机制;同时涉及连接状态表维护、多级队列与调度下的并发连接处理,并强调减少冗余计算与内存访问的吞吐优化。适合希望理解FPGA并行加速网络功能、动手实践TCP协议硬件实现的中高级读者对照研读。
1. 用硬件描述语言写一个 TCP 代理:这项目到底在解决什么问题
第一次看到「基于 Verilog 语言的 TCP 代理程序」这个标题,我下意识以为是把软件代理逻辑硬塞进 FPGA 里跑,翻完源码才明白它的真实定位:在硬件侧实现一个能接管 TCP 连接、做转发与加速的中间层,让原本跑在通用 CPU 上的代理逻辑下沉到可编程逻辑里。它适合两类人——手里有 FPGA 开发板、想把网络功能加速落到硬件的工程师,以及想通过一个完整可综合工程理解 TCP 状态机、握手拆包、数据搬运怎么在 RTL 里落地的学习者。软件代理你随手能写,但把它写成时序逻辑、跑在时钟沿上,是完全不同的一套思维方式,这个项目就是那条从软件思维跨到硬件思维的桥。
2. 架构拆解:一个 TCP 代理在 RTL 里被切成哪几块
硬件代理和软件代理最大的差别在于「没有操作系统帮你兜底」。软件里一个 socket 调用背后是内核协议栈,而在 Verilog 里,从物理层收字节、解析以太网帧、拆 IP 包、维护 TCP 状态机、再到把数据写回发送通道,每一步都得自己用状态机和 FIFO 拼出来。这个项目的整体思路是把代理拆成一条流水线:接收侧负责帧解析与校验,连接管理侧维护每条 TCP 连接的状态,转发侧做数据搬运与重组,最后经发送侧封包发出。理解这条流水线,比死记某个模块的端口更重要。
2.1 顶层模块与数据通路划分
顶层模块通常只做三件事:例化子模块、连线、给全局时钟与复位。真正干活的是下面几条并行通路。接收通路从 PHY 接口拿到字节流,先过以太网帧解析,剥掉前导码和 FCS,再交给 IP 解析层判断协议号与目的地址,命中代理规则的包才继续往 TCP 层送。发送通路反过来,把 TCP 层组好的段加上 IP 头、以太网头,推回 PHY。中间还有一个连接表模块,用一张小容量的表记录五元组到内部通道号的映射,相当于软件里的连接哈希表,只不过这里是用寄存器堆或 Block RAM 实现的。
// 顶层:只做例化和连线,保持薄 module tcp_proxy_top ( input wire clk, input wire rst_n, // 面向 PHY 的收发接口 input wire [7:0] rx_data, input wire rx_valid, output wire [7:0] tx_data, output wire tx_valid, // 代理规则配置口 input wire [31:0] cfg_listen_ip, input wire [15:0] cfg_listen_port ); wire [7:0] ip_payload_data; wire ip_payload_valid; wire [7:0] tcp_payload_data; wire tcp_payload_valid; // 接收侧:帧解析 -> IP 解析 -> TCP 处理 eth_rx_parser u_eth_rx ( .clk(clk), .rst_n(rst_n), .rx_data(rx_data), .rx_valid(rx_valid), .payload_data(ip_payload_data), .payload_valid(ip_payload_valid) ); ip_rx_parser u_ip_rx ( .clk(clk), .rst_n(rst_n), .in_data(ip_payload_data), .in_valid(ip_payload_valid), .out_data(tcp_payload_data), .out_valid(tcp_payload_valid) ); tcp_proxy_core u_tcp_core ( .clk(clk), .rst_n(rst_n), .in_data(tcp_payload_data), .in_valid(tcp_payload_valid), .cfg_listen_ip(cfg_listen_ip), .cfg_listen_port(cfg_listen_port), .tx_data(tx_data), .tx_valid(tx_valid) ); endmodule这段顶层代码刻意保持「薄」,所有解析逻辑都下沉到子模块。cfg_listen_ip和cfg_listen_port是代理的监听配置,改这两个寄存器就能换代理目标,不用动 RTL 结构。rx_valid/tx_valid是典型的 valid-ready 握手思路的简化版,实际工程里我一般会补上 ready 信号做反压,否则下游 FIFO 满了会丢包——这是硬件代理最容易翻车的地方之一。
2.2 TCP 状态机的 RTL 实现要点
TCP 代理的核心是状态机。软件里你可以用阻塞调用等三次握手完成,硬件里不行,必须把 LISTEN、SYN_RCVD、ESTABLISHED、FIN_WAIT 这些状态显式编码,每个时钟沿根据收到的标志位跳转。项目里通常用一个state寄存器加组合逻辑的next_state来实现,收到 SYN 且目的端口匹配就进 SYN_RCVD 并回 SYN-ACK,收到 ACK 进 ESTABLISHED,之后数据段才允许转发。
// TCP 连接状态机的核心跳转(简化) localparam S_LISTEN = 3'd0, S_SYN_RCVD = 3'd1, S_ESTABLISHED = 3'd2, S_FIN_WAIT = 3'd3, S_CLOSED = 3'd4; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= S_LISTEN; end else begin case (state) S_LISTEN: // 收到 SYN 且端口命中监听配置,进入半连接 if (rx_syn && port_match) state <= S_SYN_RCVD; S_SYN_RCVD: // 收到对端 ACK,握手完成 if (rx_ack) state <= S_ESTABLISHED; else if (rx_rst) state <= S_CLOSED; S_ESTABLISHED: // 任一方发 FIN 进入关闭流程 if (rx_fin) state <= S_FIN_WAIT; S_FIN_WAIT: if (rx_ack) state <= S_CLOSED; S_CLOSED: state <= S_LISTEN; // 回收连接,回到监听 default: state <= S_LISTEN; endcase end endrx_syn、rx_ack、rx_fin、rx_rst是从 TCP 头标志位字段解析出来的单比特信号,port_match是比较目的端口与cfg_listen_port的结果。这里有个血泪经验:真实 TCP 里序列号、确认号必须维护,否则对端会认为你发的段非法直接 RST。项目里一般用两个 32 位寄存器seq_num和ack_num跟踪,发送数据时seq_num递增,收到数据时ack_num更新。状态机本身不难,难的是把序列号维护、窗口判断、超时重传这些「软件里内核帮你做的事」一件件补上,这也是读这个项目最值钱的部分。
2.3 数据搬运与缓冲:FIFO 怎么用才不丢包
代理的本质是搬运数据,硬件里搬运靠 FIFO。接收方向解析出的 payload 先写进接收 FIFO,转发逻辑从 FIFO 读出再组发送段。FIFO 深度是玄学也是硬指标:太浅,突发流量一来就溢出丢包;太深,Block RAM 不够用。我一般按「最大段长 × 在途段数」估算,比如 MSS 1460 字节、允许 4 个段在途,深度至少 8KB 才稳。项目里通常用双时钟 FIFO 做跨时钟域,因为 PHY 侧和内部处理时钟可能不同频。
// 接收缓冲 FIFO 例化(异步 FIFO,跨时钟域) async_fifo #( .DATA_WIDTH (8), .DEPTH (8192) // 深度按突发流量估算,别抠 ) u_rx_fifo ( .wr_clk (phy_clk), .wr_en (tcp_payload_valid), .wr_data (tcp_payload_data), .rd_clk (core_clk), .rd_en (fwd_ready), .rd_data (fwd_data), .full (rx_fifo_full), // 满信号必须接回反压 .empty (rx_fifo_empty) );DEPTH是深度参数,full信号一定要接回上游做反压,否则就是前面说的丢包。wr_clk和rd_clk分开是因为 PHY 恢复时钟和内部逻辑时钟往往不同源,跨时钟域不做同步直接传数据,等着你的就是偶发的、极难复现的数据错乱——这种 bug 抓波形能抓一整天。
3. 从源码到跑起来:仿真、综合与上板验证
拿到一个 Verilog 工程,最怕的是「能综合但跑不通」。这个项目的验证路径分三层:先跑行为级仿真确认协议逻辑对,再综合看时序和资源,最后上板用真实流量打。三层缺一层,问题就会以最难受的方式暴露在下一层。
3.1 仿真环境搭建与 testbench 编写
仿真用常见的开源工具链即可,Icarus Verilog 或 Verilator 都能跑。关键是 testbench 要能模拟对端 TCP 行为:发 SYN、等 SYN-ACK、回 ACK、发数据、发 FIN。项目里一般带一个简化的对端模型,用任务(task)封装每个握手步骤。
// testbench 片段:模拟一次完整握手 + 数据发送 initial begin // 1. 发 SYN send_tcp_segment(SYN_FLAG, 32'h1000, 32'h0, 16'd80, 8'h00); // 2. 等代理回 SYN-ACK wait_for_segment(SYN_FLAG | ACK_FLAG); // 3. 回 ACK,握手完成 send_tcp_segment(ACK_FLAG, 32'h1001, 32'h2001, 16'd80, 8'h00); // 4. 发一段数据 send_tcp_segment(ACK_FLAG | PSH_FLAG, 32'h1001, 32'h2001, 16'd80, 8'hAB); // 5. 发 FIN 关闭 send_tcp_segment(FIN_FLAG | ACK_FLAG, 32'h1002, 32'h2001, 16'd80, 8'h00); #1000 $finish; endsend_tcp_segment的四个参数分别是标志位、序列号、确认号、目的端口和 payload。跑仿真时重点看波形里状态机的跳转是否和预期一致,以及 FIFO 的full有没有误触发。我习惯在 testbench 里加断言(assertion),比如「进入 ESTABLISHED 后 10 个周期内必须能收数据」,比人眼看波形靠谱得多。
3.2 综合约束与时序收敛
综合阶段最容易踩的是时序。TCP 状态机里组合逻辑一多,关键路径就长,时钟频率上不去。约束文件里要明确时钟周期、输入输出延迟,综合工具报的 slack 为负就得回去拆逻辑或加流水级。资源方面,连接表用寄存器堆还是 Block RAM 要看连接数:几十条连接用寄存器堆够,上百条就得上 BRAM。
| 资源项 | 小规模配置 | 大规模配置 | 说明 |
|---|---|---|---|
| 连接表存储 | 寄存器堆 | Block RAM | 按并发连接数选 |
| 接收 FIFO | 4KB | 16KB | 按突发流量估算 |
| 时钟频率 | 100MHz | 125MHz | 受关键路径限制 |
| 逻辑单元 | 较少 | 较多 | 状态机与解析逻辑占大头 |
这张表不是让你照抄,而是提醒你综合前先想清楚规模。我见过有人拿小配置直接上高并发场景,结果连接表溢出,新连接全被拒,排查半天以为是协议 bug,其实是资源没算够。
3.3 上板联调:用真实流量验证代理行为
上板才是真正的考试。把 bitstream 烧进 FPGA,网线接上,用另一台机器当客户端去连代理监听端口。验证顺序建议是:先 ping 通确认物理链路,再用最简单的 TCP 客户端发一个字节看能不能转发,最后压流量看稳定性和丢包率。抓包工具在这步是刚需,对比进出两个方向的包,能快速定位是解析错了还是转发错了。
提示:上板前务必确认 PHY 的接口时序和复位极性,不同开发板差异很大,接反了就是「链路 up 但一个包都收不到」的经典现象。
4. 避坑与排查:硬件代理最容易翻车的几个点
4.1 现象:仿真通过,上板收不到任何包
原因多半在 PHY 接口层。仿真里 testbench 直接喂字节,绕过了物理层时序;上板后 MII/RGMII 的时钟、数据对齐、复位极性任何一项不对,数据就进不来。解决方法是先用一个极简的「回环」逻辑把收到的字节原样发回,确认物理层通了再叠协议解析。这一步别省,省了后面所有调试都是盲人摸象。
4.2 现象:偶发数据错乱,重启就好
典型的跨时钟域问题。接收 FIFO 的读写时钟不同源却没做同步,或者格雷码指针没处理好,就会偶发错位。这种 bug 最折磨人,因为复现概率低。解决办法是老老实实用成熟的异步 FIFO IP 或标准双触发器同步器,别自己手搓跨时钟域逻辑。我一般会在关键跨时钟路径上加断言监控,出问题能第一时间定位。
4.3 现象:高负载下丢包严重
FIFO 深度不够或反压没接。突发流量超过 FIFO 容量,full拉高但上游没停,数据就丢了。解决是两件事一起做:加深 FIFO,以及把full信号真正接回上游的 valid 通路做反压。只加深不接反压,只是把丢包推迟到更大的突发而已。
4.4 现象:连接建立后对端立刻 RST
序列号和确认号维护错了。TCP 对序列号极其敏感,你回的 ACK 号对不上,对端直接判定连接非法。排查时抓包看你的 SYN-ACK 里 seq/ack 字段,和标准协议栈的行为对比。这个坑我踩过,最后发现是ack_num在收到数据后忘了加 payload 长度。
5. 二次开发:把代理逻辑改成你自己的加速通路
这个工程真正的价值不在「能代理」,而在它给了一套可综合的 TCP 处理骨架,你可以把转发逻辑换成任何你想加速的处理。比如在tcp_proxy_core的数据通路上插一个自定义计算模块,收到数据先做变换再转发,就变成了一个硬件加速中间件。改造时我建议守住三条线:第一,别动状态机和序列号维护,那是协议正确性的地基;第二,新增模块必须遵守 valid-ready 握手,插在 FIFO 之后、组包之前最安全;第三,每加一个模块就补一组仿真用例,硬件调试成本远高于软件,仿真阶段多花一小时,上板少熬一晚上。
验证改造是否成功,我习惯用「对照法」:同一份流量分别打给原始工程和改造后的工程,抓包对比输出,字节级一致才算过。下面是一个把 payload 做简单异或变换的插入示例,思路可以照搬到你自己的算法上。
// 在转发通路上插入自定义处理:对 payload 做异或变换 module payload_xform ( input wire clk, input wire rst_n, input wire [7:0] in_data, input wire in_valid, output reg [7:0] out_data, output reg out_valid ); always @(posedge clk or negedge rst_n) begin if (!rst_n) begin out_data <= 8'h00; out_valid <= 1'b0; end else begin out_valid <= in_valid; // 握手信号透传,别丢拍 out_data <= in_valid ? (in_data ^ 8'h5A) : 8'h00; end end endmodulein_valid透传到out_valid是关键,少打一拍下游就会少收一个字节,这种错在抓包里表现为「数据莫名少一截」,极难定位。8'h5A只是示例掩码,换成你的算法即可。把这个模块插在接收 FIFO 和组包逻辑之间,就完成了一次最小改造。
从那以后我每次改这类硬件代理,都强制先跑一遍「回环 + 对照抓包」再动协议逻辑,宁可多花半天验证,也不愿在上板后对着波形猜。希望这个拆解能帮你少走点弯路,把这份源码真正跑成你自己的加速通路。
本文还有配套的精品资源,点击获取