
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()同步/异步接受一个连接返回TcpClientAcceptTcpClientAsync(CancellationToken)支持取消令牌的异步接受.NET 9 项目中使用1.2 基础实现TCPListen业务TCP 服务器收到客户端数据 → 写入串口串口返回数据 → 回发给当前客户端。使用ConcurrentQueue把多个客户端请求串行化避免串口并发冲突。// 启动监听tcpListennewTcpListener(IPAddress.Parse(192.168.219.14),9999);tcpListen.Start();// 异步循环接受连接privatevoidAcceptRequest(){ctsnewCancellationTokenSource();Task.Run(async(){while(!cts.IsCancellationRequested){if(tcpListennull)break;if(tcpListen.Pending())continue;// 若挂起连接则跳过TcpClienttcpClientawaittcpListen.AcceptTcpClientAsync();AcceptData(tcpClient);// 转入数据接收循环}},cts.Token);}// 读取客户端数据并通过 NetworkStream 回写串口响应NetworkStreamstreamcurrentClient.GetStream();stream.Write(fullFrame,0,fullFrame.Length);1.3 完整设备服务器实现TCPListen连接设备服务器在基础版上增加了配置持久化FileIni、本机 IP 自动识别、收发字节计数、Modbus 帧按功能码解析0x03读 /0x06写、带CancellationToken的异步 API。// 自动获取本机 IPv4 地址用于监听IPAddress[]addressesDns.GetHostAddresses(Dns.GetHostName());foreach(IPAddressaddressinaddresses)if(address.AddressFamilyAddressFamily.InterNetwork)txtServerIP.Textaddress.ToString();// 启动tcpListenernewTcpListener(IPAddress.Parse(txtServerIP.Text),int.Parse(txtPort.Text));tcpListener.Start();// 带取消令牌的异步接受TcpClientclientawaittcpListener.AcceptTcpClientAsync(_cts.Token);// 带取消令牌的异步读取NetworkStreamstreamclient.GetStream();intlenawaitstream.ReadAsync(buffer,_cts.Token);if(len0)return;// len0 表示客户端已正常断开// 回写响应异步awaitstream.WriteAsync(fullFrame);停止服务器时释放资源privatevoidStopServer(){_cts?.Cancel();serialPort?.Close();tcpListener?.Stop();// 停止监听tcpListenernull;}二、TcpClient —— 客户端客户端 (TcpClient) 服务器 │ │ │ ───① new TcpClient() ───────────── │ │ │ │ ───② .ConnectAsync(host, port) ─► │ AcceptTcpClientAsync() │ │ │ ◄── 连接成功 (Connectedtrue) ──── │ │ │ │ ───③ 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(){tcpClientnewTcpClient();awaittcpClient.ConnectAsync(192.168.219.14,9999);}// 接收循环Available 判断 ReadAsyncprivatevoidAcceptData(){ctsnewCancellationTokenSource();Task.Run(async(){byte[]buffernewbyte[1024];while(!cts.IsCancellationRequested){if(tcpClientnull||!tcpClient.Connected)continue;NetworkStreamstreamtcpClient.GetStream();intlenawaitstream.ReadAsync(buffer,0,buffer.Length,cts.Token);if(len0)break;// 处理 buffer[0..len)}},cts.Token);}// 发送NetworkStreamstreamtcpClient.GetStream();byte[]bsEncoding.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 读保持寄存器请求功能码 0x03privatevoidSendRequest(ushortslaveAddress,ushortstartAddress,ushortdataLength){lock(sendLock){if(clientnull)return;byte[]bsnewbyte[8];bs[0](byte)slaveAddress;bs[1]0X03;bs[2](byte)(startAddress8);bs[3](byte)(startAddress0XFF);bs[4](byte)(dataLength8);bs[5](byte)(dataLength0XFF);byte[]crcCRCHelper.CRC16(bs);bs[6]crc[0];bs[7]crc[1];client.GetStream().Write(bs);}}// 接收并按功能码解析响应intfuncCodebuffer[1];if(funcCode0X03){/* 解析寄存器值 */}elseif(funcCode0X06){/* 解析写入回显 */}断开连接privatevoidDisConnectServer(){_cts?.Cancel();client?.Close();clientnull;}三、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与RemoteEndPointClose()/Dispose()释放资源3.2 UDP 客户端业务向设备发送 Modbus 请求帧异步接收并解析响应寄存器值。// 绑定本地任意端口目标端点由 endPoint 指定clientnewUdpClient(newIPEndPoint(IPAddress.Any,0));endPointnewIPEndPoint(IPAddress.Parse(txtIp.Text),int.Parse(txtPort.Text));// 发送client.Send(bytes,bytes.Length,endPoint);// 异步接收varresawaitclient.ReceiveAsync();if(res.Buffer[1]!0X03)continue;intbyteCountres.Buffer[2];// 解析寄存器值...3.3 UDP 服务器业务UDP 服务器收到客户端请求 → 写串口 → 串口返回数据 → 用Send回发给该客户端RemoteEndPoint。同样用ConcurrentQueue串行化串口访问。// 绑定本机 IP 9999 端口servernewUdpClient(newIPEndPoint(address,9999));// 接收循环varresawaitserver.ReceiveAsync();requestQueue.Enqueue(newRequestItem{clientEndPointres.RemoteEndPoint,datares.Buffer});// 串口响应回发给对应客户端server.Send(fullFrame,fullFrame.Length,currentRequest.clientEndPoint);四、通用设计模式总结4.1 异步接收循环统一范式所有子项目都采用相同范式CancellationTokenSourceTask.Runwhile(!cts.IsCancellationRequested)循环 await XxxAsync(cts.Token)。这是 WinForms 下避免阻塞 UI 线程的标准做法。4.2 请求队列 串行化处理串口是独占资源不能并发写入。服务器端统一使用ConcurrentQueueRequestItemlockisProcessing标志位把并发的网络请求排成队列逐个转发给串口串口响应完成后再出队下一个privatevoidTryProcessQueue(){lock(processLock){if(isProcessing)return;if(requestQueue.IsEmpty)return;if(requestQueue.TryDequeue(outvaritem)){currentClientitem;isProcessingtrue;serialPort.Write(item.Data,0,item.Data.Length);}}}4.3 网口 ↔ 串口协议转换以上这三个服务器项目的本质都是协议网关把以太网(TCP/UDP)上收到的 Modbus RTU 帧透传给串口设备再把串口返回的帧回发给发起请求的网络客户端。4.4 Modbus RTU 帧解析按功能码确定帧长度从Listbyte缓冲区中切片取完整帧功能码0x03读帧长 3 字节数 2(CRC)功能码0x06写固定 8 字节帧尾 2 字节 CRC16CRCHelper.CRC16完整帧解析实现以串口数据接收为例privatevoidSerialPort_DataReceived(objectsender,SerialDataReceivedEventArgse){if(serialPortnull||!serialPort.IsOpen)return;// 读取串口缓冲区全部数据byte[]buffernewbyte[serialPort.BytesToRead];serialPort.Read(buffer,0,buffer.Length);list.AddRange(buffer);// 循环处理缓冲区中可能存在的多个完整帧while(list.Count5){// 根据功能码计算帧长度intfuncCodelist[1];intfullFrameLength0;if(funcCode0X03)// 读寄存器响应{intlenlist[2];// 字节数fullFrameLength3len2;// 地址 功能码 字节数 数据 CRC}elseif(funcCode0X06)// 写单寄存器响应{fullFrameLength8;// 固定 8 字节}// 缓冲区长度足够一个完整帧if(list.CountfullFrameLength){byte[]fullFramelist.Take(fullFrameLength).ToArray();list.RemoveRange(0,fullFrameLength);// 将完整帧回发给发起请求的客户端if(currentClient!nullcurrentClient.TcpClient!nullcurrentClient.TcpClient.Connected){NetworkStreamstreamcurrentClient.TcpClient.GetStream();awaitstream.WriteAsync(fullFrame);}}elsebreak;// 数据不足等待更多字节}}4.5 连接状态与生命周期管理接受端AcceptTcpClientAsync返回的TcpClient用Connected判断活性ReadAsync返回0视为断开。停止统一先cts.Cancel()取消后台任务再Close()/Stop()/Dispose()释放网络与串口资源并把对象置null。客户端发送用lock保护NetworkStream.Write避免定时器与按钮并发发送互相踩踏。五、关键方法/属性速查表类方法/属性用途出现子项目TcpListenerStart()/Stop()启停监听TCPListen、TCPListen连接设备服务器TcpListenerPending()是否有挂起连接TCPListen、TCPListen连接设备服务器TcpListenerAcceptTcpClientAsync(CancellationToken)异步接受连接同上TcpClientConnectAsync(host, port)异步连接服务器TCPClient、TCPClientTest、TCPClient连接设备客户端TcpClientConnected/Available连接状态 / 可读字节全部 TCP 客户端TcpClientGetStream()获取 NetworkStream全部 TCPTcpClientClose()关闭连接全部 TCPNetworkStreamReadAsync(buf, token)异步读返回 0断开全部 TCPNetworkStreamWrite(buf)/WriteAsync(buf)同步/异步写全部 TCPUdpClientnew UdpClient(IPEndPoint)绑定本地端点WindowsFormsApp5/6UdpClientSend(buf, len, IPEndPoint)发送到指定端点WindowsFormsApp5/6UdpClientReceiveAsync()异步接收含来源端点WindowsFormsApp5/6UdpClientClose()/Dispose()释放资源WindowsFormsApp5/6六、对比与要点6.1 TCP vs UDP维度TCPTcpListener / TcpClientUDPUdpClient连接面向连接需Accept/Connect无连接直接Send/Receive可靠性可靠传输、保序不可靠、可能丢包/乱序服务器区分TcpListener监听 TcpClient表示每个连接仅UdpClient靠RemoteEndPoint区分客户端数据流NetworkStream读写流数据报Send/ReceiveAsync适用需要稳定可靠的设备控制Modbus TCP简单查询/广播/低开销场景6.2 同步 vs 异步项目统一采用异步 APIXxxAsyncawait配合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 帧需按功能码计算帧长并校验 CRC16TCP 承载时帧边界由ReadAsync返回长度保证UDP 则由数据报天然保界。