《从零开始搭建游戏服务器》自定义兼容多种Protobuf协议的编解码器

直接在protobuf序列化数据的前面,加上一个自定义的协议头,协议头里包含序列数据的长度和对应的数据类型,在数据解包的时候根据包头来进行反序列化。

1.协议头定义

关于这一块,我打算先采取比较简单的办法,结构如下: 
 
协议号是自定义的一个int类型的枚举(当然,假如协议吧比较少的话,可以用一个short来代替int以缩小数据包),这个协议号与协议类型是一一对应的,而协议头通常使用数据总长度来填入,具体过程如下:

  • 当客户端向服务器发送数据时,会根据协议类型加上协议号,然后使用protobuf序列化之后再发送给服务器;
  • 当服务器发送数据给客户端时,根据协议号,确定protobuf协议类型以反序列化数据,并调用相应回调方法。

2.自定义的编码器和解码器

编码器: 
参考netty自带的编码器ProtobufEncoder可以发现,被绑定到ChannelPipeline上用于序列化协议数据的编码器,必须继承MessageToByteEncoder<MessageLite>这个基类,并通过重写protected void encode(ChannelHandlerContext ctx, MessageLite msg, ByteBuf out)这个方法来实现自定义协议格式的目的:

package com.tw.login.tools;
import com.google.protobuf.MessageLite;
import com.tw.login.proto.CsEnum.EnmCmdID;
import com.tw.login.proto.CsLogin;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
/**
* 自定义编码器
* @author linsh
*
*/
public class PackEncoder extends MessageToByteEncoder<MessageLite> {
/**
* 传入协议数据,产生携带包头之后的数据
*/
@Override
protected void encode(ChannelHandlerContext ctx, MessageLite msg, ByteBuf out) throws Exception {
// TODO Auto-generated method stub
byte[] body = msg.toByteArray();
byte[] header = encodeHeader(msg, (short)body.length);
out.writeBytes(header);
out.writeBytes(body);
return;
}
/**
* 获得一个协议头
* @param msg
* @param bodyLength
* @return
*/
private byte[] encodeHeader(MessageLite msg,short bodyLength){
short _typeId = 0;
if(msg instanceof CsLogin.CSLoginReq){
_typeId = EnmCmdID.CS_LOGIN_REQ_VALUE;
}else if(msg instanceof CsLogin.CSLoginRes){
_typeId = EnmCmdID.CS_LOGIN_RES_VALUE;
}
//存放两个short数据
byte[] header = new byte[4];
//前两位放数据长度
header[0] = (byte) (bodyLength & 0xff);
header[1] = (byte) ((bodyLength >> 8) & 0xff);
//后两个字段存协议id
header[2] = (byte) (_typeId & 0xff);
header[3] = (byte) ((_typeId >> 8) & 0xff);
return header;
}
}

解码器: 
参考netty自带的编码器ProtobufDecoder可以发现,被绑定到ChannelPipeline上用于序列化协议数据的解码器,必须继承ByteToMessageDecoder这个基类,并通过重写protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out)这个方法来实现解析自定义协议格式的目的:

package com.tw.login.tools;
import java.util.List;
import com.google.protobuf.MessageLite;
import com.tw.login.proto.CsEnum.EnmCmdID;
import com.tw.login.proto.CsLogin.CSLoginReq;
import com.tw.login.proto.CsLogin.CSLoginRes;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
public class PackDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
// 获取包头中的body长度
byte low = in.readByte();
byte high = in.readByte();
short s0 = (short) (low & 0xff);
short s1 = (short) (high & 0xff);
s1 <<= 8;
short length = (short) (s0 | s1);
// 获取包头中的protobuf类型
byte low_type = in.readByte();
byte high_type = in.readByte();
short s0_type = (short) (low_type & 0xff);
short s1_type = (short) (high_type & 0xff);
s1_type <<= 8;
short dataTypeId = (short) (s0_type | s1_type);
// 如果可读长度小于body长度,恢复读指针,退出。
if (in.readableBytes() < length) {
in.resetReaderIndex();
return;
}
//开始读取核心protobuf数据
ByteBuf bodyByteBuf = in.readBytes(length);
byte[] array;
//反序列化数据的起始点
int offset;
//可读的数据字节长度
int readableLen= bodyByteBuf.readableBytes();
//分为包含数组数据和不包含数组数据两种形式
if (bodyByteBuf.hasArray()) {
array = bodyByteBuf.array();
offset = bodyByteBuf.arrayOffset() bodyByteBuf.readerIndex();
} else {
array = new byte[readableLen];
bodyByteBuf.getBytes(bodyByteBuf.readerIndex(), array, 0, readableLen);
offset = 0;
}
//反序列化
MessageLite result = decodeBody(dataTypeId, array, offset, readableLen);
out.add(result);
}
/**
* 根据协议号用响应的protobuf类型来解析协议数据
* @param _typeId
* @param array
* @param offset
* @param length
* @return
* @throws Exception
*/
public MessageLite decodeBody(int _typeId,byte[] array,int offset,int length) throws Exception{
if(_typeId == EnmCmdID.CS_LOGIN_REQ_VALUE){
return CSLoginReq.getDefaultInstance().getParserForType().parseFrom(array,offset,length);
}
else if(_typeId == EnmCmdID.CS_LOGIN_RES_VALUE){
return CSLoginRes.getDefaultInstance().getParserForType().parseFrom(array,offset,length);
}
return null;
}
}

3.修改Socket管道绑定的编解码器:

在创建Socket管道的时候,将编解码器替换为自定义的编解码器,而具体数据发送和接受过程无需做任何修改:

ChannelPipeline pipeline = ch.pipeline();
// 协议数据的编解码器
pipeline.addLast("frameDecoder",new ProtobufVarint32FrameDecoder());
pipeline.addLast("protobufDecoder",new PackDecoder());
pipeline.addLast("frameEncoder",new ProtobufVarint32LengthFieldPrepender());
pipeline.addLast("protobufEncoder", new PackEncoder());
pipeline.addLast("handler",new SocketServerHandler());

自定义兼容多种Protobuf协议的编解码器的更多相关文章

  1. SQL 横转竖 、竖专横 (转载) 使用Dapper.Contrib 开发.net core程序,兼容多种数据库 C# 读取PDF多级书签 Json.net日期格式化设置 ASPNET 下载共享文件 ASPNET 文件批量下载 递归,循环,尾递归 利用IDisposable接口构建包含非托管资源对象 《.NET 进阶指南》读书笔记2------定义不可改变类型

    SQL 横转竖 .竖专横 (转载)   普通行列转换 问题:假设有张学生成绩表(tb)如下: 姓名 课程 分数 张三 语文 74 张三 数学 83 张三 物理 93 李四 语文 74 李四 数学 84 ...

  2. 使用Dapper.Contrib 开发.net core程序,兼容多种数据库

    关于Dapper的介绍,我想很多人都对它有一定的了解,这个类似一个轻型的ORM框架是目前应用非常火的一个东西,据说各方面的性能都不错,而且可以支持多种数据库,在开始介绍这个文章之前,我花了不少功夫来学 ...

  3. Protobuf 协议语言指南

    l  定义一个消息(message)类型 l  标量值类型 l  Optional 的字段及默认值 l  枚举 l  使用其他消息类型 l  嵌套类型 l  更新一个消息类型 l  扩展 l  包(p ...

  4. Dapper.Contrib 开发.net core程序,兼容多种数据库

    Dapper.Contrib 开发.net core程序,兼容多种数据库 https://www.cnblogs.com/wuhuacong/p/9952900.html 使用Dapper.Contr ...

  5. Protobuf协议应用干货

    Protobuf应用广泛,尤其作为网络通讯协议最为普遍.本文将详细描述几个让人眼前一亮的protobuf协议设计,对准备应用或已经应用protobuf的开发者会有所启发,甚至可以直接拿过去用. 这里描 ...

  6. 开源项目SMSS开源项目(三)——protobuf协议设计

    本文的第一部分将介绍protobuf使用基础以及如何利用protobuf设计通信协议.第二部分会给出smss项目的协议设计规范和源码讲解. 一.Protobuf使用基础 什么是protobuf pro ...

  7. protobuf 协议浅析

    目录 Protobuf 协议浅析 1. Protobuf 介绍 1.1 Protobuf 基本概念 1.2 Protobuf 的优点 1.3 Protobuf, JSON, XML 的区别 2. Pr ...

  8. Google的Protobuf协议分析

    protobuf和thrift类似,也是一个序列化的协议实现,简称PB(下文出现的PB代表protobuf). Github:https://github.com/google/protobuf 上图 ...

  9. 真正的让iframe自适应高度 兼容多种浏览器随着窗口大小改变

    今天有朋友问到我关于"iframe自适应高度"的问题,原本以为是很简单的问题,没想到折腾了20分钟才搞定.期间遇到几个问题,要么是高度自适应了,但是当窗口改变时会出现滚动条.也就是 ...

随机推荐

  1. mysql子查询 exists,not exists,all和any

    (1)实现让结果集A - 结果集B:--利用not exists,合并则可用union . exists,not exists:用于判断且获取结果集A是否存在地结果集B中! ==========结果集 ...

  2. 发布构件到Maven中央仓库

    一.注册jira账号 访问如下网址: https://issues.sonatype.org/secure/Signup.jspa 记住邮箱,用户名,密码以备以后使用,一定牢记. 二.创建一个issu ...

  3. 阿里云url解析,发布web后去除url中的端口号

    归根结底就是80端口的使用,不是http的80 的 或 https的  都得加端口号 [问题描述] http://wisecores.wisers.com:8080/JsonProject/servl ...

  4. 微信小程序Http高级封装 es6 promise

    公司突然要开放微信小程序,持续蒙蔽的我还不知道小程序是个什么玩意. 于是上网查了一下,就开始着手开发..... 首先开发客户端的东西,都有个共同点,那就是  数据请求! 看了下小程序的请求方式大概和a ...

  5. 【PAT】1091 Acute Stroke(30 分)

    1091 Acute Stroke(30 分) One important factor to identify acute stroke (急性脑卒中) is the volume of the s ...

  6. docker重命名镜像

    一.docker tag IMAGEID(镜像id) REPOSITORY:TAG(仓库:标签)

  7. 恢复spark挂掉的节点

    背景: 某个Worker节点挂掉了,我们需要将Worker进行启动恢复,同时可以恢复streaming中的executor 解决办法: 其实很简单,去worker节点中执行sbin/start-sla ...

  8. mybatis中的增删改查操作

    在这一个部分,主要进行增删改查的示例书写. 增删改查可以基于xml的,也可以基于注解的方式. 一:对单条数据的查询 1.目录结构 这个使得目录更加清晰 2.User.java 这个使用以前的user表 ...

  9. 对C转换说明符的误解以及关于数组的一些知识

    事实上,scanf()函数中%c并非是用来输入单个字符的,而是用来输入一组字符的. 例如: ]; scanf("%3c",a); 其中“3”规定了输入数据的宽度,当然宽度为“1”的 ...

  10. Go面试题精编100题

    Golang精编100题 选择题 1.   [初级]下面属于关键字的是()A. funcB. defC. structD. class 参考答案:AC 2.   [初级]定义一个包内全局字符串变量,下 ...