用 TcpClient
和 TcpListener
快速建立连接
最直接的方式是用 .NET 封装好的高层类,避开原始
Socket的细节。它们底层仍基于
Socket,但自动处理地址解析、连接状态管理等常见逻辑。
客户端用
TcpClient,服务端用
TcpListener,两者配对使用即可通信。注意:一个
TcpClient实例只对应一个连接;
TcpListener启动后需调用
AcceptTcpClient()或
AcceptSocket()才真正接收连接。 服务端监听必须指定
IPAddress.Any或具体 IP,不能用
localhost字符串(会解析失败)
TcpClient.Connect()是同步阻塞的,超时默认无限等待,建议改用带超时的重载或配合
CancellationToken读写前务必检查
NetworkStream.CanRead/CanWrite,断连后流可能仍可读但返回 0 字节
var listener = new TcpListener(IPAddress.Any, 8080); listener.Start(); using var client = listener.AcceptTcpClient(); // 阻塞直到有连接 using var stream = client.GetStream(); byte[] buffer = new byte[1024]; int len = stream.Read(buffer, 0, buffer.Length); // 返回实际读取字节数
手动管理 Socket
时必须处理的三个状态点
原始
Socket更灵活,但也更易出错。关键不是“怎么发”,而是“连接是否还活着”“对方是否已关闭”“缓冲区是否满”。这三个状态不显式判断,程序大概率在收发时卡死或抛异常。
典型错误现象:
Socket.Receive()返回 0 表示对端已关闭连接,不是“没数据”;
Socket.Send()返回值小于请求长度,说明内核发送缓冲区已满,需重试;
Socket.Connected属性不可靠,它只反映最后一次 I/O 操作后的状态,不是实时连接状态。 永远用
Send()和
Receive()的返回值判断实际传输量,别假设一次调用就完成全部数据 检测对端关闭:当
Receive()返回 0,应主动
Shutdown(SocketShutdown.Both)再
Close()避免轮询
Connected,改用异步方法(如
BeginReceive)或
Poll()+
SelectMode.SelectRead判断可读性
NetworkStream
不能直接用于大文件传输
TcpClient.GetStream()返回的
NetworkStream默认无缓冲,且不支持
Seek()或
Length。如果直接拿它套
BinaryReader或
StreamReader读长消息,容易因粘包或半包导致解析失败。
常见场景:发送 JSON 或自定义协议消息时,必须约定长度前缀(如 4 字节 int 表示后续内容长度),否则接收方无法知道一次消息何时结束。
不要依赖NetworkStream.Read()一次性读完整条消息——它只保证至少读 1 字节,最多读缓冲区剩余空间 发送前先写长度头,再写内容;接收时先读 4 字节长度,再循环读够指定字节数 若用
StreamReader.ReadLine(),确保发送端每行末尾是
\r\n,且未禁用
AutoFlush
// 发送带长度头的消息
var data = Encoding.UTF8.GetBytes("hello");
var header = BitConverter.GetBytes(data.Length);
stream.Write(header, 0, 4);
stream.Write(data, 0, data.Length);
异步操作中别混用 async/await
和回调模式
.NET 提供两套 API:基于
BeginXXX/EndXXX的 APM 模式和基于
XXXAsync的 TAP 模式。二者底层机制不同,混用会导致资源泄漏或未触发回调。
例如,在
TcpClient上调用
BeginConnect()后,不能再对同一实例调用
ConnectAsync();同样,用
Socket.AcceptAsync()初始化的
SocketAsyncEventArgs,不能拿去传给
SendAsync()。 新项目统一用 TAP(
ConnectAsync、
ReadAsync、
WriteAsync),它与
async/await天然契合 APM 模式仅用于维护旧代码,且必须配对使用
BeginXxx和
EndXxx
SocketAsyncEventArgs是高性能场景专用,需手动复用对象、管理缓冲区,普通业务没必要上
真正的难点不在语法,而在如何安全地在线程间传递连接上下文、如何设计消息边界、以及断线后重连时的状态清理——这些不会报编译错误,但会让程序在高并发下悄无声息地丢数据或卡死。
