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