• Hello World
    • 需求
    • 一、初始化目录
    • 二、制作证书
      • 私钥
      • 自签名公钥
      • 填写信息
    • 三、proto
      • 编写
      • 编译
    • 四、命令行模块 cmd
      • 介绍
      • 编写server
      • 编写cmd
      • 讲解
      • 测试
    • 五、服务端模块 server
      • 编写hello.go
      • *编写server.go
        • server流程剖析
          • 一、启动监听
          • 二、获取TLS
          • 三、创建内部服务
          • 四、创建tls.NewListener
          • 五、服务开始接受请求
    • 六、验证功能
      • 编写测试客户端
      • 启动服务端
      • 执行测试客户端
      • 执行测试Restful Api
    • 最终目录结构
    • 参考
      • 示例代码

    Hello World

    在上一节中我们已经完成了对环境的基本配置

    这节将开始编写一个复杂的Hello World,涉及到许多的知识,建议大家认真思考其中的概念

    需求

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

    一、初始化目录

    我们先在$GOPATH中新建grpc-hello-world文件夹,我们项目的初始目录目录如下:

    1. grpc-hello-world/
    2. ├── certs
    3. ├── client
    4. ├── cmd
    5. ├── pkg
    6. ├── proto
    7. ├── google
    8. └── api
    9. └── server
    • certs:证书凭证
    • client:客户端
    • cmd:命令行
    • pkg:第三方公共模块
    • protoprotobuf的一些相关文件(含.protopb.go.pb.gw.go),google/api中用于存放annotations.protohttp.proto
    • server:服务端

    二、制作证书

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

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

    私钥

    1. openssl genrsa -out server.key 2048
    2. openssl ecparam -genkey -name secp384r1 -out server.key
    • openssl genrsa:生成RSA私钥,命令的最后一个参数,将指定生成密钥的位数,如果没有指定,默认512
    • openssl ecparam:生成ECC私钥,命令为椭圆曲线密钥参数生成及操作,本文中ECC曲线选择的是secp384r1

    自签名公钥

    1. openssl req -new -x509 -sha256 -key server.key -out server.pem -days 3650
    • openssl req:生成自签名证书,-new指生成证书请求、-sha256指使用sha256加密、-key指定私钥文件、-x509指输出证书、-days 3650为有效期,此后则输入证书拥有者信息

    填写信息

    1. Country Name (2 letter code) [XX]:
    2. State or Province Name (full name) []:
    3. Locality Name (eg, city) [Default City]:
    4. Organization Name (eg, company) [Default Company Ltd]:
    5. Organizational Unit Name (eg, section) []:
    6. Common Name (eg, your name or your server's hostname) []:grpc server name
    7. Email Address []:

    三、proto

    编写

    1、 google.api

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

    annotations.proto文件:

    1. // Copyright (c) 2015, Google Inc.
    2. //
    3. // Licensed under the Apache License, Version 2.0 (the "License");
    4. // you may not use this file except in compliance with the License.
    5. // You may obtain a copy of the License at
    6. //
    7. // http://www.apache.org/licenses/LICENSE-2.0
    8. //
    9. // Unless required by applicable law or agreed to in writing, software
    10. // distributed under the License is distributed on an "AS IS" BASIS,
    11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12. // See the License for the specific language governing permissions and
    13. // limitations under the License.
    14. syntax = "proto3";
    15. package google.api;
    16. import "google/api/http.proto";
    17. import "google/protobuf/descriptor.proto";
    18. option java_multiple_files = true;
    19. option java_outer_classname = "AnnotationsProto";
    20. option java_package = "com.google.api";
    21. extend google.protobuf.MethodOptions {
    22. // See `HttpRule`.
    23. HttpRule http = 72295728;
    24. }

    http.proto文件:

    1. // Copyright 2016 Google Inc.
    2. //
    3. // Licensed under the Apache License, Version 2.0 (the "License");
    4. // you may not use this file except in compliance with the License.
    5. // You may obtain a copy of the License at
    6. //
    7. // http://www.apache.org/licenses/LICENSE-2.0
    8. //
    9. // Unless required by applicable law or agreed to in writing, software
    10. // distributed under the License is distributed on an "AS IS" BASIS,
    11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12. // See the License for the specific language governing permissions and
    13. // limitations under the License.
    14. syntax = "proto3";
    15. package google.api;
    16. option cc_enable_arenas = true;
    17. option java_multiple_files = true;
    18. option java_outer_classname = "HttpProto";
    19. option java_package = "com.google.api";
    20. // Defines the HTTP configuration for a service. It contains a list of
    21. // [HttpRule][google.api.HttpRule], each specifying the mapping of an RPC method
    22. // to one or more HTTP REST API methods.
    23. message Http {
    24. // A list of HTTP rules for configuring the HTTP REST API methods.
    25. repeated HttpRule rules = 1;
    26. }
    27. // Use CustomHttpPattern to specify any HTTP method that is not included in the
    28. // `pattern` field, such as HEAD, or "*" to leave the HTTP method unspecified for
    29. // a given URL path rule. The wild-card rule is useful for services that provide
    30. // content to Web (HTML) clients.
    31. message HttpRule {
    32. // Selects methods to which this rule applies.
    33. //
    34. // Refer to [selector][google.api.DocumentationRule.selector] for syntax details.
    35. string selector = 1;
    36. // Determines the URL pattern is matched by this rules. This pattern can be
    37. // used with any of the {get|put|post|delete|patch} methods. A custom method
    38. // can be defined using the 'custom' field.
    39. oneof pattern {
    40. // Used for listing and getting information about resources.
    41. string get = 2;
    42. // Used for updating a resource.
    43. string put = 3;
    44. // Used for creating a resource.
    45. string post = 4;
    46. // Used for deleting a resource.
    47. string delete = 5;
    48. // Used for updating a resource.
    49. string patch = 6;
    50. // Custom pattern is used for defining custom verbs.
    51. CustomHttpPattern custom = 8;
    52. }
    53. // The name of the request field whose value is mapped to the HTTP body, or
    54. // `*` for mapping all fields not captured by the path pattern to the HTTP
    55. // body. NOTE: the referred field must not be a repeated field.
    56. string body = 7;
    57. // Additional HTTP bindings for the selector. Nested bindings must
    58. // not contain an `additional_bindings` field themselves (that is,
    59. // the nesting may only be one level deep).
    60. repeated HttpRule additional_bindings = 11;
    61. }
    62. // A custom pattern is used for defining custom HTTP verb.
    63. message CustomHttpPattern {
    64. // The name of this custom HTTP verb.
    65. string kind = 1;
    66. // The path matched by this custom verb.
    67. string path = 2;
    68. }
    1. hello.proto

    这一小节将编写Demo.proto文件,我们在proto目录下新建hello.proto文件,写入文件内容:

    1. syntax = "proto3";
    2. package proto;
    3. import "google/api/annotations.proto";
    4. service HelloWorld {
    5. rpc SayHelloWorld(HelloWorldRequest) returns (HelloWorldResponse) {
    6. option (google.api.http) = {
    7. post: "/hello_world"
    8. body: "*"
    9. };
    10. }
    11. }
    12. message HelloWorldRequest {
    13. string referer = 1;
    14. }
    15. message HelloWorldResponse {
    16. string message = 1;
    17. }

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

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

    编译

    在编写完.proto文件后,我们需要对其进行编译,就能够在server中使用

    进入proto目录,执行以下命令

    1. # 编译google.api
    2. protoc -I . --go_out=plugins=grpc,Mgoogle/protobuf/descriptor.proto=github.com/golang/protobuf/protoc-gen-go/descriptor:. google/api/*.proto
    3. #编译hello_http.proto为hello_http.pb.proto
    4. protoc -I . --go_out=plugins=grpc,Mgoogle/api/annotations.proto=grpc-hello-world/proto/google/api:. ./hello.proto
    5. #编译hello_http.proto为hello_http.pb.gw.proto
    6. protoc --grpc-gateway_out=logtostderr=true:. ./hello.proto

    执行完毕后将生成hello.pb.gohello.gw.pb.go,分别针对grpcgrpc-gateway的功能支持

    四、命令行模块 cmd

    介绍

    这一小节我们编写命令行模块,为什么要独立出来呢,是为了将cmdserver两者解耦,避免混淆在一起。

    我们采用 Cobra 来完成这项功能,Cobra既是创建强大的现代CLI应用程序的库,也是生成应用程序和命令文件的程序。提供了以下功能:

    • 简易的子命令行模式
    • 完全兼容posix的命令行模式(包括短和长版本)
    • 嵌套的子命令
    • 全局、本地和级联flags
    • 使用Cobra很容易的生成应用程序和命令,使用cobra create appnamecobra add cmdname
    • 智能提示
    • 自动生成commands和flags的帮助信息
    • 自动生成详细的help信息-h--help等等
    • 自动生成的bash自动完成功能
    • 为应用程序自动生成手册
    • 命令别名
    • 定义您自己的帮助、用法等的灵活性。
    • 可选与viper紧密集成的apps

    编写server

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

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

    1. package server
    2. import (
    3. "log"
    4. )
    5. var (
    6. ServerPort string
    7. CertName string
    8. CertPemPath string
    9. CertKeyPath string
    10. )
    11. func Serve() (err error){
    12. log.Println(ServerPort)
    13. log.Println(CertName)
    14. log.Println(CertPemPath)
    15. log.Println(CertKeyPath)
    16. return nil
    17. }

    编写cmd

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

    1. package cmd
    2. import (
    3. "fmt"
    4. "os"
    5. "github.com/spf13/cobra"
    6. )
    7. var rootCmd = &cobra.Command{
    8. Use: "grpc",
    9. Short: "Run the gRPC hello-world server",
    10. }
    11. func Execute() {
    12. if err := rootCmd.Execute(); err != nil {
    13. fmt.Println(err)
    14. os.Exit(-1)
    15. }
    16. }

    新建server.go文件,写入内容:

    1. package cmd
    2. import (
    3. "log"
    4. "github.com/spf13/cobra"
    5. "grpc-hello-world/server"
    6. )
    7. var serverCmd = &cobra.Command{
    8. Use: "server",
    9. Short: "Run the gRPC hello-world server",
    10. Run: func(cmd *cobra.Command, args []string) {
    11. defer func() {
    12. if err := recover(); err != nil {
    13. log.Println("Recover error : %v", err)
    14. }
    15. }()
    16. server.Serve()
    17. },
    18. }
    19. func init() {
    20. serverCmd.Flags().StringVarP(&server.ServerPort, "port", "p", "50052", "server port")
    21. serverCmd.Flags().StringVarP(&server.CertPemPath, "cert-pem", "", "./certs/server.pem", "cert pem path")
    22. serverCmd.Flags().StringVarP(&server.CertKeyPath, "cert-key", "", "./certs/server.key", "cert key path")
    23. serverCmd.Flags().StringVarP(&server.CertName, "cert-name", "", "grpc server name", "server's hostname")
    24. rootCmd.AddCommand(serverCmd)
    25. }

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

    1. package main
    2. import (
    3. "grpc-hello-world/cmd"
    4. )
    5. func main() {
    6. cmd.Execute()
    7. }

    讲解

    要使用Cobra,按照Cobra标准要创建main.go和一个rootCmd文件,另外我们有子命令server

    1、rootCmd
    rootCmd表示在没有任何子命令的情况下的基本命令

    2、&cobra.Command

    • UseCommand的用法,Use是一个行用法消息
    • ShortShorthelp命令输出中显示的简短描述
    • Run:运行:典型的实际工作功能。大多数命令只会实现这一点;另外还有PreRunPreRunEPostRunPostRunE等等不同时期的运行命令,但比较少用,具体使用时再查看亦可

    3、rootCmd.AddCommandAddCommand向这父命令(rootCmd)添加一个或多个命令

    4、serverCmd.Flags().StringVarP()

    一般来说,我们需要在init()函数中定义flags和处理配置,以serverCmd.Flags().StringVarP(&server.ServerPort, "port", "p", "50052", "server port")为例,我们定义了一个flag,值存储在&server.ServerPort中,长命令为--port,短命令为-p,,默认值为50052,命令的描述为server port。这一种调用方式成为Local Flags

    我们延伸一下,如果觉得每一个子命令都要设一遍觉得很麻烦,我们可以采用Persistent Flags

    rootCmd.PersistentFlags().BoolVarP(&Verbose, "verbose", "v", false, "verbose output")

    作用:

    flag是可以持久的,这意味着该flag将被分配给它所分配的命令以及该命令下的每个命令。对于全局标记,将标记作为根上的持久标志。

    另外还有Local Flag on Parent CommandsBind Flags with ConfigRequired flags等等,使用到再 传送 了解即可

    测试

    回到grpc-hello-world/目录下执行go run main.go server,查看输出是否为(此时应为默认值):

    1. 2018/02/25 23:23:21 50052
    2. 2018/02/25 23:23:21 dev
    3. 2018/02/25 23:23:21 ./certs/server.pem
    4. 2018/02/25 23:23:21 ./certs/server.key

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

    1. 2018/02/25 23:24:56 8000
    2. 2018/02/25 23:24:56 test-name
    3. 2018/02/25 23:24:56 test-pem
    4. 2018/02/25 23:24:56 test-key

    若都无误,那么恭喜你cmd模块的编写正确了,下一部分开始我们的重点章节!

    五、服务端模块 server

    编写hello.go

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

    1. package server
    2. import (
    3. "golang.org/x/net/context"
    4. pb "grpc-hello-world/proto"
    5. )
    6. type helloService struct{}
    7. func NewHelloService() *helloService {
    8. return &helloService{}
    9. }
    10. func (h helloService) SayHelloWorld(ctx context.Context, r *pb.HelloWorldRequest) (*pb.HelloWorldResponse, error) {
    11. return &pb.HelloWorldResponse{
    12. Message : "test",
    13. }, nil
    14. }

    我们创建了helloService及其方法SayHelloWorld,对应.protorpc SayHelloWorld,这个方法需要有2个参数:ctx context.Context用于接受上下文参数、r *pb.HelloWorldRequest用于接受protobufRequest参数(对应.protomessage HelloWorldRequest

    *编写server.go

    这一小章节,我们编写最为重要的服务端程序部分,涉及到大量的grpcgrpc-gateway及一些网络知识的应用

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

    1. package util
    2. import (
    3. "net/http"
    4. "strings"
    5. "google.golang.org/grpc"
    6. )
    7. func GrpcHandlerFunc(grpcServer *grpc.Server, otherHandler http.Handler) http.Handler {
    8. if otherHandler == nil {
    9. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    10. grpcServer.ServeHTTP(w, r)
    11. })
    12. }
    13. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    14. if r.ProtoMajor == 2 && strings.Contains(r.Header.Get("Content-Type"), "application/grpc") {
    15. grpcServer.ServeHTTP(w, r)
    16. } else {
    17. otherHandler.ServeHTTP(w, r)
    18. }
    19. })
    20. }

    GrpcHandlerFunc函数是用于判断请求是来源于Rpc客户端还是Restful Api的请求,根据不同的请求注册不同的ServeHTTP服务;r.ProtoMajor == 2也代表着请求必须基于HTTP/2

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

    1. package util
    2. import (
    3. "crypto/tls"
    4. "io/ioutil"
    5. "log"
    6. "golang.org/x/net/http2"
    7. )
    8. func GetTLSConfig(certPemPath, certKeyPath string) *tls.Config {
    9. var certKeyPair *tls.Certificate
    10. cert, _ := ioutil.ReadFile(certPemPath)
    11. key, _ := ioutil.ReadFile(certKeyPath)
    12. pair, err := tls.X509KeyPair(cert, key)
    13. if err != nil {
    14. log.Println("TLS KeyPair err: %v\n", err)
    15. }
    16. certKeyPair = &pair
    17. return &tls.Config{
    18. Certificates: []tls.Certificate{*certKeyPair},
    19. NextProtos: []string{http2.NextProtoTLS},
    20. }
    21. }

    GetTLSConfig函数是用于获取TLS配置,在内部,我们读取了server.keyserver.pem这类证书凭证文件

    • tls.X509KeyPair:从一对PEM编码的数据中解析公钥/私钥对。成功则返回公钥/私钥对
    • http2.NextProtoTLSNextProtoTLS是谈判期间的NPN/ALPN协议,用于HTTP/2的TLS设置
    • tls.Certificate:返回一个或多个证书,实质我们解析PEM调用的X509KeyPair的函数声明就是func X509KeyPair(certPEMBlock, keyPEMBlock []byte) (Certificate, error),返回值就是Certificate

    总的来说该函数是用于处理从证书凭证文件(PEM),最终获取tls.Config作为HTTP2的使用参数

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

    1. package server
    2. import (
    3. "crypto/tls"
    4. "net"
    5. "net/http"
    6. "log"
    7. "golang.org/x/net/context"
    8. "google.golang.org/grpc"
    9. "google.golang.org/grpc/credentials"
    10. "github.com/grpc-ecosystem/grpc-gateway/runtime"
    11. pb "grpc-hello-world/proto"
    12. "grpc-hello-world/pkg/util"
    13. )
    14. var (
    15. ServerPort string
    16. CertName string
    17. CertPemPath string
    18. CertKeyPath string
    19. EndPoint string
    20. )
    21. func Serve() (err error){
    22. EndPoint = ":" + ServerPort
    23. conn, err := net.Listen("tcp", EndPoint)
    24. if err != nil {
    25. log.Printf("TCP Listen err:%v\n", err)
    26. }
    27. tlsConfig := util.GetTLSConfig(CertPemPath, CertKeyPath)
    28. srv := createInternalServer(conn, tlsConfig)
    29. log.Printf("gRPC and https listen on: %s\n", ServerPort)
    30. if err = srv.Serve(tls.NewListener(conn, tlsConfig)); err != nil {
    31. log.Printf("ListenAndServe: %v\n", err)
    32. }
    33. return err
    34. }
    35. func createInternalServer(conn net.Listener, tlsConfig *tls.Config) (*http.Server) {
    36. var opts []grpc.ServerOption
    37. // grpc server
    38. creds, err := credentials.NewServerTLSFromFile(CertPemPath, CertKeyPath)
    39. if err != nil {
    40. log.Printf("Failed to create server TLS credentials %v", err)
    41. }
    42. opts = append(opts, grpc.Creds(creds))
    43. grpcServer := grpc.NewServer(opts...)
    44. // register grpc pb
    45. pb.RegisterHelloWorldServer(grpcServer, NewHelloService())
    46. // gw server
    47. ctx := context.Background()
    48. dcreds, err := credentials.NewClientTLSFromFile(CertPemPath, CertName)
    49. if err != nil {
    50. log.Printf("Failed to create client TLS credentials %v", err)
    51. }
    52. dopts := []grpc.DialOption{grpc.WithTransportCredentials(dcreds)}
    53. gwmux := runtime.NewServeMux()
    54. // register grpc-gateway pb
    55. if err := pb.RegisterHelloWorldHandlerFromEndpoint(ctx, gwmux, EndPoint, dopts); err != nil {
    56. log.Printf("Failed to register gw server: %v\n", err)
    57. }
    58. // http服务
    59. mux := http.NewServeMux()
    60. mux.Handle("/", gwmux)
    61. return &http.Server{
    62. Addr: EndPoint,
    63. Handler: util.GrpcHandlerFunc(grpcServer, mux),
    64. TLSConfig: tlsConfig,
    65. }
    66. }

    server流程剖析

    我们将这一大块代码,分成以下几个部分来理解

    一、启动监听

    net.Listen("tcp", EndPoint)用于监听本地的网络地址通知,它的函数原型func Listen(network, address string) (Listener, error)

    参数:network必须传入tcptcp4tcp6unixunixpacket,若address为空或为0则会自动选择一个端口号
    返回值:通过查看源码我们可以得知其返回值为Listener,结构体原型:

    1. type Listener interface {
    2. Accept() (Conn, error)
    3. Close() error
    4. Addr() Addr
    5. }

    通过分析得知,最后net.Listen会返回一个监听器的结构体,返回给接下来的动作,让其执行下一步的操作,它可以执行三类操作

    • Accept:接受等待并将下一个连接返回给Listener
    • Close:关闭Listener
    • Addr:返回Listener的网络地址
    二、获取TLS

    通过util.GetTLSConfig解析得到tls.Config,传达给http.Server服务的TLSConfig配置项使用

    三、创建内部服务

    createInternalServer函数,是整个服务端的核心流转部分

    程序采用的是HTT2HTTPS也就是需要支持TLS,因此在启动grpc.NewServer前,我们要将认证的中间件注册进去

    而前面所获取的tlsConfig仅能给HTTP使用,因此第一步我们要创建grpcTLS认证凭证

    1、创建grpcTLS认证凭证

    新增引用google.golang.org/grpc/credentials的第三方包,它实现了grpc库支持的各种凭证,该凭证封装了客户机需要的所有状态,以便与服务器进行身份验证并进行各种断言,例如关于客户机的身份,角色或是否授权进行特定的呼叫

    我们调用NewServerTLSFromFile来达到我们的目的,它能够从输入证书文件和服务器的密钥文件构造TLS证书凭证

    1. func NewServerTLSFromFile(certFile, keyFile string) (TransportCredentials, error) {
    2. //LoadX509KeyPair读取并解析来自一对文件的公钥/私钥对
    3. cert, err := tls.LoadX509KeyPair(certFile, keyFile)
    4. if err != nil {
    5. return nil, err
    6. }
    7. //NewTLS使用tls.Config来构建基于TLS的TransportCredentials
    8. return NewTLS(&tls.Config{Certificates: []tls.Certificate{cert}}), nil
    9. }

    2、设置grpc ServerOption

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

    3、创建grpc服务端

    函数原型:

    1. func NewServer(opt ...ServerOption) *Server

    我们在此处创建了一个没有注册服务的grpc服务端,还没有开始接受请求

    1. grpcServer := grpc.NewServer(opts...)

    4、注册grpc服务

    1. pb.RegisterHelloWorldServer(grpcServer, NewHelloService())

    5、创建grpc-gateway关联组件

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

    6、创建HTTP NewServeMux及注册grpc-gateway逻辑

    1. gwmux := runtime.NewServeMux()
    2. // register grpc-gateway pb
    3. if err := pb.RegisterHelloWorldHandlerFromEndpoint(ctx, gwmux, EndPoint, dopts); err != nil {
    4. log.Println("Failed to register gw server: %v\n", err)
    5. }
    6. // http服务
    7. mux := http.NewServeMux()
    8. mux.Handle("/", gwmux)
    • runtime.NewServeMux:返回一个新的ServeMux,它的内部映射是空的;ServeMuxgrpc-gateway的一个请求多路复用器。它将http请求与模式匹配,并调用相应的处理程序
    • RegisterHelloWorldHandlerFromEndpoint:如函数名,注册HelloWorld服务的HTTP Handlegrpc端点
    • http.NewServeMux分配并返回一个新的ServeMux
    • mux.Handle:为给定模式注册处理程序

    (带着疑问去看程序)为什么gwmux可以放入mux.Handle中?

    首先我们看看它们的原型是怎么样的

    (1)http.NewServeMux()

    1. func NewServeMux() *ServeMux {
    2. return new(ServeMux)
    3. }
    1. type Handler interface {
    2. ServeHTTP(ResponseWriter, *Request)
    3. }

    (2)runtime.NewServeMux

    1. func NewServeMux(opts ...ServeMuxOption) *ServeMux {
    2. serveMux := &ServeMux{
    3. handlers: make(map[string][]handler),
    4. forwardResponseOptions: make([]func(context.Context, http.ResponseWriter, proto.Message) error, 0),
    5. marshalers: makeMarshalerMIMERegistry(),
    6. }
    7. ...
    8. return serveMux
    9. }

    (3)http.NewServeMux()Handle方法

    1. func (mux *ServeMux) Handle(pattern string, handler Handler)

    通过分析可得知,两者NewServeMux都是最终返回serveMuxHandler中导出的方法仅有ServeHTTP,功能是用于响应HTTP请求

    我们回到Handle interface中,可以得出结论就是任何结构体,只要实现了ServeHTTP方法,这个结构就可以称为HandleServeMux会使用该Handler调用ServeHTTP方法处理请求,这也就是自定义Handler

    而我们这里正是将grpc-gateway中注册好的HTTP Handler无缝的植入到net/httpHandle方法中

    补充:在go中任何结构体只要实现了与接口相同的方法,就等同于实现了接口

    7、注册具体服务

    1. if err := pb.RegisterHelloWorldHandlerFromEndpoint(ctx, gwmux, EndPoint, dopts); err != nil {
    2. log.Println("Failed to register gw server: %v\n", err)
    3. }

    在这段代码中,我们利用了前几小节的

    • 上下文
    • gateway-grpc的请求多路复用器
    • 服务网络地址
    • 配置好的安全凭据

    注册了HelloWorld这一个服务

    四、创建tls.NewListener
    1. func NewListener(inner net.Listener, config *Config) net.Listener {
    2. l := new(listener)
    3. l.Listener = inner
    4. l.config = config
    5. return l
    6. }

    NewListener将会创建一个Listener,它接受两个参数,第一个是来自内部Listener的监听器,第二个参数是tls.Config(必须包含至少一个证书)

    五、服务开始接受请求

    在最后我们调用srv.Serve(tls.NewListener(conn, tlsConfig)),可以得知它是http.Server的方法,并且需要一个Listener作为参数,那么Serve内部做了些什么事呢?

    1. func (srv *Server) Serve(l net.Listener) error {
    2. defer l.Close()
    3. ...
    4. baseCtx := context.Background() // base is always background, per Issue 16220
    5. ctx := context.WithValue(baseCtx, ServerContextKey, srv)
    6. for {
    7. rw, e := l.Accept()
    8. ...
    9. c := srv.newConn(rw)
    10. c.setState(c.rwc, StateNew) // before Serve can return
    11. go c.serve(ctx)
    12. }
    13. }

    粗略的看,它创建了一个context.Background()上下文对象,并调用ListenerAccept方法开始接受外部请求,在获取到连接数据后使用newConn创建连接对象,在最后使用goroutine的方式处理连接请求,达到其目的

    补充:对于HTTP/2支持,在调用Serve之前,应将srv.TLSConfig初始化为提供的Listener的TLS配置。如果srv.TLSConfig非零,并且在Config.NextProtos中不包含字符串h2,则不启用HTTP/2支持

    六、验证功能

    编写测试客户端

    grpc-hello-world/下新建目录client,新建client.go文件,新增内容:

    1. package main
    2. import (
    3. "log"
    4. "golang.org/x/net/context"
    5. "google.golang.org/grpc"
    6. "google.golang.org/grpc/credentials"
    7. pb "grpc-hello-world/proto"
    8. )
    9. func main() {
    10. creds, err := credentials.NewClientTLSFromFile("../certs/server.pem", "dev")
    11. if err != nil {
    12. log.Println("Failed to create TLS credentials %v", err)
    13. }
    14. conn, err := grpc.Dial(":50052", grpc.WithTransportCredentials(creds))
    15. defer conn.Close()
    16. if err != nil {
    17. log.Println(err)
    18. }
    19. c := pb.NewHelloWorldClient(conn)
    20. context := context.Background()
    21. body := &pb.HelloWorldRequest{
    22. Referer : "Grpc",
    23. }
    24. r, err := c.SayHelloWorld(context, body)
    25. if err != nil {
    26. log.Println(err)
    27. }
    28. log.Println(r.Message)
    29. }

    由于客户端只是展示测试用,就简单的来了,原本它理应归类到cobra的管控下,配置管理等等都应可控化

    在看这篇文章的你,可以试试将测试客户端归类好

    启动服务端

    回到grpc-hello-world/目录下,启动服务端go run main.go server,成功则仅返回

    1. 2018/02/26 17:19:36 gRPC and https listen on: 50052

    执行测试客户端

    回到client目录下,启动客户端go run client.go,成功则返回

    1. 2018/02/26 17:22:57 Grpc

    执行测试Restful Api

    1. curl -X POST -k https://localhost:50052/hello_world -d '{"referer": "restful_api"}'

    成功则返回{"message":"restful_api"}


    最终目录结构

    1. grpc-hello-world
    2. ├── certs
    3. ├── server.key
    4. └── server.pem
    5. ├── client
    6. └── client.go
    7. ├── cmd
    8. ├── root.go
    9. └── server.go
    10. ├── main.go
    11. ├── pkg
    12. └── util
    13. ├── grpc.go
    14. └── tls.go
    15. ├── proto
    16. ├── google
    17. └── api
    18. ├── annotations.pb.go
    19. ├── annotations.proto
    20. ├── http.pb.go
    21. └── http.proto
    22. ├── hello.pb.go
    23. ├── hello.pb.gw.go
    24. └── hello.proto
    25. └── server
    26. ├── hello.go
    27. └── server.go

    至此本节就结束了,推荐一下jergoo的文章,大家有时间可以看看

    另外本节涉及了许多组件间的知识,值得大家细细的回味,非常有意义!

    参考

    示例代码

    • grpc-hello-world