手游服务端框架之客户端协议组合下发

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/littleschemer/article/details/77893224
很多时候,客户端一个请求过来,服务端可能需要发多个响应消息。典型地,当客户端请求加载登录加载资源完成,服务端需要推送非常多的消息。功能模块越多,消息也越多。这个时候,可能就需要对下发的客户端协议进行优化。
个人理解,针对这种情况,比较好的优化方式是整合多个小消息一起下发。也就是说,对于多个消息,我们不直接一个一个下发,而是把多个消息看作一个特殊的消息组合下发
多包组合下发至少有以下几个好处:
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


猜你喜欢

转载自blog.csdn.net/littleschemer/article/details/77893224