iohao / ioGame

无锁异步化、事件驱动架构设计的 java netty 网络编程框架; 轻量级,无需依赖任何第三方中间件或数据库就能支持集群、分布式; 适用于网络游戏服务器、物联网、内部系统及各种需要长连接的场景; 通过 ioGame 你可以很容易的搭建出一个集群无中心节点、集群自动化、分布式的网络服务器;FXGL、Unity、UE、Cocos Creator、Godot、Netty、Protobuf、webSocket、tcp、socket;java Netty 游戏服务器框架;
http://game.iohao.com
GNU Affero General Public License v3.0
834 stars 183 forks source link

SDK TypeScript 客户端代码生成;方便 CocosCeator、或其他支持 TypeScript 的客户端对接。 #329

Open iohao opened 2 months ago

iohao commented 2 months ago

在线文档:SDK TypeScript 代码生成 (yuque.com)

新增功能的使用场景

[对接文档][SDK TypeScript 联调代码生成] 根据 action 、广播、错误码 ...等信息,自动生成对接代码,生成的对接代码适用于 CocosCeator、或其他支持 TypeScript 语言的客户端,从而减少客户端模板代码的编写 。

该特性所生成的代码,是基于 SDK TypeScript 上使用的,也就是 ioGameSDK TypeScript。

SDK 代码生成主要解决以下几个问题

SDK TypeScript 代码生成可为客户端开发者减少巨大的工作量,并可为客户端开发者屏蔽路由等概念。直接面向接口编程,面向接口编程的几个优点

  1. 帮助客户端开发者减少巨大的工作量。
  2. 方法参数类型安全、明确。可有效避免安全隐患,从而减少联调时的低级错误。
  3. 减少服务器与客户端双方对接时的沟通成本,代码即文档。生成的联调代码中有文档与使用示例,即使是新手,也能将使用成本拉到 0。
  4. 生成的联调代码,能明确所需要的参数类型,也能明确服务器是否会返回数据,返回什么样的数据,会在生成接口时就明确好。接口有 callback 回调参数的,就表示该 api 会有返回值,而具体的值类型则会在接口文档中生成好。
  5. 帮助客户端开发者屏蔽与服务器交互部分,将更多的精力放在真正的业务上。
  6. 为双方联调减少心智负担。联调代码使用简单,与本地方法调用一般丝滑。
  7. 抛弃传统面向协议对接的方式,转而使用面向接口方法的对接方式。

传统的对接方式,在客户端发送请求后并不能知道该请求会返回什么,这通常需要在协议文件中阅读与查找。如果协议较少时,这样做的问题并不大,但如果有数百个协议时,这样的工作方式是低效的,因为协议文件中存在着大量的干扰因素。

如果你的协议与路由是强绑定的,那么问题则会更大。因为这失去了协议复用的可能性,这将会产生大量的协议碎片

以上几个问题,在 ioGame 中得到了很好的解决;为了直观,下面我们首先做客户端功能上的展示,其次介绍客户端如何使用生成的代码的。

服务器示例代码

后续介绍将使用以下服务器代码讲解。

@ActionController(SdkCmd.cmd)
public class SdkAction {
    ... ... 省略部分代码
    int inc;

    /**
     * 测试 int 返回 int
     *
     * @param value value
     * @return 返回 int
     */
    @ActionMethod(SdkCmd.intCmd)
    public int intCmd(int value) {
        return value + inc++;
    }

    /**
     * 测试 intList 返回 intList
     *
     * @param list value list
     * @return 返回 intList
     */
    @ActionMethod(SdkCmd.listIntCmd)
    public List<Integer> listIntCmd(List<Integer> list) {
        return list.stream().map(value -> value + this.inc++).toList();
    }

    /**
     * 测试 student 返回 student
     *
     * @param student student
     * @return 返回 student
     */
    @ActionMethod(SdkCmd.student)
    public Student student(Student student) {
        int inc = this.inc++;
        student.age += inc;
        student.name += inc;

        return student;
    }

    /**
     * 测试 studentList 返回 studentList
     *
     * @param studentList 接收 studentList
     * @return 返回 studentList
     */
    @ActionMethod(SdkCmd.listStudent)
    public List<Student> listStudent(List<Student> studentList) {
        return studentList.stream().peek(student -> {
            int inc = this.inc++;
            student.age += inc;
            student.name += inc;
        }).toList();
    }
}

onClickInt 演示

我们的客户端界面如下,提供了 int、long、bool、string 及其相关数组的协议,这些协议是 ioGame SDK 提供的,用于处理协议碎片相关的。其中,student 是我们自定义的一个协议。后续的介绍中,通过用 int、intList、student、studentList 进行举例介绍。

当我们点击 int 按钮后,会将一个 int 的值传递给服务器,并得到服务器响应的数据。

image


onClick Int 对应的代码如下

async onClickInt() {
    console.log("--------- onClick Int ---------------")
    const data = 1;

    // 编码风格 1 - 异步回调
    SdkAction.ofIntCmd(data, result => {
        result.log(result.getInt());
    }).execute();

    // 编码风格 2 - async await
    const result = await SdkAction.ofAwaitIntCmd(data);
    result.log(result.getInt());
}

从下图中我可以看到同一个 api 有两种编码风格。SDK 代码生成时,会帮助我们生成两种编码风格 api,一种回调风格的,另一种是 await async 风格的;开发者可根据自身业务的复杂度来选择其中的一种编码风格与服务器进行交互。

编码风格 1,异步回调

我们先介绍回调风格的 api,细心的朋友会发现,在工具的右边显示了该方法的相关文档,文档包括:

  1. 方法描述。
  2. 参数描述。
  3. 回调说明。当服务器的方法有返回值时,才会生成该回调参数。注意,这点很重要,因为这样就明确了当请求触发后会得到一个服务器的响应,这就与传统的对接方式区别开来了。而传统的对接方式,在客户端发送请求后并不能知道该请求会返回什么、是否会有返回 ...等。
  4. 使用示例。文档中会给出当前 api 的使用示例。通常,前端开发者只需要 copy 就行。这是一个质的飞越,因为这几乎可以做到 0 沟通成本了,直接面向 api 编程,且 api 上都有使用示例展示。即使是新手,也能将使用成本拉到 0。

image


编码风格 2,async await

async await 风格的 api,也就是编码风格 2。在最终结果的获取上,与回调风格是一样的,并无任何区别。async await 在处理复杂业务时,可以有效的避免回调地狱

同样,api 的文档中有相关描述及使用示例,这里就不同义反复了。

image

客户端控制台介绍

请求时,在控制台中会打印几个标签,分别是:

  1. msgId :是消息标记号,具体的 id 由 SDK 自动生成并设置到请求中,服务器响应时会携带上;(类似**透传**参数)。
  2. 当前请求所使用的编码风格:execute 对应回调风格,executeAwait 则对应 async await 风格。
  3. 当前请求的方法描述。
  4. 路由信息
  5. 当前请求的参数,会被服务器接收。

响应时,在控制台中会打印几个标签,分别是:

  1. msgId :同上,是消息标记号,服务器响应时携带上的。也就是说,响应的 msgId 总是能与请求对应得上的。
  2. 当前请求所使用的编码风格,同上。
  3. 方法描述,同上。
  4. 路由信息,同上。
  5. 耗时:从请求发送到接收服务器响应的总耗时。
  6. 当前打印的内容。

image

onClickInt 小结

本次我们主要展示了传递 “基础类型” 的单个参数给服务器。下面,我们将介绍传递 “基础类型” 的 List 参数给服务器。


onClick ListInt 演示

当我们点击 intList 按钮后,会将一个 int list 的值传递给服务器,并得到服务器响应的数据。

image

编码风格

onClick ListInt 对应的代码如下,从图中我可以看到同一个 api 有两种编码风格。与上面介绍的类似,这里就不重复了。开发者可根据自身业务的复杂度来选择其中的一种编码风格与服务器进行交互。

async onClickListInt() {
    console.log("--------- onClick ListInt ---------------")
    let data = [1, 2];

    // 编码风格 1 - 异步回调
    SdkAction.ofListIntCmd(data, result => {
        result.log(result.listInt());
    }).execute();

    // 编码风格 2 - async await
    let result = await SdkAction.ofAwaitListIntCmd(data);
    result.log(result.listInt());
}

编码风格 1 - 回调及文档

image

编码风格 2 - async await 及文档

image

客户端控制台介绍

同上。

image

onClick ListInt 小结

本次我们主要展示了传递 “基础类型” 的 List 参数给服务器,并接收服务器返回的 int list 数据。这意味着,客户端与服务器都具备接收与返回 “基础类型” List 数据的能力。

目前我们只演示了 int、intList,关于其他 “基础类型” 相关的,如 long、bool、string ...等,这里就不一一介绍了。该特性称为协议碎片,协议碎片可让我们大量的减少协议的定义,这利益于协议可复用的设计。

下面,我们将介绍自定义协议的单个参数的处理和多个参数的处理。在传统框架中,基本只能支持单个参数的处理。

onClick Student 自定义协议 演示

自定义协议 Student

syntax = "proto3";
package proto;

// 学生
message Student {
  // 年龄
  int32 age = 1;
  // 姓名
  string name = 2;
}

当我们点击 student 按钮后,会将一个 Student 对象传递给服务器,并得到服务器响应的数据。

image

编码风格

onClick Student 对应的代码如下,从图中我可以看到同一个 api 有两种编码风格。与上面介绍的类似,这里就不重复了。开发者可根据自身业务的复杂度来选择其中的一种编码风格与服务器进行交互。

async onClickStudent() {
    console.log("--------- onClick Student ---------------")

    let data = proto.Student.create({
        age: 1,
        name: "ioGame"
    });

    // 编码风格 1 - 异步回调
    SdkAction.ofStudent(data, result => {
        let value = result.getValue(root.proto.Student.decode);
        result.log(value);
    }).execute();

    // 编码风格 2 - async await
    let result = await SdkAction.ofAwaitStudent(data);
    let value = result.getValue(root.proto.Student.decode);
    result.log(value);
}

编码风格 1 - 回调及文档

image

编码风格 2 - async await 及文档

image

客户端控制台介绍

打印 Student 协议对象的信息,其他同上。

image

onClick Student 小结

本次我们主要展示了传递自定义协议的单个参数给服务器,并接收服务器返回的 Student 数据。这是最普遍的使用方法,而传统框架基本也只能处理单参数的概念。

下面,我们将介绍将自定义协议 List 传递给服务器的使用方式。这意味着,客户端与服务器都具备接收与返回 “自定义协议” List 数据的能力。

onClick ListStudent 自定义协议 演示

当我们点击 student 按钮后,会将一个 Student 对象列表(即 List、数组)传递给服务器,并得到服务器响应的数据。

image

编码风格

onClick ListStudent 对应的代码如下,从图中我可以看到同一个 api 有两种编码风格。与上面介绍的类似,这里就不重复了。开发者可根据自身业务的复杂度来选择其中的一种编码风格与服务器进行交互。

async onClickListStudent() {
    console.log("--------- onClick ListStudent ---------------")
    let studentList: root.proto.Student[] = [new proto.Student({
        age: 101,
        name: "ioGame - java SDK"
    }), new proto.Student({
        age: 101,
        name: "ioGame - C# SDK"
    }), new proto.Student({
        age: 101,
        name: "ioGame - ts SDK"
    })];

    // 编码风格 1 - 异步回调
    SdkAction.ofListStudent(studentList, result => {
        // 拿到学生列表
        let dataList = result.listValue(proto.Student.decode);
        result.log(dataList);
    }).execute();

    // 编码风格 2 - async await
    let result = await SdkAction.ofAwaitListStudent(studentList);
    // 拿到学生列表
    let dataList = result.listValue(proto.Student.decode);
    result.log(dataList);
}

编码风格 1 - 回调及文档

image

编码风格 2 - async await 及文档

image

客户端控制台介绍

打印 Student 协议对象 List 的信息,其他同上。

image

onClick ListStudent 小结

本次我们主要展示了传递自定义协议的 List 参数给服务器,并接收服务器返回的 Student List 数据。这也是协议复用的优势,同一个协议即可用在单参数的传递中,也可用在 List 参数的传递中,这一概念在传统框架中是无法做到的。

SDK 所生成的代码

以下客户端 xxxAction 的代码是由系统自动生成的,客户端开发者只需调用方法就能与服务器通信了。为了节约篇幅,删除了方法的注释与实现,只保留了方法名。

export class SdkAction {
    private static readonly intCmd_1_1: number = CmdKit.merges(1, 1, "测试 int 返回 int");
    private static readonly listIntCmd_1_5: number = CmdKit.merges(1, 5, "测试 intList 返回 intList");
    private static readonly student_1_9: number = CmdKit.merges(1, 9, "测试 student 返回 student");
    private static readonly listStudent_1_10: number = CmdKit.merges(1, 10, "测试 studentList 返回 studentList");

    static ofIntCmd(value: number, callback: (result: ResponseResult) => void): RequestCommand {
        return RequestCommand.ofInt(this.intCmd_1_1, value).onCallback(callback);
    }

    static async ofAwaitIntCmd(value: number): Promise<ResponseResult> {
        return await RequestCommand.ofAwaitInt(this.intCmd_1_1, value);
    }

    static ofListIntCmd(list: number[], callback: (result: ResponseResult) => void): RequestCommand {
        return RequestCommand.ofIntList(this.listIntCmd_1_5, list).onCallback(callback);
    }

    static async ofAwaitListIntCmd(list: number[]): Promise<ResponseResult> {
        return await RequestCommand.ofAwaitIntList(this.listIntCmd_1_5, list);
    }

    static ofStudent(student: root.proto.Student, callback: (result: ResponseResult) => void): RequestCommand {
        return ...
    }

    static async ofAwaitStudent(student: root.proto.Student): Promise<ResponseResult> {
        return ...
    }

    static ofListStudent(studentList: root.proto.Student[], callback: (result: ResponseResult) => void): RequestCommand {
        return ...
    }

    static async ofAwaitListStudent(studentList: root.proto.Student[]): Promise<ResponseResult> {
        return ...
    }
}

小结

SDK TypeScript 代码生成可为客户端开发者减少巨大的工作量,并可为客户端开发者屏蔽路由等概念。直接面向接口编程,面向接口编程的几个优点

  1. 帮助客户端开发者减少巨大的工作量。
  2. 方法参数类型安全、明确。可有效避免安全隐患,从而减少联调时的低级错误。
  3. 减少服务器与客户端双方对接时的沟通成本,代码即文档。生成的联调代码中有文档与使用示例,即使是新手,也能将使用成本拉到 0。
  4. 生成的联调代码,能明确所需要的参数类型,也能明确服务器是否会返回数据,返回什么样的数据,会在生成接口时就明确好。接口有 callback 回调参数的,就表示该 api 会有返回值,而具体的值类型则会在接口文档中生成好。
  5. 帮助客户端开发者屏蔽与服务器交互部分,将更多的精力放在真正的业务上。
  6. 为双方联调减少心智负担。联调代码使用简单,与本地方法调用一般丝滑。
  7. 抛弃传统面向协议对接的方式,转而使用面向接口方法的对接方式。

上面只展示了 action 相关的,除了能生成 action 的代码外,还能生成广播及错误码相关的代码,这部分的文档将在功能正式推出后补全。

其他参考资料