C# WinForm Socket 服务器 + 串口转发(最新整理)实用指南
平时做技术实践时,很多问题不是概念不会,而是细节没串起来。拿“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.InterNetwork | IPv4 地址 |
SocketType.Stream | 流式,对应 TCP |
ProtocolType.Tcp | TCP 协议 |
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 转换!
- 程序加载:读取
config.ini设置文件,初始化 IP、端口、串口参数 - 启动服务器:新建 TCP Socket 服务端 + 打开 SerialPort 串口
- 异步 Accept:持续接收多个客户端连接,存入
List集合 - 接收客户端数据:客户端发来 Modbus 报文,写入串口发给硬件设备
- 串口事件
DataReceived:硬件回复报文,循环遍历所有客户端 Socket 发送回去 - 停止服务器:关闭 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; }
}
- 语法:C# 轻松实体类,自动属性
{get;set;}用来保存每一个连接上来的客户端信息 - 用途:
List保存所有在线客户端,实现多客户端管理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。
游戏推荐
推荐专题
热门阅读
推荐下载
-
-
下载
- |
-
-
下载
- 《行尸走肉第一章》免安装中文汉化硬盘版下载
- 单机|436 MB
- 一款以动作冒险为主题的游戏
-
-
下载
- 《街头霸王X铁拳》免安装中文汉化硬盘版下载
- 单机|111MB
- 一款非常好玩的格斗游戏
-
-
下载
- |
-
-
下载
- 《暗黑破坏神3》免安装繁体中文正式版下载
- 单机|7630 MB
- 一款以角色扮演为主题的游戏
-
-
下载
- 《马克思佩恩3》免安装硬盘版下载
- 单机|27033 MB
- 一款以第三人称射击为主题的游戏