RPC
RPC是远程过程调用的简称,是分布式系统中不同节点间流行的通信方式。
go语言标准库提供了简单RPC的实现,包路径为net/rpc
RPC-hello world
构造一个HelloService类型,其中的Hello方法用于实现打印功能
type HelloService struct {}
func (p *HelloService) Hello(request string, reply *string) error {
*reply = "hello:" + request
return nil
}
Hello方法必须满足Go语言的RPC规则
RPC规则
方法只能有两个可序列化的参数,其中第二个参数是指针类型,并且返回一个error类型,同时必须是公开的方法。
然后就可以将HelloService类型的对象注册为一个RPC服务
func main() {
rpc.RegisterName("HelloService", new(HelloService))
listener, err := net.Listen("tcp", ":1234")
if err != nil {
log.Fatal("ListenTCP error:", err)
}
conn, err := listener.Accept()
if err != nil {
log.Fatal("Accept error:", err)
}
rpc.ServeConn(conn)
}
客户端请求HelloService服务
func main() {
client, err := rpc.Dial("tcp", "localhost:1234")
if err != nil {
log.Fatal("dialing:", err)
}
var reply string
err = client.Call("HelloService.Hello", "hello", &reply)
if err != nil {
log.Fatal(err)
}
fmt.Println(reply)
}
RPC规范的设计
在涉及RPC的应用中,开发人员一般分为三种角色
- 服务端实现RPC方法的开发人员
- 客户端调用RPC方法的人员
- 制定服务端和客户端RPC接口规范的设计人员
接下来重构HelloService服务
第1步,明确服务名字和接口
将RPC服务的接口规范分为三个部分
- 服务的名字
- 服务要实现的详细方法列表
- 注册该类型服务的函数
const HelloServiceName = "path/to/pkg.HelloService"
type HelloServiceInterface interface {
Hello(request string, reply *string) error
}
func RegisterHelloService(svc HelloServiceInterface) error {
return rpc.RegisterName(HelloServiceName, svc)
}
第2步,客户端根据规范编写RPC调用的代码
type HelloServiceClient struct {
*rpc.Client
}
var _ HelloServiceInterface = (*HelloServiceClient)(nil)
func DialHelloService(network, address string) (*HelloServiceClient, error) {
c, err := rpc.Dial(network, address)
if err != nil {
return nil, err
}
return &HelloServiceClient{Client: c}, nil
}
func (p *HelloServiceClient) Hello(request string, reply *string) error {
return p.Client.Call(HelloServiceName+".Hello", request, reply)
}
func main() {
client, err := DialHelloService("tcp", "localhost:1234")
if err != nil {
log.Fatal("dialing:", err)
}
var reply string
err = client.Hello("hello", &reply)
if err != nil {
log.Fatal(err)
}
}
第3步,基于RPC接口规范编写真实的服务端代码
type HelloService struct {}
func (p *HelloService) Hello(request string, reply *string) error {
*reply = "hello:" + request
return nil
}
func main() {
RegisterHelloService(new(HelloService))
listener, err := net.Listen("tcp", ":1234")
if err != nil {
log.Fatal("ListenTCP error:", err)
}
for {
conn, err := listener.Accept()
if err != nil {
log.Fatal("Accept error:", err)
}
go rpc.ServeConn(conn)
}
}
跨语言的RPC
通信对端不一定都使用go作为rpc通信的语言,所以go支持将通信数据转成json格式的数据,发送时转成json格式数据,对端接收时从json数据提取
Protobuf
可以生成代码,并实现将结构化数据序列化的功能
Protobuf中最基本的数据单元是message,是类似Go语言中结构体的存在。
在message中可以嵌套message或其它的基础数据类型的成员。
在XML或JSON等数据描述语言中,一般通过成员的名字来绑定对应的数据。但是Protobuf编码却是通过成员的唯一编号来绑定对应的数据,因此Protobuf编码后数据的体积会比较小,但是也非常不便于人类查阅。
proto语法
-
message:用于定义消息类型
-
字段规则
- required:消息中必填字段,不设置会导致编码异常;在protobuf23中被删掉
- optional:可选字段,protobuf3没有了required和optional等关键字,默认为optional
- repeate:消息中可重复字段,重复的值的顺序会被保存成切片
-
消息号:在消息体的定义中,每个字段都必须有一个唯一标识符,是一个整数
-
嵌套消息:可以在其他消息类型中定义,就是message里套message
-
服务定义:如果想要将消息类型用在rpc中,需要在proto文件中定义rpc服务接口
gRPC
gRPC是Google公司开发的跨语言RPC框架,基于HTTP/2协议设计,使用protobuf(protocol buffers)作为其接口定义语言,同时底层消息交换格式也使用protobuf
gRPC示例
protobuf安装
1.下载protocol buffers编译器
下载连接https://github.com/protocolbuffers/protobuf/releases
这里下载21.9版本,下载后将bin目录添加到环境变量,命令行protoc测试是否配置成功
2.安装gRPC核心库
在go项目的IDE的terminal里执行即可
go get google.golang.org/grpc
3.除了编译器还需要安装转换成go的工具,github的protoc-gen-go是旧版本,新版本是google的protoc-gen-go
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
下载完后在GOPATH目录下会出现相应的工具protoc-gen-go-grpc和protoc-gen-go,到此环境准备完毕
proto文件编写
编写server的hello.proto
这个.proto其实就是一个接口文档,用来定义约束
//bproto版本
syntax = "proto3";
//规定生成的go文件存放位置(.代表当前目录; service代表生成的文件包名是service)
option go_package = ".;service";
//定义一个服务service名为SayHello,接收HelloRequest,返回HelloResponse
service SayHello{
rpc SayHello(HelloRequest) returns (HelloResponse){}
}
message HelloRequest{
string requestName=1;
}
message HelloResponse{
string responseMsg=1;
}
在proto目录下执行命令,就会在同目录下生成两个go文件,这三个文件都拷贝到client项目的相同位置
protoc --go_out=. hello.proto # 生成request和response等消息格式文件
protoc --go-grpc_out=. hello.proto # 生成服务端和客户端可调用的包文件
编写服务端步骤
- 创建grpc server对象,可以理解为server端的抽象对象
- 将server(其中包含需要被调用的服务端接口)注册到grpc server的内部注册中心
- 创建listen,监听tcp端口
- grpc server开始listen。Accept,直到Stop
编写客户端步骤
- 创建和给定目标服务器的连接交互
- 创建server的客户端对象
- 发送rpc请求,等待同步响应,得到回调后返回响应结果
- 输出响应结果
编写服务端
编辑server项目main.go
package main
import (
"fmt"
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
pb "grpcProject/hello-server/proto"
"net"
)
type server struct {
pb.UnimplementedSayHelloServer
}
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
return &pb.HelloResponse{ResponseMsg: "Hello " + in.RequestName}, nil
}
func main() {
// 监听本地的8972端口
listen, err := net.Listen("tcp", ":9090")
if err != nil {
fmt.Printf("failed to listen: %v", err)
return
}
grpcServer := grpc.NewServer() // 创建gRPC服务器
pb.RegisterSayHelloServer(grpcServer, &server{}) // 在gRPC服务端注册服务
reflection.Register(grpcServer) //在给定的gRPC服务器上注册服务器反射服务
// Serve方法在lis上接受传入连接,为每个连接创建一个ServerTransport和server的goroutine。
// 该goroutine读取gRPC请求,然后调用已注册的处理程序来响应它们。
err = grpcServer.Serve(listen)
if err != nil {
fmt.Printf("failed to serve: %v", err)
return
}
}
编写客户端
编写client项目的main.go
package main
import (
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
pb "grpcProject/hello-server/proto"
"log"
)
func main() {
//连接server端,不使用安全传输
conn, err := grpc.Dial("127.0.0.1:9090", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("connect fail: %v", err)
}
defer conn.Close()
//建立连接
client := pb.NewSayHelloClient(conn)
//执行rpc调用,这个方法在服务器端实现并返回结果
resp, _ := client.SayHello(context.Background(), &pb.HelloRequest{RequestName: "ahahahahah"})
fmt.Println(resp.GetResponseMsg())
}