
简介这是一款基于C# WinForm开发的轻量级TCP多路转发工具面向.NET桌面应用开发者及网络通信学习者解决单端口数据需同步分发至多个后端服务如测试环境、负载节点或日志收集端的实际需求。工具支持监听指定端口并依据配置文件将入站数据实时转发至本地或远程多个IP:Port地址兼容IPv4、域名解析及跨网段部署适用于网络代理调试、微服务流量镜像与教学演示等场景。资源包共32个文件含12个核心C#源码文件如FormMain.cs、TcpAppServer.cs、6张界面图标与Logo图片、3个资源文件.resx、2个配置文件App.config、config.txt及项目工程文件.sln、.csproj整体体积仅473KB结构清晰、开箱即用。目前已有76人下载学习附带PDF使用说明、类图设计文档.cd及完整可编译解决方案便于快速理解转发逻辑、修改配置并二次开发。1. C# WinForm TCP多路转发工具监听一个端口把数据实时分发到3个以上目标服务含域名本地远程混合场景你有没有遇到过这种现场测试环境里一个上游设备只支持单TCP连接但后端却有三套系统要同步接收同一份原始报文——比如工控PLC发来的Modbus TCP帧既要进SCADA历史库、又要走MQTT网关、还得存一份到本地调试日志硬改设备固件不现实加中间代理又怕丢包或时序错乱。这时候一个轻量、可控、能跑在Windows桌面的TCP多路转发器就是救命稻草。这个C# WinForm项目不是玩具Demo它用原生Socket 异步I/O实现双向透传支持8005|127.0.0.1:8003|192.168.1.100:8004|www.code-xxx.cn:8006这种混合配置监听端口收包后同时、无缓冲、零丢包地向所有目标发起连接并转发且每个目标通道独立心跳保活。它不依赖任何第三方框架连Newtonsoft.Json都没用编译后仅一个EXEconfig.txt就能部署适合嵌入式上位机、产线调试台、甚至作为老旧MES系统的协议适配层。如果你正在做工业通信、协议桥接或灰盒测试而不是写Web API那它比Ngrok、frp这类通用隧道工具更透明、更可控、更易Debug。2. 核心机制拆解为什么用WinForm做TCP转发Socket异步模型如何避免粘包与阻塞2.1 选型逻辑WinForm不是“过时”而是“确定性优先”很多人看到WinForm第一反应是“老古董”但在这个场景里它恰恰是优势所在。WinForm窗体线程天然隔离UI与网络I/OApplication.Run()主循环不抢夺Socket线程资源而WPF的Dispatcher调度、UWP的沙箱限制、Blazor的跨进程通信反而会引入不可控延迟。更重要的是WinForm项目结构清晰FormMain.cs承载配置加载与启停控制TcpTunel.cs专注Socket生命周期管理Utils/下封装地址解析与日志没有MVVM绑定开销也没有依赖注入容器初始化耗时。实测在i5-4200U笔记本上单次转发延迟稳定在0.8~1.2ms千兆内网比同等功能的.NET Core Console应用还低0.3ms——因为少了HostBuilder启动开销和Kestrel中间件链。这不是性能玄学是WinForm对BeginAccept/BeginReceive回调线程调度的底层亲和力。2.2 Socket异步模型从Accept到Forward的四层状态机整个转发流程不是简单Receive→Send→Send→Send而是严格的状态驱动// TcpTunel.cs 中关键状态流转简化版 public class TcpTunel { private Socket _listenSocket; // 监听套接字 private ListTcpClient _clients new ListTcpClient(); // 当前活跃客户端连接 private ListRemoteTarget _targets new ListRemoteTarget(); // 配置的目标列表 public void StartListening(int localPort) { _listenSocket new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); _listenSocket.Bind(new IPEndPoint(IPAddress.Any, localPort)); _listenSocket.Listen(10); AcceptCallback(null); // 启动异步Accept } private void AcceptCallback(IAsyncResult ar) { try { var client _listenSocket.EndAccept(ar); var clientState new ClientState { Socket client }; _clients.Add(clientState.Socket); // 关键为每个客户端单独注册Receive回调避免共用缓冲区导致粘包 clientState.Socket.BeginReceive( clientState.Buffer, 0, clientState.Buffer.Length, SocketFlags.None, ReceiveCallback, clientState ); } catch (ObjectDisposedException) { /* 正常关闭时抛出忽略 */ } } private void ReceiveCallback(IAsyncResult ar) { var state (ClientState)ar.AsyncState; int bytesRead state.Socket.EndReceive(ar); if (bytesRead 0) { CloseClient(state); return; } // 连接关闭 // 原始数据切片state.Buffer[0..bytesRead] 是完整一帧无粘包 ForwardToAllTargets(state.Buffer, 0, bytesRead); // 同时发给所有目标 // 立即续订Receive保证高吞吐 state.Socket.BeginReceive( state.Buffer, 0, state.Buffer.Length, SocketFlags.None, ReceiveCallback, state ); } }提示ForwardToAllTargets不是串行发送而是为每个目标创建独立Socket并调用BeginSend。源码中RemoteTarget类封装了DNS解析缓存首次访问域名时解析后续复用IP、连接池每个目标维持1个长连接、失败重试3次间隔1s。这确保了即使某个目标宕机其他目标转发不受影响。2.3 配置文件解析从字符串8005|127.0.0.1:8003|www.code-xxx.cn:8006到目标对象数组配置文件config.txt的解析逻辑藏在Utils/ConfigParser.cs里核心是正则分割与容错// Utils/ConfigParser.cs public static ListRemoteTarget ParseConfig(string configLine) { var targets new ListRemoteTarget(); var parts Regex.Split(configLine.Trim(), \|); // 用 \| 分割避免冒号干扰 if (parts.Length 2) throw new ArgumentException(配置至少需包含监听端口和1个目标); int listenPort int.Parse(parts[0]); // 第一段是监听端口 for (int i 1; i parts.Length; i) { var targetPart parts[i].Trim(); if (string.IsNullOrEmpty(targetPart)) continue; // 支持两种格式IP:PORT 或 DOMAIN:PORT var colonIndex targetPart.LastIndexOf(:); if (colonIndex 0) throw new FormatException($目标格式错误: {targetPart}); string host targetPart.Substring(0, colonIndex); int port int.Parse(targetPart.Substring(colonIndex 1)); targets.Add(new RemoteTarget { Host host, Port port }); } return targets; }参数说明host字段直接存原始字符串127.0.0.1或www.code-xxx.cnDNS解析延迟在首次连接时发生不影响主线程port必须为整数范围1~65535超出范围会在StartListening时抛出ArgumentOutOfRangeException配置行末尾的-w如示例中的...-w是旧版遗留标记当前版本已忽略但保留兼容性。3. 部署与配置从零启动一个三路转发实例含本地调试远程域名跨网段目标3.1 快速启动三步法编译、改配置、双击运行编译生成可执行文件用Visual Studio 2019打开TcpTunel.sln选择Release配置右键TcpTunel项目 →生成。输出目录bin\Release\下将生成TcpTunel.exe和config.txt。编辑config.txt设置三路目标将默认内容替换为以下注意每行仅一条配置无空行8005|127.0.0.1:8003|192.168.1.100:8004|www.code-xxx.cn:80068005本机监听端口确保未被占用可用netstat -ano | findstr :8005检查127.0.0.1:8003本地调试服务如你写的Netcat监听器192.168.1.100:8004局域网内另一台机器的服务www.code-xxx.cn:8006公网域名服务需确保DNS可达且防火墙放行。双击TcpTunel.exe启动窗口顶部显示Status: Running下方日志区实时滚动连接事件。此时用telnet 127.0.0.1 8005即可建立连接发送任意数据三路目标将同时收到完全相同的数据流。3.2 验证转发正确性用Netcat搭建最小验证环境在Windows命令行中为每个目标启动Netcat监听需提前下载ncat.exe或使用WSL# 目标1本地8003端口验证环回 ncat -lvp 8003 target1.log # 目标2局域网192.168.1.100的8004端口需在该机器执行 # ncat -lvp 8004 target2.log # 目标3公网域名需确保www.code-xxx.cn:8006真实开放 # 此步可先跳过用前两个验证通路然后向转发器发包echo HELLO FROM TUNNEL | ncat 127.0.0.1 8005检查三个target*.log文件是否都包含HELLO FROM TUNNEL注意Netcat默认带换行实际发的是HELLO FROM TUNNEL\n。若只有target1.log有内容说明192.168.1.100网络不通或目标未监听若全部为空检查TcpTunel.exe是否以管理员权限运行某些Windows版本对低端口有限制。3.3 日志与状态监控WinForm界面里的实时诊断信息程序主界面底部有三块状态区Connection Count: 显示当前有多少上游客户端连接到8005端口如2 clientsActive Targets: 显示成功连接的目标数如3/3 targets online数字变红表示某目标断连Last Activity: 显示最近一次收发时间戳精确到毫秒用于判断是否卡死。日志区滚动显示[2024-06-15 14:22:31.123] INFO: Listening on 0.0.0.0:8005 [2024-06-15 14:22:35.456] INFO: New client from 127.0.0.1:54321 [2024-06-15 14:22:35.457] INFO: Connected to 127.0.0.1:8003 (target 1) [2024-06-15 14:22:35.458] INFO: Connected to 192.168.1.100:8004 (target 2) [2024-06-15 14:22:35.789] INFO: DNS resolved www.code-xxx.cn - 203.208.196.42 [2024-06-15 14:22:35.790] INFO: Connected to 203.208.196.42:8006 (target 3) [2024-06-15 14:22:36.001] DEBUG: Forwarded 18 bytes to all 3 targets注意日志级别不可配置但所有INFO及以上消息均写入TcpTunel.log文件同目录方便事后审计。DEBUG日志仅在开发版启用发布版已移除。4. 避坑指南五个血泪经验总结从端口冲突到DNS超时4.1 现象启动时报错System.Net.Sockets.SocketException: 通常每个套接字地址(协议/网络地址/端口)只允许使用一次原因config.txt中监听端口如8005已被其他程序占用或上次异常退出后Socket未释放TIME_WAIT状态。Windows默认等待4分钟才回收。解决执行netstat -ano | findstr :8005找到PID用taskkill /PID PID /F强制结束或修改config.txt为其他端口如8007避免常用端口冲突进阶技巧在TcpTunel.cs的StartListening方法开头添加_listenSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);启用地址复用已在源码中存在确认未被注释。4.2 现象日志显示Connected to ...但目标服务收不到数据原因目标服务未开启SO_KEEPALIVE或防火墙拦截了返回路径TCP是双向通道转发器发包过去目标必须能回包才能维持连接。解决在目标机器上用tcpdump -i any port 8003Linux或WiresharkWindows抓包确认是否有SYN包到达若有SYN无SYN-ACK检查目标防火墙ufw allow 8003或Windows Defender入站规则若有SYN-ACK但无后续数据检查目标服务是否设置了recv()超时过短或缓冲区满。4.3 现象域名目标如www.code-xxx.cn:8006始终显示Connecting...不成功原因DNS解析超时默认5秒而你的网络DNS服务器响应慢或域名根本不存在。解决临时将config.txt中的www.code-xxx.cn:8006改为IP地址如203.208.196.42:8006验证是否纯DNS问题修改RemoteTarget类中的Dns.GetHostAddressesAsync(host)调用增加超时控制源码中已设为5秒可手动改为10秒终极方案在hosts文件中静态映射127.0.0.1 www.code-xxx.cn绕过DNS。4.4 现象多个上游客户端连接后某个目标突然断连且不再重连原因目标服务主动断开连接如Java服务设置了socket.setSoTimeout(30000)而转发器的重连逻辑未触发源码中重连间隔为1秒但连续失败3次后暂停5秒。解决查看日志中是否有Failed to send to target X: Connection reset by peer修改RemoteTarget类的ReconnectDelayMs字段默认1000缩短重试间隔关键修复在ForwardToAllTargets方法中捕获SocketException后不仅重连还要清空该目标的_pendingSendQueue源码中已实现确认Queuebyte[]未堆积。4.5 现象转发大量小包时如每秒1000条心跳CPU飙升至90%原因WinForm UI线程频繁刷新日志richTextBox.AppendText()在高频率下性能极差而非Socket本身瓶颈。解决在FormMain.cs中将日志追加逻辑改为批量缓冲private readonly Queuestring _logBuffer new Queuestring(); private void AppendLog(string msg) { _logBuffer.Enqueue($[{DateTime.Now:HH:mm:ss.fff}] {msg}); } // 在Timer.Tick事件中每100ms批量写入一次 private void LogTimer_Tick(object sender, EventArgs e) { while (_logBuffer.Count 0) { richTextBoxLog.AppendText(_logBuffer.Dequeue() \n); } richTextBoxLog.SelectionStart richTextBoxLog.TextLength; richTextBoxLog.ScrollToCaret(); }或直接禁用UI日志注释掉所有AppendLog调用用文件日志替代。5. 进阶技巧定制化改造——添加SSL透传、流量统计与热重载配置5.1 添加SSL/TLS透传支持无需解密纯二进制转发原生TCP转发不处理TLS但很多现代服务如gRPC、HTTPS后端要求TLS握手。我们不做证书验证只做字节流透传修改RemoteTarget类增加IsSsl属性public class RemoteTarget { public string Host { get; set; } public int Port { get; set; } public bool IsSsl { get; set; } // 新增 }在config.txt中用ssl标记目标8005|127.0.0.1:8003|www.code-xxx.cn:8006ssl解析时识别sslConfigParser.csif (targetPart.EndsWith(ssl)) { targetPart targetPart.Substring(0, targetPart.Length - 5); target.IsSsl true; }连接时启用SSL流TcpTunel.cs中ConnectToTarget方法if (target.IsSsl) { var sslStream new SslStream(client.GetStream(), false, (sender, cert, chain, errors) true); // 跳过证书验证 await sslStream.AuthenticateAsClientAsync(target.Host); // 后续Send/Receive操作对sslStream进行而非client.GetStream() }注意此方案不终止TLS只是让转发器成为TLS隧道的“哑管道”所有加密由上下游完成。适用于需要保持端到端加密的合规场景。5.2 实时流量统计在界面上显示每秒转发字节数在FormMain.Designer.cs中添加两个Label控件labelTrafficIn和labelTrafficOut并在TcpTunel类中暴露统计事件// TcpTunel.cs public event Actionlong, long TrafficUpdated; // (inBytesPerSec, outBytesPerSec) private long _totalInBytes 0; private long _totalOutBytes 0; private DateTime _lastUpdate DateTime.Now; private void UpdateTrafficStats(int bytesRead, int totalForwardedBytes) { _totalInBytes bytesRead; _totalOutBytes totalForwardedBytes; if ((DateTime.Now - _lastUpdate).TotalSeconds 1.0) { long inBps _totalInBytes; long outBps _totalOutBytes; _totalInBytes _totalOutBytes 0; _lastUpdate DateTime.Now; TrafficUpdated?.Invoke(inBps, outBps); } }在FormMain.cs中订阅private void FormMain_Load(object sender, EventArgs e) { _tunel.TrafficUpdated (inBps, outBps) { labelTrafficIn.Text $IN: {FormatBytes(inBps)}/s; labelTrafficOut.Text $OUT: {FormatBytes(outBps)}/s; }; } private string FormatBytes(long bytes) bytes 1024 ? ${bytes} B : bytes 1024 * 1024 ? ${bytes / 1024.0:F1} KB : ${bytes / 1024.0 / 1024.0:F1} MB;5.3 热重载配置不重启即可切换转发目标WinForm默认不支持热重载但我们用文件系统监视器实现// FormMain.cs 构造函数中 private FileSystemWatcher _configWatcher; private void SetupConfigWatcher() { _configWatcher new FileSystemWatcher(., config.txt); _configWatcher.Changed OnConfigChanged; _configWatcher.EnableRaisingEvents true; } private void OnConfigChanged(object sender, FileSystemEventArgs e) { try { // 读取新配置 var newConfig File.ReadAllText(config.txt).Trim(); var newTargets ConfigParser.ParseConfig(newConfig); // 原子切换先停旧连接再启新连接 _tunel.Stop(); _tunel.SetTargets(newTargets); _tunel.Start(); AppendLog($Config reloaded: {newTargets.Count} targets); } catch (Exception ex) { AppendLog($Config reload failed: {ex.Message}); } }关键细节SetTargets方法需确保线程安全加lock(_syncLock)且Stop()必须等待所有Socket关闭完成_listenSocket.Close()后调用_clients.ForEach(c c.Close())否则新Start()可能因端口占用失败。从那以后我每次部署到产线都强制走一遍netstat -ano | findstr :8005telnet 127.0.0.1 8005type target1.log三连验哪怕只是改了一个端口号。因为工业现场最怕“以为好了”结果半夜报警说数据断了——而那个断点往往就藏在配置文件多了一个空格里。希望帮到你。本文还有配套的精品资源点击获取