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
