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=newTcpListener(IPAddress.Parse("192.168.219.14"),9999);tcpListen.Start();// 异步循环接受连接privatevoidAcceptRequest(){cts=newCancellationTokenSource();Task.Run(async()=>{while(!cts.IsCancellationRequested){if(tcpListen==null)break;if(tcpListen.Pending())continue;// 若挂起连接则跳过TcpClienttcpClient=awaittcpListen.AcceptTcpClientAsync();AcceptData(tcpClient);// 转入数据接收循环}},cts.Token);}// 读取客户端数据并通过 NetworkStream 回写串口响应NetworkStreamstream=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(IPAddressaddressinaddresses)if(address.AddressFamily==AddressFamily.InterNetwork)txtServerIP.Text=address.ToString();// 启动tcpListener=newTcpListener(IPAddress.Parse(txtServerIP.Text),int.Parse(txtPort.Text));tcpListener.Start();// 带取消令牌的异步接受TcpClientclient=awaittcpListener.AcceptTcpClientAsync(_cts.Token);// 带取消令牌的异步读取NetworkStreamstream=client.GetStream();intlen=awaitstream.ReadAsync(buffer,_cts.Token);if(len<=0)return;// len==0 表示客户端已正常断开// 回写响应(异步)awaitstream.WriteAsync(fullFrame);停止服务器时释放资源:
privatevoidStopServer(){_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)
业务:连接服务器 → 后台循环接收 → 定时器周期性发送。
// 连接privateasyncTaskConnectServer(){tcpClient=newTcpClient();awaittcpClient.ConnectAsync("192.168.219.14",9999);}// 接收循环:Available 判断 + ReadAsyncprivatevoidAcceptData(){cts=newCancellationTokenSource();Task.Run(async()=>{byte[]buffer=newbyte[1024];while(!cts.IsCancellationRequested){if(tcpClient==null||!tcpClient.Connected)continue;NetworkStreamstream=tcpClient.GetStream();intlen=awaitstream.ReadAsync(buffer,0,buffer.Length,cts.Token);if(len<=0)break;// 处理 buffer[0..len)}},cts.Token);}// 发送NetworkStreamstream=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)保证线程安全。
// 连接(含异常提示)awaitclient.ConnectAsync(txtServerIP.Text,int.Parse(txtPort.Text));// 发送 Modbus 读保持寄存器请求(功能码 0x03)privatevoidSendRequest(ushortslaveAddress,ushortstartAddress,ushortdataLength){lock(sendLock){if(client==null)return;byte[]bs=newbyte[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);}}// 接收并按功能码解析响应intfuncCode=buffer[1];if(funcCode==0X03){/* 解析寄存器值 */}elseif(funcCode==0X06){/* 解析写入回显 */}断开连接:
privatevoidDisConnectServer(){_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=newUdpClient(newIPEndPoint(IPAddress.Any,0));endPoint=newIPEndPoint(IPAddress.Parse(txtIp.Text),int.Parse(txtPort.Text));// 发送client.Send(bytes,bytes.Length,endPoint);// 异步接收varres=awaitclient.ReceiveAsync();if(res.Buffer[1]!=0X03)continue;intbyteCount=res.Buffer[2];// 解析寄存器值...3.3 UDP 服务器
业务:UDP 服务器收到客户端请求 → 写串口 → 串口返回数据 → 用Send回发给该客户端RemoteEndPoint。同样用ConcurrentQueue串行化串口访问。
// 绑定本机 IP + 9999 端口server=newUdpClient(newIPEndPoint(address,9999));// 接收循环varres=awaitserver.ReceiveAsync();requestQueue.Enqueue(newRequestItem{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标志位,把并发的网络请求排成队列,逐个转发给串口,串口响应完成后再出队下一个:
privatevoidTryProcessQueue(){lock(processLock){if(isProcessing)return;if(requestQueue.IsEmpty)return;if(requestQueue.TryDequeue(outvaritem)){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)
完整帧解析实现(以串口数据接收为例):
privatevoidSerialPort_DataReceived(objectsender,SerialDataReceivedEventArgse){if(serialPort==null||!serialPort.IsOpen)return;// 读取串口缓冲区全部数据byte[]buffer=newbyte[serialPort.BytesToRead];serialPort.Read(buffer,0,buffer.Length);list.AddRange(buffer);// 循环处理缓冲区中可能存在的多个完整帧while(list.Count>5){// 根据功能码计算帧长度intfuncCode=list[1];intfullFrameLength=0;if(funcCode==0X03)// 读寄存器响应{intlen=list[2];// 字节数fullFrameLength=3+len+2;// 地址 + 功能码 + 字节数 + 数据 + CRC}elseif(funcCode==0X06)// 写单寄存器响应{fullFrameLength=8;// 固定 8 字节}// 缓冲区长度足够一个完整帧if(list.Count>=fullFrameLength){byte[]fullFrame=list.Take(fullFrameLength).ToArray();list.RemoveRange(0,fullFrameLength);// 将完整帧回发给发起请求的客户端if(currentClient!=null&¤tClient.TcpClient!=null&¤tClient.TcpClient.Connected){NetworkStreamstream=currentClient.TcpClient.GetStream();awaitstream.WriteAsync(fullFrame);}}elsebreak;// 数据不足,等待更多字节}}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 则由数据报天然保界。