美文网首页
使用TypeScript与Golang进行gRPC通信

使用TypeScript与Golang进行gRPC通信

作者: 柠檬信息技术有限公司 | 来源:发表于2020-02-14 16:49 被阅读0次

    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通信

          本文链接:https://www.haomeiwen.com/subject/qhkjfhtx.html