☰
VB.NET实现TCP/IP通讯转发:源码解析与Socket编程实践
2026/10/8 1:13:12 网站建设 项目流程

简介:这是一份面向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)、ObjectDisposedException(socket 已被关闭,正常退出路径)、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="调试口"> <ListenPort>9000</ListenPort> <TargetHost>127.0.0.1</TargetHost> <TargetPort>8000</TargetPort> <BufferSize>8192</BufferSize> </Rule> </ForwardRules>

参数说明:BufferSize 放在规则级别,不同规则可以按流量特征单独调。name 用于日志标识,多规则时排查能快速定位是哪条转发出的问题。

5.2 用计数器验证转发是否正常

光看日志不够,得有量化指标。每个会话维护两个计数器:上行字节数、下行字节数。定时输出到日志或界面,就能判断转发是否在正常工作。如果上行有数、下行长期为零,说明目标端没回数据,问题在目标服务不在转发层。

指标含义异常判断
上行字节客户端发往目标端长期为零说明客户端没发数据
下行字节目标端发往客户端长期为零说明目标端没响应
活跃会话数当前连接数只增不减说明会话没释放
异常计数各类 Socket 错误突增说明网络或目标端异常

5.3 一个具体技巧:用端口映射验证转发链路

调试转发时,最怕分不清是转发层的问题还是两端业务的问题。我的习惯是先不接真实业务,用两个简单的 TCP 工具分别模拟客户端和目标端,确认转发链路通了,再接真实服务。具体做法:目标端用一个只回显的监听程序,客户端发什么它回什么,转发层接在中间。如果客户端能收到回显,说明转发双向都正常,问题一定在真实业务端。

从那以后我每次调转发程序,都强制先走一遍「回显验证」,确认链路再上业务,能省掉大量扯皮时间。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询