天天看點

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個窗體程式,一個服務端,一個用戶端,再建立一個庫項目,用來生成用戶端和伺服器共用的代碼服務。就像下面這樣子

C# Thrift 實戰開發 從PLC到Thrift再到用戶端內建開發

接下來我們既然要讀取PLC的資料,使用Thrift技術。那麼我們就要進行安裝相關的插件支援,我們在NuGet界面上進行安裝兩個插件,Thrift和HslCommunication,對于Thrift而言,三個項目都需要安裝,對于HslCommunication隻需要安裝到伺服器:

C# Thrift 實戰開發 從PLC到Thrift再到用戶端內建開發

安裝HslCommunication

C# Thrift 實戰開發 從PLC到Thrift再到用戶端內建開發

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

程式架構設計如下:

C# Thrift 實戰開發 從PLC到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檔案如下:

C# Thrift 實戰開發 從PLC到Thrift再到用戶端內建開發

這個檔案存放的目錄在下面這個目錄,和安裝thrift的package目錄一緻:

C# Thrift 實戰開發 從PLC到Thrift再到用戶端內建開發

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

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

C# Thrift 實戰開發 從PLC到Thrift再到用戶端內建開發

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

C# Thrift 實戰開發 從PLC到Thrift再到用戶端內建開發

輸入thrift-0.9.1.exe -help

C# Thrift 實戰開發 從PLC到Thrift再到用戶端內建開發

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

C# Thrift 實戰開發 從PLC到Thrift再到用戶端內建開發

然後我們就看到路徑下多了一個檔案夾

C# Thrift 實戰開發 從PLC到Thrift再到用戶端內建開發

點進去後就是:

C# Thrift 實戰開發 從PLC到Thrift再到用戶端內建開發

就是我們之前填寫的資訊生成的檔案。接下來,把這兩個檔案添加到一開始我們建立的三個項目的Common項目中去:

C# Thrift 實戰開發 從PLC到Thrift再到用戶端內建開發
C# Thrift 實戰開發 從PLC到Thrift再到用戶端內建開發

重新生成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();
        }
      

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

C# Thrift 實戰開發 從PLC到Thrift再到用戶端內建開發
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 + "完成!");
        }
      

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

C# Thrift 實戰開發 從PLC到Thrift再到用戶端內建開發

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

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