1. 什么是RPC

RPC:Remote Procedure Call,远程过程调用。简单来说就是两个进程之间的数据交互。正常服务端的接口服务是提供给用户端(在Web开发中就是浏览器)或者自身调用的,也就是本地过程调用。和本地过程调用相对的就是:假如两个服务端不在一个进程内怎么进行数据交互?使用RPC。尤其是现在微服务的大量实践,服务与服务之间的调用不可避免,RPC更显得尤为重要

在这里插入图片描述

上图描述了一个RPC的完整调用流程:
1:client向client stub发起方法调用请求。
2:client stub接收到请求后,将方法名,请求参数等信息进行编码序列化。
3:client stub通过配置的ip和端口使用socket通过网络向远程服务器server发起请求。
4:远程服务器server接收到请求,解码反序列化请求信息。
5:server将请求信息交给server stub,server stub找到对应的本地真实方法实现。
6:本地方法处理调用请求并将返回的数据交给server stub。
7:server stub 将数据编码序列化交给操作系统内核,使用socket将数据返回。
8:client端socket接收到远程服务器的返回信息。
9:client stub将信息进行解码反序列化。
10:client收到远程服务器返回的信息。

上图中有一个stub(存根)的概念。stub负责接收本地方法调用,并将它们委托给各自的具体实现对象。server端stub又被称为skeleton(骨架)。可以理解为代理类。而实际上基于Java的RPC框架stub基本上也都是使用动态代理。我们所说的client端和server端在RPC中一般也都是相对的概念。
而所谓的RPC框架也就是封装了上述流程中2-9的过程,让开发者调用远程方法就像调用本地方法一样。

2. gRPC的原理

gRPC是Google的开源产品,是跨语言的通用型RPC框架,使用Go语言编写。 Java语言的应用同样使用了Netty做网络通信,Go采用了Goroutine做网络通信。序列化方式采用了Google自己开源的Protobuf。请求的调用和返回使用HTTP2的Stream。

一个RPC框架必须有两个基础的组成部分:数据的序列化和进程数据通信的交互方式。

对于序列化gRPC采用了自家公司开源的Protobuf。Google Protocol Buffer(简称 Protobuf)是一种轻便高效的结构化数据存储格式,平台无关、语言无关、可扩展,可用于通讯协议和数据存储等领域。似乎和我们熟悉的JSON类似,但其实着重点有些本质的区别。JSON主要是用于数据的传输,因为它轻量级,可读性好,解析简单。Protobuf主要是用于跨语言的IDL,它除了和JSON、XML一样能定义结构体之外,还可以使用自描述格式定于出接口的特性,并可以使用针对不同语言的protocol编译器产生不同语言的stub类。所以天然的适用于跨语言的RPC框架中(非常重要)

而关于进程间的通讯,无疑是Socket。Java方面gRPC同样使用了成熟的开源框架Netty。使用Netty Channel作为数据通道。传输协议使用了HTTP2。
通过以上的分析,我们可以将一个完整的gRPC流程总结为以下几步:

● 通过.proto文件定义传输的接口和消息体。
● 通过protocol编译器生成server端和client端的stub程序。
● 将请求封装成HTTP2的Stream。
● 通过Channel作为数据通信通道使用Socket进行数据传输。

3 实践开始

下面我们使用代码基于以上的步骤来实现一个简单gRPC。我们用Go实现server端,Java作为client端来实现。

3.1 安装Protocol Buffers,定义.proto文件

下载Protocol Buffers:https://github.com/protocolbuffers/protobuf/releases
检查安装

protoc --version

定义一个simple.proto,这也是后续实现gRPC的基础:

syntax = "proto3"; //定义了我们使用的Protocol Buffers版本。

option go_package = "./;simple";//***在java端请注释本行***

 //表明我们定义了一个命名为Simple的服务(接口),内部有一个远程rpc方法,名字为SayHello。
 //我们只要在server端实现这个接口,在实现类中书写我们的业务代码。在client端调用这个接口。
 service Simple{
    rpc SayHello(HelloRequest) returns (HelloReplay){}
 }

 //请求的结构体
 message HelloRequest{
     string name = 1;
 }
 //返回的结构体
 message HelloReplay{
     string message = 1;
 }

3.2 在Go端实现server

根据官方文档使用如下命令安装针对Go的gRPC:

go get -u google.golang.org/grpc

建立Go的project:go-server-grpc,然后将前面写的simple.proto放入项目proto的package中。
cd到proto目录执行如下命令:

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

这样就将simple.proto编译成了Go语言对应的stub程序了。
在这里插入图片描述

随后我们就可以写我们server端的代码了:main.go。
以下的代码都是模板代码,main函数是socket使用Go的标准实现。作为开发者我们只关注远程服务提供的具体接口实现即可。
我们可以在生成的simple.pb.go中发现需要实现的接口:

// 客户端调用的接口
type SimpleClient interface {
	SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReplay, error)
}

type simpleClient struct {
	cc grpc.ClientConnInterface
}

func NewSimpleClient(cc grpc.ClientConnInterface) SimpleClient {
	return &simpleClient{cc}
}

func (c *simpleClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReplay, error) {
	out := new(HelloReplay)
	err := c.cc.Invoke(ctx, "/Simple/SayHello", in, out, opts...)
	if err != nil {
		return nil, err
	}
	return out, nil
}

// 服务端需要实现的接口
type SimpleServer interface {
	SayHello(context.Context, *HelloRequest) (*HelloReplay, error)
}

之后我们在main.go中实现接口:

package main

import (
    "context"
    "grpc-server/proto"//引入对应的包
    "fmt"
    "net"
    "log"
    "google.golang.org/grpc"
    "google.golang.org/grpc/reflection"
)
//定义接下来要开放的socket的端口
const(
    port = ":50051"
)

type server struct{}

func (s *server) SayHello(ctx context.Context,req *simple.HelloRequest) (*simple.HelloReplay, error){

    fmt.Println(req.Name)

    return &simple.HelloReplay{Message:"hello =======> " + req.Name},nil
}

之后填写main方法:

func main() {
	//创建一个socket监听
	lis, err := net.Listen("tcp", port)
	if err != nil {
		log.Fatal("fail to listen")
	}
	//新建一个grpc服务器
	s := grpc.NewServer()
	//使用 simple.RegisterSimpleServer 函数将实现了 SimpleServer 接口的 server 对象注册到 gRPC 服务器(s)上
	simple.RegisterSimpleServer(s, &server{})
	//使用 reflection.Register 函数将 gRPC 服务器(s)注册到反射服务中。这样,可以通过 gRPC 提供的工具来动态地查看和调用服务器上的服务。
	reflection.Register(s)
	//使用 s.Serve 方法启动 gRPC 服务器(s),开始接受来自客户端的连接请求并提供服务。如果启动过程中出现错误,程序会输出一条错误信息并终止运行。
	if err := s.Serve(lis); err != nil {
		log.Fatal("fail to server")
	}
}

目前服务端已经完成了。

3.3 在Go端实现client

package main

import (
	"context"
	"fmt"
	simple "go-server-grpc/proto"
	"log"

	"google.golang.org/grpc"
)

const (
	address = "localhost:50051"
)

func main() {
	// 创建与服务器的连接
	conn, err := grpc.Dial(address, grpc.WithInsecure())
	if err != nil {
		log.Fatalf("无法连接到服务器:%v", err)
	}
	defer conn.Close()
	// 创建一个新的 gRPC 客户端
	client := simple.NewSimpleClient(conn)
	// 构建请求
	request := &simple.HelloRequest{
		Name: "John",
	}
	// 调用 gRPC 方法
	response, err := client.SayHello(context.Background(), request)
	if err != nil {
		log.Fatalf("调用 gRPC 方法失败:%v", err)
	}
	// 打印响应
	fmt.Println(response.Message)
}

需要先启动服务端,再启动客户端就可以看到效果。
输出:hello=======>John

4.下次预告

实现Java作为客户端调用go服务端的服务

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐