C# Thrift 實戰開發 從PLC到Thrift再到客戶端集成開發


About Thrift:


 

本文並不是說明Thrift設計及原理的,直接拿Thrift來開發一個Demo程序,如果想要了解Thrift的細節,可以訪問官方網站:https://thrift.apache.org/ 官方的網站上除了介紹說明外,當然還有白皮書,詳細的說明Thrift是干嘛用的。

簡單的說,Thrift可以作為一個中間數據站,我們可以將數據丟到Thrift上,等待客戶端的請求,而這個客戶端可能是C#程序,當然也有可能是java程序,甚至是php,ruby,python等等,就像白皮書的介紹一樣,一個靈活的,可伸縮的,多語言的服務集成。

 

About Demo:


 

關於本項目的意圖,基於對Thrift簡單的學習后,就想要拿個Demo進行練手,模擬一些實際的操作,順便測試測試一些東西,加強自己對Thrift的理解,才能判別這個技術是否真的適合你。

大致介紹下本項目,本項目主體功能是,服務器端程序不停的讀取西門子PLC進行數據更新,並將數據刷新到Thrift,客戶端調用Thrift服務來訪問服務器的數據,除此之外,實現一個操作,在客戶端做一個按鈕,點擊按鈕后,將一個數據(通過服務器程序中轉)寫入到PLC中,並返回是否寫入成功的標記。

其他的功能就是測試測試連接穩定性,網絡重連機制的試驗。

 

Getting Started


說了那么多,趕緊開始吧,此處我的IDE時VS2017,先創建一個簡單的winform項目吧。在這個解決方案里,共創建2個窗體程序,一個服務端,一個客戶端,再創建一個庫項目,用來生成客戶端和服務器共用的代碼服務。就像下面這樣子

 

接下來我們既然要讀取PLC的數據,使用Thrift技術。那么我們就要進行安裝相關的插件支持,我們在NuGet界面上進行安裝兩個插件,Thrift和HslCommunication,對於Thrift而言,三個項目都需要安裝,對於HslCommunication只需要安裝到服務器:

安裝HslCommunication

OK,到這里為止,我們前期的准備工作基本完成,接下來需要設計讀取的數據和實現的功能,以這個為前提去設計Thrift的實現接口。

 

程序架構設計如下:


 

有了上述的基礎設計后,接下來就是設計Thrift這一層希望提供什么樣子的接口操作了,此處我們就舉一些簡單的例子,首先呢,設備不會只有一台,我們就假設有好多台設備,每台設備有如下參數信息:

  • 設備的名稱,我們采用string來存儲
  • 設備的唯一ID,我們也采用string來存儲
  • 設備的IP地址,string存儲
  • 設備的運行狀態,允許有多個狀態,int存儲
  • 設備的報警狀態,允許組合實現32種報警,int存儲,每個位對應一種報警
  • 設備的溫度,double數據
  • 設備的壓力,double數據

然后在Thrift中,我們希望公開的數據有獲取單台設備的信息,也有針對報警中的統計信息。獲取所有設備運行狀態的json數據,所有設備報警狀態的json數據,單獨獲取所有設備的溫度數據,單獨獲取所有設備的壓力值,最后再提供一個允許手動更改設備狀態的接口,參考了官方的白皮書(地址為:https://thrift.apache.org/static/files/thrift-20070401.pdf),最終完成的Demo.thrift文件如下:

這個文件存放的目錄在下面這個目錄,和安裝thrift的package目錄一致:

接下來就是調用上圖中的thrift-0.9.1.exe來生成代碼了,具體方式如下:

打開電腦的cmd指令(也就是命令提示符):

然后cd到上面的目錄里去,指令為cd /d 目錄,結果如下:

輸入thrift-0.9.1.exe -help

ok,到這里為止,我們知道了怎么去生成C# 代碼了:指令如下:thrift-0.9.1.exe --gen csharp Demo.thrift

 

然后我們就看到路徑下多了一個文件夾

點進去后就是:

就是我們之前填寫的信息生成的文件。接下來,把這兩個文件添加到一開始我們創建的三個項目的Common項目中去:

 

重新生成Common項目,OK,到這里為止,我們前期的任務都完成了,接下來就是真正寫代碼的時候了。

 

Server Implementation


在Server端要做的第一件事就是添加對Common項目生成的dll組件的引用,第二件事是創建一個類,繼承Common項目中的一個接口:如下:

namespace Thrift.Server
{
    public class PublicServiceHandle : ThriftInterface.PublicService.Iface
    {
        public int GetAlarmCount()
        {
            throw new NotImplementedException();
        }

        public List<MachineOne> GetAllMachineOnes()
        {
            throw new NotImplementedException();
        }

        public string GetJsonMachineAlarm()
        {
            throw new NotImplementedException();
        }

        public string GetJsonMachinePress()
        {
            throw new NotImplementedException();
        }

        public string GetJsonMachineState()
        {
            throw new NotImplementedException();
        }

        public string GetJsonMachineTemp()
        {
            throw new NotImplementedException();
        }

        public MachineOne GetMachineOne(string machineId)
        {
            throw new NotImplementedException();
        }

        public int GetRunningCount()
        {
            throw new NotImplementedException();
        }

        public bool SetMachineRunState(string machineId, int state)
        {
            throw new NotImplementedException();
        }
    }
}

  接下來就實現這些具體代碼了。

namespace Thrift.Server
{
    public class PublicServiceHandle : ThriftInterface.PublicService.Iface
    {
        #region Constructor

        /// <summary>
        /// 實例化一個對象
        /// </summary>
        public PublicServiceHandle(Func<string,int,bool> write)
        {
            // 初始化數據
            list = new List<MachineOne>()
            {
                new MachineOne()
                {
                    Name = "測試設備",
                    Id = "1#",
                    IpAddress = "192.168.1.195",
                },
                new MachineOne()
                {
                    Name = "測試設備",
                    Id = "2#",
                },
                new MachineOne()
                {
                    Name = "測試設備",
                    Id = "3#",
                },
                new MachineOne()
                {
                    Name = "測試設備",
                    Id = "4#",
                },
                new MachineOne()
                {
                    Name = "測試設備",
                    Id = "5#",
                },
                new MachineOne()
                {
                    Name = "測試設備",
                    Id = "6#",
                },
                new MachineOne()
                {
                    Name = "測試設備",
                    Id = "7#",
                },
                new MachineOne()
                {
                    Name = "測試設備",
                    Id = "8#",
                },
                new MachineOne()
                {
                    Name = "測試設備",
                    Id = "9#",
                },
                new MachineOne()
                {
                    Name = "測試設備",
                    Id = "10#",
                },
            };

            hybirdLock = new HslCommunication.Core.SimpleHybirdLock();

            FuncWriteIntoPlc = write ?? throw new ArgumentNullException("write");
        }


        #endregion
        
        #region Private Member

        private List<MachineOne> list;                              // 總的數據倉庫
        private HslCommunication.Core.SimpleHybirdLock hybirdLock;  // 混合同步鎖,比Lock性能要高的多
        private Func<string, int, bool> FuncWriteIntoPlc;           // 寫入數據的委托,最終實現在外層

        #endregion

        #region Public Method

        /// <summary>
        /// 更新一台設備的數據,這個數據最終來自PLC
        /// </summary>
        /// <param name="id"></param>
        /// <param name="content"></param>
        public void UpdateMachineOne(string id, byte[] content)
        {
            if (content == null) return;

            hybirdLock.Enter();
            for (int i = 0; i < list.Count; i++)
            {
                if (list[i].Id == id)
                {
                    byte[] buffer = new byte[4];
                    // 獲取運行狀態
                    Array.Copy(content, 0, buffer, 0, 4);
                    Array.Reverse(buffer);
                    list[i].RunState = BitConverter.ToInt32(buffer, 0);
                    // 獲取報警狀態
                    Array.Copy(content, 4, buffer, 0, 4);
                    Array.Reverse(buffer);
                    list[i].AlarmState = BitConverter.ToInt32(buffer, 0);

                    // 其實信息參照這個就行
                    break;
                }
            }
            hybirdLock.Leave();
        }

        #endregion

        #region PublicService.Interface


        /// <summary>
        /// 獲取當前報警的機台數
        /// </summary>
        /// <returns></returns>
        public int GetAlarmCount()
        {
            int count = 0;
            hybirdLock.Enter();
            for (int i = 0; i < list.Count; i++)
            {
                if (list[i].AlarmState != 0) count++;
            }
            hybirdLock.Leave();
            return count;
        }

        /// <summary>
        /// 獲取所有設備的所有信息,一般不建議這么做
        /// </summary>
        /// <returns></returns>
        public List<MachineOne> GetAllMachineOnes()
        {
            return new List<MachineOne>(list);
        }

        /// <summary>
        /// 獲取當前所有機台的報警信息
        /// </summary>
        /// <returns></returns>
        public string GetJsonMachineAlarm()
        {
            JArray jArray = new JArray();
            hybirdLock.Enter();
            for (int i = 0; i < list.Count; i++)
            {
                JObject json = new JObject();
                json.Add(nameof(MachineOne.Name), new JValue(list[i].Name));
                json.Add(nameof(MachineOne.Id), new JValue(list[i].Id));
                json.Add(nameof(MachineOne.AlarmState), new JValue(list[i].AlarmState));
                jArray.Add(json);
            }
            hybirdLock.Leave();
            return jArray.ToString();
        }

        /// <summary>
        /// 獲取當前所有機台的壓力值
        /// </summary>
        /// <returns></returns>
        public string GetJsonMachinePress()
        {
            JArray jArray = new JArray();
            hybirdLock.Enter();
            for (int i = 0; i < list.Count; i++)
            {
                JObject json = new JObject();
                json.Add(nameof(MachineOne.Name), new JValue(list[i].Name));
                json.Add(nameof(MachineOne.Id), new JValue(list[i].Id));
                json.Add(nameof(MachineOne.Press), new JValue(list[i].Press));
                jArray.Add(json);
            }
            hybirdLock.Leave();
            return jArray.ToString();
        }

        /// <summary>
        /// 獲取當前所有機台的狀態
        /// </summary>
        /// <returns></returns>
        public string GetJsonMachineState()
        {
            JArray jArray = new JArray();
            hybirdLock.Enter();
            for (int i = 0; i < list.Count; i++)
            {
                JObject json = new JObject();
                json.Add(nameof(MachineOne.Name), new JValue(list[i].Name));
                json.Add(nameof(MachineOne.Id), new JValue(list[i].Id));
                json.Add(nameof(MachineOne.RunState), new JValue(list[i].RunState));
                jArray.Add(json);
            }
            hybirdLock.Leave();
            return jArray.ToString();
        }

        /// <summary>
        /// 獲取當前所有機台的溫度
        /// </summary>
        /// <returns></returns>
        public string GetJsonMachineTemp()
        {
            JArray jArray = new JArray();
            hybirdLock.Enter();
            for (int i = 0; i < list.Count; i++)
            {
                JObject json = new JObject();
                json.Add(nameof(MachineOne.Name), new JValue(list[i].Name));
                json.Add(nameof(MachineOne.Id), new JValue(list[i].Id));
                json.Add(nameof(MachineOne.Temp), new JValue(list[i].Temp));
                jArray.Add(json);
            }
            hybirdLock.Leave();
            return jArray.ToString();
        }

        /// <summary>
        /// 獲取單獨的一台設備信息
        /// </summary>
        /// <param name="machineId"></param>
        /// <returns></returns>
        public MachineOne GetMachineOne(string machineId)
        {
            // 這里需要不需要使用克隆對象?不太清楚,直接返回列表的對象會不會有影響?
            return list.Find(m => m.Id == machineId);
        }

        /// <summary>
        /// 獲取當前正在運行的總的機台數
        /// </summary>
        /// <returns></returns>
        public int GetRunningCount()
        {
            int count = 0;
            hybirdLock.Enter();
            for (int i = 0; i < list.Count; i++)
            {
                if (list[i].RunState == 1) count++;
            }
            hybirdLock.Leave();
            return count;
        }
        
        /// <summary>
        /// 設置設備的運行狀態
        /// </summary>
        /// <param name="machineId"></param>
        /// <param name="state"></param>
        /// <returns></returns>
        public bool SetMachineRunState(string machineId, int state)
        {
            // 按道理說這個方法應該向PLC進行數據寫入,但是具體的實現不應該在這一層
            return FuncWriteIntoPlc(machineId, state);
        }
        
        #endregion
    }
}

  

  主要功能就是實例化了一個數組,擁有十個設備,我們只有一台PLC,就模擬讀取一個就行了,但數組的操作需要加同步鎖,這里我們還要添加一個寫入數據的功能,這個功能應該在外面實現。至此,我們可以開發真正的服務器代碼了:

server上項目的form1窗口上添加兩個按鈕,分別為啟動,和停止,都觸發一個事件,然后在代碼里完成Thrift的初始化:

        private PublicServiceHandle handler;
        private TServer server;

        private void userButton1_Click(object sender, EventArgs e)
        {
            new System.Threading.Thread(() =>
            {
                // 啟動服務
                handler = new PublicServiceHandle(WritePlc);
                var processor = new ThriftInterface.PublicService.Processor(handler);

                TServerTransport transport = new TServerSocket(9090);

                server = new TThreadPoolServer(processor, transport);
                server.Serve();
            })
            {
                IsBackground = true
            }.Start();

            // 啟動定時器去讀取PLC數據
            timerReadPLC.Start();
        }
        private void userButton2_Click(object sender, EventArgs e)
        {
            // 關閉服務
            server?.Stop();
        }

 

  

  接下來需要完成讀取PLC數據,並提供一個方法WritePlc實現數據的真正寫入,此處由於我只有一個PLC所以,就方便實現了讀寫,不再區分多個設備。

        #region PLC Connection

        private SiemensTcpNet siemensTcp;                           // 和PLC的核心連接引擎
        private Timer timerReadPLC;                                 // 讀取PLC的定時器

        #endregion


        private void Form1_Load(object sender, EventArgs e)
        {
            siemensTcp = new SiemensTcpNet(SiemensPLCS.S1200)
            {
                PLCIpAddress = System.Net.IPAddress.Parse("192.168.1.195")
            };

            // 連接到PLC
            siemensTcp.ConnectServer();

            timerReadPLC = new Timer();
            timerReadPLC.Interval = 1000;
            timerReadPLC.Tick += TimerReadPLC_Tick;
        }

        private void TimerReadPLC_Tick(object sender, EventArgs e)
        {
            // 每秒執行一次去讀取PLC數據,此處簡便操作,放在前台執行,正常邏輯應該放到后台
            HslCommunication.OperateResult<byte[]> read = siemensTcp.ReadFromPLC("M100", 24);
            if(read.IsSuccess)
            {
                handler.UpdateMachineOne("1#", read.Content);
            }
            else
            {
                // 讀取失敗,應該提示並記錄日志,此處省略
            }
        }

        private bool WritePlc(string id, int value)
        {
            // 按道理根據不同的id寫入不同的PLC,此處只有一個PLC,就直接寫入到一個PLC中
            return siemensTcp.WriteIntoPLC("M100", value).IsSuccess;
        }

  到這里為止,我們已經把服務器端的程序都已經開發完成了,已經可以生成並運行了。

 

Client Implementation


 服務器端開發完成后,客戶端就相對容易多了,實例化變量名,並初始化后,就可以隨便使用了:

        private ThriftInterface.PublicService.Client client;

        private void Form1_Load(object sender, EventArgs e)
        {
            var transport = new TSocket("localhost", 9090);
            var protocol = new TBinaryProtocol(transport);
            client = new ThriftInterface.PublicService.Client(protocol);

            transport.Open();

            // 啟動后台線程實時更新機器狀態
            thread = new System.Threading.Thread(ThreadRead);
            thread.IsBackground = false;
            thread.Start();
        }

  

增加幾個按鈕及顯示框之后,增加一個定時讀取服務器各機台狀態並實時更新界面的功能:

 

System.Threading.Thread thread;
        private void ThreadRead()
        {
            while(true)
            {
                System.Threading.Thread.Sleep(1000);

                JArray jArray = JArray.Parse(client.GetJsonMachineState());
                int[] values = new int[10];
                // 解析開始
                for (int i = 0; i < jArray.Count; i++)
                {
                    JObject json = (JObject)jArray[i];
                    values[i] = json[nameof(ThriftInterface.MachineOne.RunState)].ToObject<int>();
                }

                if(IsHandleCreated) Invoke(new Action(() =>
                {
                    label1.Text = values[0].ToString();
                    label2.Text = values[1].ToString();
                    label3.Text = values[2].ToString();
                    label4.Text = values[3].ToString();
                    label5.Text = values[4].ToString();
                    label6.Text = values[5].ToString();
                    label7.Text = values[6].ToString();
                    label8.Text = values[7].ToString();
                    label9.Text = values[8].ToString();
                    label10.Text = values[9].ToString();
                }));
            }
        }


        private void ShowMessage(string msg)
        {
            if(textBox1.InvokeRequired)
            {
                textBox1.Invoke(new Action<string>(ShowMessage), msg);
                return;
            }

            textBox1.AppendText(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss  ") + msg + Environment.NewLine);
        }

        private void userButton1_Click(object sender, EventArgs e)
        {
            // 讀取運行中機台總數
            ShowMessage(client.GetRunningCount().ToString());
        }

        private void userButton2_Click(object sender, EventArgs e)
        {
            // 讀取報警中機台總數
            ShowMessage(client.GetAlarmCount().ToString());
        }

        private void userButton3_Click(object sender, EventArgs e)
        {
            // 讀取所有的報警信息
            ShowMessage(client.GetJsonMachineAlarm());
        }

        private void userButton4_Click(object sender, EventArgs e)
        {
            // 讀取所有的壓力信息
            ShowMessage(client.GetJsonMachinePress());
        }

        private void userButton5_Click(object sender, EventArgs e)
        {
            // 讀取所有的運行信息
            ShowMessage(client.GetJsonMachineState());
        }

        private void userButton6_Click(object sender, EventArgs e)
        {
            // 讀取所有的溫度信息
            ShowMessage(client.GetJsonMachineTemp());
        }

        private void userButton7_Click(object sender, EventArgs e)
        {
            // 讀取指定機台信息
            ThriftInterface.MachineOne machine = client.GetMachineOne("1#");
        }

        private void userButton8_Click(object sender, EventArgs e)
        {
            // 強制機台啟動
            if(client.SetMachineRunState("1#",1))
            {
                ShowMessage("寫入成功!");
            }
            else
            {
                ShowMessage("寫入失敗!");
            }
        }

        private void userButton10_Click(object sender, EventArgs e)
        {
            // 強制機台停止
            if(client.SetMachineRunState("1#",0))
            {
                ShowMessage("寫入成功!");
            }
            else
            {
                ShowMessage("寫入失敗!");
            }
        }



        private void userButton9_Click(object sender, EventArgs e)
        {
            // 用於高頻多線程壓力測試
            new System.Threading.Thread(ThreadReadManyTimes) { IsBackground = true, Name = "1" }.Start();
            new System.Threading.Thread(ThreadReadManyTimes) { IsBackground = true, Name = "2" }.Start();
            new System.Threading.Thread(ThreadReadManyTimes) { IsBackground = true, Name = "3" }.Start();
        }

        private void ThreadReadManyTimes()
        {
            for (int i = 0; i < 1000; i++)
            {
                client.GetRunningCount();
            }

            ShowMessage(System.Threading.Thread.CurrentThread.Name + "完成!");
        }

  

所有的代碼都已經寫完,接下來就是最終演示了:

 

 

但是在三條線程的壓力測試中,會出現異常,內部同步機制可能沒有做好,不知道什么原因,如果你知道,本人非常感謝!

 

本項目的github地址:https://github.com/dathlin/ThriftDemo


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM