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里的包体长度进行切包。
代码较长,不贴了。