十年河東,十年河西,莫欺少年窮
學無止境,精益求精
和上一節一致,通信數據結構如下:
/// +-------+---+-------------------------------+ /// |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如下:
{ "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值,下面還會強調這一點
包類型管道過濾器如下:
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、定義命令及命令過濾器
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代碼如下:
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
項目整體測試如下:
客戶端:
服務器
@天才臥龍的博客