C# WinForm Socket 服务器 + 串口转发

一、整体业务逻辑

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 转换!

  1. 程序加载:读取config.ini配置文件,初始化 IP、端口、串口参数

  2. 启动服务器:创建 TCP Socket 服务端 + 打开 SerialPort 串口

  3. 异步 Accept:持续接收多个客户端连接,存入List<Client>集合

  4. 接收客户端数据:客户端发来 Modbus 报文,写入串口发给硬件设备

  5. 串口事件DataReceived:硬件回复报文,循环遍历所有客户端 Socket 发送回去

  6. 停止服务器:关闭 Socket、关闭串口、清空客户端列表、释放资源

    cs 复制代码
    using 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 行)

    cs 复制代码
    public class Client
    {
        //属性:客户端终端地址、套接字对象
        public EndPoint EndPoint { get; set; }
        public Socket Socket { get; set; }
    }
  7. 语法 :C# 简单实体类,自动属性{get;set;}用来保存每一个连接上来的客户端信息

  8. 用途:List<Client> clients保存所有在线客户端,实现多客户端管理

  9. CancellationTokenSource:异步 Task 停止的标准语法,调用cts.Cancel()通知任务退出循环

  10. AcceptAsync() 异步非阻塞,WinForm 必须用异步,不然界面卡死

  11. cts.IsCancellationRequested:取消令牌,用来安全退出 while 循环,不要用break硬退出

  12. List.FindIndex():泛型集合查找元素,lambda 表达式条件匹配

  13. ❌不要用Thread.Sleep,卡死线程;✅用CancellationTokenSource优雅退出循环

  14. 每一个客户端连接,单独开一个 Task 循环接收数据

  15. 每一个客户端 Socket 包装进 Client 实体存入集合

  16. 断开服务器时遍历集合逐个 Disconnect,最后 Clear 清空

  17. 新连接来时FindIndex判断重复连接,剔除旧连接

  18. socket.ReceiveAsync()接收字节

  19. socket.Send(byte[])发送字节

  20. serialPort.Read(buffer,0,len)读硬件返回字节

  21. serialPort.Write(buffer,0,len)写 Modbus 请求给硬件

  22. 端口已被占用:上一次程序没有正常关闭 Socket,调试停止没有走 DisConnect 释放端口。

  23. 跨线程操作无效 :后台线程直接修改 TextBox/ComboBox,解决方案:Invoke(Action)

  24. 空引用异常 :serverSocket/serialPort 没有 new 就调用方法;使用?.判空

  25. 客户端收不到串口应答 :遍历clients集合发送时,判断s.Connected,过滤已经断开的 Socket。

相关推荐
云飞云共享云桌面1 小时前
广东精密机械设备工厂8-10人共享一台SolidWorks设计服务器
运维·服务器·3d·自动化·汽车·制造
jieyucx1 小时前
【高级利用】条件竞争与逻辑漏洞:与服务器赛跑的艺术
android·运维·服务器·web安全·文件上传
云贝教育-郑老师2 小时前
OceanBase 是如何搞定海量数据分片(Sharding)的?
学习·oceanbase·dba
tianyuanwo2 小时前
服务器OS组件研发效能度量可视系统——顶层方案设计与阶段性规划
运维·服务器·度量系统
初级炼丹师(爱说实话版)2 小时前
Ubuntu服务器配置docker
服务器·ubuntu·docker
编程圈子2 小时前
电机驱动开发学习23. 保护体系与故障状态机
驱动开发·学习
雪隐2 小时前
WPF + MVVM 实战系列01-一个老狗回炉重造 WPF 的 12 天血泪史
sqlite·c#·mvvm
机建狂魔2 小时前
Codex 接入第三方模型 API 实战:以 Mimo 为例
java·服务器·数据库·ai·ai编程·codex
土星云SaturnCloud3 小时前
智慧温室精准环控:土星云边缘计算设备实现温光水肥本地闭环调节
服务器·人工智能·ai·边缘计算
别动我齐刘海3 小时前
Day6 unitree_G1人形机器人GMR—— MotionInput
c语言·c++·人工智能·学习·机器学习·机器人·github