介绍
rpc是一种远程调用协议,在微服务框架中经常使用。其内部也是走的tcp协议,在数据传输过程中也根据数据序列化的不同分化不同数据传输的api比如jsonrpc,grpc。
grpc是使用protocol buffers作为接口定义语言(IDL)和底层数据交换的格式。它可以通过可插拔的支持来有效地连接数据中心内和跨数据中心的服务,以实现负载平衡,跟踪,运行状况检查和身份验证。另外,通过protobuf可以将数据序列化为二进制编码,这会大幅减少需要传输的数据量,从而大幅提高性能
protoc
用protoc生成grpc服务端的代码protocol buffers 在上一节已经介绍了安装教程。
然后编写test.proto文件用来定义服务以及服务中的方法。这里的实例也是对照 golan中文网
//语法声明 新版本就是proto3 老版本有proto2 proto1
syntax = "proto3";
package pd; //包名一定要
// 定以 Greeter 微服务
service Greeter {
// 定义sanHello方法
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
// Xiukangr 微服务 (这里定义的服务会生成同名的接口 XiukangrServer,在业务代码中需要实现该接口,然后注册该服务)
service Xiukangr{
rpc SayXiukang(HelloRequest) returns (HelloReply) {}
rpc SayTest(HelloRequest) returns (HelloReply) {}
}
// HelloRequest 请求数据格式
message HelloRequest {
string name = 1;
}
// HelloReply 响应数据格式
message HelloReply {
string message = 1;
}
在以上的test.proto中定义了两个服务,每个服务中有相应方法,另外请求数据格式响应数据格式也已经定义好,在实际的过程中服务结构可能会更加的复杂。
定义好了就要生存相应的go文件了 ,protoc是真的强大,能生成go php c++等十几种语言的相应代码。
protoc -I=./ test.proto --go_out=plugins=grpc:.
# -I 是proto的输入文件路径 如果需要单独生成某个文件的相应代码需要单独填文件名,比如上面的test.proto
# 如果需要全部的文件那就 -I=./*
# --go_out 这是要输出生成的go服务文件路径 ,java_out那就是java的代码 其他语言依次类推。
# 因为本代码中有rpc的服务所以在输出时一定要带上prlugin=grpc 这句话,生成的代码中将不能实用该服务
上面的注释中有相应的重点需要铭记不然会有坑。
在运行中可能大部分人会遇到报错如下
这是因为go中缺少protoc-gen-to这个包,需要安装一下。
go get -u github.com/golang/protobuf/protoc-gen-go
- 如果下载很慢的话,需要修改go的代理或者实用go mod安装,
- 也可以直接通通过迅雷下载到本地然后使用 go install 安装一下就好。需要注意一下安装的路径
生成代码
生成的test.pd.go 代码如下
// source: test.proto
package pd
import (
context "context"
fmt "fmt"
proto "github.com/golang/protobuf/proto"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
math "math"
)
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_c161fcfdc0c3ff1e, []int{0}
}
// HelloReply 响应数据格式
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_c161fcfdc0c3ff1e, []int{1}
}
func init() {
proto.RegisterType((*HelloRequest)(nil), "pd.HelloRequest")
proto.RegisterType((*HelloReply)(nil), "pd.HelloReply")
}
func init() { proto.RegisterFile("test.proto", fileDescriptor_c161fcfdc0c3ff1e) }
var _ context.Context
var _ grpc.ClientConnInterface
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
const _ = grpc.SupportPackageIsVersion6
// GreeterClient is the client API for Greeter service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
type GreeterClient interface {
// Sends a greeting
SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error)
}
type greeterClient struct {
cc grpc.ClientConnInterface
}
func NewGreeterClient(cc grpc.ClientConnInterface) GreeterClient {
return &greeterClient{cc}
}
func (c *greeterClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) {
out := new(HelloReply)
err := c.cc.Invoke(ctx, "/pd.Greeter/SayHello", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// GreeterServer is the server API for Greeter service.
type GreeterServer interface {
// Sends a greeting
SayHello(context.Context, *HelloRequest) (*HelloReply, error)
}
func RegisterGreeterServer(s *grpc.Server, srv GreeterServer) {
s.RegisterService(&_Greeter_serviceDesc, srv)
}
核心的代码主要是生成的 Server和对应的Client相关的代码主要是注册服务,创建客服端相关,另外还有每个服务下的方法的注册和定义描述,剩下就是protobuf序列话相关的东西,代码很多就没有依次贴出来,读者需要细心阅读全部的代码
使用
上面已经生成了相应的服务端和客户端的代码,只需要在服务端直接使用相关方法和实现相应的接口即可。
服务
package main
import (
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
pd "grpc/pd"
"log"
"net"
)
const (
port = ":50051"
)
// 1.定义服务
type server struct{} //服务对象
type xiukangServer struct{} //服务对象
// 2.实现服务的方法
// SayHello 实现服务的接口 在proto中定义的所有服务都是接口
func (s *server) SayHello(ctx context.Context, in *pd.HelloRequest) (*pd.HelloReply, error) {
fmt.Println("request: ", in.Name)
return &pd.HelloReply{Message: "Hello " + in.Name}, nil
}
func (xiu *xiukangServer) SayXiukang(ctx context.Context, in *pd.HelloRequest) (*pd.HelloReply, error) {
fmt.Println("request: ", in.Name)
return &pd.HelloReply{Message: "Hello xiukang:" + in.Name}, nil
}
func main() {
lis, err := net.Listen("tcp", port)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer() //起一个服务
// 3.注册服务
pd.RegisterGreeterServer(s, &server{})
pd.RegisterXiukangrServer(s, &xiukangServer{})
// 注册反射服务 这个服务是CLI使用的 跟服务本身没有关系
reflection.Register(s)
log.Println("rpc服务已经开启 0.0.0.0:50051")
// 服务监听阻塞中若需要结合其他的服务(如 gin,beego)则需要将本断代码使用在协程中
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
上面代码的注释中已经讲了过程,首先需要启动一tcp服务,然后定义和实现test.proto中生成的服务接口,然后再注册服务,这里和jsonrpc的方式是一样的,每个服务都需要单独注册。
客服端
客服端代码 /client.go代码如下
/*
@Time : 2020/8/21 下午10:52
@Author : xiukang
@File : client
@Software: GoLand
*/
package grpc
import (
"context"
"ginweb/runtime"
"ginweb/service/grpc/pd"
"google.golang.org/grpc"
"log"
"os"
"time"
)
const (
address = "localhost:50051"
defaultName = "world"
)
func StartGrpc() {
// 1.建立链接
conn, err := grpc.Dial(address, grpc.WithInsecure())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
// 2.获取相应服务的连接
greeter := pd.NewGreeterClient(conn)
xiukang := pd.NewXiukangrClient(conn)
// Contact the server and print out its response.
name := defaultName
if len(os.Args) > 1 {
name = os.Args[1]
}
// 1秒的上下文
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
r, err := greeter.SayHello(ctx, &pd.HelloRequest{Name: name})
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", r.Message)
r, err = xiukang.SayXiukang(ctx, &pd.HelloRequest{Name: name})
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", r.Message)
runtime.Info.Println("grpc service start at 0.0.0.0:50051")
}
客户端代码需要提前获取每个service对应的链接 然后请求相应的方法,具体实现效果如下
具体的细节还请看细节代码,因为不知道还有什么其他的坑。
致谢
每次的学习和实践离不开各路大神的前车之鉴,才避免了很多的坑,向在代码中奋斗的人致敬。