版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/littleschemer/article/details/77893224
很多时候,客户端一个请求过来,服务端可能需要发多个响应消息。典型地,当客户端请求加载登录加载资源完成,服务端需要推送非常多的消息。功能模块越多,消息也越多。这个时候,可能就需要对下发的客户端协议进行优化。
个人理解,针对这种情况,比较好的优化方式是整合多个小消息一起下发。也就是说,对于多个消息,我们不直接一个一个下发,而是把多个消息看作一个特殊的消息组合下发。
多包组合下发至少有以下几个好处:
1. 省流量。由于每个消息都会一些包头,几个消息一起下发也只需要一个包头。
2. 防止客户端解包降帧。客户端每帧拆包的数量是有限的,瞬间拆多个包看起来就会卡。
3. 缓解服务端io线程压力。服务端推送消息一般是异步线程处理的,包数量越少,io线程就不会太繁忙。
当然,组合包的应用场合,应该是整合多个小的消息。如果单独的消息体信息量大,那么就没必要整合了。
对于我们的服务端框架,加上一个组合包,代码也很少。
首先,由于每个消息都带有模块号,类型id,还有消息体,我们有必要抽象出这个整体概念,也是为了传输数据方便。
对于这个数据结构,应该重点说明的是byte[]的body字段。我们下发的消息体是经过protobuf序列化的,这里不能直接申明为Message类型,因为protobuf不支持抽象类型。
测试代码也很简单,在LoginManager的handleAccountLogin方法验证登录成功后,直接下发组合包。
手游服务端开源框架系列完整的代码请移步github ->> jforgame
个人理解,针对这种情况,比较好的优化方式是整合多个小消息一起下发。也就是说,对于多个消息,我们不直接一个一个下发,而是把多个消息看作一个特殊的消息组合下发。
多包组合下发至少有以下几个好处:
1. 省流量。由于每个消息都会一些包头,几个消息一起下发也只需要一个包头。
2. 防止客户端解包降帧。客户端每帧拆包的数量是有限的,瞬间拆多个包看起来就会卡。
3. 缓解服务端io线程压力。服务端推送消息一般是异步线程处理的,包数量越少,io线程就不会太繁忙。
当然,组合包的应用场合,应该是整合多个小的消息。如果单独的消息体信息量大,那么就没必要整合了。
对于我们的服务端框架,加上一个组合包,代码也很少。
首先,由于每个消息都带有模块号,类型id,还有消息体,我们有必要抽象出这个整体概念,也是为了传输数据方便。
public class Packet {
@Protobuf(order = 10)
private int module;
@Protobuf(order = 11)
private int cmd;
@Protobuf(order = 12,fieldType = FieldType.BYTES)
/** 序列化的消息体 */
private byte[] body ;
public Packet(){
}
public static Packet valueOf(Message message) {
Packet packet = new Packet();
packet.module = message.getModule();
packet.cmd = message.getCmd();
try {
Codec codec = ProtobufProxy.create(message.getClass());
packet.body = codec.encode(message);
}catch (Exception e){
LoggerUtils.error("生成Packet出错", e);
throw new IllegalArgumentException("parse packet attachment failed",e);
}
return packet;
}
public static Message asMessage(Packet packet) {
Class<?> msgClazz = MessageFactory.INSTANCE.getMessage((short)packet.module, (short)packet.cmd);
try {
Codec<?> codec = ProtobufProxy.create(msgClazz);
Message message = (Message) codec.decode(packet.body);
return message;
} catch (IOException e) {
LoggerUtils.error("读取消息出错,模块号{},类型{},异常{}", packet.module, packet.cmd );
}
return null;
}
}
对于这个数据结构,应该重点说明的是byte[]的body字段。我们下发的消息体是经过protobuf序列化的,这里不能直接申明为Message类型,因为protobuf不支持抽象类型。
接着,我们申明一种特殊的消息CombineMessage,继承自Message抽象类。CombineMessage唯一的字段就是一个Packet列表,以及提供添加消息和获取全部消息的接口。作为区分,CombineMessage的模块号和cmd都是0。
@MessageMeta(module=0, cmd=0)
public final class CombineMessage extends Message {
@Protobuf(order = 1)
private final List<Packet> packets = new ArrayList<>();
public CombineMessage(){
}
/**
* 添加新的消息对象
* @param message
*/
public void addMessage(Message message){
this.packets.add(Packet.valueOf(message));
}
public List<Packet> getPackets() {
return packets;
}
public int getCacheSize(){
return this.packets.size();
}
}
最后,我们就来处理编解码了。由于CombineMessage类也是一种Message,且消息体的序列化已由Packet处理,所以编码的代码我们不需要修改。我们只需要处理解码就可以了。只需要在解码类(MessageDecoder)插入一小段特殊处理的代码,当发现消息是组合消息,向下转型获取全部Packet的,依次将Packet反序列化为具体的Message即可。
if (ioBuffer.remaining() >= length) {
short moduleId = ioBuffer.getShort();
short cmd = ioBuffer.getShort();
byte[] body = new byte[length-4];
ioBuffer.get(body);
Message msg = readMessage(moduleId, cmd, body);
if (moduleId > 0) {
out.write(msg);
} else { //属于组合包
CombineMessage combineMessage = (CombineMessage)msg;
List<Packet> packets = combineMessage.getPackets();
for (Packet packet :packets) {
//依次拆包反序列化为具体的Message
out.write(Packet.asMessage(packet));
}
}
if (ioBuffer.remaining() == 0) {
ioBuffer.clear();
break;
}
ioBuffer.compact();
}
测试代码也很简单,在LoginManager的handleAccountLogin方法验证登录成功后,直接下发组合包。
public void handleAccountLogin(IoSession session, long accoundId, String password) {
if ("kingston".equals(password)) {
Message response = new ResLoginMessage(LoginDataPool.LOGIN_SUCC, "登录成功");
CombineMessage combineMessage = new CombineMessage();
combineMessage.addMessage(response);
combineMessage.addMessage(new ResPlayerEnterSceneMessage());
combineMessage.addMessage(ResGmResultMessage.buildSuccResult("执行gm成功"));
MessagePusher.pushMessage(session, combineMessage);
} else {
MessagePusher.pushMessage(session,
new ResLoginMessage(LoginDataPool.LOGIN_FAIL, "登录失败"));
}
}
客户端打印
收到响应-->ResLoginMessage [code=1, tips=登录成功]
收到响应-->ResPlayerEnterSceneMessage [mapId=0]
收到响应-->ResGmResultMessage [result=1, message=执行gm成功]
手游服务端开源框架系列完整的代码请移步github ->> jforgame