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

news2025/4/15 9:50:04

这里写目录标题

  • 说明
  • 一、新建项目
  • 二、创建适配器
  • 三、创建服务器和客户端
    • 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资源。没别的意思,就是我的积分也没有了。得赚点积分。

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

二叉树的右视图[中等]

优质博文&#xff1a;IT-BLOG-CN 一、题目 给定一个二叉树的 根节点root&#xff0c;想象自己站在它的右侧&#xff0c;按照从顶部到底部的顺序&#xff0c;返回从右侧所能看到的节点值。 示例 1: 输入: [1,2,3,null,5,null,4] 输出: [1,3,4] 示例 2: 输入: [1,null,3] 输出…

强化学习第1天:强化学习概述

☁️主页 Nowl &#x1f525;专栏《机器学习实战》 《机器学习》 &#x1f4d1;君子坐而论道&#xff0c;少年起而行之 ​​ 文章目录 介绍 强化学习要素 强化学习任务示例 环境搭建&#xff1a;gym 基本用法 环境信息查看 创建智能体 过程可视化 完整代码 结语…

0基础学java-day15

一、泛型 1 泛型的理解和好处 1.1 看一个需求 【不小心加入其它类型&#xff0c;会导致出现类型转换异常】 package com.hspedu.generic;import java.util.ArrayList;/*** author 林然* version 1.0*/ public class Generic01 {SuppressWarnings("all")public st…

企业数字档案馆室建设指南

数字化时代&#xff0c;企业数字化转型已经成为当下各行业发展的必然趋势。企业数字化转型不仅仅是IT系统的升级&#xff0c;也包括企业内部各种文件、档案、合同等信息的数字化管理。因此&#xff0c;建设数字档案馆室也变得尤为重要。本篇文章将为您介绍企业数字档案馆室建设…

SpringMVC修炼之旅(2)基础入门

一、第一个程序 1.1环境配置 略 1.2代码实现 package com.itheima.controller;import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody;//定义…

初识MQ——消息队列技术选型

文章目录 同步和异步通讯同步通讯异步通讯 技术对比 同步和异步通讯 微服务间通讯有同步和异步两种方式&#xff1a; 同步通讯&#xff1a;就像打电话&#xff0c;需要实时响应。 异步通讯&#xff1a;就像发邮件&#xff0c;不需要马上回复。 两种方式各有优劣&#xff0c…

CCF编程能力等级认证GESP—C++1级—20230318

CCF编程能力等级认证GESP—C1级—20230318 单选题&#xff08;每题 2 分&#xff0c;共 30 分&#xff09;判断题&#xff08;每题 2 分&#xff0c;共 20 分&#xff09;编程题 (每题 25 分&#xff0c;共 50 分)每月天数长方形面积 答案及解析单选题判断题编程题1编程题2 单选…

SQL手工注入漏洞测试(Sql Server数据库)-墨者

———靶场专栏——— 声明&#xff1a;文章由作者weoptions学习或练习过程中的步骤及思路&#xff0c;非正式答案&#xff0c;仅供学习和参考。 靶场背景&#xff1a; 来源&#xff1a; 墨者学院 简介&#xff1a; 安全工程师"墨者"最近在练习SQL手工注入漏洞&#…

国内AI翘楚,看看有没有你心动的offer?

科技创新争占高地&#xff0c;AI领域各显神通。从一战成名的阿尔法狗到引起轩然大波的ChatGPT&#xff0c;我们早已卷入了一场没有硝烟的革命。前方世人看到的科技日新日异、岁月静好&#xff0c;后方是各大企业的绞尽脑汁、争先恐后。人工智能时代&#xff0c;AI是挡不住的时代…

Lebesgue积分及应用

Lebesgue积分及应用 文章目录 Lebesgue积分及应用一、Lebesgue测度和可测函数1.1 Riemann积分和Lebesgue积分1.2 直线上的Lebesgue测度【定义】外测度&#xff08;Outer Measure&#xff09;【定理】外测度的性质【定义】内测度【定义】可测、Lebesgue测度【定理】卡氏条件&…

Java注册并监听全局快捷键

背景 之前在博客中分享了SWT托盘功能, 随之带来一个问题, 当程序最小化后无法快速唤醒, 按照平时使用软件的思路, 自然想到了注册全局快捷键, 本文介绍使用java方式实现全局快捷键的注册. 方案 通过google,搜到一个现成的库: jintellitype, 使用maven可以直接引用, 非常方便…

C语言易错知识点八(整形与浮点型在内存中存储的实质)

整形与浮点型在内存中存储的实质 当我们在刷抖音或者其他短视频平台时&#xff0c;可能会时不时(总是&#xff0c;我相信大家肯定是不会被外表骗到的那一类人ヾ(●゜ⅴ゜)&#xff89;)刷到各种帅哥美女的视频&#xff0c;或者我们在学校里看到帅哥美女时&#xff0c;如果我们只…

NFC和蓝牙在物联网中有什么意义?如何选择?

#NFC物联网# #蓝牙物联网# 在物联网中&#xff0c;NFC和蓝牙有什么意义&#xff1f; NFC在物联网中代表近场通信技术。它是一种短距离、高频的无线通信技术&#xff0c;可以在近距离内实现设备间的数据传输和识别。NFC技术主要用于移动支付、电子票务、门禁、移动身份识别、防…

Vue2中v-html引发的安全问题

前言&#xff1a;v-html指令 1.作用&#xff1a;向指定节点中渲染包含html结构的内容。 2.与插值语法的区别&#xff1a; (1).v-html会替换掉节点中所有的内容&#xff0c;{{xx}}则不会。 (2).v-html可以识别html结构。 3.严重注意&#xff1a;v-html有安全性问题&#xff0…

STM32串口接收数据包(自定义帧头帧尾)

1、基本概述 本实验基于stm32c8t6单片机&#xff0c;串口作为基础且重要的外设&#xff0c;具有广泛的应用。本文主要理解串口数据包的发送与接收是如何实现的&#xff0c;重要的是理解程序的实现思路。 2、关键程序 定义好需要用到的变量&#xff1a; uint8_t rxd_buf[4];//…

湖南麒麟下默认使用串口输出系统日志

有时候为了调试方便&#xff0c;需要将系统日志通过CPU的串口进行输出&#xff0c;以下是针对至强E5V4处理器上安装湖南麒麟操作系统后将日志通过串口输出的配置。 首先在bios中打开串口重定向功能&#xff0c;这里的BIOS是AMI的BIOS 内部配置如下&#xff0c;波特率115200配置…

ESP32网络开发实例-发送邮件

发送邮件 文章目录 发送邮件1、邮件发送配置2、软件准备3、硬件准备4、代码实现本文将详细介绍在ESP32中如何使用SMTP协议发送邮件。 1、邮件发送配置 在本次实例中,我们将通过QQ邮箱向指定邮件地址发送邮件。 第一步,注册QQ邮箱 第二步,开启QQ邮箱SMTP/IMAP 服务: 生成…

一文搞懂Git版本控制系统

1. Git简介 当涉及到软件开发或协作时&#xff0c;版本管理是一个不可或缺的概念。无论你是一个独立开发者还是一个团队成员&#xff0c;都会遇到需要跟踪和管理代码变更的情况。这时候&#xff0c;Git作为一个强大而流行的版本控制系统就发挥着重要的作用。 Git&#xff08;读…

使用Pytoch实现Opencv warpAffine方法

随着深度学习的不断发展&#xff0c;GPU/NPU的算力也越来越强&#xff0c;对于一些传统CV计算也希望能够直接在GPU/NPU上进行&#xff0c;例如Opencv的warpAffine方法。Opencv的warpAffine的功能主要是做仿射变换&#xff0c;如果不了解仿射变换的请自行了解。由于Pytorch的图像…

每日一题:LeetCode-75. 颜色分类

每日一题系列&#xff08;day 12&#xff09; 前言&#xff1a; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f50e…