一、整体业务逻辑
2. 核心对象声明
cs
Socket serverSocket; //服务端监听套接字
SerialPort serialPort; //串口对象,和Modbus硬件通讯
List<Client> clients=new List<Client>(); //存储全部连接客户端
CancellationTokenSource cts; //异步任务取消令牌,用来停止Task
bool isStart = false; //服务器启停标记位
3. INI 配置读写(第三方帮助类 FileIni)
cs
//读ini
txtIP.Text=FileIni.Read("Config","ip");
//写ini
FileIni.Write("Config","ip",txtIP.Text);
ini 文件结构
cs
[Config]
ip=127.0.0.1
port=9999
com=COM3
botelv=9600
三、Socket 服务端核心语法
1. 创建监听 Socket ConnectServer ()
cs
//1 创建TCP流式Socket
serverSocket=new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);
//2 IP+端口终结点
IPEndPoint endPoint=new IPEndPoint(IPAddress.Parse(txtIP.Text),int.Parse(txtPort.Text));
//3 绑定
serverSocket.Bind(endPoint);
//4 监听,挂起连接队列最大100
serverSocket.Listen(100);
| 参数 | 含义 |
|---|---|
AddressFamily.InterNetwork |
IPv4 地址 |
SocketType.Stream |
流式,对应 TCP |
ProtocolType.Tcp |
TCP 协议 |
2. 异步接收客户端 AcceptRequest + AcceptClient
cs
cts=new CancellationTokenSource();
//启动异步Task
Task task=new Task(AcceptClient,cts.Token);
task.Start();
//异步循环接收客户端
public async void AcceptClient()
{
while(!cts.IsCancellationRequested)
{
//异步等待客户端连接,不会卡死UI
Socket 客户端=await serverSocket.AcceptAsync();
//去重:如果该终端已经存在,移除旧的
int index=clients.FindIndex(c=>c.EndPoint==客户端.RemoteEndPoint);
if(index!=-1) clients.RemoveAt(index);
//加入客户端集合
clients.Add(new Client(){
EndPoint=客户端.RemoteEndPoint,
Socket=客户端
});
//开启循环接收该客户端发来的数据
ReceiveMessage(客户端);
}
}
✨重难点
3. 接收客户端数据 ReceiveMessage
cs
public void ReceiveMessage(Socket c1)
{
cts=new CancellationTokenSource();
Task task=new Task(async ()=>
{
while(!cts.IsCancellationRequested)
{
byte[] buffer=new byte[1024];
//Available:获取缓冲区现有字节数量
int count=c1.Available;
if(count>0)
{
//接收字节到buffer数组
int len=await c1.ReceiveAsync(new ArraySegment<byte>(buffer),SocketFlags.None);
//把收到的字节写入串口,发给硬件Modbus设备
serialPort.Write(buffer,0,len);
//🔴跨线程访问UI控件!必须Invoke
Invoke(new Action(()=>{
txtReceiveByte.Text=xxx;
}));
}
}
},cts.Token);
task.Start();
}
⚠️高频考点:跨线程 UI 报错 Task / 后台线程不能直接修改 WinForm 控件,必须
Invoke(Action 委托)切回 UI 线程更新文本框。
4. 关闭服务器 DisConnect ()
cs
cts.Cancel(); //通知所有异步Task停止
//遍历全部客户端Socket,断开连接
foreach(var item in clients)
{
Socket s1=item.Socket;
if(s1!=null&&s1.Connected)
{
s1.Disconnect(false);
}
}
clients.Clear();
//关闭串口
if(serialPort!=null&&serialPort.IsOpen)
serialPort.Close();
serverSocket?.Close();
isStart=false;
语法点:?.空条件运算符,对象为 null 不会调用方法,防止空引用报错。
四、SerialPort 串口核心语法
打开串口 ConnectServer 内部
cs
serialPort=new SerialPort();
serialPort.BaudRate=int.Parse(cbbBaudRate.SelectedItem.ToString());
serialPort.PortName=cbbPortNames.SelectedItem.ToString();
serialPort.DataBits=8;
serialPort.StopBits=StopBits.One;
serialPort.Parity=Parity.None;
serialPort.Open(); //打开串口
//绑定串口数据到达事件:硬件返回数据触发
serialPort.DataReceived+=SerialPort_DataReceived;
DataReceived 事件(硬件应答回来)
cs
private void SerialPort_DataReceived(object sender,SerialDataReceivedEventArgs e)
{
//读取串口缓冲区全部字节
byte[] buffer=new byte[serialPort.BytesToRead];
int count=serialPort.Read(buffer,0,buffer.Length);
if(count==0) return;
//【核心】把串口收到的硬件应答,转发给**每一个Socket客户端**
foreach(var item in clients)
{
Socket s=item.Socket;
if(s!=null&&s.Connected)
{
s.Send(buffer); //Socket发送字节数组
}
}
//更新UI,依然要Invoke跨线程
Invoke(new Action(()=>{
txtSendByte.Text=...;
}));
}
业务流程闭环:客户端 Socket→服务端→串口→硬件;硬件应答→串口事件→遍历
List<Client>逐个 Send 给全部客户端。
五、控件初始化 BindConfig ()
cs
//获取本机全部串口名称,绑定下拉框
cbbPortNames.DataSource=SerialPort.GetPortNames();
//波特率下拉框添加选项
cbbBaudRate.Items.Add(1200);
//获取本机IPv4地址
IPAddress[] address=Dns.GetHostAddresses(Dns.GetHostName());
foreach(IPAddress ip in address)
{
if(ip.AddressFamily==AddressFamily.InterNetwork)
{
txtIP.Text=ip.ToString();
}
}
Dns.GetHostAddresses(Dns.GetHostName()):获取本机所有 IP,筛选InterNetwork就是 IPv4。
六、重难点汇总
1. 异步 Task + CancellationTokenSource
2. 跨线程访问 UI(必考题)
后台 Task、SerialPort 的 DataReceived 事件都不是 UI 线程,直接赋值控件会抛异常
cs
Invoke(new Action(()=>{
//这里写修改控件代码
}));
3. List<Client>多客户端管理
5. 启停标记 bool isStart
按钮点击:if(!isStart)启动服务,else 停止服务,防止重复多次启动服务器,报端口占用异常。
6. 资源释放
关闭时:Socket.Disconnect → Close;SerialPort.Close;cts.Cancel ();集合 Clear,避免端口被占用、内存泄漏。
七、常见报错
1. Socket 和 SerialPort 字节操作
全部都是byte \[\] 字节数组,Modbus RTU 传输原始二进制字节,不是字符串,不能随便 Encoding 转换!
-
程序加载:读取
config.ini配置文件,初始化 IP、端口、串口参数 -
启动服务器:创建 TCP Socket 服务端 + 打开 SerialPort 串口
-
异步 Accept:持续接收多个客户端连接,存入
List<Client>集合 -
接收客户端数据:客户端发来 Modbus 报文,写入串口发给硬件设备
-
串口事件
DataReceived:硬件回复报文,循环遍历所有客户端 Socket 发送回去 -
停止服务器:关闭 Socket、关闭串口、清空客户端列表、释放资源
csusing System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.IO; using System.IO.Ports; using System.Linq; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; using 服务器.Helpers; namespace Socket连接设备服务器 { public partial class Form1 : Form { //1.客户端发送请求帧 01 03 00 00 00 04 44 09 //2.服务器接收请求帧之后 再把这个请求帧发给串口设备 (服务器和串口设备通过串口连接) //3.串口设备拿到请求帧,返回响应帧,把响应帧发给服务器 //4.服务器再把响应帧转发给每一个客户端 //服务器的作用就是转发客户端的请求帧,转发设备的响应帧 public Form1() { InitializeComponent(); } private void Form1_Load(object sender, EventArgs e) { BindConfig(); //加载本地INI文件 if (File.Exists("config.ini")) { //加载本地文件 txtIP.Text= FileIni.Read("Config","ip"); txtPort.Text = FileIni.Read("Config", "port"); cbbPortNames.Text= FileIni.Read("Config", "com"); cbbBaudRate.Text = FileIni.Read("Config", "botelv"); } //else //{ // BindConfig();//不存在 初始化配置 //} } /// <summary> /// 初始化配置 /// </summary> public void BindConfig() { //初始化地址 IPAddress[] address=Dns.GetHostAddresses(Dns.GetHostName());//根据主机名获取地址列表 foreach (IPAddress ip in address) { if (ip.AddressFamily==AddressFamily.InterNetwork)// 如果ip是ipv4 显示在输入框上 { txtIP.Text = ip.ToString(); } } //初始化端口 txtPort.Text=9999.ToString(); //初始化串口 cbbPortNames.DataSource=SerialPort.GetPortNames(); cbbPortNames.SelectedIndex=0; int botelv = 1200; cbbBaudRate.Items.Add(botelv); for (int i = 1; i < 5; i++)//创建多个波特率 { if (i!=3) { cbbBaudRate.Items.Add(botelv * i * 2); } } cbbBaudRate.SelectedIndex = 3; // cbbDataBits.Items.Add(8); cbbDataBits.SelectedIndex = 0; cbbStopBits.Items.Add("One"); cbbStopBits.SelectedIndex = 0; cbbParity.Items.Add("None"); cbbParity.SelectedIndex = 0; } private void btnSave_Click(object sender, EventArgs e) { FileIni.Write("Config","ip",txtIP.Text);//存储IP到ini文件中 FileIni.Write("Config", "port", txtPort.Text); FileIni.Write("Config", "com", cbbPortNames.SelectedItem.ToString()); FileIni.Write("Config", "botelv", cbbBaudRate.SelectedItem.ToString()); } bool isStart = false; /// <summary> /// 启动服务器 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btnStart_Click(object sender, EventArgs e) { try { if (!isStart) { //启动服务器 ConnectServer(); //接受请求 AcceptRequest(); } else { //关闭服务器 DisConnect(); } } catch (Exception) { throw; } } CancellationTokenSource cts; public void DisConnect() { cts?.Cancel(); foreach (var item in clients) { Socket s1 = item.Socket; if (s1!=null&&s1.Connected) { s1.Disconnect(false);//不重复使用套接字 } } clients.Clear(); if (serialPort!=null&&serialPort.IsOpen) { serialPort.Close(); } serverSocket?.Close(); btnStart.Text = "启动"; isStart = false; txtIP.Enabled = txtPort.Enabled = cbbBaudRate.Enabled = cbbDataBits.Enabled = cbbParity.Enabled = cbbPortNames.Enabled = cbbStopBits.Enabled = true; } public void AcceptRequest() { cts= new CancellationTokenSource(); Task task = new Task(AcceptClient, cts.Token); task.Start(); } /// <summary> /// TASK的异步方法 /// </summary> List<Client> clients =new List<Client>(); //存放客户端对象的集合 public async void AcceptClient() { while (!cts.IsCancellationRequested) { //接受客户端对象 try { Socket 客户端 = await serverSocket.AcceptAsync();//异步等待接收客户端连接 //转发给每一个客户端,把客户端对象存起来 int index = clients.FindIndex(c => c.EndPoint == 客户端.RemoteEndPoint); if (index != -1)// 找到了重复的终端 { clients.RemoveAt(index);//移除一个 } //添加客户端对象 clients.Add(new Client() { EndPoint = 客户端.RemoteEndPoint, Socket = 客户端 }); //接收客户端传递消息 ReceiveMessage(客户端); } catch (Exception) { Console.WriteLine("ss"); } } } /// <summary> /// 接受客户端发来的消息 01 03 00 00 00 04 44 09 /// </summary> public void ReceiveMessage(Socket c1) { cts=new CancellationTokenSource(); Task task = new Task(async() => { while (!cts.IsCancellationRequested) { try { byte[] buffer = new byte[c1.Available]; //接收数据的方法 异步方法 ,参数1数组的一个结构体类型 参数2socket套接字标识 ,none 不加任何标识 int count = await c1.ReceiveAsync(new ArraySegment<byte>(buffer), SocketFlags.None); if (count > 0) { panel2.BackColor = Color.Yellow; await Task.Delay(50);//延迟50ms 把颜色变成灰色的 panel2.BackColor = Color.Gray; await Task.Delay(50);// panel2.BackColor = Color.Green; //byte[] b1 = HexStringToBytes(Encoding.UTF8.GetString(buffer)); //serialPort.Write(b1, 0, b1.Length); //serailport执行请求帧 serialPort.Write(buffer, 0, buffer.Length); panel1.BackColor = Color.Yellow; await Task.Delay(50);//延迟50ms 把颜色变成灰色的 panel1.BackColor = Color.Gray; await Task.Delay(50);// panel1.BackColor = Color.Green; //更新UI Invoke(new Action(() => { int oldCount = int.Parse(txtReceiveByte.Text); txtReceiveByte.Text = (oldCount + count).ToString(); })); } } catch (Exception) { Console.WriteLine(""); } } }, cts.Token); task.Start(); } /// <summary> /// 启动服务器 /// </summary> Socket serverSocket; SerialPort serialPort; public void ConnectServer() { //1.启动服务 serverSocket=new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); serverSocket.Bind(new IPEndPoint(IPAddress.Parse(txtIP.Text), int.Parse(txtPort.Text))); serverSocket.Listen(100); //2.网口指示灯变亮 panel2.BackColor = Color.Green; //3.开启串口 serialPort = new SerialPort(); serialPort.BaudRate = int.Parse(cbbBaudRate.SelectedItem.ToString()); serialPort.PortName = cbbPortNames.SelectedItem.ToString(); serialPort.DataBits=int.Parse(cbbDataBits.SelectedItem.ToString()); serialPort.Parity=(Parity)Enum.Parse(typeof(Parity),cbbParity.SelectedItem.ToString()); serialPort.StopBits = (StopBits)Enum.Parse(typeof(StopBits), cbbStopBits.SelectedItem.ToString()); if (!serialPort.IsOpen) serialPort.Open(); serialPort.DataReceived += SerialPort_DataReceived; panel1.BackColor = Color.Green; //4.按钮的标题修改 btnStart.Text = "停止"; isStart = true; txtIP.Enabled=txtPort.Enabled=cbbBaudRate.Enabled=cbbDataBits.Enabled=cbbParity.Enabled=cbbPortNames.Enabled=cbbStopBits.Enabled=false; } /// <summary> /// 十六进制字符串 → byte数组 /// 例:输入"010300000002840A",输出对应的Modbus RTU字节数组 /// </summary> //public static byte[] HexStringToBytes(string hexStr) //{ // //清除所有空格 // string cleanHex = hexStr.Replace(" ", ""); // if (cleanHex.Length % 2 != 0) // throw new Exception("十六进制字符长度非法!"); // byte[] buf = new byte[cleanHex.Length / 2]; // for (int i = 0; i < buf.Length; i++) // { // string sub = cleanHex.Substring(i * 2, 2); // buf[i] = Convert.ToByte(sub, 16); // } // return buf; //} private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e) { //拿到串口响应帧 通过服务器发给每个客户端 byte[] buffer=new byte[serialPort.BytesToRead]; int count=serialPort.Read(buffer,0,buffer.Length); if (count==0) { return; } //把数据转发给每一个客户端 foreach (var item in clients) { Socket s = item.Socket; if (s!=null&&s.Connected) { s.Send(buffer); } } Invoke(new Action(() => { txtSendByte.Text=(int.Parse(txtSendByte.Text) + count).ToString(); })); } } public class Client { public EndPoint EndPoint { get; set; }//客户端终端 public Socket Socket { get; set; }//客户端对象 } }二、关键类定义语法
1. Client 自定义类(324‑329 行)
cspublic class Client { //属性:客户端终端地址、套接字对象 public EndPoint EndPoint { get; set; } public Socket Socket { get; set; } } -
语法 :C# 简单实体类,自动属性
{get;set;}用来保存每一个连接上来的客户端信息 -
用途:
List<Client> clients保存所有在线客户端,实现多客户端管理 -
CancellationTokenSource:异步 Task 停止的标准语法,调用cts.Cancel()通知任务退出循环 -
AcceptAsync()异步非阻塞,WinForm 必须用异步,不然界面卡死 -
cts.IsCancellationRequested:取消令牌,用来安全退出 while 循环,不要用break硬退出 -
List.FindIndex():泛型集合查找元素,lambda 表达式条件匹配 -
❌不要用
Thread.Sleep,卡死线程;✅用CancellationTokenSource优雅退出循环 -
每一个客户端连接,单独开一个 Task 循环接收数据
-
每一个客户端 Socket 包装进 Client 实体存入集合
-
断开服务器时遍历集合逐个 Disconnect,最后 Clear 清空
-
新连接来时
FindIndex判断重复连接,剔除旧连接 -
socket.ReceiveAsync()接收字节 -
socket.Send(byte[])发送字节 -
serialPort.Read(buffer,0,len)读硬件返回字节 -
serialPort.Write(buffer,0,len)写 Modbus 请求给硬件 -
端口已被占用:上一次程序没有正常关闭 Socket,调试停止没有走 DisConnect 释放端口。
-
跨线程操作无效 :后台线程直接修改 TextBox/ComboBox,解决方案:
Invoke(Action) -
空引用异常 :serverSocket/serialPort 没有 new 就调用方法;使用
?.判空 -
客户端收不到串口应答 :遍历
clients集合发送时,判断s.Connected,过滤已经断开的 Socket。