一. 安装
1.1 gRPC简介
- gRPC由google开发,是一款语言中立,平台中立,开源的远程过程调用系统。
- gRPC客户端和服务器可以在多种环境中运行和交互,例如用java写一个服务器端,可以用go语言写客户端调用。
1.2 gRPC与Protobuf介绍
- 微服务架构中,由于每个服务对应的代码库是独立运行的,无法直接调用,彼此之间的通信就是一个大问题。
- gRPC可以实现微服务,将大的项目拆分为多个小且独立的业务模块,也就是服务,各个服务将使用高效的protobuf协议进行RPC调用,gRPC默认使用protocol buffers,这个是google开源的一套成熟的结构数据序列化机制,当然也可以使用其它数据结构如JSON。
- 可以用proto files创建gRPC服务,用message类型来定义方法参数和返回类型。
1.3 安装gRPC和Protobuf
- go get google.golang.org/protobuf/proto 安装proto
- go get google.golang.org/grpc 安装grpc
- go get google.golang.org/protobuf/cmd/protoc-gen-go 安装好后会在$GOPATH/bin目录下生成protoc-gen-go.exe
- 但还需要一个protoc.exe,windows平台编译受限,很难自己手动编译,直接去网站下载:https://github.com/protocolbuffers/protobuf/releases/tag/v3.9.0
分享一个下载好的:
https://pan.baidu.com/s/1T8eJkHib2uPL3gNMCRdZmQ 提取码:s42z
二. gRPC简介
gRPC是一个高性能,开源,通用的RPC框架,由Google推出,基于HTTP2协议标准设计开发,默认采用Protocol Buffers数据序列化协议,支持多种开发语言。gRPC提供了一种简单的方法来精确的定义服务,并且为客户端和服务器自动生成可靠的功能库。
在gRPC客户端可以直接调用不同服务器器上的远程程序,使用姿势看起来就像调用本地程序一样,很容易去构建分布式应用和服务。和很多RPC系统一样,服务器负责实现定义好的接口并处理客户端的请求,客户端根据接口描述直接调用需要的服务。客户端和服务端可以分别使用gRPC支持的不同语言实现。
2.1 主要特性
- 强大的IDL
gRPC使用ProtoBuf来定义服务,ProtoBuf是由Google开发的一种数据序列化协议(类似XML,JSON,hessian)。ProtoBuf能够将数据进行序列化,并广泛应用在数据存储,通信协议等方面。
- 多语言支持
gRPC支持多种语言,能够基于语言自动生成客户端和服务端功能库。目前已提供了C版本grpc,Java版本grpc-java和Go版本grpc-go,其它语言的版本正在积极开发中,其中,grpc支持c,c++,Node.js,Python,Ruby,Objective-C,PHP和C#等语言,grpc-java已经支持Android开发。
- HTTP2
gRPC基于HTTP2标准设计,所以相对于其他RPC框架,gRPC带来了更多强大的功能,如双向流,头部压缩,多复用请求等。这些功能给移动设备带来重大益处,如节省带宽,降低TCP链接次数,节省CPU使用和延长电池寿命等。同时,gRPC还能提高了云端服务和web应用性能。gRPC即能够在客户端应用,也能够在服务器应用,从而以透明的方式实现客户端和服务端的通信和简化通信系统的构建。
三. ProtoBuf与Go转换
Proto文件
syntax = "proto3";
package test;
option go_package="test1";message Test {int32 age = 1;int64 count = 2;double money = 3;float score = 4;string name = 5;bool fat = 6;bytes char = 7;//Status 枚举enum Status {OK = 0;FAIL = 1;}Status status = 8;//Child子结构message Child {string sex = 1;}Child child = 9;map<string, string> dict = 10;
}
通过protoc生成对应语言功能代码:
protoc --go_out=./ test.proto
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: test.protopackage test1import (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//Status 枚举
type Test_Status int32const (Test_OK Test_Status = 0Test_FAIL Test_Status = 1
)var Test_Status_name = map[int32]string{0: "OK",1: "FAIL",
}var Test_Status_value = map[string]int32{"OK": 0,"FAIL": 1,
}func (x Test_Status) String() string {return proto.EnumName(Test_Status_name, int32(x))
}func (Test_Status) EnumDescriptor() ([]byte, []int) {return fileDescriptor_c161fcfdc0c3ff1e, []int{0, 0}
}type Test struct {Age int32 `protobuf:"varint,1,opt,name=age,proto3" json:"age,omitempty"`Count int64 `protobuf:"varint,2,opt,name=count,proto3" json:"count,omitempty"`Money float64 `protobuf:"fixed64,3,opt,name=money,proto3" json:"money,omitempty"`Score float32 `protobuf:"fixed32,4,opt,name=score,proto3" json:"score,omitempty"`Name string `protobuf:"bytes,5,opt,name=name,proto3" json:"name,omitempty"`Fat bool `protobuf:"varint,6,opt,name=fat,proto3" json:"fat,omitempty"`Char []byte `protobuf:"bytes,7,opt,name=char,proto3" json:"char,omitempty"`Status Test_Status `protobuf:"varint,8,opt,name=status,proto3,enum=test.Test_Status" json:"status,omitempty"`Child *Test_Child `protobuf:"bytes,9,opt,name=child,proto3" json:"child,omitempty"`Dict map[string]string `protobuf:"bytes,10,rep,name=dict,proto3" json:"dict,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`XXX_NoUnkeyedLiteral struct{} `json:"-"`XXX_unrecognized []byte `json:"-"`XXX_sizecache int32 `json:"-"`
}func (m *Test) Reset() { *m = Test{} }
func (m *Test) String() string { return proto.CompactTextString(m) }
func (*Test) ProtoMessage() {}
func (*Test) Descriptor() ([]byte, []int) {return fileDescriptor_c161fcfdc0c3ff1e, []int{0}
}func (m *Test) XXX_Unmarshal(b []byte) error {return xxx_messageInfo_Test.Unmarshal(m, b)
}
func (m *Test) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {return xxx_messageInfo_Test.Marshal(b, m, deterministic)
}
func (m *Test) XXX_Merge(src proto.Message) {xxx_messageInfo_Test.Merge(m, src)
}
func (m *Test) XXX_Size() int {return xxx_messageInfo_Test.Size(m)
}
func (m *Test) XXX_DiscardUnknown() {xxx_messageInfo_Test.DiscardUnknown(m)
}var xxx_messageInfo_Test proto.InternalMessageInfofunc (m *Test) GetAge() int32 {if m != nil {return m.Age}return 0
}func (m *Test) GetCount() int64 {if m != nil {return m.Count}return 0
}func (m *Test) GetMoney() float64 {if m != nil {return m.Money}return 0
}func (m *Test) GetScore() float32 {if m != nil {return m.Score}return 0
}func (m *Test) GetName() string {if m != nil {return m.Name}return ""
}func (m *Test) GetFat() bool {if m != nil {return m.Fat}return false
}func (m *Test) GetChar() []byte {if m != nil {return m.Char}return nil
}func (m *Test) GetStatus() Test_Status {if m != nil {return m.Status}return Test_OK
}func (m *Test) GetChild() *Test_Child {if m != nil {return m.Child}return nil
}func (m *Test) GetDict() map[string]string {if m != nil {return m.Dict}return nil
}//Child子结构
type Test_Child struct {Sex string `protobuf:"bytes,1,opt,name=sex,proto3" json:"sex,omitempty"`XXX_NoUnkeyedLiteral struct{} `json:"-"`XXX_unrecognized []byte `json:"-"`XXX_sizecache int32 `json:"-"`
}func (m *Test_Child) Reset() { *m = Test_Child{} }
func (m *Test_Child) String() string { return proto.CompactTextString(m) }
func (*Test_Child) ProtoMessage() {}
func (*Test_Child) Descriptor() ([]byte, []int) {return fileDescriptor_c161fcfdc0c3ff1e, []int{0, 0}
}func (m *Test_Child) XXX_Unmarshal(b []byte) error {return xxx_messageInfo_Test_Child.Unmarshal(m, b)
}
func (m *Test_Child) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {return xxx_messageInfo_Test_Child.Marshal(b, m, deterministic)
}
func (m *Test_Child) XXX_Merge(src proto.Message) {xxx_messageInfo_Test_Child.Merge(m, src)
}
func (m *Test_Child) XXX_Size() int {return xxx_messageInfo_Test_Child.Size(m)
}
func (m *Test_Child) XXX_DiscardUnknown() {xxx_messageInfo_Test_Child.DiscardUnknown(m)
}var xxx_messageInfo_Test_Child proto.InternalMessageInfofunc (m *Test_Child) GetSex() string {if m != nil {return m.Sex}return ""
}func init() {proto.RegisterEnum("test.Test_Status", Test_Status_name, Test_Status_value)proto.RegisterType((*Test)(nil), "test.Test")proto.RegisterMapType((map[string]string)(nil), "test.Test.DictEntry")proto.RegisterType((*Test_Child)(nil), "test.Test.Child")
}func init() { proto.RegisterFile("test.proto", fileDescriptor_c161fcfdc0c3ff1e) }var fileDescriptor_c161fcfdc0c3ff1e = []byte{// 303 bytes of a gzipped FileDescriptorProto0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x4c, 0x91, 0xcd, 0x4b, 0xc3, 0x40,0x10, 0xc5, 0x9d, 0x7c, 0xb5, 0x99, 0x8a, 0xc4, 0xa5, 0x87, 0xb5, 0xa7, 0xa5, 0x07, 0x59, 0x2f,0x05, 0xeb, 0x41, 0xf1, 0xe6, 0x27, 0x88, 0x82, 0x30, 0x7a, 0xf2, 0x16, 0xd3, 0xd5, 0x96, 0xb6,0x89, 0x64, 0xb7, 0x62, 0x8e, 0xfe, 0xe7, 0x32, 0xbb, 0x2a, 0xbd, 0xbd, 0x37, 0xef, 0x17, 0xf2,0x78, 0x8b, 0xe8, 0x8c, 0x75, 0x93, 0x8f, 0xb6, 0x71, 0x8d, 0x48, 0x58, 0x8f, 0xbf, 0x63, 0x4c,0x9e, 0x8d, 0x75, 0xa2, 0xc0, 0xb8, 0x7c, 0x37, 0x12, 0x14, 0xe8, 0x94, 0x58, 0x8a, 0x21, 0xa6,0x55, 0xb3, 0xa9, 0x9d, 0x8c, 0x14, 0xe8, 0x98, 0x82, 0xe1, 0xeb, 0xba, 0xa9, 0x4d, 0x27, 0x63,0x05, 0x1a, 0x28, 0x18, 0xbe, 0xda, 0xaa, 0x69, 0x8d, 0x4c, 0x14, 0xe8, 0x88, 0x82, 0x11, 0x02,0x93, 0xba, 0x5c, 0x1b, 0x99, 0x2a, 0xd0, 0x39, 0x79, 0xcd, 0xff, 0x79, 0x2b, 0x9d, 0xcc, 0x14,0xe8, 0x3e, 0xb1, 0x64, 0xaa, 0x9a, 0x97, 0xad, 0xec, 0x29, 0xd0, 0xbb, 0xe4, 0xb5, 0x38, 0xc2,0xcc, 0xba, 0xd2, 0x6d, 0xac, 0xec, 0x2b, 0xd0, 0x7b, 0xd3, 0xfd, 0x89, 0x6f, 0xce, 0x4d, 0x27,0x4f, 0x3e, 0xa0, 0x5f, 0x40, 0x1c, 0x62, 0x5a, 0xcd, 0x17, 0xab, 0x99, 0xcc, 0x15, 0xe8, 0xc1,0xb4, 0xd8, 0x22, 0xaf, 0xf8, 0x4e, 0x21, 0x16, 0x1a, 0x93, 0xd9, 0xa2, 0x72, 0x12, 0x55, 0xac,0x07, 0xd3, 0xe1, 0x16, 0x76, 0xbd, 0xa8, 0xdc, 0x4d, 0xed, 0xda, 0x8e, 0x3c, 0x31, 0x3a, 0xc0,0xd4, 0x7f, 0xc9, 0x5d, 0xad, 0xf9, 0xf2, 0x9b, 0xe4, 0xc4, 0x72, 0x74, 0x8a, 0xf9, 0x3f, 0xcd,0xf1, 0xd2, 0x74, 0x7f, 0xf1, 0x32, 0xcc, 0xf0, 0x59, 0xae, 0x36, 0xc6, 0x4f, 0x96, 0x53, 0x30,0xe7, 0xd1, 0x19, 0x8c, 0x47, 0x98, 0x85, 0xde, 0x22, 0xc3, 0xe8, 0xf1, 0xbe, 0xd8, 0x11, 0x7d,0x4c, 0x6e, 0x2f, 0xee, 0x1e, 0x0a, 0xb8, 0xec, 0xbd, 0xa4, 0x5c, 0xe6, 0xf8, 0x35, 0xf3, 0x2f,0x73, 0xf2, 0x13, 0x00, 0x00, 0xff, 0xff, 0x80, 0xdf, 0xd8, 0xf5, 0xa7, 0x01, 0x00, 0x00,
}
3.1 syntax
使用protoc的版本:
syntax = "proto3";
3.2 Package
在proto文件中使用package关键字声明包名,默认go语言中的包名,如果需要指定其它的包名,可以使用go_package选项:
package test;
option go_package="test1";
3.3 Message
proto中的message对应go中的struct,全部使用驼峰命名规则。嵌套定义的message,enum转换为go之后,名称变为Parent_Child结构。
如下:
除了会生成对应的结构外,还会有些工具方法。
枚举会生成对应名称的常量,同时会有两个map方便使用。
3.4 Service
定义一个简单的Service,TestService有一个方法Test,接收一个Request参数,返回Response:
service TestService
{rpc Test(Request) returns(Response){};
}message Request
{string name = 1;
}message Response
{string message = 1;
}
执行命令:
protoc --go_out=plugins=grpc:. test.proto
生成的go代码中包含该Service定义的接口,客户端接口已经自动实现了,直接提供客户端使用者调用,服务端接口需要由服务提供方实现。
// TestServiceClient is the client API for TestService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
type TestServiceClient interface {Test(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Response, error)
}type testServiceClient struct {cc *grpc.ClientConn
}func NewTestServiceClient(cc *grpc.ClientConn) TestServiceClient {return &testServiceClient{cc}
}func (c *testServiceClient) Test(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Response, error) {out := new(Response)err := c.cc.Invoke(ctx, "/test.TestService/Test", in, out, opts...)if err != nil {return nil, err}return out, nil
}// TestServiceServer is the server API for TestService service.
type TestServiceServer interface {Test(context.Context, *Request) (*Response, error)
}
四. Protobuf语法
4.1 基本规范
- 文件以.proto作为文件后缀,除结构体之外的语句以分号结尾
- 结构定义可以包含: message,service,enum
- rpc方法定义结尾的分号可有可无
- message命名采用驼峰式命名方式,字段命名采用小写字母加下划线分隔方式
message SongServerRequest
{required string song_name = 1;
}
- enum采用驼峰命名方式,字段命名采用大写字母加下划线分隔方式
enum Foo
{FIRST_VALUE = 1;SECOND_VALUE = 2;
}
- service与rpc方法名统一采用驼峰式命名
4.2 字段规则
- 字段格式:限定修饰符 | 数据类型 | 字段名称 = 字段编码值 | [字段默认值]
- 限定修饰符包含 required\optional\repeated
- required:表示必须字段,必须相对于发送方,在发送消息之前必须设置该字段的值,对于接收方,必须能够识别该字段的意思。发送之前没有设置required字段或者无法识别required字段都会引发编解码异常,导致消息被丢弃
- optional:表示是一个可选字段,可选对于发送方,在发送该消息时,可以有选择性的设置或者不设置该字段的值。对于接收方,如果能够识别可选字段就进行相应处理,如果无法识别,则忽略该字段,消息中其它字段正常处理。——因为optional字段的特性,很多接口在升级版本中都把后来添加的字段都统一的设置为optional字段,这样老版本无需升级程序也可以正常与新的软件进行通信,只不过新字段无法识别而已,因为并不是每一个节点都需要新的功能,因此可以做到按需升级和平滑过渡。
- repeated:表示该字段可以包含0~N个元素。其特性和optional一样,但是每一次可以包含多个值。可以看作时在传递一个数组的值。
- 数据类型
- protobuf定义了一套基本数据类型。几乎都可以映射到C++\JAVA等语言的基础数据类型。
+N表示打包字节不固定,而是根据数据大小和长度。
关于fixed32和int32的区别:fixed32打包效率比int32的效率高,但是使用空间一般比int32多。因此一个时间效率高,一个属于空间效率高。
- 字段名称
- 字段名称的命名与C,C++,Java等语言的变量命名方式几乎相同
- protobuf建议字段的命名采用以下划线分隔的驼峰式。例如first_name而不是firstName
- 字段编码值
- 有了该值,通信双方才能互相识别对方的字段,相同的编码值,其限定修饰符和数据类型必须相同,编码值的取值范围为1~2^32
- 其中1~15的编码时间和空间效率是最高的,编码值越大,其编码时间和空间效率就越低,所以建议把经常要传递的值的字段编码设置在1~15之间的值。
- 1900~2000编码值为Google protobuf系统内部保留值,建议不要在自己的项目中使用
- 字段默认值
- 当在传递数据时,对于required数据类型,如果用户没有设置值,则使用默认值传递到对端
4.3 service如何定义
- 如果想要将消息类型用在RPC系统中,可以在.proto文件中定义一个RPC服务接口,protocol buffer编译器会根据所选择的不同语言生成服务接口代码
- 例如:想要定义一个RPC服务并具有一个方法,该方法接收SearchRequest并返回一个SearchResponse,此时可以在.proro文件中进行如下定义:
service SearchService
{rpc Search(SearchRequest) returns (SearchResponse){};
}
- 生成的接口代码作为客户端与服务端的约定,服务端必须实现定义的所有接口方法,客户端直接调用同名方法向服务器发起请求,比较麻烦的是,即便业务上不需要参数也必须指定一个请求消息,一般会定义一个空message
4.4 message如何定义
- 一个message类型定义描述了一个请求或响应的消息格式,可以包含多种类型字段
- 字段名用小写,转为go文件后自动变为大写,message就相当于结构体
- 每个字段声明以分号结尾,.proto文件支持双斜杠(//)添加注释
syntax = "proto3"
message SearchRequest
{string query = 1;int32 page_number = 2;int32 result_per_page = 3;
}
- 添加多个message类型,一个.proto文件中可以定义多个消息类型,一般用于同时定义多个相关信息
- message支持嵌套使用,作为另一message中的字段类型
message SearchResponse
{repeated Result results = 1;
}message Result
{string url = 1;string title = 2;repeated string snippets = 3;
}
- 支持嵌套消息,消息可以包含另一个消息作为字段。也可以在消息内定义一个新的消息
- 内部嵌套的message类型名称只可在内部直接使用
syntax = "proto3";
package test;message A
{message B{int32 i = 1;int32 j = 2;}B b = 1;
}message C
{//B b = 1; 报错test.proto:17:9: "B" is not defined.
}
- 另外,还可以多层嵌套
4.5 proto3的Map类型
- proto3支持map类型声明
syntax = "proto3";
package test;message C
{}message A
{message B{}B b = 1;map<int32, C> m = 2;
}
- 键,值可以是内置类型,也可以是自定义message类型
- 字段不支持repeated属性
4.6 proto文件编译
- 通过定义好的.proto文件生成Java,Python,C++,Go,Ruby,JavaNano, Objective-C,orC#代码,需要安装编译器protoc
- 当使用protocol buffer编译器运行.proto文件时,编译器将生成所选语言的代码,用于使用在.proto文件中定义的消息类型、服务接口约定等。不同语言生成的代码格式不同:
- C++:每个.proto文件生成一个.h文件和一个.cc文件,每个消息类型对应一个类
- Java:生成一个.java文件,同样每个消息对应一个类,同时还有一个特殊的Builder类用于创建消息接口
- Python:姿势不太一样,每个.proto文件中的消息类型生成一个含有静态描述符的模块,该模块与一个元类metaclass在运行时创建需要的Python数据访问类
- Go:生成一个.pb.go文件,每个消息类型对应一个结构体
- Ruby:生成一个.rb文件的Ruby模块,包含所有消息类型
- JavaNano:类似Java,但不包含Builder类
- Objective-C:每个.proto文件生成一个pbobjc.h和一个pbobjc.m文件
- C#:生成.cs文件包含,每个消息类型对应一个类
protobuf在linux下载编译和使用_protobuf下载-CSDN博客
4.7 import导入定义
- 可以使用import语句导入使用其它描述文件中声明的类型
- protobuf接口文件可以像C语言的.h文件一个,分离为多个,在需要的时候通过import导入需要的文件。其行为和C语言的include大致相同。例如:import "others.proto"
- protocol buffer 编译器会在-I 或 -proto_path参数指定的目录中查找导入的文件,如果没有指定该参数,默认在当前目录中查找。
注意:import导入使用其它描述文件中的类型,需要两个proto文件的包名相同。
例子:
- 在不同的语言中,包名定义对编译后生成的代码的影响不
- C++ 中:对应C++命名空间,例如Open会在命名空间foo::bar中
- Java 中:package会作为Java包名,除非指定了option jave_package选项
- Python 中:package被忽略
- Go 中:默认使用package名作为包名,除非指定了option go_package选项
- JavaNano 中:同Java
- C# 中:package会转换为驼峰式命名空间,如Foo.Bar,除非指定了option csharp_namespace选项
4.8 小案例
利用gRPC实现一个Hello Service,客户端发送包含字符串名字的请求,服务端返回hello消息。
流程:
- 编写.proto描述文件
- 编译成.pb.go文件
- 服务端实现约定的接口并提供服务
- 客户端按照约定调用.pb.go文件中的方法请求服务
项目结构:
- 步骤1:编写描述文件:
hello.proto文件中定义了一个Hello Service,该服务包含一个SayHello方法,同时声明了HelloRequest和HelloResponse消息结构用作请求和响应。客户端使用HelloRequest参数调用SayHello方法请求服务端,服务端响应HelloResponse消息。一个简单的服务就定义好了。
syntax="proto3";
package hello;
option go_package="hello";service Hello
{rpc SayHello(HelloRequest)returns(HelloResponse){};
}message HelloRequest
{string name = 1;
}message HelloResponse
{string message = 1;
}
- 步骤2:生成.pb.go文件
按照.proto文件中的说明,包含HelloServer描述,客户端接口及实现HelloClient,以及HelloRequest,HelloResponse结构体。
protoc --go_out=plugins=grpc:.\grpc\proto -I=.\grpc\proto .\grpc\proto\hello.proto
- 步骤3:编写服务器
服务端定义了一个空结构来实现了HelloServer接口,实例化gRPC server并注册,开始提供服务。
package mainimport ("context""fmt""net"hello "sample-app/grpc/proto""google.golang.org/grpc"
)var Addr = "127.0.0.1:8080"type helloService struct{}var HelloService = helloService{}func (h helloService) SayHello(c context.Context, req *hello.HelloRequest) (*hello.HelloResponse, error) {resp := new(hello.HelloResponse)resp.Message = fmt.Sprintf("Hello %s", req.Name)return resp, nil
}func main() {ls, err := net.Listen("tcp", Addr)if err != nil {fmt.Println(err)return}//新建一个grpc服务器server := grpc.NewServer()//注册HelloServicehello.RegisterHelloServer(server, HelloService)fmt.Println("Listen on" + Addr)server.Serve(ls)
}
- 步骤4:编译客户端
客户端只需要初始化连接,之后调用.pb.go中的SayHello方法,即可向服务端发送请求。使用的姿势就像调用本地接口一样。
package mainimport ("context""fmt"hello "sample-app/grpc/proto""google.golang.org/grpc"
)const (Addr = "127.0.0.1:8080"
)func main() {conn, err := grpc.Dial(Addr, grpc.WithInsecure())if err != nil {fmt.Println(err)return}defer conn.Close()c := hello.NewHelloClient(conn)req := new(hello.HelloRequest)req.Name = "gRPC"resp, err := c.SayHello(context.Background(), req)if err != nil {fmt.Println(err)return}fmt.Println(resp.Message)
}
- 运行:
编译客户端和服务器代码,先启动服务器,在启动客户端发送请求。