使用TouchSocket适配一个c++的自定义协议

这里写目录标题

  • 说明
  • 一、新建项目
  • 二、创建适配器
  • 三、创建服务器和客户端
    • 3.1 服务器
    • 3.2 客户端
    • 3.3 客户端发送
    • 3.4 客户端接收
    • 3.5 服务器接收与发送
  • 四、关于同步Send

说明

今天有小伙伴咨询我,他和同事(c++端)协商了一个协议,如果使用TouchSocket应该如何解析呢。协议大致如下。

我一看,这个协议还是比较复杂的,因为里面有package len、command len、data len三个部分是不固定的。

而且是不对称协议。即:请求包格式和响应包格式是不一样的(响应包多了一个Code)。

在这里插入图片描述
首先先解释一下数据格式。

  • head:两个字节,固定为“mr”。
  • package len:4字节,int32大端有符号类型。值等于head+package len+command len+command+(code)+data len+data。即所有数据长度。
  • command len:2字节,uint16大端无符号类型,标识command长度
  • command:不固定长度
  • code:仅响应时包含,一个字节
  • data len:4字节,int32大端有符号类型。标识data长度
  • data:不固定长度

看得人头皮发麻,不过虽然难。但是也属于固定包头的范畴。

因为如果我们把head和package len看成一个固定包头的话,固定头就是6。那command len、command、(code)、data len、data就相当于Body,body长度就是package len-6。然后可以再解析command len,command,data len data等。那么就可以使用模板解析“固定包头”数据适配器。

一、新建项目

首先,新建一个控制台项目。使用net6.0。然后nuget安装TouchSocket。此操作不会的小伙伴可以看看入门 TouchSocket入门和VS、Unity安装和使用Nuget包

二、创建适配器

在TouchSocket中,适配器就是负责对数据编解码的。具体可以看数据处理适配器。

首先新建一个类,名为MyBase。用于存放请求和响应的共同成员。结构大致如下:

class MyBase
{
    /// <summary>
    /// header固定为mr
    /// </summary>
    public string Header => "mr";
    public ushort CommandLen { get; protected set; }
    public byte[] Command { get; protected set; }
    public int DataLen { get; protected set; }
    public byte[] Data { get; protected set; }

    public void SetCommand(byte[] command)
    {
        this.Command = command;
        this.CommandLen = (ushort)(command == null ? 0 : command.Length);
    }

    public void SetData(byte[] data)
    {
        this.Data = data;
        this.DataLen = data == null ? 0 : data.Length;
    }
}

因为此协议是不对称协议,对于客户端,它需要发送Request,然后能解析Response。

对于服务器,它需要接受(解析)Request,响应(发送)Response。

那么我们先来写客户端适配器。

首先再新建一个类,名为MyResponsePackage。然后继承MyBase,同时实现IFixedHeaderRequestInfo。

操作原理可以看模板解析“固定包头”数据适配器

class MyResponsePackage : MyBase, IFixedHeaderRequestInfo
{
    public byte Code { get; private set; }
    private int m_length;

    public void SetCode(byte code)
    {
        this.Code = code;
    }

    int IFixedHeaderRequestInfo.BodyLength => this.m_length;

    bool IFixedHeaderRequestInfo.OnParsingBody(byte[] body)
    {
        try
        {
            //下标索引
            int index = 0;
            this.CommandLen = TouchSocketBitConverter.BigEndian.ToUInt16(body, index);
            index += 2;

            this.Command = body.Skip(index).Take(this.CommandLen).ToArray();
            index += this.CommandLen;

            this.Code = body[index];
            index += 1;

            this.DataLen = TouchSocketBitConverter.BigEndian.ToInt32(body, index);
            index += 4;

            this.Data = body.Skip(index).Take(this.DataLen).ToArray();
            index += this.DataLen;
            return true;
        }
        catch (Exception ex)
        {
            return false;
        }
    }

    bool IFixedHeaderRequestInfo.OnParsingHeader(byte[] header)
    {
        var headerStr = Encoding.ASCII.GetString(header, 0, 2);
        if (this.Header.Equals(headerStr))
        {
            this.m_length = TouchSocketBitConverter.BigEndian.ToInt32(header, 2) - 6;
            return true;
        }

        return false;
    }
}

然后再新建一个类,名为MyClientAdapter,继承CustomFixedHeaderDataHandlingAdapter,同时指定MyResponsePackage为泛型成员。

/// <summary>
/// 此适配器仅用于客户端。解析收到的<see cref="MyResponsePackage"/>
/// </summary>
internal class MyClientAdapter : CustomFixedHeaderDataHandlingAdapter<MyResponsePackage>
{
    public override int HeaderLength => 6;
    protected override MyResponsePackage GetInstance()
    {
        return new MyResponsePackage();
    }
}

至此,客户端的适配器解析就完成了。

现在我们来写服务器端适配器。

首先新建一个类,名为MyRequestPackage,同样继承MyBase,然后实现IFixedHeaderRequestInfo。

class MyRequestPackage : MyBase, IFixedHeaderRequestInfo
{
    private int m_length;

    int IFixedHeaderRequestInfo.BodyLength => this.m_length;

    bool IFixedHeaderRequestInfo.OnParsingBody(byte[] body)
    {
        try
        {
            //下标索引
            int index = 0;
            this.CommandLen = TouchSocketBitConverter.BigEndian.ToUInt16(body, index);
            index += 2;

            this.Command = body.Skip(index).Take(this.CommandLen).ToArray();
            index += this.CommandLen;

            this.DataLen = TouchSocketBitConverter.BigEndian.ToInt32(body, index);
            index += 4;

            this.Data = body.Skip(index).Take(this.DataLen).ToArray();
            index += this.DataLen;
            return true;
        }
        catch (Exception ex)
        {
            return false;
        }
    }

    bool IFixedHeaderRequestInfo.OnParsingHeader(byte[] header)
    {
        var headerStr = Encoding.ASCII.GetString(header, 0, 2);
        if (this.Header.Equals(headerStr))
        {
            this.m_length = TouchSocketBitConverter.BigEndian.ToInt32(header, 2) - 6;
            return true;
        }

        return false;
    }
}

然后新建一个类,名为MyServerAdapter。同样继承CustomFixedHeaderDataHandlingAdapter,指定MyRequestPackage为泛型成员。

/// <summary>
/// 此适配器仅用于服务器。主要功能是解析收到的<see cref="MyRequestPackage"/>
/// </summary>
internal class MyServerAdapter : CustomFixedHeaderDataHandlingAdapter<MyRequestPackage>
{
    public override int HeaderLength => 6;
    
    protected override MyRequestPackage GetInstance()
    {
        return new MyRequestPackage();
    }
}

至此。服务器适配器就写好了。

如果你的工作只是其中的一部分。那么你可以直接交差了。但是对我们来说还差点东西。

比如,对于客户端。我们应该怎么发送数据呢?按字节发送吗?那就太low了。

我们当然是要封装成对象来发送才比较好操作。

那么,让我们来改造一下MyRequestPackage。

首先,我们需要让MyRequestPackage再实现一个IRequestInfoBuilder的接口。该接口大概如下,其中Build方法,会指示成员应当如何构建数据。

/// <summary>
/// 指示<see cref="IRequestInfo"/>应当如何构建
/// </summary>
public interface IRequestInfoBuilder
{
    /// <summary>
    /// 构建数据时,指示内存池的申请长度。
    /// </summary>
    int MaxLength { get;}

    /// <summary>
    /// 构建对象到<see cref="ByteBlock"/>
    /// </summary>
    /// <param name="byteBlock"></param>
    void Build(ByteBlock byteBlock);
}

实现完大概这样。

class MyRequestPackage : MyBase, IRequestInfoBuilder, IFixedHeaderRequestInfo
{
    ...
    
    public int MaxLength => 1024 * 1024;//构建数据时,指示内存池的申请长度。也就是单个包可能达到的最大长度。避免内存池扩容带来消耗

    public int PackageLen
    {
        get
        {
            int len = 0;
            len += 2;//head
            len += 4;//PackageLen
            len += 2;//commandlen
            len += Command == null ? 0 : Command.Length; //Command
            len += 2;//data len
            len += this.Data == null ? 0 : this.Data.Length;//Data

            return len;
        }
    }
    public void Build(ByteBlock byteBlock)
    {
        byteBlock.Write(Encoding.ASCII.GetBytes(this.Header));
        byteBlock.Write(this.PackageLen, bigEndian: true);
        byteBlock.Write(this.CommandLen, bigEndian: true);
        byteBlock.Write(this.Command);
        byteBlock.Write(this.DataLen, bigEndian: true);
        byteBlock.Write(this.Data);
    }
}

然后此时,我们只需要在MyClientAdapter里面设置支持对象发送即可。

/// <summary>
/// 此适配器仅用于客户端。主要功能是包装发送的<see cref="MyRequestPackage"/>。解析收到的<see cref="MyResponsePackage"/>
/// </summary>
internal class MyClientAdapter : CustomFixedHeaderDataHandlingAdapter<MyResponsePackage>
{
    ...
     
    //因为MyRequestPackage已经实现IRequestInfoBuilder接口,所以可以使用True。
    public override bool CanSendRequestInfo => true;
}

此后,我们只需要发送MyRequestPackage对象,然后适配器内部会自动调用Build函数,然后执行发送。

同理,对于服务也需要这样做。

class MyResponsePackage : MyBase, IFixedHeaderRequestInfo, IRequestInfoBuilder
{
    ...
    public int PackageLen
    {
        get
        {
            int len = 0;
            len += 2;//head
            len += 4;//PackageLen
            len += 2;//commandlen
            len += Command == null ? 0 : Command.Length; //Command
            len += 1;//code
            len += 2;//data len
            len += this.Data == null ? 0 : this.Data.Length;//Data

            return len;
        }
    }

    public int MaxLength => 1024 * 1024;//构建数据时,指示内存池的申请长度。也就是单个包可能达到的最大长度。避免内存池扩容带来消耗

    public void Build(ByteBlock byteBlock)
    {
        byteBlock.Write(Encoding.ASCII.GetBytes(this.Header));
        byteBlock.Write(this.PackageLen, bigEndian: true);
        byteBlock.Write(this.CommandLen, bigEndian: true);
        byteBlock.Write(this.Command);
        byteBlock.Write(this.Code);
        byteBlock.Write(this.DataLen, bigEndian: true);
        byteBlock.Write(this.Data);
    }
}
/// <summary>
/// 此适配器仅用于服务器。主要功能是包装发送的<see cref="MyResponsePackage"/>。解析收到的<see cref="MyRequestPackage"/>
/// </summary>
internal class MyServerAdapter : CustomFixedHeaderDataHandlingAdapter<MyRequestPackage>
{
    ...
    //因为MyRequestPackage已经实现IRequestInfoBuilder接口,所以可以使用True。
    public override bool CanSendRequestInfo => true;
}

至此,基本的工作就完全完成了。

三、创建服务器和客户端

3.1 服务器

服务器应该使用MyServerAdapter适配器。其他配置可以看TcpService

var service = new TcpService();
service.Received = async (client, e) =>
{
    if (e.RequestInfo is MyRequestPackage requestPackage)
    {
        await Console.Out.WriteLineAsync("已收到MyRequestPackage");

        //构建响应
        var response=new MyResponsePackage();
        response.SetCode(200);
        response.SetCommand(new byte[] {0,1,2 });
        response.SetData(new byte[] {3,4,5 });
        await client.SendAsync(response);
    }
};

service.Setup(new TouchSocketConfig()//载入配置
    .SetListenIPHosts("tcp://127.0.0.1:7789", 7790)//同时监听两个地址
    .SetTcpDataHandlingAdapter(() => new MyServerAdapter())
    .ConfigureContainer(a =>//容器的配置顺序应该在最前面
    {
        a.AddConsoleLogger();//添加一个控制台日志注入(注意:在maui中控制台日志不可用)
    })
    .ConfigurePlugins(a =>
    {
        //a.Add();//此处可以添加插件
    }));

service.Start();//启动

3.2 客户端

客户端应该使用MyClientAdapter适配器。其他配置可以看TcpClient

var tcpClient = new TcpClient();
tcpClient.Received =async (client, e) =>
{
    //从服务器收到信息。但是一般byteBlock和requestInfo会根据适配器呈现不同的值。

    if (e.RequestInfo is MyResponsePackage responsePackage)
    {
        await Console.Out.WriteLineAsync("已收到MyResponsePackage");
    }
};

//载入配置
tcpClient.Setup(new TouchSocketConfig()
    .SetRemoteIPHost("127.0.0.1:7789")
    .SetTcpDataHandlingAdapter(()=>new MyClientAdapter())
    .ConfigureContainer(a =>
    {
        a.AddConsoleLogger();//添加一个日志注入
    })) ;

tcpClient.Connect();//调用连接,当连接不成功时,会抛出异常。

tcpClient.Logger.Info("客户端成功连接");

3.3 客户端发送

在发送时,我们可以直接发送一个MyRequestPackage的对象,因为适配器里面已经定义了如何Build。

var client = GetTcpClient();

var request = new MyRequestPackage();
request.SetCommand(new byte[] {0,1,2 });
request.SetData(new byte[] {3,4,5 });
client.Send(request);

3.4 客户端接收

客户端在接收时,适配器会做好解析,然后直接投递MyResponsePackage对象。

var tcpClient = new TcpClient();
tcpClient.Received =async (client, e) =>
{
    //从服务器收到信息。但是一般byteBlock和requestInfo会根据适配器呈现不同的值。

    if (e.RequestInfo is MyResponsePackage responsePackage)
    {
        await Console.Out.WriteLineAsync("已收到MyResponsePackage");
    }
};

3.5 服务器接收与发送

同理,服务器接收时,适配器会解析投递MyRequestPackage,发送时直接发送MyResponsePackage即可。

var service = new TcpService();
service.Received = async (client, e) =>
{
    if (e.RequestInfo is MyRequestPackage requestPackage)
    {
        await Console.Out.WriteLineAsync("已收到MyRequestPackage");

        //构建响应
        var response=new MyResponsePackage();
        response.SetCode(200);
        response.SetCommand(new byte[] {0,1,2 });
        response.SetData(new byte[] {3,4,5 });
        await client.SendAsync(response);
    }
};

四、关于同步Send

同步Send,就是发送一个数据,然后等待响应,详情可以看Tcp同步请求

但是此处有个小问题,就是waitClient.SendThenReturn函数并没有发送对象的实现。那么我们就需要手动Build数据。

同时只能用SendThenResponse,而不是SendThenReturn。

var client = GetTcpClient();

var request = new MyRequestPackage();
request.SetCommand(new byte[] { 0, 1, 2 });
request.SetData(new byte[] { 3, 4, 5 });
client.Send(request);

var waitingClient = client.CreateWaitingClient(new WaitingOptions());
var responsedData = waitingClient.SendThenResponse(request.BuildAsBytes());
if (responsedData.RequestInfo is MyResponsePackage responsePackage)
{
    //to do
}

结束,看起来很麻烦的协议,实际上也可以很优雅的解决。

最后,完整代码我上传到 csdn资源。没别的意思,就是我的积分也没有了。得赚点积分。

如果大家下载困难,不妨把文中代码复制一下也可以,因为全部代码也在这里。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/223581.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

http和https的区别有哪些

目录 HTTP&#xff08;HyperText Transfer Protocol&#xff09; HTTPS&#xff08;HyperText Transfer Protocol Secure&#xff09; 区别与优势 应用场景 未来趋势 当我们浏览互联网时&#xff0c;我们经常听到两个常用的协议&#xff1a;HTTP&#xff08;HyperText Tra…

【LeetCode刷题-链表】--92.反转链表II

92.反转链表II /*** Definition for singly-linked list.* public class ListNode {* int val;* ListNode next;* ListNode() {}* ListNode(int val) { this.val val; }* ListNode(int val, ListNode next) { this.val val; this.next next; }* }*/ cla…

项目中使用之Maven BOM

✅作者简介&#xff1a;大家好&#xff0c;我是Leo&#xff0c;热爱Java后端开发者&#xff0c;一个想要与大家共同进步的男人&#x1f609;&#x1f609; &#x1f34e;个人主页&#xff1a;Leo的博客 &#x1f49e;当前专栏&#xff1a; 工具教程 ✨特色专栏&#xff1a; MyS…

MYSQL全语法速查(含示例)

文章目录 1.从简单的查询开始查找所有记录(SELECT *)查找记录中的所有登录名(SELECT)查找登录名为admin的密码(WHERE)查找电话号码非空的记录(IS NOT NULL)查找所在城市为北京或者用户名字是李四的记录(OR)查找所在城市为北京并且用户名字是张三的记录(AND)查找用户名字是李四或…

【节日专栏】Python海龟绘制圣诞树代码

圣诞节就要到了&#xff0c;有没有要送给朋友礼物的程序员呢&#xff1f; import turtledef tree(d, s):if d < 0:returnturtle.forward(s)tree(d - 1, s * 0.8)turtle.right(120)tree(d - 3, s * 0.5)turtle.right(120)tree(d - 3, s * 0.5)turtle.right(120)turtle.backw…

4.10 构建onnx结构模型-Softmax

前言 构建onnx方式通常有两种&#xff1a; 1、通过代码转换成onnx结构&#xff0c;比如pytorch —> onnx 2、通过onnx 自定义结点&#xff0c;图&#xff0c;生成onnx结构 本文主要是简单学习和使用两种不同onnx结构&#xff0c; 下面以 Softmax 结点进行分析 方式 方法一…

神通数据库备份

备份策略&#xff1a; 每月1、15日00:00全量物理备份 每周一凌晨02:00差异物理备份 其余每天凌晨03:00增量物理备份 使用的工具&#xff1a; 数据库维护工具 环境&#xff1a;windows11 step1&#xff1a;创建全量/差异/增量物理备份调度任务 其中&#xff1a; 备份模式&…

C语言之多维数组

所谓多维数组就是以多个数组为单位组成的数组&#xff0c;即元素本身是数组的数组。下面我们来学习多维数组的基本知识&#xff1a; 多维数组 上一节学习的数组都是int型或double型等单一类型&#xff0c;实际上数组本身也可以作为组成数组的元素。 以数组作为元素的数组时二…

HarmonyOS学习--创建和运行Hello World

创建和运行Hello World 打开DevEco Studio&#xff0c;在欢迎页单击Create Project&#xff0c;创建一个新工程。根据工程创建向导&#xff0c;选择创建Application应用或Atomic Service元服务。选择“Empty Ability”模板&#xff0c;然后单击Next。关于工程模板的介绍和支持…

C++ Core Guidelines解析 ( 好书推荐 )

C Core Guidelines是Bjarne和 Herb Sutter发起编写的一个开源项目&#xff0c;汇聚了 C社区多年来积累的宝贵经验&#xff0c;是非常全面的编程最佳实践指导&#xff0c;包括代码风格、函数、类、错误处理、性能优化等&#xff0c;可以说是C社区的集大成者。用Effective Modern…

Python函数

1.函数 1.1 函数概述 函数定义和优势 不同形状正方形打印 # 2个 for i in range(0, 2):for j in range(0, 2):print("*", end"")print() # 3个 for i in range(0, 3):for j in range(0, 3):print("*", end"")print() # 4个 for i …

NET Core迁移前工作

前段时间迁移.NET Core做了大量的试水和评估&#xff0c;今天整理一下分享给大家。大致有以下几个部分&#xff1a; 1. .NET Core的由来 2. 为什么要迁移.NET Core 3. .NET Core3.X主要特性 4. .NET Standard和.NET Core 5. .NET Core Roadmap&版本选择 接下来&#…

记录一次vscode markdown的图片路径相关插件学习配置过程

插件及说明查找过程 csdn搜索markdown图片路径&#xff0c;找到关于这一款插件的回答。打开vscode拓展搜索Paste Image这款插件&#xff0c;看到下载量挺高的&#xff0c;应该不赖。 点击仓库,进入该插件开源的github仓库,查看README文件阅读说明. 淡然在Vscode 插件项目下的细…

minio的k8s的单机部署

minio的k8s的单机部署 apiVersion: apps/v1 kind: Deployment metadata:name: minionamespace: itshare spec:replicas: 1selector:matchLabels:app: miniotemplate:metadata:labels:app: miniospec:containers:- name: minioimage: minio/minio:RELEASE.2022-10-15T19-57-03Z…

微服务开发:断路器详解

微服务是目前业界使用的最重要的实现方面。通过使用微服务架构&#xff0c;开发人员可以消除他们以前在单体应用程序中遇到的许多问题。展望未来&#xff0c;人们开始在微服务中搜索和采用各种模式。大多数时候&#xff0c;新模式的产生是为了解决另一个模式中出现的常见问题。…

MySQL Server 层和引擎层是如何交互的

Server 层、引擎层、BufferPool、磁盘间的关系 大体来说&#xff0c; MySQL可以分为Server层和存储引擎层两部分。 1&#xff09;Server 层&#xff1a;Server 层包括连接器、查询缓存、分析器、优化器、执行器等&#xff0c;涵盖MySQL的大多数核心服务功能&#xff0c;以及所…

深信服行为管理AC设置禁止用户使用向日葵等远程软件

需求&#xff1a;在特定的时间内禁止内外网用户使用向日葵、todesk等远程软件&#xff1b;只禁止使用专业的远程软件&#xff0c;内网的ssh、telnet、RDP需要正常放行 AC版本&#xff1a;AC13.0.62.001 Build20221107 通过访问权限策略来控制 1、行为管理→访问权限策略→新…

基于springboot+vue篮球联盟管理系统源码

&#x1f345; 简介&#xff1a;500精品计算机源码学习 &#x1f345; 欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; 文末获取源码 目录 一、以下学习内容欢迎交流&#xff1a; 二、文档资料截图&#xff1a; 三、项目技术栈 四、项目运行图 背景&#xff1a; 篮球运…

「智慧城市」这一概念科学吗?还是炒作?

智慧城市是一个综合性的概念&#xff0c;它利用信息技术和创新概念&#xff0c;将城市的各个系统和服务集成起来&#xff0c;以提升城市运行效率、优化城市管理和服务&#xff0c;改善市民的生活质量。 具体来说&#xff0c;智慧城市涵盖了许多领域&#xff0c;包括城市规划、建…