Skip to main content

Solving Protocol Fragments

Introduction

This mainly solves two problems:

  • Reduce redundant/similar data protocol fragments.
  • Support automatic boxing and unboxing during use.

Problem

In real development, you often have basic-type business parameters such as:

  • Get clothing equipment details by equipment-clothes-id.
  • Get helmet equipment details by equipment-helmet-id.
  • Get xx data by xx-id.
  • ...

These parameters are essentially similar. If every such request defines a separate data protocol, it creates many protocol fragments, for example:

@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;
}
}

Solution

The business framework provides boxing/unboxing support for types such as int, long, boolean, and String to solve the problem above.

Let's look at the refactored version using protocol-fragment elimination. It keeps the same business behavior while reducing definitions like EquipClothesId.


Current code

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

Previous code

@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;
}

Built-in Auto Boxing/Unboxing Types

DescriptionPrimitive/Generic TypeCorresponding Java Built-in Type
EquivalentintIntValue
EquivalentList<Integer>IntValueList
EquivalentlongLongValue
EquivalentList<Long>LongValueList
EquivalentbooleanBoolValue
EquivalentList<Boolean>BoolValueList
EquivalentStringStringValue
EquivalentList<String>StringValueList
EquivalentList<YourClass>ByteValueList

int Example

tip

The two styles below are equivalent.

@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);
}
}

Principle

The framework provides common messages in the external server protocol.

  • IntValue is used to store a single int.
  • IntValueList is used to store List<Integer>.

The framework converts List<Integer> into a IntValueList object, and converts int into a IntValue object, then stores them in ExternalMessage.data. ExternalMessage is the protocol used for data exchange between the external server and the game frontend.

Likewise, if the game client wants to send data related to int, it also needs to wrap it with the corresponding message.

syntax = "proto3";

message IntValue {
sint32 value = 1;
}

message IntValueList {
repeated sint32 values = 1;
}

long Example

tip

The two styles below are equivalent.

@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);
}
}

Principle

The framework provides common messages in the external server protocol.

  • LongValue is used to store a single long.
  • LongValueList is used to store List<Long>.

The framework converts List<Long> into a LongValueList object, and converts long into a LongValue object, then stores them in ExternalMessage.data. ExternalMessage is the protocol used for data exchange between the external server and the game frontend.

Likewise, if the game client wants to send data related to long, it also needs to wrap it with the corresponding message.

syntax = "proto3";

message LongValue {
sint64 value = 1;
}

message LongValueList {
repeated sint64 values = 1;
}

bool Example

tip

The two styles below are equivalent.

@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);
}
}

Principle

The framework provides common messages in the external server protocol.

  • BoolValue is used to store a single bool.
  • BoolValueList is used to store List<Boolean>.

The framework converts List<Boolean> into a BoolValueList object, and converts bool into a BoolValue object, then stores them in ExternalMessage.data. ExternalMessage is the protocol used for data exchange between the external server and the game frontend.

Likewise, if the game client wants to send data related to bool, it also needs to wrap it with the corresponding message.

syntax = "proto3";

message BoolValue {
bool value = 1;
}

message BoolValueList {
repeated bool values = 1;
}

String Example

tip

The two styles below are equivalent.

@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", "ionet");
}
}

Principle

The framework provides common messages in the external server protocol.

  • StringValue is used to store a single String.
  • StringValueList is used to store List<String>.

The framework converts List<String> into a StringValueList object, and converts String into a StringValue object, then stores them in ExternalMessage.data. ExternalMessage is the protocol used for data exchange between the external server and the game frontend.

Likewise, if the game client wants to send data related to String, it also needs to wrap it with the corresponding message.

syntax = "proto3";

message StringValue {
string value = 1;
}

message StringValueList {
repeated string values = 1;
}

List Example

action supports List parameters and return values, which can effectively reduce protocol fragments and workload. Before List was supported, transmitting list data usually required wrapping protobuf objects in another response object.

Let's first look at an example. The action logic is simple: return queried list data to the requester. Previously, since List return values were not supported, developers had to define an extra response class to return list data.

Imagine your system has a lot of fixed configuration data, such as equipment, items, event info, hero profiles, enemy data, map data, skill data, pet base data, etc. You would usually end up with dozens or even hundreds of such response objects.

Creating matching response objects only to return fixed configuration lists is very tedious. These extra response objects are protocol fragments: optional and often unnecessary protocols. They also have drawbacks:

  • Become noise/interference in code.
  • Increase maintenance cost.
  • Increase workload (each new config table requires new classes, and each action must create this response object).

Traditional Approach

@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;
}
}

From the example above, you can see how serious protocol fragmentation can become. Our real requirement is simple: return list data to the requester. At this point, we can directly return list data via action List, which avoids all the drawbacks above. At the same time, code becomes cleaner, benefiting both frontend and backend.

We achieve the same functionality with less code, reduce workload, and avoid protocol fragments. This way, developers no longer need to create extra matching response protocols. Using framework-provided List return values can eliminate dozens or hundreds of protocols like xxxResponse and 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;
}
}

Principle

In external-server protocol definitions, the framework provides a common ByteValueList message to store List data.

The framework converts List data into a ByteValueList object and stores it in ExternalMessage.data. ExternalMessage is the protocol used for data interaction between external server and game client.

Similarly, if the game client wants to send List data, it should also wrap it using ByteValueList.

message ByteValueList {
repeated bytes values = 1;
}

How to Extend

Besides built-in int, long, bool, and String, developers can extend this part. If you want to support more types, refer to existing extension code and register extended types in the MethodParsers argument parser.

Summary

With the framework's automatic business-parameter boxing/unboxing, you can significantly reduce protocol fragments while keeping usage convenient and flexible.

Broadcast Example

Example

@ActionController(BroadcastCmd.cmd)
public class BroadcastAction {
AtomicInteger inc = new AtomicInteger();

@ActionMethod(triggerBroadcastMulticast)
private void triggerBroadcastMulticast() {
var communication = CommunicationKit.getCommunication();
// ---------- empty ----------
communication.broadcastMulticast(broadcastMulticastEmpty);

// ---------- int ----------
int dataInt = inc.getAndIncrement();
communication.broadcastMulticast(broadcastMulticastInt, dataInt);

// ---------- boolean ----------
boolean dataBool = inc.getAndIncrement() % 2 == 0;
communication.broadcastMulticast(broadcastMulticastBool, dataBool);

// ---------- long ----------
long dataLong = inc.getAndIncrement();
communication.broadcastMulticast(broadcastMulticastLong, dataLong);

// ---------- string ----------
String dataString = "ionet-" + inc.getAndIncrement();
communication.broadcastMulticast(broadcastMulticastString, dataString);

// ---------- object ----------
BookMessage dataObject = new BookMessage();
dataObject.authorName = "ionet";
dataObject.bookName = "book-" + inc.getAndIncrement();
communication.broadcastMulticast(broadcastMulticastObject, dataObject);

// ---------- list int ----------
List<Integer> dataListInt = List.of(inc.getAndIncrement(), inc.getAndIncrement());
communication.broadcastMulticastListInt(broadcastMulticastIntList, dataListInt);

// ---------- list boolean ----------
List<Boolean> dataListBool = List.of(
inc.getAndIncrement() % 2 == 0,
inc.getAndIncrement() % 2 == 0
);

communication.broadcastMulticastListBool(broadcastMulticastBoolList, dataListBool);

// ---------- list long ----------
List<Long> dataListLong = List.of(
(long) inc.getAndIncrement(),
(long) inc.getAndIncrement()
);

communication.broadcastMulticastListLong(broadcastMulticastLongList, dataListLong);

// ---------- list string ----------
List<String> dataListString = List.of(
"ionet-" + inc.getAndIncrement(),
"ionet-" + inc.getAndIncrement()
);

communication.broadcastMulticastListString(broadcastMulticastStringList, dataListString);

// ---------- list object ----------
BookMessage message1 = new BookMessage();
message1.authorName = "ionet";
message1.bookName = "book-" + inc.getAndIncrement();

BookMessage message2 = new BookMessage();
message2.authorName = "ionet";
message2.bookName = "book-" + inc.getAndIncrement();

List<BookMessage> dataList = List.of(message1, message2);
communication.broadcastMulticast(broadcastMulticastObjectList, dataList);
}
}

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;
}

Built-in framework types correspond to the external-server protocol. For more protocol details, see External Server Protocol