go 中 rpc 和 grpc 的使用

RPC

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

远程过程调用有很多问题

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

  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
}

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

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

  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 语言编写简单的分布式系统

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYUawD38' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片