Net6/SuperSocket通過命令和命令過濾器實現服務端/客戶端雙工通信

十年河東,十年河西,莫欺少年窮

學無止境,精益求精

和上一節一致,通信數據結構如下:

        /// +-------+---+-------------------------------+
        /// |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加密/揭祕的證書的配置項目;
View Code

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;
        }
    }
}
View Code

包類型過濾器的主要作用是用於接收到包的解析,其提供了一些重載方法,也可以自定義自己的方法,這裏使用的是固定包頭長度類型

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("全局命令過濾器結束執行");
        }
    }
}
View Code

這裏需要解讀的是:

    [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();
        }
    }
}
View Code

需要說明的是,

上述代碼中註釋掉的代碼不能使用,因爲在命令中使用了參數session,session代表客戶端,一旦使用了上述註釋的代碼,則命令中session不生效,從而導致命令中發送消息給客戶端的方法因找不到客戶端發送失敗。

其他代碼無非就是註冊命令和全局命令過濾器

2、客戶端

客戶端和上一節使用的客戶端一致,代碼幾乎無變化,可參考:Net6/SuperSocket服務端和客戶端雙工交互, 自定義包類型及實現你的 PipelineFilter

項目整體測試如下:

客戶端:

服務器

 

@天才臥龍的博客

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章