RPC

9 2月

RPC和前端熟悉的Ajax一样,都是两个计算机之间,约定一个数据格式进行的网络通信。区别是:

  • IP寻址:Ajax域名要经过DNS解析。RPC不一定使用DNS寻址服务,RPC不用域名,例如阿里,腾讯内网会用个id,经由内网的寻址服务器获取到IP。
  • 数据格式:Ajax的应用层协议用http。RPC用二进制协议,有更小的体积和解编码速度。
  • 通信方式:Ajax都是基于TCP协议。RPC基于TCP(可以全双工,也可以半双工通信)或UDP协议。

数据格式

RPC数据是二进制协议,所以要用Node的内置Buffer模块。创建Buffer用from,alloc方法,读取Buffer用write方法

const buffer1 = Buffer.from("hello")    // <Buffer 68 65 6c 6c 6f>
const buffer2 = Buffer.from([1,2,3,4])	// <Buffer 01 02 03 04>
const buffer3 = Buffer.alloc(20)	// <Buffer 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00>

// 原始 buffer2:<Buffer 01 02 03 04>
buffer2.writeInt8(12, 1)	// <Buffer 01 0c 03 04>,从第二位开始写入12(0c)
buffer2.writeInt16BE(512, 2)	// <Buffer 01 0c 02 00>,从第三位开始写入512(0200),BE表示高位排前面
buffer2.writeInt16LE(512, 2)	// <Buffer 01 0c 00 02>,从第三位开始写入512(0200),LE表示低位排前面

当然网上很多开源包代码写起来比Buffer更容易,例如protocol-buffers,能让你处理二进制协议像处理JSON.stringify一样容易:

// xxx.proto
message Shop {
    required float id = 1;
    required string name = 2;
    repeated Deals deals = 3;
}

message Deals {
    required float id = 1;
    required float price = 2;
}


// xxx.js
const fs = require('fs');
const protobuf = require('protocol-buffers');

const shop = protobuf(fs.readFileSync(`${__dirname}/test.proto`));
const buffer = shop.Shop.encode({
    id: 4,
    name: '烧烤店',
    deals: [
        { id: 5, price: 10}, 
        { id: 6, price: 12}
    ]
})

console.log(shop.Shop.decode(buffer));     
// { id: 4, name: '烧烤店', deals: [ { id: 5, price: 10 }, { id: 6, price: 12 } ] }

通信方式(基础)

处理完数据,还需要为RPC建立通信方式,用内置的net模块就行,发起调用的称为client端,接受调用的称为server端。

server端创建tcp服务并监听端口:

const net = require('net')

const server = net.createServer((socket) => {
    socket.on('data', function(buffer) {
        console.log(buffer)
    })
})
server.listen(4000)

client端创建socket服务并连接服务器:

const net = require('net')

const socket = new net.Socket({})
socket.connect({
    host: '127.0.0.1',
    port: 4000
})

socket.write('hello rpc')

通信方式(半双工)

RPC可以设计成半双工通信,服务端响应回来后,客户端再次发起新请求

server端:(收到客户端请求的类目id后,模拟服务器处理业务500ms后返回给客户端类目名)

const net = require('net');

const category = { 1: "烧烤店", ... }

const server = net.createServer((socket) => {   // 创建tcp服务器
    socket.on('data', function(buffer) {
        const id = buffer.readInt32BE()

        setTimeout(()=> {
            socket.write(Buffer.from(category[id])) 
        }, 500)
    })
});
server.listen(4000);

client端:(收到服务端相应后,再发送给服务端新请求)

const net = require('net')

const categoryIds = ["1", ...]

const socket = new net.Socket({})

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

let buffer = Buffer.alloc(4)
buffer.writeInt32BE(categoryIds[0])  // 模拟发送给服务端类目id
socket.write(buffer)

socket.on('data', (buffer) => {   // 收到服务端相应后,再发送给服务端类目id
    buffer = Buffer.alloc(4)
    buffer.writeInt32BE(categoryIds[1])
    socket.write(buffer);
})

通信方式(全双工)

全双工通信主要解决两个问题:

  • 请求与响应间的配对:需要有个sequenceid来关联请求与响应。
  • 切分粘包:tcp请求为了优化性能会自动将多个请求包合并成一个包,所以服务端需要切分粘包,将合并的请求切分开分部处理后,返回给客户端。

设计思路是每个包分包头header和包体body。包头定长例如定长6位,前2位是sequence序号,后4位是包体的length。包体不定长。每次服务端根据header里的包体长度进行切包。

代码较长,不贴了。

发表评论

电子邮件地址不会被公开。 必填项已用*标注