C#实现TCP/IP监听接收XML流写入文件方案合理性咨询
原实现存在的问题
你当前的代码在简单测试场景下可能偶尔跑通,但放到生产环境大概率会出现数据丢失、程序卡死的问题,核心缺陷如下:
- 单线程阻塞模型:整个循环同一时间只能处理1个客户端连接,执行当前连接的读取、写文件逻辑时,新的连接请求会直接被拒绝。如果有多台透析机同时上报、或者某台设备连接后传输速度慢,后续所有上报都会失败。
ReadToEnd()逻辑错误:TCP是面向流的协议,没有天然的消息边界,StreamReader.ReadToEnd()只有在TCP连接被主动关闭时才会返回结果。如果透析机推送完XML后没有主动断开连接,代码会永久卡在读取步骤,既不会存储文件也不会接收新连接;如果出现网络粘包,还会把多次推送的XML拼成一个文件,完全不符合需求。- 文件名生成逻辑有bug:默认的
DateTime.Now.ToString()输出包含:等Windows文件名非法字符,仅替换空格无法解决问题,会直接触发文件写入异常;如果同一时间点收到多个上报,还会出现文件名重复、覆盖已有文件的问题。 - 资源泄漏风险:
TcpClient、NetworkStream、StreamReader都没有做显式释放,一旦中间抛出异常(比如磁盘满、路径不存在、网络中断),连接和文件句柄会一直被占用,运行时间久了会导致程序崩溃、端口无法正常监听。 - 无容错机制:整个代码没有异常捕获,任意一步报错都会直接导致整个监听程序退出,无人值守的情况下会持续丢失数据。
- 无数据校验:传输过程中如果出现网络丢包、内容截断,会直接把损坏的内容存为XML文件,后续数据处理环节会出问题。
更优实现方案
针对透析机每60秒主动推送XML的场景,优化方向如下:
- 采用异步监听模型,主循环仅负责接收连接,每个客户端连接的读取、存文件逻辑放到独立任务中处理,避免阻塞新连接接入。
- 基于XML的格式特征做消息边界判断:边读取流内容边拼接缓冲区,检测到完整的XML根节点闭合标签后,立刻取出内容存储文件,不需要等待连接断开。
- 修正文件名生成规则:使用明确的无非法字符的时间格式,追加随机后缀避免重名覆盖。
- 所有实现了IDisposable接口的对象用
using块包裹,确保异常场景下资源也能被正常回收。 - 增加分层异常捕获:单个连接处理失败仅记录日志、释放当前连接,不影响整个监听服务运行。
- 增加基础XML完整性校验,损坏的内容单独归档,不写入正常文件目录。
参考实现代码:
using System.Net; using System.Net.Sockets; using System.Text; using System.Xml.Linq; // 初始化监听器,开启地址重用避免重启时端口占用 TcpListener server = new TcpListener(IPAddress.Any, port); server.Server.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); server.Start(); NiproEventsLog.WriteLog("监听服务启动成功,等待设备连接..."); // 主循环持续接收连接 while (true) { try { TcpClient client = await server.AcceptTcpClientAsync(); // 接收到连接后交给独立任务处理,主循环立刻返回接收下一个连接 _ = ProcessClientAsync(client); } catch (Exception ex) { NiproEventsLog.WriteLog($"接收连接出错:{ex.Message}"); // 短暂等待避免异常循环占满CPU await Task.Delay(100); } } // 单个客户端连接处理逻辑 async Task ProcessClientAsync(TcpClient client) { try { using (client) using (NetworkStream stream = client.GetStream()) { // 设置读取超时,避免异常连接永久占用资源 client.ReceiveTimeout = 10000; StringBuilder buffer = new StringBuilder(); byte[] recvBuffer = new byte[1024]; int recvLen; NiproEventsLog.WriteLog($"设备连接成功,客户端地址:{client.Client.RemoteEndPoint}"); // 循环读取直到拿到完整XML或者连接断开 while ((recvLen = await stream.ReadAsync(recvBuffer, 0, recvBuffer.Length)) > 0) { buffer.Append(Encoding.UTF8.GetString(recvBuffer, 0, recvLen)); string currentContent = buffer.ToString(); // 判断XML是否接收完整:匹配XML头和根节点闭合标签,请替换为实际业务的根节点名 if (currentContent.TrimStart().StartsWith("<?xml") && currentContent.TrimEnd().EndsWith("</NiproMI>")) { // 校验XML格式合法性 try { XDocument.Parse(currentContent); } catch { NiproEventsLog.WriteLog("接收到非法XML内容,跳过存储"); buffer.Clear(); continue; } // 生成合法文件名:时间精确到毫秒+GUID片段避免重名 string fileName = $"NiproMI{DateTime.Now.ToString("yyyyMMddHHmmssfff")}_{Guid.NewGuid().ToString("N").Substring(0,6)}.xml"; string savePath = Path.Combine(GetAppConfigValues.GetAppConfigValue("XmlFilePath"), fileName); // 自动创建不存在的存储目录 Directory.CreateDirectory(Path.GetDirectoryName(savePath)); await File.WriteAllTextAsync(savePath, currentContent); NiproEventsLog.WriteLog($"文件存储成功:{fileName}"); buffer.Clear(); // 若设备为单次连接推完即断,可直接break关闭连接 break; } } } } catch (Exception ex) { NiproEventsLog.WriteLog($"处理客户端连接出错:{ex.Message}"); } finally { NiproEventsLog.WriteLog("设备连接断开"); } }
注意:代码里的XML根节点判断标记
</NiproMI>需要替换为你实际业务中XML的真实根节点闭合标签,如果设备协议明确规定推完数据后会主动断开连接,也可以去掉节点判断逻辑,配合读取超时机制使用即可。
内容的提问来源于stack exchange,提问作者Ravi Kumar
相关产品推荐
相关产品推荐

