使用TypeScript与Golang进行gRPC通信


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

你可能感兴趣的:(使用TypeScript与Golang进行gRPC通信)