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 框架进行开发或者更深的知识还需要诸君自己去摸索。

又是秃头的一天。

最新文章

  1. Lesson 6 Percy Buttons
  2. The Factory pattern
  3. webApp路由控制-vue-router2.0
  4. c#面向对象基础 封装、继承
  5. Winform快速开发组件的实现(二)
  6. mysql线上一些隐患查询sql
  7. iOS:app直播---原理篇
  8. nginx+keepalived双主高可用负载均衡
  9. 【转】10 个迅速提升你 Git 水平的提示
  10. ibatis 更改resultmap后 java.sql.SQLException: Column 'del_status' not found.
  11. HDU4344(大数分解)
  12. Django之路: 模版篇
  13. allego 输出报告说明
  14. Maven打包时去掉项目版本号
  15. 代理(Proxy)模式
  16. SQL Server 第四章 存储过程(Procedure),触发器(Trigger),数据完整性(Data Integrity)
  17. centos7下安装docker(26如何配置Health Check)
  18. Bootstrap学习目录
  19. spring事物的传播行为及隔离
  20. 【CF587D】Duff in Mafia 二分+前缀优化建图+2-SAT

热门文章

  1. 在docker的镜像中安装vim
  2. python根据窗口标题找句柄,将窗口前置活动
  3. viewport深入理解和使用
  4. Java基础00-IO流27
  5. [刘阳Java]_MySQL数据优化总结_查询备忘录
  6. Day10 类与对象-面向对象编程(1)
  7. 在docker for windows建立mssql容器后,ssms连接mssql出现错误号码18456的问题
  8. OVERLAPPED 结构
  9. Adaptive AUTOSAR 学习笔记 10 - 执行管理
  10. tomcat与springmvc 结合 之---第19篇(下,补充) springmvc 加载.xml文件的bean标签的过程