ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

c#软件开发学习笔记--TcpListener\TcpClient\UdpClient

c#软件开发学习笔记--TcpListener\TcpClient\UdpClient

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(含BufferRemoteEndPoint
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&&currentClient.TcpClient!=null&&currentClient.TcpClient.Connected){NetworkStreamstream=currentClient.TcpClient.GetStream();awaitstream.WriteAsync(fullFrame);}}elsebreak;// 数据不足,等待更多字节}}

4.5 连接状态与生命周期管理

  • 接受端AcceptTcpClientAsync返回的TcpClientConnected判断活性,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全部 TCP
TcpClientClose()关闭连接全部 TCP
NetworkStreamReadAsync(buf, token)异步读(返回 0=断开)全部 TCP
NetworkStreamWrite(buf)/WriteAsync(buf)同步/异步写全部 TCP
UdpClientnew UdpClient(IPEndPoint)绑定本地端点WindowsFormsApp5/6
UdpClientSend(buf, len, IPEndPoint)发送到指定端点WindowsFormsApp5/6
UdpClientReceiveAsync()异步接收(含来源端点)WindowsFormsApp5/6
UdpClientClose()/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 异步

  • 项目统一采用异步 APIXxxAsync+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()触发异常退出循环。

七、学习要点清单

  1. 服务器三步走:new TcpListener(ip, port)Start()AcceptTcpClientAsync()
  2. 客户端两步走:new TcpClient()ConnectAsync(host, port),再用GetStream()读写。
  3. ReadAsync返回0是判断对端正常关闭的关键信号。
  4. Available属性可用于接收循环中避免空转,但异步ReadAsync本身会阻塞至有数据,二者择一即可。
  5. UDP 服务器与客户端本质相同,区别仅在绑定端点是否固定、是否主动Send
  6. 串口 + 网口混合场景下,务必用队列 + 锁串行化串口访问。
  7. 停止流程要有序:Cancel()取消任务 →Close()/Stop()关闭资源 → 置null
  8. Modbus RTU 帧需按功能码计算帧长并校验 CRC16,TCP 承载时帧边界由ReadAsync返回长度保证,UDP 则由数据报天然保界。
返回列表