目的
为了让浏览器可以支持grpc的调用,以此记录相关研究过程,共勉
grpc-web搭建
此处使用官网helloworld例子进行说明,github入口
服务端
- 使用protocol buffers定义回声服务原型,命名为helloworld.proto
syntax = "proto3";
package helloworld;
service Greeter {
rpc SayHello(HelloRequest) returns (HelloReply);
rpc SayRepeatHello(RepeatHelloRequest) returns (stream HelloReply);
rpc SayHelloAfterDelay(HelloRequest) returns (HelloReply);
}
message HelloRequest {
string name = 1;
}
message RepeatHelloRequest {
string name = 1;
int32 count = 2;
}
message HelloReply {
string message = 1;
}
- 编写服务器代码,例子中使用的是NodeJS
var PROTO_PATH = __dirname + '/helloworld.proto';
var grpc = require('grpc');
var _ = require('lodash');
var async = require('async');
var protoLoader = require('@grpc/proto-loader');
var packageDefinition = protoLoader.loadSync(
PROTO_PATH,
{keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true
});
var protoDescriptor = grpc.loadPackageDefinition(packageDefinition);
var helloworld = protoDescriptor.helloworld;
/**
* @param {!Object} call
* @param {function():?} callback
*/
function doSayHello(call, callback) {
callback(null, {message: 'Hello! '+ call.request.name});
}
/**
* @param {!Object} call
*/
function doSayRepeatHello(call) {
var senders = [];
function sender(name) {
return (callback) => {
call.write({
message: 'Hey! ' + name
});
_.delay(callback, 500); // in ms
};
}
for (var i = 0; i < call.request.count; i++) {
senders[i] = sender(call.request.name + i);
}
async.series(senders, () => {
call.end();
});
}
/**
* @param {!Object} call
* @param {function():?} callback
*/
function doSayHelloAfterDelay(call, callback) {
function dummy() {
return (cb) => {
_.delay(cb, 5000);
};
}
async.series([dummy()], () => {
callback(null, {
message: 'Hello! '+call.request.name
});
});
}
/**
* @return {!Object} gRPC server
*/
function getServer() {
var server = new grpc.Server();
server.addService(helloworld.Greeter.service, {
sayHello: doSayHello,
sayRepeatHello: doSayRepeatHello,
sayHelloAfterDelay: doSayHelloAfterDelay
});
return server;
}
if (require.main === module) {
var server = getServer();
server.bind('0.0.0.0:9090', grpc.ServerCredentials.createInsecure());
server.start();
}
exports.getServer = getServer;
- 配置Envoy代理将浏览器的gRPC Web请求转发到后端。命名为envoy.yaml。例子中envoy监听端口8080,将任何gRPC Web请求转发到端口9090的群集。
admin:
access_log_path: /tmp/admin_access.log
address:
socket_address: { address: 0.0.0.0, port_value: 9901 }
static_resources:
listeners:
- name: listener_0
address:
socket_address: { address: 0.0.0.0, port_value: 8080 }
filter_chains:
- filters:
- name: envoy.http_connection_manager
config:
codec_type: auto
stat_prefix: ingress_http
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains: ["*"]
routes:
- match: { prefix: "/" }
route:
cluster: greeter_service
max_grpc_timeout: 0s
cors:
allow_origin_string_match:
- prefix: "*"
allow_methods: GET, PUT, DELETE, POST, OPTIONS
allow_headers: keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,custom-header-1,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web,grpc-timeout
max_age: "1728000"
expose_headers: custom-header-1,grpc-status,grpc-message
http_filters:
- name: envoy.grpc_web
- name: envoy.cors
- name: envoy.router
clusters:
- name: greeter_service
connect_timeout: 0.25s
type: logical_dns
http2_protocol_options: {}
lb_policy: round_robin
# win/mac hosts: Use address: host.docker.internal instead of address: localhost in the line below
hosts: [{ socket_address: { address: host.docker.internal, port_value: 9090 }}]
# 以下是为了满足https请求,http不需要添加
transport_socket:
name: envoy.transport_sockets.tls
typed_config:
"@type": type.googleapis.com/envoy.api.v2.auth.DownstreamTlsContext
注意:
- 如果docker是运行在Mac/Windows,socket_address需要更改地址为host.docker.internal
hosts: [{ socket_address: { address: host.docker.internal, port_value: 9090 }}]
- 如果是https请求报以下错误
upstream connect error or disconnect/reset before headers. reset reason: connection failure
是因为需要加上transport_socket
- 创建Dockerfile,为之后运行envoy 。命名为envoy.Dockerfile
注意以下,官网中是envoyproxy/envoy:last 版本可修改,参考
FROM envoyproxy/envoy:v1.14.1
COPY ./envoy.yaml /etc/envoy/envoy.yaml
CMD /usr/local/bin/envoy -c /etc/envoy/envoy.yaml
- 使用envoy的二进制文件构建镜像,使用docker命令
$ docker build -t helloworld/envoy -f ./envoy.Dockerfile .
$ docker run -d -p 8080:8080 -p 9901:9901 --network=host helloworld/envoy
如果docker是部署在Mac/Windows,命令中去掉 --network=host option:
$ docker run -d -p 8080:8080 -p 9901:9901 helloworld/envoy
docker安装:确保您已经安装了最新版本的 docker、docker-compose 和 docker-machine。
安装这些软件最简单的方式是使用 Docker Toolbox。
- 启动服务器
$ node server.js &
服务监听端口9090,需要注意的nodejs运行需要安装模块,package.json定义在后面说明
客户端
- 客户端请求代码,命名client.js
const {HelloRequest, RepeatHelloRequest,
HelloReply} = require('./helloworld_pb.js');
const {GreeterClient} = require('./helloworld_grpc_web_pb.js');
var client = new GreeterClient('http://' + window.location.hostname + ':8080',
null, null);
// simple unary call
var request = new HelloRequest();
request.setName('World');
client.sayHello(request, {}, (err, response) => {
console.log(response);
//console.log(response.getMessage());
});
// server streaming call
var streamRequest = new RepeatHelloRequest();
streamRequest.setName('World');
streamRequest.setCount(5);
var stream = client.sayRepeatHello(streamRequest, {});
stream.on('data', (response) => {
console.log(response);
//console.log(response.getMessage());
});
// deadline exceeded
var deadline = new Date();
deadline.setSeconds(deadline.getSeconds() + 1);
client.sayHelloAfterDelay(request, {deadline: deadline.getTime()},
(err, response) => {
console.log('Got error, code = ' + err.code +
', message = ' + err.message);
});
- 安装模块,定义package.json。同时安装server.js和client.js模块
{
"name": "grpc-web-simple-example",
"version": "0.1.0",
"description": "gRPC-Web simple example",
"devDependencies": {
"@grpc/proto-loader": "^0.3.0",
"google-protobuf": "^3.6.1",
"grpc": "^1.15.0",
"grpc-web": "^1.0.0",
"webpack": "^4.16.5",
"webpack-cli": "^3.1.0"
}
}
- 定义html文件,命名为index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>gRPC-Web Example</title>
<script src="./dist/main.js"></script>
</head>
<body>
<p>Open up the developer console and see the logs for the output.</p>
</body>
</html>
其中使用webpack打包将生成/dist/main.js文件
- 使用protoc命令行工具生成CommonJS客户端代码
$ protoc -I=$DIR helloworld.proto --js_out=import_style=commonjs:$OUT_DIR
以上命令生成pb.js文件,此文件主要用于发送request,包含request相关函数。需要下载 proto 工具。
$ protoc -I=$DIR helloworld.proto --grpc-web_out=import_style=commonjs,mode=grpcwebtext:$OUT_DIR
以上命令生成web_pb.js文件,此文件主要用于获取response,包含response相关函数,同时包含请求地址的设置 。需要 protoc plugin
- 打包
$ npm install
$ npx webpack client.js
- 运行网站
#python2
$ python2 -m SimpleHTTPServer 8081 &
#python3
$ python3 -m http.server 8081 &
访问地址localhost:8081,打开开发者工具,会看到打印出Hello! World。成功~
grpc-web结合react
服务端按着以上方式启动。客户端只需要把pb.js以及web_pb.js放入react项目中,编写client.js文件。但是当run的时候会报错不成功。这是因为react默认配置了eslint,会检测出pb.js文件的部分变量undefined。知道问题的原因,是不是修改.eslintrc 规则,把未定义的变量加入globals就成功,却发现怎么修改eslint的配置都没有生效,查阅很多反馈,发现官网有说明:可以看到的是,我们即使配置了 .eslintrc 规则,也只会影响到我们浏览器对于 eslint 规则的运用,无法在编译调试的过程中,对代码进行规范。
必须要用默认的配置,除非修改node_modules 内部,但是对小组开发并不友好,所以需要找到不修改 .eslintrc 能成功的办法。
发现可以定义到window中,默认 Windows 是一个全局变量,而第三方框架的全局变量肯定是会挂在到 Windows 对象上去的
问题参考链接
https://www.jianshu.com/p/7fec779528a6
https://create-react-app.dev/docs/using-global-variables/
在pb.js文件中添加
const proto = window.proto;
const COMPILED = window.COMPILED;
重新启动运行,成功~
相关概念
-
rpc
RPC(remote procedure call 远程过程调用)框架实际是提供了一套机制,使得应用程序之间可以进行通信,而且也遵从server/client模型。使用的时候客户端调用server端提供的接口就像是调用本地的函数一样 -
grpc
gRPC 是一个高性能、开源和通用的 RPC 框架,面向服务端和移动端,支持多语言,基于 HTTP/2 设计。
特点:- 基于 IDL 文件定义服务,通过 proto3 工具生成指定语言的数据结构、服务端接口以及客户端 Stub;
- 通信协议基于标准的 HTTP/2 设计,支持双向流、消息头压缩、单 TCP的多路复用、服务端推送等特性,这些特性使得 gRPC 在移动端设备上更加省电和节省网络流量;
- 序列化支持 PB(Protocol Buffer)和 JSON,PB 是一种语言无关的高性能序列化框架,基于 HTTP/2 + PB, 保障了 RPC 调用的高性能。
-
protocol buffers
gRPC默认使用protocl buffers,protoc buffers 是谷歌成熟的开源的用于结构化数据序列化的机制,需要 protoc 编译工具 -
grpc-web
gRPC-Web是一个JavaScript客户端库,使Web应用程序能够直接与后端gRPC服务通信 -
CommonJS
CommonJS 是以在浏览器环境之外构建 javaScript 生态系统为目标而产生的写一套规范,主要是为了解决 javaScript 的作用域问题而定义的模块形式,可以使每个模块它自身的命名空间中执行,该规范的主要内容是,模块必须通过 module.exports 导出对外的变量或者接口,通过 require() 来导入其他模块的输出到当前模块的作用域中。CommonJS模块基本上包括两个基础的部分:一个取名为exports的自由变量,它包含模块希望提供给其他模块的对象,以及模块所需要的可以用来引入和导出其它模块的函数。 -
http2
2015年,HTTP/2 发布。HTTP/2是现行HTTP协议(HTTP/1.x)的替代,但它不是重写,HTTP方法/状态码/语义都与HTTP/1.x一样。HTTP/2基于SPDY,专注于性能,最大的一个目标是在用户和网站间只用一个连接(connection)。 -
webpack
webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle -
envoy
网络代理 -
docker
一个开源的应用容器引擎,基于 Go 语言 并遵从 Apache2.0 协议开源,可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中,然后发布到任何流行的 Linux 机器上,也可以实现虚拟化。容器是完全使用沙箱机制,相互之间不会有任何接口(类似 iPhone 的 app),更重要的是容器性能开销极低。
参考链接
1. https://github.com/grpc/grpc-web
2. https://www.envoyproxy.io/
3. https://grpc.io/docs/tutorials/basic/web/
网友评论