返回顶部
首页 > 资讯 > 前端开发 > 其他 >什么是RPC?聊聊node中怎么实现 RPC 通信
  • 531
分享到

什么是RPC?聊聊node中怎么实现 RPC 通信

Node.jsRPC 2022-11-22 23:11:38 531人浏览 安东尼
摘要

RPC vs HTTP相同点都是两台计算机之间的网络通信。ajax是浏览器和服务器之间的通行,rpc是服务器与服务器之间的通行需要双方约定一个数据格式不同点寻址服务器不同ajax 是使用 DNS作为寻址服务获取域名所对应的ip地址,浏览器拿

RPC vs HTTP

相同点

  • 都是两台计算机之间的网络通信。ajax是浏览器和服务器之间的通行,rpc是服务器与服务器之间的通行
  • 需要双方约定一个数据格式

不同点

  • 寻址服务器不同

ajax 是使用 DNS作为寻址服务获取域名所对应的ip地址,浏览器拿到ip地址之后发送请求获取数据。

RPC一般是在内网里面相互请求,所以它一般不用DNS做寻址服务。因为在内网,所以可以使用规定的id或者一个虚拟vip,比如v5:8001,然后到寻址服务器获取v5所对应的ip地址。

  • 应用层协议不同

ajax使用Http协议,它是一个文本协议,我们交互数据的时候文件格式要么是html,要么是JSON对象,使用json的时候就是key-value的形式。

RPC采用二进制协议。采用二进制传输,它传输的包是这样子的[0001 0001 0111 0110 0010],里面都是二进制,一般采用那几位表示一个字段,比如前6位是一个字段,依次类推。

这样就不需要http传输json对象里面的key,所以有更小的数据体积。

因为传输的是二进制,更适合于计算机来理解,文本协议更适合人类理解,所以计算机去解读各个字段的耗时是比文本协议少很多的。

RPC采用二进制有更小的数据体积,及更快的解读速度。

  • tcp通讯方式
  • 单工通信:只能客户端给服务端发消息,或者只能服务端给客户端发消息

  • 半双工通信:在某个时间段内只能客户端给服务端发消息,过了这个时间段服务端可以给客户端发消息。如果把时间分成很多时间片,在一个时间片内就属于单工通信

  • 全双工通信:客户端和服务端能相互通信

选择这三种通信方式的哪一种主要考虑的因素是:实现难度和成本。全双工通信是要比半双工通信的成本要高的,在某些场景下还是可以考虑使用半双工通信。

ajax是一种半双工通信。http是文本协议,但是它底层是tcp协议,http文本在tcp这一层会经历从二进制数据流到文本的转换过程。

理解RPC只是在更深入地理解前端技术。

buffer编解码二进制数据包

创建buffer

buffer.from: 从已有的数据创建二进制

const buffer1 = Buffer.from('geekbang')
const buffer2 = Buffer.from([0, 1, 2, 3, 4])


<Buffer 67 65 65 6b 62 61 6e 67>
<Buffer 00 01 02 03 04>

buffer.alloc: 创建一个空的二进制

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>

往buffer里面写东西

  • buffer.write(string, offset): 写入字符串
  • buffer.writeInt8(value, offset): int8表示二进制8位(8位表示一个字节)所能表示的整数,offset开始写入之前要跳过的字节数。
  • buffer.writeInt16BE(value, offset): int16(两个字节数),表示16个二进制位所能表示的整数,即32767。超过这个数程序会报错。
const buffer = Buffer.from([1, 2, 3, 4]) // <Buffer 01 02 03 04>

// 往第二个字节里面写入12
buffer.writeInt8(12, 1) // <Buffer 01 0c 03 04>

大端BE与小端LE:主要是对于2个以上字节的数据排列方式不同(writeInt8因为只有一个字节,所以没有大端和小端),大端的话就是低位地址放高位,小端就是低位地址放低位。如下:

const buffer = Buffer.from([1, 2, 3, 4])

buffer.writeInt16BE(512, 2) // <Buffer 01 02 02 00>
buffer.writeInt16LE(512, 2) // <Buffer 01 02 00 02>

RPC传输的二进制如何表示传递的字段

PC传输的二进制是如何表示字段的呢?现在有个二进制包[00, 00, 00, 00, 00, 00, 00],我们假定前三个字节表示一个字段值,后面两个表示一个字段的值,最后两个也表示一个字段的值。那写法如下:

writeInt16BE(value, 0)
writeInt16BE(value, 2)
writeInt16BE(value, 4)

发现像这样写,不仅要知道写入的值,还要知道值的数据类型,这样就很麻烦。不如json格式那么方便。针对这种情况业界也有解决方案。npm有个库protocol-buffers,把我们写的参数转化为buffer

// test.proto 定义的协议文件
message Column {
  required float num  = 1;
  required string payload = 2;
}
// index.js
const fs = require('fs')
var protobuf = require('protocol-buffers')
var messages = protobuf(fs.readFileSync('test.proto'))

var buf = messages.Column.encode({
	num: 42,
	payload: 'hello world'
})
console.log(buf)
// <Buffer 0d 00 00 28 42 12 0b 68 65 6c 6c 6f 20 77 6f 72 6c 64>

var obj = messages.Column.decode(buf)
console.log(obj)
// { num: 42, payload: 'hello world' }

net建立RPC通道

半双工通信

服务端代码:

const net = require('net')

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服务器'
}

const server = net.createServer(Socket => {
  // 监听客户端发送的消息
  socket.on('data', buffer => {
    const lessonId = buffer.readInt32BE()
    setTimeout(() => {
      // 往客户端发送消息
      socket.write(LESSON_DATA[lessonId])
    }, 1000)
  })
})

server.listen(4000)

客户端代码:

const net = require('net')

const socket = new net.Socket({})

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

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

let buffer = Buffer.alloc(4)
buffer.writeInt32BE(LESSON_IDS[Math.floor(Math.random() * LESSON_IDS.length)])

// 往服务端发送消息
socket.write(buffer)

// 监听从服务端传回的消息
socket.on('data', buffer => {
  console.log(buffer.toString())

  // 获取到数据之后再次发送消息
  buffer = Buffer.alloc(4)
  buffer.writeInt32BE(LESSON_IDS[Math.floor(Math.random() * LESSON_IDS.length)])

  socket.write(buffer)
})

以上半双工通信步骤如下:

  • 客户端发送消息 socket.write(buffer)
  • 服务端接受消息后往客户端发送消息 socket.write(buffer)
  • 客户端接受消息后再次发送消息

这样在一个时间端之内,只有一个端往另一个端发送消息,这样就实现了半双工通信。那如何实现全双工通信呢,也就是在客户端往服务端发送消息的同时,服务端还没有消息返回给客户端之前,客户端又发送了一个消息给服务端。

全双工通信

先来看一个场景:

image.png

客户端发送了一个id1的请求,但是服务端还来不及返回,接着客户端又发送了一个id2的请求。

等了一个之后,服务端先把id2的结果返回了,然后再把id1的结果返回。

那如何结果匹配到对应的请求上呢?

如果按照时间顺序,那么id1的请求对应了id2的结果,因为id2是先返回的;id2的请求对应了id1的结果,这样就导致请求包和返回包错位的情况。

怎么办呢?

我们可以给请求包和返回包都带上序号,这样就能对应上。

错位处理

客户端代码:

socket.on('data', buffer => {
  // 包序号
  const seqBuffer = buffer.slice(0, 2)
  // 服务端返回的内容
  const titleBuffer = buffer.slice(2)
    
  console.log(seqBuffer.readInt16BE(), titleBuffer.toString())
})

// 包序号
let seq = 0
function encode(index) {
  // 请求包的长度现在是6 = 2(包序号) + 4(课程id)
  buffer = Buffer.alloc(6)
  buffer.writeInt16BE(seq)
  buffer.writeInt32BE(LESSON_IDS[index], 2)

  seq++
  return buffer
}

// 每50ms发送一次请求
setInterval(() => {
  id = Math.floor(Math.random() * LESSON_IDS.length)
  socket.write(encode(id))
}, 50)

服务端代码:

const server = net.createServer(socket => {
  socket.on('data', buffer => {
    // 把包序号取出
    const seqBuffer = buffer.slice(0, 2)
    // 从第2个字节开始读取
    const lessonId = buffer.readInt32BE(2)
    setTimeout(() => {
      const buffer = Buffer.concat([
        seqBuffer,
        Buffer.from(LESSON_DATA[lessonId])
      ])
      socket.write(buffer)
      // 这里返回时间采用随机的,这样就不会按顺序返回,就可以测试错位的情况
    }, 10 + Math.random() * 1000)
  })
})
  • 客户端把包序号和对应的id给服务端
  • 服务端取出包序号和对应的id,然后把包序号和id对应的内容返回给客户端,同时设置返回的时间是随机的,这样就不会按照顺序返回。

粘包处理

如果我们这样发送请求:

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

我们发现服务端接收到的信息如下:

<Buffer 00 00 00 02 16 64 00 01 00 02 16 68 00 02 00 02 31 1c 00 03 00 02 3c 96 00 04 00 02 16 68 00 05 00 02 16 5e 00 06 00 02 16 66 00 07 00 02 16 67 00 08 ... 550 more bytes>

这是因为TCP自己做的一个优化,它会把所有的请求包拼接在一起,这样就会产生粘包的现象。

服务端需要把包进行拆分,拆分成100个小包。

那如何拆分呢?

首先客户端发送的数据包包括两部分:定长的包头和不定长的包体

包头又分为两部分:包序号及包体的长度。只有知道包体的长度,才能知道从哪里进行分割。

let seq = 0
function encode(data) {
    // 正常情况下,这里应该是使用 protocol-buffers 来encode一段代表业务数据的数据包
    // 为了不要混淆重点,这个例子比较简单,就直接把课程id转buffer发送
    const body = Buffer.alloc(4);
    body.writeInt32BE(LESSON_IDS[data.id]);

    // 一般来说,一个rpc调用的数据包会分为定长的包头和不定长的包体两部分
    // 包头的作用就是用来记载包的序号和包的长度,以实现全双工通信
    const header = Buffer.alloc(6); // 包序号占2个字节,包体长度占4个字节,共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;
}

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

服务端进行拆包

const server = net.createServer(socket => {
  let oldBuffer = null
  socket.on('data', buffer => {
    // 把上一次data事件使用残余的buffer接上来
    if (oldBuffer) {
      buffer = Buffer.concat([oldBuffer, buffer])
    }
    let packageLength = 0
    // 只要还存在可以解成完整包的包长
    while ((packageLength = checkComplete(buffer))) {
      // 确定包的长度后进行slice分割
      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
  })
})

checkComplete 函数的作用来确定一个数据包的长度,然后进行分割:

function checkComplete(buffer) {
  // 如果包的长度小于6个字节说明只有包头,没有包体,那么直接返回0
  if (buffer.length <= 6) {
    return 0
  }
  // 读取包头的第二个字节,取出包体的长度
  const bodyLength = buffer.readInt32BE(2)
  // 请求包包括包头(6个字节)和包体body
  return 6 + bodyLength
}

decode对包进行解密:

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
  }
}

encode把客户端想要的数据转化为二进制返回,这个包同样包括包头和包体,包头又包括包需要包序号和包体的长度。

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
}

当客户端收到服务端发送的包之后,同样也要进行拆包,因为所有的包同样都粘在一起了:

 <Buffer 00 00 00 00 00 1d 30 36 20 7c 20 e4 bb 80 e4 b9 88 e6 98 af e6 8a 80 e6 9c af e9 a2 84 e7 a0 94 ef bc 9f 00 01 00 00 00 1d 30 36 20 7c 20 e4 bb 80 e4 ... 539 more bytes>

因此,客户端也需要拆包,拆包策略与服务端的拆包策略是一致的:

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
})

到这里就实现了双全工通行,这样客户端和服务端随时都可以往对方发小消息了。

以上就是什么是RPC?聊聊node中怎么实现 RPC 通信的详细内容,更多请关注编程网其它相关文章!

--结束END--

本文标题: 什么是RPC?聊聊node中怎么实现 RPC 通信

本文链接: https://lsjlt.com/news/173436.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • 什么是RPC?聊聊node中怎么实现 RPC 通信
    RPC vs HTTP相同点都是两台计算机之间的网络通信。ajax是浏览器和服务器之间的通行,RPC是服务器与服务器之间的通行需要双方约定一个数据格式不同点寻址服务器不同ajax 是使用 DNS作为寻址服务获取域名所对应的ip地址,浏览器拿...
    99+
    2022-11-22
    Node.js RPC
  • node中如何实现RPC通信
    本篇内容主要讲解“node中如何实现RPC通信”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“node中如何实现RPC通信”吧!什么是RPC?RPC:Remote Procedure Call(远...
    99+
    2023-07-04
  • 聊聊Node中怎么用async函数
    借助于新版 V8 引擎,Node 从 7.6 开始支持 async 函数特性。今年 10 月 31 日,Node.js 8 也开始成为新的长期支持版本,因此你完全可以放心大胆地在你的代码中使用 async 函数了。在这边文章里,我会简要地介...
    99+
    2023-05-14
    async node
  • node怎么实现语音聊天
    本教程操作环境:Windows10系统、node-v16.18.0版、DELL G3电脑node怎么实现语音聊天?基于nodejs的语音聊天描述程序在 iamshaunjp 的群聊功能基础上利用webRTC技术,添加了语音群聊功能,在其他人...
    99+
    2023-05-14
    node
  • php中rpc框架是什么
    这篇文章主要介绍php中rpc框架是什么,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!什么是RPC框架?通常我们调用一个php中的方法,比如这样一个函数方法: localAdd(10, 20),localAdd方法的...
    99+
    2023-06-15
  • linux中rpc指的是什么
    本篇内容介绍了“linux中rpc指的是什么”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!在linux中,rpc是远程过程调用的意思,是Re...
    99+
    2023-06-30
  • Go gRPC怎么实现Simple RPC
    本篇内容介绍了“Go gRPC怎么实现Simple RPC”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!前言gRPC主要...
    99+
    2023-07-02
  • dubbo怎么实现rpc调用
    Dubbo是一个基于Java的高性能RPC框架,可以实现远程服务的调用。以下是使用Dubbo实现RPC调用的步骤:1. 定义服务接口...
    99+
    2023-10-23
    dubbo
  • .Net Core微服务rpc框架GRPC通信的方法是什么
    本文小编为大家详细介绍“.Net Core微服务rpc框架GRPC通信的方法是什么”,内容详细,步骤清晰,细节处理妥当,希望这篇“.Net Core微服务rpc框架GRPC通信的方法是什么”文章能帮助大家解决疑惑,下面跟...
    99+
    2023-06-26
  • 什么是模块化?聊聊Node模块化的那些事
    在上方的定义中未免有一些晦涩难懂,简单的给大家举个例子:我们小时候玩的小霸王游戏机,当我们玩烦了一款游戏的时候,我们不可能直接更换一个游戏机呀,我们可以通过更换游戏带从而体验各种不同的游戏。这种形式就是模块化,把游戏分化成一个个小模块,当我...
    99+
    2022-11-23
    nodejs node 模块化
  • 怎么用Springboot和Netty实现rpc
    这篇文章主要介绍“怎么用Springboot和Netty实现rpc”,在日常操作中,相信很多人在怎么用Springboot和Netty实现rpc问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”怎么用Spring...
    99+
    2023-06-29
  • 聊聊Go怎么实现SSE?需要注意什么?
    本篇文章给大家带来了关于Go的相关知识,其中主要跟大家聊一聊Go用什么方式实现SSE,以及需要注意的事项,感兴趣的朋友下面一起来看一下吧,希望对大家有帮助。一、服务端代码package main import ( "fmt...
    99+
    2023-05-14
    SSE go语言
  • 聊聊php中的type是什么意思
    PHP是一种广泛使用的程序设计语言,它可以用于Web开发和其他类似用途。在PHP中,Type是一个很常见的概念。Type 的意思是数据类型,它描述了一个数据所属的类别。在PHP中,一共有八种基本数据类型:布尔型 bool:表示布尔值,取值为...
    99+
    2023-05-14
    php
  • 聊聊Python中的@符号是什么意思
    Python中的@符号是装饰器的意思。Python中装饰器本质上就是一个函数,它可以让其他函数在不需要做任何代码变动的前提下增加额外的功能,装饰器的返回值也是一个函数对象(函数的指针...
    99+
    2024-04-02
  • 聊聊JavaScript怎么实现视差滚动
    本篇文章给大家带来了关于js的相关知识,其中主要跟大家聊一聊有关视差滚动效果,以及如何用js实现视差滚动 ,感兴趣的朋友下面一起来看一下吧,希望对大家有帮助。前言现代网站设计已经不再依赖于简单的滚动页面,而是使用各种动画和交互来吸引用户的注...
    99+
    2023-05-14
    前端 JavaScript CSS
  • 怎么在HTML5中实现实时语音通话聊天
    本篇文章为大家展示了怎么在HTML5中实现实时语音通话聊天,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。(1)数据传输github demo中考虑到减少对服务器的依赖,因此采用了WebRTC P2P...
    99+
    2023-06-09
  • Node中的进程间通信怎么实现
    这篇“Node中的进程间通信怎么实现”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“Node...
    99+
    2024-04-02
  • Python怎么实现在线聊天室私聊
    本篇内容主要讲解“Python怎么实现在线聊天室私聊”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Python怎么实现在线聊天室私聊”吧!实现思路对于私聊,我觉得应该有如下两点需要实现私聊列表更...
    99+
    2023-06-02
  • 使用Java怎么实现一个RPC框架
    使用Java怎么实现一个RPC框架?相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。一、RPC简介RPC,全称为Remote Procedure Call,即远程过程调用,它是一个...
    99+
    2023-05-30
    java rpc
  • 怎么使用Java socket通信模拟QQ实现多人聊天室
    这篇文章主要介绍“怎么使用Java socket通信模拟QQ实现多人聊天室”,在日常操作中,相信很多人在怎么使用Java socket通信模拟QQ实现多人聊天室问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法...
    99+
    2023-07-02
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作