09.protobuf

protobuf是谷歌开源的二进制字节传输,由于手游要适配用户的不同手机,减少网络请求包能减小用户的手机发热耗电等问题,所以webscoket中压缩用户传输的数据就显得很重要

protobuf

先跟着官网启动一个grpc服务 grpc

gRPC 是一个高性能的远程过程调用框架,使用 protobuf 定义二进制的数据结构。相比之下 RESTful API 是基于 http 超文本协议,性能可想而知 gRPC 是高不少的。gRPC 支持node.js,适用微服务、client-server 等场景

proto

proto文件是谷歌约定的语法,类似于.d.ts对于接口或者类的方法约束

proto
syntax = "proto3";

option java_multiple_files = true;
option java_package = "io.grpc.examples.helloworld";
option java_outer_classname = "HelloWorldProto";
option objc_class_prefix = "HLW";

package helloworld;

// The greeting service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}

  rpc SayHelloStreamReply (HelloRequest) returns (stream HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}

protobuf的应用

client

ts
var PROTO_PATH = __dirname + '/../../protos/helloworld.proto';

var parseArgs = require('minimist');
var grpc = require('@grpc/grpc-js');
var protoLoader = require('@grpc/proto-loader');
var packageDefinition = protoLoader.loadSync(
  PROTO_PATH,
  {
    keepCase: true,
    longs: String,
    enums: String,
    defaults: true,
    oneofs: true
  });
var hello_proto = grpc.loadPackageDefinition(packageDefinition).helloworld;

function main() {
  var argv = parseArgs(process.argv.slice(2), {
    string: 'target'
  });
  var target;
  if (argv.target) {
    target = argv.target;
  } else {
    target = 'localhost:50051';
  }
  var client = new hello_proto.Greeter(target,
    grpc.credentials.createInsecure());
  var user;
  if (argv._.length > 0) {
    user = argv._[0];
  } else {
    user = 'world111';
  }
  client.sayHello({ name: user }, function (err, response) {
    console.log('Greeting:', response.message);
  });
}

main();

server

ts
var PROTO_PATH = __dirname + '/../../protos/helloworld.proto';

var grpc = require('@grpc/grpc-js');
var protoLoader = require('@grpc/proto-loader');
var packageDefinition = protoLoader.loadSync(
    PROTO_PATH,
    {keepCase: true,
     longs: String,
     enums: String,
     defaults: true,
     oneofs: true
    });
var hello_proto = grpc.loadPackageDefinition(packageDefinition).helloworld;

/**
 * Implements the SayHello RPC method.
 */
function sayHello(call, callback) {
  callback(null, {message: 'Hello ' + call.request.name});
}

/**
 * Starts an RPC server that receives requests for the Greeter service at the
 * sample server port
 */
function main() {
  var server = new grpc.Server();
  server.addService(hello_proto.Greeter.service, {sayHello: sayHello});
  server.bindAsync('0.0.0.0:50051', grpc.ServerCredentials.createInsecure(), () => {
    server.start();
  });
}

main();

开拔

上面呢是个demo,简单的protobuf使用,很简单我就不详细介绍了 接下来引入protobuf包的js cli和生成js文件的包,自动生成的js

bash
import protoRoot from "../common/idl/auto-gen-ws";

如果没有的话cd到root然后yarn common

server to client

这个方法在之前是ws通信的时候校验了token的,然后需要调用发送信息实现一个sendMessgae方法

ts
  import { AuthClient, CheckTokenReq, RpcFunc, getProtoPathByRpcFunc } from '../common';

   async handleMessage(ws: WebSocket, buffer: Buffer): Promise<void> {
    const name = buffer.readUint8(0);
    const path = getProtoPathByRpcFunc(name, 'req')
    const coder = protoRoot.lookup(path)
    const data = coder.decode(buffer.slice(1))

    if (name === RpcFunc.enterGame) {
      const res = await this.checkTocken(data)
      this.sendMessage(ws, name, res)
    } else {
      // TODO 和副本通信
    }
  }

  sendMessage(ws: Webscoket, name: RpcFunc,res: any){
    const headerBuffer = Buffer.alloc(1)
    headerBuffer.writeUInt8(name)

    const path = getProtoPathByRpcFunc(name, 'res')
    const coder = protoRoot.lookup(path)
    const dataBuffer = coder.encode(res).finish()

    const buffer = Buffer.concat([headerBuffer, dataBuffer])
    ws.send(buffer)
  }

  // checkTocken也需要改成promise
  checkTocken({ token }: { token: string }) {
    return new Promise((rs) => {
      const client = new AuthClient('localhost:3333', grpc.credentials.createInsecure());
      const req = new CheckTokenReq()
      req.setToken(token);

      client.checkToken(req, (err, message) => {
        rs(message.toObject())
      })
    })
  }

client to server

ts
  this.ws.onmessage = (e) => {
    try {
      const ta = new Uint8Array(e.data);
      const name = ta[0];
      const path = getProtoPathByRpcFunc(name, 'res')
      const coder = protoRoot.lookup(path)
      const data = coder.decode(ta.slice(1))
      try {
        if (this.maps.has(name) && this.maps.get(name).length) {
          this.maps.get(name).forEach(({ cb, ctx }) => cb.call(ctx, data));
        } else {
          console.log(`no ${name} message or callback, maybe timeout`);
        }
      } catch (error) {
        console.log("call error:", error);
      }
    } catch (error) {
      console.log("onmessage parse error:", error);
    }
  };

在onmessage里面需要把typeArray转成数组

send里面同样需要改造一下

ts
  async send(name: RpcFunc, data: IData) {
    const path = getProtoPathByRpcFunc(name, 'req')
    const coder = protoRoot.lookup(path)
    const typeArrayData = coder.encode(data).finish()
    const arraybufferData = new ArrayBuffer(typeArrayData.length + 1)
    const view = new DataView(arraybufferData)
    let index = 0
    view.setUint8(index++, name)
    for (let i = 0; i < typeArrayData.length; i++) {
      view.setUint8(index++, typeArrayData[i])
    }

    this.ws.send(view.buffer)
  }
10.柏林噪声生成地图
08.grpc