ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

VB.NET TCP/IP 通讯转发程序源码:连接管理、粘包处理与断线重连

VB.NET TCP/IP 通讯转发程序源码:连接管理、粘包处理与断线重连 简介这份源码面向需要研究TCP/IP通讯转发与数据包监视的VB.NET开发者尤其适合新手及有一定经验的程序员借鉴。作者针对现有抓包工具不够直观、只能转发却看不到数据包内容的痛点自行实现了一套可视化转发程序采用VS2019编写接收端使用Listener监听转发目的端为TcpClient客户端支持客户端与服务端之间的多级转发链路。资源包共33个文件约456KB包含6个vb源码文件、2个resx资源文件、2个config配置、2个exe可执行程序、1个vbproj工程文件与1个sln解决方案另附xml、pdb、dll等编译与调试辅助文件结构完整可直接打开运行。目前已有1058人学习下载读者可从中获取通讯转发的核心实现思路、监听与客户端连接配置方法以及数据包内容可视化展示的参考方案便于二次开发与排错调试。1. 从一台老工控机说起VB.NET 做 TCP/IP 通讯转发到底在转什么车间里有台跑了八年的工控机上面挂着一套 VB.NET 写的上位机只认串口和本地 TCP可新上的 MES 系统只吃 HTTP 和 MQTT。两边数据要通又没人敢动那套老代码最省事的办法就是在中间架一个转发程序一头连老系统的 TCP 端口一头把数据吐给新系统。这就是 VB.NET 实现 TCP/IP 通讯转发功能程序源码要解决的事——用 .NET 里现成的System.Net.Sockets把一路 TCP 连接上的字节流接过来按规则处理后转发到另一个或多个目的地。它适合手里有 VB.NET 存量项目、又需要做协议桥接或数据分发的工程师尤其是工控、医疗设备、自助终端这类现场。源码本身不复杂难的是连接管理、粘包处理和异常重连这几处后面逐层拆开讲。2. 转发程序的骨架TcpListener、TcpClient 与双工数据流2.1 为什么用原生 Socket 而不是第三方库VB.NET 做 TCP 转发第一层选型就是通信底座。常见做法有三种原生System.Net.Sockets、TcpListener/TcpClient封装、以及引入 SuperSocket 之类的第三方框架。第三方框架功能全但对一个只做转发的小程序来说依赖越少越好部署尤其现场机器往往不能随便装运行时。原生 Socket 和 TcpListener 都在 .NET Framework 里自带VB.NET 项目直接引用即可不需要额外包。我一般选TcpListener做服务端监听、TcpClient做下游连接。原因是这两个类比裸Socket少写不少样板代码AcceptTcpClient直接返回一个带NetworkStream的对象读写用流的方式更直观。代价是它们对底层选项的控制弱一些比如想精细设置LingerState或NoDelay还是得回到Socket对象上操作。转发场景里延迟敏感NoDelay True基本是必设的否则小包会被 Nagle 算法攒着发现场表现为「数据慢半拍」。选型上还有一点转发程序通常是长连接常驻不能每来一包就新建连接。所以结构上要维护一张连接表每个上游客户端对应一个下游连接双向各起一个读循环。这就是后面代码要落地的骨架。2.2 最小可运行的转发服务端代码下面这段是转发程序的核心骨架监听本地端口收到连接后连到目标地址然后双向搬运数据。可以直接放进一个 VB.NET 控制台项目里跑。Imports System.Net Imports System.Net.Sockets Imports System.Text Imports System.Threading Module Forwarder 本地监听端口与转发目标 Private Const ListenPort As Integer 9000 Private Const TargetHost As String 127.0.0.1 Private Const TargetPort As Integer 9100 Sub Main() Dim listener As New TcpListener(IPAddress.Any, ListenPort) listener.Start() Console.WriteLine(转发服务已启动监听 ListenPort) While True 阻塞等待上游连接 Dim upstream As TcpClient listener.AcceptTcpClient() upstream.NoDelay True 每个连接丢到线程池避免阻塞 Accept ThreadPool.QueueUserWorkItem(Sub(state) HandleClient(upstream)) End While End Sub Private Sub HandleClient(upstream As TcpClient) Dim downstream As New TcpClient() Try downstream.Connect(TargetHost, TargetPort) downstream.NoDelay True 双向转发两个方向各一个线程 Dim t1 As New Thread(Sub() Pump(upstream, downstream)) Dim t2 As New Thread(Sub() Pump(downstream, upstream)) t1.IsBackground True t2.IsBackground True t1.Start() t2.Start() t1.Join() t2.Join() Catch ex As Exception Console.WriteLine(连接异常: ex.Message) Finally upstream.Close() downstream.Close() End Try End Sub 从 from 读写到 [to] Private Sub Pump(from As TcpClient, [to] As TcpClient) Dim buffer(4095) As Byte Try While True Dim n As Integer from.GetStream().Read(buffer, 0, buffer.Length) If n 0 Then Exit While 对端关闭 [to].GetStream().Write(buffer, 0, n) [to].GetStream().Flush() End While Catch 任一端断开退出循环由外层统一关闭 End Try End Sub End Module逻辑说明Main里TcpListener常驻监听AcceptTcpClient拿到上游连接后不直接处理而是丢进线程池保证还能继续接受新连接。HandleClient负责建立下游连接然后开两个后台线程做双向Pump。Pump是纯字节搬运读到 0 字节代表对端正常关闭直接退出。参数说明ListenPort和TargetPort按现场改buffer大小 4096 是经验值太小会增加系统调用次数太大在低延迟场景反而增加单次拷贝耗时一般 2K 到 8K 之间都行。NoDelay True关掉 Nagle转发场景建议保留。IsBackground True保证主程序退出时线程不挂住进程。这段代码能跑通「连上就转发」的最小闭环但它有两个明显短板一是没处理粘包二是没做断线重连。这两点正是后面章节要补的。2.3 连接表与生命周期管理上面的写法每个连接独立成对简单但不好管。真实项目里往往需要知道「当前有几路连接」「哪一路断了」「能不能主动踢掉某一路」。这就需要一张连接表。常见做法是定义一个ForwardSession类持有上游、下游两个TcpClient加一个唯一 Id 和状态标志。用一个ConcurrentDictionary(Of String, ForwardSession)存起来连接建立时加入Finally里移除。这样监控线程可以遍历这张表打印状态也可以在需要时调用Close主动断开。生命周期上有三个关键点。第一关闭顺序先Close上游再Close下游或者反过来都行但两个Pump线程要能感知到关闭否则会卡在Read上。第二Read阻塞是没法直接打断的通常靠关闭 socket 让Read抛异常来退出所以Pump里的Catch不能吞掉后继续循环。第三Join两个线程时如果其中一个先退出另一个可能还在阻塞所以Join前最好先关闭对端 socket或者给Join设超时。这块是转发程序最容易出玄学问题的地方——表面看连接还在实际数据早就不通了。血泪经验是任何一处Close都要配日志记录是谁触发的关闭否则线上排查只能靠猜。3. 粘包、半包与心跳转发层必须处理的三个数据边界3.1 粘包和半包为什么在转发场景更致命TCP 是字节流没有消息边界。上游一次Send的 100 字节下游可能分两次Read收到也可能和下一包粘在一起一次收到。如果转发程序只是无脑搬运字节那粘包问题其实被「原样传递」掩盖了——因为下游收到的字节序列和上游发出的完全一致边界由下游自己解析。这是纯字节转发的一个好处。但一旦转发程序需要在中间做处理比如解析出完整报文再转发、或者按协议改字段粘包就必须自己解决。常见做法是定长头 长度字段先读固定长度的头从头里解析出包体长度再读够这么多字节才算一包。VB.NET 里可以用BitConverter处理长度字段注意大小端要和协议一致。半包则出现在连接刚建立或大包传输时Read返回的字节数小于预期。处理方式是循环读直到凑够或者用一个缓冲区累积。下面是一个按「4 字节长度前缀」拆包的累积读法。 按 4 字节大端长度前缀拆包返回完整报文列表 Private Function ExtractPackets(buffer As List(Of Byte)) As List(Of Byte()) Dim packets As New List(Of Byte()) While buffer.Count 4 前 4 字节是包体长度 Dim lenBytes As Byte() buffer.GetRange(0, 4).ToArray() If BitConverter.IsLittleEndian Then Array.Reverse(lenBytes) Dim bodyLen As Integer BitConverter.ToInt32(lenBytes, 0) If buffer.Count 4 bodyLen Then Exit While 半包等下次 Dim body As Byte() buffer.GetRange(4, bodyLen).ToArray() packets.Add(body) buffer.RemoveRange(0, 4 bodyLen) End While Return packets End Function逻辑说明buffer是跨多次Read累积的字节列表。每次进来先看够不够 4 字节头够就解析长度再看包体够不够不够就退出等下次数据。够就切出一包从缓冲区移除继续循环处理下一包。参数说明长度字段的字节序要和协议对齐BitConverter.IsLittleEndian判断当前机器字节序网络协议通常是大端所以要Reverse。bodyLen要做上限校验防止恶意或错误长度导致内存暴涨一般设个 1MB 之类的上限超了直接断连接。3.2 心跳与空闲检测长连接转发最怕的是「假连接」TCP 层面没断但对端进程已经死了数据发出去石沉大海。TCP 自带的 KeepAlive 默认两小时才探测现场根本等不起。所以转发程序一般自己做心跳。两种思路。一种是应用层心跳转发程序定期往上下游各发一个约定好的心跳包对端要回。收不到回应超过 N 次就判定断开主动关闭重连。另一种是空闲超时记录每个连接最后一次收到数据的时间超过阈值没数据就关闭。前者更可靠但需要协议支持后者实现简单但可能误杀正常空闲连接。我一般两个都用应用层心跳负责探活空闲超时兜底。心跳间隔设 30 秒连续 3 次无响应断开空闲超时设 5 分钟。这些值要按现场网络质量调网络抖动大的地方间隔要放宽否则会频繁重连。3.3 断线重连的正确姿势重连不是简单地在Catch里再Connect一次。要区分几种情况上游断了下游还活着下游断了上游还活着两边都断了。上游断了下游要跟着关否则下游会一直等一个不会来的数据下游断了要尝试重连重连期间上游数据要么缓存要么丢弃取决于业务能不能容忍丢数据。重连要有退避不能死循环猛连。常见做法是第一次等 1 秒之后翻倍封顶 30 秒。VB.NET 里用Thread.Sleep或Task.Delay都行注意别阻塞主线程。重连成功后再把状态恢复如果之前有缓存数据按顺序补发。这里有个坑重连时如果目标地址是域名Connect会做 DNS 解析解析失败也会抛异常要和连接被拒区分开。日志里把异常类型打出来排查时能省很多事。4. 避坑与排查转发程序上线后最常翻车的五个地方4.1 现象连接数涨到几百后程序卡死原因AcceptTcpClient在主线程里同步处理或者线程池被占满。每个连接开两个线程几百个连接就是上千线程上下文切换开销巨大还可能触发线程池饥饿。解决改用异步模型AcceptTcpClientAsync配合Async/Await读写也用ReadAsync/WriteAsync。异步模型下少量线程就能撑住大量连接。如果暂时不想大改至少把Thread换成Task并限制最大连接数超了直接拒绝。4.2 现象数据偶尔丢一段日志里看不到异常原因Pump里的Catch把异常吞了或者Write之后没Flush。NetworkStream的Write一般不需要手动Flush但如果中间套了BufferedStream就必须Flush否则数据留在缓冲区里。解决Catch里至少记一条日志包含异常类型和消息。检查是否有缓冲层没刷新。另外确认Read的返回值有没有被忽略——返回 0 是对端关闭不是「没数据」处理错了会死循环。4.3 现象转发延迟忽高忽低现场说「时快时慢」原因Nagle 算法攒包或者缓冲区大小不合适。小包频繁发送时 Nagle 会等 ACK 再发延迟就上来了。解决两端都设NoDelay True。缓冲区大小按实际报文大小调如果报文普遍在 1KB 以下缓冲区设 4096 足够如果经常传大文件设 64KB 减少系统调用。还可以考虑用Socket.SendBufferSize和ReceiveBufferSize调整内核缓冲。4.4 现象程序跑几天后内存持续上涨原因连接表里的ForwardSession没被移除或者缓冲区List(Of Byte)只增不减。异常路径下Finally没执行到连接对象泄漏。解决所有资源释放放在Finally里连接表移除也放Finally。缓冲区在拆包后及时RemoveRange不要一直累积。用Using包裹能自动释放的对象。上线前用压力测试跑一天观察内存曲线。4.5 现象下游重连后收到重复数据原因重连时把之前已发送但未确认的数据又发了一遍或者缓存没清空。解决重连后清空发送缓存除非业务明确要求补发。如果要求补发要有去重机制比如带序列号下游按序列号丢弃重复包。这个要和下游系统约定好单方面补发容易出问题。5. 进阶把转发程序做成可配置、可观测的服务5.1 配置外置与多目标分发硬编码端口和目标地址只能应付 demo。真实项目里监听端口、目标地址、缓冲区大小、心跳间隔这些都应该外置到配置文件。VB.NET 里可以用App.config的appSettings或者读一个 JSON 文件。我倾向 JSON改起来直观也方便程序运行时热加载。多目标分发是另一个常见需求一路上游数据要同时发给三个下游。做法是把Pump里的单个[to]换成一个下游列表收到数据后遍历列表逐个Write。注意某个下游写失败不能影响其他下游每个下游独立Try/Catch失败的标记为断开并触发重连。 多目标分发一份数据写给多个下游 Private Sub Broadcast(data As Byte(), n As Integer, targets As List(Of TcpClient)) For Each t In targets Try If t.Connected Then t.GetStream().Write(data, 0, n) End If Catch ex As Exception 单个下游失败不影响其他 Console.WriteLine(下游写入失败: ex.Message) End Try Next End Sub逻辑说明遍历下游列表每个独立Try/Catch一个失败继续下一个。Connected属性只能反映上次操作后的状态不完全可靠所以Write本身也要包在Try里。参数说明data和n是本次读到的字节和长度直接透传。如果下游协议和上游不同这里就是做转换的地方比如改字段、加头、转编码。5.2 用日志和计数器做可观测转发程序出问题时最怕的是「不知道哪一步断了」。至少要记录这几类日志连接建立和关闭带 Id 和时间、异常带类型和堆栈、重连尝试和结果、心跳超时。日志级别分 Info 和 Error正常连接关闭用 Info异常用 Error。计数器更直观当前连接数、累计接收字节、累计发送字节、重连次数、丢弃包数。这些可以用Interlocked做线程安全的自增定期打印或暴露一个本地 HTTP 接口给监控拉取。现场排查时看一眼计数器就知道是没数据还是没转发。5.3 一个验证转发是否正常的土办法上线前我会做两件事。第一用两个telnet或者netcat分别连上游和下游手动发一串字节看另一边能不能原样收到。第二写一个小的压力脚本模拟 100 个连接持续发数据跑一小时观察内存、CPU 和连接数是否稳定。这两个测试能覆盖大部分低级错误。如果现场没有 netcat用 VB.NET 再写一个几十行的测试客户端也行发固定序列收端校验序列是否连续。序列号用递增整数收到不连续就说明丢包或乱序能直接定位到是转发层的问题还是网络的问题。这套东西做完一个 VB.NET 的 TCP/IP 转发程序基本就能扛住生产环境了。我自己的习惯是任何转发程序上线前必须先在测试环境跑满 24 小时盯一遍内存曲线和连接数曲线曲线平了才敢往现场推。希望帮到你。本文还有配套的精品资源点击获取
返回列表