一、整体业务逻辑
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<client>逐个 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<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、关闭串口、清空客户端列表、释放资源
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<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。
到此这篇关于c# winform socket 服务器 + 串口转发的文章就介绍到这了,更多相关c# winform socket 服务器内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论