准备
使用 go mod 建立工程和新增api目录
.
├── api
│ ├── gen # protoc或buf编译*.proto的代码
│ ├── google # gRPC或googleapis提供的*.proto文件
│ └── v1 # 业务相关的*.proto
├── build
├── cmd
├── configs
├── internal
├── pkg
├── go.mod # go mod 初始化建立
└── go.sum # go mod 初始化建立
第三方.proto文件
googleapis | github.com/googleapis/… | HTTP映射gRPC规则 |
protocolbuffers | github.com/protocolbuf… | protobuf源码的类型 |
在编写自定义.proto时,必要时需要引入其它预定义好的数据结构,避免重复编码,而HTTP映射gRPC需要引入googleapi中的http.proto注解,这里先把可能用到的数据结构copy进来
├── google
│ ├── api
│ │ ├── annotations.proto
│ │ └── http.proto
│ └── protobuf
│ ├── any.proto
│ ├── api.proto
│ ├── descriptor.proto
│ ├── duration.proto
│ ├── empty.proto
│ ├── field_mask.proto
│ ├── plugin.proto
│ ├── source_context.proto
│ ├── struct.proto
│ ├── timestamp.proto
│ ├── type.proto
│ └── wrappers.proto
又或者可以参考buf中BSR仓库 Docs · googleapis/googleapis 引入,按照这个仓库的说法,存放的30+个文件足够覆盖99.999%开发者的使用需求。
编码自定义业务
还是以最简单的Echo服务,按照 上一节 介绍的编写业务代码的流程
1.编写.proto描述服务
proto语法请参考 Language Guide (proto 3) | Protocol Buffers Documentation ,在 /api/v1 下新建 echo.proto
syntax = "proto3";
package api.v1;
option go_package = "api.v1;pb";
message EchoRequest {
string value = 1;
}
message EchoResponse {
string value = 1;
}
service EchoService {
rpc Echo(EchoRequest) returns (EchoResponse) {}
}
2.引入http.rule
当你浏览 grpc-ecosystem/grpc-gateway 会发现映射规则是这样写的:
import "google/api/annotations.proto";
service EchoService {
rpc Echo(EchoRequest) returns (EchoResponse) {
option (google.api.http) = {
post: "/v1/example/echo"
body: "*"
}
}
}
老实说并不是很喜欢这种写法,因为对于服务与服务之间的调用通常是通过RPC的,这样HTTP的映射规则显得有点多余,这样会污染proto的代码。
通过buf配置映射HTTP Rule
除了通过 protoc 编译 *.proto,还可以通过 Buf 配置编译,以echo服务为例,在 /api/v1 下新建 gateway.yaml 指定转换规则,同样可以达到在echo.proto引入注解的效果
type: google.api.Service
config_version: 3
http:
rules:
- selector: api.v1.EchoService.Echo
post: /v1/example/echo
body: "*"
3.HTTP Rule与gRPC的参数转换
我们知道一个HTTP请求包含请求头和请求体,当发出一个请求时,通常会在几个地方携带请求参数
Request Header 中的自定义字段
URL携带的查询参数
Request Body 中请求体
http.proto就规定了参数如何转换成.proto定义的请求参数,这里主要是关于URL查询参数和Body数据如何转换
3.1 URL path
service Messaging {
rpc GetMessage(GetMessageRequest) returns (Message) {
option (google.api.http) = {
get: "/v1/{name=messages/*}"
};
}
}
message GetMessageRequest {
string name = 1; // Mapped to URL path.
}
message Message {
string text = 1; // The resource content.
}
HTTP | gRPC |
---|---|
GET /v1/messages/123456 | GetMessage(name: “messages/123456”) |
3.2 查询参数
service Messaging {
rpc GetMessage(GetMessageRequest) returns (Message) {
option (google.api.http) = {
get:"/v1/messages/{message_id}"
};
}
}
message GetMessageRequest {
message SubMessage {
string subfield = 1;
}
string message_id = 1; // Mapped to URL path.
int64 revision = 2; // Mapped to URL query parameter `revision`.
SubMessage sub = 3; // Mapped to URL query parameter `sub.subfield`.
}
HTTP | gRPC |
---|---|
GET /v1/messages/123456?revision=2&sub.subfield=foo | GetMessage(message_id: “123456” revision: 2 sub: SubMessage(subfield: “foo”)) |
3.3 Body
service Messaging {
rpc UpdateMessage(UpdateMessageRequest) returns (Message) {
option (google.api.http) = {
patch: "/v1/messages/{message_id}"
body: "message"
};
}
}
message UpdateMessageRequest {
string message_id = 1; // mapped to the URL
Message message = 2; // mapped to the body
}
HTTP | gRPC |
---|---|
PATCH /v1/messages/123456 { “text”: “Hi!” } | UpdateMessage(message_id: “123456” message { text: “Hi!” }) |
3.4 Body = “*”
service Messaging {
rpc UpdateMessage(Message) returns (Message) {
option (google.api.http) = {
patch: "/v1/messages/{message_id}"
body: "*"
};
}
}
message Message {
string message_id = 1;
string text = 2;
}
HTTP | gRPC |
---|---|
PATCH /v1/messages/123456 { “text”: “Hi!” } | UpdateMessage(message_id: “123456” text: “Hi!”) |
3.5 多个HTTP-URL绑定一个服务
service Messaging {
rpc GetMessage(GetMessageRequest) returns (Message) {
option (google.api.http) = {
get: "/v1/messages/{message_id}"
additional_bindings {
get: "/v1/users/{user_id}/messages/{message_id}"
}
};
}
}
message GetMessageRequest {
string message_id = 1;
string user_id = 2;
}
HTTP | gRPC |
---|---|
GET /v1/messages/123456 | GetMessage(message_id: “123456”) |
GET /v1/users/me/messages/123456 | GetMessage(user_id: “me” message_id: “123456”) |
更多的可以参考 http.proto 中的注释,以上例子均出自此文件用例
4.编译.proto文件生成接口层
现在已经完成了 echo.proto 服务定义和 gateway.yaml 对HTTP Rule的映射。
避免使用 protoc 繁琐的命令行操作,这里使用 buf 工具对其编译生成 HTTP 和 gRPC 的接口层代码
在编译之前, buf 工具需要指向被编译的 *.proto 的目录,在 /api 下新建 buf.yaml
version: v2
# The v2 buf.yaml file specifies a local workspace, which consists of at least one module.
# The buf.yaml file should be placed at the root directory of the workspace, which
# should generally be the root of your source control repository.
modules:
# Each module entry defines a path, which must be relative to the directory where the
# buf.yaml is located. You can also specify directories to exclude from a module.
# A module can have its own lint and breaking configuration, which overrides the default
# lint and breaking configuration in its entirety for that module. All values from the
# default configuration are overridden and no rules are merged.
- path: v1
lint:
use:
- DEFAULT
except:
- PACKAGE_DIRECTORY_MATCH
- RPC_REQUEST_RESPONSE_UNIQUE
- RPC_REQUEST_STANDARD_NAME
- RPC_RESPONSE_STANDARD_NAME
breaking:
use:
- FILE
并且,编译过程要指定哪些插件参与编译,编译代码输出目录,在 /api 下新建 buf.gen.yaml
version: v2
clean: true
plugins:
# protoc-gen-go needs to be installed, generate go files based on proto files
- local: protoc-gen-go
out: gen/v1
opt:
- paths=source_relative
# protoc-gen-go-grpc needs to be installed, generate grpc go files based on proto files
- local: protoc-gen-go-grpc
out: gen/v1
opt:
- paths=source_relative
- require_unimplemented_servers=false
# protoc-gen-grpc-gateway needs to be installed, generate grpc-gateway go files based on proto files
- local: protoc-gen-grpc-gateway
out: gen/v1
opt:
- paths=source_relative
- grpc_api_configuration=v1/gateway.yaml
# protoc-gen-openapiv2 needs to be installed, generate swagger config files based on proto files
- local: protoc-gen-openapiv2
out: gen/v1
opt:
- grpc_api_configuration=v1/gateway.yaml
> protoc-gen-go --version
protoc-gen-go v1.36.5
> protoc-gen-go-grpc --version
protoc-gen-go-grpc 1.5.1
> protoc-gen-grpc-gateway --version
Version v2.26.1, commit unknown, built at unknown
> protoc-gen-openapiv2 --version
Version v2.26.1, commit unknown, built at unknown
> buf --version
1.50.0
> cd api/
> buf generate
编译成功后会在 api/gen 下生成请求参数,返回响应,HTTP和gRPC的接口层的代码
.
├── api
│ ├── buf.gen.yaml # 指定哪些插件参与编译
│ ├── buf.yaml # 指定*.proto目录
│ ├── gen
│ │ └── v1
│ │ ├── echo.pb.go # 请求参数,返回响应数据结构
│ │ ├── echo.pb.gw.go # http伪装gRPC代码
│ │ ├── echo.swagger.json # api文档
│ │ └── echo_grpc.pb.go # grpc服务
│ ├── google
│ │ ├── api
│ │ │ ├── annotations.proto
│ │ │ ├── http.proto
│ │ ├── protobuf
│ │ │ ├── any.proto
│ │ │ ├── api.proto
│ │ │ ├── descriptor.proto
│ │ │ ├── duration.proto
│ │ │ ├── empty.proto
│ │ │ ├── field_mask.proto
│ │ │ ├── plugin.proto
│ │ │ ├── source_context.proto
│ │ │ ├── struct.proto
│ │ │ ├── timestamp.proto
│ │ │ ├── type.proto
│ │ │ └── wrappers.proto
│ └── v1
│ ├── echo.proto # 自定义业务服务
│ └── gateway.yaml # HTTP Rule映射
├── build
├── cmd
├── configs
├── internal
├── pkg
├── go.mod
└── go.sum
5.实现业务逻辑
对于自动生成的代码中并不会包含业务逻辑,所以在 internal/ 目录内新建 service.go
├── internal
│ └── app
│ └── echo
│ └── service
│ └── service.go # 新增
在编写 echo.proto 时指定编译之后的包名
option go_package = "api.v1;pb"; // 导入包名定义为pb,编码时编辑器能够自动补全
简单实现 echo 业务服务:返回输入字符串
package service
import (
"context"
pb "go-web/api/gen/v1"
)
var _ pb.EchoServiceServer = (*EchoService)(nil)
type EchoService struct{}
func NewEchoService() *EchoService {
return &EchoService{}
}
func (srv *EchoService) Echo(ctx context.Context, req *pb.EchoRequest) (*pb.EchoResponse, error) {
return &pb.EchoResponse{Value: req.Value}, nil
}
实例化gRPC服务
在 interal/ 目录下新建 grpc.go
├── internal
│ └── app
│ └── health
│ ├── server
│ │ └── grpc.go # 新增
│ └── service
│ └── service.go
gRPC服务与业务服务之间究竟存在着怎样的关系呢?
让我们先来回顾一下RPC的调用格式:(服务器地址+服务名+API+请求参数)-> 服务响应。当RPC服务接收到请求参数后,会依据参数中给出的服务名和API进行查找。以echo示例而言,服务名便是在proto文件中定义的EchoService,而API则是rpc Echo(EchoRequest) returns (EchoResponse) {},随后便会展开调用。
然而,在gRPC生成的代码里,并不包含自定义业务逻辑的实现部分。这就意味着,开发者需要自行实现一个与API签名完全一致的业务方法。完成业务方法的编写后,还需将其绑定(注入)到gRPC服务当中。只有经过这一步骤,gRPC服务才具备调用该业务方法的能力,从而实现业务逻辑与服务框架的有效衔接,达成完整的服务功能。
package server
import (
"net"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
pb "go-web/api/gen/v1"
"go-web/internal/app/health/service"
)
const GRPC_ADDR = ":58081"
func NewGRPC(srv *service.EchoService) (grpcSvr *grpc.Server, err error) {
// 实例化gRPC服务
grpcSvr = grpc.NewServer()
// 这里就是用自定义业务服务绑定(注入)到gRPC服务当中,gRPC能够调用业务服务,因为它们有相同的函数签名
pb.RegisterEchoServiceServer(grpcSvr, srv)
// 注册反射服务,方便我们用外部工具调试
reflection.Register(grpcSvr)
go func() {
// 绑定并监听IP和Port
listener, err := net.Listen("tcp", GRPC_ADDR)
if err != nil {
panic(err)
}
if err = grpcSvr.Serve(listener); err != nil {
panic(err)
}
}()
return
}
实例化HTTP服务
在 interal/ 目录下新建 http.go
├── internal
│ └── app
│ └── health
│ ├── server
│ │ ├── grpc.go
│ │ └── http.go # 新增
│ └── service
│ └── service.go
实例化HTTP服务:
package server
import (
"context"
"errors"
"net/http"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
pb "go-web/api/gen/v1"
)
const APP_ADDR = ":8081"
func NewHTTP() (httpSvr *http.Server, err error) {
// HTTP路由
mux := runtime.NewServeMux(runtime.WithErrorHandler(errorHandler))
// HTTP服务同时也是gRPC客户端,客户端以非安全模式发出请求时
opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
// 客户端指向gRPC服务地址,并向HTTP路由写入URL => gRPC服务的映射
err = pb.RegisterEchoServiceHandlerFromEndpoint(context.Background(), mux, APP_ADDR, opts)
if err != nil {
panic(err)
}
// 启动HTTP服务
httpSvr = &http.Server{Addr: APP_ADDR, Handler: mux}
go func() {
if err = httpSvr.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
panic(err)
}
}()
return
}
// 错误处理,将gRPC返回结果封装成HTTP的响应,以JSON格式返回
func errorHandler(ctx context.Context, mux *runtime.ServeMux, marshaler runtime.Marshaler, writer http.ResponseWriter, request *http.Request, err error) {
runtime.DefaultHTTPErrorHandler(ctx, mux, marshaler, writer, request, err)
}
测试一下
此时把 EchoService、GrpcServer、HTTPServer 封装成应用程序,在 internal/ 内新建 app.go
├── internal
│ └── app
│ └── health
│ ├── di
│ │ └── app.go # 新增
│ ├── server
│ │ ├── grpc.go
│ │ └── http.go
│ └── service
│ └── service.go
package di
import (
"context"
"net/http"
"time"
"google.golang.org/grpc"
"go-web/internal/app/health/service"
)
type App struct {
srv *service.EchoService
httpSvr *http.Server
grpcSvr *grpc.Server
}
func NewApp(srv *service.EchoService, httpSvr *http.Server, grpcSvr *grpc.Server) (*App, func(), error) {
app := &App{
srv: srv,
httpSvr: httpSvr,
grpcSvr: grpcSvr,
}
// 清理函数,关闭HTTP和gRPC服务
closeFunc := func() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
app.grpcSvr.Stop()
_ = app.httpSvr.Shutdown(ctx)
}
return app, closeFunc, nil
}
接着,在 main 函数中实例化 App 并注入相关依赖,在 cmd 中新建 echo.go
├── cmd
│ └── echo.go
package main
import (
"os"
"os/signal"
"syscall"
"go-web/internal/app/health/di"
"go-web/internal/app/health/server"
"go-web/internal/app/health/service"
)
func main() {
echoSrv := service.NewEchoService()
grpcSvr, _ := server.NewGRPC(echoSrv)
httpSvr, _ := server.NewHTTP()
_, closeFunc, _ := di.NewApp(echoSrv, httpSvr, grpcSvr)
// Wait for interrupt signal to gracefully shutdown the server
quit := make(chan os.Signal, 1)
// kill (no param) default send syscall.SIGTERM
// kill -2 is syscall.SIGINT
// kill -9 is syscall.SIGKILL but can't be catch, so don't need add it
signal.Notify(quit, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGINT, syscall.SIGTERM)
for {
signal := <-quit
switch signal {
case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT:
closeFunc()
return
case syscall.SIGHUP:
default:
return
}
}
}
最后,启动 cmd/echo.go 并测试:
go run cmd/echo.go
使用 grpcurl 工具测试gRPC服务
grpcurl -d '{"value": "Hello, World!"}' -plaintext localhost:58081 api.v1.EchoService.Echo
可以看到回显输入字符
{
"value": "Hello, World!"
}
使用 curl 测试HTTP服务,
curl -X POST \
-d '{"value": "Hello, World!"}' \
-H 'Content-Type: application/json' \
http://localhost:8081/v1/example/echo
同样可以看到回显输入字符
{"value":"Hello, World!"}%
gRPC对HTTP的映射
- How gRPC error codes map to HTTP status codes in the response.
- HTTP request source IP is added as
X-Forwarded-For
gRPC request header. - HTTP request host is added as
X-Forwarded-Host
gRPC request header. - HTTP
Authorization
header is added asauthorization
gRPC request header. - Remaining Permanent HTTP header keys (as specified by the IANA here) are prefixed with
grpcgateway-
and added with their values to gRPC request header. - HTTP headers that start with ‘Grpc-Metadata-’ are mapped to gRPC metadata (prefixed with
grpcgateway-
). - While configurable, the default {un,}marshaling uses protojson.
- The path template used to map gRPC service methods to HTTP endpoints supports the google.api.http path template syntax. For example,
/api/v1/{name=projects/*/topics/*}
or/prefix/{path=organizations/**}
.
可以看到针对 HTTP Header 的 IP、host、Authorization 有特殊映射外,其余字段会默认添加 grpcgateway- 前缀,如果需要在请求时带上自定义头,务必在HTTP自定义请求头加上 Grpc-Metadata- 前缀,gateway框架会帮你映射到gRPC的 metadata 上面,方便在业务逻辑上提取。
文章来源: https://study.disign.me/article/202508/11.grpc-gateway.md
发布时间: 2025-02-20
作者: 技术书栈编辑