5.2 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
└── servercerts:证书凭证client:客户端cmd:命令行pkg:第三方公共模块proto:protobuf的一些相关文件(含.proto、pb.go、.pb.gw.go),google/api中用于存放annotations.proto、http.protoserver:服务端
二、制作证书
在服务端支持Rpc和Restful Api,需要用到TLS,因此我们要先制作证书
进入certs目录,生成TLS所需的公钥密钥文件
私钥
openssl genrsa:生成RSA私钥,命令的最后一个参数,将指定生成密钥的位数,如果没有指定,默认512openssl ecparam:生成ECC私钥,命令为椭圆曲线密钥参数生成及操作,本文中ECC曲线选择的是secp384r1
自签名公钥
openssl req:生成自签名证书,-new指生成证书请求、-sha256指使用sha256加密、-key指定私钥文件、-x509指输出证书、-days 3650为有效期,此后则输入证书拥有者信息
填写信息
三、proto
proto编写
1、 google.api
我们看到proto目录中有google/api目录,它用到了google官方提供的两个api描述文件,主要是针对grpc-gateway的http转换提供支持,定义了Protocol Buffer所扩展的HTTP Option
annotations.proto文件:
http.proto文件:
hello.proto
这一小节将编写Demo的.proto文件,我们在proto目录下新建hello.proto文件,写入文件内容:
在hello.proto文件中,引用了google/api/annotations.proto,达到支持HTTP Option的效果
定义了一个
serviceRPC服务HelloWorld,在其内部定义了一个HTTP Option的POST方法,HTTP响应路径为/hello_world定义
message类型HelloWorldRequest、HelloWorldResponse,用于响应请求和返回结果
编译
在编写完.proto文件后,我们需要对其进行编译,就能够在server中使用
进入proto目录,执行以下命令
执行完毕后将生成hello.pb.go和hello.gw.pb.go,分别针对grpc和grpc-gateway的功能支持
四、命令行模块 cmd
cmd介绍
这一小节我们编写命令行模块,为什么要独立出来呢,是为了将cmd和server两者解耦,避免混淆在一起。
我们采用 Cobra 来完成这项功能,Cobra既是创建强大的现代CLI应用程序的库,也是生成应用程序和命令文件的程序。提供了以下功能:
简易的子命令行模式
完全兼容posix的命令行模式(包括短和长版本)
嵌套的子命令
全局、本地和级联
flags使用
Cobra很容易的生成应用程序和命令,使用cobra create appname和cobra add cmdname智能提示
自动生成commands和flags的帮助信息
自动生成详细的help信息
-h,--help等等自动生成的bash自动完成功能
为应用程序自动生成手册
命令别名
定义您自己的帮助、用法等的灵活性。
可选与viper紧密集成的apps
编写server
server在编写cmd时需要先用server进行测试关联,因此这一步我们先写server.go用于测试
在server模块下 新建server.go文件,写入测试内容:
编写cmd
cmd在cmd模块下 新建root.go文件,写入内容:
新建server.go文件,写入内容:
我们在grpc-hello-world/目录下,新建文件main.go,写入内容:
讲解
要使用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,查看输出是否为(此时应为默认值):
执行go run main.go server --port=8000 --cert-pem=test-pem --cert-key=test-key --cert-name=test-name,检验命令行参数是否正确:
若都无误,那么恭喜你cmd模块的编写正确了,下一部分开始我们的重点章节!
五、服务端模块 server
server编写hello.go
hello.go在server目录下新建文件hello.go,写入文件内容:
我们创建了helloService及其方法SayHelloWorld,对应.proto的rpc SayHelloWorld,这个方法需要有2个参数:ctx context.Context用于接受上下文参数、r *pb.HelloWorldRequest用于接受protobuf的Request参数(对应.proto的message HelloWorldRequest)
*编写server.go
server.go这一小章节,我们编写最为重要的服务端程序部分,涉及到大量的grpc、grpc-gateway及一些网络知识的应用
1、在pkg下新建util目录,新建grpc.go文件,写入内容:
GrpcHandlerFunc函数是用于判断请求是来源于Rpc客户端还是Restful Api的请求,根据不同的请求注册不同的ServeHTTP服务;r.ProtoMajor == 2也代表着请求必须基于HTTP/2
2、在pkg下的util目录下,新建tls.go文件,写入内容:
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文件,该文件是我们服务里的核心文件,写入内容:
server流程剖析
server流程剖析我们将这一大块代码,分成以下几个部分来理解
一、启动监听
net.Listen("tcp", EndPoint)用于监听本地的网络地址通知,它的函数原型func Listen(network, address string) (Listener, error)
参数:network必须传入tcp、tcp4、tcp6、unix、unixpacket,若address为空或为0则会自动选择一个端口号 返回值:通过查看源码我们可以得知其返回值为Listener,结构体原型:
通过分析得知,最后net.Listen会返回一个监听器的结构体,返回给接下来的动作,让其执行下一步的操作,它可以执行三类操作
Accept:接受等待并将下一个连接返回给ListenerClose:关闭ListenerAddr:返回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证书凭证
2、设置grpc ServerOption
以grpc.Creds(creds)为例,其原型为func Creds(c credentials.TransportCredentials) ServerOption,该函数返回ServerOption,它为服务器连接设置凭据
3、创建grpc服务端
函数原型:
我们在此处创建了一个没有注册服务的grpc服务端,还没有开始接受请求
4、注册grpc服务
5、创建grpc-gateway关联组件
context.Background:返回一个非空的空上下文。它没有被注销,没有值,没有过期时间。它通常由主函数、初始化和测试使用,并作为传入请求的顶级上下文credentials.NewClientTLSFromFile:从客户机的输入证书文件构造TLS凭证grpc.WithTransportCredentials:配置一个连接级别的安全凭据(例:TLS、SSL),返回值为type DialOptiongrpc.DialOption:DialOption选项配置我们如何设置连接(其内部具体由多个的DialOption组成,决定其设置连接的内容)
6、创建HTTP NewServeMux及注册grpc-gateway逻辑
runtime.NewServeMux:返回一个新的ServeMux,它的内部映射是空的;ServeMux是grpc-gateway的一个请求多路复用器。它将http请求与模式匹配,并调用相应的处理程序RegisterHelloWorldHandlerFromEndpoint:如函数名,注册HelloWorld服务的HTTP Handle到grpc端点http.NewServeMux:分配并返回一个新的ServeMuxmux.Handle:为给定模式注册处理程序
(带着疑问去看程序)为什么gwmux可以放入mux.Handle中?
首先我们看看它们的原型是怎么样的
(1)http.NewServeMux()
(2)runtime.NewServeMux?
(3)http.NewServeMux()的Handle方法
通过分析可得知,两者NewServeMux都是最终返回serveMux,Handler中导出的方法仅有ServeHTTP,功能是用于响应HTTP请求
我们回到Handle interface中,可以得出结论就是任何结构体,只要实现了ServeHTTP方法,这个结构就可以称为Handle,ServeMux会使用该Handler调用ServeHTTP方法处理请求,这也就是自定义Handler
而我们这里正是将grpc-gateway中注册好的HTTP Handler无缝的植入到net/http的Handle方法中
补充:在go中任何结构体只要实现了与接口相同的方法,就等同于实现了接口
7、注册具体服务
在这段代码中,我们利用了前几小节的
上下文
gateway-grpc的请求多路复用器服务网络地址
配置好的安全凭据
注册了HelloWorld这一个服务
四、创建tls.NewListener
NewListener将会创建一个Listener,它接受两个参数,第一个是来自内部Listener的监听器,第二个参数是tls.Config(必须包含至少一个证书)
五、服务开始接受请求
在最后我们调用srv.Serve(tls.NewListener(conn, tlsConfig)),可以得知它是http.Server的方法,并且需要一个Listener作为参数,那么Serve内部做了些什么事呢?
粗略的看,它创建了一个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文件,新增内容:
由于客户端只是展示测试用,就简单的来了,原本它理应归类到cobra的管控下,配置管理等等都应可控化
在看这篇文章的你,可以试试将测试客户端归类好
启动服务端
回到grpc-hello-world/目录下,启动服务端go run main.go server,成功则仅返回
执行测试客户端
回到client目录下,启动客户端go run client.go,成功则返回
执行测试Restful Api
成功则返回{"message":"restful_api"}
最终目录结构
至此本节就结束了,推荐一下jergoo的文章,大家有时间可以看看
另外本节涉及了许多组件间的知识,值得大家细细的回味,非常有意义!
参考
示例代码
Last updated
Was this helpful?