C# TcpListener接收XML流偶发拆分存储为多文件问题求助
问题根因
你遇到的XML拆分存储问题是两个核心逻辑缺陷共同导致的:
- 文件名生成时机错误:当前每次执行写入操作时都会重新读取
DateTime.Now生成文件名,而TCP是流传输协议,你设置的接收缓冲区仅256字节,单份4KB的XML需要十几次Read操作才能读完。只要相邻两次读取操作刚好跨过时间格式的最小精度边界(默认DateTime.Now.ToString()精度到秒),两次写入拿到的文件名就会不同,直接生成第二个文件拆分数据。 - 未做应用层报文完整性校验:TCP不保证单次
Read返回完整的业务报文,你当前逻辑没有判断XML是否接收完成,只要接收过程触发文件名变更就会直接拆分存储,没有任何报文聚合逻辑。
除此之外代码还存在几个隐性问题:
- 接收数据时用ASCII解码、写文件时用UTF8编码,非ASCII字符会乱码
- TcpClient、NetworkStream、文件流均未手动释放,长期运行会出现句柄泄漏
- 空catch块直接吞掉Socket异常,网络故障时无日志可排查
- 先判断文件存在再打开的逻辑存在竞态条件,极端场景会触发文件占用错误
- 用
WriteLine写入会在每个接收分片后追加换行符,可能破坏XML结构
修复方案
调整逻辑:新连接建立后累计接收字节流,检测到XML起始标记时生成唯一文件名持续写入,直到检测到对应XML的完整闭合标记后,结束当前文件写入,等待下一份XML。所有流资源用语法糖自动释放,统一编码格式,补全异常日志。
修复后可直接运行的代码如下:
public static void GetXMLStream() { TcpListener server = null; try { Int32 port = Int32.Parse(GetAppConfigValues.GetAppConfigValue("Port")); IPAddress localAddr = IPAddress.Parse(GetAppConfigValues.GetAppConfigValue("IPAddress")); server = new TcpListener(localAddr, port); server.Start(); // 调大缓冲区到8KB适配单份XML大小,可根据实际报文大小调整 Byte[] bytes = new Byte[8192]; while (true) { // 自动释放客户端连接和流资源,避免句柄泄漏 using TcpClient client = server.AcceptTcpClient(); using NetworkStream stream = client.GetStream(); StringBuilder xmlBuffer = new StringBuilder(); string currentFileName = null; FileStream currentFs = null; StreamWriter currentSw = null; int i; // 逐块读取流数据 while ((i = stream.Read(bytes, 0, bytes.Length)) != 0) { // 统一用UTF8编解码,和写文件编码保持一致 string segment = System.Text.Encoding.UTF8.GetString(bytes, 0, i); xmlBuffer.Append(segment); string currentContent = xmlBuffer.ToString(); // 检测到新XML开头,仅生成一次文件名 if (currentFileName == null && currentContent.TrimStart().StartsWith("<?xml")) { // 文件名加毫秒级精度,避免同秒多文件重名 string fileName = "NiproMI" + DateTime.Now.ToString("yyyyMMddHHmmssfff") + ".xml"; string filePath = Path.Combine(System.Configuration.ConfigurationManager.AppSettings["XmlFilePath"].ToString(), fileName); currentFs = new FileStream(filePath, FileMode.Create, FileAccess.Write); currentSw = new StreamWriter(currentFs, System.Text.Encoding.UTF8); currentFileName = fileName; } // 已有打开的文件就写入当前分片 if (currentSw != null) { currentSw.Write(segment); currentSw.Flush(); // 检测XML根节点闭合(请替换成你实际业务的XML根节点闭合标签,比如</NiproMI>) if (currentContent.TrimEnd().EndsWith("</Root>")) { // 写完一份完整XML,释放流,重置状态等待下一份 currentSw.Close(); currentFs.Close(); currentSw = null; currentFs = null; currentFileName = null; xmlBuffer.Clear(); } } } // 连接断开时兜底释放未关闭的文件流 currentSw?.Close(); currentFs?.Close(); } } catch (SocketException e) { // 补全异常日志,禁止吞异常 NiproEventsLog.WriteLog(e.ToString()); } finally { server?.Stop(); } }
注意事项:
- 代码中
</Root>请替换为你实际业务XML的根节点闭合标签,比如你的XML根节点是<NiproData>就改成</NiproData>,这是判断单份XML是否接收完成的核心标记- 文件名仅在检测到新XML开头时生成一次,从根源上规避跨时间边界拆分文件的问题
- 写入用
Write替代原WriteLine,不会额外追加换行符破坏XML结构- 所有连接、流资源都确保释放,长期运行不会出现句柄泄漏
- 异常全部写入日志,出现网络问题可直接追溯
内容的提问来源于stack exchange,提问作者Sup Ravi Kumar
相关产品推荐
相关产品推荐

