Go 实现 RPC | 青训营

76 阅读9分钟

一 rpc概述

RPC即远程过程调用协议(Remote Procedure Call Protocol),是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。

二 rpc执行过程

七层网络模型如下:

  • 第一层:应用层。定义了用于在网络中进行通信和传输数据的接口;
  • 第二层:表示层。定义不同的系统中数据的传输格式,编码和解码规范等;
  • 第三层:会话层。管理用户的会话,控制用户间逻辑连接的建立和中断;
  • 第四层:传输层。管理着网络中的端到端的数据传输;
  • 第五层:网络层。定义网络设备间如何传输数据;
  • 第六层:链路层。将上面的网络层的数据包封装成数据帧,便于物理层传输;
  • 第七层:物理层。这一层主要就是传输这些二进制数据

实际应用过程中,五层协议结构里面是没有表示层和会话层的。应该说它们和应用层合并了。

RPC与web请求类似,都是客户端向远端服务器请求服务返回结果,但是web请求使用的网络协议是http高层协议,而rpc所使用的协议多为网络层的TCP协议,减少了信息的包装,加快了处理速度。

在OSI网络通信模型中,RPC跨越了传输层和应用层,使得开发包括网络分布式多程序在内的应用程序更加容易。运行时,一次客户机对服务器的RPC调用,步骤如下:

  • 1 调用客户端句柄,传送参数
  • 2 调用本地系统内核发送网络消息
  • 3 消息传送到远程主机
  • 4 服务器句柄得到消息并得到参数
  • 5 执行远程过程
  • 6 返回执行结果给服务器句柄
  • 7 服务器句柄返回结果,调用远程系统内核
  • 8 消息传回本地主机
  • 9 客户句柄由内核接收消息
  • 10 客户接收句柄返回的数据

三 rpc架构

一个完整的RPC架构里面包含了四个核心的组件,分别是Client ,Server,Client Stub以及Server Stub,这个Stub大家可以理解为存根。分别说说这几个组件:

  • 客户端(Client):服务的调用方。
  • 服务端(Server):真正的服务提供者。
  • 客户端存根:存放服务端的地址消息,再将客户端的请求参数打包成网络消息,然后通过网络远程发送给服务方。
  • 服务端存根:接收客户端发送过来的消息,将消息解包,并调用本地的方法。

调用过程如图:\

image.png

四 rpc的实现

常见的rpc实现有:

  • go原生rpc:go的rpc包封装了rpc相关实现,但Go的RPC它只支持Go开发的服务器与客户端之间的交互,因为在内部,它们采用了Gob来编码
  • grpc:Google开源的rpc实现,基于最新的HTTP2.0协议,并支持常见的众多编程语言
  • thrift:Facebook开源的跨语言的服务开发框架,它有一个代码生成器来对它所定义的IDL定义文件自动生成服务代码框架。用户只要在其之前进行二次开发就行,对于底层的RPC通讯等都是透明的
  • dubbo:阿里开源的rpc框架,远程接口是基于Java Interface,依托于spring框架,可以方便的打包成单一文件,独立进程运行,和现在的微服务概念一致。
  • HSF:淘宝系内部rpc框架

一 Go 与 RPC

Go标准包中已经提供了对RPC的支持,而且支持三个级别的RPC:TCP、HTTP、JSONRPC。但Go的RPC它只支持Go开发的服务器与客户端之间的交互,因为在内部,它们采用了Gob来编码。

Go RPC的函数只有符合下面的条件才能被远程访问,不然会被忽略,详细的要求如下:

  • 函数必须是导出的(首字母大写)
  • 必须有两个导出类型的参数,
  • 第一个参数是接收的参数,第二个参数是返回给客户端的参数,第二个参数必须是指针类型的
  • 函数还要有一个返回值error

举个例子,正确的RPC函数格式如下:

func (t *T) MethodName(argType T1, replyType *T2) error			// T、T1和T2类型必须能被`encoding/gob`包编解码。

任何的RPC都需要通过网络来传递数据,Go RPC可以利用HTTP和TCP来传递数据,利用HTTP的好处是可以直接复用net/http里面的一些函数。

二 HTTP RPC

2.1 http rpc 服务端

package main

import (
	"errors"
	"fmt"
	"net/http"
	"net/rpc"
)

type Args struct {
	A, B int
}

type Quotient struct {
	Quo, Rem int
}

type Arith int

func (t *Arith) Multiply(args *Args, reply *int) error {
	*reply = args.A * args.B
	return nil
}

func (t *Arith) Divide(args *Args, quo *Quotient) error {
	if args.B == 0 {
		return errors.New("divide by zero")
	}
	quo.Quo = args.A / args.B
	quo.Rem = args.A % args.B
	return nil
}

func main() {

	arith := new(Arith)
	rpc.Register(arith)
	rpc.HandleHTTP()

	err := http.ListenAndServe(":1234", nil)
	if err != nil {
		fmt.Println(err.Error())
	}
}

在上述案例中:注册了一个Arith的RPC服务,然后通过rpc.HandleHTTP函数把该服务注册到了HTTP协议上,此后可以利用http的方式来传递数据了。

2.2 http rpc 客户端


package main

import (
	"fmt"
	"log"
	"net/rpc"
	"os"
)

type Args struct {
	A, B int
}

type Quotient struct {
	Quo, Rem int
}

func main() {
	if len(os.Args) != 2 {
		fmt.Println("Usage: ", os.Args[0], "server")
		os.Exit(1)
	}
	serverAddress := os.Args[1]

	client, err := rpc.DialHTTP("tcp", serverAddress+":1234")
	if err != nil {
		log.Fatal("dialing:", err)
	}
	// Synchronous call
	args := Args{17, 8}
	var reply int
	err = client.Call("Arith.Multiply", args, &reply)
	if err != nil {
		log.Fatal("arith error:", err)
	}
	fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply)

	var quot Quotient
	err = client.Call("Arith.Divide", args, &quot)
	if err != nil {
		log.Fatal("arith error:", err)
	}
	fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)

}

通过上面的调用可以看到参数和返回值是我们定义的struct类型,在服务端我们把它们当做调用函数的参数的类型,在客户端作为client.Call的第2,3两个参数的类型。客户端最重要的就是这个Call函数,它有3个参数,第1个要调用的函数的名字,第2个是要传递的参数,第3个要返回的参数(注意是指针类型),通过上面的代码例子我们可以发现,使用Go的RPC实现相当方便。

三 TCP RPC

基于TCP协议的RPC,服务端的实现代码如下所示:


package main

import (
	"errors"
	"fmt"
	"net"
	"net/rpc"
	"os"
)

type Args struct {
	A, B int
}

type Quotient struct {
	Quo, Rem int
}

type Arith int

func (t *Arith) Multiply(args *Args, reply *int) error {
	*reply = args.A * args.B
	return nil
}

func (t *Arith) Divide(args *Args, quo *Quotient) error {
	if args.B == 0 {
		return errors.New("divide by zero")
	}
	quo.Quo = args.A / args.B
	quo.Rem = args.A % args.B
	return nil
}

func main() {

	arith := new(Arith)
	rpc.Register(arith)

	tcpAddr, err := net.ResolveTCPAddr("tcp", ":1234")
	checkError(err)

	listener, err := net.ListenTCP("tcp", tcpAddr)
	checkError(err)

	for {
		conn, err := listener.Accept()
		if err != nil {
			continue
		}
		rpc.ServeConn(conn)
	}

}

func checkError(err error) {
	if err != nil {
		fmt.Println("Fatal error ", err.Error())
		os.Exit(1)
	}
}

上面这个代码和http的服务器相比,采用了TCP协议,然后需要自己控制连接,当有客户端连接上来后,我们需要把这个连接交给rpc来处理。


package main

import (
	"fmt"
	"log"
	"net/rpc"
	"os"
)

type Args struct {
	A, B int
}

type Quotient struct {
	Quo, Rem int
}

func main() {
	if len(os.Args) != 2 {
		fmt.Println("Usage: ", os.Args[0], "server:port")
		os.Exit(1)
	}
	service := os.Args[1]

	client, err := rpc.Dial("tcp", service)
	if err != nil {
		log.Fatal("dialing:", err)
	}
	// Synchronous call
	args := Args{17, 8}
	var reply int
	err = client.Call("Arith.Multiply", args, &reply)
	if err != nil {
		log.Fatal("arith error:", err)
	}
	fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply)

	var quot Quotient
	err = client.Call("Arith.Divide", args, &quot)
	if err != nil {
		log.Fatal("arith error:", err)
	}
	fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)

}

这个客户端代码和http的客户端代码对比,唯一的区别一个是DialHTTP,一个是Dial(tcp),其他处理一模一样。

四 JSON RPC

JSON RPC是数据编码采用了JSON,而不是gob编码,其他和上面介绍的RPC概念一模一样,服务端实现如下:


package main

import (
	"errors"
	"fmt"
	"net"
	"net/rpc"
	"net/rpc/jsonrpc"
	"os"
)

type Args struct {
	A, B int
}

type Quotient struct {
	Quo, Rem int
}

type Arith int

func (t *Arith) Multiply(args *Args, reply *int) error {
	*reply = args.A * args.B
	return nil
}

func (t *Arith) Divide(args *Args, quo *Quotient) error {
	if args.B == 0 {
		return errors.New("divide by zero")
	}
	quo.Quo = args.A / args.B
	quo.Rem = args.A % args.B
	return nil
}

func main() {

	arith := new(Arith)
	rpc.Register(arith)

	tcpAddr, err := net.ResolveTCPAddr("tcp", ":1234")
	checkError(err)

	listener, err := net.ListenTCP("tcp", tcpAddr)
	checkError(err)

	for {
		conn, err := listener.Accept()
		if err != nil {
			continue
		}
		jsonrpc.ServeConn(conn)
	}

}

func checkError(err error) {
	if err != nil {
		fmt.Println("Fatal error ", err.Error())
		os.Exit(1)
	}
}

通过示例我们可以看出 json-rpc是基于TCP协议实现的,目前它还不支持HTTP方式。

请看客户端的实现代码:


package main

import (
	"fmt"
	"log"
	"net/rpc/jsonrpc"
	"os"
)

type Args struct {
	A, B int
}

type Quotient struct {
	Quo, Rem int
}

func main() {
	if len(os.Args) != 2 {
		fmt.Println("Usage: ", os.Args[0], "server:port")
		log.Fatal(1)
	}
	service := os.Args[1]

	client, err := jsonrpc.Dial("tcp", service)
	if err != nil {
		log.Fatal("dialing:", err)
	}
	// Synchronous call
	args := Args{17, 8}
	var reply int
	err = client.Call("Arith.Multiply", args, &reply)
	if err != nil {
		log.Fatal("arith error:", err)
	}
	fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply)

	var quot Quotient
	err = client.Call("Arith.Divide", args, &quot)
	if err != nil {
		log.Fatal("arith error:", err)
	}
	fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)

}

一 grpc

1.1 grpc概念

grpc是Google开源的rpc实现,基于最新的HTTP2.0协议,并支持常见的众多编程语言。

与许多RPC系统类似,gRPC里客户端应用可以像调用本地对象一样直接调用另一台不同的机器上服务端应用的方法,使得开发者能够更容易地创建分布式应用和服务。

gRPC理念:

  • 定义一个服务,指定其能够被远程调用的方法(包含参数和返回类型)。
  • 在服务端实现这个接口,并运行一个gRPC服务器来处理客户端调用。

gRPC客户端和服务端可以在多种环境中运行和交互,并且可以用任何 gRPC 支持的语言来编写。所以,开发者可以很容易地用 Java 创建一个 gRPC 服务端,用 Go、 Python、Ruby来创建客户端。

1.2 GRPC 与 protocol buffers

gRPC默认使用protoBuf,这是 Google 开源的一套成熟的结构数据序列化机制(当然也可以使用其他数据格式如 JSON )。

二 go搭建 grpc helloworld

2.0 项目结构

image.png

2.1 设置proto文件

创建文件:server/protoes/hello.proto server是go mod的模块名

syntax = "proto3";

package protoes;

service HelloServer{
    rpc SayHi(HelloRequest)returns(HelloReplay){}
    rpc GetMsg(HelloRequest)returns(HelloMessage){}
}

message HelloRequest{
    string name = 1 ;
}

message HelloReplay{
    string message = 1;
}

message HelloMessage{
    string msg = 1;
}

在protoes文件所在的文件夹输入下面命令,生产pb.go文件:

protoc --go_out=plugins=grpc:. *.proto

2.1 服务端

package main

import (
	"fmt"
	"context"
	"google.golang.org/grpc"
	"net"
	pb "test/protoes"
)

// 对象要和proto内定义的服务一致
type server struct{

}

// 实现rpc的 SayHi接口
func(s *server)SayHi(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReplay, error){
	return &pb.HelloReplay{
		Message: "Hi " + in.Name,
	}, nil
}

// 实现rpc的 GetMsg接口
func(s *server)GetMsg(ctx context.Context, in *pb.HelloRequest) (*pb.HelloMessage, error){
	return &pb.HelloMessage{
		Msg: "Server msg is coming...",
	}, nil
}

func main() {

	// 监听网络
	ln, err := net.Listen("tcp", "127.0.0.1:3000")
	if err != nil {
		fmt.Println("网络异常:", err)
		return
	}

	// 创建grpc句柄
	srv := grpc.NewServer()

	// 将server结构体注册到grpc服务中
	pb.RegisterHelloServerServer(srv, &server{})

	// 监听服务
	err = srv.Serve(ln)
	if err != nil {
		fmt.Println("监听异常:", err)
		return
	}

}

2.2 客户端

package main

import (
	"fmt"
	"context"
	"google.golang.org/grpc"
	pb "test/protoes"
)

func main() {

	// 客户端连接服务器
	conn,err := grpc.Dial("127.0.0.1:3000", grpc.WithInsecure())
	if err != nil {
		fmt.Println("连接服务器失败",err)
	}
	defer conn.Close()

	// 获得grpc句柄
	c := pb.NewHelloServerClient(conn)

	// 远程单调用 SayHi 接口
	r1, err := c.SayHi(
		context.Background(),
		&pb.HelloRequest{
			Name: "Kitty",
		},
	)
	if err !=  nil {
		fmt.Println("Can not get SayHi:", err)
		return
	}
	fmt.Println("SayHi 响应:", r1)

	// 远程单调用 GetMsg 接口
	r2, err := c.GetMsg(
		context.Background(),
		&pb.HelloRequest{
			Name: "Kitty",
		},
	)
	if err !=  nil {
		fmt.Println("Can not get GetMsg:", err)
		return
	}
	fmt.Println("GetMsg 响应:", r2)

}

2.3 测试

依次进入server与client文件夹,执行:

go run server.go
go run client.go

客户端输出结果:

SayHi 响应: message:"Hi Kitty" 
GetMsg 响应: msg:"Server msg is coming..."