title: 使用TypeScript与Golang进行gRPC通信
tags:
- gRPC
categories: -
- 后端开发
- Golang开发
-
- 前端开发
- TypeScript
abbrlink: 87cd08b2
date: 2020-02-14 15:21:14
原文链接:https://blog.lemonit.cn/posts/87cd08b2.html
0x00 前言
最近为了提高自身能力,尝试自己用golang搞了个微服务框架,服务间通信全部使用的是gRPC,越用越觉得舒服,性能很高,而且protoc自动生成代码这点也很值得使用。
随着框架开发过程的推进,该进行WebUI的开发了,翻了下gRPC官网https://grpc.io,发现官方对TypeScript的支持也已经很成熟了,反正也是秉着提高自身能力的心情来开发的,决定入坑走一圈……
至于protobuf、gRPC的介绍这里就不赘述了,各位请自己百度或谷歌
0x01 准备环境
A. 安装protoc
首先需要下载安装protoc,用于根据proto定义文件生成各种语言的代码。下载地址为:
https://github.com/protocolbuffers/protobuf/releases
进入releases页面后,向下滚动,找到适合自己操作系统的版本,下载下来,我的操作系统是macOS,所以选择的是protoc-3.11.3-osx-x86_64.zip
,下载后解压,然后将目录加入到系统环境变量中,最后保证在控制台中可以使用protoc命令,效果如下:
liuri@liurideMacBook-Pro blog.lemonit.cn % protoc --version
libprotoc 3.11.3
B. 安装protoc-gen-go
由于我这里后端使用的是Golang,所以接下来安装用于Go语言代码的插件,首先需要正确配置好$GOPATH环境变量,如果不确定是否自己已经成功配置,使用这个命令查看一下,输出的GOPATH是否如你所愿:
liuri@liurideMacBook-Pro blog.lemonit.cn % go env | grep GOPATH
GOPATH="/Users/liuri/go"
请确认你的gopath所指向的文件夹中有src、pkg、bin三个文件夹
接下来,我们开始下载,执行如下命令:
go get github.com/golang/protobuf/protoc-gen-go
下载完毕后确认$GOPATH/bin/中存在protoc-gen-go可执行文件即可
C.安装ts-protoc-gen
这里特殊说明一下,gRPC官方针对gRPC-web有两个实现,一个是grpc/grpc-web,一个是improbable-eng/grpc-web,这里我选择的是第二个improbable-eng/grp-web,因为据我目前对第grpc/grpc-web的了解,必须启用一个独立的proxy做base64->binary的转发(也可能是我了解不够,如果各位大神有知晓的可以指点一下),而我目前开发的框架的场景下因为种种原因不允许有这层独立的proxy,而第二种可以选择内置的grpcWeb方式来监听普通的Http端口,所以我选择了improbable-eng/grpc-web,如果您选择的是grpc/grpc-web,那么本文对您可能没有太大意义
首先大家可以看一下官方Github仓库,里面有一些对大家有用的文档可以作为参考资料:
https://github.com/improbable-eng/ts-protoc-gen#readme
首先我们创建一个新项目,我这里创建了一个vue+typescript项目,大家根据自身情况自行选择。然后我们使用npm命令开始安装,控制台执行:
npm install ts-protoc-gen --save
当然也可以用yarn来安装:
yarn add ts-protoc-gen
安装后,确保项目node_modules/.bin/
目录中存在protoc-gen-ts
文件,然后大家要记录一下这个文件在电脑中的绝对路径,稍后会用到。
0x02. 定义服务
环境安装好后,接下来,我们开始定义message和服务,首先,先定义message部分:
syntax = "proto3";
package usr_dto;
option go_package = "github.com/lemon-cloud-service/lemon-cloud-dashboard/lemon-cloud-dashboard-common/usr_dto";
message AdminLoginRequest {
string number = 1;
string password = 2;
}
message AdminLoginResponse {
string token = 1;
string username = 2;
}
然后是service部分的定义:
syntax = "proto3";
package usr_service;
option go_package = "github.com/lemon-cloud-service/lemon-cloud-dashboard/lemon-cloud-dashboard-common/usr_service";
import "protobuf/usr_dto/admin.proto";
service AdminService {
rpc Login (usr_dto.AdminLoginRequest) returns (usr_dto.AdminLoginResponse) {
}
}
由于工程结构的习惯,我将message的定义和service定义拆分成两个文件夹存储,大家可以根据自己的习惯而定,然后我们来根据定义自动生成对应语言的代码,这里我写了一个shell脚本,大家可以作为参考,根据自己的实际目录结构进行修改,其中第一行是刚才让大家记录的proto-gen-ts文件的绝对路径,不过大家可能会有疑问为什么我写的是相对路径,官方文档中说明:macOS和Linux中可以使用命令行所处位置的相对路径,但是Windows中必须使用绝对路径,所以使用绝对路径肯定没错,macOS和Linux用户可以偷下懒:
PROTOC_GEN_TS_PATH="../../lemon-cloud-dashboard-ui/node_modules/.bin/protoc-gen-ts"
# generate usr-dto-golang
rm usr_dto/**.pb.go
protoc -I . --go_out=plugins=grpc:usr_dto protobuf/usr_dto/*.proto
cp $(find usr_dto/ -type f -name "*.pb.go") usr_dto/
rm -rf usr_dto/github.com
# generate usr-service-golang
rm usr_service/**.pb.go
protoc -I . --go_out=plugins=grpc:usr_service protobuf/usr_service/*.proto
cp $(find usr_service/ -type f -name "*.pb.go") usr_service/
rm -rf usr_service/github.com
# generate usr-dto-web
rm js_usr_dto/**_pb.js
rm js_usr_dto/**.ts
protoc \
--plugin="protoc-gen-ts=${PROTOC_GEN_TS_PATH}" \
--js_out="import_style=commonjs,binary:js_usr_dto" \
--ts_out="service=grpc-web:js_usr_dto" \
protobuf/usr_dto/*.proto
cp $(find js_usr_dto/ -type f -name "*.js") js_usr_dto/
cp $(find js_usr_dto/ -type f -name "*.ts") js_usr_dto/
rm -rf js_usr_dto/protobuf
# generate usr-service-web
rm js_usr_service/**_pb.js
rm js_usr_service/**.ts
protoc \
--plugin="protoc-gen-ts=${PROTOC_GEN_TS_PATH}" \
--js_out="import_style=commonjs,binary:js_usr_service" \
--ts_out="service=grpc-web:js_usr_service" \
protobuf/usr_service/*.proto
cp $(find js_usr_service/ -type f -name "*.js") js_usr_service/
cp $(find js_usr_service/ -type f -name "*.ts") js_usr_service/
rm -rf js_usr_service/protobuf
脚本执行后,目前我的文件目录结构如下:
├── js_usr_dto // 以下是JS/TS代码生成输出
│ ├── admin_pb.d.ts
│ ├── admin_pb.js
│ ├── admin_pb_service.d.ts
│ └── admin_pb_service.js
├── js_usr_service
│ ├── admin_pb.d.ts
│ ├── admin_pb.js
│ ├── admin_pb_service.d.ts
│ └── admin_pb_service.js
├── proto.gen.sh // 代码的生成脚本(上段提供的代码所在文件)
├── protobuf
│ ├── usr_dto
│ │ └── admin.proto
│ └── usr_service
│ └── admin.proto
├── usr_dto // 以下是Golang代码生成输出
│ └── admin.pb.go
└── usr_service
└── admin.pb.go
0x03. 编码对接服务
接下来,我们先将处理后端部分,我们创建一个Golang项目,将刚才生成的usr_dto、usr_service两个文件夹拷贝进去,接下来,我们需要为刚才定义的service写实现部分,创建admin_service_impl.go文件,内容如下:
package usr_service_impl
import (
"context"
"github.com/lemon-cloud-service/lemon-cloud-dashboard/lemon-cloud-dashboard-common/usr_dto"
)
type AdminUsrServiceImpl struct{}
func (AdminUsrServiceImpl) Login(context.Context, *usr_dto.AdminLoginRequest) (*usr_dto.AdminLoginResponse, error) {
return &usr_dto.AdminLoginResponse{
Token: "token1122334455",
Username: "LemonIT.CN柠檬信息技术有限公司",
}, nil
}
然后创建一个main.go文件作为入口,并使用如下代码进行定义服务并启动:
package main
import (
"fmt"
"github.com/improbable-eng/grpc-web/go/grpcweb"
"github.com/lemon-cloud-service/lemon-cloud-dashboard/lemon-cloud-dashboard-common/usr_service"
"github.com/lemon-cloud-service/lemon-cloud-dashboard/lemon-cloud-dashboard-service/usr_service_impl"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
"google.golang.org/grpc"
"net"
"net/http"
)
func main() {
s := grpc.NewServer()
usr_service.RegisterAdminServiceServer(s, &usr_service_impl.AdminUsrServiceImpl{})
grpcWebServer := grpcweb.WrapServer(s)
httpServer := &http.Server{
Handler: h2c.NewHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.ProtoMajor == 2 {
grpcWebServer.ServeHTTP(w, r)
} else {
// 此部分代码用作跨域配置,允许前台跨域访问
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, X-User-Agent, X-Grpc-Web")
if grpcWebServer.IsGrpcWebRequest(r) {
grpcWebServer.ServeHTTP(w, r)
}
}
}), &http2.Server{}),
}
listen1, _ := net.Listen("tcp", ":33385")
fmt.Println("服务启动完毕...")
httpServer.Serve(listen1)
}
我们现在把服务启动起来,使用如下命令,并可以看见我们刚刚打印的提示服务启动完毕:
liuri@liurideMacBook-Pro lemon-cloud-dashboard-service % go run main.go
服务启动完毕...
好了到此为止后台服务已经准备就绪了,接下来我们来打开刚刚创建的前端项目,将刚刚生成的js_usr_dto、js_usr_service两个文件夹复制到项目中,大家需要看一下文件中是否有报错,因为service引用dto部分的路径可能和实际的部分,大家根据自己的情况自行修改一下
除了路径的错误,大家还可以看见找不到一些依赖的错误提示,大家可以根据错误提示进行安装:
yarn add @improbable-eng/grpc-web
yarn add @types/google-protobuf
最后在适当的地方(根据您刚创建的项目类型自行选择,我的是vue+ts项目,所以我选择了在某个vue文件的mounted生命周期中执行)编写如下代码:
const client = new AdminServiceClient('http://localhost:33385')
const req = new AdminLoginRequest()
req.setNumber('lemonitcn')
req.setPassword('123456')
client.login(req, (err: ServiceError|null, rsp: AdminLoginResponse | null) => {
console.log('err: %O, ', err)
console.log(rsp?.getUsername())
console.log(rsp?.getToken())
})
接下来,我们测试一下,执行上述代码后,可以在console中看到我们刚刚输出的信息,第一行err: null表示无错误,第二行第三行分别是后台刚刚返回的username和token字段内容:
err: null,
LemonIT.CN柠檬信息技术有限公司
token1122334455
0x04. 后记
到此为止,我们的通信算是成功了,这期间踩了不少坑,百度发现根本找不到类似的问题,但是Google可以找到很多解决方案,如果读者也有许多问题,建议脱墙后访问谷歌进行搜索,百度中的资料实在有限,可能是国内的web项目中太少用gRPC导致的吧。
大家有问题可以随时骚扰交流,微信号:qbxx002
参考资料
grpc-web官方Github仓库:https://github.com/improbable-eng/grpc-web
protoc-typescript代码生成工具Github仓库:https://github.com/improbable-eng/ts-protoc-gen#readme