暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Netty实现自定义通信协议

程序员顺仔和他的朋友们 2021-09-02
1263

概述

    在网络编程中,无论使用netty还是其它的socket通讯框架,都是通过TCP或UDP传输二进制流。发送方把要发送的对象转化成二进制流发送出去;接收方把接收到的二进制流转化为对象进行处理。 

    为了能让接收方和发送方能对同一个二进制流有相同的认识,双方必须提前约定好一个协议,即对象如何转化为二进制流,二进制流如何转化为对象,这样通信双方才不会产生误解。


自定义通信协议

    在 easy-im 项目中,定义如下通信协议:


魔数:4字节,一般为固定值,本项目中使用0x88888888。一般我们的应用于某个端口对外开放,为了防止该端口被意外调用,我们可以在收到报文后,取前4个字节与魔数比对,如果不相同,则直接拒绝并关闭连接。

版本号: 1字节,一般是预留字段,为了支持协议升级(这种情况极少出现)。

序列化算法:1字节,表示如何将java对象转化为二进制数据,以及如何反序列化。

指令:1字节,表示该消息的意图,如私聊、群聊、登录等。最多支持256种指令。

数据长度:4字节,表示该字段后数据部分的长度。

数据:具体数据的内容。每种指令对应的数据是不同的。


序列化算法

    本项目为了简单起见,使用json序列化算法。将Java对象转换成json字符串,再转化为二进制数据,代码如下:

    /**
    * Created by Sunnick on 2019/1/13/013.
    *
    * Serializer,用来指定序列化算法,用于序列化对象
    */
    public interface Serializer {




    /**
    * @return 序列化算法
    */
    byte getSerializerAlgorithm();


    /**
    * 将对象序列化成二进制
    *
    */
    byte[] serialize(Object object);


    /**
    * 将二进制反序列化为对象
    */
    <T> T deSerialize(Class<T> clazz, byte[] bytes);
    }
      /**
      * Created by Sunnick on 2019/1/13/013.
      *
      * json序列化器
      */
      public class JsonSerializer implements Serializer {


      public byte getSerializerAlgorithm() {
      return SerializeAlgorithm.json;
      }


      public byte[] serialize(Object object) {
      return JSON.toJSONBytes(object);
      }


      public <T> T deSerialize(Class<T> clazz, byte[] bytes) {
      return JSON.parseObject(bytes,clazz);
      }
      }

          先定义Serializer接口,serialize方法用于将对象序列化为二进制数据,deSerialize方法用于将二进制反序列化成对象。getSerializerAlgorithm方法返回序列化算法。 

          JsonSerializer是Serializer的实现。


      指令设计

          定义Packet类,作为所有指令的基类,其中getCommand方法为抽象方法,需由子类实现,返回具体的指令类型。

        /**
        * Created by Sunnick on 2019/1/13/013.
        *
        * 通信过程的对象,所有需要通过网络传输的对象,都继承自Packet
        */


        public abstract class Packet {
        /**
        * 协议版本号
        */
        private Byte version = 1;


        /**
        * 操作指令
        */
        public abstract Byte getCommand();


        public Byte getVersion() {
        return version;
        }


        public void setVersion(Byte version) {
        this.version = version;
        }
        }

            录指令如下,登录时需要发送userId,useName,password等信息,以及指令command:

          /**
          * Created by Sunnick on 2019/1/13/013.
          * 登录请求包
          */
          public class LoginRequestPacket extends Packet {


          private String userId;
          private String userName;
          private String password;


          @Override
          public Byte getCommand() {
          return Command.LOGIN_REQUEST;
              }
          }

          指令类型如下:

            /**
            * Created by Sunnick on 2019/1/13/013.
            *
            * 指令集
            */
            public interface Command {
            /**
            * 心跳包
            */
            final Byte HEART_BEAT = 0;
            /**
            * 登录请求
            */
            final Byte LOGIN_REQUEST = 1;
            /**
            * 登录响应
            */
            final Byte LOGIN_RESPONSE = 2;


            /**
            * 消息请求
            */
            final Byte MESSAGE_REQUEST = 3;
            /**
            * 消息响应
            */
            final Byte MESSAGE_RESPONSE = 4;
            /**
            * 创建群聊
            */
            final Byte CREATE_GROUP_REQUEST = 5;
            }


            编解码实现

                定义好序列化算法和指令之后,就可以进行编解码的实现了。编码即将通信包转化为二进制;解码即将二进制转化为通信包。

                编码过程比较简单,代码如下,参照注释即可明白,ByteBuf里即是最后要发送的二进制数据:

              public ByteBuf encode(ByteBuf buf,Packet packet){
              //序列化java对象
              byte[] objBytes = serializer.serialize(packet);
              //实际编码过程,即组装通信包
              //魔数(4字节) + 版本号(1字节) + 序列化算法(1字节) +
                      // 指令(1字节) + 数据长度(4字节) + 数据(N字节)
              buf.writeInt(MAGIC_NUMBER);
              buf.writeByte(packet.getVersion());
              buf.writeByte(serializer.getSerializerAlgorithm());
              buf.writeByte(packet.getCommand());
              buf.writeInt(objBytes.length);
              buf.writeBytes(objBytes);


              return buf;
              }

                   解码过程如下:

                public Packet decode(ByteBuf buf){
                //魔数校验(暂不做)
                buf.skipBytes(4);
                //版本号校验(暂不做)
                buf.skipBytes(1);
                //序列化算法
                byte serializeAlgorithm = buf.readByte();
                //指令
                byte command = buf.readByte();
                //数据长度
                int length = buf.readInt();
                //数据
                byte[] dataBytes = new byte[length];
                        buf.readBytes(dataBytes);
                        
                Class<? extends Packet> packetType = getRequestType(command);
                Serializer serializer = getSerializer(serializeAlgorithm);
                if(packetType != null && serializer != null){
                return serializer.deSerialize(packetType,dataBytes);
                        }
                return null;
                }

                这里暂时跳过魔数和版本校验,获取到序列化算法、指令、数据长度和数据内容。根据指令我们可以知道请求类型是什么(如登录请求LoginRequestPacket、群聊请求GroupChatRequestPacket),根据序列化算法,将数据内容反序列化成目标对象,解码结束。

                    可以看到,编码与解码是相反的过程。


                总结

                基于netty作为网络通信基础组件时,我们必须要做如下几个步骤:

                • 定义通讯协议,通信双方需对此协议有一致的理解;

                • 编码,将Java对象转化为二进制;

                • 解码,将二进制转化为Java对象;

                • 业务处理

                如果采用http、websocket等公有协议通信,netty提供了许多类可以实现步骤1,2,3,无需我们编码实现,只需调用相应的类和方法即可。


                项目地址:https://github.com/sunnick/easy-im

                欢迎关注公众号:程序员顺仔


                参考文档: 《netty入门实战:仿写微信IM及时通讯系统》


                文章转载自程序员顺仔和他的朋友们,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                评论