ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

VB.NET实现TCP/IP通讯转发:源码解析与Socket编程实践

VB.NET实现TCP/IP通讯转发:源码解析与Socket编程实践 简介这是一份面向VB.NET开发者的TCP/IP通讯转发程序源码适合新手及有一定经验的开发人员参考借鉴。针对现有抓包工具不够直观、只能转发却看不到数据包内容的痛点作者自行编写了这套工具采用VS2019开发接收端使用Listener监听转发目的端为TcpClient客户端可实现客户端与服务端之间的双向转发便于在调试通讯时直观查看数据流向。资源包共33个文件约456KB包含6个vb源码文件、2个resx资源文件、2个config配置文件、2个exe可执行程序、1个vbproj工程文件与1个sln解决方案文件以及pdb调试符号、dll依赖库、png界面截图等工程结构完整可直接用VS2019打开编译运行。目前已有1058人学习下载。读者可从中获取完整的通讯转发实现思路、监听与客户端连接配置方法以及界面与工程组织方式便于二次开发或集成到自己的调试工具中。1. 从一份 VB.NET 源码看 TCP/IP 转发到底解决什么问题手里这份 VB.NET 实现 TCP/IP 通讯转发功能的程序源码解决的是一个很具体的场景A 设备只能连某个固定端口B 服务只监听另一个地址中间需要一层不解析业务、只做字节搬运的转发层。它适合做协议调试、串口服务器对接、老系统迁移时的临时桥接也适合刚接触 Socket 编程、想找一个能跑起来的完整工程对照学习的人。源码本身不依赖第三方通讯库用 .NET 自带的 System.Net.Sockets 就能编译运行这一点对还在维护 WinForm 老项目的同学比较友好。下面按「这份源码怎么落地 → 参数怎么调 → 坑在哪」的顺序拆开讲中间会给出可直接抄的监听与转发核心代码。2. 转发程序的骨架监听、接入、双向泵2.1 为什么用 TcpListener 双线程泵而不是异步回调这份源码走的是同步阻塞模型一个 TcpListener 负责接受客户端接入每接入一个连接就开两条线程一条读客户端写目标端一条读目标端写客户端。选这个模型的原因很实际——转发逻辑本身不复杂同步写法调试直观断点能停在 Read 上出问题一眼看得出卡在哪。异步回调BeginRead/EndRead虽然吞吐更好但回调嵌套深对刚上手 Socket 的人不友好。常见做法是给每个连接分配一个会话对象里面存客户端 Socket、目标 Socket、两个线程引用和一个停止标志。会话对象是整份源码的核心数据结构理解它就理解了转发流程。 会话对象一条客户端连接对应一个实例 Public Class ForwardSession Public ClientSocket As Socket 客户端接入的 socket Public TargetSocket As Socket 转发目标 socket Public ClientThread As Thread 客户端 - 目标 的泵线程 Public TargetThread As Thread 目标 - 客户端 的泵线程 Public Running As Boolean True 停止标志volatile 语义靠锁保证 Public BufferSize As Integer 8192 单次读写缓冲默认 8KB End Class逻辑说明Running 用布尔标志控制两条泵线程的退出避免用 Thread.Abort那玩意在 .NET 里是血泪经验容易把 socket 留在半关闭状态。BufferSize 设成 8192 是折中值太小系统调用频繁太大单连接内存占用高转发场景一般 4KB 到 16KB 都行。参数说明ClientSocket 和 TargetSocket 都要设 NoDelay True否则小包会被 Nagle 算法攒着发调试时表现为「数据延迟几百毫秒才到」很多人第一次踩这个坑会怀疑是网络问题。2.2 监听与接入的完整代码Imports System.Net Imports System.Net.Sockets Imports System.Threading Public Class TcpForwarder Private listenPort As Integer 9000 本地监听端口 Private targetHost As String 127.0.0.1 转发目标地址 Private targetPort As Integer 8000 转发目标端口 Private listener As TcpListener Public Sub Start() listener New TcpListener(IPAddress.Any, listenPort) listener.Start() 单独线程接受连接避免阻塞调用方 Dim acceptThread As New Thread(AddressOf AcceptLoop) acceptThread.IsBackground True acceptThread.Start() End Sub Private Sub AcceptLoop() While True Dim client As Socket listener.AcceptSocket() client.NoDelay True Dim session As New ForwardSession() session.ClientSocket client 为每个客户端建立到目标端的连接 Dim target As New Socket(AddressFamily.InterNetwork, _ SocketType.Stream, ProtocolType.Tcp) target.NoDelay True target.Connect(targetHost, targetPort) session.TargetSocket target 启动两条泵线程 session.ClientThread New Thread(AddressOf PumpClientToTarget) session.TargetThread New Thread(AddressOf PumpTargetToClient) session.ClientThread.IsBackground True session.TargetThread.IsBackground True session.ClientThread.Start(session) session.TargetThread.Start(session) End While End Sub End Class逻辑说明AcceptLoop 放在后台线程里死循环接受连接每来一个客户端就同步 Connect 到目标端。这里用同步 Connect 是有意的——如果目标端不可达会立刻抛异常方便在日志里定位而不是异步那样错误藏在回调里。参数说明IPAddress.Any 表示监听本机所有网卡如果只想监听某个网卡改成对应 IP。targetHost 可以是域名Socket.Connect 内部会做解析但生产环境建议先解析成 IP 缓存起来避免每次连接都走 DNS。2.3 双向数据泵的实现Private Sub PumpClientToTarget(state As Object) Dim s As ForwardSession CType(state, ForwardSession) Dim buf(s.BufferSize - 1) As Byte Try While s.Running Dim n As Integer s.ClientSocket.Receive(buf) If n 0 Then Exit While 对端正常关闭 s.TargetSocket.Send(buf, 0, n, SocketFlags.None) End While Catch ex As Exception 连接异常断开记录后退出 Finally s.Running False CloseSession(s) End Try End Sub Private Sub PumpTargetToClient(state As Object) Dim s As ForwardSession CType(state, ForwardSession) Dim buf(s.BufferSize - 1) As Byte Try While s.Running Dim n As Integer s.TargetSocket.Receive(buf) If n 0 Then Exit While s.ClientSocket.Send(buf, 0, n, SocketFlags.None) End While Catch ex As Exception Finally s.Running False CloseSession(s) End Try End Sub逻辑说明Receive 返回 0 表示对端发了 FIN正常关闭直接退出循环。任何一端断开都会把 Running 置 False另一条泵线程下一轮循环就会退出然后 CloseSession 统一关闭两个 socket。这个「一端断、两端关」的策略是转发程序的默认行为符合大多数场景预期。参数说明Send 用阻塞模式如果对端接收慢这里会阻塞进而让 Receive 也停住形成背压。这是同步模型的天然限流不需要额外做流控。如果业务要求不能阻塞就得换成异步或加独立发送队列。3. 参数调优与异常处理让转发扛得住真实流量3.1 缓冲区大小、超时与 KeepAlive 怎么设转发程序的性能瓶颈通常不在 CPU而在系统调用次数和内存拷贝。BufferSize 从 8KB 提到 64KB吞吐能明显上升但单连接内存占用也上去。我一般按并发连接数反推100 个连接以内用 32KB上千连接用 8KB 甚至 4KB。超时方面Receive 默认无限等待如果对端假死TCP 连接还在但不发数据泵线程会一直挂着。常见做法是给 Socket 设 ReceiveTimeout但注意——ReceiveTimeout 触发会抛 SocketException得在 Catch 里区分「超时」和「真断开」。更稳的方案是开一个心跳线程定期检查会话最后活跃时间超时就主动 Close。 设置接收超时毫秒超时抛 SocketException client.ReceiveTimeout 30000 client.SendTimeout 30000 开启 TCP KeepAlive探测死连接 client.SetSocketOption(SocketOptionLevel.Socket, _ SocketOptionName.KeepAlive, True)参数说明ReceiveTimeout 设 30 秒是经验值太短会误杀正常但空闲的长连接太长则假死连接占资源。KeepAlive 在 .NET 里只能开关探测间隔要改注册表或用 IOControl这点很多人不知道以为设了 True 就万事大吉。3.2 异常分类与日志转发程序最怕的是异常被吞掉连接断了却不知道为什么。源码里 Catch 块至少要区分三类SocketException网络层看 SocketErrorCode、ObjectDisposedExceptionsocket 已被关闭正常退出路径、IOException底层 IO 错误。日志里记下会话 ID、断开方向、错误码排查时能省大量时间。Catch ex As SocketException 10054 对端重置连接10060 连接超时 Log(String.Format(会话{0} 方向{1} Socket错误:{2}, _ s.SessionId, direction, ex.SocketErrorCode)) Catch ex As ObjectDisposedException socket 已被另一端关闭忽略 Catch ex As Exception Log(未知异常: ex.Message)逻辑说明SocketErrorCode 是排查网络问题的关键10054 通常是对端进程崩了10060 是网络不通或目标没响应10061 是目标端口拒绝连接。把这些码和现象对应起来比看异常消息有用得多。3.3 半关闭状态的处理TCP 支持半关闭一端发 FIN 后仍能接收数据。转发程序如果一收到 FIN 就关两个 socket可能丢掉对端还在路上的数据。严谨做法是收到 FIN 后只关闭对应方向的发送等另一方向也收到 FIN 再彻底关闭。但大多数转发场景比如串口透传不需要这么讲究直接全关更简单。要不要处理半关闭取决于业务协议是否依赖「发完还能收」。4. 避坑与排查转发程序最容易翻车的五个地方4.1 现象客户端连上了但目标端收不到数据原因目标端 Connect 失败但异常被吞或者 NoDelay 没设导致数据被 Nagle 攒着。排查时先在 Accept 后打印目标端连接结果再抓包看数据有没有发出。解决Connect 用同步并捕获异常NoDelay 两端都设 True抓包工具确认数据已离开本机。4.2 现象大流量下内存持续上涨原因每条连接分配了固定缓冲区连接没及时释放或者会话对象被线程引用无法回收。常见于客户端异常断开但泵线程卡在 Receive 上。解决给 Receive 设超时会话结束时显式置空 socket 引用用性能计数器观察连接数是否和预期一致。4.3 现象转发一段时间后新连接连不上原因监听 backlog 满了或者 AcceptLoop 里同步 Connect 目标端阻塞太久导致接受新连接变慢。解决把目标端 Connect 放到独立线程AcceptLoop 只负责接受TcpListener 的 Start 可传 backlog 参数默认值偏小高并发场景调到 100 以上。4.4 现象中文数据转发后乱码原因转发层做了字符串转换把字节流按某种编码解码再编码破坏了原始字节。转发必须只搬字节不碰编码。解决全程用 Byte 数组Receive 和 Send 之间不做任何 String 转换。乱码问题一定出在业务层不在转发层。4.5 现象程序退出后端口还被占用原因socket 没设 ReuseAddress或者进程被杀时连接处于 TIME_WAIT。调试时反复启动会撞端口。解决监听 socket 设 SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, True)退出时先 Stop listener 再关所有会话。5. 进阶把转发做成可配置、可观测的服务5.1 配置外置与多规则转发把监听端口、目标地址、缓冲区大小抽到配置文件程序启动时读入就能一份代码跑多个转发规则。常见做法是用 XML 或 JSON 存规则列表每条规则一个 TcpForwarder 实例。这样改转发关系不用重新编译运维友好。ForwardRules Rule name调试口 ListenPort9000/ListenPort TargetHost127.0.0.1/TargetHost TargetPort8000/TargetPort BufferSize8192/BufferSize /Rule /ForwardRules参数说明BufferSize 放在规则级别不同规则可以按流量特征单独调。name 用于日志标识多规则时排查能快速定位是哪条转发出的问题。5.2 用计数器验证转发是否正常光看日志不够得有量化指标。每个会话维护两个计数器上行字节数、下行字节数。定时输出到日志或界面就能判断转发是否在正常工作。如果上行有数、下行长期为零说明目标端没回数据问题在目标服务不在转发层。指标含义异常判断上行字节客户端发往目标端长期为零说明客户端没发数据下行字节目标端发往客户端长期为零说明目标端没响应活跃会话数当前连接数只增不减说明会话没释放异常计数各类 Socket 错误突增说明网络或目标端异常5.3 一个具体技巧用端口映射验证转发链路调试转发时最怕分不清是转发层的问题还是两端业务的问题。我的习惯是先不接真实业务用两个简单的 TCP 工具分别模拟客户端和目标端确认转发链路通了再接真实服务。具体做法目标端用一个只回显的监听程序客户端发什么它回什么转发层接在中间。如果客户端能收到回显说明转发双向都正常问题一定在真实业务端。从那以后我每次调转发程序都强制先走一遍「回显验证」确认链路再上业务能省掉大量扯皮时间。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进