Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[功能请求] 添加标准通用数据包的接口 #732

Open
bunnyi116 opened this issue Aug 25, 2023 · 8 comments
Open

[功能请求] 添加标准通用数据包的接口 #732

bunnyi116 opened this issue Aug 25, 2023 · 8 comments
Labels
Feature New feature good first issue Good for newcomers

Comments

@bunnyi116
Copy link

bunnyi116 commented Aug 25, 2023

原因

在编写独立的Http与WebSocket适配器的API的时候,发现需要重复写的东西很多,因为Http请求接口路径问题,得每个为他编写方法,为了能统一调用适配器API方法,为此建议制定标准数据包格式接口以便调用。至于其他的适配器我也不清楚,没用过

这样做的好处

可以减少编写适配器的接口的工作量,例:http、ws通过适配器接口去实现一个ResponsePacket SendPacket(RequestPacket packet)的方法,然后将已实现的适配器派生对象放入一个适配器管理器中的列表中,然后适配器管理器的也实现ResponsePacket SendPacket(RequestPacket packet)方法,不过这个方法是遍历适配器列表进行发送(如果适配器模式允许发送的话则发送),之后我准备实现发送消息API,那我只需要在适配器管理器(或新建一个专门实现API类)中实现一个叫做SendFriendMessage方法,然后调用适配器管理器的SendPacket即可实现不同适配器一起发送。
无标题

实现(mirai-api-http)

  1. 在Http适配器中,添加一个POST请求接口,该POST请求为标准数据包格式。例:http://127.0.0.1/general
  2. 在服务端响应返回给客户端的数据包需要携带RestfulResult信息,也就是状态码和状态消息,如果是成功状态可以忽略

数据包格式

RequestPacket(请求数据包)

  1. syncId(同步标识,根据适配器需求而定,但标准中必须有,他可以为null不进行序列化)
  2. command(请求的命令也就是请求路径)
  3. data(请求的数据)

ResponsePacket(响应数据包)

  1. syncId(同步标识,根据适配器需求而定,但标准中必须有,他可以为null不进行序列化)
  2. code(状态代码,所有响应的数据包都需要实现,当成功状态时,序列化可以忽略,个人觉得不管成功与否都需要携带)
  3. msg(状态消息,所有响应的数据包都需要实现,当成功状态时,序列化可以忽略)
  4. data(响应的数据,如果响应没有数据,只是一个确认是否成功,那么它可以为null不进行序列化)
@ryoii ryoii added good first issue Good for newcomers Feature New feature labels Sep 5, 2023
@bunnyi116
Copy link
Author

bunnyi116 commented Sep 10, 2023

顺便提一句:这不仅仅针对http,也适用于其他适配器。

mirai-api-http有多种适配器且每个适配器都可以独立存在,为了开发者更好对接mirai-api-http,需要为开发者编写一个通用请求与响应的接口(标准的传出传入的数据包),让开发者更好的管理适配器API,避免重复编写API方法。

@bunnyi116
Copy link
Author

建议清单

  1. 制定一个标准的请求与响应数据包格式
  2. 当客户端连接服务端并认证成功后(我觉得http中 verify 与 bind 请求可以进行合并,有点多余)

应当响应数据

https://docs.mirai.mamoe.net/mirai-api-http/adapter/HttpAdapter.html#%E8%8E%B7%E5%8F%96%E4%BC%9A%E8%AF%9D%E4%BF%A1%E6%81%AF

{
  "syncId": "", 
  "code": 0,
  "msg": "",
  "data": {
    "sessionKey": "YourSessionKey",
    "qq": {
      "id": 1234567890,
      "nickname": "",
      "remark": ""
    }
  }
}

不应该响应格式

{
  "syncId": "",
  "data": {
    "code": 0,
    "session": "YourSessionKey"
  }
}
  1. 【客户端】所有请求的数据都通过RequestPacket进行包装在发送
  2. 【服务端】所有响应的数据都通过ResponsePacket(RestfulResult)进行包装在发送
  3. 增加心跳机制,这个心跳包用于服务端和服务端互相感知连接状态,如果一定时间没有进行交互(比如:1分钟一次,然后更新上一次心跳的时间),那么双方就连接然后释放资源。
  4. 退出账号后再登录,之前的会话会丢失Bot实例,所以在登录账号成功后Bot触发上线事件后,请 mirai-api-http 更新当前会话列表对应账号的Bot实例,避免会话对应的Bot实例映射丢失,从而导致客户端接收不到信息。
  5. 为了更好的区分新的协议,可以在 /about 请求中,增加协议版本号字段(protocol version),采用整数形式,这样客户端就不需要进行版本字符串分割检查版本号,只需要根据协议版本号进行调整,比如v1 Api的版本协议版本就命名为1,故此类推
  6. Socket文件上传文件特殊接口。针对于socket处理长连接类型的适配器(WebSocket),会阻塞Socket的情况,可以考虑添加新连接,专门用于上传文件的连接接口,拿WebSocket举例,目前我有个ws主连接,然后准备上传一个文件\图片,客户端可以使用当前的会话key主动与ws服务端创建一个新连接,该连接通道为upload,然后传入参数文件名,文件字节大小,md5,等等...,然后通过二进制流直接传输,服务端根据文件的字节大小进行接收,当传输完成后,服务端给客户端一个成功响应。

数据包格式(这边我是以客户端视角命名)

请求数据包RequestPacket

  1. syncId(用于同步,可为null不进行序列化,根据适配器情况而定)
  2. command(请求的命令,我觉得没必要有子命令,跟http请求路径相同就行)
  3. data(请求的数据)

响应数据包ResponsePacket(mirai-api-http中叫做RestfulResult)

  1. syncId(用于同步,可为null不进行序列化,根据适配器情况而定)
  2. code (必须携带,这里使用枚举类进行统一约束,序列化的时候转换成int就行)
  3. msg (这个其实可以省略为null不进行序列化,因为有状态代码了,这个可以当做一个附加状态信息)
  4. data (数据的对象,用于承载通用数据,如果没有数据,则为null不进行序列化)

@bunnyi116
Copy link
Author

bunnyi116 commented Sep 21, 2023

syncId 我觉得服务端主动推送直接为空或null就行,不用-1配置保留字段,然后客户端请求API的时候,syncId的值长度必须大于0,然后服务端校验一下客户端的请求syncId的长度 <= 0 则服务端直接响应错误表示拒绝\syncId值不正确,避免客户端传入的syncId空或null值与服务端主动推送syncId冲突

@ryoii
Copy link
Collaborator

ryoii commented Oct 10, 2023

HTTP body部分的格式和 Websocket content 部分的格式是一样的,应该不存在说需要重复实现的问题

@bunnyi116
Copy link
Author

数据格式都差不多,就是不规范,有很多不同的数据结构。

然后我说重复的是请求接口,因为http接口不同方法单独实现,在接口管理上的时候是很麻烦的,需要一个通用的请求接口。不然http ws api需要很多单独方法,然后接口管理里面又要实现适配器api调用,换句话说套娃。

@ryoii
Copy link
Collaborator

ryoii commented Oct 10, 2023

不规范当然是有的,但已经发布的接口,数据格式不是说随随便便就直接改的

不同 adapter 之间请求和响应在实现上用的都是同一个实体,结构理论上都是一样的,不存在说这部分在不同 adapter 之间要重复实现。你所说的很多单独的、难以管理的方法,能不能提供一个例子出来

@bunnyi116
Copy link
Author

bunnyi116 commented Oct 11, 2023

public interface IApi
{
    void GetFriendList();
    // 其他...
}

public interface IAdapter : IApi
{
    AdapterMode Mode { get; }
}

public enum AdapterMode
{
    Receive, Send
}

internal class HttpAdapter : IAdapter, IApi
{
    public AdapterMode Mode { get; }
    public void GetFriendList()
    {
        // http...
    }
    // 其他...
}


internal class WebSocketAdapter : IAdapter, IApi
{
    public AdapterMode Mode { get; }

    public void GetFriendList()
    {
        // ws...
    }
    // 其他...
}

internal class AdapterManager : IApi
{
    private readonly List<IAdapter> adapters = new List<IAdapter>();

    public void GetFriendList()
    {
        foreach (var adapter in adapters)
        {
            // 适配器模式(是否支持发送API)
            if (adapter.Mode == AdapterMode.Send)
            {
                try
                {
                    adapter.GetFriendList();
                    return;
                }
                catch (Exception) { }
            }
        }
    }

    // 其他...
}

这边需要适配器把全部的API编写出来,然后管理器也要全部编写出来。

如果通过一个通用的接口,让他们整合起来就方便许多,只要实现一个通用接口,然后通过请求通用的接口去统一实现

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MiraiBotClient;

public class RequestPacket
{
    // ....
}
public class ResponsePacket
{
    // ....
}

public interface IApi
{
    void GetFriendList();
}

public interface IAdapter
{
    AdapterMode Mode { get; }
    ResponsePacket SendPacket(RequestPacket requestPacket);
}

public enum AdapterMode
{
    Receive, Send
}

internal class HttpAdapter : IAdapter
{
    public AdapterMode Mode { get; }

    public ResponsePacket SendPacket(RequestPacket requestPacket)
    {
        throw new NotImplementedException();
    }
}


internal class WebSocketAdapter : IAdapter
{
    public AdapterMode Mode { get; }

    public ResponsePacket SendPacket(RequestPacket requestPacket)
    {
        throw new NotImplementedException();
    }
}

internal class AdapterManager
{
    private readonly List<IAdapter> adapters = new List<IAdapter>();

    public ResponsePacket SendPacket(RequestPacket requestPacket)
    {
        foreach (var adapter in adapters)
        {
            // 或者弄个优先级,自行设置
            if (adapter.Mode == AdapterMode.Send)
            {
                return adapter.SendPacket(requestPacket);
            }
        }
    }


}

internal class Api : IApi
{
    AdapterManager AdapterManager { get; }

    public Api(AdapterManager adapterManager)
    {
        AdapterManager = adapterManager;
    }

    public void GetFriendList()
    {
        var friendList = new RequestPacket();
        // ....
        AdapterManager.SendPacket(friendList);
    }

    // 其他...
}

可能比较简陋

@ryoii
Copy link
Collaborator

ryoii commented Oct 11, 2023

不同接口的逻辑不同,即使提供通用接口,我认为该重复的地方还是会重复的。

不同 IAdapter 只负责到参数的解包和封包,共同抽离处理逻辑可以适当减少重复代码。

当然通用接口是可以提供的,但修改现有的请求结构可能性不大。你可以看看 general-router分支的这个功能能不能满足你的需求

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Feature New feature good first issue Good for newcomers
Projects
None yet
Development

No branches or pull requests

2 participants