likes
comments
collection
share

一个破RPC,又又又让我复习了一遍计算机网络

作者站长头像
站长
· 阅读数 30

作者:于水增

如题,我又不换工作,为什么要写这篇文章🙄️。。。

背景

我所在的小组中有一部分同学是和设备打交道的,通过周会的分享会了解到设备的控制、设备间的通信设计等等。。。

另外以前在做需求的时候会经常从服务端同学口中听到RPC调用,那么浅浅了解下吧

先抛出几个问题

  1. RPC是谁?做什么工作的?芳龄几何...(跑题)
  2. 怎样用,能玩出花吗?
  3. 我一个前端,跟我有毛关系?🙄️

先了解下RPC的概念

RPC 全称Remote Procedure Call,即远程过程调用。在维基百科中简单理解就是,程序员无需关注网络通信的复杂性,在实际开发中调用远程方法就像调用本地程序一样简单。

说白了,就是我们经常干的函数调用,既然是远程调用,就需要解决不同进程或服务间相互数据传输也就是通信问题。

那么数据是如何在计算机中传输的?

聊聊数据是如何在不同应用之间传输的?

先回顾下OSI模型(开放式系统互联模型

这个模型很牛X,因为它就像画了一个圈,然后业界大佬们就在这个圈圈中设计各种协议,来满足我们日益变化的需求。

根据Wiki中对各层的介绍,整理出下边图表,列出了每层中我们比较熟悉的协议,个别陌生的也做了相应的备注,但是并不是所有场景的通信都会走完每一层。比如服务与服务的通信可以直接跳过应用层、系统内部可以直接物理层、设备与设备通信也是在物理层(比如往U盘里边下点电影。。。)

一个破RPC,又又又让我复习了一遍计算机网络

作为一个用户,无论是浏览器中打开一个链接(🈲️非法链接),还是打开微信去聊天。整个消息在网络上的传递是满足TCP/IP 4层概念模型的,数据层层传递(数据处理、建立连接、寻址。。。),最终满足用户的使用需求。

简单来看就是这样

一个破RPC,又又又让我复习了一遍计算机网络

其实,客户端与服务端的远比这个复杂,系统为我们做了很多事情,大体可以分为以下几个阶段

  1. 域名解析,查询IP(通过DNS服务查询IP地址)

  2. 建立TCP连接(我们常说的三次握手是在这个阶段完成)

a. 调用socket组件创建套接字

b. 调用connect与服务端的套接字建立管道

  1. 收发数据

a. 数据拆包

b. 数据确认(通过ACK号确认数据是否收到)

  1. 断开连接并删除套接字(四次挥手阶段。但是并不会立马删除套接字,主要防止像ACK号丢失的这种异常场景,比如客户端在发最后一个ACK后随即删除了套接字,但是服务端没有收到,所以服务端会再次发送一遍FIN,但是此时客户端已经删了呀,并且有个新的套接字被分配到了上次相同的端口号,那么FIN就会发给新的应用程序了,也就是我们常说的还没开始就结束了😂)

这部分内容很多,在每一步操作系统默默做了很多事,聊的很细的话肯本聊不完。。

好了,可以了,我要说RPC了。。。

假如我们有下面这个场景

一个破RPC,又又又让我复习了一遍计算机网络

商品中心需要去校验库存,那么就需要两个服务进行通信,在这里就可以用RPC来实现,库存中心只需要提供一个校验库存的方法供商品中心调用。那为啥不用HTTP?别问,问就是性能不好。。。

一个破RPC,又又又让我复习了一遍计算机网络

首先,我们目前用的比较多的还是HTTP/1.1的版本,它有很多缺陷,比如消息头部增加了很多看似冗余的字段。同时还有为了方便内部解析加了很多的换行符、回车符等。另外还需要考虑浏览器的各种行为。总之服务与服务之间并不需要这些。

而RPC的灵活性、定制化程度更高。可优化的空间很大,所以很多微服务架构中都选择使用RPC。

OSI模型图中我们发现RPC存在于会话层,其实它实现的方式有多种,可以基于HTTP/Websocket、TCP/UDP,甚至可以在物理层去实现。所以它更像一个规范、一个调用方式。目前大部分的RPC底层使用TCP。谷歌的gRPC是基于HTTP/2实现的。

基于HTTP来实现个RPC(虽然没什么用)

感觉基于HTTP/1.1版本来实现的RPC有点鸡肋。更像是约定了一个数据格式,本身还是通过JSON来序列化。HTTP/2改进了很多,甚至比很多RPC的性能还要好。谷歌的gRPC的底层实现就是HTTP/2。

这个小demo是用JSON-RPC来实现的,看下定义

JSON-RPC

是一种轻量级的远程调用(RPC)协议,它使用JSON(Javascript Object Notation)作为数据交换格式,通过HTTP或其他传输协议在客户端和服务器之间进行通信,具有简单、轻量级、易于使用的特点。

JSON大家都知道,经常出现在项目配置文件,到Restful API规范里的响应数据、JSON Schema等。它具有易读写、易解析,具有很好的约束性。

详细的jsonrpc规范可参考 www.jsonrpc.org/specificati…

用HTTP实现JSON-RPC和普通HTTP请求有什么区别?

  1. 无非是将post请求中key/value格式替换为JSON格式
  2. 服务端解析json,通过method和params两个属性进行处理业务/数据
  3. 响应头中的Content-type变成了application/json-rpc

Nodejs + HTTP + JSON-RPC

异常对象设计

当RPC调用遇到错误时,响应对象必须包含以下几个属性

type ErrorBody = {
  code: number; // 错误编码
  message: string; // 异常描述
  data?: any; // 附加信息的原始或结构化值,由服务器定义
};

官方文档给出以下code

/*
| code      |   message          | meaning |
| -32700    |   Parse error      | Invalid JSON was received by the server.An error occurred on the server while parsing the JSON text.  |
| -32600    |   Invalid Request  | The JSON sent is not a valid Request object.  |
| -32601    |   Method not found | The method does not exist / is not available.  |
| -32602    |   Invalid params   | Invalid method parameter(s).  |
| -32603    |   Internal error   | Internal JSON-RPC error.  |
| -32000 to -32099   |   Server error        | Reserved for implementation-defined server-errors.  |
*/

请求参数设计

type RequestBody = {
  jsonrpc: string; // 版本号,分为1.0和2.0两个版本,因存在兼容性问题,必须保证为2.0
  method: string; // 方法名,以单词 rpc 开头
  params?: any; // 一个结构化值,方法调用期间要使用的参数值
  id: numberstring; // 唯一标识,由客户端建立
}

响应结果设计

其中result与error为互斥关系,即成功只返回result,异常只返回error。

type ResponseBody = {
  jsonrpc: string; // 必须保证为2.0
  result?: any; 
  error?: ErrorBody;
	id: numberstring; // 必须与请求的ID相同
}

封装一个JSONRPC

export default class JSONRPC {
  version: string = "2.0";

  errorMsg = {
    [-32700]: "Parse Error.",
    [-32600]: "Invalid Request.",
    [-32601]: "Method Not Found.",
    [-32602]: "Invalid Params.",
    [-32603]: "Internal Error.",
  };

  methods = {};

  normalize(rpc, obj) {
    obj.id = rpc && typeof rpc.id === "number" ? rpc.id : null;
    obj.jsonrpc = this.version;
    // 如果错误根据错误不存在错误信息的话代码获取错误信息
    if (obj.error && !obj.error.message) {
        obj.error.message = this.errorMsg[obj.error.code] || obj.error.message;
    }
    return obj;
  }
  
  /**
   * JSONRPC 请求处理
   * @param  {Object} rpc
   * @param  {Function} response 响应回调
   */
  handleRequest(rpc, response) {
    // ...版本与一些参数验证逻辑

    //函数查找
    const method = this.methods[rpc.method];
    if (typeof method !== "function") {
      return response(this.normalize(rpc, { error: { code: -32601 } })); // 函数或方法未找到
    }
    
    // 调用函数将其执行结果作为响应结果返回客户端
    try {
      response(this.normalize(rpc, { result: method.apply(this, rpc.params) }));
    } catch (error: unknown) {
      if (error instanceof Error) {
        response(this.normalize(rpc, { error: { code: -32000, message: error.message } }));
      } else {
        response(
          this.normalize(rpc, { error: { code: 0, message: "unknown error" } })
        );
      }
    }
  }
}

起一个HTTP服务

import { createServer } from "http";
import url from "url";
import JSONPRC from "./rpc";

const HOST = "localhost";
const PORT = 8080;
const RPC = new JSONPRC();
// 添加方法
RPC.methods = {
  rpcDivide(a, b) {
    if (b === 0) throw Error("Not allow 0");
    return a / b;
  },
};
// 路由设计,供客户端调用
const routes = {
  "/rpc-divide": (request, response) => {
    if (request.method === "POST") {
      let data = "";
      request.setEncoding("utf8");
      request.addListener("data", (chunk) => {
        data += chunk;
      });
      request.addListener("end", () => {
        RPC.handleRequest(JSON.parse(data), (obj) => {
          const body = JSON.stringify(obj);
          response.writeHead(200, {
            "Content-Type": "application/json",
            "Content-Length": Buffer.byteLength(body),
          });
          response.end(body);
        });
      });
    } else {
      response.end("hello nodejs http server");
    }
  },
};

const server = createServer((request, response) => {
  // 解析请求,包括文件名
  const pathname = url.parse(request.url || "").pathname || "";
  const route = routes[pathname];
  if (route) {
    route(request, response);
  } else {
    response.end("hello nodejs http server");
  }
});

server.listen(PORT, HOST, 0, () => {
  console.log(`server is listening on http://${HOST}:${PORT} ...`);
});

运行效果

最终看下Postman的请求结果

一个破RPC,又又又让我复习了一遍计算机网络

出现非法请求时会跑出异常

一个破RPC,又又又让我复习了一遍计算机网络

完整实例代码在这里github.com/YSZ0927/nod… 感兴趣的可以试着调试下。。。

前端领域也能看到RPC的影子

Chrome远程调试工具

CDP(Chrome Devtools Protocol)是Chrome 的调试协议,用来调用 Chrome 内部的方法实现 js,css ,dom 的开发调试。它可以实现调试目标页面与控制台的相互通信。

首先Devtools由Fronend、Backend、Protocol、Message Channel构成。其中Protocol就是基于JSON-RPC来实现的,而Fronend与Backend之间则通过Websocket实现消息通信

如何调试?

  1. 终端启动本地Chrome实例
sudo /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222 --remote-allow-origins=http://127.0.0.1:9222
  1. 随便输入一个调试网址,然后通过http://127.0.0.1:9222/json来获取 websocket target

一个破RPC,又又又让我复习了一遍计算机网络

  1. 新开tab页面http://127.0.0.1:9222/{devtoolsFrontendUrl}

一个破RPC,又又又让我复习了一遍计算机网络

感兴趣的可以在这篇文章 详细了解CDP的原理

JSON-RPC在门店业务中的应用场景

在门店里店员可以通过平板来操控设备。那就需要平板与设备与模组之间的通信

一个破RPC,又又又让我复习了一遍计算机网络

总结

  • RPC的作用主要是让我们屏蔽网络编程的复杂性,实现远程调用像调用本地方法一样的效果
  • RPC的实现有多种,可以基于HTTP1.1/HTTP2、Websocket、TCP、UDP任何一种
  • RPC与HTTP没有可比性,包括任何技术而言都是在某些场景下是相对合适的
  • JSON-RPC依赖JSON数据标准,兼容性高,大部分语言都支持且有相应的库。主打轻量级,简单,易用。

最后

关注公众号「Goodme前端团队」,获取更多干货实践,欢迎交流分享~