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