在网络通讯应用中直接操作数据流是比较繁琐的事情,毕竟在业务层面处理的都是对象化消息;为了让网络数据操作变得更友好直观,一般都会引用序列化组件来处理网络流和对象之前的转换工作;在这里介绍组件如何使能Protobuf进行数据交互通讯。
协议定义
组件使用对象处理就不同之前HelloWorld示例一样简单操作流就可以,在这里需要进一步封装一个简单的应用协议。
|-----------------------------------------------------------------|
|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7 |
|----------------------------------------------------------------|
|Probobuf数据流长度,占4字节 |
|----------------------------------------------------------------|
|Protobuf数据流 |
|----------------------------------------------------------------|
以上是一个简单的应用协议,消息头4字节描述消息的长度,对应长度的数据则是Protobuf序列化对象数据。但这样的协议用在Protobuf上还是有问题,那就是无法知道这些数据对应于那个数据类型对象;所以还需要调整一下
|----------------------------------------------------------------|
|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7 |
|----------------------------------------------------------------|
| Probobuf数据流长度,占4字节 |
|----------------------------------------------------------------|
| 消息类型,占4字节 |
|----------------------------------------------------------------|
| Protobuf数据流 |
|----------------------------------------------------------------|
在序列化数据流前添加了4字节用于描述对应的消息类型,对于这个类型其实可以使用2字节来描述,毕竟一个无符号的短整型可以描述6万多个消息类型了。如果想表达得更友好些可以用字符来描述消息类型,只是字符所占有的空间比较多些。
协议分析器
组件提供了FixedHeaderPacket抽象协议分析器来处理这种简单应用协议,只要继承重写两个方法即可完成。
public class ProtobufPacket : FixedHeaderPacket{static ProtobufPacket(){TypeHeaer.Register(typeof(ProtobufPacket).Assembly);}public static CustomTypeHeader TypeHeaer { get; private set; } = new CustomTypeHeader(MessageIDType.INT);public override IPacket Clone(){return new ProtobufPacket();}protected override object OnRead(ISession session, PipeStream stream){Type type = TypeHeaer.ReadType(stream);return ProtoBuf.Meta.RuntimeTypeModel.Default.Deserialize(type, stream, null, null, CurrentSize - 4);}protected override void OnWrite(ISession session, object data, PipeStream stream){TypeHeaer.WriteType(data, stream);ProtoBuf.Meta.RuntimeTypeModel.Default.Serialize(stream, data);}}
分析器定义CustomTypeHeader用于管理消息类型和数据的转换,并在协议分析器静态初始化的时候把当前程序集中所有消息映射关系加载进去。
消息编码
OnWrite方法负责消息编码,写入消息类型,然后再写入Protobuf序列化后的数据流。
消息解码
OnRead方法负责消息解码, 先读取消息类型,然后再拿数据流中的数据反序列化到相应的对象。正常做法这里需要判断没有获取到正常的消息类型,没有则抛对应的异常。
通过以上简单的扩展,一个基于Protobuf的通讯应用协议就扩展完成。
定义消息
在这个示例中使用了Protobuf.net组件,用这组件在定义消息的时候还是要遵循某些规则的,接下来看一下消息的定义:
[MessageType(1)][ProtoContract]public class Register{[ProtoMember(1)]public string Name { get; set; }[ProtoMember(2)]public string EMail { get; set; }[ProtoMember(3)]public string Password { get; set; }}[MessageType(2)][ProtoContract]public class RegisterResult{[ProtoMember(1)]public bool Success { get; set; }[ProtoMember(2)]public string Message { get; set; }}
使用Protobuf.net组件需要用ProtoContract来描述一个序列化消息,并用ProtoMember来描述对应的成员属性。MessageType特性用于描述消息类型对应关系。
制定服务
针对有协议解释对象的TCP服务在处理上和基础的hello world服务差不多,只是重写接管的消息处理方法有所不同。
class Program : ServerHandlerBase
{static IServer mServer;static void Main(string[] args){mServer = SocketFactory.CreateTcpServer(new Program(), new ProtobufPacket());mServer.Options.LogLevel = LogType.Info; //mServer.Options.DefaultListen.Port=9090//mServer.Options.DefaultListen.SSL = true;//mServer.Options.DefaultListen.CertificateFile = "ssl.pfx";//mServer.Options.DefaultListen.CertificatePassword = "123456";mServer.Open();System.Threading.Thread.Sleep(-1);}protected override void OnReceiveMessage(IServer server, ISession session, object message){RegisterResult result = new RegisterResult();if (message is Register register){server.Log(LogType.Info, session, $"{session.RemoteEndPoint} 注册 {register.Name}");result.Success = true;result.Message = $"{register.Name}你已注册成功,注册邮件地址:{register.EMail}";}else{result.Success = false;result.Message = "非法请求";}session.Send(result);base.OnReceiveMessage(server, session, message);}
}
通过重写OnReceiveMessage方法来接管消息处理,在SSL处理上和之前的hello world示例是一样的配置方式。
客户端访问
组件提供了AwaiterClient对象来实现基于async/await的消息接收处理,所以在这个示例中就没有使用AsyncTcpClient这个类来处理了(AsyncTcpClient是基于事件方式接收消息在处理上相对麻烦一些)。
class Program
{static async Task Main(string[] args){var client = new AwaiterClient("localhost", 9090, new ProtobufClientPacket());while (true){Register register = new Register();Console.Write("Enter you name:");register.Name = Console.ReadLine();Console.Write("Enter you email:");register.EMail = Console.ReadLine();Console.Write("Enter you password:");register.Password = Console.ReadLine();var result = await client.ReceiveFrom<RegisterResult>(register);Console.WriteLine($"{result.Success} {result.Message}");Console.WriteLine("-".PadLeft(100, '-'));}}
}
以上代码是向服务端发送一个注册信息并等待返回输出;通过AwaiterClient的ReciveFrom<T>方法可以在发送消息后等待一个消息返回;以下示例是运行显示效果。
小结
虽然传递消息比起直接数据流操作方便很多,但如果针对每个逻辑都写请求和响应其工作也是相当繁琐的。组件有专门针对接口远程调用扩展组件
https://github.com/IKende/XRPC
下载
链接:https://pan.baidu.com/s/118Qal6kJKZ6T9tglaZT3bw
提取码:1b84
【BeetleX通讯框架代码详解】
BeetleX
开源跨平台通讯框架(支持TLS)
轻松实现高性能:tcp、http、websocket、redis、rpc和网关等服务应用
https://beetlex.io