《从零开始搭建游戏服务器》自定义兼容多种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. Springboot 之 自定义配置文件及读取配置文件

    本文章来自[知识林] 读取核心配置文件 核心配置文件是指在resources根目录下的application.properties或application.yml配置文件,读取这两个配置文件的方法有两 ...

  2. shell加密

    如何保护自己编写的shell程序要保护自己编写的shell脚本程序,方法有很多,最简单的方法有两种:1.加密 2.设定过期时间,下面以shc工具为例说明: 一.下载安装shc工具shc是一个加密she ...

  3. 2018-2019-2 《网络对抗技术》Exp0 Kali安装 Week1 20165301

    2018-2019-2 <网络对抗技术>Exp0 Kali安装 Week1 20165301 安装kali 参考此网站 设置共享文件夹 虚拟机->设置->选项->共享文件 ...

  4. sql语句添加查询字段

    SELECT * FROM( SELECT ROW_NUMBER() OVER (ORDER BY r.UpdateTime desc) tempRowNum,h.BizID,h.OrgID FROM ...

  5. .NetCore 使用Jenkins发布多环境下的项目并适配数据库EFCore数据库更新及替换配置文件

    说明 1.git上的配置地址可能都是本地环境,提交到git server后怎么来使用发布不同环境? 2.关于EFCore本地数据库有更新的情况 添加了 Migrations文件 怎么在构建的时候去更新 ...

  6. Top 10 Revit Architecture 2014 books

    Revit Architecture, along with ArchiCAD, is most used BIM software in architectural design. Although ...

  7. ZOJ.3551.Bloodsucker(期望DP)

    题目链接 \(Description\) 有1个吸血鬼和n-1个人,每天有且只会有两个人/吸血鬼相遇,如果是人与吸血鬼相遇,那个人会有p的概率变成吸血鬼:否则什么也不发生.求n个都变成吸血鬼的期望天数 ...

  8. NOIP练习赛题目3

    魔兽争霸 难度级别:C: 运行时间限制:1000ms: 运行空间限制:262144KB: 代码长度限制:2000000B 试题描述 小x正在销魂地玩魔兽他正控制着死亡骑士和n个食尸鬼(编号1-n)去打 ...

  9. BZOJ2612 : [Poi2003]Sums

    设d[i]表示能拼出的x中满足x%a[0]=i的最小的x,其中d[0]=0. 若d[x%a[0]]<=x,则一定可以拼出x,否则一定不可以. 建出带权有向图,点的标号从0到a[0]-1,i号点向 ...

  10. java基础学习总结——super关键字

    一.super关键字