TCP / UDP 设备连接学习总结
一、TcpListener —— 服务器端
客户端 服务器 (TcpListener)
│ │
│ ───① new TcpListener(ip, port) │
│ ───② .Start() 开始监听 ────────── │
│ │
│ ◄─── AcceptTcpClientAsync() 阻塞等待 │
│ │
│ ───③ TcpClient.ConnectAsync() ──► │
│ │
│ ◄── 返回 TcpClient 实例 ───────── │
│ │
│ ───④ GetStream() 获取 NetworkStream │
│ │
│ ═════⑤ ReadAsync() / WriteAsync() ═══数据交换═════
│ │
│ ───⑥ 停止: Stop() 停止监听 ─────── │
│ │
1.1 关键方法与属性
| 成员 | 说明 |
|---|---|
new TcpListener(IPAddress, int port) | 构造,绑定本地 IP 与端口 |
Start() | 开始监听 |
Stop() | 停止监听 |
Pending() | 是否有挂起的连接请求(常用于循环里非阻塞判断) |
AcceptTcpClient() / AcceptTcpClientAsync() | 同步/异步接受一个连接,返回 TcpClient |
AcceptTcpClientAsync(CancellationToken) | 支持取消令牌的异步接受(.NET 9 项目中使用) |
1.2 基础实现(TCPListen)
业务:TCP 服务器收到客户端数据 → 写入串口;串口返回数据 → 回发给当前客户端。使用 ConcurrentQueue 把多个客户端请求串行化,避免串口并发冲突。
// 启动监听
tcpListen = new TcpListener(IPAddress.Parse("192.168.219.14"), 9999);
tcpListen.Start();
// 异步循环接受连接
private void AcceptRequest()
{
cts = new CancellationTokenSource();
Task.Run(async () =>
{
while (!cts.IsCancellationRequested)
{
if (tcpListen == null) break;
if (tcpListen.Pending()) continue; // 若挂起连接则跳过
TcpClient tcpClient = await tcpListen.AcceptTcpClientAsync();
AcceptData(tcpClient); // 转入数据接收循环
}
}, cts.Token);
}
// 读取客户端数据并通过 NetworkStream 回写串口响应
NetworkStream stream = currentClient.GetStream();
stream.Write(fullFrame, 0, fullFrame.Length);
1.3 完整设备服务器实现(TCPListen连接设备服务器)
在基础版上增加了:配置持久化(FileIni)、本机 IP 自动识别、收发字节计数、Modbus 帧按功能码解析(0x03 读 / 0x06 写)、带 CancellationToken 的异步 API。
// 自动获取本机 IPv4 地址用于监听
IPAddress[] addresses = Dns.GetHostAddresses(Dns.GetHostName());
foreach (IPAddress address in addresses)
if (address.AddressFamily == AddressFamily.InterNetwork)
txtServerIP.Text = address.ToString();
// 启动
tcpListener = new TcpListener(IPAddress.Parse(txtServerIP.Text), int.Parse(txtPort.Text));
tcpListener.Start();
// 带取消令牌的异步接受
TcpClient client = await tcpListener.AcceptTcpClientAsync(_cts.Token);
// 带取消令牌的异步读取
NetworkStream stream = client.GetStream();
int len = await stream.ReadAsync(buffer, _cts.Token);
if (len <= 0) return; // len==0 表示客户端已正常断开
// 回写响应(异步)
await stream.WriteAsync(fullFrame);
停止服务器时释放资源:
private void StopServer()
{
_cts?.Cancel();
serialPort?.Close();
tcpListener?.Stop(); // 停止监听
tcpListener = null;
}
二、TcpClient —— 客户端
客户端 (TcpClient) 服务器
│ │
│ ───① new TcpClient() ───────────── │
│ │
│ ───② .ConnectAsync(host, port) ─► │ AcceptTcpClientAsync()
│ │
│ ◄── 连接成功 (Connected=true) ──── │
│ │
│ ───③ GetStream() 获取 NetworkStream │
│ │
│ ───④ Write() / WriteAsync() 发送 ──► │ ReadAsync() 接收
│ │
│ ◄──⑤ ReadAsync() 接收响应 ───────── │ WriteAsync() 回写
│ │
│ ═════ 循环收发 ═══════════════════ │
│ │
│ ───⑥ 停止: Close() 关闭连接 ────── │
│ │
2.1 关键方法与属性
| 成员 | 说明 |
|---|---|
new TcpClient() | 构造一个未连接的客户端 |
Connect(host, port) / ConnectAsync(host, port) | 同步/异步连接服务器 |
Connected (bool) | 当前是否处于连接状态 |
Available (int) | 网络缓冲区中可读字节数 |
GetStream() | 返回 NetworkStream 用于读写 |
Client (Socket) | 底层 Socket,可取 RemoteEndPoint 等信息 |
Close() | 关闭连接 |
NetworkStream 关键成员:Read / ReadAsync(返回 0 表示对端关闭)、Write / WriteAsync。
2.2 基础实现(TCPClient)
业务:连接服务器 → 后台循环接收 → 定时器周期性发送。
// 连接
private async Task ConnectServer()
{
tcpClient = new TcpClient();
await tcpClient.ConnectAsync("192.168.219.14", 9999);
}
// 接收循环:Available 判断 + ReadAsync
private void AcceptData()
{
cts = new CancellationTokenSource();
Task.Run(async () =>
{
byte[] buffer = new byte[1024];
while (!cts.IsCancellationRequested)
{
if (tcpClient == null || !tcpClient.Connected) continue;
NetworkStream stream = tcpClient.GetStream();
int len = await stream.ReadAsync(buffer, 0, buffer.Length, cts.Token);
if (len <= 0) break;
// 处理 buffer[0..len)
}
}, cts.Token);
}
// 发送
NetworkStream stream = tcpClient.GetStream();
byte[] bs = Encoding.UTF8.GetBytes(textBox1.Text);
stream.Write(bs, 0, bs.Length);
2.3 设备客户端实现(TCPClient连接设备客户端)
业务:通过 TCP 承载 Modbus RTU 协议——组装请求帧(功能码 0x03 读 / 0x06 写)+ CRCHelper.CRC16 校验,发送后解析响应。发送使用 lock(sendLock) 保证线程安全。
// 连接(含异常提示)
await client.ConnectAsync(txtServerIP.Text, int.Parse(txtPort.Text));
// 发送 Modbus 读保持寄存器请求(功能码 0x03)
private void SendRequest(ushort slaveAddress, ushort startAddress, ushort dataLength)
{
lock (sendLock)
{
if (client == null) return;
byte[] bs = new byte[8];
bs[0] = (byte)slaveAddress;
bs[1] = 0X03;
bs[2] = (byte)(startAddress >> 8);
bs[3] = (byte)(startAddress & 0XFF);
bs[4] = (byte)(dataLength >> 8);
bs[5] = (byte)(dataLength & 0XFF);
byte[] crc = CRCHelper.CRC16(bs);
bs[6] = crc[0]; bs[7] = crc[1];
client.GetStream().Write(bs);
}
}
// 接收并按功能码解析响应
int funcCode = buffer[1];
if (funcCode == 0X03) { /* 解析寄存器值 */ }
else if (funcCode == 0X06) { /* 解析写入回显 */ }
断开连接:
private void DisConnectServer()
{
_cts?.Cancel();
client?.Close();
client = null;
}
三、UdpClient —— 无连接收发
客户端 (UdpClient) 服务器 (UdpClient)
│ │
│ ───① new UdpClient(任意端口) │
│ │
│ ───② 构造 IPEndPoint(server, port) │
│ │
│ ───③ Send(data, len, endPoint) ─► │ ReceiveAsync() 阻塞等待
│ │
│ │ 获得 RemoteEndPoint
│ │
│ ◄──④ ReceiveAsync() 收到响应 ───── │ Send(data, len, remoteEndPoint)
│ │
│ │
│ ═════ 无需连接,直接数据报收发 ════ │
│ │
│ ───⑤ 停止: Close() / Dispose() ─── │
│ │
UDP 不需要建立连接,服务器与客户端的区别仅在于:服务器绑定固定端口并 ReceiveAsync 等待数据;客户端绑定任意端口(IPAddress.Any, 0)后用 Send 指定目标端点。
3.1 关键方法与属性
| 成员 | 说明 |
|---|---|
new UdpClient(IPEndPoint) | 构造并绑定本地端点 |
Send(byte[], int, IPEndPoint) | 向指定远程端点发送数据报 |
ReceiveAsync() | 异步接收,返回 UdpReceiveResult(含 Buffer 与 RemoteEndPoint) |
Close() / Dispose() | 释放资源 |
3.2 UDP 客户端
业务:向设备发送 Modbus 请求帧,异步接收并解析响应寄存器值。
// 绑定本地任意端口,目标端点由 endPoint 指定
client = new UdpClient(new IPEndPoint(IPAddress.Any, 0));
endPoint = new IPEndPoint(IPAddress.Parse(txtIp.Text), int.Parse(txtPort.Text));
// 发送
client.Send(bytes, bytes.Length, endPoint);
// 异步接收
var res = await client.ReceiveAsync();
if (res.Buffer[1] != 0X03) continue;
int byteCount = res.Buffer[2];
// 解析寄存器值...
3.3 UDP 服务器
业务:UDP 服务器收到客户端请求 → 写串口 → 串口返回数据 → 用 Send 回发给该客户端 RemoteEndPoint。同样用 ConcurrentQueue 串行化串口访问。
// 绑定本机 IP + 9999 端口
server = new UdpClient(new IPEndPoint(address, 9999));
// 接收循环
var res = await server.ReceiveAsync();
requestQueue.Enqueue(new RequestItem { clientEndPoint = res.RemoteEndPoint, data = res.Buffer });
// 串口响应回发给对应客户端
server.Send(fullFrame, fullFrame.Length, currentRequest.clientEndPoint);
四、通用设计模式总结
4.1 异步接收循环(统一范式)
所有子项目都采用相同范式:CancellationTokenSource + Task.Run + while(!cts.IsCancellationRequested) 循环 + await XxxAsync(cts.Token)。这是 WinForms 下避免阻塞 UI 线程的标准做法。
4.2 请求队列 + 串行化处理
串口是独占资源,不能并发写入。服务器端统一使用 ConcurrentQueue<RequestItem> + lock + isProcessing 标志位,把并发的网络请求排成队列,逐个转发给串口,串口响应完成后再出队下一个:
private void TryProcessQueue()
{
lock (processLock)
{
if (isProcessing) return;
if (requestQueue.IsEmpty) return;
if (requestQueue.TryDequeue(out var item))
{
currentClient = item;
isProcessing = true;
serialPort.Write(item.Data, 0, item.Data.Length);
}
}
}
4.3 网口 ↔ 串口协议转换
以上这三个服务器项目的本质都是 协议网关:把以太网(TCP/UDP)上收到的 Modbus RTU 帧透传给串口设备,再把串口返回的帧回发给发起请求的网络客户端。
4.4 Modbus RTU 帧解析
按功能码确定帧长度,从 List<byte> 缓冲区中切片取完整帧:
- 功能码
0x03(读):帧长 =3 + 字节数 + 2(CRC) - 功能码
0x06(写):固定 8 字节 - 帧尾 2 字节 CRC16(
CRCHelper.CRC16)
完整帧解析实现(以串口数据接收为例):
private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
if (serialPort == null || !serialPort.IsOpen) return;
// 读取串口缓冲区全部数据
byte[] buffer = new byte[serialPort.BytesToRead];
serialPort.Read(buffer, 0, buffer.Length);
list.AddRange(buffer);
// 循环处理缓冲区中可能存在的多个完整帧
while (list.Count > 5)
{
// 根据功能码计算帧长度
int funcCode = list[1];
int fullFrameLength = 0;
if (funcCode == 0X03) // 读寄存器响应
{
int len = list[2]; // 字节数
fullFrameLength = 3 + len + 2; // 地址 + 功能码 + 字节数 + 数据 + CRC
}
else if (funcCode == 0X06) // 写单寄存器响应
{
fullFrameLength = 8; // 固定 8 字节
}
// 缓冲区长度足够一个完整帧
if (list.Count >= fullFrameLength)
{
byte[] fullFrame = list.Take(fullFrameLength).ToArray();
list.RemoveRange(0, fullFrameLength);
// 将完整帧回发给发起请求的客户端
if (currentClient != null && currentClient.TcpClient != null &&
currentClient.TcpClient.Connected)
{
NetworkStream stream = currentClient.TcpClient.GetStream();
await stream.WriteAsync(fullFrame);
}
}
else
break; // 数据不足,等待更多字节
}
}
4.5 连接状态与生命周期管理
- 接受端:
AcceptTcpClientAsync返回的TcpClient用Connected判断活性,ReadAsync返回0视为断开。 - 停止:统一先
cts.Cancel()取消后台任务,再Close()/Stop()/Dispose()释放网络与串口资源,并把对象置null。 - 客户端发送:用
lock保护NetworkStream.Write,避免定时器与按钮并发发送互相踩踏。
五、关键方法/属性速查表
| 类 | 方法/属性 | 用途 | 出现子项目 |
|---|---|---|---|
TcpListener | Start() / Stop() | 启停监听 | TCPListen、TCPListen连接设备服务器 |
TcpListener | Pending() | 是否有挂起连接 | TCPListen、TCPListen连接设备服务器 |
TcpListener | AcceptTcpClientAsync(CancellationToken) | 异步接受连接 | 同上 |
TcpClient | ConnectAsync(host, port) | 异步连接服务器 | TCPClient、TCPClientTest、TCPClient连接设备客户端 |
TcpClient | Connected / Available | 连接状态 / 可读字节 | 全部 TCP 客户端 |
TcpClient | GetStream() | 获取 NetworkStream | 全部 TCP |
TcpClient | Close() | 关闭连接 | 全部 TCP |
NetworkStream | ReadAsync(buf, token) | 异步读(返回 0=断开) | 全部 TCP |
NetworkStream | Write(buf) / WriteAsync(buf) | 同步/异步写 | 全部 TCP |
UdpClient | new UdpClient(IPEndPoint) | 绑定本地端点 | WindowsFormsApp5/6 |
UdpClient | Send(buf, len, IPEndPoint) | 发送到指定端点 | WindowsFormsApp5/6 |
UdpClient | ReceiveAsync() | 异步接收(含来源端点) | WindowsFormsApp5/6 |
UdpClient | Close() / Dispose() | 释放资源 | WindowsFormsApp5/6 |
六、对比与要点
6.1 TCP vs UDP
| 维度 | TCP(TcpListener / TcpClient) | UDP(UdpClient) |
|---|---|---|
| 连接 | 面向连接,需 Accept/Connect | 无连接,直接 Send/Receive |
| 可靠性 | 可靠传输、保序 | 不可靠、可能丢包/乱序 |
| 服务器区分 | TcpListener 监听 + TcpClient 表示每个连接 | 仅 UdpClient,靠 RemoteEndPoint 区分客户端 |
| 数据流 | NetworkStream 读写流 | 数据报 Send/ReceiveAsync |
| 适用 | 需要稳定可靠的设备控制(Modbus TCP) | 简单查询/广播/低开销场景 |
6.2 同步 vs 异步
- 项目统一采用 异步 API(
XxxAsync+await),配合CancellationToken实现可控停止,避免阻塞 WinForms UI 线程。 Pending()+continue是一种轻量的非阻塞轮询写法,但更推荐直接await AcceptTcpClientAsync(cts.Token)。
6.3 .NET 9 vs .NET Framework 4.8 差异
.NET 9项目(date87 下 TCP 系列)可使用AcceptTcpClientAsync(CancellationToken)、ReadAsync(buffer, token)等带取消令牌的重载。.NET Framework 4.8项目(date84 下 UDP 系列)使用ReceiveAsync()(无取消令牌重载),停止时靠cts循环判断 +Close()触发异常退出循环。
七、学习要点清单
- 服务器三步走:
new TcpListener(ip, port)→Start()→AcceptTcpClientAsync()。 - 客户端两步走:
new TcpClient()→ConnectAsync(host, port),再用GetStream()读写。 ReadAsync返回0是判断对端正常关闭的关键信号。Available属性可用于接收循环中避免空转,但异步ReadAsync本身会阻塞至有数据,二者择一即可。- UDP 服务器与客户端本质相同,区别仅在绑定端点是否固定、是否主动
Send。 - 串口 + 网口混合场景下,务必用队列 + 锁串行化串口访问。
- 停止流程要有序:
Cancel()取消任务 →Close()/Stop()关闭资源 → 置null。 - Modbus RTU 帧需按功能码计算帧长并校验 CRC16,TCP 承载时帧边界由
ReadAsync返回长度保证,UDP 则由数据报天然保界。
425



被折叠的 条评论
为什么被折叠?



