likes
comments
collection
share

go 中 rpc 和 grpc 的使用

作者站长头像
站长
· 阅读数 19

RPC

RPC 是远程过程调用,是一个节点向请求另一个节点提供的服务,像调用本地函数一样去调用远程函数

远程过程调用有很多问题

  1. Call ID 映射:如何知道远程机器上的函数名 go 中 rpc 和 grpc 的使用
  2. 序列化和反序列化:怎么把参数传递给远程函数,如:json/xml/protobuf/msgpack
    • 客户端
      • 建立连接 tcp/http
      • 序列化
      • 向服务端发送数据 -> 这是二进制的数据
      • 等待服务端响应 -> 这是二进制数据
      • 反序列化
    • 服务端:
      • 监听端口
      • 读取客服端发送过来的数据 -> 这是二进制的数据
      • 反序列化
      • 处理业务逻辑
      • 将客户端需要的数据序列化
      • 返回给客户端 -> 这是二进制的数据
    • 在大型分布式系统中,使用 json 作为数据格式协议几乎不可维护 go 中 rpc 和 grpc 的使用
  3. 网络传输:如何进行网络传输,如:http/tcp
    • 对于 http 协议来说,它是一次性的,一旦对方有了结果,连接就断开了
    • http1.x 在微服务这块应用有性能问题
    • http2.0 可以解决这个问题 go 中 rpc 和 grpc 的使用

如果不使用 RPC 框架,如何实现远程调用呢?

利用 go 内置的 rpc 实现

go 内置 rpc 实现源码:源码

新建 server 包,提供一个 HelloService 服务,这个函数的作用传进来一个 string 类型的值 xxx,返回 hello, xxx

import (
  "net"
  "net/rpc"
)

type HelloService struct{}

func main() {
  // 监听 tcp 服务的 1234 端口
  listener, _ := net.Listen("tcp", ":1234")
  // 注册一个 HelloService 服务
  _ = rpc.RegisterName("HelloService", &HelloService{})

  for {
    //启动服务
    conn, _ := listener.Accept()
    rpc.ServeConn(conn)
  }
}

// Hello 方法
func (s *HelloService) Hello(request string, reply *string) error {
	*reply = "hello, " + request
	return nil
}

新建 client 包,用来调用 server 包中的 HelloServiceHello 方法

import (
  "fmt"
  "net/rpc"
)

func main() {
  // 与 server 建立连接
  conn, err := rpc.Dial("tcp", "localhost:1234")
  if err != nil {
    panic(err)
  }
  var require *string = new(string)
  // 调用 HelloService 的 Hello 方法,传入参数 uccs,返回值赋值给 require
  err = conn.Call("HelloService.Hello", "uccs", require)
  if err != nil {
    panic(err)
  }
  fmt.Println(*require)
}

我们可以看到在不使用任何框架前,我们写的 rpc 代码是非常冗余的:

  1. 我们需要自己去定义 HelloService
  2. server 端注册服务,启动服务
  3. client 端建立连接,调用方法,返回结果

这些是非常繁琐的,我们将这些步骤进行简化

利用 go 内置的 rpc 实现优化版本

利用 go 内置的 rpc 实现优化版本:源码

我们最先能够想到的优化点是将 HelloService 的定义提出来,放在一个单独的包中

新建包 handler

type HelloService struct{}

client

我们现在在调用 Hello 时,需要写成 conn.Call("HelloService.Hello", xxx, xxx),但我们想要的调用方式是 conn.Hello(xxx, xxx)

go 中一个变量不可能凭空多出 Hello 方法

我们怎么才能实现这种方法呢?

可以通过 go 的结构体来实现

新建包 client_proxy

type HelloServiceStub struct {
  *rpc.Client
}

// 建立连接,返回一个 HelloServiceStub
func NewHelloServiceClient(protocol string, address string) HelloServiceStub {
  conn, err := rpc.Dial(protocol, address)
  if err != nil {
    panic("连接失败")
  }
  return HelloServiceStub{
    conn,
  }
}

// 在结构体 HelloServiceStub 中定义 Hello 方法
func (c *HelloServiceStub) Hello(request string, reply *string) error {
  return c.Client.Call(handler.HelloServiceName+".Hello", request, reply)
}

client 中,我们调用就简单了,直接调用 NewHelloServiceClient 传入 protocoladdress

然后在返回的值中调用 Hello 方法

import (
  "fmt"
  "go-rpc/optimized-rpc/client_proxy"
)

func main() {
  client := client_proxy.NewHelloServiceClient("tcp", "localhost:1234")

  var require *string = new(string)
  err := client.Hello("uccs", require)
  if err != nil {
    panic(err)
  }
  fmt.Println(*require)
}

server

服务端要做的事情是将注册服务的函数提取出来

但这里有个问题:我们需要在注册服务时接受一个包含 Hello 方法的结构体

这个结构体是在 server 中定义的,但我们在 server_proxy 包中是无法引用的

这就可以使用接口来解决,就是下面定义的 HelloServicer 接口

新建包 server_proxy

import (
  "go-rpc/optimized-rpc/handler"
  "net/rpc"
)

type HelloServicer interface {
  Hello(request string, reply *string) error
}

func RegisterHelloService(srv *HelloServicers) error {
  return rpc.RegisterName(handler.HelloServiceName, srv)
}

server 中,我们调用 RegisterService 方法,传入 HelloService,就可以注册服务了

import (
  "go-rpc/optimized-rpc/handler"
  "go-rpc/optimized-rpc/server_proxy"
  "net"
  "net/rpc"
)

type HelloService struct{}

func main() {
  listener, _ := net.Listen("tcp", ":1234")
  _ = server_proxy.RegisterHelloService(&HelloService{})

  for {
    conn, _ := listener.Accept()
    go rpc.ServeConn(conn)
  }
}

func (s *HelloService) Hello(request string, reply *string) error {
  *reply = "hello, " + request
  return nil
}

最终它的结构如下图所示:

go 中 rpc 和 grpc 的使用

使用 grpc 重写 rpc

使用 grpc 重写 rpc源码

新建 proto

定义 rpc 类型的 SayHello

syntax = "proto3";

option go_package ="./;proto";

service Greeter{
  rpc SayHello(HelloRequest) returns (HelloReply); // hello 接口
}

message HelloRequest {
  string name = 1;
}

message HelloReply{
  string message = 1;
}

运行命令:

protoc --go_out=. --go-grpc_out=require_unimplemented_servers=false:. helloworld.proto

会生成两个文件:helloworld.pb.gohelloworld_grpc.pb.go

client

新建 client

使用 grpcserver 建立连接,然后就可以调用 SayHello 方法了

import (
  "context"
  "fmt"
  "go-rpc/grpc/proto"

  "google.golang.org/grpc"
)

func main() {
  conn, err := grpc.Dial("127.0.0.1:8080", grpc.WithInsecure())
  if err != nil {
    panic(err)
  }
  defer conn.Close()

  client := proto.NewGreeterClient(conn)
  r, err := client.SayHello(context.Background(), &proto.HelloRequest{Name: "uccs"})
  if err != nil {
    panic(err)
  }

  fmt.Println(r.Message)
}

server

新建 server

定义 SayHello 方法,入参:proto.HelloRequest 出参:proto.HelloReply,并启动服务

import (
  "context"
  "go-rpc/grpc/proto"
  "net"

  "google.golang.org/grpc"
)

type Server struct{}

func (s *Server) SayHello(ctx context.Context, request *proto.HelloRequest) (*proto.HelloReply, error) {
  return &proto.HelloReply{Message: "Hello " + request.Name}, nil
}

func main() {
  g := grpc.NewServer()
  proto.RegisterGreeterServer(g, &Server{})
  lis, err := net.Listen("tcp", ":8080")
  if err != nil {
    panic(err)
  }

  if err := g.Serve(lis); err != nil {
    panic(err)
  }
}

grpc

grpc 是谷歌开源的 rpc 框架,底层通信协议是 http2.0

使用 apt 安装

  1. linux 环境下,使用 apt 安装:
apt install -y protoc-gen-go protoc-gen-go-grpc

不需要额外安装 protoc,因为他们自带了 protoc

  1. 安装完成,检查版本
protoc --version
# libprotoc 3.21.12

protoc-gen-go --version
# protoc-gen-go v1.28.1

protoc-gen-go-grpc --version
# protoc-gen-go-grpc 1.0

使用 wget 远程下载

  1. 确认linux 系统版本(我这里是 x86_64)
uname -a
# Linux 667711fd2ac3 5.10.104-linuxkit #1 SMP Thu Mar 17 17:08:06 UTC 2022 x86_64 GNU/Linux
  1. 官方地址 中选择对应的版本下载并解压
# 下载
wget https://github.com/protocolbuffers/protobuf/releases/download/v23.3/protoc-23.3-linux-x86_64.zip

# 解压
unzip protoc-23.3-linux-x86_64.zip
  1. proto 放到环境变量弘
mv -f bin/proto /usr/local/bin
  1. 如果要使用它里面的类型,需要将 include 目录中的内容放到 /usr/local/include
mv -f include/google /usr/local/include
  1. 如果要使用 go 生成 proto 文件,需要安装 protoc-gen-goprotoc-gen-go-grpc
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.1
  1. 安装完成
protoc --version
# libprotoc 23.3

protoc-gen-go --version
# protoc-gen-go v1.31.0

protoc-gen-go-grpc --version
# protoc-gen-go-grpc v1.3.0

生成 go 文件

  1. 新建一个 helloWorld.proto 文件
syntax = "proto3";

// 在最新的 protoc 版本中,option 要像下面这样写,否则会报错
option go_package ="./;proto";

message HelloRequest {
  string name = 1;
}
  1. 在当前目录下执行命令,就会在 helloWorld.proto 同级目录下生成 helloWorld.pb.go 文件
protoc --go_out=. --go-grpc_out=require_unimplemented_servers=false:. helloWorld.proto

序列化和反序列化

go-rpc/protogrpc 生成的文件用来定义数据格式

google.golang.org/protobuf/proto 进行序列化和反序列化

import (
  proto2 "go-rpc/proto"

  "google.golang.org/protobuf/proto"
)

func main() {
  // 序列化
  req := proto2.HelloRequest{
    Name: "uccs",
  }
  rsp, _ := proto.Marshal(&req)
  fmt.Println(rsp)

  // 反序列化
  req2 := proto2.HelloRequest{}
  _ = proto.Unmarshal(rsp, &req2)
  fmt.Println(req2.Name)
}

往期文章

  1. go 项目ORM、测试、api文档搭建
  2. go 开发短网址服务笔记
  3. go 实现统一加载资源的入口
  4. go 语言编写简单的分布式系统
  5. protocol 和 grpc 的基本使用
  6. go 基础知识
  7. grpc 的单向流和双向流
  8. GORM 基本使用
  9. gin 基本使用