Grpc+Grpc Gateway实践二 有些复杂的Hello World
Hello World
在上一节中我们已经完成了对环境的基本配置
这节将开始编写一个复杂的Hello World,涉及到许多的知识,建议大家认真思考其中的概念
需求
由于本实践偏向Grpc
+Grpc Gateway
的方面,我们的需求是同一个服务端支持Rpc
和Restful Api
,那么就意味着http2
、TLS
等等的应用,功能方面就是一个服务端能够接受来自Grpc
和Restful Api
的请求并响应
一、初始化目录
我们先在$GOPATH中新建grpc-hello-world
文件夹,我们项目的初始目录目录如下:
grpc-hello-world/ ├── certs ├── client ├── cmd ├── pkg ├── proto │ ├── google │ │ └── api └── server
certs
:证书凭证client
:客户端cmd
:命令行pkg
:第三方公共模块proto
:protobuf
的一些相关文件(含.proto
、pb.go
、.pb.gw.go
),google/api
中用于存放annotations.proto
、http.proto
server
:服务端
二、制作证书
在服务端支持Rpc
和Restful Api
,需要用到TLS
,因此我们要先制作证书
进入certs
目录,生成TLS
所需的公钥密钥文件
私钥
openssl genrsa -out server.key 2048 openssl ecparam -genkey -name secp384r1 -out server.key
openssl genrsa
:生成RSA
私钥,命令的最后一个参数,将指定生成密钥的位数,如果没有指定,默认512openssl ecparam
:生成ECC
私钥,命令为椭圆曲线密钥参数生成及操作,本文中ECC
曲线选择的是secp384r1
自签名公钥
openssl req -new -x509 -sha256 -key server.key -out server.pem -days 3650
openssl req
:生成自签名证书,-new
指生成证书请求、-sha256
指使用sha256
加密、-key
指定私钥文件、-x509
指输出证书、-days 3650
为有效期,此后则输入证书拥有者信息
填写信息
Country Name (2 letter code) [XX]: State or Province Name (full name) []: Locality Name (eg, city) [Default City]: Organization Name (eg, company) [Default Company Ltd]: Organizational Unit Name (eg, section) []: Common Name (eg, your name or your server's hostname) []:grpc server name Email Address []:
三、proto
编写
1、 google.api
我们看到proto
目录中有google/api
目录,它用到了google
官方提供的两个api
描述文件,主要是针对grpc-gateway
的http
转换提供支持,定义了Protocol Buffer
所扩展的HTTP Option
annotations.proto
文件:
// Copyright (c) 2015, Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. syntax = "proto3"; package google.api; import "google/api/http.proto"; import "google/protobuf/descriptor.proto"; option java_multiple_files = true; option java_outer_classname = "AnnotationsProto"; option java_package = "com.google.api"; extend google.protobuf.MethodOptions { // See `HttpRule`. HttpRule http = 72295728; }
http.proto
文件:
// Copyright 2016 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. syntax = "proto3"; package google.api; option cc_enable_arenas = true; option java_multiple_files = true; option java_outer_classname = "HttpProto"; option java_package = "com.google.api"; // Defines the HTTP configuration for a service. It contains a list of // [HttpRule][google.api.HttpRule], each specifying the mapping of an RPC method // to one or more HTTP REST API methods. message Http { // A list of HTTP rules for configuring the HTTP REST API methods. repeated HttpRule rules = 1; } // Use CustomHttpPattern to specify any HTTP method that is not included in the // `pattern` field, such as HEAD, or "*" to leave the HTTP method unspecified for // a given URL path rule. The wild-card rule is useful for services that provide // content to Web (HTML) clients. message HttpRule { // Selects methods to which this rule applies. // // Refer to [selector][google.api.DocumentationRule.selector] for syntax details. string selector = 1; // Determines the URL pattern is matched by this rules. This pattern can be // used with any of the {get|put|post|delete|patch} methods. A custom method // can be defined using the 'custom' field. oneof pattern { // Used for listing and getting information about resources. string get = 2; // Used for updating a resource. string put = 3; // Used for creating a resource. string post = 4; // Used for deleting a resource. string delete = 5; // Used for updating a resource. string patch = 6; // Custom pattern is used for defining custom verbs. CustomHttpPattern custom = 8; } // The name of the request field whose value is mapped to the HTTP body, or // `*` for mapping all fields not captured by the path pattern to the HTTP // body. NOTE: the referred field must not be a repeated field. string body = 7; // Additional HTTP bindings for the selector. Nested bindings must // not contain an `additional_bindings` field themselves (that is, // the nesting may only be one level deep). repeated HttpRule additional_bindings = 11; } // A custom pattern is used for defining custom HTTP verb. message CustomHttpPattern { // The name of this custom HTTP verb. string kind = 1; // The path matched by this custom verb. string path = 2; }
hello.proto
这一小节将编写Demo
的.proto
文件,我们在proto
目录下新建hello.proto
文件,写入文件内容:
syntax = "proto3"; package proto; import "google/api/annotations.proto"; service HelloWorld { rpc SayHelloWorld(HelloWorldRequest) returns (HelloWorldResponse) { option (google.api.http) = { post: "/hello_world" body: "*" }; } } message HelloWorldRequest { string referer = 1; } message HelloWorldResponse { string message = 1; }
在hello.proto
文件中,引用了google/api/annotations.proto
,达到支持HTTP Option
的效果
- 定义了一个
service
RPC服务HelloWorld
,在其内部定义了一个HTTP Option
的POST
方法,http
响应路径为/hello_world
- 定义
message
类型HelloWorldRequest
、HelloWorldResponse
,用于响应请求和返回结果
编译
在编写完.proto
文件后,我们需要对其进行编译,就能够在server
中使用
进入proto
目录,执行以下命令
# 编译google.api protoc -I . --go_out=plugins=grpc,Mgoogle/protobuf/descriptor.proto=github.com/golang/protobuf/protoc-gen-go/descriptor:. google/api/*.proto #编译hello_http.proto为hello_http.pb.proto protoc -I . --go_out=plugins=grpc,Mgoogle/api/annotations.proto=grpc-hello-world/proto/google/api:. ./hello.proto #编译hello_http.proto为hello_http.pb.gw.proto protoc --grpc-gateway_out=logtostderr=true:. ./hello.proto
执行完毕后将生成hello.pb.go
和hello.gw.pb.go
,分别针对Grpc
和grpc-gateway
的功能支持
四、命令行模块 cmd
介绍
这一小节我们编写命令行模块,为什么要独立出来呢,是为了将cmd
和server
两者解耦,避免混淆在一起。
我们采用 Cobra 来完成这项功能,Cobra
既是创建强大的现代CLI应用程序的库,也是生成应用程序和命令文件的程序。提供了以下功能:
- 简易的子命令行模式
- 完全兼容posix的命令行模式(包括短和长版本)
- 嵌套的子命令
- 全局、本地和级联
flags
- 使用
Cobra
很容易的生成应用程序和命令,使用cobra create appname
和cobra add cmdname
- 智能提示
- 自动生成commands和flags的帮助信息
- 自动生成详细的help信息
-h
,--help
等等 - 自动生成的bash自动完成功能
- 为应用程序自动生成手册
- 命令别名
- 定义您自己的帮助、用法等的灵活性。
- 可选与viper紧密集成的apps
编写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 }
编写cmd
在cmd
模块下 新建root.go
文件,写入内容:
package cmd import ( "fmt" "os" "github.com/spf13/cobra" ) var rootCmd = &cobra.Command{ Use: "grpc", Short: "Run the gRPC hello-world server", } func Execute() { if err := rootCmd.Execute(); err != nil { fmt.Println(err) os.Exit(-1) } }
新建server.go
文件,写入内容:
package cmd import ( "log" "github.com/spf13/cobra" "grpc-hello-world/server" ) var serverCmd = &cobra.Command{ Use: "server", Short: "Run the gRPC hello-world server", Run: func(cmd *cobra.Command, args []string) { defer func() { if err := recover(); err != nil { log.Println("Recover error : %v", err) } }() server.Serve() }, } func init() { 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 server name", "server's hostname") rootCmd.AddCommand(serverCmd) }
我们在grpc-hello-world/
目录下,新建文件main.go
,写入内容:
package main import ( "grpc-hello-world/cmd" ) func main() { cmd.Execute() }
讲解
要使用Cobra
,按照Cobra
标准要创建main.go
和一个rootCmd
文件,另外我们有子命令server
1、rootCmd
:rootCmd
表示在没有任何子命令的情况下的基本命令
2、&cobra.Command
:
Use
:Command
的用法,Use
是一个行用法消息Short
:Short
是help
命令输出中显示的简短描述Run
:运行:典型的实际工作功能。大多数命令只会实现这一点;另外还有PreRun
、PreRunE
、PostRun
、PostRunE
等等不同时期的运行命令,但比较少用,具体使用时再查看亦可
3、rootCmd.AddCommand
:AddCommand
向这父命令(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 Commands
、Bind Flags with Config
、Required flags
等等,使用到再 传送 了解即可
测试
回到grpc-hello-world/
目录下执行go run main.go server
,查看输出是否为(此时应为默认值):
2018/02/25 23:23:21 50052 2018/02/25 23:23:21 dev 2018/02/25 23:23:21 ./certs/server.pem 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
,检验命令行参数是否正确:
2018/02/25 23:24:56 8000 2018/02/25 23:24:56 test-name 2018/02/25 23:24:56 test-pem 2018/02/25 23:24:56 test-key
若都无误,那么恭喜你cmd
模块的编写正确了,下一部分开始我们的重点章节!
五、服务端模块 server
编写hello.go
在server
目录下新建文件hello.go
,写入文件内容:
package server import ( "golang.org/x/net/context" pb "grpc-hello-world/proto" ) type helloService struct{} func NewHelloService() *helloService { return &helloService{} } func (h helloService) SayHelloWorld(ctx context.Context, r *pb.HelloWorldRequest) (*pb.HelloWorldResponse, error) { return &pb.HelloWorldResponse{ Message : "test", }, nil }
我们创建了helloService
及其方法SayHelloWorld
,对应.proto
的rpc SayHelloWorld
,这个方法需要有2个参数:ctx context.Context
用于接受上下文参数、r *pb.HelloWorldRequest
用于接受protobuf
的Request
参数(对应.proto
的message HelloWorldRequest
)
*编写server.go
这一小章节,我们编写最为重要的服务端程序部分,涉及到大量的Grpc
、grpc-gateway
及一些网络知识的应用
1、在pkg
下新建util
目录,新建grpc.go
文件,写入内容:
package util import ( "net/http" "strings" "google.golang.org/grpc" ) 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
2、在pkg
下的util
目录下,新建tls.go
文件,写入内容:
package util import ( "crypto/tls" "io/ioutil" "log" "golang.org/x/net/http2" ) func GetTLSConfig(certPemPath, certKeyPath string) *tls.Config { var certKeyPair *tls.Certificate cert, _ := ioutil.ReadFile(certPemPath) key, _ := ioutil.ReadFile(certKeyPath) pair, err := tls.X509KeyPair(cert, key) if err != nil { log.Println("TLS KeyPair err: %v\n", err) } certKeyPair = &pair return &tls.Config{ Certificates: []tls.Certificate{*certKeyPair}, NextProtos: []string{http2.NextProtoTLS}, } }
GetTLSConfig
函数是用于获取TLS
配置,在内部,我们读取了server.key
和server.pem
这类证书凭证文件
tls.X509KeyPair
:从一对PEM
编码的数据中解析公钥/私钥对。成功则返回公钥/私钥对http2.NextProtoTLS
:NextProtoTLS
是谈判期间的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
文件,该文件是我们服务里的核心文件,写入内容:
package server import ( "crypto/tls" "net" "net/http" "log" "golang.org/x/net/context" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "github.com/grpc-ecosystem/grpc-gateway/runtime" pb "grpc-hello-world/proto" "grpc-hello-world/pkg/util" ) var ( ServerPort string CertName string CertPemPath string CertKeyPath string EndPoint string ) func Serve() (err error){ EndPoint = ":" + ServerPort conn, err := net.Listen("tcp", EndPoint) if err != nil { log.Printf("TCP Listen err:%v\n", err) } tlsConfig := util.GetTLSConfig(CertPemPath, CertKeyPath) srv := createInternalServer(conn, tlsConfig) log.Printf("gRPC and https listen on: %s\n", ServerPort) if err = srv.Serve(tls.NewListener(conn, tlsConfig)); err != nil { log.Printf("ListenAndServe: %v\n", err) } return err } func createInternalServer(conn net.Listener, tlsConfig *tls.Config) (*http.Server) { var opts []grpc.ServerOption // grpc server creds, err := credentials.NewServerTLSFromFile(CertPemPath, CertKeyPath) if err != nil { log.Printf("Failed to create server TLS credentials %v", err) } opts = append(opts, grpc.Creds(creds)) grpcServer := grpc.NewServer(opts...) // register grpc pb pb.RegisterHelloWorldServer(grpcServer, NewHelloService()) // gw server ctx := context.Background() dcreds, err := credentials.NewClientTLSFromFile(CertPemPath, CertName) if err != nil { log.Printf("Failed to create client TLS credentials %v", err) } dopts := []grpc.DialOption{grpc.WithTransportCredentials(dcreds)} gwmux := runtime.NewServeMux() // register grpc-gateway pb if err := pb.RegisterHelloWorldHandlerFromEndpoint(ctx, gwmux, EndPoint, dopts); err != nil { log.Printf("Failed to register gw server: %v\n", err) } // http服务 mux := http.NewServeMux() mux.Handle("/", gwmux) return &http.Server{ Addr: EndPoint, Handler: util.GrpcHandlerFunc(grpcServer, mux), TLSConfig: tlsConfig, } }
server
流程剖析
我们将这一大块代码,分成以下几个部分来理解
一、启动监听
net.Listen("tcp", EndPoint)
用于监听本地的网络地址通知,它的函数原型func Listen(network, address string) (Listener, error)
参数:network
必须传入tcp
、tcp4
、tcp6
、unix
、unixpacket
,若address
为空或为0则会自动选择一个端口号
返回值:通过查看源码我们可以得知其返回值为Listener
,结构体原型:
type Listener interface { Accept() (Conn, error) Close() error Addr() Addr }
通过分析得知,最后net.Listen
会返回一个监听器的结构体,返回给接下来的动作,让其执行下一步的操作,它可以执行三类操作
Accept
:接受等待并将下一个连接返回给Listener
Close
:关闭Listener
Addr
:返回Listener
的网络地址
二、获取TLS
通过util.GetTLSConfig
解析得到tls.Config
,传达给http.Server
服务的TLSConfig
配置项使用
三、创建内部服务
createInternalServer
函数,是整个服务端的核心流转部分
程序采用的是HTT2
、HTTPS
也就是需要支持TLS
,因此在启动grpc.NewServer
前,我们要将认证的中间件注册进去
而前面所获取的TLSConfig
仅能给http
使用,因此第一步我们要创建Grpc
的TLS
认证凭证
1、创建Grpc
的TLS
认证凭证
新增引用google.golang.org/grpc/credentials
的第三方包,它实现了Grpc
库支持的各种凭证,该凭证封装了客户机需要的所有状态,以便与服务器进行身份验证并进行各种断言,例如关于客户机的身份,角色或是否授权进行特定的呼叫
我们调用NewServerTLSFromFile
来达到我们的目的,它能够从输入证书文件和服务器的密钥文件构造TLS证书凭证
func NewServerTLSFromFile(certFile, keyFile string) (TransportCredentials, error) { //LoadX509KeyPair读取并解析来自一对文件的公钥/私钥对 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 }
2、设置grpc ServerOption
以grpc.Creds(creds)
为例,其原型为func Creds(c credentials.TransportCredentials) ServerOption
,该函数返回ServerOption
,它为服务器连接设置凭据
3、创建Grpc
服务端
函数原型:
func NewServer(opt ...ServerOption) *Server
我们在此处创建了一个没有注册服务的Grpc
服务端,还没有开始接受请求
grpcServer := grpc.NewServer(opts...)
4、注册Grpc
服务
pb.RegisterHelloWorldServer(grpcServer, NewHelloService())
5、创建grpc-gateway
关联组件
ctx := context.Background() dcreds, err := credentials.NewClientTLSFromFile(CertPemPath, CertName) if err != nil { log.Println("Failed to create client TLS credentials %v", err) } dopts := []grpc.DialOption{grpc.WithTransportCredentials(dcreds)}
context.Background
:返回一个非空的空上下文。它没有被注销,没有值,没有过期时间。它通常由主函数、初始化和测试使用,并作为传入请求的顶级上下文credentials.NewClientTLSFromFile
:从客户机的输入证书文件构造TLS凭证grpc.WithTransportCredentials
:配置一个连接级别的安全凭据(例:TLS
、SSL
),返回值为type DialOption
grpc.DialOption
:DialOption
选项配置我们如何设置连接(其内部具体由多个的DialOption
组成,决定其设置连接的内容)
6、创建HTTP NewServeMux
及注册grpc-gateway
逻辑
gwmux := runtime.NewServeMux() // register grpc-gateway pb if err := pb.RegisterHelloWorldHandlerFromEndpoint(ctx, gwmux, EndPoint, dopts); err != nil { log.Println("Failed to register gw server: %v\n", err) } // http服务 mux := http.NewServeMux() mux.Handle("/", gwmux)
runtime.NewServeMux
:返回一个新的ServeMux
,它的内部映射是空的;ServeMux
是grpc-gateway
的一个请求多路复用器。它将http
请求与模式匹配,并调用相应的处理程序RegisterHelloWorldHandlerFromEndpoint
:如函数名,注册HelloWorld
服务的HTTP Handle
到Grpc
端点http.NewServeMux
:分配并返回一个新的ServeMux
mux.Handle
:为给定模式注册处理程序
(带着疑问去看程序)为什么gwmux
可以放入mux.Handle
中?
首先我们看看它们的原型是怎么样的
(1)http.NewServeMux()
func NewServeMux() *ServeMux { return new(ServeMux) }
type Handler interface { ServeHTTP(ResponseWriter, *Request) }
(2)runtime.NewServeMux
?
func NewServeMux(opts ...ServeMuxOption) *ServeMux { serveMux := &ServeMux{ handlers: make(map[string][]handler), forwardResponseOptions: make([]func(context.Context, http.ResponseWriter, proto.Message) error, 0), marshalers: makeMarshalerMIMERegistry(), } ... return serveMux }
(3)http.NewServeMux()
的Handle
方法
func (mux *ServeMux) Handle(pattern string, handler Handler)
通过分析可得知,两者NewServeMux
都是最终返回ServeMux
,Handler
中导出的方法仅有ServeHTTP
,功能是用于响应HTTP请求
我们回到Handle interface
中,可以得出结论就是任何结构体,只要实现了ServeHTTP
方法,这个结构就可以称为Handle
,ServeMux
会使用该Handler
调用ServeHTTP
方法处理请求,这也就是自定义Handler
而我们这里正是将grpc-gateway
中注册好的HTTP Handler
无缝的植入到net/http
的Handle
方法中
补充:在go
中任何结构体只要实现了与接口相同的方法,就等同于实现了接口
7、注册具体服务
if err := pb.RegisterHelloWorldHandlerFromEndpoint(ctx, gwmux, EndPoint, dopts); err != nil { log.Println("Failed to register gw server: %v\n", err) }
在这段代码中,我们利用了前几小节的
- 上下文
gateway-grpc
的请求多路复用器- 服务网络地址
- 配置好的安全凭据
注册了HelloWorld
这一个服务
四、创建tls.NewListener
func NewListener(inner net.Listener, config *Config) net.Listener { l := new(listener) l.Listener = inner l.config = config return l }
NewListener
将会创建一个Listener
,它接受两个参数,第一个是来自内部Listener
的监听器,第二个参数是tls.Config
(必须包含至少一个证书)
五、服务开始接受请求
在最后我们调用srv.Serve(tls.NewListener(conn, tlsConfig))
,可以得知它是http.Server
的方法,并且需要一个Listener
作为参数,那么Serve
内部做了些什么事呢?
func (srv *Server) Serve(l net.Listener) error { defer l.Close() ... baseCtx := context.Background() // base is always background, per Issue 16220 ctx := context.WithValue(baseCtx, ServerContextKey, srv) for { rw, e := l.Accept() ... c := srv.newConn(rw) c.setState(c.rwc, StateNew) // before Serve can return go c.serve(ctx) } }
粗略的看,它创建了一个context.Background()
上下文对象,并调用Listener
的Accept
方法开始接受外部请求,在获取到连接数据后使用newConn
创建连接对象,在最后使用goroutine
的方式处理连接请求,达到其目的
补充:对于HTTP/2
支持,在调用Serve
之前,应将srv.TLSConfig
初始化为提供的Listener
的TLS配置。如果srv.TLSConfig
非零,并且在Config.NextProtos
中不包含字符串h2
,则不启用HTTP/2
支持
六、验证功能
编写测试客户端
在grpc-hello-world/
下新建目录client
,新建client.go
文件,新增内容:
package main import ( "log" "golang.org/x/net/context" "google.golang.org/grpc" "google.golang.org/grpc/credentials" pb "grpc-hello-world/proto" ) func main() { creds, err := credentials.NewClientTLSFromFile("../certs/server.pem", "dev") if err != nil { log.Println("Failed to create TLS credentials %v", err) } conn, err := grpc.Dial(":50052", grpc.WithTransportCredentials(creds)) defer conn.Close() if err != nil { log.Println(err) } c := pb.NewHelloWorldClient(conn) context := context.Background() body := &pb.HelloWorldRequest{ Referer : "Grpc", } r, err := c.SayHelloWorld(context, body) if err != nil { log.Println(err) } log.Println(r.Message) }
由于客户端只是展示测试用,就简单的来了,原本它理应归类到Cobra
的管控下,配置管理等等都应可控化
在看这篇文章的你,可以试试将测试客户端归类好
启动服务端
回到grpc-hello-world/
目录下,启动服务端go run main.go server
,成功则仅返回
2018/02/26 17:19:36 gRPC and https listen on: 50052
执行测试客户端
回到client
目录下,启动客户端go run client.go
,成功则返回
2018/02/26 17:22:57 Grpc
执行测试Restful Api
curl -X POST -k https://localhost:50052/hello_world -d '{"referer": "restful_api"}'
成功则返回{"message":"restful_api"}
最终目录结构
grpc-hello-world ├── certs │ ├── server.key │ └── server.pem ├── client │ └── client.go ├── cmd │ ├── root.go │ └── server.go ├── main.go ├── pkg │ └── util │ ├── grpc.go │ └── tls.go ├── proto │ ├── google │ │ └── api │ │ ├── annotations.pb.go │ │ ├── annotations.proto │ │ ├── http.pb.go │ │ └── http.proto │ ├── hello.pb.go │ ├── hello.pb.gw.go │ └── hello.proto └── server ├── hello.go └── server.go
至此本节就结束了,推荐一下jergoo
的文章,大家有时间可以看看
另外本节涉及了许多组件间的知识,值得大家细细的回味,非常有意义!