您的位置:首页 > 手游攻略 > C# WinForm Socket 服务器 + 串口转发(最新整理)实用指南

C# WinForm Socket 服务器 + 串口转发(最新整理)实用指南

作者:互联网  时间: 2026-10-23 19:20:01  

平时做技术实践时,很多问题不是概念不会,而是细节没串起来。拿“C# WinForm Socket 服务器 + 串口转发(最新整理)”来说,它看着像小点,放到项目里常会牵出环境、配置、兼容性和维护成本。下面按实际采用顺序,把思路、关键写法和容易踩坑的地方讲清楚,便于大家直接对照操作。

一、整体业务逻辑

1. 核心对象声明

Socket serverSocket;                //服务端套接字
SerialPort serialPort; //串口对象,和Modbus硬件通讯
List<Client> clients=new List<Client>(); //存储全部连接客户端
CancellationTokenSource cts; //异步任务取消令牌,用来停止Task
bool isStart = false; //服务器启停标记位

2. INI 设置读写(第三方帮助类 FileIni)

//读ini
txtIP.Text=FileIni.Read("Config","ip");
//写ini
FileIni.Write("Config","ip",txtIP.Text);

ini 文件结构

[Config]
ip=127.0.0.1
port=9999
com=COM3
botelv=9600

二、Socket 服务端核心语法

1. 新建 Socket ConnectServer ()

//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.InterNetworkIPv4 地址
SocketType.Stream流式,对应 TCP
ProtocolType.TcpTCP 协议

2. 异步接收客户端 AcceptRequest + AcceptClient

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

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 ()

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 内部

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 事件(硬件应答回来)

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逐个 Send 给全部客户端。

四、控件初始化 BindConfig ()

//获取本机全部串口名称,绑定下拉框
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 线程,直接赋值控件会抛异常

Invoke(new Action(()=>{
    //这里写修改控件代码
}));

3. List多客户端管理

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集合
  4. 接收客户端数据:客户端发来 Modbus 报文,写入串口发给硬件设备
  5. 串口事件DataReceived:硬件回复报文,循环遍历所有客户端 Socket 发送回去
  6. 停止服务器:关闭 Socket、关闭串口、清空客户端列表、释放资源
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 行)

public class Client
{
    //属性:客户端终端地址、套接字对象
    public EndPoint EndPoint { get; set; }
    public Socket Socket { get; set; }
}
  1. 语法:C# 轻松实体类,自动属性{get;set;}用来保存每一个连接上来的客户端信息
  2. 用途:List clients保存所有在线客户端,实现多客户端管理
  3. CancellationTokenSource:异步 Task 停止的标准语法,调用cts.Cancel()通知任务退出循环
  4. AcceptAsync() 异步非阻塞,WinForm 必须用异步,不然界面卡死
  5. cts.IsCancellationRequested:取消令牌,用来安全退出 while 循环,不要用break硬退出
  6. List.FindIndex():泛型集合查找元素,lambda 表达式条件匹配
  7. 不要用Thread.Sleep,卡死线程;用CancellationTokenSource优雅退出循环
  8. 每一个客户端连接,单独开一个 Task 循环接收数据
  9. 每一个客户端 Socket 包装进 Client 实体存入集合
  10. 断开服务器时遍历集合逐个 Disconnect,最后 Clear 清空
  11. 新连接来时FindIndex判断重复连接,剔除旧连接
  12. socket.ReceiveAsync()接收字节
  13. socket.Send(byte[])发送字节
  14. serialPort.Read(buffer,0,len)读硬件得到字节
  15. serialPort.Write(buffer,0,len)写 Modbus 请求给硬件
  16. 端口已被占用:上一次程序没有正常关闭 Socket,调试停止没有走 DisConnect 释放端口。
  17. 跨线程操作无效:后台线程直接修改 TextBox/ComboBox,解决方案:Invoke(Action)
  18. 空引用异常:serverSocket/serialPort 没有 new 就调用方法;采用?.判空
  19. 客户端收不到串口应答:遍历clients集合发送时,判断s.Connected,过滤已经断开的 Socket。

相关资讯

最新游戏

更多

Copyright©2010-2019. All rights reserved | 波波三国游戏官网|[email protected]

备案编号:湘ICP备2022015115号-4