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