gRPC(八)生态 grpc-gateway 应用:同一个服务端支持Rpc和Restful Api

news2025/7/19 9:00:50

目录

  • 前言
  • 一、gRPC-Gateway概述
    • 1、简述
    • 2、出现
  • 二、准备工作
    • 1、目录结构
    • 2、环境准备
      • 1)Protobuf
      • 2)gRPC
      • 3)gRPC-Gateway
    • 3、编写 IDL
      • 1)google.api
      • 2)hello.proto
      • 3)编译proto
    • 4、制作证书
      • 1)生成CA根证书
      • 2)生成终端用户证书
  • 三、命令行模块 cmd
    • 1、Cobra介绍
    • 2、概念
    • 3、安装
    • 4、编写 server
    • 5、编写 cmd
    • 6、启动 & 请求
    • 7、目录结构
  • 四、服务端模块 server
    • 1、编写 hello.proto
    • 2、编写 grpc.go
    • 3、编写 tls.go
    • 4、重新编写核心文件 server/server.go
    • 5、server流程刨析
      • 1)启动监听
      • 2)获取TLSConfig
      • 3)创建内部服务
      • 4)创建Listener
      • 5)服务接受请求
  • 五、验证功能
    • 1、编写 client
    • 2、启动 & 请求
  • 六、最终目录结构

前言

个人网站:https://linzyblog.netlify.app/
示例代码已经上传到github:点击跳转
gRPC官方文档:点击跳转
grpc-gateway官方文档:点击跳转
源自 coreos 的一篇博客,转载到了 gRPC 官方博客 gRPC with REST and Open APIs。

一、gRPC-Gateway概述

1、简述

取自官方概述:
grpc-gateway is a plugin of protoc. It reads gRPC service definition, and generates a reverse-proxy server which translates a RESTful JSON API into gRPC. This server is generated according to custom options in your gRPC definition.

gRPC-Gateway 是 protoc 的插件。它读取gRPC服务定义并生成反向代理服务器,将 RESTful JSON API 转换为 gRPC。该服务器是根据服务定义中的 google.api.http 注释生成的。

2、出现

etcd v3 改用 gRPC 后为了兼容原来的 API,同时要提供 HTTP/JSON 方式的API,为了满足这个需求,要么开发两套 API,要么实现一种转换机制,所以grpc-gateway诞生了。

  • 通过protobuf的自定义option实现了一个网关,服务端同时开启gRPC和HTTP服务。
  • HTTP服务接收客户端请求后转换为grpc请求数据,获取响应后转为json数据返回给客户端。
  • 当 HTTP 请求到达 gRPC-Gateway 时,它将 JSON 数据解析为 Protobuf 消息。使用解析的 Protobuf 消息发出正常的 Go gRPC 客户端请求。
  • Go gRPC 客户端将 Protobuf 结构编码为 Protobuf 二进制格式,然后将其发送到 gRPC 服务器。
  • gRPC 服务器处理请求并以 Protobuf 二进制格式返回响应。
  • Go gRPC 客户端将其解析为 Protobuf 消息,并将其返回到 gRPC-Gateway,后者将 Protobuf 消息编码为 JSON 并将其返回给原始客户端。

架构如下

在这里插入图片描述

二、准备工作

由于本实践偏向 Grpc+Grpc Gateway的方面,我们的需求是同一个服务端支持Rpc和Restful Api,那么就意味着http2、TLS等等的应用,功能方面就是一个服务端能够接受来自grpc和Restful Api的请求并响应。

本文示例代码已经上传到github:点击跳转

1、目录结构

新建grpc-gateway-example文件夹,我们项目的初始目录目录如下:

grpc-gateway-example/
├── certs
├── client
├── cmd
├── pkg
├── proto
│   ├── google
│   │   └── api
│   │   │   └── annotations.proto
│   │   │   └── http.proto
│   │   └── protobuf
│   │   │   └── descriptor.proto
├── server
└── Makefile
  • certs:存放证书凭证
  • client:客户端
  • cmd:存放 cobra 命令模块
  • pkg:第三方公共模块
  • proto:protobuf的一些相关文件(含.proto、pb.go、.pb.gw.go),google/api中用于存放annotations.proto、http.proto、google/protobuf中用于存放descriptor.proto
    • 如果你生成Go代码的时候出现File not found,那一定就是找不到下面的文件。
    • annotations.proto和http.proto文件需要手动从 https://github.com/googleapis/googleapis/tree/master/google/api地址复制到自己的项目中或者手动复制代码!!
    • descriptor.proto文件可以直接复制代码!!
  • server:服务端
  • Makefile:用于存放编译的代码。

2、环境准备

1)Protobuf

详细的请移步到《gRPC(二)入门:Protobuf入门》

2)gRPC

详细的请移步到《gRPC(三)基础:gRPC快速入门》

3)gRPC-Gateway

gRPC-Gateway 只是一个插件,只需要安装一下就可以了。这里建议科学上网:

go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway

3、编写 IDL

1)google.api

proto 目录中有 google/api 目录,它用到了 google 官方提供的两个 api 描述文件,主要是针对 grpc-gateway 的 http 转换提供支持,定义了 Protocol Buffer 所扩展的 HTTP Option。

2)hello.proto

编写Demo的 .proto 文件,我们在 proto目录下新建 hello.proto 文件,写入文件内容:

syntax = "proto3";

package proto;
option go_package = "./proto/helloworld;helloworld";

import "proto/google/api/annotations.proto";

// 定义Hello服务
service Hello {
  // 定义SayHello方法
  rpc SayHello(HelloRequest) returns (HelloResponse) {
    // http option 网关
    option (google.api.http) = {
      post: "/hello_world"
      body: "*"
    };
  }
}

// HelloRequest 请求结构
message HelloRequest {
  string referer = 1;
}

// HelloResponse 响应结构
message HelloResponse {
  string message = 1;
}

在 hello.proto 文件中,引用了 google/api/annotations.proto ,达到支持HTTP Option的效果

  • 定义了一个 serviceRPC 服务 HelloWorld,在其内部定义了一个 HTTP Option 的POST方法,HTTP 响应路径为/hello_world
  • 定义message类型HelloWorldRequest、HelloWorldResponse,用于响应请求和返回结果。

每个方法都必须添加 google.api.http 注解后 gRPC-Gateway 才能生成对应 http 方法。
其中post为 HTTP Method,即 POST 方法,/hello_world 则是请求路径。

3)编译proto

在Makefile文件内输入以下内容:

protoc:
		protoc  --go_out=.  --go-grpc_out=. --grpc-gateway_out=. ./proto/*.proto
  • Go Plugins 用于生成 .pb.go 文件
  • gRPC Plugins 用于生成 _grpc.pb.go
  • gRPC-Gateway 则是 pb.gw.go

使用 make protoc 编译proto:

make protoc
protoc  --go_out=.  --go-grpc_out=. --grpc-gateway_out=. ./proto/*.proto

在这里插入图片描述

4、制作证书

详细的请移步到《gRPC(五)进阶:通过TLS建立安全连接》

在服务端支持Rpc和Restful Api,需要用到TLS,因此我们要先制作证书

进入certs目录,生成TLS所需的公钥密钥文件

1)生成CA根证书

ca.conf 文件并写入内容如下:

[ req ]
default_bits       = 4096
distinguished_name = req_distinguished_name

[ req_distinguished_name ]
countryName                 = GB
countryName_default         = CN
stateOrProvinceName         = State or Province Name (full name)
stateOrProvinceName_default = ZheJiang
localityName                = Locality Name (eg, city)
localityName_default        = HuZhou
organizationName            = Organization Name (eg, company)
organizationName_default    = Step
commonName                  = linzyblog.netlify.app
commonName_max              = 64
commonName_default          = linzyblog.netlify.app
  1. 生成ca私钥,得到ca.key
openssl genrsa -out ca.key 4096
  1. 生成ca证书签发请求,得到ca.csr
$ openssl req -new -sha256 -out ca.csr -key ca.key -config ca.conf
GB [CN]:
State or Province Name (full name) [ZheJiang]:
Locality Name (eg, city) [HuZhou]:
Organization Name (eg, company) [Step]:
linzyblog.netlify.app [linzyblog.netlify.app]:
  1. 生成ca根证书,得到ca.crt
openssl x509 -req -days 3650 -in ca.csr -signkey ca.key -out ca.crt

在这里插入图片描述

2)生成终端用户证书

server.conf 写入以下内容:

[ req ]
default_bits       = 2048
distinguished_name = req_distinguished_name

[ req_distinguished_name ]
countryName                 = Country Name (2 letter code)
countryName_default         = CN
stateOrProvinceName         = State or Province Name (full name)
stateOrProvinceName_default = ZheJiang
localityName                = Locality Name (eg, city)
localityName_default        = HuZhou
organizationName            = Organization Name (eg, company)
organizationName_default    = Step
commonName                  = CommonName (e.g. server FQDN or YOUR name)
commonName_max              = 64
commonName_default          = linzyblog.netlify.app

[ req_ext ]
subjectAltName = @alt_names

[alt_names]
DNS.1   = grpc-gateway-example
IP      = 127.0.0.1
  1. 生成私钥,得到server.key
openssl genrsa -out server.key 2048
  1. 生成证书签发请求,得到server.csr
openssl req -new -sha256 -out server.csr -key server.key -config server.conf

这里也一直回车就好。

  1. 用CA证书生成终端用户证书,得到server.crt
openssl x509 -req -days 3650 -CA ca.crt -CAkey ca.key -CAcreateserial -in server.csr -out server.pem -extensions req_ext -extfile server.conf

在这里插入图片描述

这样我们需要的证书凭证就足够了

三、命令行模块 cmd

1、Cobra介绍

官方文档:点击跳转

Cobra 是一个用于创建强大的现代 CLI 应用程序的库。它提供了一个简单的界面来创建强大的现代 CLI 界面,类似于 git 和 go 工具。

Cobra 提供:

  • 简易的子命令行模式
  • 完全兼容 POSIX 的命令行模式(包括短版和长版)
  • 嵌套的子命令
  • 全局、本地和级联flags
  • 使用Cobra很容易的生成应用程序和命令,使用 cobra create appnamecobra add cmdname
  • 提供智能提示
  • 自动生成commands和flags的帮助信息
  • 自动生成详细的 help 信息,如 app -help。
  • 自动识别帮助 flag、 -h,--help
  • 自动生成应用程序在 bash 下命令自动完成功能。
  • 自动生成应用程序的 man 手册。
  • 命令行别名。
  • 自定义 helpusage 信息。
  • 可选的与 viper 的紧密集成。

2、概念

Cobra 建立在命令(commands)、参数(arguments )、选项(flags)的结构之上。

  • commands:命令代表行为,一般表示 action,即运行的二进制命令服务。同时可以拥有子命令(children commands)
  • arguments:参数代表命令行参数。
  • flags:选项代表对命令行为的改变,即命令行选项。二进制命令的配置参数,可对应配置文件。参数可分为全局参数和子命令参数。

最好的命令行程序在实际使用时,就应该像在读一段优美的语句,能够更加直观的知道如何与用户进行交互。

执行命令行程序应该遵循一般的格式:

#appname command  arguments
docker pull alpine:latest

#appname command flag
docker ps -a

#appname command flag argument
git commit -m "linzy"

3、安装

使用 Cobra 很容易。首先,用于go get安装最新版本的库。

go get -u github.com/spf13/cobra@latest

4、编写 server

在编写 cmd 时需要先用 server 进行测试关联,因此这一步我们先写 server.go 用于测试

在 server 模块下 新建 server.go 文件,写入测试内容:

package server

import (
	"log"
)

var (
	ServerPort  string
	CertName    string
	CertPemPath string
	CertKeyPath string
)

func Serve() (err error) {
	log.Println(ServerPort)

	log.Println(CertName)

	log.Println(CertPemPath)

	log.Println(CertKeyPath)

	return nil
}

5、编写 cmd

在cmd模块下 新建 root.go 文件,写入内容:

package cmd

import (
	"fmt"
	"os"

	"github.com/spf13/cobra"
)

// rootCmd表示在没有任何子命令的情况下的基本命令
var rootCmd = &cobra.Command{
	// Command的用法,Use是一个行用法消息
	Use: "grpc",
	// Short是help命令输出中显示的简短描述
	Short: "Run the gRPC hello-world server",
}

func Execute() {
	if err := rootCmd.Execute(); err != nil {
		fmt.Println(err)
		os.Exit(-1)
	}
}

当前 cmd 目录下继续 新建 server.go 文件,写入内容:

package cmd

import (
	"github.com/spf13/cobra"
	"grpc-gateway-example/server"
	"log"
)

// 创建附加命令
// 本地标签:在本地分配一个标志,该标志仅适用于该特定命令。
var serverCmd = &cobra.Command{
	Use:   "server",
	Short: "Run the gRPC hello-world server",
	// 运行:典型的实际工作功能。大多数命令只会实现这一点;
	// 另外还有PreRun、PreRunE、PostRun、PostRunE等等不同时期的运行命令,但比较少用,具体使用时再查看亦可
	Run: func(cmd *cobra.Command, args []string) {
		defer func() {
			if err := recover(); err != nil {
				log.Println("Recover error : %v", err)
			}
		}()

		server.Serve()
	},
}

// 在 init() 函数中定义flags和处理配置。
func init() {
	// 我们定义了一个flag,值存储在&server.ServerPort中,长命令为--port,短命令为-p,,默认值为50052。
	// 命令的描述为server port。这一种调用方式成为Local Flags 本地标签
	serverCmd.Flags().StringVarP(&server.ServerPort, "port", "p", "50052", "server port")
	serverCmd.Flags().StringVarP(&server.CertPemPath, "cert-pem", "", "./certs/server.pem", "cert pem path")
	serverCmd.Flags().StringVarP(&server.CertKeyPath, "cert-key", "", "./certs/server.key", "cert key path")
	serverCmd.Flags().StringVarP(&server.CertName, "cert-name", "", "grpc-gateway-example", "server's hostname")

	// AddCommand向这父命令(rootCmd)添加一个或多个命令
	rootCmd.AddCommand(serverCmd)
}

6、启动 & 请求

我们在 grpc-gateway-example 目录下,新建文件main.go,写入内容:

package main

import "grpc-gateway-example/cmd"

func main() {
	cmd.Execute()
}

当前目录下执行·go run main.go server·,查看输出是否为(此时应为默认值):

$ go run main.go server
2022/11/10 12:08:22 50052
2022/11/10 12:08:22 grpc-gateway-example                       
2022/11/10 12:08:22 ./certs/server.pem                         
2022/11/10 12:08:22 ./certs/server.key                         

执行go run main.go server --port=8000 --cert-pem=test-pem --cert-key=test-key --cert-name=test-name,检验命令行参数是否正确:

$ go run main.go server --port=8000 --cert-pem=test-pem --cert-key=test-key --cert-name=test-name
2022/11/10 12:24:53 8000
2022/11/10 12:24:54 test-name
2022/11/10 12:24:54 test-pem 
2022/11/10 12:24:54 test-key

到这都无误,我们的 cmd 模块编写就正确了,下面开始我们的重点

7、目录结构

完成以上操作之后我们的目录是这样的结构,看看是不是缺少了:

grpc-gateway-example/
├── certs
├── client
├── cmd						// 命令行模块
│   ├── root.go
│   └── server.go
├── pkg
├── proto
│   ├── google
│   │   └── api
│   │   │   ├── annotations.proto
│   │   │   └── http.proto
│   │   └── protobuf
│   │   │   └── descriptor.proto
│   ├── helloworld
│   │   ├── hello.pb.go		// proto编译后文件
│   │   ├── hello.pb.gw.go	// gateway编译后文件
│   │   └── hello_grpc.pb.go// proto编译后接口文件
│   ├── hello.proto
├── server					// GRPC服务端
│   └── server.go
└── Makefile

四、服务端模块 server

1、编写 hello.proto

在server目录下新建文件 hello.go ,写入文件内容:

package server

import (
	"context"
	"grpc-gateway-example/proto/helloworld"
)

type helloService struct {
	helloworld.UnimplementedHelloServer
}

func NewHelloService() *helloService {
	return &helloService{}
}

// ctx context.Context用于接受上下文参数
// r *pb.HelloWorldRequest用于接受protobuf的Request参数
func (h helloService) SayHello(ctx context.Context, r *helloworld.HelloRequest) (*helloworld.HelloResponse, error) {
	return &helloworld.HelloResponse{
		Message: "hello grpc-gateway",
	}, nil
}

2、编写 grpc.go

在 pkg 下新建 util 目录,新建 grpc.go 文件,写入内容:

package util

import (
	"google.golang.org/grpc"
	"net/http"
	"strings"
)

// 将gRPC请求和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)
		}
	})
}

GrpcHandlerFunc 函数是用于判断请求是来源于 Rpc 客户端还是 Restful Api 的请求,根据不同的请求注册不同的 ServeHTTP 服务;r.ProtoMajor == 2 也代表着请求必须基于HTTP/2
简而言之函数将gRPC请求和HTTP请求分别调用不同的handler处理。

如果不需要 TLS 建立安全链接,则可以使用h2c

func GrpcHandlerFunc(grpcServer *grpc.Server, otherHandler http.Handler) http.Handler {
	return h2c.NewHandler(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)
		}
	}), &http2.Server{})
}

3、编写 tls.go

在pkg下的 util 目录下,新建 tls.go 文件,写入内容:

package util

import (
	"crypto/tls"
	"golang.org/x/net/http2"
	"io/ioutil"
	"log"
)

// 用于处理从证书凭证文件(PEM),最终获取tls.Config作为HTTP2的使用参数
func GetTLSConfig(certPemPath, certKeyPath string) *tls.Config {
	var certKeyPair *tls.Certificate
	cert, _ := ioutil.ReadFile(certPemPath)
	key, _ := ioutil.ReadFile(certKeyPath)

	// 从一对PEM编码的数据中解析公钥/私钥对。成功则返回公钥/私钥对
	pair, err := tls.X509KeyPair(cert, key)
	if err != nil {
		log.Println("TLS KeyPair err: %v\n", err)
	}

	certKeyPair = &pair

	return &tls.Config{
		// tls.Certificate:返回一个或多个证书,实质我们解析PEM调用的X509KeyPair的函数声明
		// 就是func X509KeyPair(certPEMBlock, keyPEMBlock []byte) (Certificate, error),返回值就是Certificate
		Certificates: []tls.Certificate{*certKeyPair},
		// http2.NextProtoTLS:NextProtoTLS是谈判期间的NPN/ALPN协议,用于HTTP/2的TLS设置
		NextProtos: []string{http2.NextProtoTLS},
	}
}

GetTLSConfig 函数是用于获取TLS配置,在内部,我们读取了 server.key 和 server.pem 这类证书凭证文件。经过一系列处理获取 tls.Config 作为 HTTP2 的使用参数。

4、重新编写核心文件 server/server.go

修改server目录下的server.go文件,该文件是我们服务里的核心文件,写入内容:

package server

import (
	"context"
	"crypto/tls"
	"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
	"grpc-gateway-example/pkg/util"
	"grpc-gateway-example/proto/helloworld"
	"log"
	"net"
	"net/http"
)

var (
	ServerPort  string
	CertName    string
	CertPemPath string
	CertKeyPath string
	EndPoint    string
)

func Serve() (err error) {
	EndPoint = ":" + ServerPort
	// 用于监听本地的网络地址通知
	// 它的函数原型func Listen(network, address string) (Listener, error)
	conn, err := net.Listen("tcp", EndPoint)
	if err != nil {
		log.Printf("TCP Listen err:%v\n", err)
	}

	// 通过util.GetTLSConfig解析得到tls.Config,传达给http.Server服务的TLSConfig配置项使用
	tlsConfig := util.GetTLSConfig(CertPemPath, CertKeyPath)
	srv := createInternalServer(conn, tlsConfig)

	log.Printf("gRPC and https listen on: %s\n", ServerPort)

	// NewListener将会创建一个Listener
	// 它接受两个参数,第一个是来自内部Listener的监听器,第二个参数是tls.Config(必须包含至少一个证书)
	if err = srv.Serve(tls.NewListener(conn, tlsConfig)); err != nil {
		log.Printf("ListenAndServe: %v\n", err)
	}

	return err
}

// 将认证的中间件注册进去, 前面所获取的tlsConfig仅能给HTTP使用
func createInternalServer(conn net.Listener, tlsConfig *tls.Config) *http.Server {
	var opts []grpc.ServerOption

	// 输入证书文件和服务器的密钥文件构造TLS证书凭证
	creds, err := credentials.NewServerTLSFromFile(CertPemPath, CertKeyPath)
	if err != nil {
		log.Printf("Failed to create server TLS credentials %v", err)
	}

	// grpc.Creds()其原型为func Creds(c credentials.TransportCredentials) ServerOption
	// 该函数返回 ServerOption,它为服务器连接设置凭据
	opts = append(opts, grpc.Creds(creds))

	// 创建了一个没有注册服务的grpc服务端
	grpcServer := grpc.NewServer(opts...)

	// 注册grpc服务
	helloworld.RegisterHelloServer(grpcServer, NewHelloService())

	// 创建 grpc-gateway 关联组件
	// context.Background()返回一个非空的空上下文。
	// 它没有被注销,没有值,没有过期时间。它通常由主函数、初始化和测试使用,并作为传入请求的顶级上下文
	ctx := context.Background()
	// 从客户端的输入证书文件构造TLS凭证
	dcreds, err := credentials.NewClientTLSFromFile(CertPemPath, CertName)
	if err != nil {
		log.Printf("Failed to create client TLS credentials %v", err)
	}
	// grpc.WithTransportCredentials 配置一个连接级别的安全凭据(例:TLS、SSL),返回值为type DialOption
	// grpc.DialOption DialOption选项配置我们如何设置连接(其内部具体由多个的DialOption组成,决定其设置连接的内容)
	dopts := []grpc.DialOption{grpc.WithTransportCredentials(dcreds)}

	// 创建HTTP NewServeMux及注册grpc-gateway逻辑
	// runtime.NewServeMux:返回一个新的ServeMux,它的内部映射是空的;
	// ServeMux是grpc-gateway的一个请求多路复用器。它将http请求与模式匹配,并调用相应的处理程序
	gwmux := runtime.NewServeMux()

	// RegisterHelloWorldHandlerFromEndpoint:注册HelloWorld服务的HTTP Handle到grpc端点
	if err := helloworld.RegisterHelloHandlerFromEndpoint(ctx, gwmux, EndPoint, dopts); err != nil {
		log.Printf("Failed to register gw server: %v\n", err)
	}

	// http服务
	// 分配并返回一个新的ServeMux
	mux := http.NewServeMux()
	// 为给定模式注册处理程序
	mux.Handle("/", gwmux)

	return &http.Server{
		Addr:      EndPoint,
		Handler:   util.GrpcHandlerFunc(grpcServer, mux),
		TLSConfig: tlsConfig,
	}
}

5、server流程刨析

1)启动监听

net.Listen("tcp", EndPoint) 函数用于监听本地网络地址的监听。其函数原型Listen(ctx context.Context, network, address string) (Listener, error)

参数:

  • network:必须是tcp, tcp4, tcp6, unix或unixpacket。
  • address:对于TCP网络,如果address参数中的host为空或未指定的IP地址,则会自动返回一个可用的端口或者IP地址。

net.Listen(“tcp”, EndPoint)函数返回值是Listener

type Listener interface {
	// 接受等待并将下一个连接返回给Listener
	Accept() (Conn, error)

	// 关闭Listener
	Close() error

	// 返回 Listener 的网络地址。
	Addr() Addr
}

net.Listen 会返回一个监听器的结构体,返回接下来的动作,让其执行下一步的操作,可用执行以下操作Accept、Close、Addr。

2)获取TLSConfig

通过调用 util.GetTLSConfig 函数解析得到 tls.Config,通过传达给 createInternalServer函数完成 http.Server 服务的 TLSConfig 配置项使用。

3)创建内部服务

程序采用HTTP2、HTTPS,需要支持TLS,在启动 grpc.NewServer() 前需要将serverOptions(服务器选项,类似于中间件,可用设置例如凭证、编解码器和保持存活参数等选项。),而前面所获取的 tlsConfig 仅能给HTTP使用,因此第一步我们要创建 grpc 的 TLS 认证凭证。

  1. 创建 grpc 的 TLS 认证凭证
    引用 google.golang.org/grpc/credentials 第三方包,credentials 包实现gRPC库支持的各种凭据,这些凭据封装了客户机与服务器进行身份验证所需的所有状态,并进行各种断言,例如,关于客户机的身份、角色或是否授权进行特定调用。

我们调用 NewServerTLSFromFile 它能够从服务器的输入证书文件和密钥文件构造TLS凭据。

func NewServerTLSFromFile(certFile, keyFile string) (TransportCredentials, error) {
	// LoadX509KeyPair从一对文件中读取并解析一个公私钥对。文件中必须包含PEM编码的数据。
	cert, err := tls.LoadX509KeyPair(certFile, keyFile)
	if err != nil {
		return nil, err
	}
	// NewTLS使用tls.Config来构建基于TLS的TransportCredentials(传输凭证)
	return NewTLS(&tls.Config{Certificates: []tls.Certificate{cert}}), nil
}
  1. grpc ServerOption

grpc.Creds() 其原型为func Creds(c credentials.TransportCredentials) ServerOption,返回一个为服务器连接设置凭据的ServerOption。

  1. 创建 grpc 服务端
    grpc.NewServer() 创建一个没有注册服务的grpc服务端,可以配置 ServerOption
  2. 注册grpc服务
// 注册grpc服务
helloworld.RegisterHelloServer(grpcServer, NewHelloService())
  1. 创建 grpc-gateway 关联组件
// context.Background()返回一个非空的空上下文。
// 它没有被注销,没有值,没有过期时间。它通常由主函数、初始化和测试使用,并作为传入请求的顶级上下文
ctx := context.Background()
// 从客户端的输入证书文件构造TLS凭证
dcreds, err := credentials.NewClientTLSFromFile(CertPemPath, CertName)
if err != nil {
	log.Printf("Failed to create client TLS credentials %v", err)
}
// grpc.WithTransportCredentials 配置一个连接级别的安全凭据(例:TLS、SSL),返回值为type DialOption
// grpc.DialOption DialOption选项配置我们如何设置连接(其内部具体由多个的DialOption组成,决定其设置连接的内容)
dopts := []grpc.DialOption{grpc.WithTransportCredentials(dcreds)}
  1. 创建HTTP NewServeMux及注册 grpc-gateway 逻辑
// 创建HTTP NewServeMux及注册grpc-gateway逻辑
// runtime.NewServeMux:返回一个新的ServeMux,它的内部映射是空的;
// ServeMux是grpc-gateway的一个请求多路复用器。它将http请求与模式匹配,并调用相应的处理程序
gwmux := runtime.NewServeMux()

// RegisterHelloWorldHandlerFromEndpoint:注册HelloWorld服务的HTTP Handle到grpc端点
if err := helloworld.RegisterHelloHandlerFromEndpoint(ctx, gwmux, EndPoint, dopts); err != nil {
	log.Printf("Failed to register gw server: %v\n", err)
}

// http服务
// 分配并返回一个新的ServeMux
mux := http.NewServeMux()
// 为给定模式注册处理程序
mux.Handle("/", gwmux)
  1. 注册具体服务
// RegisterHelloWorldHandlerFromEndpoint:注册HelloWorld服务的HTTP Handle到grpc端点
if err := helloworld.RegisterHelloHandlerFromEndpoint(ctx, gwmux, EndPoint, dopts); err != nil {
	log.Printf("Failed to register gw server: %v\n", err)
}
  • ctx:上下文
  • gwmux:grpc-gateway 的请求多路复用器
  • EndPoint:服务网络地址
  • dopts:配置好的安全凭据

4)创建Listener

// NewListener将会创建一个Listener
// 它接受两个参数,第一个是来自内部Listener的监听器,第二个参数是tls.Config(必须包含至少一个证书)
if err = srv.Serve(tls.NewListener(conn, tlsConfig)); err != nil {
	log.Printf("ListenAndServe: %v\n", err)
}

5)服务接受请求

我们调用 srv.Serve(tls.NewListener(conn, tlsConfig))它是http.Server的方法,并且需要一个Listener作为参数。

func (srv *Server) Serve(l net.Listener) error {
	...
    defer l.Close()
    ...
    baseCtx := context.Background()
    ...
    ctx := context.WithValue(baseCtx, ServerContextKey, srv)
    for {
        rw, e := l.Accept()
        ...
        c := srv.newConn(rw)
        c.setState(c.rwc, StateNew, runHooks) // before Serve can return
        go c.serve(ctx)
    }
}

它创建了一个 context.Background() 上下文对象,并调用 Listener 的 Accept 方法开始接受请求,在获取到连接数据后使用 newConn 创建连接对象,在最后使用goroutine的方式处理连接请求,完成请求后自动关闭连接。

五、验证功能

1、编写 client

在目录client下,创建 main.go 文件,新增以下内容:

package main

import (
	"golang.org/x/net/context"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
	"grpc-gateway-example/proto/helloworld"
	"log"
)

func main() {
	creds, err := credentials.NewClientTLSFromFile("./certs/server.pem", "grpc-gateway-example")
	if err != nil {
		log.Println("Failed to create TLS credentials %v", err)
		return
	}
	conn, err := grpc.Dial(":50052", grpc.WithTransportCredentials(creds))
	defer conn.Close()

	if err != nil {
		log.Println(err)
	}

	c := helloworld.NewHelloClient(conn)
	ct := context.Background()
	body := &helloworld.HelloRequest{
		Referer: "Grpc",
	}

	r, err := c.SayHello(ct, body)
	if err != nil {
		log.Println(err)
	}

	log.Println(r)
}

2、启动 & 请求

# 启动服务端
$ go run main.go server
2022/11/10 16:34:06 gRPC and https listen on: 50052

# 启动客户端
$ go run client/main.go
2022/11/10 16:34:43 message:"hello grpc-gateway"

执行测试Restful Api,用POST方式访问https://localhost:50052/hello_world
在这里插入图片描述

六、最终目录结构

grpc-gateway-example/
├── certs					//证书凭证
│   ├── ca.conf
│   ├── ca.crt
│   ├── ca.csr
│   ├── ca.key
│   ├── server.conf
│   ├── server.csr
│   ├── server.key
│   └── server.pem
├── client					// 客户端
│   └── main.go
├── cmd						// 命令行模块
│   ├── root.go
│   └── server.go
├── pkg						// 第三方公共模块
│   └── util
│   │   │   ├── grpc.go
│   │   │   └── tls.go
├── proto
│   ├── google
│   │   └── api
│   │   │   ├── annotations.proto
│   │   │   └── http.proto
│   │   └── protobuf
│   │   │   └── descriptor.proto
│   ├── helloworld
│   │   ├── hello.pb.go		// proto编译后文件
│   │   ├── hello.pb.gw.go	// gateway编译后文件
│   │   └── hello_grpc.pb.go// proto编译后接口文件
│   ├── hello.proto
├── server					// GRPC服务端
│   └── server.go
├── main.go
└── Makefile

参考:
https://developer.aliyun.com/article/879486
https://eddycjy.com/posts/go/grpc-gateway/2018-02-27-hello-world/

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/8903.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

香港服务器一定比美国服务器好吗?

香港服务器一定比美国服务器好吗?从出海业务兴起以来就有不少的站长拿较为热门的香港服务器和美国服务器来作出对比,对其两者孰优孰劣的探讨一直都没有停止过。在这里,我们对两者做个比较。 香港服务器和美国服务器两者的对比: 1. 香港服务器…

linux笔记(3):东山哪吒STU开发板初体验

文章目录1.开发板上电观察串口1.1 从nand flash启动1.2 从SD卡启动2.上传文件到开发板2.1 使用FileZilla软件连接开发板2.2 使用ADB软件双11下单后,经过多日的等待,终于在昨天下午收到了开发板。在等待的过程中,看了一下文档和B站东山老师的视…

Rhino Linux:滚动发布但也很稳定的 Ubuntu

导读滚动发布的 Ubuntu 发行版?等等,什么? Rhino Linux 听起来不错……Ubuntu but rolling but also stable! Thats what Rhino Linux aims to be Rhino Linux 将成为 Rolling Rhino Remix 的继任者。这个由 http.llamaz 构建的 Linux 发行版…

01 Jenkins CICD 之 Git 命令使用

1. 前言 由于项目没有外网,需要在内网打镜像。但自己对git 还不是太熟悉。看着pipline 一脸的懵。所有针对git 命令在工作中常用的参数及用法简单学习记录下 git 技能树链接 2. git 常用参数 2.1 git全局设置 git config --global user.name "xxxxx" …

STC51单片机27——控制无刷电机

编写程序控制电调&#xff1a; #include<reg52.h> sbit PpmP2^0; sbit UpP2^1; sbit DownP2^2; sbit Led_UpP2^3; sbit Led_DownP2^4; unsigned char k0; unsigned char a0; void Control() { if(Up0&am…

云原生网关的可观测性体系实践

作者&#xff1a;井轶 概述 可观测性一词来源于控制理论&#xff0c;是指系统可以由其外部输出推断其其内部状态的程度&#xff0c;随着 IT 行业几十年的发展&#xff0c;IT 系统的监控、告警、问题排查等领域的逐渐成熟&#xff0c;IT 行业也将其抽象形成了一整套可观测性工…

Linux基础学习记录

LInux学习 文章目录LInux学习1. Linux快捷键2. Shell基本命令”*“&#xff0c;”&#xff1f;“&#xff0c;”[]“&#xff1a;通配符pwd&#xff1a;显示当前目录cd&#xff1a;改变目录ls&#xff1a;列出目录内容cat和more&#xff1a;查看文本文件catmorehead和tail&…

为什么C语言需要指定平台开发?

前言&#xff1a; 笔者心血来潮&#xff0c;特意站在初学者角度去思考为什么C语言需要指定平台去开发呢&#xff1f; 物有本末&#xff0c;事有终始&#xff0c;知其先后&#xff0c;则近道矣。 语言历史&#xff1a; 读者应该能明白程序最底层无非就是01010101二进制被CPU给调…

(续)SSM整合之springmvc笔记(SpringMVC获取请求参数)(P131-135)

目录 一 通过ServletAPI获取 1 . 新建TestParamController类 2 . index.html 3 . 在TestParamController类里面写getParamByServletAPI 4. 测试 重新部暑 二 通过控制器方法的形参获取请求参数 1 . index.html 2 . TestParamController 3. 测试 三 RequestPar…

Vue2.0开发之——Vue基础用法-事件绑定$event(20)

一 概述 事件参数对象$event表示事件参数对象event事件修饰符 二 事件参数对象 2.1 说明 在原生的 DOM 事件绑定中&#xff0c;可以在事件处理函数的形参处&#xff0c;接收事件参数对象 event 2.2 示例 布局代码 <button v-on:click"addCount">1</bu…

高光谱解混和图片去噪(Matlab代码实现)

&#x1f468;‍&#x1f393;个人主页&#xff1a;研学社的博客 &#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜…

使用id限定优化mysql分页查询limit偏移量大问题

在工作中可能偶尔会遇到&#xff0c;当使用limit实现分页查询时&#xff0c;当limit的偏移量越大时&#xff0c;sql语句的耗时也越大。 如图&#xff1a; 偏移量为0时&#xff0c;sql语句耗时在35毫秒。 顺便说下偏移量与页码、页大小的关系&#xff1a; 偏移量 (页码 - 1…

Spark的内存管理机制

在执行Spark 的应用程序时&#xff0c;Spark 集群会启动 Driver 和 Executor 两种 JVM 进程&#xff0c;前者为主控进程&#xff0c;负责创建 Spark 上下文&#xff0c;提交 Spark 作业&#xff08;Job&#xff09;&#xff0c;并将作业转化为计算任务&#xff08;Task&#xf…

深度学习基础--神经网络(1)激活函数

文章目录从感知机到神经网络激活函数阶跃函数&#xff08;感知机的激活函数&#xff09;sigmoid函数阶跃函数和sigmoid函数绘制和对比ReLU函数本文为学习笔记参考书籍&#xff1a;《深度学习入门 : 基于Python的理论与实现 》/ (日) 斋藤康毅著 ; 陆宇杰译. – 北京 : 人民邮电…

根据水声和摄影测量数据建立数字测深模型

无人船和无人车正越来越多地用于水深地形测量。使用这些平台采集数据的技术得到普遍的应用&#xff0c;但数据的融合仍然需要深入研究&#xff0c;其融合方法通常依赖于所使用的传感器和测量区域的特性。本文提出了一种融合无人艇&#xff08;USV&#xff09;和无人机&#xff…

[附源码]java毕业设计基于的前端课程学习网站

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

Mybatis—SqlNode

SqlNode的主要职责就是描述Mapper文件中配置的SQL信息&#xff0c;在Mybatis源代码中随处都能看见其身影&#xff0c;可见SqlNode地位是相当高的。SqlNode接口只定义了一个apply方法&#xff0c;且该方法只有一个DynamicContext对象作为入参。DynamicContext对象中不仅封装了Ma…

抓包工具 Charles 使用手册

Charles 是一款抓包软件&#xff0c;通过代理的形式拦截所有的 HTTP 和 HTTPS 请求&#xff0c;是开发测试的一大利器 下载和激活 在 Charles 下载安装包&#xff0c;在 此处 获得注册码&#xff0c;点击下方输入注册码激活软件 抓主机的 HTTP 包 选中 Proxy > Windows P…

第五章 数组和广义表

数组和广义表 5.1多维数组 5.1.1数组的逻辑结构 数组是我们熟悉的一种数据结构&#xff0c;可以看作线性表的推广。 数组作为一种数据结构其特点是结构中的元素本身可以是具有某种结构的数据&#xff0c;但属于同一类型。比如&#xff1a;一维数组可以看作一个线性表&#…

NDepend v2022.2.1.9665 专业版

NDepend 基本上被描述为.NET 托管代码的静态分析工具。这个特定的工具能够支持大量代码度量&#xff0c;允许使用直接图和依赖矩阵来可视化依赖关系。 NDepend 工具还能够通过架构验证过程以及规则及其质量为用户和开发人员执行基于代码的快照比较。有些规则基本上是用户定义的…