Net6/SuperSocket通过命令和命令过滤器实现服务端/客户端双工通信
十年河东,十年河西,莫欺少年穷
学无止境,精益求精
参考:https://www.cnblogs.com/xhubobo/p/14721463.html
和上一节一致,通信数据结构如下:
/// +-------+---+-------------------------------+ /// |request| l | | /// | type | e | request body | /// | (1) | n | | /// | |(2)| | /// +-------+---+-------------------------------+
关于superSocket命令及命令过滤器相关的基础知识大家可参考:https://docs.supersocket.net/v2-0/zh-CN/Command-and-Command-Filter
需要说明的是,在使用命令及命令过滤器之前,系统代码其他地方不能使用带有session参数的方法,譬如:
不能使用
.UsePackageHandler(async (session, package) => 和 .UseSessionHandler(async (session) =>
之所以不能使用这些方法,是因为session代表通信管道,一个session对象意味着一个客户端,如果系统多处出现session参数,则意味着只有一个session对应客户端,其他session是无效的,如果使用无效的session发送消息给客户端,客户端无法收到
本篇为双工通信,因此分为服务端和客户端
1、服务端构造
1.1、新建控制台应用程序
并引入如下Nuget包
1.2、新增配置文件
superSocket默认从appsettings.json中读取配置,这点不清楚可参考:https://docs.supersocket.net/v2-0/zh-CN/Start-SuperSocket-by-Configuration
appsettings.json如下:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
{ "serverOptions": { "name": "GameMsgServer", "listeners": [ { "ip": "Any", "port": "4040" }, { "ip": "127.0.0.1", "port": "8040" } ] } } //配置项目 //name: 服务器的名称; //maxPackageLength: 此服务器允许的最大的包的大小; 默认4M; //receiveBufferSize: 接收缓冲区的大小; 默认4k; //sendBufferSize: 发送缓冲区的大小; 默认4k; //receiveTimeout: 接收超时时间; 微秒为单位; //sendTimeout: 发送超时的事件; 微秒为单位; //listeners: 服务器的监听器; //listeners/*/ip: 监听IP; Any: 所有 ipv4 地址, IPv6Any: 所有 ipv6 地址, 其它具体的IP地址; //listeners/*/port: 监听端口; //listeners/*/backLog: 连接等待队列的最大长度; //listeners/*/noDelay: 定义 Socket 是否启用 Nagle 算法; //listeners/*/security: None/Ssl3/Tls11/Tls12/Tls13; 传输层加密所使用的TLS协议版本号; //listeners/*/certificateOptions: 用于TLS加密/揭秘的证书的配置项目;
1.3、新增包类型及包类型管道过滤器
需要注意的是,在使用命令进行通信的包类型必须继承自IKeyedPackageInfo<T>
包类型如下:
public class MyPackage: IKeyedPackageInfo<int> { public int Key { get; set; } public int BodyLength { get; set; } public string Body { get; set; } }
这里面的Key值对应命令指定的key值,下面还会强调这一点
包类型管道过滤器如下:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
using MySupersocket.Package; using SuperSocket.ProtoBase; using System; using System.Buffers; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace MySupersocket.Filters { /// <summary> /// 过滤器 /// </summary> public class MyPackageFilter : FixedHeaderPipelineFilter<MyPackage> { /// +-------+---+-------------------------------+ /// |request| l | | /// | type | e | request body | /// | (1) | n | | /// | |(2)| | /// +-------+---+-------------------------------+ public MyPackageFilter() : base(3)// // 包头的大小是3字节,所以将3传如基类的构造方法中去 { } // 从数据包的头部返回包体的大小 protected override int GetBodyLengthFromHeader(ref ReadOnlySequence<byte> buffer) { var reader = new SequenceReader<byte>(buffer); reader.Advance(1); // skip the first byte reader.TryReadBigEndian(out short len); return len; } // 将数据包解析成 MyPackage 的实例 // 将数据包解析成 MyPackage 的实例 protected override MyPackage DecodePackage(ref ReadOnlySequence<byte> buffer) { var package = new MyPackage(); var reader = new SequenceReader<byte>(buffer); reader.TryRead(out byte packageKey); package.Key = packageKey; reader.Advance(2); // skip the length // var sas = buffer.ToArray(); package.BodyLength = BitConverter.ToUInt16(sas.Skip(1).Take(2).Reverse().ToArray()); var body = string.Empty;//功能码 foreach (var item in sas) { body += item.ToString("x2"); } body = body.ToLower(); package.Body = body; return package; } } }
包类型过滤器的主要作用是用于接收到包的解析,其提供了一些重载方法,也可以自定义自己的方法,这里使用的是固定包头长度类型
1.4、定义命令及命令过滤器
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
using MySupersocket.Package; using SuperSocket; using SuperSocket.Command; using SuperSocket.ProtoBase; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace MySupersocket.Command { /// <summary> /// 命令基类 /// </summary> public abstract class BaseCommand : IAsyncCommand<MyPackage> { /// <summary> /// 进行适配的命令后,会立即执行该方法 /// </summary> /// <param name="session"></param> /// <param name="package"></param> /// <returns></returns> public async ValueTask ExecuteAsync(IAppSession session, MyPackage package) { var result = GetResult(package); //发送消息给客户端 await session.SendAsync(result); } /// <summary> /// 抽象方法,用于重写 /// </summary> /// <param name="package"></param> /// <returns></returns> public abstract byte[] GetResult(MyPackage package); } /// <summary> /// 接收key值为0x01的 /// </summary> [SendKey01CommandFilter] [Command(Key = 0x01, Name = "SendKey01")] public class SendKey01 : BaseCommand { public override byte[] GetResult(MyPackage package) { return new byte[] { 0x01, 0x00, 0x03, 0x1a, 0x1b, 0x1c }; } } [SendKey02CommandFilter] [Command(Key = 0x02, Name = "SendKey02")] public class SendKey02 : BaseCommand { public override byte[] GetResult(MyPackage package) { return new byte[] { 0x02, 0x00, 0x03, 0x2a, 0x2b, 0x2c }; } } [SendKey03CommandFilter] [Command(Key = 0x03, Name = "SendKey03")] public class SendKey03 : BaseCommand { public override byte[] GetResult(MyPackage package) { return new byte[] { 0x03, 0x00, 0x03, 0x3a, 0x3b, 0x3c }; } } // public class SendKey01CommandFilterAttribute : CommandFilterAttribute { public override bool OnCommandExecuting(CommandExecutingContext commandContext) { Console.WriteLine("SendKey01命令过滤器开始执行"); Console.WriteLine("进入SendKey01方法之前执行..."); return true; } public override void OnCommandExecuted(CommandExecutingContext commandContext) { Console.WriteLine("进入SendKey01方法之后执行..."); Console.WriteLine("SendKey01命令过滤器执行结束"); } } // public class SendKey02CommandFilterAttribute : CommandFilterAttribute { public override bool OnCommandExecuting(CommandExecutingContext commandContext) { Console.WriteLine("SendKey02命令过滤器开始执行"); Console.WriteLine("进入SendKey02方法之前执行..."); return true; } public override void OnCommandExecuted(CommandExecutingContext commandContext) { Console.WriteLine("进入SendKey02方法之后执行..."); Console.WriteLine("SendKey02命令过滤器执行结束"); } } // public class SendKey03CommandFilterAttribute : CommandFilterAttribute { public override bool OnCommandExecuting(CommandExecutingContext commandContext) { Console.WriteLine("SendKey03命令过滤器开始执行"); Console.WriteLine("进入SendKey03方法之前执行..."); return true; } public override void OnCommandExecuted(CommandExecutingContext commandContext) { Console.WriteLine("进入SendKey03方法之后执行..."); Console.WriteLine("SendKey03命令过滤器执行结束"); } } /// <summary> /// 全局命令过滤器 /// </summary> public class GlobalCommandFilterAttribute : CommandFilterAttribute { public override bool OnCommandExecuting(CommandExecutingContext commandContext) { Console.WriteLine("全局命令过滤器开始执行"); return true; } public override void OnCommandExecuted(CommandExecutingContext commandContext) { Console.WriteLine("全局命令过滤器结束执行"); } } }
这里需要解读的是:
[SendKey01CommandFilter] [Command(Key = 0x01, Name = "SendKey01")]
SendKey01CommandFilter 为命令过滤器,必须继承自:CommandFilterAttribute,其提供两个方法
OnCommandExecuting 为进入命令之前执行
OnCommandExecuted 为命令执行完后执行
[Command(Key = 0x03, Name = "SendKey03")] 为命令执行key值,这里指定的key值为十六进制0x03,其对应包类型MyPackage中的key,MyPackage中key定义为int是因为C#会将byte自动转为int,当然,你可以将命令和包类型的key值都是用字符串,在包类型中将byte类型的key值转为字符串即可。
1.5、Program.cs代码如下:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
using System; using System.Text; using System.Threading.Tasks; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using MySupersocket.Command; using MySupersocket.Filters; using MySupersocket.Package; using Newtonsoft.Json; using SuperSocket; using SuperSocket.Command; using SuperSocket.ProtoBase; namespace MySupersocket // Note: actual namespace depends on the project name. { internal class Program { static async Task Main(string[] args) { var host = SuperSocketHostBuilder .Create<MyPackage, MyPackageFilter>() //.UsePackageHandler(async (session, package) => //{ // byte[] result = new byte[1]; // var GnmCode = string.Empty;//功能码 // foreach (var item in new List<byte>() { package.Key }) // { // GnmCode += item.ToString("x2"); // } // switch (GnmCode.ToLower()) // { // case ("01"): // result = new byte[] { 0x01, 0x00, 0x03, 0x1a, 0x1b, 0x1c }; // break; // case ("02"): // result = new byte[] { 0x02, 0x00, 0x03, 0x2a, 0x2b, 0x2c }; // break; // case ("03"): // result = new byte[] { 0x03, 0x00, 0x03, 0x3a, 0x3b, 0x3c }; // break; // } // await session.SendAsync(new ReadOnlyMemory<byte>(result)); //}) //.UseSessionHandler(async (session) => //{ // Console.WriteLine($"session connected: {session.RemoteEndPoint}"); // //发送消息给客户端 // var msg = $@"Welcome to TelnetServer: {session.RemoteEndPoint}"; // await session.SendAsync(Encoding.UTF8.GetBytes(msg + "\r\n")); //}, async (session, reason) => //{ // await Task.Delay(0); // Console.WriteLine($"session {session.RemoteEndPoint} closed: {reason}"); //}) .UseCommand((commandOptions) => { // 一个一个的注册命令 commandOptions.AddCommand<SendKey01>(); commandOptions.AddCommand<SendKey02>(); commandOptions.AddCommand<SendKey03>(); commandOptions.AddGlobalCommandFilter<GlobalCommandFilterAttribute>(); }) .ConfigureLogging((hostCtx, loggingBuilder) => { loggingBuilder.AddConsole(); }).Build(); await host.RunAsync(); Console.Read(); } } }
需要说明的是,
上述代码中注释掉的代码不能使用,因为在命令中使用了参数session,session代表客户端,一旦使用了上述注释的代码,则命令中session不生效,从而导致命令中发送消息给客户端的方法因找不到客户端发送失败。
其他代码无非就是注册命令和全局命令过滤器
2、客户端
客户端和上一节使用的客户端一致,代码几乎无变化,可参考:Net6/SuperSocket服务端和客户端双工交互, 自定义包类型及实现你的 PipelineFilter
项目整体测试如下:
客户端:
服务器
@天才卧龙的博客