Gonet2中,大量使用了gRPC,而对这个我不熟,所以这里花点时间了解一下。当然,环境我已经配好了,这里只是讲代码上如何使用,环境的搭建,网上应该蛮多。不过用gRPC要用科学的方式上网,这个对我华厦民族的同胞们,应该都不陌生了。
远程调用,一开始我想的很复杂,但是真的了解过之后,无非是,server side提供一个开方的接口,公开调用时传送数据的格式,client side遵照这种规定,调用接口提供的方法。
问题来了,既然是远程,那肯定是跨进程,甚至是跨计算机。所以可以通过网络传输的方式来远程调用。比如tcp/ip, http。
gRPC是远程调用框架的一种,传输是通过http2协议,使用接口描述语言 proto3 来定义服务接口,以及数据结构。之前有proto2的,不过proto3是最新的,语法结构有小小不同。
既然是有 A 调用 B 这个过程,有网络协议,那对于接口来说,就肯定会存在客户端,与服务端。服务端处理请求,客户端去调用接口。所以,废话了那么多,来看看一个最简单的hello world接口定义吧。
// 没得更简单了,我试着用int32来做参数和返回值,
// 可是结果确是必须为message类型.
// message是grpc的封装类型,相当于go语言的struct或java的class.
syntax="proto3";
message Req{}
message Res{}
service HelloService {
rpc Hello(Req) returns (Res) {}
}
生成的代码,有点吓到我的感觉!我写了6行,生成这么多!先把完整代码帖上,然后我们一点点拆开看,要是怕吓到,先跳过。
// Code generated by protoc-gen-go.
// source: test.proto
// DO NOT EDIT!
/*
Package test is a generated protocol buffer package.
It is generated from these files:
test.proto
It has these top-level messages:
Req
Res
*/
package test
import proto "github.com/golang/protobuf/proto"
import (
context "golang.org/x/net/context"
grpc "google.golang.org/grpc"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
type Req struct {
}
func (m *Req) Reset() { *m = Req{} }
func (m *Req) String() string { return proto.CompactTextString(m) }
func (*Req) ProtoMessage() {}
type Res struct {
}
func (m *Res) Reset() { *m = Res{} }
func (m *Res) String() string { return proto.CompactTextString(m) }
func (*Res) ProtoMessage() {}
// Reference imports to suppress errors if they are not otherwise used.
var _ context.Context
var _ grpc.ClientConn
// Client API for HelloService service
type HelloServiceClient interface {
Hello(ctx context.Context, in *Req, opts ...grpc.CallOption) (*Res, error)
}
type helloServiceClient struct {
cc *grpc.ClientConn
}
func NewHelloServiceClient(cc *grpc.ClientConn) HelloServiceClient {
return &helloServiceClient{cc}
}
func (c *helloServiceClient) Hello(ctx context.Context, in *Req, opts ...grpc.CallOption) (*Res, error) {
out := new(Res)
err := grpc.Invoke(ctx, "/.HelloService/Hello", in, out, c.cc, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// Server API for HelloService service
type HelloServiceServer interface {
Hello(context.Context, *Req) (*Res, error)
}
func RegisterHelloServiceServer(s *grpc.Server, srv HelloServiceServer) {
s.RegisterService(&_HelloService_serviceDesc, srv)
}
func _HelloService_Hello_Handler(srv interface{}, ctx context.Context, codec grpc.Codec, buf []byte) (interface{}, error) {
in := new(Req)
if err := codec.Unmarshal(buf, in); err != nil {
return nil, err
}
out, err := srv.(HelloServiceServer).Hello(ctx, in)
if err != nil {
return nil, err
}
return out, nil
}
var _HelloService_serviceDesc = grpc.ServiceDesc{
ServiceName: ".HelloService",
HandlerType: (*HelloServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "Hello",
Handler: _HelloService_Hello_Handler,
},
},
Streams: []grpc.StreamDesc{},
}
仔细看一下,注释写的也挺清楚了。我个人的感觉是,打个比喻来说,同样的代码,仔细看的话,觉得难度是5,不仔细看,一下就蒙了,那难度可能是8。
上面分析过rpc调用,有服务端和客户端,还有通讯的数据格式,在这个生成分件里刚好有这三个部分。(其实,我是先看了这gRPC,再反过来分析理论的,反正说得通!)
先看一下Server部分,我加了一些注释,更详细的解释了每一部分的作用,完全是照顾新手,还有跟我一样,不喜欢仔细看代码的。
// HelloService服务接口定义
// 通过实现这个接口,来定义服务方法Hello的具体内容,这就是服务端(Server Side)了
type HelloServiceServer interface {
Hello(context.Context, *Req) (*Res, error)
}
// 实现了上面的服务端(Server Side)
// 需要调用这个方法来告诉gRPC框架,我们的服务端对象是谁。
func RegisterHelloServiceServer(s *grpc.Server, srv HelloServiceServer) {
s.RegisterService(&_HelloService_serviceDesc, srv)
}
// 这个是服务接口的代理,就是加了一层封装的意思,
// 网络传来的数据是byte[],在这里解析成了一个Req对象,
// 然后才去调用我们server side的Hello方法。
func _HelloService_Hello_Handler(srv interface{}, ctx context.Context, codec grpc.Codec, buf []byte) (interface{}, error) {
in := new(Req)
if err := codec.Unmarshal(buf, in); err != nil {
return nil, err
}
out, err := srv.(HelloServiceServer).Hello(ctx, in)
if err != nil {
return nil, err
}
return out, nil
}
// 这个变量是服务的描述对象。定义了服务的名字,类型,方法等。
// 在RegisterHelloServiceServer方法中,就是把这个变量传给了gRPC框架。
var _HelloService_serviceDesc = grpc.ServiceDesc{
ServiceName: ".HelloService",
HandlerType: (*HelloServiceServer)(nil),
Methods: []grpc.MethodDesc{
//服务方法数组
{
MethodName: "Hello",
Handler: _HelloService_Hello_Handler,
},
},
Streams: []grpc.StreamDesc{},
}
第一步 - 定义并实现服务:
type helloServiceServer struct {
}
func (s *helloServiceServer) Hello(ctx context.Context, req *test.Req) (*test.Res, error) {
fmt.Println("Hello")
return &test.Res{}, nil
}
第二步 - 开启网络服务:
// 监听端口
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
// 创建grpc实例
grpcServer := grpc.NewServer()
// 注册helloService服务
test.RegisterHelloServiceServer(grpcServer, &helloServiceServer{})
// 启动服务
grpcServer.Serve(lis)
其实逻辑上,它跟一个http service也差不了多少啊,监听一个端口,然后注册服务,然后就等着收到请求啦。如果再仔细看一看Methods里面是一个数组,这里对Hello和_HelloService_Hello_Handle做了一个映射,应该是一个路由功能,说白了,就是根据请求的名字,调用相应的方法进行处理。
// 客户端接口,里面定义了服务的方法
type HelloServiceClient interface {
Hello(ctx context.Context, in *Req, opts ...grpc.CallOption) (*Res, error)
}
// 这是客户端(Client side)的实现
type helloServiceClient struct {
cc *grpc.ClientConn
}
// 工厂方法,获得一个客户端实例。
// 我们的代码要通过这个实例来调用远程服务。
func NewHelloServiceClient(cc *grpc.ClientConn) HelloServiceClient {
return &helloServiceClient{cc}
}
// 客户端(Client side)的服务方法封装。
// 我们调用这个方法,在这里gRPC把我们的参数封装好,然后发送出去。
// 作为客户端,只管调用就完事了,是不是很方便!
func (c *helloServiceClient) Hello(ctx context.Context, in *Req, opts ...grpc.CallOption) (*Res, error) {
out := new(Res)
err := grpc.Invoke(ctx, "/.HelloService/Hello", in, out, c.cc, opts...)
if err != nil {
return nil, err
}
return out, nil
}
上面是生成的go语言代码,里面我自己加了注释。相信大家看过便懂,不懂的话是我表达的不好。接下来看看如何使用:
// 先连接到服务器
conn, err := grpc.Dial(*serverAddr)
if err != nil {
...
}
defer conn.Close()
// 通过工厂方法得到一个客户端对象
client := test.NewHelloServiceClient(conn)
// 调用
client.Hello(context.Background(), &test.Req{})
*注明一下,以上代码,除了proto是我自己生成了,下面的测试代码我都没跑过,说了是解析嘛。