StreamReader.ReadLineAsync忽略单独回车符导致读取挂起问题求解
问题根因
你遇到的是StreamReader内置行读取逻辑的已知行为:当读取到单独的\r(回车符,ASCII 13)时,ReadLineAsync会等待下一个字符到达,以判断是否属于\r\n序列,不会直接将单独的\r判定为行结束。而网络流在对方没有发送新数据、也没有断开连接的情况下,读取操作会一直挂起,就出现了你遇到的问题。同时不建议在网络场景下使用EndOfStream属性做循环判断,该属性会触发阻塞读取来判定流是否结束,也会引发卡顿问题。
解决方案
自行实现支持单独\r作为行结束符的异步读取逻辑,替换原生的ReadLineAsync即可,实现代码如下:
自定义行读取扩展方法
using System.Text; public static class StreamReaderExtensions { public static async Task<string> ReadLineWithCrSupportAsync(this StreamReader reader, CancellationToken cancellationToken = default) { var stringBuilder = new StringBuilder(); bool lastCharIsCr = false; while (true) { cancellationToken.ThrowIfCancellationRequested(); char[] buffer = new char[1]; int readLength = await reader.ReadAsync(buffer, 0, 1, cancellationToken); // 流已结束,返回已读取内容 if (readLength == 0) { if (lastCharIsCr) stringBuilder.Append('\r'); return stringBuilder.Length > 0 ? stringBuilder.ToString() : null; } char currentChar = buffer[0]; // 上一个字符是\r的场景判断 if (lastCharIsCr) { // 是\r\n序列,直接返回当前构建的行 if (currentChar == '\n') return stringBuilder.ToString(); // 是单独\r,将当前字符退回缓冲区,返回当前构建的行 else { var charBufferField = typeof(StreamReader).GetField("_charBuffer", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); var charPosField = typeof(StreamReader).GetField("_charPos", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); if (charBufferField != null && charPosField != null) { char[] charBuffer = (char[])charBufferField.GetValue(reader); int charPos = (int)charPosField.GetValue(reader); if (charPos > 0) { charPosField.SetValue(reader, charPos - 1); charBuffer[charPos - 1] = currentChar; } else { stringBuilder.Append('\r'); } } else { stringBuilder.Append('\r'); } return stringBuilder.ToString(); } } if (currentChar == '\n') return stringBuilder.ToString(); if (currentChar == '\r') { lastCharIsCr = true; continue; } stringBuilder.Append(currentChar); } } }
修改后的服务端代码
using System.Net.Sockets; using System.Text; static async Task Main(string[] args) { var listener = TcpListener.Create(22); listener.Start(); // 增加取消令牌支持,方便程序优雅退出 using var cts = new CancellationTokenSource(); Console.CancelKeyPress += (s, e) => { e.Cancel = true; cts.Cancel(); listener.Stop(); }; try { var client = await listener.AcceptTcpClientAsync(cts.Token); using (var networkStream = client.GetStream()) // 根据对接系统实际编码修改此处的编码参数 using (var sr = new StreamReader(networkStream, Encoding.UTF8, leaveOpen: true)) using (var sw = new StreamWriter(networkStream, Encoding.UTF8, leaveOpen: true) { AutoFlush = true }) { try { while (!cts.IsCancellationRequested) { var clientData = await sr.ReadLineWithCrSupportAsync(cts.Token); // 客户端断开连接 if (clientData == null) break; // 此处填写你的业务处理逻辑 Console.WriteLine($"收到行数据:{clientData}"); } } catch (OperationCanceledException) { Console.WriteLine("服务正在退出"); } catch (Exception ex) { Console.WriteLine($"业务处理异常:{ex.Message}"); } } } catch (OperationCanceledException) { Console.WriteLine("服务已停止"); } }
额外优化建议
- 给
StreamWriter设置AutoFlush = true,避免写入的内容滞留在缓冲区未发送到网络 - 如果对接的外部系统传输的是二进制数据而非纯文本,建议直接处理
byte流而非用StreamReader做字符转换,避免编码转换导致的数据损坏 - 可以根据业务需求增加读取超时逻辑,避免连接长时间空闲占用资源
内容的提问来源于stack exchange,提问作者martinm
相关产品推荐
相关产品推荐

