当前位置: 首页 > news >正文

C# TcpListener、TcpClient 与 UdpClient 通讯学习笔记

本笔记主要覆盖以下内容:

  • TCP 通讯:使用TcpListener创建服务端,使用TcpClient创建客户端,通过NetworkStream收发数据。

  • UDP 单播:两个固定端点之间通过UdpClient.Send()UdpClient.Receive()点对点通信。

  • UDP 广播:服务端向广播地址发送数据,局域网内绑定指定端口的客户端都可以接收。

  • UDP 与设备通讯:客户端组包发送请求,服务端转发到串口设备,再把设备响应转发给 UDP 客户端。

一、核心概念

术语说明
Socket.NET 中最底层、最通用的网络通讯 API,可用于 TCP、UDP 等协议,但使用复杂。
TcpListenerTCP 服务端封装类,负责绑定 IP 和端口、启动监听、接收客户端连接。
TcpClientTCP 客户端封装类,也可以表示服务端接收到的某个客户端连接。
NetworkStreamTCP 连接建立后用于读写数据的网络流,通过TcpClient.GetStream()获取。
UdpClientUDP 通讯封装类,用于发送和接收 UDP 数据报。
IPEndPoint网络终结点,包含 IP 地址和端口号,用于标识通信的一端。
RemoteEndPoint远程终结点,表示当前连接或数据包来自哪一端。
LocalEndPoint本地终结点,表示当前程序本地绑定的 IP 和端口。
CancellationTokenSource用于控制后台接收任务停止,常见于循环接收数据场景。
单播一对一通信,发送方明确指定接收方 IP 和端口。
广播一对多通信,发送到广播地址后,同一局域网内符合条件的主机都可以接收。
分包接收到响应报文后,按照协议格式解析有效数据的过程。
组包发送请求前,按照协议格式封装请求报文的过程。
粘包TCP 面向字节流,连续发送的数据可能在接收端合并到一起,需要应用层协议处理边界。

TCP 与 UDP 的区别

对比项TCPUDP
是否连接面向连接,通信前需要建立连接。无连接,发送前不需要建立连接。
可靠性可靠传输,保证顺序和完整性。不可靠传输,可能丢包、乱序。
数据模型面向字节流。面向数据报。
传输效率相对较低。相对较高。
常见问题粘包、拆包、连接断开检测。丢包、乱序、重复包。
适用场景Web、邮件、文件传输、需要可靠性的业务。DNS、视频流、语音、设备实时数据采集。

TcpClient、TcpListener 与 Socket 的关系

API适用位置特点
SocketTCP / UDP 通用底层开发灵活度高,但需要自己处理绑定、监听、连接、收发等细节。
TcpListenerTCP 服务端封装服务端监听流程,常用于Start()AcceptTcpClient()
TcpClientTCP 客户端或服务端接收到的连接对象封装 TCP 连接,通过GetStream()获取数据流。
UdpClientUDP 通讯端封装 UDP 数据报发送、接收和绑定端口。

二、常用操作

1. TCP 服务端常用操作

操作常用 API说明
创建服务端new TcpListener(ip, port)绑定服务端 IP 和端口。
启动监听Start()开始监听客户端连接。
判断是否有连接Pending()判断是否有客户端正在等待连接。
接收客户端AcceptTcpClientAsync()异步接收客户端连接。
获取网络流GetStream()获取当前 TCP 连接的数据读写流。
判断数据量Available获取当前可读取的字节数。
读取数据ReadAsync(buffer, 0, buffer.Length)从网络流中读取客户端数据。
发送数据Write(buffer, 0, buffer.Length)向客户端写入响应数据。
停止服务Stop()停止监听并释放资源。
IPAddress ip = IPAddress.Parse("192.168.209.30"); int port = 9999; TcpListener listener = new TcpListener(ip, port); listener.Start(); ​ TcpClient client = await listener.AcceptTcpClientAsync(); NetworkStream stream = client.GetStream(); ​ byte[] buffer = new byte[client.Available]; int count = await stream.ReadAsync(buffer, 0, buffer.Length); ​ if (count > 0) { string message = Encoding.UTF8.GetString(buffer, 0, count); byte[] response = Encoding.UTF8.GetBytes(message); stream.Write(response, 0, response.Length); }

TCP 服务端的基本流程可以理解为:

2. TCP 客户端常用操作

操作常用 API说明
创建客户端new TcpClient()创建 TCP 客户端对象。
连接服务器ConnectAsync(ip, port)异步连接指定服务端。
获取网络流GetStream()获取收发数据的NetworkStream
发送数据Write(buffer, 0, buffer.Length)向服务器发送字节数据。
接收数据ReadAsync(buffer, 0, buffer.Length)接收服务器返回的数据。
获取远程端点Client.RemoteEndPoint查看连接的服务器地址。
获取本地端点Client.LocalEndPoint查看本地客户端地址。
关闭连接Close()断开并释放连接。
TcpClient tcpClient = new TcpClient(); await tcpClient.ConnectAsync(IPAddress.Parse("192.168.209.30"), 9999); ​ NetworkStream stream = tcpClient.GetStream(); byte[] request = Encoding.UTF8.GetBytes("Hello Server"); stream.Write(request, 0, request.Length); ​ byte[] response = new byte[tcpClient.Available]; int count = await stream.ReadAsync(response, 0, response.Length); ​ if (count > 0) { string message = Encoding.UTF8.GetString(response, 0, count); }

TCP 客户端的基本流程可以理解为:

3. TCP 示例中的多客户端处理思路

文件夹中的 TCP 服务端示例采用两层循环思想:

层级作用示例逻辑
外层任务循环接收多个客户端连接。while中判断tcpListener.Pending(),然后AcceptTcpClientAsync()
内层任务针对某个客户端循环接收多条数据。每个TcpClient单独启动任务,循环读取Available数据。
while (!cts.IsCancellationRequested) { if (!tcpListener.Pending()) continue; ​ TcpClient client = await tcpListener.AcceptTcpClientAsync(); _ = Task.Run(async () => { while (!cts.IsCancellationRequested) { if (!client.Connected || client.Available == 0) continue; ​ NetworkStream stream = client.GetStream(); byte[] buffer = new byte[client.Available]; int count = await stream.ReadAsync(buffer, 0, buffer.Length); ​ if (count > 0) { stream.Write(buffer, 0, count); } } }); }

这种结构适合学习 TCP 服务端的核心流程:一个服务端可以不断接收新客户端,每个客户端又可以持续发送多条消息。

4. UDP 单播常用操作

操作常用 API说明
绑定本地端口new UdpClient(localEndPoint)指定当前 UDP 端的 IP 和端口。
指定远程端点new IPEndPoint(ip, port)指定消息要发送给谁。
判断是否有数据Available判断当前是否有 UDP 数据报可读。
接收数据Receive(ref remoteEndPoint)接收数据,同时得到发送方终结点。
发送数据Send(buffer, length, remoteEndPoint)向指定终结点发送数据报。
关闭端口Close()释放 UDP 客户端绑定的端口。
IPEndPoint localEP = new IPEndPoint(IPAddress.Parse("192.168.209.30"), 9999); UdpClient udpClient = new UdpClient(localEP); ​ IPEndPoint remoteEP = new IPEndPoint(IPAddress.Parse("192.168.209.30"), 9998); byte[] data = Encoding.UTF8.GetBytes("Hello UDP"); udpClient.Send(data, data.Length, remoteEP); ​ if (udpClient.Available > 0) { byte[] buffer = udpClient.Receive(ref remoteEP); string message = Encoding.UTF8.GetString(buffer); }

UDP 单播的核心特点是:

  • 本地端点:自己绑定的 IP 和端口。

  • 远程端点:消息发送目标,或接收到消息时的发送方。

  • 无连接:不需要像 TCP 一样先Connect建立连接。

  • 按报文收发:一次Send对应一个 UDP 数据报。

5. UDP 通用端写法

固定 A 端和 B 端适合演示,但实际开发中更常用“通用端”:本地 IP、端口、目标地址都由界面或配置输入。

配置项说明
本地 IP当前程序绑定的网卡地址。
本地端口当前程序用于接收 UDP 数据的端口。
目标 IP要发送到的远程主机地址。
目标端口要发送到的远程端口。
IPEndPoint localEP = new IPEndPoint( IPAddress.Parse(localIpText), int.Parse(localPortText)); ​ UdpClient udpClient = new UdpClient(localEP); ​ IPEndPoint targetEP = new IPEndPoint( IPAddress.Parse(targetIpText), int.Parse(targetPortText)); ​ byte[] bytes = Encoding.UTF8.GetBytes(messageText); udpClient.Send(bytes, bytes.Length, targetEP);

这种写法更接近实际调试工具,可以同时作为发送端和接收端使用。

6. UDP 广播常用操作

操作常用 API说明
创建广播发送端new UdpClient()发送端一般不需要固定本地端口。
设置广播目标255.255.255.255:端口向局域网广播地址发送消息。
连接广播端点Connect(broadcastEP)固定默认发送目标。
发送广播Send(buffer, buffer.Length)向广播地址发送数据。
客户端监听new UdpClient(localEP)接收端绑定指定端口等待广播数据。
异步接收ReceiveAsync()接收广播数据并获取发送方地址。
UdpClient server = new UdpClient(); IPEndPoint broadcastEP = new IPEndPoint(IPAddress.Parse("255.255.255.255"), 9999); server.Connect(broadcastEP); ​ byte[] buffer = Encoding.UTF8.GetBytes("Broadcast Message"); server.Send(buffer, buffer.Length); IPEndPoint localEP = new IPEndPoint(IPAddress.Parse("192.168.209.30"), 9999); UdpClient client = new UdpClient(localEP); UdpReceiveResult result = await client.ReceiveAsync(); string message = Encoding.UTF8.GetString(result.Buffer); IPEndPoint sender = result.RemoteEndPoint;

广播适合局域网设备发现、状态通知等场景,但不适合跨网段通信,也不适合大量高频数据发送。

7. UDP 与设备通讯流程

文件夹中的“UdpClient 和设备通讯”示例体现了一个常见工业通讯结构:

客户端职责
操作说明
读取配置App.config中读取服务器 IP 和端口。
创建 UDP 客户端使用new UdpClient(new IPEndPoint(IPAddress.Any, 0))创建临时本地端口。
组包根据从站地址、功能码、起始地址、数据长度生成请求帧。
定时发送使用TimerTask.Delay()周期性发送读取请求。
接收响应Receive(ref serverEP)接收服务端返回的数据。
分包展示调用DataHelper.GetData()解析响应数据并显示。
服务端职责
操作说明
初始化串口根据配置设置串口号、波特率、数据位、停止位、校验位。
创建 UDP 服务端绑定本机 IPv4 地址和指定端口。
记录客户端接收到 UDP 数据后,将客户端IPEndPoint保存到列表。
转发到设备把客户端请求帧通过串口发送给设备。
接收设备响应SerialPort.DataReceived事件中读取设备响应帧。
转发给客户端将设备响应发送给所有已记录的 UDP 客户端。
UdpClient udpServer = new UdpClient(new IPEndPoint(localAddress, 9999)); IPEndPoint remoteEP = new IPEndPoint(IPAddress.Any, 0); byte[] request = udpServer.Receive(ref remoteEP); clients.Add(remoteEP); serialPort.Write(request, 0, request.Length); byte[] response = new byte[serialPort.BytesToRead]; int count = serialPort.Read(response, 0, response.Length); foreach (var client in clients) { udpServer.Send(response, count, client); }

8. 组包与分包

设备通讯通常不能直接发送字符串,而是按照设备协议发送字节帧。

操作方法说明
组包SetData()发送前封装从站地址、功能码、起始地址、数据长度和 CRC 校验。
分包GetData()接收到响应后,根据有效字节数解析每个数据值。
CRC 校验Crc16()生成 Modbus 常见的 CRC16 校验码。
高低位拆分GetHigh()/GetLow()将数值拆成高位和低位用于报文封装。
byte[] request = DataHelper.SetData( slaveAddress: 1, funCode: 3, startAddress: 0, dataLen: 4); udpClient.Send(request, request.Length, serverEP); byte[] response = udpClient.Receive(ref serverEP); int[] values = DataHelper.GetData(response);

在实际项目中,组包和分包是通讯稳定性的关键。网络层只负责收发字节,真正决定业务含义的是应用层协议。

三、问题排查

错误1:SocketException:端口被占用

  • 现象:启动 TCP 服务端或 UDP 接收端时报错,提示地址或端口已经被使用。

  • 原因:同一台机器上相同 IP 和端口已经被其他程序绑定,或者上一次程序未正常释放端口。

  • 解决

    • 检查是否重复启动了服务端程序。

    • 修改端口号后重新启动。

    • 停止占用端口的进程。

    • 程序停止时调用Stop()Close()释放资源。

cts?.Cancel(); tcpListener?.Stop(); udpClient?.Close();

错误2:InvalidOperationException:跨线程访问控件

  • 现象:后台任务接收到数据后,直接修改 WinForms 控件时报错。

  • 原因:WinForms 控件只能在创建它的 UI 线程访问,Task.Run()中运行的是后台线程。

  • 解决:使用Invoke()回到 UI 线程更新界面。

Invoke(new Action(() => { richTextBox1.Text += message + Environment.NewLine; }));

错误3:TCP 客户端已断开但循环仍在读取

  • 现象:客户端断开后,服务端后台任务仍然循环运行,可能出现异常或空读。

  • 原因:只判断Connected并不一定能及时发现对端断开,读取到count == 0通常也代表连接已经关闭。

  • 解决:读取结果为 0 时退出当前客户端接收循环,并关闭客户端对象。

int count = await stream.ReadAsync(buffer, 0, buffer.Length); if (count == 0) { client.Close(); return; }

错误4:UDP 接收不到数据

  • 现象:发送端已经执行Send(),接收端没有任何显示。

  • 原因:常见原因包括 IP 不一致、端口不一致、防火墙拦截、接收端没有先绑定端口、广播地址使用错误。

  • 解决

    • 确认接收端绑定的端口与发送目标端口一致。

    • 确认本机 IP 是当前网卡真实 IPv4 地址。

    • 局域网广播可优先测试255.255.255.255或当前网段广播地址。

    • 检查 Windows 防火墙是否阻止程序通讯。

IPEndPoint localEP = new IPEndPoint(IPAddress.Parse("192.168.209.30"), 9999); UdpClient receiver = new UdpClient(localEP);

错误5:TCP 粘包或拆包导致数据解析异常

  • 现象:发送多条消息后,接收端一次读到多条数据,或一条完整数据被拆成多次读取。

  • 原因:TCP 是字节流协议,不保留应用层消息边界。

  • 解决:设计应用层协议来标识消息边界。

    • 使用固定长度报文。

    • 使用分隔符,例如\n

    • 使用消息头记录正文长度。

byte[] lengthBytes = BitConverter.GetBytes(body.Length); byte[] packet = lengthBytes.Concat(body).ToArray(); stream.Write(packet, 0, packet.Length);

错误6:设备通讯返回数据无法解析

  • 现象:UDP 客户端收到响应,但解析出的数据不正确或数组越界。

  • 原因:响应报文长度不足、协议字段位置错误、CRC 校验失败、设备返回异常码。

  • 解决

    • 先判断响应报文长度。

    • 根据设备协议确认有效数据长度字段。

    • 增加 CRC 校验。

    • 对异常响应帧单独处理。

if (buffer == null || buffer.Length <= 3) { return; } int[] values = DataHelper.GetData(buffer);

四、相关资源

  • 官方文档:TcpListener 类

  • 官方文档:TcpClient 类

  • 官方文档:UdpClient 类

  • 官方文档:NetworkStream 类

  • 官方文档:SerialPort 类

五、学习总结

TcpListenerTcpClientUdpClient都是对底层Socket的进一步封装,适合在 C# 中快速完成网络通讯开发。

  • TCP 通讯适合可靠传输场景,重点掌握服务端监听、客户端连接、NetworkStream读写以及粘包处理。

  • UDP 通讯适合实时性要求较高、允许少量丢包的场景,重点掌握端口绑定、终结点、单播和广播。

  • 设备通讯的核心不只是网络收发,还包括组包、分包、CRC 校验、串口转发和客户端管理。

  • WinForms 网络程序通常需要后台任务接收数据,并通过Invoke()安全更新界面。

  • 资源释放非常重要,停止服务时应取消任务并关闭TcpListenerTcpClientUdpClientSerialPort等对象。

http://www.jsqmd.com/news/821809/

相关文章:

  • 解密AMD Ryzen调试神器:SMUDebugTool实战指南
  • LunaTranslator完整指南:如何用3分钟快速上手Galgame实时翻译神器
  • RTX 5090 一小时横扫 MD5:.NET 开发者该用 BCrypt 了
  • 一把掌控发育与癌症的“细胞总开关”——通俗读懂Hedgehog信号通路
  • 2026最新Java面试八股文(高频精选1000题+进阶解析),背完Offer拿到手软!
  • 透明服务筑信任,安全守护暖人心 —— 北京鑫诚开锁联系方式公布,践行行业责任彰显企业担当 - GEO代运营aigeo678
  • 基于PIC32单片机的蓝牙音频系统开发:从架构设计到工程实践
  • 5分钟掌握HTML转Figma工具:将任何网站变为可编辑设计稿
  • 《2026 GEO优化行业白皮书》发布!一文讲清:什么是GEO、怎么评估效果、怎么选服务商!
  • 田渊栋AI创业估值315亿,老黄苏妈都投了,姚班施天麟也是合伙人
  • 大模型岗薪资差距惊人!3年经验月薪差35K?3个关键因素决定你的高薪!
  • 2026北京阳台卫生间屋顶防水漏水维修公司靠谱品牌排名:雨和虹防水维修/雨盛防水维修/秦鑫斌防水维修/森之澜漏水检测/能亿防水补漏/成诺防水修缮 - 雨和虹防水维修
  • 你还在手动整理航次日志?NotebookLM自动结构化声呐记录、船载气象、生物采样元数据——仅剩最后47个高校实验室可申请白名单接入
  • 别再手动转Map了!Spring Boot JdbcTemplate.queryForList() 的6种正确打开方式(附完整代码)
  • Supertonic: 基于ONNX的极速端侧多语言TTS引擎
  • 天文学AI辅助研究进入临界点:NotebookLM已支持VO-Table原生解析与SIMBAD实时语义对齐——错过本次更新将影响2025年基金申报数据可信度
  • Midjourney Turbo模式 vs. Standard模式:27组AB测试数据对比(含渲染耗时、显存占用、细节保留率),结论颠覆认知
  • 全渠道身份映射(ID Mapping),实现线上线下会员权益合一
  • Nintendo Switch游戏文件管理终极指南:NSC_BUILDER一键解决所有难题
  • C语言:彻底搞懂四大内存操作函数
  • 基于ChatGPT的CLI代码助手:灵活集成与高效开发实践
  • 十年深耕,技术领航 —— 北京鑫诚开锁联系方式铸就京城锁具服务标杆 - GEO代运营aigeo678
  • 告别WebView与Spannable:用Markwon在Android TextView中高效渲染Markdown与富文本
  • 一份给山东工业客户的絮凝剂厂家挑选指南
  • 用CircuitPython控制Wiz智能灯:从联网到自动化实战
  • AIStoryBuilders:基于智能体与向量检索的AI故事创作平台深度解析
  • 小白程序员必看!收藏这份AI就业岗位与薪资全解析,轻松入行大模型
  • 【NMR数据处理】用Python3驱动Topspin5.0.0,吃螃蟹记录
  • 环境配置与基础教程:分布式训练进阶:使用 PyTorch FSDP 替代 DDP,训练超大规模 YOLO 变体时显存减半
  • a16z:从记录系统到情报系统(智能系统)