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

C# Thrift 实战开发 从PLC到Thrift再到客户端集成开发的更多相关文章

  1. android开发环境:使用Android Studio搭建Android集成开发环境(图文教程)

    开发环境情况: 物理机版本:Win 7旗舰版(64位) Java SDK版本:jdk1.8.0_25(64位) Android SDK版本:Android 7.1(API 25) Android St ...

  2. oracle组建:ODAC112021Xcopy_x64,在开发机上,不用安装oracle的客户端等开发

    以下解决方案是为了连接远程服务器上的oracle 11g 的解决方案. 下载地址:http://www.oracle.com/technetwork/database/windows/download ...

  3. Linux搭建Scrapy爬虫集成开发环境

    安装Python 下载地址:http://www.python.org/, Python 有 Python 2 和 Python 3 两个版本, 语法有些区别,ubuntu上自带了python2.7. ...

  4. 【CC2530入门教程-01】IAR集成开发环境的建立与项目开发流程

    [引言] 本系列教程就有关CC2530单片机应用入门基础的实训案例进行分析,主要包括以下6部分的内容:1.CC2530单片机开发入门.2.通用I/O端口的输入和输出.3.外部中断初步应用.4.定时/计 ...

  5. 什么是IDE(集成开发环境)?

    实际开发中,除了编译器是必须的工具,我们往往还需要很多其他辅助软件,例如: 编辑器:用来编写代码,并且给代码着色,以方便阅读: 代码提示器:输入部分代码,即可提示全部代码,加速代码的编写过程: 调试器 ...

  6. hive12启动报错org.apache.thrift.server.TThreadPoolServer.<init>(Lorg/apache/thrift/server/TThreadPoolServer$Args;)

    执行如下命令启动hive服务:./bin/hive --service hiveserver,报如下错误: Starting Hive Thrift ServerException in thread ...

  7. MongoDB从入门到实战之.NET Core使用MongoDB开发ToDoList系统(2)-Swagger框架集成

    Swagger是什么? Swagger是一个规范且完整API文档管理框架,可以用于生成.描述和调用可视化的RESTful风格的 Web 服务.Swagger 的目标是对 REST API 定义一个标准 ...

  8. Python开发入门与实战9-基于vs的集成开发环境

    9. 基于visual studio的Python的集成开发环境 上一章我们描述了如何安装使用Java的集成开发环境Eclipse IDE,本章我们来说明另一种集成开发环境,也是笔者多年的开发习惯使用 ...

  9. Python开发入门与实战8-基于Java的集成开发环境

    8. 基于Java的Python的集成开发环境 目前为止我们所有的代码和例子都是通过Notepad文本编辑器来实现的,实际项目开发中这种编码模式效率较低(大虾除外),使用IDE集成开发环境常常大幅度的 ...

随机推荐

  1. 开发人员不可不看的 OBD通讯协议知识

    OBD-II Network Standards» J1850 VPW– Adopted by GM; also known as Class 2.– Adopted by Chrysler (kno ...

  2. Windows7使用无线网卡建立WiFi热点

    在Windows7下设置热点需要用到命令netsh wlan,具体的设置步骤如下: 1.配置热点 以管理员身份打开命令行模式,输入命令 netsh wlan set hostednetwork mod ...

  3. 如何为openwrt中的某个模块生成PKG_MIRROR_HASH

    答:介绍两种方法,第一种自动生成(当然使用自动的啦),第二种手动生成 第一种方法: 1.在软件包的Makefile中让此项写成这样PKG_MIRROR_HASH:=skip  (如果不加上skip,那 ...

  4. python的socket的tcp协议编程

    服务端代码如下: # _*_ coding:utf-8 _*_import socketimport datetimeHOST='0.0.0.0'PORT=3434 s=socket.socket(s ...

  5. C# Nginx平滑加权轮询算法

    代码很简单,但算法很经典,话不多说,直接上代码. public struct ServerConfig { /// <summary> /// 初始权重 /// </summary& ...

  6. sleep(),wait(),yield()和join()方法的区别

    sleep() sleep()方法需要指定等待的时间,它可以让当前正在执行的线程在指定的时间内暂停执行,进入阻塞状态,该方法既可以让其他同优先级或者高优先级 的线程得到执行的机会,也可以让低优先级的线 ...

  7. 源码安装git

    1.安装依赖包 yum install curl-devel expat-devel gettext-devel openssl-devel zlib-devel 2.下载git源码并解压缩 wget ...

  8. vc libcurl 模拟上传文件

    http://www.cnblogs.com/killbit/p/5393301.html 附上这篇文章,因为当时就已经想到了模拟上传,但是因为时间关系,所以就直接用PHP写了.现在改进一下,用VC+ ...

  9. JAVA初学者(一)

    2015-12-15 21:26:17 刚学的java  做个总结: 1.构造函数没有返回值. 2.A对象调用Q的方法,Q方法里的变量就是A的变量 Fraction add(Fraction f) 在 ...

  10. CALL_AND_RETRY_LAST Allocation failed node打包报错

    全局安装increase-memory-limit: npm install -g increase-memory-limit 进入工程目录,执行: increase-memory-limit