美文网首页
pomelo源码分析(6)--connector协议处理mess

pomelo源码分析(6)--connector协议处理mess

作者: 天一阁图书管理员 | 来源:发表于2017-08-26 22:34 被阅读169次

    作者:shihuaping0918@163.com,转载请注明作者

    pomelo框架核心提供了sioconnector,udpconnector,hybirdconnector,mqttconnector。sioconnector基于socket.io,使用json通信,pc端通信。hybirdconnector基于tcp和websocket,使用二进制通信,主要用于手机端通信。mqttconnector使用mqtt协议通信,mqtt是二进制协议,是物联网协议,这个就是用于嵌入式设备通信。而udpconnector,这个看名字也知道是基于udp的,它也是使用二进制协议进行通信。这个主要用于网络环境不好,数据包小的场景。

    connector按照约定是要提供encode/decode的。sioconnector的encode/decode最简单。因为它是处理json的。在connector提供encode/decode之外,还可以单独设自定义的encode/decode。先看sioconnector,因为它比较简单。

    从decode看起,decode就是json解析。

    /**
     * Decode client message package.
     *
     * Package format:
     *   message id: 4bytes big-endian integer
     *   route length: 1byte
     *   route: route length bytes
     *   body: the rest bytes
     *
     * @param  {String} data socket.io package from client
     * @return {Object}      message object
     */
    Connector.decode = Connector.prototype.decode = function(msg) {
      var index = 0;
    
    //package ID
      var id = parseIntField(msg, index, PKG_ID_BYTES);
      index += PKG_ID_BYTES;
    //route体长
      var routeLen = parseIntField(msg, index, PKG_ROUTE_LENGTH_BYTES);
    //route字符串
      var route = msg.substr(PKG_HEAD_BYTES, routeLen);
      var body = msg.substr(PKG_HEAD_BYTES + routeLen);
    
      return {
        id: id,  
        route: route,
        body: JSON.parse(body) //json包体
      };
    };
    //取长度
    var parseIntField = function(str, offset, len) {
      var res = 0;
      for(var i=0; i<len; i++) {  //big-endian,网络字节序,高位在前
        if(i > 0) {
          res <<= 8;
        }
        res |= str.charCodeAt(offset + i) & 0xff;
      }
    
      return res;
    };
    

    从decode可以看出来,消息格式是有一个package id,一个route,然后就是消息体。消息体是json。而encode稍微复杂一点。

    Connector.encode = Connector.prototype.encode = function(reqId, route, msg) {
      if(reqId) { //有reqId,这个序号是客户端编的
        return composeResponse(reqId, route, msg);
      } else { //没有就是广播
        return composePush(route, msg);
      }
    };
    
    //注意这个地方,route被忽略了
    var composeResponse = function(msgId, route, msgBody) {
      return {
        id: msgId, //reqId,请求包序号
        body: msgBody // 回复消息体
      };
    };
    
    var composePush = function(route, msgBody) {
      return JSON.stringify({route: route, body: msgBody});
    };
    
    

    sioconnector.js的协议处理是非常简单的。字段也很少,但是body里面可能就千变万化了,这个是业务相关的。相信写过稍大一点项目的都很清楚,有的模块甚至有几百个命令,几百个命令就会产生几百种body。

    下面再分析一下hybirdconnector.js。到了这里就要正式讲一下pomelo的消息格式了,pomelo的消息分为两层,package和message。 以下引用原文:“pomelo的二进制协议包含两层编码:package和message。message层主要实现route压缩和protobuf压缩,message层的编码结果将传递给package层。package层主要实现pomelo应用基于二进制协议的握手过程,心跳和数据传输编码,package层的编码结果可以通过tcp,websocket等协议以二进制数据的形式进行传输。message层编码可选,也可替换成其他二进制编码格式,都不影响package层编码和发送。”

    package格式
    
    package分为header和body两部分。header描述package包的类型和包的长度,body则是需要传输的数据内容。具体格式如下:
    
    type - package类型,1个byte,取值如下。
    0x01: 客户端到服务器的握手请求以及服务器到客户端的握手响应
    0x02: 客户端到服务器的握手ack
    0x03: 心跳包
    0x04: 数据包
    0x05: 服务器主动断开连接通知
    length - body内容长度,3个byte的大端整数,因此最大的包长度为2^24个byte。
    body - 二进制的传输内容。
    
    message协议的主要作用是封装消息头,包括route和消息类型两部分,不同的消息类型有着不同的消息头,在消息头里面可能要打入message id(即requestId)和route信息。由于可能会有route压缩,而且对于服务端push的消息,message id为空,对于客户端请求的响应,route为空,因此message的头格式比较复杂。
    消息头分为三部分,flag,message id,route。
    pomelo消息头是可变的,会根据具体的消息类型和内容而改变。其中:
    flag位是必须的,占用一个byte,它决定了后面的消息类型和内容的格式;
    message id和route则是可选的。其中message id采用[varints 128变长编码](https://developers.google.com/protocol-buffers/docs/encoding#varints)方式,根据值的大小,长度在0~5byte之间。route则根据消息类型以及内容的大小,长度在0~255byte之间。
    

    从这段文字的描述可以看出来,我们刚才对sioconnector.js中encode和decode的分析都是基于message的,package部分的没有涉及到。

    本篇暂时不讲package部分,聚集点在于message部分。因为一发散的话,就没有重点了。

    hybirdconnector.js对于encode和decode的处理是,写了一个coder.js作为抽象。

    var coder = require('./common/coder');
    
    Connector.decode = Connector.prototype.decode = coder.decode;
    
    Connector.encode = Connector.prototype.encode = coder.encode;
    

    可以看到encode和decode独立出去了,做了一个单独的抽象,这样提高了复用性和扩展性。

    coder.js

    //这是pomelo的另一个开源组件
    var Message = require('pomelo-protocol').Message;
    var Constants = require('../../util/constants');
    //pomelo-logger也是另一个组件,不在核心模块里
    var logger = require('pomelo-logger').getLogger('pomelo', __filename);
    
    //encode函数
    var encode = function(reqId, route, msg) {
      if(!!reqId) {
        return composeResponse(this, reqId, route, msg);
      } else {
        return composePush(this, route, msg);
      }
    };
    
    //decode函数
    var decode = function(msg) {
      msg = Message.decode(msg.body);
      var route = msg.route;
    
      // decode use dictionary
      if(!!msg.compressRoute) {
        if(!!this.connector.useDict) {
          var abbrs = this.dictionary.getAbbrs();
          if(!abbrs[route]) {
            logger.error('dictionary error! no abbrs for route : %s', route);
            return null;
          }
          route = msg.route = abbrs[route];
        } else {
          logger.error('fail to uncompress route code for msg: %j, server not enable dictionary.', msg);
          return null;
        }
      }
    
      // decode use protobuf,protobuf协议解码
      if(!!this.protobuf && !!this.protobuf.getProtos().client[route]) {
        msg.body = this.protobuf.decode(route, msg.body);
      } else if(!!this.decodeIO_protobuf && !!this.decodeIO_protobuf.check(Constants.RESERVED.CLIENT, route)) {
        msg.body = this.decodeIO_protobuf.decode(route, msg.body);
      } else {
        try {
          msg.body = JSON.parse(msg.body.toString('utf8'));
        } catch (ex) {
          msg.body = {};
        }
      }
    
      return msg;
    };
    
    var composeResponse = function(server, msgId, route, msgBody) {
      if(!msgId || !route || !msgBody) {
        return null;
      }
      msgBody = encodeBody(server, route, msgBody);
      return Message.encode(msgId, Message.TYPE_RESPONSE, 0, null, msgBody);
    };
    
    var composePush = function(server, route, msgBody) {
      if(!route || !msgBody){
        return null;
      }
      msgBody = encodeBody(server, route, msgBody);
      // encode use dictionary
      var compressRoute = 0;
      if(!!server.dictionary) {
        var dict = server.dictionary.getDict();
        if(!!server.connector.useDict && !!dict[route]) {
          route = dict[route];
          compressRoute = 1;
        }
      }
      return Message.encode(0, Message.TYPE_PUSH, compressRoute, route, msgBody);
    };
    
    var encodeBody = function(server, route, msgBody) {
        // encode use protobuf
      if(!!server.protobuf && !!server.protobuf.getProtos().server[route]) {
        msgBody = server.protobuf.encode(route, msgBody);
      } else if(!!server.decodeIO_protobuf && !!server.decodeIO_protobuf.check(Constants.RESERVED.SERVER, route)) {
         msgBody = server.decodeIO_protobuf.encode(route, msgBody);
      } else { //兼容json
        msgBody = new Buffer(JSON.stringify(msgBody), 'utf8');
      }
      return msgBody;
    };
    
    module.exports = {
      encode: encode,
      decode: decode
    };
    

    对于coder.js中的encode和decode,里面调用的函数名和sioconnector.js中都是一致的。所不同的是对于body的处理,json的话直接用JSON相关的函数就可以了。从coder.js文件来看,所谓的二进制实际上是用的protobuf,不支持其它的二进制协议。代码是比较清晰的,就不再对代码做太多解释了。

    最后补充说明,协议这种东西,最好不要自定义二进制协议,更不要自定义类似query string那种文本协议。自定义二进制协议一是调试非常的麻烦,二是要做协议转换的时候,开发速度慢,出错率高,工作量大,自定义二进制协议少有能直接DSL生成转换代码的。最好的方案目前看到的也是用lua去映射,然后写一段通用代码去转换。而类query string的文本协议就更痛苦了,长的就是像这样子a=b&c=d。这种协议第一,要做编码转换,特殊字符转换。二,这种表示是一维的,key-value形式,也就是一个map转成了数组。它的扩展性非常地差,嵌套表达能力基本为0,因为嵌套表达就需要新增分隔符,多层嵌套以后,调协议会成为开发之间的导火索。同时作为文本协议,它的体积很大,无法压缩,也不能直观地格式化。非要用文本协议,直接用json就好了。

    相关文章

      网友评论

          本文标题:pomelo源码分析(6)--connector协议处理mess

          本文链接:https://www.haomeiwen.com/subject/swaudxtx.html