1、介绍
正当有这个需求的时候,就看到了这个实现姿势。源自coreos的一篇博客,转载到了grpc官方博客 gRPC with REST and Open APIs。
etcd3 改用 grpc 后为了兼容原来的api,同时要提供 http/json 方式的API,为了满足这个需求,要么开发两套API,要么实现一种转换机制,他们选择了后者,而我们选择跟随他们的脚步。
他们实现了一个协议转换的网关,对应github上的项目 grpc-gateway ,这个网关负责接收客户端请求,然后决定直接转发给grpc 服务还是转给http服务,当然,http服务也需要请求grpc服务获取响应,然后转为 json 响应给客户端。
结构如图:
基于hello-tls项目扩展,客户端改动不大,服务端和proto改动较大。
2、安装grpc-gateway
# 如这个命令不起作用或者报错
go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway
# 使用这个
go install github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway@latest
备注:目前 grpc-gateway 已经升级到 v2
v2 的安装如下:
go install \
github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway \
github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2 \
google.golang.org/protobuf/cmd/protoc-gen-go \
google.golang.org/grpc/cmd/protoc-gen-go-grpc
3、代码示例
当前还是以非v2版本示例。
这里用到了google官方Api中的两个proto描述文件:annotations.proto
、http.proto
,直接拷贝不要做修改,里面定义了protocol buffer扩展的HTTP option,为grpc的http转换提供支持。
在项目根目录创建 third_party
目录,用于存放 第三方扩展。
上述文件可以在一下GitHub库找到:
https://github.com/googleapis/googleapis
此存储库包含支持 REST 和 gRPC 协议的公共 Google API 的原始接口定义。
由于 annotations.proto
中还 引用了 google/protobuf/descriptor.proto
故最好 descriptor.proto
也拷贝进去
具体效果如下图:
Step 1. 编写proto描述文件
pb/hello.proto文件如下:
这里在原来的SayHello方法定义中增加了http option, POST方式,路由为/example/echo
。
syntax = "proto3";
package pb; // 当前包名
option go_package = "./;pb";
import "google/api/annotations.proto";
message HelloRequest{
string name = 1;
}
message HelloResponse{
string result = 1;
}
service HelloService {
rpc SayHello(HelloRequest) returns (HelloResponse) {
// 这里在原来的SayHello方法定义中增加了http option, POST方式,路由为”/example/echo”。
option (google.api.http) = {
post: "/example/echo",
body: "*"
};
}
}
Step 2. 编译proto
protoc -I ./pb --proto_path=./third_party --go_out ./pb --go_opt paths=source_relative --go-grpc_out ./pb --go-grpc_opt paths=source_relative --grpc-gateway_out ./pb --grpc-gateway_opt paths=source_relative ./pb/hello.proto
备注:查看百度很多其他文档,都报错,只有这个才有效果
这里在编译的 时候会引入 third_party
目录中 proto文件,最后使用grpc-gateway编译生成hello.pb.gw.go
文件。
这个文件就是用来做协议转换的,查看文件可以看到里面生成的http handler,处理proto文件中定义的路由example/echo
接收POST参数,调用 HelloHTTP 服务的客户端请求grpc服务并响应结果。
Step 3: 实现服务端和客户端
这里主要是服务端的修改
服务端代码如下:
package main
import (
"context"
"crypto/tls"
"fmt"
"github.com/grpc-ecosystem/grpc-gateway/runtime"
"go_study/framework/grpc/pb"
"golang.org/x/net/http2"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/grpclog"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"io/ioutil"
"net"
"net/http"
"strings"
)
// 定义 UserInfoService 服务
type UserInfoService struct{
pb.UnimplementedUserInfoServiceServer
}
// GetUserInfo 实现方法
func (s *UserInfoService) GetUserInfo(ctx context.Context, req *pb.UserRequest) (resp *pb.UserResponse, err error) {
//err = tokenAuth(ctx)
//if err != nil {
// return nil, err
//}
// 通过用户名查询用户信息
name := req.Name
// 数据里查用户信息
if name == "zs" {
resp = &pb.UserResponse{
Id: 1,
Name: name,
Age: 22,
Hobby: []string{"Sing", "Run"},
}
}
return
}
type HelloService struct {
pb.UnimplementedHelloServiceServer
}
func (h *HelloService) SayHello(ctx context.Context, in *pb.HelloRequest) (resp *pb.HelloResponse, err error) {
//err = tokenAuth(ctx)
//if err != nil {
// return nil, err
//}
resp = new(pb.HelloResponse)
resp.Result = fmt.Sprintf("hello %s", in.Name)
return resp, nil
}
// 使用这种方式验证token 每个服务端每个服务中都验证一次,比较繁琐
func tokenAuth(ctx context.Context) error {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return status.Errorf(codes.Unauthenticated, "无Token认证信息")
}
var (
appid string
appkey string
)
if val, ok := md["appid"]; ok {
appid = val[0]
}
if val, ok := md["appkey"]; ok {
appkey = val[0]
}
if appid != "101010" || appkey != "i am key" {
return status.Errorf(codes.Unauthenticated, "Token认证信息无效: appid=%s, appkey=%s", appid, appkey)
}
return nil
}
// interceptor 拦截器
// grpc.UnaryServerInfo 包含有关服务器端一元 RPC 的各种信息。所有 per-rpc 信息都可能被拦截器改变
func interceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
err := tokenAuth(ctx)
if err != nil {
return nil, err
}
// 继续处理请求
return handler(ctx, req)
}
// grpcHandlerFunc 检查请求协议并返回http handler
func grpcHandlerFunc(grpcServer *grpc.Server, otherHandler http.Handler) http.Handler {
if otherHandler == nil {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
grpcServer.ServeHTTP(w, r)
})
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.ProtoMajor == 2 && strings.Contains(r.Header.Get("Content-Type"), "application/grpc") {
grpcServer.ServeHTTP(w, r)
} else {
otherHandler.ServeHTTP(w, r)
}
})
}
func getTLSConfig() *tls.Config {
cert, _ := ioutil.ReadFile("./framework/grpc/keys/server.pem")
key, _ := ioutil.ReadFile("./framework/grpc/keys/server.key")
var demoKeyPair *tls.Certificate
pair, err := tls.X509KeyPair(cert, key)
if err != nil {
grpclog.Fatalf("TLS KeyPair err: %v\n", err)
}
demoKeyPair = &pair
return &tls.Config{
Certificates: []tls.Certificate{*demoKeyPair},
NextProtos: []string{http2.NextProtoTLS}, // HTTP2 TLS支持
}
}
func main(){
// 地址
//addr := "127.0.0.1:8180"
endpoint := "127.0.0.1:8180"
// 1.监听
conn, err := net.Listen("tcp", endpoint)
if err != nil {
grpclog.Fatalf("Failed to listen: %v", err)
}
fmt.Printf("监听端口:%s\n", endpoint)
// tls认证
creds,err := credentials.NewServerTLSFromFile("./framework/grpc/keys/server.pem", "./framework/grpc/keys/server.key")
if err != nil {
grpclog.Fatalf("Failed to generate credentials %v", err)
}
var opts []grpc.ServerOption
// 添加 tls 认证
opts = append(opts, grpc.Creds(creds))
// 注册拦截器
opts = append(opts, grpc.UnaryInterceptor(interceptor))
// 2.实例化gRPC
s := grpc.NewServer(opts...)
// 3.在gRPC上注册微服务
var u = UserInfoService{}
pb.RegisterUserInfoServiceServer(s, &u)
var h = HelloService{}
pb.RegisterHelloServiceServer(s, &h)
// gw server
ctx := context.Background()
dcreds, err := credentials.NewClientTLSFromFile("./framework/grpc/keys/server.pem", "www.admincms.com")
if err != nil {
grpclog.Fatalf("Failed to create client TLS credentials %v", err)
}
dopts := []grpc.DialOption{grpc.WithTransportCredentials(dcreds)}
gwmux := runtime.NewServeMux()
if err = pb.RegisterHelloServiceHandlerFromEndpoint(ctx, gwmux, endpoint, dopts); err != nil {
grpclog.Fatalf("Failed to register gw server: %v\n", err)
}
if err = pb.RegisterHelloServiceHandlerFromEndpoint(ctx, gwmux, endpoint, dopts); err != nil {
grpclog.Fatalf("Failed to register gw server: %v\n", err)
}
// http服务
mux := http.NewServeMux()
mux.Handle("/", gwmux)
srv := &http.Server{
Addr: endpoint,
Handler: grpcHandlerFunc(s, mux),
TLSConfig: getTLSConfig(),
}
grpclog.Infof("gRPC and https listen on: %s\n", endpoint)
if err = srv.Serve(tls.NewListener(conn, srv.TLSConfig)); err != nil {
grpclog.Fatal("ListenAndServe: ", err)
}
return
}
客户端代码不变:
package main
import (
"context"
"fmt"
"go_study/framework/grpc/pb" // 导入proto
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/grpclog"
"time"
)
// OpenTLS 是否开启TLS认证
var OpenTLS = true
type customCredential struct {}
func (c *customCredential) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
return map[string]string{
"appid": "101010",
"appkey": "i am key",
}, nil
}
func (c *customCredential) RequireTransportSecurity() bool {
return OpenTLS
}
// interceptor 客户端拦截器
func interceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
start := time.Now()
err := invoker(ctx, method, req, reply, cc, opts...)
grpclog.Infof("method=%s req=%v rep=%v duration=%s error=%v\n", method, req, reply, time.Since(start), err)
return err
}
func main(){
addr := "127.0.0.1:8180"
var err error
var opts []grpc.DialOption
if OpenTLS {
// TLS连接 记得把server name改成你写的服务器地址
creds, err := credentials.NewClientTLSFromFile("./framework/grpc/keys/server.pem", "www.admincms.com")
if err != nil {
grpclog.Fatalf("Failed to create TLS credentials %v", err)
}
opts = append(opts, grpc.WithTransportCredentials(creds))
}else{
opts = append(opts, grpc.WithInsecure())
}
// 使用自定义认证
opts = append(opts, grpc.WithPerRPCCredentials(new(customCredential)))
// 使用客户端拦截器
opts = append(opts, grpc.WithUnaryInterceptor(interceptor))
// 1.连接
conn, err := grpc.Dial(addr, opts...)
if err != nil {
grpclog.Fatalln(err)
}
defer conn.Close()
// 2. 实例化gRPC客户端
client := pb.NewUserInfoServiceClient(conn)
// 3.组装请求参数
req := new(pb.UserRequest)
req.Name = "zs"
// 4. 调用接口
response, err := client.GetUserInfo(context.Background(), req)
if err != nil {
grpclog.Fatalln(err)
}
fmt.Printf("响应结果: %v\n", response)
// 客户端调用HelloService
hClient := pb.NewHelloServiceClient(conn)
res1,err := hClient.SayHello(context.Background(), &pb.HelloRequest{
Name: "grpc",
})
if err != nil {
grpclog.Fatalln(err)
}
fmt.Println("响应结果:", res1)
}
运行结果
开启服务端:
API server listening at: 127.0.0.1:6334
监听端口:127.0.0.1:8180
运行客户端:
API server listening at: 127.0.0.1:6612
响应结果: id:1 name:"zs" age:22 hobby:"Sing" hobby:"Run"
响应结果: result:"hello grpc"
HTTP 请求(有token验证的,注意在请求头中配置metaData):
备注:HTTP方式的 metaData 数据 放在 header 中,使用
Grpc-Metadata-
前缀。
最后编辑:joker.liu 更新时间:2023-05-24 14:59