|
設(shè)備控制軟件編程涉及到的基本通信方式主要有TCP/IP與串口,用到的數(shù)據(jù)通信協(xié)議有Fins與ModBus。 更高級別的通信如.net中的Remoting與WCF在進行C/S架構(gòu)軟件開發(fā)時會采用。
本篇文章結(jié)合Fins/ModBus協(xié)議的指令幀結(jié)構(gòu)與數(shù)據(jù)編碼與解碼過程,自定義了一套TcpChatter數(shù)據(jù)數(shù)據(jù)通信協(xié)議,編寫了一個聊天程序,說明TCP/IP的在一個項目中應(yīng)用。
本文涉及到的源代碼工程項目為 - TcpChatter 后面附件提供源代碼下載 ( OpenSource Code 軟件版本:VS2008 語言:C#)
1 先普及幾個基本概念
Socket
接觸C/C++的人都知道,編寫網(wǎng)絡(luò)程序會用到Socket,對于Socket編程,其基本編程思想就是使用listen,accept,connect,send與write等幾個操作來實現(xiàn)客戶端與服務(wù)端的通信。
對于使用C#的程序員,.net為我們提供了Socket類來編寫服務(wù)程序,提供了TcpClient來編寫客戶端程序。我們只需要知道如何使用listen,accept,connect,send與write操作就能編寫我們需要的網(wǎng)絡(luò)程序了。
簡單的說:
Socket是支持TCP/IP協(xié)議的網(wǎng)絡(luò)通信的基本操作單元,它是建立在TCP/IP協(xié)議上的一組編程接口,是我們編寫代碼,使用TCP/IP進行數(shù)據(jù)通信的入口,它是對TCP/IP協(xié)議棧的抽像,等效于是TCP/IP協(xié)議棧提供的對外編程接口,在.net中,只是說這個編程接口的實現(xiàn)由Micosoft為我們完成,我們做的唯一工作只是使用這些接口就能在我們的應(yīng)用程序間進行TCP/IP通信了
TCP/IP - Transmission Control Protocol/Internet Protocol
傳輸控制協(xié)議/因特網(wǎng)互聯(lián)協(xié)議,是Internet互聯(lián)網(wǎng)絡(luò)的基礎(chǔ),由傳輸層的TCP協(xié)議與網(wǎng)絡(luò)層的IP協(xié)議構(gòu)成。網(wǎng)絡(luò)層負責(zé)在節(jié)點與節(jié)點之間傳送數(shù)據(jù)包(IP數(shù)據(jù)包),該IP數(shù)據(jù)包由TCP協(xié)議來進行組裝,IP數(shù)據(jù)包再通過它的下層協(xié)議以太網(wǎng)協(xié)議 (IEEE802)在光纖上進行傳輸,從而將不同的信息從一臺計算機傳送到了另一臺計算機。
對于程序員,我們編寫的程序要在不同的計算機間進行數(shù)據(jù)通信,可以通過Socket編程來使用TCP/IP,從而將我們的數(shù)據(jù)從一臺計算機傳到了另一臺計算機。
PLC - Programmable Logic Controller
可編程邏輯控制器,一種數(shù)字運算操作的電子系統(tǒng),專為工業(yè)環(huán)境應(yīng)用而設(shè)計,與計算機一樣,可以把它看成是一種用于工業(yè)控制的計算機,它也有自己的編程語言 - T形圖,可以通過T形圖編程來實現(xiàn)各種設(shè)備的控制。
Fins - Factory Interface Network Service
Fins協(xié)議是歐姆龍開發(fā)的用于工業(yè)自動化控制網(wǎng)絡(luò)的指令/響應(yīng)通信協(xié)議,它借助TCP/IP協(xié)議與串口通信協(xié)議,通過發(fā)送Fins指令實現(xiàn)在各種網(wǎng)絡(luò)間的無縫通信,這里主工是PC與PLC的通信,我們PC可以通過發(fā)送Fins指令與PLC進行通信。
ModBus
Modbus是由Modicon公司于1979年發(fā)明,是全球第一個真正用于工業(yè)現(xiàn)場的總線協(xié)議。在工業(yè)控制系統(tǒng)中,目前ModBus已經(jīng)成為一通用工業(yè)標(biāo)準(zhǔn).
Modbus主要應(yīng)用于電子控制器上的一種通用數(shù)據(jù)協(xié)議,借助TCP/IP協(xié)議與串口通信協(xié)議,通過發(fā)送Modbus指令實現(xiàn)在各種設(shè)備之間的通信。目前公司的溫控表與PC間的通信采用ModBus。
2 TcpChatter消息傳輸結(jié)構(gòu)
2.1 TcpChatter軟件架構(gòu)
與傳統(tǒng)的軟件系統(tǒng)一樣,TcpChatter采用C/S架構(gòu),即客戶端/服務(wù)端架構(gòu),Client參與通信會話,Server不參與通信會話,只負責(zé)將Client的消息通過Server進行轉(zhuǎn)發(fā),從而實現(xiàn)Client-Client間的通信。
整個TcpChatter的代碼結(jié)構(gòu)由 ChatServer + ChatClient構(gòu)成
2.2 TcpChatter消息處理
如下圖所示TcpChatterMessageTransaction,為TcpChatter系統(tǒng)中使用的消息處理結(jié)構(gòu)。下圖異展示了從 客戶端A←→客戶端B 的消息傳遞過程。
2.3 TcpChatter消息處理原理
在不同的客戶端進行通信,客戶端通過將消息封裝為TcpChatter指定的數(shù)據(jù)格式 [TcpChater指令 + 數(shù)據(jù)] 然后發(fā)送給服務(wù)端程序ChatServer,服務(wù)端程序再將消息轉(zhuǎn)發(fā)給指定的客戶端,客戶端收到消息后解析TcpChater數(shù)據(jù)包然后做其它的處理。

3 TcpChatter指令幀結(jié)構(gòu)
目前工業(yè)控制中的溫控主流采用串口通信,使用數(shù)據(jù)通信協(xié)議為ModBus協(xié)議,而與底層PLC通信則多采用Fins協(xié)議。下面分別解釋ModBus協(xié)議與Fins協(xié)議的指令幀結(jié)構(gòu)與TcpChatter指令幀結(jié)構(gòu)。
TcpChatter指令 幀用于在客戶端與服務(wù)端進行統(tǒng)一格式的數(shù)據(jù)通信。其基本構(gòu)成為 : TcpChatter指令域 + TcpChatter數(shù)據(jù)域;
Fins協(xié)議與ModBus協(xié)議原理基本一樣,其各自的指令幀結(jié)構(gòu) 基本有2部分構(gòu)成: 指令域 + 數(shù)據(jù)域
指令域為Fins協(xié)議與ModBus協(xié)議定義的數(shù)據(jù)通信格式,指令域字節(jié)長度也不一樣.比如Fins指令有效指令域(Fin頭 + Fin指令域)為12個節(jié)字,數(shù)據(jù)域長度能到2000字節(jié),而ModBus協(xié)議有效指令域(地址 + 功能碼 + CRC校驗碼)為4個字節(jié),數(shù)據(jù)域為(256-4)字節(jié)或(260-4)字節(jié)。
數(shù)據(jù)域為發(fā)送的真正數(shù)據(jù)。由于受限于硬件設(shè)備通信的數(shù)據(jù)速率, 在串口與TCP/IP通信中, 指令域 + 數(shù)據(jù)域的總長度是有限制的。我們通過PC與設(shè)備進行通信,實際上是在反復(fù)的發(fā)送這些 數(shù)據(jù)包與解析這些數(shù)據(jù)包,從而達到PC與設(shè)備信息交互的目的。
熟悉嵌入式的人都知道,我們編寫代碼跟設(shè)備進行通信,基本是在通過操作設(shè)備的寄存器對寄存器進行讀寫從而達到控制設(shè)備狀態(tài)與獲取設(shè)備狀態(tài)的目的。寄存器普通的有8位與16位,我們最常見的溫控表是8位寄存器,正好一個byte,每一位都可以看成是硬件上的一個I/O,我們通過操作這些位從而操作了對應(yīng)的硬件的I/O狀態(tài) (0/1),設(shè)備跟據(jù)這些I/O狀態(tài)做出相應(yīng)的動作。
3.1 Fins指令幀結(jié)構(gòu)
(Fins響應(yīng)幀(應(yīng)答幀結(jié)構(gòu))結(jié)構(gòu)與Fins指令幀結(jié)構(gòu)類似)


3.2 ModBus指令幀結(jié)構(gòu)
(ModBus也具備響應(yīng)幀結(jié)構(gòu))

2.3 TcpChatter指令幀結(jié)構(gòu)
TcpChatter指令 幀用于在客戶端與服務(wù)端進行統(tǒng)一格式的數(shù)據(jù)通信。其基本構(gòu)成為 : TcpChatter指令域 + TcpChatter數(shù)據(jù)域;
TcpChatter指令域構(gòu)成: 命令頭 + 命令請求模式+ 發(fā)送者ID + 收發(fā)模式 + 收接都ID + 預(yù)留指令
TcpChatter指令域長度: 8bytes
TcpChatter數(shù)據(jù)域長度: 20kb

3.4 TcpChatter 指令域結(jié)構(gòu) (該指令在通信過程中變換成了byte,可以進行位操作)
- /// <summary>
- /// TcpChatter 數(shù)據(jù)通信命令格式定義
- /// </summary>
- public struct LCmd
- {
- public int Head; // 有效命令開始標(biāo)志(命令頭)
- public int CmdMode; // 命令請求模式
- public int SendID; // 發(fā)送者用戶ID
- public int WR; // 發(fā)送或讀寫模式
- public int RecvID; // 接收者用戶ID
- public int Resv2; // 預(yù)留
- public int Resv3; // 預(yù)留
- public int Resv4; // 預(yù)留
- }
3.5 TcpChatter 指令集
- /// <summary>
- /// 應(yīng)答請求命令
- /// </summary>
- public enum CmdRequest
- {
- MinID = -1,
- Online = 0x01, // 在線請求
- FixUser = 0x02, // 向固定用戶發(fā)送消息請求
- Flush = 0x03, // 向固定用戶閃屏請求
- FlushAll = 0x04, // 向所有用戶閃屏請求
- Broadcast = 0x05, // 廣播消息請求
- Offline = 0x06, // 離線請求
- UpdateUsers = 0x07, // 用戶列表更新請求
- Success = 0x08, // 用戶連接服務(wù)成功應(yīng)答
- InvalidUser = 0x09, // 非法用戶名 - (預(yù)留)
- Failed = 0x0A, // 用戶連接服務(wù)失敗應(yīng)答
- InvalidCmd = 0xFF, // 非法命令包 - (預(yù)留)
- MaxID,
- }
4 ChatServer - TcpChatter服務(wù)端程序
4.1 ChatAgent服務(wù)端 獲取客戶端獨立的Socket連接請求
在TcpChatter項目中,通過TcpListener創(chuàng)建一個監(jiān)聽端口獲取Socket連接請求,不同的客戶端連接請求(TcpClient的Connect),服務(wù)端會創(chuàng)建客戶端各自獨立的Socket對象,在ChatAgent中通過ClientContext管理了所有連接客戶端的Socket,消息的轉(zhuǎn)發(fā)通過各自不同的Socket進行。
4.1.1 ClientContext
ChatServer服務(wù)端通過dicClientContext 表保存了所有連接客戶端的信息,當(dāng)客戶端異?;螂x線,其客戶端資源會被從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 監(jiān)聽連接請求與消息監(jiān)聽流程圖
如下圖所示,ChatServer啟動了一個監(jiān)聽端口,當(dāng)有新的連接請求達到,會生成新的Socket對象,同時啟動Socket服務(wù)消息監(jiān)聽線程:
服務(wù)監(jiān)聽線程:客戶端連接請求線程,有新的客戶端成功連接服務(wù)端時會生成新的Socket對象。該線程為所有客戶端服務(wù)。
Socket服務(wù)線程:服務(wù)監(jiān)聽線程的子線程,用于處理服務(wù)端使用Socket轉(zhuǎn)發(fā)的消息。為指定Socket的獨立客戶端服務(wù)。

4.3 IChatAgent服務(wù)代理接口
TcpChatter的服務(wù)端接口含2個屬性與2個接口
Name : 服務(wù)器名稱
IsAlive:服務(wù)器激活狀態(tài)
StartChatServer: 啟動服務(wù)接口
StopChatServer:關(guān)閉服務(wù)接口
- public interface IChatAgent
- {
- string Name { get;}
- bool IsAlive { get; }
- bool StartChatServer();
- bool StopChatServer();
- }
4.4 服務(wù)監(jiān)聽線程
- /// <summary>
- /// 客戶端 消息處理主線程
- /// </summary>
- private void MessageProcessThread()
- {
- ClientContext client = null;
- while (IsAlive)
- {
- try
- {
- byte[] useNameBuf = new byte[MAXBUFSIZE];
-
- // 監(jiān)聽連接請求對像
- Socket msgSkt = tcpListener.AcceptSocket();
-
- // 等待上線請求
- int actualLens = msgSkt.Receive(useNameBuf);
-
- // 獲取實際數(shù)據(jù)長度
- 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;
- }
-
-
- // 服務(wù)端生成用戶信息 并動態(tài)分配獨立用戶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;
-
- // 發(fā)送登陸成功命令
- msgSkt.Send(CurrentCmd);
-
- string sysmsg = string.Format("[系統(tǒng)消息]\n新用戶 {0} 在[{1}] 已成功連接服務(wù)器[當(dāng)前在線人數(shù): {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))
- {
- // 啟用用戶 消息監(jiān)聽線程
- 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服務(wù)消息線程
- /// <summary>
- /// 用戶消息監(jiān)聽線程
- /// </summary>
- /// <param name="client"></param>
- private void SubMsgProcessThread(ClientContext clientx, string message)
- {
- ClientContext client = clientx;
- while (true)
- {
- try
- {
- byte[] msgBuf = new byte[MAXBUFSIZE];
- // 監(jiān)聽 并接收數(shù)據(jù)
- 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
- {
- // 用戶消息轉(zhuǎn)發(fā)
- UserMessageProcess(client);
- }
- }
- catch (Exception ex)
- {
- ClientOfflineProcess(client);
- //Innerlog.Error(dcrlringType, "Current user = " + client.UserName + " was offline!", ex);
- Thread.CurrentThread.Abort();
- }
- }
- }
4.6 序列化
序列化與反序列化在TcpChatter中被用于消息的編碼與解碼。編碼與解碼過程可以詳細的參看ChatAgent代碼內(nèi)部實現(xiàn)。
序列化描述了持久化一個對像對流的過程,反序列化則與此過程相反,表示從流到對象的重建過程。在.net中,消息傳遞,數(shù)據(jù)存儲都大量的用到了序列化與反序列化的操作。
由于客戶端與服務(wù)端消息傳輸以byte字節(jié)流的方式進行傳輸,當(dāng)在客戶端之前傳遞對象時需要對對象進行序列化。如傳遞客戶端在線列表,該列表是一個Dictionary,在客戶端與服務(wù)端進行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 服務(wù)端程序
- 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所示,輸入端口服務(wù)啟動
運行ChatClient,輸入用戶名就可以聊天了。當(dāng)有新用戶上線或新用戶離線時,ChatServer控制臺會顯示當(dāng)前用在線用戶的情況。

運行ChatClient輸入用戶名

ChatClient聊天界面

附錄:TcpChatter源代碼下載 軟件版本: VS2008 語言:C#
TcpChatter源代碼.rar
|