通用访问 - 用“反射”来设计通用的通信协议,以及配套的SDK、工具
1. 效果演示
- 功能介绍
- 特点
- TCP协议
- WebApi协议
- 迷你网管
- 通用GIS
- 系统管理
1. 效果演示
//创建服务器 var __服务端 = FT通用访问工厂.创建服务端(); __服务端.端口 = 8888; __服务端.开启(); //实际对象 var __基本状态 = new M基本状态(); __基本状态.版本 = "1.0.0.0"; __基本状态.待处理问题.Add(new M问题 { 等级 = E重要性.普通, 内容 = "xxxx" }); __基本状态.待处理问题.Add(new M问题 { 等级 = E重要性.重要, 内容 = "yyyy" }); __基本状态.开启时间 = DateTime.Now; __基本状态.连接设备.Add(new M设备连接 { IP = IPAddress.Parse("192.168.0.1"), 标识 = "X1", 类型 = "X", 连接中 = true }); __基本状态.连接设备.Add(new M设备连接 { IP = IPAddress.Parse("192.168.0.2"), 标识 = "Y1", 类型 = "Y", 连接中 = true }); __基本状态.位置 = "威尼斯"; __基本状态.业务状态 = new Dictionary<string, string> { { "参数1", "参数1值" }, { "参数2", "参数2值" } }; //可通信对象 var __对象 = new M对象("基本状态", ""); __对象.添加属性("版本", () => __基本状态.版本, E角色.所有, null);//最后一个参数是元数据定义 __对象.添加属性("位置", () => __基本状态.位置, E角色.所有, null); __对象.添加属性("开启时间", () => __基本状态.开启时间.ToString(), E角色.所有, null); __对象.添加属性("待处理问题", () => HJSON.序列化(__基本状态.待处理问题), E角色.所有, null); __对象.添加属性("连接设备", () => HJSON.序列化(__基本状态.连接设备), E角色.所有, null); __对象.添加属性("业务状态", () => HJSON.序列化(__基本状态.业务状态), E角色.所有, null); __对象.添加方法("重启", __实参 => { //处理实参 return string.Empty; //返回空字符串或者json格式的对象 }, E角色.所有, null, null); //最后两个参数是对参数和返回值的元数据定义 __对象.添加事件("发生了重要变化", E角色.所有, null); //将对象加入到服务器 __服务端.添加对象("基本状态", () => __对象); //触发服务器端事件 __服务端.触发事件("基本状态", "发生了重要变化", null, null);//最后两个参数是实参和客户端地址列表(可选)
//创建客户端 var __客户端 = FT通用访问工厂.创建客户端(); __客户端.连接(new IPEndPoint(IPAddress.Any, 8888)); var __版本 = __客户端.查询属性值("基本状态", "版本"); var __待处理问题 = HJSON.反序列化<List<M问题>>(__客户端.查询属性值("基本状态", "待处理问题")); __客户端.执行方法("基本状态", "重启", null); //最后一个参数是实参 __客户端.订阅事件("基本状态", "发生了重要变化", __实参 => { //处理实参 });
2. 通信协议
功能介绍
- 该协议是应用层的底层协议,可以承载各种业务,开发具体业务时,只需定义需要通信的对象元数据
- 协议支持面向3种角色,开发/工程/客户,可以优雅的解决程序设计时的角色混杂问题
- 基于配套的SDK开发分布式应用,像开发单机应用一样容易
- 支持通用客户端(winform/web),面向开发/工程的功能,无需写代码就能访问服务器端对象;面向客户的功能,服务器端开发也能方便的测试
特点
- 只要定义通信对象就可以开发分布式应用,简单强大
- 跨平台,通用性强;开发SDK的难度不是很大,可以参考我写的.NET版本的,可以做到深度控制
TCP协议
报文格式
字段 |
类型 |
长度(字节) |
说明 |
起始标识 |
Byte[] |
2 |
固定为0xAAAA,用于识别报文开始 |
报文内容长度 |
Int32 |
4 |
余下所有字段长度 |
功能标识 |
Int16 |
2 |
用于识别报文类型 |
发送方事务标识 |
Int32 |
4 |
由发送方指定。当会话类型为请求型时,用于区别发送方的不同请求;当会话类型为通知型时,固定为0。 |
接收方事务标识 |
Int32 |
4 |
由接收方指定。当会话类型为请求型时,用于区别接收方的不同请求,特别的,会话开始的第一条报文,发送方的该字段填0;当会话类型为通知型时,固定为0。 |
负载 |
Byte[] |
* |
由报文内容长度字段指定, 推荐编码格式为UTF8文本, 采用json格式, 大数据量时采用压缩算法 |
功能码
功能 |
码 |
查询对象列表(请求) |
1 |
查询对象列表(响应) |
2 |
查询对象明细(请求) |
3 |
查询对象明细(响应) |
4 |
执行方法(请求) |
5 |
执行方法(响应) |
6 |
查询属性值(请求) |
7 |
查询属性值(响应) |
8 |
订阅事件 |
9 |
取消订阅事件 |
A |
接收事件 |
B |
查询对象列表
请求: 无
响应: [{名称, 分类, 角色}]
查询对象明细
请求: {对象名称}
响应: {属性列表[{名称, 元数据, 角色}], 方法列表[{名称, 形参列表[{名称, 元数据}, 角色], 返回值元数据}], 事件列表[{名称, 形参列表[{名称, 元数据}], 角色}]}
元数据: {类型, 结构(单值/对象/单值数组/对象数组), 描述, 默认值, 范围, 子成员列表[{名称, 元数据}]}
执行方法
请求: {对象名称, 方法名称, 实参列表[{名称, 值}]}
响应: {成功, 描述, 返回值}
查询属性值
请求: {对象名称, 属性名称}
响应: {成功, 描述, 返回值}
订阅事件
通知: {对象名称, 事件名称}
取消订阅事件
通知: {对象名称, 事件名称}
接收事件
通知: {对象名称, 事件名称, 实参列表[{名称, 值}]}
WebApi协议
响应数据格式(ContentType)为application/json;charset=utf-8
功能 |
HTTP方法 |
地址 |
查询对象列表 |
GET/ POST |
objects.k |
查询对象明细 |
GET/ POST |
object.k |
执行对象方法 |
GET/ POST |
method.k |
查询对象属性值 |
GET/ POST |
proterty.k |
登录 |
POST |
login.k |
心跳 |
GET |
heart.k |
注:成功后会设置cookies, 键为token
查询对象列表
参数: 无
响应: [{名称, 分类, 角色}]
查询对象明细
参数: 对象名称
响应: {属性列表[{名称, 元数据, 角色}], 方法列表[{名称, 形参列表[{名称, 元数据}], 返回值元数据, 角色}]}
执行方法
参数: 对象名称, 方法名称, 实参列表[{名称, 值}]
响应: {成功, 描述, 返回值}
查询对象属性值
参数: 对象名称, 属性名称
响应: {成功, 描述, 返回值}
登录
参数: password
响应: {成功(true/false), 描述}
心跳
参数: 无
响应: 服务器时间
3. SDK与工具
public interface IT服务端 { int 端口 { get; set; } int WebApi端口 { get; set; } void 添加对象(string 对象名称, Func<M对象> 获取对象); void 删除对象(string 对象名称); void 开启(); event Action<IPEndPoint> 客户端已连接; event Action<IPEndPoint> 客户端已断开; List<IPEndPoint> 客户端列表 { get; } void 关闭(); void 触发事件(string 对象名, string 事件名, Dictionary<string, string> 实参 = null, List<IPEndPoint> 地址列表 = null); } public interface IT客户端 { IPEndPoint 设备地址 { get; } void 连接(IPEndPoint 设备地址); void 断开(); bool 连接正常 { get; } /// <summary> /// true:主动断开,false:被动断开 /// </summary> event Action<bool> 已断开; bool 自动重连 { get; set; } event Action 已连接; M对象列表查询结果 查询可访问对象(); M对象明细查询结果 查询对象明细(string 对象名称); string 查询属性值(string 对象名, string 属性名, int 超时毫秒 = 3000); string 执行方法(string 对象名, string 方法名, Dictionary<string,string> 参数列表 = null, int 超时毫秒 = 3000); void 订阅事件(string 对象名, string 事件名, Action<Dictionary<string, string>> 处理方法); void 注销事件(string 对象名, string 事件名, Action<Dictionary<string, string>> 处理方法); /// <summary> /// 通常用于调试 /// </summary> event Action<M接收事件> 收到了事件; } public class M对象 { public string 名称 { get { return 概要.名称; } set { 概要.名称 = value; } } public string 分类 { get { return 概要.分类; } set { 概要.分类 = value; } } public E角色 角色 { get { return 概要.角色; } set { 概要.角色 = value; } } internal M对象概要 概要 { get; set; } internal M对象明细查询结果 明细 { get; set; } /// <summary> /// 字典中的键string:方法名; 值:方法; 方法中的string:返回值 /// </summary> Dictionary<string, Func<Dictionary<string, string>, string>> _所有方法 = new Dictionary<string, Func<Dictionary<string, string>, string>>(); /// <summary> /// 字典中的键string:方法名; 值:方法; 方法中的string:返回值 /// </summary> Dictionary<string, Func<Dictionary<string, string>, IPEndPoint, string>> _所有方法_带地址 = new Dictionary<string, Func<Dictionary<string, string>, IPEndPoint, string>>(); /// <summary> /// 字典中的键string:属性名; 值:属性值计算方法 /// </summary> Dictionary<string, Func<string>> _所有属性方法 = new Dictionary<string, Func<string>>(); public M对象(string __名称, string __分类) { this.概要 = new M对象概要(__名称, __分类); this.明细 = new M对象明细查询结果(); } public void 添加属性(string __名称, Func<string> __计算值, E角色 __角色 = E角色.开发, M元数据 __元数据 = null) { this.角色 = this.角色 | __角色; this.明细.属性列表.Add(new M属性(__名称, __元数据, __角色)); _所有属性方法[__名称] = __计算值; } public void 添加方法(string __名称, Func<Dictionary<string, string>, string> __执行方法, E角色 __角色 = E角色.开发, List<M形参> __参数列表 = null, M元数据 __返回值元数据 = null) { this.角色 = this.角色 | __角色; _所有方法[__名称] = __执行方法; this.明细.方法列表.Add(new M方法(__名称, __参数列表, __返回值元数据, __角色)); } public void 添加方法(string __名称, Func<Dictionary<string, string>, IPEndPoint, string> __执行方法, E角色 __角色 = E角色.开发, List<M形参> __参数列表 = null, M元数据 __返回值元数据 = null) { this.角色 = this.角色 | __角色; _所有方法_带地址[__名称] = __执行方法; this.明细.方法列表.Add(new M方法(__名称, __参数列表, __返回值元数据, __角色)); } internal string 执行方法(string __名称, Dictionary<string, string> __参数列表, IPEndPoint __来源) { if (_所有方法.ContainsKey(__名称)) { return _所有方法[__名称].Invoke(__参数列表); } if (_所有方法_带地址.ContainsKey(__名称)) { return _所有方法_带地址[__名称].Invoke(__参数列表, __来源); } throw new ApplicationException("执行方法失败: 无此方法"); } internal string 计算属性(string __属性名称) { if (_所有属性方法.ContainsKey(__属性名称)) { return _所有属性方法[__属性名称](); } throw new ApplicationException("计算属性失败: 无此属性"); } public void 添加事件(string __名称, E角色 __角色 = E角色.开发, List<M形参> __参数列表 = null) { this.角色 = this.角色 | __角色; this.明细.事件列表.Add(new M事件(__名称, __参数列表, __角色)); } }
4. 应用示例
迷你网管
协议(对象)
名片
属性
名称描述版本号版本时间
方法
查询返回值: {名称, 描述, 版本号, 版本时间}查询参数返回值: [{名称, 值}]
状态
属性
启动时间未清除告警数量健康状态(优/良/中/差)状态开始时间注: 健康状态推荐规则是存在紧急告警为差, 存在重要告警为中, 存在次要告警为良, 否则为优
方法
查询概要状态返回值: {启动时间, 健康状态, 状态开始时间, 未清除告警数量}查询业务概要参数: 类别(可为空), 属性(可为空)返回值: [{类别, 属性, 当前值, 正常范围, 正常, 单位, 描述}]查询未清除告警参数: 条件{每页数量, 页数, 来源设备类型(可选), 来源设备标识(可选), 类别(可选), 重要性(可选), 关键字(可选)}返回值: {总数量, 列表[{标识, 来源设备类型, 来源设备标识, 产生时间, 类别, 重要性, 描述, 原因, 解决方案}]}重要性:
一般(1)
正常通知, 无需排除告警
次要(2)
非直接影响业务的使用, 不需要立即处理, 但还是需要排除告警
重要(3)
严重的故障, 可能会影响业务, 需要用户处理
紧急(4)
系统级故障, 严重影响业务, 需要用户立即处理
事件
上报告警参数: 事件参数{标识, 来源设备类型, 来源设备标识, 产生时间, 类别, 重要性, 描述, 原因, 解决方案}上报清除参数: 事件参数{标识, 来源设备类型, 来源设备标识}健康状态变化参数: 事件参数{启动时间, 健康状态, 状态开始时间, 未清除告警数量}
系统
方法
重启返回值: 无关闭返回值: 无查询版本记录返回值: [{版本号, 标签[], 修改记录}]
FTP
属性
支持(true/false)运行中(true/false)端口号目录路径编码(ASCII/UTF8/GB2312)
方法
设置端口号参数: 端口号返回值: 无开启返回值: 无关闭返回值: 无
客户端界面
通用GIS
系统管理
5. 设计初衷与演化
初衷
公司的界面软件内容非常混杂,充斥了大量的面向开发人员的元素,与其他设备间的协议,太多的地方让人恶心,耦合度极高,稳定性和可维护性极差。年前讨论重构,其中最基本的一条思路就是区别客户、工程、开发三种角色,面向最终客户的界面软件与其他设备间的协议,应该以应用为导向设计协议(上层决定下层,下层提供可行性约束,协议接口必须抽象)。将面向开发人员的内容与面向最终客户的软件分离开后,需要重新编写面向开发人员的界面与协议,加上我之前一直重视和建议面向工程(及测试)人员开发工具类软件,这就是设计通用访问协议的初衷,因为工程和开发人员不需要最终客户那样的定制化的界面。经过反复的推敲,最终以“自描述”为核心的界面以及协议诞生了。
演化
以后设计分布式的软件,首先完全面向业务的设计出需要的对象及其成员元数据,形成一份简单的但是纯粹的协议,无需定义额外的功能码、报文头、结构体;然后服务端基于通用访问SDK(与平台和开发语言相关),注入对象及其成员的实现(类似于和程序内部的模块、对象挂钩子或是牵线),通过通用访问工具可以直接测试协议接口,无需最终的客户端,同时提供一些面向工程和开发人员自身的对象(这部分对象易变性很高,以往常寄生在面向客户的界面软件中,严重加大无谓的工作量),以便部署和调试;客户端方面同样基于通用访问SDK实现访问各种设备的协议约定的对象;最后如果一个设备需要与多个其他类型的设备互联,也无需定义多套功能码、报文编解码方案,只需使用对象来提供统一的服务。似乎看到了蓝天白云,鸟语花香,人人和睦相处,高效自动化工作的画面。