Node.js:RPC通信

2,299 阅读6分钟

RPC调用

和ajax相同点

第一个相同点:总之都是两个设备之间到调用
1. ajax是浏览器到服务器
2. rpc是服务器到服务器
第二个相同点:需要双方约定一种数据格式

和ajax不同点

第一点:
1. ajax使用dns做为寻址服务到
2. rpc一般是在内网寻址
第二点:
1. ajax应用层使用http(html/json)
2. rpc通信到时候一般会使用二进制协议,性能优势
第三点
rcp是基于tcp或udp协议通信

寻址/负载均衡

ajax寻址过程是拿域名去dns进行解析,然后在发起真正请求
rpc发起网络请求之前也是需要进行寻址到,因为不一定会使用ip进行请求,
可能会使用id(l5,vip)之类的统一标识符,拿这个id去寻址服务器解析,然后通过返回的ip发起真正的请求

TCP通信

单工通信:
永远只能是一端往另一端发送数据(client -- server)
半双工通信:
双方都可以发数据,但同一时间内只有一端可以发送数据,也可以理解为‘轮番单工通信’
全双工通信:
在client发送数据的时候,server也可以发送数据

二进制协议

. 更小的数据包体积
. 更快的编码速度
rpc [0001 0000 1111 0101]

Node.js Buffer编解码二进制数据包

buffer模块介绍

创建buffer的三种方式:
1. Buffer.alloc
2. Buffer.from
3. Buffer.allocUnsafe

// from方法表示通过现有的数据结构创建一个buffer
const buffer1 = Buffer.from('test');
const buffer2 = Buffer.from([1, 2, 3]);

// alloc方法表示通过指定长度创建一个buffer·
const buffer3 = Buffer.alloc(10);

console.log(buffer1); // <Buffer 74 65 73 74>
console.log(buffer2); // <Buffer 01 02 03>
console.log(buffer3); // <Buffer 00 00 00 00 00 00 00 00 00 00>

Buffer.allocUnsafe:该方法会涉及buffer模块的内存管理机制

buffer写入的方法:

const buffer2 = Buffer.from([1, 2, 3]);

buffer2.writeInt8(12, 0)
console.log(buffer2); // <Buffer 0c 02 03>

buffer2.writeInt16BE(512, 1)
console.log(buffer2);
// <Buffer 0c 02 00> BE/LE的区别是 高位和地位的排布顺序,请仔细观察该行和下面的结果区别


buffer2.writeInt16LE(512, 1)
console.log(buffer2); // <Buffer 0c 00 02>  00 02 和上面  02 00

使用BE/LE的原因在于不同设备会使用不同标准,具体要使用什么参数需要跟后台协商

protocol buffers库:

可以达到类似JSON.stringfly这样简单的操作,不需要像上面那样麻烦的写二进制

https://github.com/protocolbuffers/protobuf
这个兼容了前端情况,所以没有上面那个好
// test.proto

message Test {
  required float num  = 1;
  required string payload = 2;
}
// index.js

const fs = require('fs');
const protobuf = require('protocol-buffers');
const schema = protobuf(fs.readFileSync(__dirname + '/test.proto', 'utf-8'));

console.log(schema)
// Messages {
//   Test: {
//     type: 2,
//     message: true,
//     name: 'Test',
//     buffer: true,
//     encode: [Function: encode] { bytes: 0 },
//     decode: [Function: decode] { bytes: 0 },
//     encodingLength: [Function: encodingLength],
//     dependencies: [ [Object], [Object] ]
//   }
// }
const buffer = schema.Test.encode({
  num: 2,
  payload: 'test'
})
console.log(buffer) // <Buffer 0d 00 00 00 40 12 04 74 65 73 74>
console.log(schema.Test.decode(buffer)) // { num: 2, payload: 'test' }

Node.js net建立多路复用的rpc通道

net模块

net模块类似http模块、比较简单

既然是通道,就会有两端,这里我们用clinet和server来代表

单工通信案例

半双工通信案例

全双工通信

  • server.js
const net = require('net');

const server = net.createServer((socket) => {

    let oldBuffer = null;
    socket.on('data', function (buffer) {
        // 把上一次data事件使用残余的buffer接上来
        if (oldBuffer) {
            buffer = Buffer.concat([oldBuffer, buffer]);
        }

        let packageLength = 0;
        // 只要还存在可以解成完整包的包长
        while (packageLength = checkComplete(buffer)) {
            const package = buffer.slice(0, packageLength);
            buffer = buffer.slice(packageLength);

            // 把这个包解成数据和seq
            const result = decode(package);

            // 计算得到要返回的结果,并write返回
            socket.write(
                encode(LESSON_DATA[result.data], result.seq)
            );
        }

        // 把残余的buffer记下来
        oldBuffer = buffer;
    })

});

server.listen(4000);

/**
 * 二进制包编码函数
 * 在一段rpc调用里,服务端需要经常编码rpc调用时,业务数据的返回包
 */
function encode(data, seq) {
    // 正常情况下,这里应该是使用 protobuf 来encode一段代表业务数据的数据包
    // 为了不要混淆重点,这个例子比较简单,就直接把课程标题转buffer返回
    const body = Buffer.from(data)

    // 一般来说,一个rpc调用的数据包会分为定长的包头和不定长的包体两部分
    // 包头的作用就是用来记载包的序号和包的长度,以实现全双工通信
    const header = Buffer.alloc(6);
    header.writeInt16BE(seq)
    header.writeInt32BE(body.length, 2);

    const buffer = Buffer.concat([header, body])

    return buffer;
}

/**
 * 二进制包解码函数
 * 在一段rpc调用里,服务端需要经常解码rpc调用时,业务数据的请求包
 */
function decode(buffer) {
    const header = buffer.slice(0, 6);
    const seq = header.readInt16BE();

    // 正常情况下,这里应该是使用 protobuf 来decode一段代表业务数据的数据包
    // 为了不要混淆重点,这个例子比较简单,就直接读一个Int32即可
    const body = buffer.slice(6).readInt32BE()

    // 这里把seq和数据返回出去
    return {
        seq,
        data: body
    }
}

/**
 * 检查一段buffer是不是一个完整的数据包。
 * 具体逻辑是:判断header的bodyLength字段,看看这段buffer是不是长于header和body的总长
 * 如果是,则返回这个包长,意味着这个请求包是完整的。
 * 如果不是,则返回0,意味着包还没接收完
 * @param {} buffer 
 */
function checkComplete(buffer) {
    if (buffer.length < 6) {
        return 0;
    }
    const bodyLength = buffer.readInt32BE(2);
    return 6 + bodyLength
}

// 假数据
const LESSON_DATA = {
    136797: "01 | 课程介绍",
    136798: "02 | 内容综述",
    136799: "03 | Node.js是什么?",
    136800: "04 | Node.js可以用来做什么?",
    136801: "05 | 课程实战项目介绍",
    136803: "06 | 什么是技术预研?",
    136804: "07 | Node.js开发环境安装",
    136806: "08 | 第一个Node.js程序:石头剪刀布游戏",
    136807: "09 | 模块:CommonJS规范",
    136808: "10 | 模块:使用模块规范改造石头剪刀布游戏",
    136809: "11 | 模块:npm",
    141994: "12 | 模块:Node.js内置模块",
    143517: "13 | 异步:非阻塞I/O",
    143557: "14 | 异步:异步编程之callback",
    143564: "15 | 异步:事件循环",
    143644: "16 | 异步:异步编程之Promise",
    146470: "17 | 异步:异步编程之async/await",
    146569: "18 | HTTP:什么是HTTP服务器?",
    146582: "19 | HTTP:简单实现一个HTTP服务器"
}
  • client.js
const net = require('net');

const socket = new net.Socket({});

socket.connect({
    host: '127.0.0.1',
    port: 4000
});

const LESSON_IDS = [
  "136797",
  "136798",
  "136799",
  "136800",
  "136801",
  "136803",
  "136804",
  "136806",
  "136807",
  "136808",
  "136809",
  "141994",
  "143517",
  "143557",
  "143564",
  "143644",
  "146470",
  "146569",
  "146582"
]

let id = Math.floor(Math.random() * LESSON_IDS.length);

let oldBuffer = null;
socket.on('data', (buffer) => {
    // 把上一次data事件使用残余的buffer接上来
    if (oldBuffer) {
        buffer = Buffer.concat([oldBuffer, buffer]);
    }
    let completeLength = 0;

    // 只要还存在可以解成完整包的包长
    while (completeLength = checkComplete(buffer)) {
        const package = buffer.slice(0, completeLength);
        buffer = buffer.slice(completeLength);

        // 把这个包解成数据和seq
        const result = decode(package);
        console.log(`包${result.seq},返回值是${result.data}`);
    }

    // 把残余的buffer记下来
    oldBuffer = buffer;
})


let seq = 0;
/**
 * 二进制包编码函数
 * 在一段rpc调用里,客户端需要经常编码rpc调用时,业务数据的请求包
 */
function encode(data) {
    // 正常情况下,这里应该是使用 protobuf 来encode一段代表业务数据的数据包
    // 为了不要混淆重点,这个例子比较简单,就直接把课程id转buffer发送
    const body = Buffer.alloc(4);
    body.writeInt32BE(LESSON_IDS[data.id]);

    // 一般来说,一个rpc调用的数据包会分为定长的包头和不定长的包体两部分
    // 包头的作用就是用来记载包的序号和包的长度,以实现全双工通信
    const header = Buffer.alloc(6);
    header.writeInt16BE(seq)
    header.writeInt32BE(body.length, 2);

    // 包头和包体拼起来发送
    const buffer = Buffer.concat([header, body])

    console.log(`包${seq}传输的课程id为${LESSON_IDS[data.id]}`);
    seq++;
    return buffer;
}

/**
 * 二进制包解码函数
 * 在一段rpc调用里,客户端需要经常解码rpc调用时,业务数据的返回包
 */
function decode(buffer) {
    const header = buffer.slice(0, 6);
    const seq = header.readInt16BE();

    const body = buffer.slice(6)

    return {
        seq,
        data: body.toString()
    }
}

/**
 * 检查一段buffer是不是一个完整的数据包。
 * 具体逻辑是:判断header的bodyLength字段,看看这段buffer是不是长于header和body的总长
 * 如果是,则返回这个包长,意味着这个请求包是完整的。
 * 如果不是,则返回0,意味着包还没接收完
 * @param {} buffer 
 */
function checkComplete(buffer) {
    if (buffer.length < 6) {
        return 0;
    }
    const bodyLength = buffer.readInt32BE(2);
    return 6 + bodyLength
}


for (let k = 0; k < 100; k++) {
    id = Math.floor(Math.random() * LESSON_IDS.length);
    socket.write(encode({ id }));
}