C# 用 TcpListener + TcpClient 搭建 TCP 通信最稳,因封装了 Accept、NetworkStream 读写与关闭语义,避免裸 Socket 易卡主线程、多客户端处理难、异常后状态难清理等问题。

直接上结论:C# 用 TcpListener + TcpClient 搭建基础 TCP 通信最稳,不依赖第三方库,.NET Core 3.1+ 和 .NET 5/6/7/8 全支持;但别用 Socket 类裸写阻塞式逻辑——容易卡主线程、无法处理多客户端、异常后连接状态难清理。
为什么推荐 TcpListener 而不是裸 Socket?
裸 Socket 需手动管理连接队列、线程/异步上下文、缓冲区边界、SocketError.ConnectionReset 等错误恢复,稍有疏忽就会出现“客户端断开后服务端还在等 recv”或“Send 后没检查返回值导致数据截断”。TcpListener 封装了监听套接字的 Accept 流程,TcpClient 封装了 NetworkStream 的读写和关闭语义,更贴近实际开发直觉。
-
TcpListener.Start()自动设置SO_REUSEADDR,避免端口占用报错AddressAlreadyInUse -
TcpClient.GetStream()返回的NetworkStream支持ReadAsync/WriteAsync,天然适配async/await - 调用
TcpClient.Close()或释放时自动触发shutdown(SD_SEND)+closesocket,比裸Socket.Shutdown()+.Close()不易漏步骤
服务端必须处理 Accept 异常和客户端断连
常见错误是把 listener.AcceptTcpClientAsync() 包在 try-catch 外层,结果 ObjectDisposedException(服务停止时 listener 已释放)或 SocketException(网络抖动)直接崩掉整个监听循环。正确做法是每个 Accept 操作独立捕获,且必须检查客户端是否已断开。
- 在
while (isRunning)循环内对每次AcceptTcpClientAsync()单独 try/catch - 拿到
TcpClient后立刻检查client.Client.Connected—— Windows 上某些 NAT 断连不会立刻触发异常,需靠这个兜底 - 读取
NetworkStream时,ReadAsync返回 0 表示对方已关闭连接,必须跳出循环并释放资源 - 不要复用同一个
NetworkStream多次 ReadAsync —— 每次读完应检查返回值,0 就 Close,负数就丢弃
示例关键片段:
while (isRunning)
{
try
{
var client = await listener.AcceptTcpClientAsync();
_ = HandleClientAsync(client); // fire-and-forget,避免阻塞 Accept
}
catch (ObjectDisposedException) when (!isRunning) { /* 正常退出 */ }
catch (SocketException ex) when (ex.SocketErrorCode == SocketError.Interrupted) { /* 忽略被中断 */ }
catch (Exception ex) { /* 记日志,不 throw */ }
}
<p>async Task HandleClientAsync(TcpClient client)
{
if (!client.Client.Connected) { client.Close(); return; }
using var stream = client.GetStream();
var buffer = new byte[1024];
while (isRunning)
{
int read = await stream.ReadAsync(buffer, CancellationToken.None);
if (read == 0) break; // 对端关闭
// 处理 buffer[0..read]
}
client.Close(); // 显式关闭,触发 FIN
}</p>
客户端连接要设超时,且必须检查 NetworkStream.CanRead/CanWrite
直接 new TcpClient().ConnectAsync(host, port) 不设超时,DNS 解析失败或目标主机防火墙拦截时会卡住 20–30 秒(取决于系统 TCP 连接超时)。另外,TcpClient.Connected 在连接刚建立时为 true,但写入时可能因网络中断抛 IOException,不能只靠它判断可用性。
- 用
TcpClient.ConnectAsync(string, int, TimeSpan)(.NET 6+)或自己用CancellationTokenSource包裹旧版 Connect - 写入前检查
stream.CanWrite,读取前检查stream.CanRead—— 某些中间设备(如代理)会单向关闭流 - 发送消息建议加简单帧头(如 4 字节长度),避免粘包;不用
StreamReader.ReadLine(),它依赖 \n 且不处理超时 - 关闭时先
stream.WriteAsync()发完再client.Close(),否则可能丢最后一点数据
真正难的不是写通第一行收发,而是让服务端扛住 100 个并发连接不泄漏句柄、客户端在 WiFi 切 4G 时能自动重连、断网恢复后旧连接不堆积。这些得靠连接池管理、心跳保活、带重试的 ConnectAsync + 状态机,而不是堆代码行数。











