c# 如何进行tcp/ip通信

来源:这里教程网 时间:2026-02-21 17:37:05 作者:

TcpClient
TcpListener
做最简双向通信

这是 C# 里最常用、最可控的 TCP 通信方式。不依赖高层封装(比如

HttpClient
),能直接读写字节流,适合自定义协议、实时数据传输等场景。

关键点:客户端用

TcpClient
连接,服务端用
TcpListener
监听;双方都通过
NetworkStream
读写;必须手动处理粘包/拆包(除非你只传固定长度消息)。

常见错误现象:

TcpClient.Connect()
SocketException
(目标主机不可达、端口被拒绝)、
NetworkStream.Read()
阻塞卡死(对方没发完或已断连但未触发异常)、中文乱码(没统一编码,比如一方用
UTF8
一方用
ASCII
)。

服务端监听必须调用
Start()
,否则
AcceptTcpClient()
会一直阻塞或抛异常
每次
Read()
返回的是实际读到的字节数,不能假设一次读完全部数据;建议用循环读取直到满足预期长度或遇到结束标记
发送前建议先
stream.Write()
stream.Flush()
,尤其在非
AutoFlush=true
的包装流中
务必在
finally
using
中关闭
TcpClient
NetworkStream
,否则连接会残留(TIME_WAIT 状态堆积)
/* 服务端示例(控制台) */
var listener = new TcpListener(IPAddress.Any, 8080);
listener.Start();
Console.WriteLine("等待连接...");
using var client = await listener.AcceptTcpClientAsync();
using var stream = client.GetStream();
<p>var buffer = new byte[1024];
int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length);
string msg = Encoding.UTF8.GetString(buffer, 0, bytesRead);
Console.WriteLine($"收到: {msg}");</p><p>string reply = "OK";
await stream.WriteAsync(Encoding.UTF8.GetBytes(reply), 0, reply.Length);</p>

异步操作别忘了
await
,也别漏掉超时控制

同步方法(如

Connect()
Read()
)会阻塞线程,在服务端高并发或客户端 UI 线程中极易卡死。C# 的
TcpClient
TcpListener
都提供了完整的异步方法族(
ConnectAsync
AcceptTcpClientAsync
ReadAsync
WriteAsync
),必须配合
await
使用。

但异步不等于无风险:如果对端突然断网,

ReadAsync
可能无限期挂起(TCP Keep-Alive 默认关闭)。所以生产环境必须设超时。

设置
TcpClient.Client.ReceiveTimeout
SendTimeout
(单位毫秒),注意:这些只对同步方法生效
异步操作推荐用
CancellationToken
+
Task.TimeoutAfter()
(.NET 6+)或
Task.WhenAny()
包装
不要依赖
client.Connected
判断连通性——它只反映上次操作的状态,网络中断后该属性仍可能返回
true
真正可靠的检测是尝试读/写并捕获
IOException
SocketException
(如
WSAETIMEDOUT
WSAECONNRESET

UdpClient
不是替代方案,别混用场景

有人看到 “TCP/IP” 就下意识查

UdpClient
,这是典型误解。
UdpClient
走的是 UDP 协议,无连接、不保证顺序、不保证送达。它适合广播、音视频推流、心跳包等容忍丢包的场景,但绝不能用来替代 TCP 做可靠命令交互或文件传输。

如果你的需求是“发一条指令,等一个确定响应”,就必须用

TcpClient
;用
UdpClient
后发现收不到回复、多次重发、自己实现 ACK 机制……说明你已经踩进协议误用的坑了。

UdpClient.Send()
成功只表示数据进了系统发送缓冲区,不代表对方收到
UdpClient.Receive()
是阻塞式,且每次只收一个 UDP 包(最大约 64KB),不会自动拼包
TCP 的流量控制、拥塞避免、重传机制都是内建的;UDP 全要你自己补

跨平台和 .NET 版本要注意
System.Net.Sockets
的行为差异

.NET 5+ 统一了 Socket API,但某些细节仍有区别:比如

TcpClient.Client.DuplicateAndClose()
在 Linux 上不支持;
TcpListener
构造函数传
IPAddress.Any
在 macOS 上可能绑定失败(需改用
IPAddress.IPv6Any
并启用 IPv6);
NetworkStream.ReadAsync
在 .NET Core 3.1 之前不支持取消令牌。

更隐蔽的问题是 DNS 解析:Windows 下

TcpClient.Connect("localhost", 8080)
通常走 IPv4,Linux/macOS 可能优先走 IPv6,导致连接被防火墙拦截或服务端没监听对应地址族。

显式指定地址族:用
new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080)
替代字符串主机名
检查目标端口是否真被监听:
netstat -an | grep 8080
(Linux/macOS)或
netstat -ano | findstr :8080
(Windows)
防火墙常拦新端口:Linux 用
ufw allow 8080
,Windows 检查“高级安全 Windows 防火墙”入站规则
Docker 容器内访问宿主服务,别用
localhost
,改用
host.docker.internal
(Docker Desktop)或宿主真实 IP

实际写的时候,最容易被忽略的是:没有处理半关闭连接(fin 包到达但 socket 还开着)和读取不完整数据包的边界逻辑。哪怕只是调试,也要在

ReadAsync
后检查返回值是否为 0(对端关闭),并设计好自己的消息头(比如前 4 字节存长度)来应对粘包。

相关推荐