go 中 rpc 和 grpc 的使用

2023年 7月 13日 87.6k 0

RPC

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

远程过程调用有很多问题

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

  • 我们需要自己去定义 HelloService
  • server 端注册服务,启动服务
  • 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
    }
    

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

    4.png

    使用 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 安装

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

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

  • 安装完成,检查版本
  • 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 远程下载

  • 确认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
    
  • 从 官方地址 中选择对应的版本下载并解压
  • # 下载
    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
    
  • proto 放到环境变量弘
  • mv -f bin/proto /usr/local/bin
    
  • 如果要使用它里面的类型,需要将 include 目录中的内容放到 /usr/local/include
  • mv -f include/google /usr/local/include
    
  • 如果要使用 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
    
  • 安装完成
  • 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 文件

  • 新建一个 helloWorld.proto 文件
  • syntax = "proto3";
    
    // 在最新的 protoc 版本中,option 要像下面这样写,否则会报错
    option go_package ="./;proto";
    
    message HelloRequest {
      string name = 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)
    }
    

    往期文章

  • go 项目ORM、测试、api文档搭建
  • go 开发短网址服务笔记
  • go 实现统一加载资源的入口
  • go 语言编写简单的分布式系统
  • 相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论