Socket编程之聊天程序 - 模拟Fins/ModBus协议通信过程

2023-11-14

 

设备控制软件编程涉及到的基本通信方式主要有TCP/IP与串口,用到的数据通信协议有Fins与ModBus。 更高级别的通信如.net中的Remoting与WCF在进行C/S架构软件开发时会采用。


本篇文章结合Fins/ModBus协议的指令帧结构与数据编码与解码过程,自定义了一套TcpChatter数据数据通信协议,编写了一个聊天程序,说明TCP/IP的在一个项目中应用。


本文涉及到的源代码工程项目为 - TcpChatter 后面附件提供源代码下载 ( OpenSource Code   软件版本:VS2008    语言:C#)


1 先普及几个基本概念



Socket

接触C/C++的人都知道,编写网络程序会用到Socket,对于Socket编程,其基本编程思想就是使用listen,accept,connect,send与write等几个操作来实现客户端与服务端的通信。
对于使用C#的程序员,.net为我们提供了Socket类来编写服务程序,提供了TcpClient来编写客户端程序。我们只需要知道如何使用listen,accept,connect,send与write操作就能编写我们需要的网络程序了。

简单的说:
Socket是支持TCP/IP协议的网络通信的基本操作单元,它是建立在TCP/IP协议上的一组编程接口,是我们编写代码,使用TCP/IP进行数据通信的入口,它是对TCP/IP协议栈的抽像,等效于是TCP/IP协议栈提供的对外编程接口,在.net中,只是说这个编程接口的实现由Micosoft为我们完成,我们做的唯一工作只是使用这些接口就能在我们的应用程序间进行TCP/IP通信了


TCP/IP - Transmission Control Protocol/Internet Protocol

传输控制协议/因特网互联协议,是Internet互联网络的基础,由传输层的TCP协议与网络层的IP协议构成。网络层负责在节点与节点之间传送数据包(IP数据包),该IP数据包由TCP协议来进行组装,IP数据包再通过它的下层协议以太网协议 (IEEE802)在光纤上进行传输,从而将不同的信息从一台计算机传送到了另一台计算机。

对于程序员,我们编写的程序要在不同的计算机间进行数据通信,可以通过Socket编程来使用TCP/IP,从而将我们的数据从一台计算机传到了另一台计算机。


PLC - Programmable Logic Controller
可编程逻辑控制器,一种数字运算操作的电子系统,专为工业环境应用而设计,与计算机一样,可以把它看成是一种用于工业控制的计算机,它也有自己的编程语言 - T形图,可以通过T形图编程来实现各种设备的控制。

Fins - Factory Interface Network Service
Fins协议是欧姆龙开发的用于工业自动化控制网络的指令/响应通信协议,它借助TCP/IP协议与串口通信协议,通过发送Fins指令实现在各种网络间的无缝通信,这里主工是PC与PLC的通信,我们PC可以通过发送Fins指令与PLC进行通信。

ModBus
Modbus是由Modicon公司于1979年发明,是全球第一个真正用于工业现场的总线协议。在工业控制系统中,目前ModBus已经成为一通用工业标准.
Modbus主要应用于电子控制器上的一种通用数据协议,借助TCP/IP协议与串口通信协议,通过发送Modbus指令实现在各种设备之间的通信。目前公司的温控表与PC间的通信采用ModBus。


2 TcpChatter消息传输结构



2.1 TcpChatter软件架构

与传统的软件系统一样,TcpChatter采用C/S架构,即客户端/服务端架构,Client参与通信会话,Server不参与通信会话,只负责将Client的消息通过Server进行转发,从而实现Client-Client间的通信。
整个TcpChatter的代码结构由 ChatServer + ChatClient构成

2.2 TcpChatter消息处理

如下图所示TcpChatterMessageTransaction,为TcpChatter系统中使用的消息处理结构。下图异展示了从 客户端A←→客户端B 的消息传递过程。

2.3 TcpChatter消息处理原理

在不同的客户端进行通信,客户端通过将消息封装为TcpChatter指定的数据格式 [TcpChater指令 + 数据]  然后发送给服务端程序ChatServer,服务端程序再将消息转发给指定的客户端,客户端收到消息后解析TcpChater数据包然后做其它的处理。




3  TcpChatter指令帧结构



目前工业控制中的温控主流采用串口通信,使用数据通信协议为ModBus协议,而与底层PLC通信则多采用Fins协议。下面分别解释ModBus协议与Fins协议的指令帧结构与TcpChatter指令帧结构。


TcpChatter指令 帧用于在客户端与服务端进行统一格式的数据通信。其基本构成为 : TcpChatter指令域 + TcpChatter数据域;

Fins协议与ModBus协议原理基本一样,其各自的指令帧结构 基本有2部分构成: 指令域 + 数据域
指令域为Fins协议与ModBus协议定义的数据通信格式,指令域字节长度也不一样.比如Fins指令有效指令域(Fin头 + Fin指令域)为12个节字,数据域长度能到2000字节,而ModBus协议有效指令域(地址 + 功能码 + CRC校验码)为4个字节,数据域为(256-4)字节或(260-4)字节。
数据域为发送的真正数据。由于受限于硬件设备通信的数据速率, 在串口与TCP/IP通信中, 指令域 + 数据域的总长度是有限制的。我们通过PC与设备进行通信,实际上是在反复的发送这些 数据包与解析这些数据包,从而达到PC与设备信息交互的目的。

熟悉嵌入式的人都知道,我们编写代码跟设备进行通信,基本是在通过操作设备的寄存器对寄存器进行读写从而达到控制设备状态与获取设备状态的目的。寄存器普通的有8位与16位,我们最常见的温控表是8位寄存器,正好一个byte,每一位都可以看成是硬件上的一个I/O,我们通过操作这些位从而操作了对应的硬件的I/O状态 (0/1),设备跟据这些I/O状态做出相应的动作。



3.1 Fins指令帧结构
(
Fins响应帧(应答帧结构)结构与Fins指令帧结构类似)





3.2 ModBus指令帧结构
(ModBus也具备响应帧结构)



2.3 TcpChatter指令帧结构


TcpChatter指令 帧用于在客户端与服务端进行统一格式的数据通信。其基本构成为 : TcpChatter指令域 + TcpChatter数据域;

TcpChatter指令域构成:   命令头 + 命令请求模式+  发送者ID + 收发模式 + 收接都ID + 预留指令
TcpChatter指令域长度: 8bytes
TcpChatter数据域长度: 20kb



3.4 TcpChatter 指令域结构 (该指令在通信过程中变换成了byte,可以进行位操作)


 

/// <summary>
    /// TcpChatter 数据通信命令格式定义
    /// </summary>
    public struct LCmd
    {
        public int Head;            // 有效命令开始标志(命令头)
        public int CmdMode;         // 命令请求模式
        public int SendID;          // 发送者用户ID
        public int WR;              // 发送或读写模式
        public int RecvID;          // 接收者用户ID
        public int Resv2;           // 预留
        public int Resv3;           // 预留
        public int Resv4;           // 预留
    }




 


3.5 TcpChatter 指令集



 

 /// <summary>
    /// 应答请求命令
    /// </summary>
    public enum CmdRequest
    {
        MinID = -1,             
        Online      = 0x01,     // 在线请求
        FixUser     = 0x02,     // 向固定用户发送消息请求
        Flush       = 0x03,     // 向固定用户闪屏请求
        FlushAll    = 0x04,     // 向所有用户闪屏请求
        Broadcast   = 0x05,     // 广播消息请求
        Offline     = 0x06,     // 离线请求
        UpdateUsers = 0x07,     // 用户列表更新请求
        Success     = 0x08,     // 用户连接服务成功应答
        InvalidUser = 0x09,     // 非法用户名 - (预留)
        Failed      = 0x0A,     // 用户连接服务失败应答
        InvalidCmd  = 0xFF,     // 非法命令包 - (预留)
        MaxID,
    }



 


4  ChatServer   -  TcpChatter服务端程序



4.1 ChatAgent服务端  获取客户端独立的Socket连接请求

在TcpChatter项目中,通过TcpListener创建一个监听端口获取Socket连接请求,不同的客户端连接请求(TcpClient的Connect),服务端会创建客户端各自独立的Socket对象,在ChatAgent中通过ClientContext管理了所有连接客户端的Socket,消息的转发通过各自不同的Socket进行。


4.1.1 ClientContext

ChatServer服务端通过dicClientContext 表保存了所有连接客户端的信息,当客户端异常或离线,其客户端资源会被从Server端移除。

private Dictionary<string, ClientContext> dicClientContext = new Dictionary<string, ClientContext>()


 

#region InnerClass - Client Instance Context

        class ClientContext
        {
            internal ClientContext()
            {
            }
            internal byte[] Buf { get; set; }
            internal byte[] HeadBuf { get; set; }
            internal byte[] DataBuf { get; set; }
            internal int UserID { get; set; }
            internal string UserName { get; set; }
            internal Thread MsgHandle { get; set; }
            internal Socket Skt { get; set; }
        }
        #endregion




 

 

4.2 监听连接请求与消息监听流程图

如下图所示,ChatServer启动了一个监听端口,当有新的连接请求达到,会生成新的Socket对象,同时启动Socket服务消息监听线程:

服务监听线程:客户端连接请求线程,有新的客户端成功连接服务端时会生成新的Socket对象。该线程为所有客户端服务。

Socket服务线程:服务监听线程的子线程,用于处理服务端使用Socket转发的消息。为指定Socket的独立客户端服务。




4.3  IChatAgent服务代理接口

TcpChatter的服务端接口含2个属性与2个接口

Name : 服务器名称
IsAlive:服务器激活状态
StartChatServer: 启动服务接口
StopChatServer:关闭服务接口


 public interface IChatAgent
    {
        string Name { get;}
        bool IsAlive { get; }
        bool StartChatServer();
        bool StopChatServer();
    }




4.4 服务监听线程

/// <summary>
        /// 客户端 消息处理主线程
        /// </summary>
        private void MessageProcessThread()
        {
            ClientContext client = null;
            while (IsAlive)
            {
                try
                {
                    byte[] useNameBuf = new byte[MAXBUFSIZE];

                    // 监听连接请求对像
                    Socket msgSkt = tcpListener.AcceptSocket();

                    // 等待上线请求
                    int actualLens = msgSkt.Receive(useNameBuf);        

                    // 获取实际数据长度
                    byte[] buf = this.CopyArrayFrom(useNameBuf, actualLens);

                    byte[] header = null;
                    byte[] dataBuf = null;

                    // 解析上线请求命令包 : 上线请求 + 用户名
                    LErrorCode error = this.ResolveDataPackage(buf, out header, out dataBuf);
                    if (error != LErrorCode.Success)
                    {
                        Console.Error.WriteLine("ResolveDataPackage failed! LErrorCode = {0}", error);
                        continue;
                    }

                    // 校验命令头
                    if (header[0] != ProtocolMsg.LCML)
                    {
                        Console.Error.WriteLine("Invalid cmmand head = {0}", header[0]);
                        continue;
                    }
                    // 是否是上线请求   -  第 1 个命令必须是: 上线请求命令包 + 用户名
                    CmdRequest request = (CmdRequest)header[1];
                    if (request != CmdRequest.Online)
                    {
                        Console.Error.WriteLine("Invalid request command! Cmd = {0}", request);
                        continue;
                    }

                    // 校验用户名的合法性
                    string user = this.GetStringFrom(dataBuf);
                    if (!CheckUserInvalid(user))
                    {
                        string msg = "User name " + user + " has been existed in TcpChatter system! User tried to join chatting failed!";

                        this.currentRequest = CmdRequest.Failed;
                        this.currentRight = LProtocolRight.WR;

                        msgSkt.Send(CurrentCmd);
                        Console.Error.WriteLine(msg);
                        continue;
                    }


                    // 服务端生成用户信息 并动态分配独立用户ID
                    client = new ClientContext();

                    client.UserID = ChatAgent.ActiveID;
                    client.UserName = user;
                    client.Skt = msgSkt;
                    dicClientContext.Add(user, client);

                    this.currentRequest = CmdRequest.Success;
                    this.currentRight = LProtocolRight.WR;
                    this.senderID = client.UserID;

                    // 发送登陆成功命令
                    msgSkt.Send(CurrentCmd);   

                    string sysmsg = string.Format("[系统消息]\n新用户 {0} 在[{1}] 已成功连接服务器[当前在线人数: {2}]\r\n\r\n", 
                        user, DateTime.Now, dicClientContext.Count);
                    Console.WriteLine(SysInfo.Timestamp + sysmsg);

                    Thread.Sleep(1000);         // Sleep 1s

                    Thread handle = new Thread(() =>
                        {
                            if (PreMessageProcess(client, sysmsg))
                            {
                                // 启用用户 消息监听线程
                                SubMsgProcessThread(client, sysmsg);
                            }
                        });
                    handle.Start();

                    dicClientContext[user].MsgHandle = handle;
                }
                catch (SocketException se)
                {
                    Innerlog.Error(dcrlringType, "SocketException Current user =  " + client.UserName + " was offline!", se);
                }
                catch (Exception ex)
                {
                    Innerlog.Error(dcrlringType, "Exception Current user =  " + client.UserName + " was offline!", ex);
                }
            }
        }


 


4.5 Socket服务消息线程

 

 /// <summary>
        /// 用户消息监听线程
        /// </summary>
        /// <param name="client"></param>
        private void SubMsgProcessThread(ClientContext clientx, string message)
        {
            ClientContext client = clientx;
            while (true)
            {
                try
                {
                    byte[] msgBuf = new byte[MAXBUFSIZE];
                    // 监听 并接收数据
                    int actualLens = client.Skt.Receive(msgBuf);
                    byte[] totalBuf = this.CopyArrayFrom(msgBuf, actualLens);

                    byte[] headBuf = null;
                    byte[] dataBuf = null;

                    // 解析命令包
                    LErrorCode error = this.ResolveDataPackage(totalBuf, out headBuf, out dataBuf);

                    client.HeadBuf = headBuf;
                    client.DataBuf = dataBuf;
                    client.Buf = totalBuf;

                    if (error != LErrorCode.Success) continue;

                    // 是否是有效命令
                    if (headBuf[0] != ProtocolMsg.LCML) continue;

                    
                    CmdRequest cmdHead = (CmdRequest)headBuf[1];
                    if (cmdHead == CmdRequest.InvalidCmd ||
                        cmdHead == CmdRequest.MaxID ||
                        cmdHead == CmdRequest.MinID)
                    {
                        Console.Error.WriteLine("Invalid Send Message!");
                        continue;
                    }
                    else
                    {
                        // 用户消息转发
                        UserMessageProcess(client);
                    }
                }
                catch (Exception ex)
                {
                    ClientOfflineProcess(client);
                    //Innerlog.Error(dcrlringType, "Current user =  " + client.UserName + " was offline!", ex);
                    Thread.CurrentThread.Abort();
                }
            }
        }


 

 

4.6  序列化

序列化与反序列化在TcpChatter中被用于消息的编码与解码。编码与解码过程可以详细的参看ChatAgent代码内部实现。

序列化描述了持久化一个对像对流的过程,反序列化则与此过程相反,表示从流到对象的重建过程。在.net中,消息传递,数据存储都大量的用到了序列化与反序列化的操作。


由于客户端与服务端消息传输以byte字节流的方式进行传输,当在客户端之前传递对象时需要对对象进行序列化。如传递客户端在线列表,该列表是一个Dictionary,在客户端与服务端进行Dictionary传递需用到序列化与反序列化。

见 TcpChatter CHTCommon中的SysInfo.cs

 

public static byte[] SerializeGraph<T>(T graph)


 
4.7  反序列化
见 TcpChatter CHTCommon中的SysInfo.cs

public static T DeserializeGraph<T>(byte[] bytes)

 


4.8 ChatClient



4.9  ChatServer 服务端程序

class Program
    {
        static void Main(string[] args)
        {
            int beginner = Win32Manager.TickCounter;
            Console.WriteLine("\r\n-----------------------------------------------------------------------");
            Console.WriteLine(SysInfo.Timestamp + "ChatServer is starting........\r\n");

            IChatAgent agent = new ChatAgent(null);
            int linkCounter = 0;
            bool isStarted = agent.StartChatServer();
            while (!agent.IsAlive)
            {
                if (linkCounter++ > 10)
                {
                    Console.WriteLine(SysInfo.Timestamp + "ChatServer start failed! Try LinkCounter = {0}",linkCounter);
                    break;
                }
                Thread.Sleep(100);
            }

            Console.WriteLine(SysInfo.Timestamp + "Total ElapsedTime = {0}ms", (Win32Manager.TickCounter - beginner));
            if (linkCounter < 10) Console.WriteLine(SysInfo.Timestamp + "ChatServer is running........");

            Console.WriteLine("-----------------------------------------------------------------------\r\n");
            Application.Run();
        }
    }


 


5  TcpChatter运行测试



运行 ChatServer,如图1所示,输入端口服务启动
运行ChatClient,输入用户名就可以聊天了。当有新用户上线或新用户离线时,ChatServer控制台会显示当前用在线用户的情况。



运行ChatClient输入用户名



ChatClient聊天界面






附录:TcpChatter源代码下载        软件版本: VS2008    语言:C#




TcpChatter源代码.rar
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Socket编程之聊天程序 - 模拟Fins/ModBus协议通信过程 的相关文章

  • jar包修改编译反编译操作

    1 首先下载一个反编译工具JD GUI 自己用的是这款 2 获取到你要改的jar包文件 3 先把jar包直接解压暂时放在一个目录里 本人准备修改这个文件 4 再把jar包拖进JD GUI进行解码然后保存到另一个文件夹中 jarTest 5
  • C++ 堆内存分配 new delete 简谈

    堆内存 堆区 heap 是内存空间 是区别于栈区 全局数据区和代码区的内存区域 是程序在运行时申请的内存空间 new和delete new和delete是C 专有的操作符 不需要声明头文件 new是用来申请分配堆内存的 delete是用来释

随机推荐

  • 剖析muduo网络库核心代码,重写muduo库

    项目简介 模拟muduo库实现nonnon blocking IO multiplexing loop线程模型的高并发 TCP 服务器模型 开发环境 Centos7 技术栈 C 多线程 socket网络编程 epoll多路转接 项目设计 整
  • 某机字长为32位,存储容量为64MB,若按字节编址.它的寻址范围是多少?

    问题 1 某计算机字长为32位 其存储容量为16MB 若按双字编址 它的寻址范围是多少 2 某机字长为32位 存储容量为64MB 若按字节编址 它的寻址范围是多少 解答 我的方法是全部换算成1位2进制的基本单元来算 先计算总容量 如第一题中
  • telnet端口不通怎么解决(单边不通的方法建议)

    telnet端口不通是大家在检测端口的时候可能会遇到的问题之一 遇到这种状况一般要如何解决呢 这里为各位带来分享 看一下telnet端口不通的解决方式 看一下如何处理吧 telnet端口不通怎么解决 1 开放供应商服务器端口 总是出现由于连
  • The engine “node“ is incompatible with this module. Expected version

    前言 vue项目用了yarn yarn install后报错如下 开始 执行 yarn config set ignore engines true 然后yarn install后成功 结束 在此记录问题 如有需要修改的地方 还请不吝赐教
  • Kubernetes—K8S运维管理

    Kubernetes K8S运维管理 更新中 一 Node管理 1 1 Node的隔离与恢复 1 2 Node 的扩容 二 更新资源对象的Label 三 Namespace 集群环境共享与隔离 3 1 创建Namespace 3 2 定义C
  • [病虫害识别|博士论文]面向农作物叶片病害鲁棒性识别的深度卷积神经网络研究

    文章目录 创新点 文章中的方法 国内外现状 手工设计特征 基于深度特征学习的农作物病害识别研究 基于高阶残差的卷积神经网络的农作物病害识别 结构图 对比方法 基于高阶残差和参数共享反馈的卷积神经网络农作物病害识别方法 结构图 对比方法 基于
  • CSS选择除第一个和最后两个以外的所有子元素 + 结构伪类选择器深度解析

    最近在练习网易严选首页的布局时 发现它的顶部导航栏需求很特殊 第一项和最后两项是没有下拉选择框的 那么问题来了 在写css的时候该怎么使用选择器去达到这样的需求呢 首先先贴一下我最后的解决方案 nav first gt li nth chi
  • 数据库技术之mysql50题

    目录 数据表介绍 数据SQL 练习题 数据表介绍 1 学 表 Student SId Sname Sage Ssex SId 学 编号 Sname 学 姓名 Sage 出 年 Ssex 学 性别 2 课程表 Course CId Cname
  • 18-Go语言之单元测试

    go test工具 Go语言中的测试依赖go test命令 编写测试代码和编写普通的Go代码过程是类似的 并不需要学习新的语法或工具 go test命令是一个按照一定约定和组织的测试代码的驱动程序 在包目录内 所有以 test go为后缀的
  • 就业DAY7_web服务器_http协议

    import socket def servece client new socket 为这个客户端返回数据 1 接收浏览器发送过来的请求 即http请求 GET HTTP 1 1 request new socket recv 1024
  • 【Unity3D】如何快速做出点击按钮切换场景

    1 首先建立第一个场景 在Canvas创建一个Button 快捷键为Ctrl N 再按Ctrl S保存该场景到文件 如图所示 图中的 开始 为按钮 2 创建第二个场景 作为点击按钮后切换的场景 点击左上角 File Build Settin
  • 精心挑选了三种热门的Python技术书籍送给大家!!

    本周三狗哥给大家挑选了三种热门的Python书籍 送给大家 每种书送两本 共6本 文末查看送书规则 Python大数据分析 公众号回复 送书 Python最优化算法实战 扫码回复 送书 Python数据分析 扫码回复 送书 公众号回复 送书
  • js 把带有对象的数组里的某个属性组成新的数组

    如果想将数组对象中的某个属性组成一个新的数组 可以使用Array map 方法 这个方法会遍历原始数组的每个元素 并返回一个新的数组 其中包含指定属性的值 以下是一个示例 假设有一个包含对象的数组 每个对象都有一个name属性 你想要将所有
  • html5新特性

    目录 使用语义化标签的目的 1 html5新增的语义化标签 2 html新增的多媒体标签 1 视频 video 2 音频 audio 属性 object fit 3 html5新增的input表单元素属性 1 新增的input标签type属
  • 准备加入第二个项目(第5960小时加入)

    今天 老师过来办事 看了我做的东西后 邀请我加入他的项目 让我受宠若惊 2012年10月 我加入老师的项目后 2天内落荒而逃 因为一句代码都没有写出来 再然后 老师以我没有项目经验为由 拒绝了我后来想加入项目的要求 2年后 老师邀请我去做项
  • 安装Anaconda科学计算包

    Anaconda介绍 最近在看 Python语言及其应用 这本书 作为一本介绍Python语言和应用的书非常不错 在这本书的最后 介绍了一些Python常用的第三方类库 像科学计算库 金融计算库 图形图像库等等 其中也介绍了Anaconda
  • 移动端H5页面生成图片解决方案

    现在有很多微信公众号运营活动 都有生成图片的需求 生成图片后可以发送给好友和发到朋友圈扩散 利于产品的宣传 1 生成图片可以用canvas 但是由于已经有了html2canvas这个开源库 所以为了节省时间就没有自己写了 github地址
  • 为什么文件删除了但磁盘空间没有释放?

    1 案例现象 这天 监控系统发来一条告警消息 内容说某台服务器根目录磁盘占用空间达到阈值 超过百分之八十了 登上服务器 df Th 看一下 发现磁盘空间确实不够用了 root localhost df Th 文件系统 类型 容量 已用 可用
  • java怎么从一个类传值到另一个类,关于JAVA的引用类型传值.

    方法参数传递都按值传递 对于基本类型 传递原始值 对于对象类型 传递其指向的对象的地址值 多个同类型不同的变量可以指向同一个对象 但是其中任何一个变量被重新赋值 也就是指向一个新的对象时 不影响其它变量的指向 方法定义的形参 在调用的发生的
  • Socket编程之聊天程序 - 模拟Fins/ModBus协议通信过程

    设备控制软件编程涉及到的基本通信方式主要有TCP IP与串口 用到的数据通信协议有Fins与ModBus 更高级别的通信如 net中的Remoting与WCF在进行C S架构软件开发时会采用 本篇文章结合Fins ModBus协议的指令帧结