gRPC简单实践

Enviroment:

  • macOS 10.14.2
  • go version go1.11 darwin/amd64

gRPC介绍

近几年技术圈最流行的软件架构,肯定非微服务莫属,而想要实现服务化,首先要解决的就是各个服务间的通信问题,RPC意思为远程过程调用,是在服务器端程序设计常用的一种技术。对于本地过程调用,要做某件事,就在本机上执行某个代码段;而对于RPC来说,服务的使用者和提供者可以位于不同的计算机上,客户端(client)只需要告诉服务器端(server)要做什么事情,这一请求通过网络发送给server,server上执行完成之后就把结果返回给客户端。

在众多开源 RPC 框架中,本次尝试使用Google 的 gRPC进行简单实践。其具有以下重要特征:

  • 强大的IDL(接口定义语言)特性 RPC使用ProtoBuf来定义服务,ProtoBuf是由Google开发的一种数据序列化协议,用于数据的序列化与反序列化,类似JSON。
  • 支持多种语言 支持C++、Java、Go、Python、Ruby、C#、Node.js、Android Java、Objective-C、PHP等编程语言。
  • 基于HTTP/2的数据传输协议,性能更优。

1

实例学习

以 helloworld为例介绍一下gRPC的开发:

1. 安装 gRPC 和 protocol buffers

go get -u google.golang.org/grpc
go get -u github.com/golang/protobuf/{proto,protoc-gen-go}

上述无效时进行手动拉取

git clone https://github.com/grpc/grpc-go.git $GOPATH/src/google.golang.org/grpc
git clone https://github.com/golang/protobuf.git $GOPATH/src/github.com/golang/protobuf
git clone https://github.com/google/go-genproto.git $GOPATH/src/google.golang.org/genproto
go install github.com/golang/protobuf/proto
go install github.com/golang/protobuf/protoc-gen-go

保证安装 Protocol Buffers v3 编译器(protoc)到开发环境中(有名为protoc-gen-go的bin文件)。

2. 定义服务

新建工作目录grpc-hello,在子目录helloworld中定义protobuf文件 (helloworld.proto):

// Copyright 2015 gRPC authors.
//
// 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";

option java_multiple_files = true;
option java_package = "io.grpc.examples.helloworld";
option java_outer_classname = "HelloWorldProto";

package helloworld;

// The greeting service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}

一个RPC service就是一个能够通过参数和返回值进行远程调用的method,可以简单地将它理解成一个函数。因

为gRPC是通过将数据编码成protocal buffer来实现传输的。因此,通过protocal buffers interface definitioin

language(IDL)来定义service method,同时将参数和返回值也定义成protocal buffer message类型。

这个文件定义了一个Greeter服务,它有一个SayHello方法,这个方法接收一个Request,返回一个Response。

其中Request的信息为请求的用户名称,Response的信息为服务端响应的信息。

3. 使用protoc编译服务文件

编译服务文件,生成客户端和服务端所需的gRPC接口的go代码。

1

protoc参数:https://github.com/golang/protobuf

cd $GOPATH/src/github.com/hansenbeast/grpc-hello/
protoc -I helloworld helloworld/helloworld.proto --go_out=$GOPATH/src/github.com/hansenbeast/grpc-hello/helloworld

1

// Code generated by protoc-gen-go. DO NOT EDIT.
// source: helloworld.proto

package helloworld

import (
	fmt "fmt"
	proto "github.com/golang/protobuf/proto"
	math "math"
)

// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf

// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package

// The request message containing the user's name.
type HelloRequest struct {
	Name                 string   `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
	XXX_NoUnkeyedLiteral struct{} `json:"-"`
	XXX_unrecognized     []byte   `json:"-"`
	XXX_sizecache        int32    `json:"-"`
}

func (m *HelloRequest) Reset()         { *m = HelloRequest{} }
func (m *HelloRequest) String() string { return proto.CompactTextString(m) }
func (*HelloRequest) ProtoMessage()    {}
func (*HelloRequest) Descriptor() ([]byte, []int) {
	return fileDescriptor_17b8c58d586b62f2, []int{0}
}

func (m *HelloRequest) XXX_Unmarshal(b []byte) error {
	return xxx_messageInfo_HelloRequest.Unmarshal(m, b)
}
func (m *HelloRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
	return xxx_messageInfo_HelloRequest.Marshal(b, m, deterministic)
}
func (m *HelloRequest) XXX_Merge(src proto.Message) {
	xxx_messageInfo_HelloRequest.Merge(m, src)
}
func (m *HelloRequest) XXX_Size() int {
	return xxx_messageInfo_HelloRequest.Size(m)
}
func (m *HelloRequest) XXX_DiscardUnknown() {
	xxx_messageInfo_HelloRequest.DiscardUnknown(m)
}

var xxx_messageInfo_HelloRequest proto.InternalMessageInfo

func (m *HelloRequest) GetName() string {
	if m != nil {
		return m.Name
	}
	return ""
}

// The response message containing the greetings
type HelloReply struct {
	Message              string   `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
	XXX_NoUnkeyedLiteral struct{} `json:"-"`
	XXX_unrecognized     []byte   `json:"-"`
	XXX_sizecache        int32    `json:"-"`
}

func (m *HelloReply) Reset()         { *m = HelloReply{} }
func (m *HelloReply) String() string { return proto.CompactTextString(m) }
func (*HelloReply) ProtoMessage()    {}
func (*HelloReply) Descriptor() ([]byte, []int) {
	return fileDescriptor_17b8c58d586b62f2, []int{1}
}

func (m *HelloReply) XXX_Unmarshal(b []byte) error {
	return xxx_messageInfo_HelloReply.Unmarshal(m, b)
}
func (m *HelloReply) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
	return xxx_messageInfo_HelloReply.Marshal(b, m, deterministic)
}
func (m *HelloReply) XXX_Merge(src proto.Message) {
	xxx_messageInfo_HelloReply.Merge(m, src)
}
func (m *HelloReply) XXX_Size() int {
	return xxx_messageInfo_HelloReply.Size(m)
}
func (m *HelloReply) XXX_DiscardUnknown() {
	xxx_messageInfo_HelloReply.DiscardUnknown(m)
}

var xxx_messageInfo_HelloReply proto.InternalMessageInfo

func (m *HelloReply) GetMessage() string {
	if m != nil {
		return m.Message
	}
	return ""
}

func init() {
	proto.RegisterType((*HelloRequest)(nil), "helloworld.HelloRequest")
	proto.RegisterType((*HelloReply)(nil), "helloworld.HelloReply")
}

func init() { proto.RegisterFile("helloworld.proto", fileDescriptor_17b8c58d586b62f2) }

var fileDescriptor_17b8c58d586b62f2 = []byte{
	// 175 bytes of a gzipped FileDescriptorProto
	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x12, 0xc8, 0x48, 0xcd, 0xc9,
	0xc9, 0x2f, 0xcf, 0x2f, 0xca, 0x49, 0xd1, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x42, 0x88,
	0x28, 0x29, 0x71, 0xf1, 0x78, 0x80, 0x78, 0x41, 0xa9, 0x85, 0xa5, 0xa9, 0xc5, 0x25, 0x42, 0x42,
	0x5c, 0x2c, 0x79, 0x89, 0xb9, 0xa9, 0x12, 0x8c, 0x0a, 0x8c, 0x1a, 0x9c, 0x41, 0x60, 0xb6, 0x92,
	0x1a, 0x17, 0x17, 0x54, 0x4d, 0x41, 0x4e, 0xa5, 0x90, 0x04, 0x17, 0x7b, 0x6e, 0x6a, 0x71, 0x71,
	0x62, 0x3a, 0x4c, 0x11, 0x8c, 0x6b, 0xe4, 0xc9, 0xc5, 0xee, 0x5e, 0x94, 0x9a, 0x5a, 0x92, 0x5a,
	0x24, 0x64, 0xc7, 0xc5, 0x11, 0x9c, 0x58, 0x09, 0xd6, 0x25, 0x24, 0xa1, 0x87, 0xe4, 0x02, 0x64,
	0xcb, 0xa4, 0xc4, 0xb0, 0xc8, 0x14, 0xe4, 0x54, 0x2a, 0x31, 0x38, 0x19, 0x70, 0x49, 0x67, 0xe6,
	0xeb, 0xa5, 0x17, 0x15, 0x24, 0xeb, 0xa5, 0x56, 0x24, 0xe6, 0x16, 0xe4, 0xa4, 0x16, 0x23, 0xa9,
	0x75, 0xe2, 0x07, 0x2b, 0x0e, 0x07, 0xb1, 0x03, 0x40, 0x5e, 0x0a, 0x60, 0x4c, 0x62, 0x03, 0xfb,
	0xcd, 0x18, 0x10, 0x00, 0x00, 0xff, 0xff, 0x0f, 0xb7, 0xcd, 0xf2, 0xef, 0x00, 0x00, 0x00,
}

4. 创建客户端和服务端代码

服务端代码主要做两方面的工作:

  • 实现上一步骤.proto生成的服务端接口。
  • 运行一个gRPC服务来监听客户端的请求并且把请求分发到正确的服务端实现里。

在grpc-hello中建立server文件夹并新建main.go,完成服务文件中的SayHello接口定义

服务器端的代码如下:

/*
 *
 * Copyright 2015 gRPC authors.
 *
 * 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.
 *
 */

//go:generate protoc -I ../helloworld --go_out=plugins=grpc:../helloworld ../helloworld/helloworld.proto

package main

import (
	"context"
	"log"
	"net"

	"google.golang.org/grpc"
	pb "google.golang.org/grpc/examples/helloworld/helloworld"
	"google.golang.org/grpc/reflection"
)

// 服务端监听的端口
const (
	port = ":50051"
)

// server is used to implement helloworld.GreeterServer.
type server struct{}

// 实现method
// SayHello implements helloworld.GreeterServer
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
	log.Printf("Received: %v", in.Name)
	return &pb.HelloReply{Message: "Hello " + in.Name}, nil
}

func main() {
	lis, err := net.Listen("tcp", port)
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}
	s := grpc.NewServer()
	pb.RegisterGreeterServer(s, &server{})
	// Register reflection service on gRPC server.
	reflection.Register(s)
	if err := s.Serve(lis); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}

类似地,在grpc-hello中建立client文件夹并新建main.go。

/*
 *
 * Copyright 2015 gRPC authors.
 *
 * 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.
 *
 */

 package main

 import (
	 "context"
	 "log"
	 "os"
	 "time"
 
	 "google.golang.org/grpc"
	 pb "google.golang.org/grpc/examples/helloworld/helloworld"
 )
 // 客户端请求连接的地址和端口
 const (
	 address     = "localhost:50051"
	 defaultName = "world"
 )
 
 func main() {
	 // Set up a connection to the server.
	 conn, err := grpc.Dial(address, grpc.WithInsecure())
	 if err != nil {
		 log.Fatalf("did not connect: %v", err)
	 }
	 defer conn.Close()
	 c := pb.NewGreeterClient(conn)
 
	 // Contact the server and print out its response.
	 name := defaultName
	 if len(os.Args) > 1 {
		 name = os.Args[1]
	 }
	 ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	 defer cancel()
	 r, err := c.SayHello(ctx, &pb.HelloRequest{Name: name})
	 if err != nil {
		 log.Fatalf("could not greet: %v", err)
	 }
	 log.Printf("Greeting: %s", r.Message)
 }

5. 测试运行

cd $GOPATH/src/github.com/hansenbeast/grpc-hello/
go run server/main.go
# 新建终端
cd $GOPATH/src/github.com/hansenbeast/grpc-hello/
go run client/main.go

文件结构

1

运行结果

1

1

总结

运行一个简单的gRPC列子,需要有下面三个步骤:

  • 使用一个.proto文件定义服务(定义了消息体和服务接口)
  • 使用protocol buffer编译器去编译.proto文件并且去生成客户端和服务端代码
  • 编写自己的客户端和服务端(服务器端额外编写接口实现)。