RPC 是什么?

RPC 英文全称是 Remote Procedure Call 既远程过程调用,维基百科中给的定义是一个计算机调用了一个函数,但这个函数并不在这台计算机上,这种远程调用方式程序员无需关注到底怎么远程调用,就像是本地执行一个函数一模一样。

听着很高大上,我们要实现一个求和的例子:

function sum(a, b) {
return a + b
}

作为客户端,实际是不知道 sum 的逻辑的,它只需要传递 ab 两个参数给服务端,服务端返回结果即可。

这里大家就会有一个疑问,为什么我们要远程调一个函数?

答案就是我们本地没有呀,上面举的是 sum 的纯逻辑,但如果是客户端有账号和密码,要获取 用户详细信息的数据呢,我们本地是没有的,所以一定要远程调用。

PRC 和 HTTP 协议的关系?

经过我们一解释,相信大家都有些明白了,但又会产生一个新的疑问,这个过程怎么和 http 的请求响应模型这么像呢,两者是什么关系呢?

其实广义的理解中,http 就是 rpc 的一种实现方式,rpc 更多像是一种思想,http 请求和响应是一种实现。

gPRC 是什么?

刚刚说了 rpc 更多的是一种思想,而我们现在说的 gPRC 则是 PRC 的一种实现,也可以称为一个框架,并且不止这一个框架,业界还有 thrift,但是目前微服务中用的比较广泛的就是它,所以我们要学习的就是它。

gRPC 官网 的介绍是 A high performance, open source universal RPC framework。 一个高性能、开源的通用RPC框架。它有以下四个特点:

  • 定义简单:它基于 Protocol Buffer 进行类型定义(就是有哪些函数、函数的参数类型、响应结果类型);
  • 跨语言和平台:通过上述定义,我们可以一键生成 typescriptgoc#java 等代码 。因为每种语言都是有函数的,函数也都有参数和返回值的,而 Protocol Buffer 是一种中间语言,那么它就可以任意转换(如果不好理解,你可以想一下 json,json 这种数据结构就是各个语言通用的概念,无论是前端的 json,还是 go 语言的 json 都可以按照统一的意思读写)。
  • 快速扩缩容。
  • 基于 HTTP/2 的双向认证。

Protocol Buffer 是什么?

VS Code 提供了 vscode-proto3 这个插件用于 proto 的高亮

protocal buffer 你可以理解为一个语言,不过不用怕,其语法是十分的简单,它的作用也很明确,就是用来定义函数、函数的参数、响应结果的,并且可以通过命令行转为不同语言的函数实现。其基本语法为:

// user.proto

syntax = "proto3";

package user; // 包名称

// 请求参数
message LoginRequest {
string username = 1;
string password = 2;
} // 响应结果
message LoginResponse {
string access_token = 1;
int32 expires = 2;
} // 用户相关接口
service User {
// 登录函数
rpc login(LoginRequest) returns (LoginResponse);
}

为了方面理解,我将上面的定义翻译为 typescript 定义:

namespace user {
interface LoginRequest {
username: string;
password: string;
} interface LoginResponse {
access_token: string;
expires: number;
} interface User {
login: (LoginRequest) => LoginResponse // ts 类型定义中,函数参数可以没有名称的。
}
}

通过对比我们知道:

  • syntax = "proto3":这句话相当于用 proto3 版本的协议,现在统一的都是 3,每个 proto 文件都这样写就对了
  • package:类似 namespace 作用域
  • message:相当于 ts 中的 interface
  • service:也是相当于 js 中的 interface
  • string、int32:分别是类型,因为 ts 中关于数的划分没那么细,所以 int32 就被转为了 number
  • User:相当于 ts 中的类或者对象
  • login:相当于 ts 中的方法
  • 数字 1、2:最令人迷惑的就是变量后的数字了,它实际是 grpc 通信过程的关键,是用于把数据编码和解码的顺序,类似于 json 对象转为字符串,再把字符串转为 json 对象中那些冒号和逗号分号的作用一样,也就是序列化与反序列化的规则。

从 proto 定义到 node 代码

动态加载版本

所谓动态加载版本是指在 nodejs 启动时加载并处理 proto,然后根据 proto 定义进行数据的编解码。

  • 创建目录和文件

gRPC 是客户端和服务端交换信息的框架,我们就建立两个 js 文件分为作为客户端和服务端,客户端发送登录的请求,服务端响应,其目录结构如下:

.
├── client.js # 客户端
├── server.js # 服务端
├── user.proto # proto 定义
└── user_proto.js # 客户端和服务端都要用到加载 proto 的公共代码
  • 安装依赖
yarn add @grpc/grpc-js  # @grpc/grpc-js:是 gRPC node 的实现(不同语言有不同语言的实现)
yarn add @grpc/proto-loader # @grpc/proto-loader:用于加载 proto
  • 编写 user_proto.js

user_proto.js对于服务端和客户端都很重要,客户端可以知道自己要发送的数据类型和参数,而服务端可以知道自己接受的参数、要响应的结果以及要实现的函数名称。

// user_proto.js
// 加载 proto
const path = require('path')
const grpc = require('@grpc/grpc-js')
const protoLoader = require('@grpc/proto-loader') const PROTO_PATH = path.join(__dirname, 'user.proto') // proto 路径
const packageDefinition = protoLoader.loadSync(PROTO_PATH, { keepCase: true, longs: String, enums: String, defaults: true, oneofs: true })
const protoDescriptor = grpc.loadPackageDefinition(packageDefinition) const user_proto = protoDescriptor. user module.exports = user_proto
  • 编写 server.js
// service.js
// 服务端
const grpc = require("@grpc/grpc-js"); // 引入 gprc 框架
const user_proto = require("./user_proto.js"); // 加载解析后的 proto // User Service 实现
const userServiceImpl = {
login: (call, callback) => {
// call.request 是请求相关信息
const { request } = call;
const { username, password } = request; // 第一个参数是错误信息,第二个参数是响应相关信息
callback(null, {
access_token: `username = ${username}; password = ${password}`,
expires: "zhang",
});
},
}; // 和 http 一样,都需要去监听一个端口,等待别人链接
function main() {
const server = new grpc.Server(); // 初始化 grpc 框架
server.addService(user_proto.User.service, userServiceImpl); // 添加 service
// 开始监听服务(固定写法)
server.bindAsync("0.0.0.0:8081", grpc.ServerCredentials.createInsecure(), () => {
server.start();
console.log("grpc server started");
}
);
} main();

因为 proto 中我们只进行了定义,并没有 login 的真正实现,所以我们需要再 server.js 中对 login 进行实现。我们可以 console.log(user_proto) 看到:

{
LoginRequest: {
// ...
},
LoginResponse: {
// ...
},
User: [class ServiceClientImpl extends Client] {
service: { login: [Object] }
}
}

所以 server.addService 我们才能填写 user_proto.User.service

  • 编写 client.js
// client.js
const user_proto = require("./user_proto");
const grpc = require("@grpc/grpc-js"); // 使用 `user_proto.User` 创建一个 client,其目标服务器地址是 `localhost:8081`
// 也就是我们刚刚 service.js 监听的地址
const client = new user_proto.User(
"localhost:8081",
grpc.credentials.createInsecure()
); // 发起登录请求
function login() {
return new Promise((resolve, reject) => {
// 约定的参数
client.login(
{ username: 123, password: "abc123" },
function (err, response) {
if (err) {
reject(err);
} else {
resolve(response);
}
}
);
})
} async function main() {
const res = await login();
console.log(res)
} main();
  • 启动服务

node server.js 启动服务端,让其保持监听,然后 node client.js 启动客户端,发送请求。

我们看到已经有了响应结果。

  • 坏心眼

我们使个坏心眼,如果发送的数据格式不是 proto 中定义的类型的会怎么样?



答案是会被强制类型转换为 proto 中定义的类型,比如我们在 server.js 中将 expires 字段的返回值改为了 zhang 那么他会被转为数字 0,而客户端发送过去的 123 也被转为了字符串类型。

静态编译版本

动态加载是运行时加载 proto,而静态编译则是提前将 proto 文件编译成 JS 文件,我们只需要加载 js 文件就行了,省去了编译 proto 的时间,也是在工作中更常见的一种方式。

  • 新建项目

我们新建一个项目,这次文件夹内只有四个文件,分别为:

.
├── gen # 文件夹,用于存放生成的代码
├── client.js # 客户端代码
├── server.js # 服务端代码
└── user.proto # proto 文件,记得将内容拷贝过来
  • 安装依赖
yarn global add grpc-tools # 用于从 proto -> js 文件的工具
yarn add google-protobuf @grpc/grpc-js # 运行时的依赖
  • 生成 js 代码
grpc_tools_node_protoc \
--js_out=import_style=commonjs,binary:./gen/ \
--grpc_out=grpc_js:./gen/ user.proto

我们看到已经生成了 user_pb.jsuser_grpc_pb.js 两个文件:

  • grpc_tools_node_protoc:是安装 grpc-tools 后生成的命令行工具
  • --js_out=import_style=commonjs,binary:./gen/:是生成 user_pb.js 的命令
  • --grpc_out=grpc_js:./gen/:是生成 user_grpc_pb.js 的命令。

pb 是 protobuf 的简写

如果你去仔细查看两者的内容你就会发现:

user_pb.js:主要是对 proto 中的 message 定义扩展各种编解码方法,也就是对 LoginRequestLoginResponse 做处理。

user_grpc_pb.js:则是对 proto 中的 service 进行各种方法定义。

  • 编写 server.js
const grpc = require("@grpc/grpc-js");

const services = require("./gen/user_grpc_pb");
const messages = require("./gen/user_pb"); const userServiceImpl = {
login: (call, callback) => {
const { request } = call; // 使用 request 里的方法获取请求的参数
const username = request.getUsername();
const password = request.getPassword(); // 使用 message 设置响应结果
const response = new messages.LoginResponse();
response.setAccessToken(`username = ${username}; password = ${password}`);
response.setExpires(7200); callback(null, response);
},
}; function main() {
const server = new grpc.Server(); // 使用 services.UserService 添加服务
server.addService(services.UserService, userServiceImpl);
server.bindAsync(
"0.0.0.0:8081",
grpc.ServerCredentials.createInsecure(),
() => {
server.start();
console.log("grpc server started");
}
);
} main();

我们发现和动态版的区别就是 addService 时直接使用了导出的 UserService 定义,然后再实现 login 时,我们能使用各种封装的方法来处理请求和响应参数。

  • 编写 client.js
// client.js

const grpc = require("@grpc/grpc-js");

const services = require("./gen/user_grpc_pb");
const messages = require("./gen/user_pb"); // 使用 services 初始化 Client
const client = new services.UserClient(
"localhost:8081",
grpc.credentials.createInsecure()
); // 发起 login 请求
function login() {
return new Promise((resolve, reject) => {
// 使用 message 初始化参数
const request = new messages.LoginRequest();
request.setUsername("zhang");
request.setPassword("123456"); client.login(request, function (err, response) {
if (err) {
reject(err);
} else {
resolve(response.toObject());
}
});
});
} async function main() {
const res = await login()
console.log(res)
} main();

从上面的注释可以看出,我们直接从生成的 JS 文件中加载内容,并且它提供了很多封装的方法,让我们传参更加可控。

从 JS 到 TS

从上面我们也看出了,对于参数类型的限制,更多是强制类型转换,在书写阶段并不能发现,这就很不科学了,不过,我们就需要通过 proto 生成 ts 类型定义来解决这个问题。

网上关于从 proto 到生成 ts 的方案有很多,我们选择了使用 protoc + grpc_tools_node_protoc_ts + grpc-tools

  • 新建项目
mkdir grpc_demo_ts && cd grpc_demo_ts # 创建项目目录

yarn global add typescript ts-node @types/node # 安装 ts 和 ts-node

tsc --init # 初始化 ts
  • 安装 proto 工具
yarn global add grpc-tools grpc_tools_node_protoc_ts # 安装 proto 工具到全局
  • 安装运行时依赖
yarn add google-protobuf @grpc/grpc-js # 运行时依赖
  • 创建文件
mkdir gen # 创建存放输出文件的目录
touch client.ts server.ts user.proto # 创建文件
# 记得把 user.proto 的内容拷贝过去
  • 安装 protoc

然后我们需要安装 protoc 这个工具,首先进入 protobuf 的 github,进入 release,下载所在平台的文件,然后进行安装,安装完记得把其加入到设置环境变量里,确保可以全局使用。

mac 可以通过 brew install protobuf 进行安装,安装后全局就会有 protoc 命令

  • 生成 js 文件和 ts 类型定义
# 生成 user_pb.js 和 user_grpc_pb.js
grpc_tools_node_protoc \
--js_out=import_style=commonjs,binary:./gen \
--grpc_out=grpc_js:./gen \
--plugin=protoc-gen-grpc=`which grpc_tools_node_protoc_plugin` \
./user.proto # 生成 d.ts 定义
protoc \
--plugin=protoc-gen-ts=`which protoc-gen-ts` \
--ts_out=grpc_js:./gen \
./user.proto
  • 编写 server.ts
// server.ts

import * as grpc from "@grpc/grpc-js";
import { IUserServer, UserService } from "./gen/user_grpc_pb";
import messages from "./gen/user_pb"; // User Service 的实现
const userServiceImpl: IUserServer = {
// 实现登录接口
login(call, callback) {
const { request } = call;
const username = request.getUsername();
const password = request.getPassword(); const response = new messages.LoginResponse();
response.setAccessToken(`username = ${username}; password = ${password}`);
response.setExpires(7200);
callback(null, response);
}
} function main() {
const server = new grpc.Server(); // UserService 是定义,UserImpl 是实现
server.addService(UserService, userServiceImpl);
server.bindAsync(
"0.0.0.0:8081",
grpc.ServerCredentials.createInsecure(),
() => {
server.start();
console.log("grpc server started");
}
);
} main();

类型提示很完美

  • 编写 client.ts
// client.ts

import * as grpc from "@grpc/grpc-js";
import { UserClient } from "./gen/user_grpc_pb";
import messages from "./gen/user_pb"; const client = new UserClient(
"localhost:8081",
grpc.credentials.createInsecure()
); // 发起登录请求
const login = () => {
return new Promise((resolve, reject) => {
const request = new messages.LoginRequest();
request.setUsername('zhang');
request.setPassword("123456"); client.login(request, function (err, response) {
if (err) {
reject(err);
} else {
resolve(response.toObject());
}
});
})
} async function main() {
const data = await login()
console.log(data)
} main();



当我们输入错类型时,ts 就会进行强制检验。

  • 启动服务

我们使用 ts-node 启动两者,发现效果一起正常。

从 Node 到 Go

上面的介绍中,client 和 server 都是用 js/ts 来写的,但实际工作中更多的是 node 作为客户端去聚合调其他语言写的接口,也就是通常说的 BFF 层,我们以 go 语言为例。

  • 改造原 ts 项目

我们将上面的 ts 项目改造为 client 和 server 两个目录,client 是 ts 项目作为客户端,server 是 go 项目,作为服务端,同时我们把原来的 server.ts 删除,把 user.proto 放到最外面,两者共用。

.
├── client # 客户端文件夹,其内容同 ts 章节,只是删除了 server.ts 相关内容
│ ├── client.ts
│ ├── gen
│ │ ├── user_grpc_pb.d.ts
│ │ ├── user_grpc_pb.js
│ │ ├── user_pb.d.ts
│ │ └── user_pb.js
│ ├── package.json
│ ├── tsconfig.json
│ └── yarn.lock
├── server # 服务端文件
└── user.proto # proto 文件
  • 安装 Go

我们进入 Go 语言官网,找到最新的版本下载安装即可:https://golang.google.cn/dl/

  • 设置 go 代理

和 npm 一样,go 语言拉包,也需要设置镜像拉包才能更快。

go env -w GOPROXY=https://goproxy.cn,direct
  • 初始化 go 项目

类似 yarn init -y 的作用。

cd server # 进入 server 目录
go mod init grpc_go_demo # 初始化包
mkdir -p gen/user # 用于存放后面生成的代码
  • 安装 protoc 的 go 语言插件

用于生成 go 语言的代码,作用与 grpc-toolsgrpc_tools_node_protoc_ts 相同。

go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.26
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.1
  • 安装运行时依赖

我们还需要安装运行时依赖,作用类似上面 node 的 google-protobuf@grpc/grpc-js

go get -u github.com/golang/protobuf/proto
go get -u google.golang.org/grpc
  • 修改 user.proto
syntax = "proto3";

option go_package = "grpc_go_demo/gen/user"; // 增加这一句

package user;

message LoginRequest {
string username = 1;
string password = 2;
} message LoginResponse {
string access_token = 1;
int32 expires = 2;
} service User {
rpc login(LoginRequest) returns (LoginResponse);
}
  • 生成 go 代码
// 要在 server 目录哦

protoc --go_out=./gen/user -I=../ --go_opt=paths=source_relative \
--go-grpc_out=./gen/user -I=../ --go-grpc_opt=paths=source_relative \
../user.proto
  • 安装 VS Code 插件并新创建打开项目

当你点击去查看生成出来的 user.pb.go 或者 user_grpc.pb.go 时,你会发现 vscode 让你装插件,装就完事了,然后你可能会发现 go 包报找不到的错误,不要慌,我们以 server 为项目根路径重新打开项目即可。

  • 创建 main.go 书写服务端代码
// server/main.go

package main

import (
"context"
"fmt"
pb "grpc_go_demo/gen/user"
"log"
"net" "google.golang.org/grpc"
) // 声明一个对象
type userServerImpl struct {
pb.UnimplementedUserServer
} // 对象有一个 Login 方法
func (s *userServerImpl) Login(ctx context.Context, in *pb.LoginRequest) (*pb.LoginResponse, error) {
// 返回响应结果
return &pb.LoginResponse{
AccessToken: fmt.Sprintf("go: username = %v, password = %v", in.GetUsername(), in.GetPassword()),
Expires: 7200,
}, nil
} // 监听服务并将 server 对象注册到 gRPC 服务器上
func main() {
// 创建 tcp 服务
lis, _ := net.Listen("tcp", ":8081") // 创建 grpc 服务
server := grpc.NewServer() // 将 UserServer 注册到 server
pb.RegisterUserServer(server, &userServerImpl{}) log.Printf("server listening at %v", lis.Addr()) if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}

为什么是 gRPC 而非 HTTP?

现在微服务架构大多数使用的是 gRPC 进行服务间通信,那么为什么不再使用我们前端熟悉的 http 呢?

有人说高效率,gRPC 是 tcp 协议、二进制传输,效率高,效率高缺失没错,但它相对于 http 并不会有明显的差距,一方面 http 中 json 编解码效率和占用空间数并不会比编解成二进制差多少,其次,tcp 和 http 在内网环境下,带来的性能我个人感觉也不会差多少(PS:gRPC 官网也并未强调它相对于 HTTP 的高效率)。

其实官网核心突出的就在于它的语言无关性,通过 protobuf 这种中间形式,可以转换为各种语言的代码,确保了代码的一致性,而非 http 那样对着 swagger 或者其他的文档平台去对接口。

结束语

本篇只是一个入门,至于 gRPC 如何结合 node 框架进行开发或者更深的知识还需要诸君自己去摸索。

又是秃头的一天。

前端从😳 到🚪 gRPC 框架的更多相关文章

  1. 准备.Net转前端开发-WPF界面框架那些事,值得珍藏的8个问题

    题外话 不出意外,本片内容应该是最后一篇关于.Net技术的博客,做.Net的伙伴们忽喷忽喷..Net挺好的,微软最近在跨平台方面搞的水深火热,更新也比较频繁,而且博客园的很多大牛也写的有跨平台相关技术 ...

  2. Yii框架学习笔记(二)将html前端模板整合到框架中

    选择Yii 2.0版本框架的7个理由 http://blog.chedushi.com/archives/8988 刚接触Yii谈一下对Yii框架的看法和感受 http://bbs.csdn.net/ ...

  3. 前端开发者使用JS框架的三个等级

    目前前端开发者使用JS框架是种很普遍的现象,因为框架可以加快开发速度,同时避免各类浏览器的兼容性问题.不过同样是用框架开发,不同开发者的境界水平还是有一定差距,本文将这些前端开发者分为三个等级. 第一 ...

  4. Web前端-Vue.js必备框架(五)

    Web前端-Vue.js必备框架(五) 页面组件,商品列表组件,详情组件,购物车清单组件,结算页组件,订单详情组件,订单列表组件. vue-router 路由 vuex 组件集中管理 webpack ...

  5. Web前端-Vue.js必备框架(四)

    Web前端-Vue.js必备框架(四) 计算属性: <div id="aaa"> {{ message.split('').reverse().join('') }} ...

  6. Web前端-Vue.js必备框架(三)

    Web前端-Vue.js必备框架(三) vue是一款渐进式javascript框架,由evan you开发.vue成为前端开发的必备之一. vue的好处轻量级,渐进式框架,响应式更新机制. 开发环境, ...

  7. Web前端-Vue.js必备框架(二)

    Web前端-Vue.js必备框架(二) vue调式工具vue-devtools 过滤器:vue.js允许你自定义过滤器,可被用作一些常见的文本格式化. mustache插值和v-bind表达式. vu ...

  8. Web前端-Vue.js必备框架(一)

    Web前端-Vue.js必备框架(一) <!DOCTYPE html> <html lang="en"> <head> <meta cha ...

  9. ModelProxy 前端接口配置建模框架

    ModelProxy    轻量级的接口配置建模框架(1) 先看一下这个博客说明为什么需要用ModelProxy的前端轻量级的框架吧:  http://developer.51cto.com/art/ ...

随机推荐

  1. 输出 time 命令的结果到文件中

    译至:http://unicus.jp/skmk/archives/338 由于输出 time 命令的结果到文件时使用的错误的方式,所以将其记录下来. 环境是bash. 目标 将运行的a.out程序的 ...

  2. 26. Remove Duplicates from Sorted Array*(快慢指针)

    description: Given a sorted array nums, remove the duplicates in-place such that each element appear ...

  3. 字符串中的第一个唯一字符 python

    给定一个字符串,找到它的第一个不重复的字符,并返回它的索引.如果不存在,则返回 -1. s = "leetcode" 返回 0. s = "loveleetcode&qu ...

  4. 从新建文件夹开始构建ShadowPlay Engine游戏引擎(3)

    本篇序言 各位可能看到博文的名字换了,也就是引擎名字换了,其实是在下想到了一个更棒的名字:皮影戏(ShadowPlay),取这个名字的含义是因为,游戏中的角色(Puppet)不也是由于我们的操作而动起 ...

  5. Java基础00-Lamda表达式30

    1. Lambda表达式 Java8新特征之Lambda表达式 1.1 函数式编程思想概述 1.2 体验Lambda表达式 代码示例: 方式一就不演示了,可以去看Java基础24 方式2:匿名内部类的 ...

  6. VS Code 与 ESP32 官方SDK配置

    开发基于 ESP XXX 微控制器应用,最简单的环境搭建方案是像 MicroPython.CircuitPython.NanoFramework 等,下载固件,直接开刷:或者基于 Arduino 的开 ...

  7. Real DOM和 Virtual DOM 的区别?优缺点?

    一.是什么 Real DOM,真实DOM, 意思为文档对象模型,是一个结构化文本的抽象,在页面渲染出的每一个结点都是一个真实DOM结构,如下: Virtual Dom,本质上是以 JavaScript ...

  8. python + pytest基本使用方法(拓展库)

    一.测试钩子配置文件 import pytest# conftest.py 是pytest特有的本地测试配置文件;# 既可以用来设置项目级别的Fixture,也可用来导入外部插件,还可以指定钩子函数# ...

  9. 超详细!搭建本地大数据研发环境(16G内存+CDH)

    工欲善其事必先利其器,在经过大量的理论学习以后,需要有一个本地的研发环境来进行练手.已经工作的可以不依赖于公司的环境,在家也可以随意的练习.而自学大数据的同学,也可以进行本地练习,大数据是一门偏实践的 ...

  10. Beautiful Soup4.4.0中文官方文档!最权威的参考---中文官方文档

    最好用的解析库Beautiful Soup 解析库-----中文官方文档 https://beautifulsoup.readthedocs.io/zh_CN/v4.4.0/