跳到主要内容

解决协议碎片

介绍

主要解决两个问题

  • 减少类似的数据协议碎片。
  • 使用时可自动装箱和拆箱。

问题

在实际开发中,会有一些基础类型的业务参数,如

  • 通过 装备-衣服-id 得到衣服装备详细
  • 通过 装备-头盔-id 得到头盔装备详细
  • 通过 xx-id 得到 xx 数据
  • ...

这些参数基本都是相同的,如果每一个类似这样的请求都创建一个数据协议,会产生比较多的数据协议碎片,如

@ProtobufClass
public class EquipHelmetId {
public int id;
}

@ProtobufClass
public class EquipClothesId {
public int id;
}

@ProtobufClass
public class EquipInfo {
public int id;
public String name;
}

@ActionController(1)
public class DemoAction {
@ActionMethod(0)
public EquipInfo getEquip(EquipClothesId equipClothesId) {
int equipId = equipClothesId.id;
...
return equipInfo;
}
}

解决办法

业务框架提供了 int、long、boolean 和 String 等类型的装箱、拆箱的使用方式,用于解决上面所描述的问题。

让我们看一下使用协议碎片改造后的代码,改造后的代码具备相同的业务功能,这样减少了 EquipClothesId 类似协议的定义。


现在的代码

@ActionController(1)
public class DemoAction {
@ActionMethod(0)
public EquipInfo getEquip(int equipId) {
...
return equipInfo;
}
}

之前的代码

@ActionController(1)
public class DemoAction {
@ActionMethod(0)
public EquipInfo getEquip(EquipClothesId equipClothesId) {
int equipId = equipClothesId.id;
...
return equipInfo;
}
}

@ProtobufClass
public class EquipClothesId {
public int id;
}

框架支持的自动装箱、拆箱基础类型

描述基础类型对应 java 内置类型
等价的intIntValue
等价的List<Integer>IntValueList
等价的longLongValue
等价的List<Long>LongValueList
等价的booleanBoolValue
等价的List<Boolean>BoolValueList
等价的StringStringValue
等价的List<String>StringValueList
等价的List<YourClass>ByteValueList

int Example

提示

下面两种风格是等价

@ActionController(6)
public class IntAction {
@ActionMethod(10)
public int int2int(int value) {
return value + 1;
}

@ActionMethod(12)
public List<Integer> intList2intList(List<Integer> intList) {
return List.of(1, 2);
}
}

原理

框架在游戏对外服的协议中,提供了公共的 message。

  • IntValue 用于存放单个 int 数据。
  • IntValueList 用于存放 List<Integer> 数据。

框架会将 List<Integer> 数据转为 IntValueList 对象, 将 int 转为 IntValue 对象, 并存放到 ExternalMessage.data 属性中,ExternalMessage 是游戏对外服与游戏前端数据交互的协议。

同样,如果游戏客户端想发送 int 相关数据,也需要使用对应的 message 来包装。

syntax = "proto3";

message IntValue {
sint32 value = 1;
}

message IntValueList {
repeated sint32 values = 1;
}

long Example

提示

下面两种风格是等价

@ActionController(6)
public class LongAction {
@ActionMethod(20)
public long long2long(long value) {
return value + 1;
}

@ActionMethod(22)
public List<Long> longList2longList(List<Long> longList) {
return List.of(1L, 2L);
}
}

原理

框架在游戏对外服的协议中,提供了公共的 message。

  • LongValue 用于存放单个 long 数据。
  • LongValueList 用于存放 List<Long> 数据。

框架会将 List<Long> 数据转为 LongValueList 对象, 将 long 转为 LongValue 对象, 并存放到 ExternalMessage.data 属性中,ExternalMessage 是游戏对外服与游戏前端数据交互的协议。

同样,如果游戏客户端想发送 long 相关数据,也需要使用对应的 message 来包装。

syntax = "proto3";

message LongValue {
sint64 value = 1;
}

message LongValueList {
repeated sint64 values = 1;
}

bool Example

提示

下面两种风格是等价

@ActionController(6)
public class BoolAction {
@ActionMethod(40)
public boolean bool2bool(boolean boolValue) {
return boolValue;
}

@ActionMethod(42)
public List<Boolean> boolList2boolList(List<Boolean> booleanList) {
return List.of(true, false);
}
}

原理

框架在游戏对外服的协议中,提供了公共的 message。

  • BoolValue 用于存放单个 bool 数据。
  • BoolValueList 用于存放 List<Boolean> 数据。

框架会将 List<Boolean> 数据转为 BoolValueList 对象, 将 bool 转为 BoolValue 对象, 并存放到 ExternalMessage.data 属性中,ExternalMessage 是游戏对外服与游戏前端数据交互的协议。

同样,如果游戏客户端想发送 bool 相关数据,也需要使用对应的 message 来包装。

syntax = "proto3";

message BoolValue {
bool value = 1;
}

message BoolValueList {
repeated bool values = 1;
}

String Example

提示

下面两种风格是等价

@ActionController(6)
public class StringAction {
@ActionMethod(30)
public String string2string(String s) {
return s + 1;
}

@ActionMethod(32)
public List<String> stringList2stringList(List<String> stringList) {
return List.of("Michael Jackson", "ioGame");
}
}

原理

框架在游戏对外服的协议中,提供了公共的 message。

  • StringValue 用于存放单个 String 数据。
  • StringValueList 用于存放 List<String> 数据。

框架会将 List<String> 数据转为 StringValueList 对象, 将 String 转为 StringValue 对象, 并存放到 ExternalMessage.data 属性中,ExternalMessage 是游戏对外服与游戏前端数据交互的协议。

同样,如果游戏客户端想发送 String 相关数据,也需要使用对应的 message 来包装。

syntax = "proto3";

message StringValue {
string value = 1;
}

message StringValueList {
repeated string values = 1;
}

List Example

action 支持 List 参数与返回值,可以有效的减少协议碎片、减少工作量等。 在没有支持 List 之前的代码,如果想要传输一个列表的数据,通常需要将 pb 对象包装到另一个 pb 响应对象中。

让我们先看一个示例,这个示例中 action 方法的的逻辑很简单,将查询到的数据列表给到请求端。 由于之前不支持 List 返回值,开发者想要将列表中的数据给到请求端, 还需要额外的定义一个与之对应的响应类,只有这样才能将列表数据给到请求端。

我们可以想象一下,如果你的系统中有很多固定的配置数据, 比如装备、道具、活动信息、英雄人物介绍、敌人相关信息、地图信息、技能信息、宠物基本信息...等等, 通常会有几十、上百个这样的响应对象。

为了将这些固定的配置数据给到请求端而建立与之对应的响应对象,想想这是一件多么无聊的事情。 这些多出来的响应对象,就是协议碎片,是一种可有可无的协议。此外还有如下缺点

  • 将会变成干扰项
  • 增加维护成本
  • 增加工作量(每次有新的配置表都要新建、在每个 action 中,都要创建这个响应对象)

传统写法

@ProtobufClass
public class Animal {
public int id;
}

@ProtobufClass
public class AnimalResponse {
public List<Animal> animals;
}

@ProtobufClass
public class AnimalRequest {
public List<Animal> animals;
}

@ActionController(3)
public class HallAction {
@ActionMethod(10)
public AnimalResponse acceptList(AnimalRequest request) {
List<Animal> animals = request.animals;
for (Animal animal : animals) {
animal.id = animal.id + 10;
}

AnimalResponse animalResponse = new AnimalResponse();
animalResponse.animals = animals;
return animalResponse;
}
}

通过上面的介绍,知道协议碎片是多么恐怖的一件事了把。 其实我们的需求也很简单,只是想把列表中的数据给到请求端就可以了。 此时,我们可以利用 action 将列表数据通过 List 直接返回,这样可以避免上面所说的各种缺点。 同时,还可以让我们的代码更加的简洁,这种方式可以使前端与后端都受益。

用更少的代码实现了同样的功能,减少了工作量,避免了协议碎片。 这样,开发者就不在需要额外的建立一个与之对应的响应协议了。 当使用了框架提供的 List 返回值后,可以帮助你的系统减少几十、上百个类似 xxxResponse 、xxxRequest 的协议。

@ActionController(3)
public class HallAction {
@ActionMethod(10)
public List<Animal> acceptList(List<Animal> animals) {
for (Animal animal : animals) {
animal.id = animal.id + 10;
}

return animals;
}
}

原理

框架在游戏对外服的协议中,提供了一个公共的 ByteValueList message,用于存放 List 数据。

框架会将 List 数据转为 ByteValueList 对象,并存放到 ExternalMessage.data 属性中, ExternalMessage 是游戏对外服与游戏前端数据交互的协议。

同样的,如果游戏客户端想发送 List 数据,也需要使用 ByteValueList 来包装。

message ByteValueList {
repeated bytes values = 1;
}

如何扩展

除了框架提供的 int、long、bool、String 外,开发者还可以对这部分进行扩展。 如果有兴趣扩展更多类型的,可以参考已有的扩展代码,将扩展好的类型配置到参数解析器 MethodParsers 中。

小结

通过框架提供的业务参数自动装箱和拆箱,可是减少很多协议上的碎片,并且使用方便、灵活。

注意事项

注意

自动装箱、拆箱只在 action 中有效果,即 action 接收参数和返回值时有效。

使用广播或跨服通信时,不能直接使用基础类型的,需要使用对应的包装类。


Example

public static void main(String[] args) {
BroadcastContext broadcastContext = BrokerClientHelper.getBroadcastContext();

List<HelloMessage> list = ...;
ByteValueList byteValueList = ByteValueList.ofList(list);

// broadcast list
broadcastContext.broadcast(CmdInfo.getCmdInfo(1, 1), byteValueList);

// broadcast int
IntValue intValue = IntValue.of(100);
broadcastContext.broadcast(CmdInfo.getCmdInfo(1, 2), intValue);

// broadcast list int
IntValueList intValueList = IntValueList.of(List.of(1,2);
broadcastContext.broadcast(CmdInfo.getCmdInfo(1, 3), intValueList);
}

ExternalMessage

syntax = "proto3";
package com.iohao.message;
// ExternalMessage. cn: 对外服数据协议
message ExternalMessage {
// Request command type: 0 heartbeat, 1 business .
// cn: 请求命令类型: 0 心跳,1 业务
int32 cmd_code = 1;
// Protocol switch, used for protocol-level switch control, such as security encryption verification, etc. : 0 no verification
// cn: 协议开关,用于一些协议级别的开关控制,比如 安全加密校验等。 : 0 不校验
optional int32 protocol_switch = 2;
// Business routing (high 16 bits for main, low 16 bits for sub)
// cn: 业务路由(高16为主, 低16为子)
optional int32 cmd_merge = 3;
// Response code: 0: success, others indicate errors
// cn: 响应码: 0:成功, 其他为有错误
optional sint32 response_status = 4;
// Verification message (error message, exception message), usually when responseStatus == -1001, there will be a value
// 验证信息(错误消息、异常消息),通常情况下 responseStatus == -1001 时, 会有值
optional string valid_msg = 5;
// Business request data
// 业务请求数据
optional bytes data = 6;
// Message tag number; set by the front-end when requesting, and will be carried by the server when responding; (similar to transparent transmission parameters)
// 消息标记号;由前端请求时设置,服务器响应时会携带上;(类似透传参数)
optional int32 msg_id = 7;
}

// int wrapper class. cn: int 包装类
message IntValue {
sint32 value = 1;
}

// int list wrapper class. cn: int list 包装类
message IntValueList {
// intList、intArray
repeated sint32 values = 1;
}

// long wrapper class. cn: long 包装类
message LongValue {
sint64 value = 1;
}

// long list wrapper class. cn: long list 包装类
message LongValueList {
// longList、longArray
repeated sint64 values = 1;
}

// string wrapper class. cn: string 包装类
message StringValue {
string value = 1;
}

// bool wrapper class. cn: string list 包装类
message StringValueList {
// stringList、stringArray
repeated string values = 1;
}

// bool wrapper class. cn: bool 包装类
message BoolValue {
bool value = 1;
}

// bool list wrapper class. cn: bool list 包装类
message BoolValueList {
// boolList、boolArray
repeated bool values = 1;
}

// pb object list wrapper class. cn: 对象 list 包装类
message ByteValueList {
// pb object List、pb object Array
// pb 对象 List、pb 对象 Array
repeated bytes values = 1;
}

框架内置的类型是对应着游戏对外服协议的。 关于协议更详细的说明,可阅读对外服协议