Bootstrap

2025秋招八股文--RPC篇

前言

1.本系列面试八股文的题目及答案均来自于网络平台的内容整理,对其进行了归类整理,在格式和内容上或许会存在一定错误,大家自行理解。内容涵盖部分若有侵权部分,请后台联系,及时删除。

2.本系列发布内容分为12篇 分别是:

c/c++语言
数据结构与算法
GDB
设计模式
操作系统
系统编程
网络原理
网络编程
mysql
redis
服务器
RPG
本文为第十二篇,后续会陆续更新。
共计200+道八股文。

3.本系列的200+道为整理的八股文系列的一小部分。完整整理完的八股文面试题共计1000+道,100W字左右,体量太大,故此处放至百度云盘链接: https://pan.baidu.com/s/1IOxQs0ifbSPGgxK7Yz7BtQ?pwd=zl1i

提取码:zl1i 需要的同学自取即可。

4.八股文对于面试的同学来说仅作为参考使用,不能作为面试上岸的唯一准备,还是要结合自身的技术能力和项目,同步发育。

最后祝各位同学都能拿到自己满意的offer,成功上岸!
————————————————

十二、RPC

01.grpc怎么用的

一、gRPC 介绍

在 gRPC 里客户端应用可以像调用本地对象一样直接调用另一台不同的机器上服务端应用的方法,使得您能够更容易地创建分布式应用和服务。与许多 RPC 系统类似,gRPC 也是基于以下理念:定义一个服务,指定其能够被远程调用的方法(包含参数和返回类型)。在服务端实现这个接口,并运行一个 gRPC 服务器来处理客户端调用。在客户端拥有一个存根能够像服务端一样的方法。

图1

二、安装与使用

安装 protoc 程序:

从 https://github.com/google/protobuf/releases下载适合你平台的预编译好的二进制文件(protoc--.zip),然后将压缩包内的 /bin/protoc 文件添加到环境变量即可:

# 下载压缩包

wget https://github.com/protocolbuffers/protobuf/releases/download/v21.9/protoc-21.9-linux-x86_64.zip

# 解压

unzip -d ./protoc protoc-21.9-linux-x86_64.zip

# 移动到 /usr/local/bin 下面

mv ./protoc/bin/protoc /usr/local/bin

首先需要确保将 GOBIN 添加进了环境变量,如果没有添加使用的时候会无法找到路径。Linux 环境下可通过以下指令添加:

export GOPATH=$HOME/go
export GOBIN=$GOPATH/bin
export PATH=$PATH:$GOPATH/bin

安装 protoc-gen-go 和 protoc-gen-go-grpc 插件:

# protoc-gen-go 插件主要用于将 *.proto 文件生成一个后缀为 *.pb.go 的文件。生成文件中包含所有 .proto 文件中定义的类型及其序列化方法

go install google.golang.org/protobuf/cmd/protoc-gen-go@latest

# protoc-gen-go-grpc 插件主要用于生成 gRPC 服务端需要实现的接口和客户端调用的接口

go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

测试环境安装是否成功:

> protoc --version
> libprotoc 3.21.9

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

> protoc-gen-go-grpc --version
> protoc-gen-go-grpc 1.2.0
> 

三、protobuff 语法

proto 文件主要包含 proto 基本信息以及 RPC 服务定义。

进行一次 RPC 调用需要注意两个点:方法和参数。在 porro 文件中,方法用 Service 定义,参数用 Message 定义。

1.基本信息

1. // 版本声明,使用Protocol Buffers v3版本
   syntax = "proto3";

// package 是 proto 的包名,一个文件就是一个 package,用于 import 时解析(与 go 的包管理类似)
package pb;

// option go_package = "path;name";
// path 表示生成的go文件的存放地址,会自动生成目录的。
// name 表示生成的go文件所属的包名
option go_package = "./;pb";

2.message

一个 Message 中主要包含:字段编号和字段类型;

字段编号

消息定义中的每个字段都有一个唯一的编号。这些字段编号用来在消息二进制格式中标识字段,在消息类型使用后就不能再更改。

注意,范围1到15中的字段编号需要一个字节进行编码,包括字段编号和字段类型。范围16到2047的字段编号采用两个字节。因此,应该为经常使用的消息元素保留数字1到15的编号。切记为将来可能添加的经常使用的元素留出一些编号。

字段类型

字段类型包含标量类型、组合类型、枚举类型、数组类型、map 类型和嵌套消息;

2.1 标量类型

下表中列举了 protobuff 中的类型与常见编程语言中类型的对应关系:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

examples:

message HelloRequest {
	string name = 1;
	int64 age = 2;
	bool man = 3;
}
2.2 枚举类型

当需要定义一个消息类型的时候,可能想为一个字段指定 “预定义值序列” 中的一个值,这时候可以通过枚举实现。

enum PhoneType // 枚举消息类型,使用 enum 关键词定义
{
    MOBILE = 0; // proto3 版本中,枚举类型首成员必须为0,成员不应有相同的值
    HOME = 1;
    WORK = 2;
}

// 定义一个电话消息
message PhoneNumber
{
    string number = 1;
    PhoneType type = 2; 
}
2.3 数组类型

在 protobuf 消息中定义数组类型,是通过在字段前面增加 repeated 关键词实现,标记当前字段是一个数组。

message Msg {
	repeated int32 arrays = 1;
	repeated string strings = 2;
}

2.4 map 类型

map 类型的定于语法为:

map<key_type, value_type> map_field = N;

key_type可以是任何整数或字符串类型(除浮点类型和字节之外的任何标量类型)。请注意,枚举不是有效的key_type。

示例:

message Msg {
	map<string, int> dictA = 1;
	map<string, string> dictB = 2;
}

2.5 嵌套类型

我们在各种语言开发中类的定义是可以互相嵌套的,也可以使用其他类作为自己的成员属性类型。

在 protobuf 中同样支持消息嵌套,可以在一个消息中嵌套另外一个消息,字段类型可以是另外一个消息类型。

引用其他消息类型

message Msg1 {
	string name = 1;
	int32 age = 2;
}

message Msg2 {
	string describe = 1;
	Msg1 msg = 2;
}

消息嵌套

message Msg1 {
	message Msg2 {
		string name = 1;
		string age = 2;
	}
	Msg2 msg = 1;
}

import 导入其他 proto 文件定义的消息

msg1.proto:

syntax = "proto3";

package msg1;

message Msg1 {
	string name = 1;
	string age = 2;
}
msg2.proto:

syntax = "proto3";

import "msg1.proto";

package msg2;

message Msg2 {
	msg1.Msg1 msg = 1;
}
  1. Service
    在 gRPC 中,可以定义四种类型的服务方法:

简单RPC (simple RPC):客户端向服务器发送一个请求,然后得到一个响应,就像普通的函数调用一样:

rpc SimpleRPC(HelloRequest) returns (HelloResponse);

服务端流式RPC (server-side streaming RPC):客户端向服务端发送一个请求,服务端返回客户端一个流。客户端可以从这个流中读取,直到服务端关闭这个流:

rpc ServerSideStreamingRPC(HelloRequest) returns (stream HelloResponse);

客户端流式RPC (client-side streaming RPC):客户端向服务端发起一个流式请求。客户端可以向流中多次写入数据,服务端从流中多次取出数据,直到客户端关闭流。服务端接收完所有数据后,向客户端返回一个普通响应:

rpc ClientSideStreamingRPC(stream HelloRequest) returns (HelloResponse);

双向流式RPC (bidirectional streaming RPC):客户端向服务端发起一个流式请求,服务端向客户端返回一个流式响应。两个流之间相互独立,互不影响:

rpc BidrectionalStreamingRPC(stream HelloRequest) returns (stream HelloResponse);

将需要定义的 RPC 调用写入 Service 字段:

service Greeter {
  rpc SimpleRPC(HelloRequest) returns (HelloResponse);

  rpc ServerSideStreamingRPC(HelloRequest) returns (stream HelloResponse);

  rpc ClientSideStreamingRPC(stream HelloRequest) returns (HelloResponse);

  rpc BidrectionalStreamingRPC(stream HelloRequest) returns (stream HelloResponse);
}

四、gRPC 示例

首先建一个 Go 的工程:

go mod init grpcDemo

然后导入 gRPC 包:

go get google.golang.org/grpc@latest

当前目录结构为:

.
├── go.mod
└── go.sum

  1. 编写 protobuff 代码
    新建一个 pb 目录,编写 demo.proto 文件:
syntax = "proto3";

option go_package = "./;pb";
package pb;

service Greeter {
  // 简单 RPC
  rpc SimpleRPC(HelloRequest) returns (HelloResponse);

  // 服务端流式 RPC
  rpc ServerSideStreamingRPC(HelloRequest) returns (stream HelloResponse);

  // 客户端流式 RPC
  rpc ClientSideStreamingRPC(stream HelloRequest) returns (HelloResponse);

  // 双端流式 RPC
  rpc BidrectionalStreamingRPC(stream HelloRequest) returns (stream HelloResponse);
}

message HelloRequest {string name = 1;}

message HelloResponse {string reply = 1;}

当前目录结构为:

.
├── go.mod
├── go.sum
└── pb
    └── demo.proto

通过以下命令将 .proto 文件生成 .go 文件:

protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative pb/demo.proto

执行命令后会在当前文件夹下生成两个 .go 文件:

.

├── go.mod
├── go.sum
└── pb
    ├── demo.pb.go
    ├── demo.proto
    └── demo_grpc.pb.go
  1. 编写 Go 代码
    新建一个 server和client 目录,创建 server.go和client.go 文件进行服务端和客户端代码编写。

2.1 Simple RPC

服务端

package main

import (
	"context"
	"grpcDemo/pb"
	"log"
	"net"

	"google.golang.org/grpc"

)

type server struct {
    // 继承 protoc-gen-go-grpc 生成的服务端代码
	pb.UnimplementedGreeterServer
}

// SimplePRC 服务端代码
func (s *server) SimpleRPC(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
	log.Println("client call simpleRPC...")

	log.Println(in)
	return &pb.HelloResponse{Reply: "Hello " + in.Name}, nil

}

func main() {
    // 监听本地 5678 端口
	listen, err := net.Listen("tcp", ":5678")
	if err != nil {
		log.Fatal(err)
		return
	}

    // 创建 gRPC 服务器
    s := grpc.NewServer()
    // 将实现的接口注册进 gRPC 服务器
    pb.RegisterGreeterServer(s, &server{})
    log.Println("gRPC server starts running...")
    // 启动 gRPC 服务器
    err = s.Serve(listen)
    if err != nil {
    	log.Fatal(err)
    	return
    }

}



客户端

package main

import (
	"context"
	"grpcDemo/pb"
	"io"
	"log"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"

)

func simpleRPC(c pb.GreeterClient) {
	ctx := context.Background()
    // 调用服务端 SimpleRPC 并获取响应
	reply, err := c.SimpleRPC(ctx, &pb.HelloRequest{Name: "simpleRPC"})
	if err != nil {
		log.Fatal(err)
	}
	log.Println(reply.GetReply())
}

func main() {
    // 连接服务端,因为我们没有SSL证书,因此这里需要禁用安全传输
	dial, err := grpc.Dial("127.0.0.1:5678", grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		log.Fatal(err)
		return
	}
	defer dial.Close()
	

	conn := pb.NewGreeterClient(dial)
	simpleRPC(conn)

}

当前目录结构:

.

├── client
│   └── client.go
├── go.mod
├── go.sum
├── pb
│   ├── demo.pb.go
│   ├── demo.proto
│   └── demo_grpc.pb.go
└── server
    └── server.go

分别使用两个终端运行服务端和客户端的代码:

> go run server/server.go 
> 2022/10/27 08:55:59 gRPC server starts running...
> 2022/10/27 08:56:41 client call simpleRPC...
> 2022/10/27 08:56:41 name:"simpleRPC"

> go run client/client.go 
> 2022/10/27 08:56:41 Hello simpleRPC

2.2 服务端流式 RPC

> // server.go
> func (s *server) ServerSideStreamingRPC(in *pb.HelloRequest, stream pb.Greeter_ServerSideStreamingRPCServer) error {
> log.Println("client call ServerSideStreamingRPC...")
> words := []string{
> 	"你好",
> 	"hello",
> 	"こんにちは",
> 	"안녕하세요",
> }

	for _, word := range words {
		data := &pb.HelloResponse{
			Reply: word + " " + in.Name,
		}

	    // 向流中不断写入数据
		if err := stream.SendMsg(data); err != nil {
			return err
		}
	}
	return nil

}
// client.go
func serverSideStreamRPC(c pb.GreeterClient) {
	ctx := context.Background()
    // 调用服务端方法
	stream, err := c.ServerSideStreamingRPC(ctx, &pb.HelloRequest{Name: "gRPC!"})
	if err != nil {
		log.Fatal(err)
	}
	for {
		// 接收服务端返回的流式数据,当收到io.EOF或错误时退出
		res, err := stream.Recv()
        // 判断流是否关闭
		if err == io.EOF {
			break
		}
		if err != nil {
			log.Fatal(err)
		}
		log.Printf("got reply: %q\n", res.GetReply())
	}
}

分别使用两个终端运行服务端和客户端的代码:

> go run server/server.go 
> 2022/10/27 09:02:59 gRPC server starts running...
> 2022/10/27 09:03:23 client call ServerSideStreamingRPC...

> go run client/client.go 
> 2022/10/27 09:03:23 got reply: "你好 gRPC!"
> 2022/10/27 09:03:23 got reply: "hello gRPC!"
> 2022/10/27 09:03:23 got reply: "こんにちは gRPC!"
> 2022/10/27 09:03:23 got reply: "안녕하세요 gRPC!"

2.3 客户端流式 RPC

> // server.go
> func (s *server) ClientSideStreamingRPC(stream pb.Greeter_ClientSideStreamingRPCServer) error {
> log.Println("client call ClientSideStreamingRPC...")
> reply := "Hello: "
> for {
>    // 从流中接收客户端发送的数据
> 	res, err := stream.Recv()
>    // 判断流是否关闭
> 	if err == io.EOF {
> 		return stream.SendAndClose(&pb.HelloResponse{Reply: reply})
> 	}

		if err != nil {
			return err
		}

		reply += ", " + res.GetName()
	}

}
// client.go
func clientSideStreamRPC(c pb.GreeterClient) {
	ctx := context.Background()
    // 调用服务端方法
	stream, err := c.ClientSideStreamingRPC(ctx)
	if err != nil {
		log.Fatal(err)
	}

	names := []string{"a1", "a2", "a3", "a4"}
	for _, name := range names {
	    // 向流中不断写入数据
		err := stream.Send(&pb.HelloRequest{Name: name})
		if err != nil {
			log.Fatal(err)
		}
	}
	
	// 向服务端发送关闭流的信号,并接收数据
	res, err := stream.CloseAndRecv()
	if err != nil {
		log.Fatal(err)
	}
	log.Println(res.Reply)

}

分别使用两个终端运行服务端和客户端的代码:

> go run server/server.go 
> 2022/10/27 09:17:04 gRPC server starts running...
> 2022/10/27 09:17:11 client call ClientSideStreamingRPC...

> go run client/client.go 
> 2022/10/27 09:17:11 Hello: , a1, a2, a3, a4

2.4 双端流式 RPC

> // server.go
> func (s *server) BidrectionalStreamingRPC(stream pb.Greeter_BidrectionalStreamingRPCServer) error {
> log.Println("client call BidrectionalStreamingRPC...")
> for {
>    // 接收来自客户端的数据
> 	res, err := stream.Recv()
> 	if err == io.EOF {
> 		return nil
> 	}
> 	if err != nil {
> 		return err
> 	}

		reply := "Hello " + res.GetName()
	    // 向客户端发送数据
		if err := stream.Send(&pb.HelloResponse{Reply: reply}); err != nil {
			return err
		}
	}

}
// client.go
func bidStreamRPC(c pb.GreeterClient) {
    // 调用服务端方法
	stream, err := c.BidrectionalStreamingRPC(context.Background())
	if err != nil {
		log.Fatal(err)
	}

	names := []string{"a1", "a2", "a3", "a4"}
	for _, name := range names {
	    // 向服务端发送数据
		err := stream.Send(&pb.HelloRequest{Name: name})
		if err != nil {
			log.Fatal(err)
		}
	
	    // 从客户端接收数据
		reply, err := stream.Recv()
		if err != nil {
			log.Fatal(err)
		}
		log.Println(reply.GetReply())
	}
	
	// 关闭流
	err = stream.CloseSend()
	if err != nil {
		log.Fatal(err)
	}

}

分别使用两个终端运行服务端和客户端的代码:

> go run server/server.go 
> 2022/10/27 09:20:51 gRPC server starts running...
> 2022/10/27 09:26:26 client call BidrectionalStreamingRPC...

> go run client/client.go 
> 2022/10/27 09:26:26 Hello a1
> 2022/10/27 09:26:26 Hello a2
> 2022/10/27 09:26:26 Hello a3
> 2022/10/27 09:26:26 Hello a4

3.gRPC 使用 Unix Socket 通信

gRPC 默认使用的是 TCP 通信。但是如果想仅仅在本机进行进程间通信,就没必要过一层网络接口了,直接使用 unix socket 即可。

// server.go
package main

import (
        "context"
        "grpcDemo/pb"
        "log"
        "net"
        "os"

        "google.golang.org/grpc"

)

type server struct {
        pb.UnimplementedGreeterServer
}

// 实现 SimpleRPC 方法
func (s *server) SimpleRPC(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
        log.Println("client call simpleRPC...")
        log.Println(in)
        return &pb.HelloResponse{Reply: "Hello " + in.Name}, nil
}

// 移除存在的 unix socket 文件
func removeExistedSock(file string) {
        stat, err := os.Stat(file)
        if err == nil {
                if stat.Mode().Type() == os.ModeSocket {
                        err := os.Remove(file)
                        if err != nil {
                                log.Fatal(err)
                        }
                }
        }
}

func main() {
        removeExistedSock("/tmp/default.sock")
    	// 解析 unix socket 地址
        addr, err := net.ResolveUnixAddr("unix", "/tmp/default.sock")
        if err != nil {
                log.Fatal(err)
                return
        }
    	// 监听 unix socket 文件
        unix, err := net.ListenUnix("unix", addr)
        if err != nil {
                log.Fatal(err)
                return
        }

        s := grpc.NewServer()
    	// 绑定 unix socket 连接
        pb.RegisterGreeterServer(s, &server{})
    	log.Println("gRPC server starts running...")
    	// 启动服务
        err = s.Serve(unix)
        if err != nil {
                log.Fatal(err)
                return
        }

}
package main

import (
        "context"
        "grpcDemo/pb"
        "log"
        "net"

        "google.golang.org/grpc"
        "google.golang.org/grpc/credentials/insecure"

)


func simpleRPC(c pb.GreeterClient) {
        ctx := context.Background()
        reply, err := c.SimpleRPC(ctx, &pb.HelloRequest{Name: "simpleRPC"})
        if err != nil {
                log.Fatal(err)
        }
        log.Println(reply.GetReply())
}

// 被 gRPC 框架调用,创建 unix socket 连接
func UnixConnect(ctx context.Context, addrs string) (net.Conn, error) {
        addr, err := net.ResolveUnixAddr("unix", addrs)
        if err != nil {
                return nil, err
        }
        conn, err := net.DialUnix("unix", nil, addr)
        if err != nil {
                return nil, err
        }
        return conn, nil
}

func main() {
    	// 创建 gRPC 连接客户端
        conn, err := grpc.Dial("/tmp/default.sock", grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithContextDialer(UnixConnect))
        if err != nil {
                log.Fatal(err)
                return
        }
		

    	// 创建当前 protobuff 的客户端对象
        client := pb.NewGreeterClient(conn)
        simpleRPC(client)

}

分别使用两个终端运行服务端和客户端的代码:

> go run server1/server.go 
> 2022/10/27 09:59:16 gRPC server starts running...
> 2022/10/27 10:01:47 client call simpleRPC...
> 2022/10/27 10:01:47 name:"simpleRPC"

> go run client1/client.go 
> 2022/10/27 10:01:47 Hello simpleRPC
> 

02.为什么grpc速度快

gRPC的速度快主要是由于以下几个原因:

  1. 采用了二进制编码:gRPC使用二进制编码来传输数据,相比于文本格式(如JSON、XML等),可以大幅降低网络传输的数据量,提高传输效率。
  2. 基于HTTP/2协议:gRPC基于HTTP/2协议实现,这种协议支持多路复用和流控制等特性,能够更好地利用网络带宽,并且可以减少网络连接数,从而提高性能。
  3. 自动生成客户端和服务端代码:gRPC使用Protocol Buffers作为接口描述语言,并通过插件自动生成客户端和服务端代码。这样就避免了手写代码时出错的可能性,也可以节省很多开发时间。
  4. 支持流式传输:gRPC支持流式传输,在一次请求-响应中可以发送多个消息,或者在多次请求-响应中保持长连接状态。这种方式可以降低通信延迟并且更加灵活。

03.rpc调用流程和机制,rpc超时计时器在什么位置,如果调用超时了怎么处理,当前连接还能继续使用吗

一、RPC概念

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

RPC 是一种技术思想而非一种规范或协议,常见 RPC 技术和框架有:

二、目前流行的开源 RPC 框架

应用级的服务框架:阿里的 Dubbo/Dubbox、Google gRPC、Spring Boot/Spring Cloud。
远程通信协议:RMI、Socket、SOAP(HTTP XML)、REST(HTTP JSON)。
通信框架:MINA 和 Netty。
Facebook 的 Thrift、Twitter 的 Finagle 等。

重点介绍三种:

  • Dubbo:国内最早开源的 RPC 框架,极为出名,由阿里巴巴公司开发并于 2011 年末对外开源,仅支持 Java 语言。协议和序列化框架都可以插拔是极其鲜明的特色。
  • gRPC:Google 于 2015 年对外开源的跨语言 RPC 框架,支持多种语言。RPC 框架是基于 HTTP 协议实现的,底层使用到了 Netty 框架的支持。
  • Thrift:最初是由 Facebook 的开源 RPC 框架,主要是一个跨语言的服务开发框架,2007 年贡献给了 Apache 基金,成为 Apache 开源项目之一,支持多种语言。用户只要在其之上进行二次开发就行,应用对于底层的 RPC 通讯等都是透明的。不过这个对于用户来说需要学习特定领域语言这个特性,还是有一定成本的。

Motan:新浪微博内部使用的 RPC 框架,于 2016 年对外开源,仅支持 Java 语言;
Tars:腾讯内部使用的 RPC 框架,于 2017 年对外开源,仅支持 C++ 语言;
Spring Cloud:国外 Pivotal 公司 2014 年对外开源的 RPC 框架,仅支持 Java 语言;

三、RPC 框架的重要组成

一个 RPC 的核心功能主要有 5 个部分组成,分别是:客户端、客户端 Stub、网络传输模块、服务端 Stub、服务端等:

  • 客户端(Client):服务调用方。
  • 客户端存根(Client Stub):存放服务端地址信息,将客户端的请求参数数据信息打包成网络消息,再通过网络传输发送给服务端。
  • 服务端存根(Server Stub):接收客户端发送过来的请求消息并进行解包,然后再调用本地服务进行处理。
  • 服务端(Server):服务的真正提供者。
  • Network Service:底层传输,可以是 TCP 或 HTTP。

img

四、RPC 框架实现

1、RPC调用流程:

(1). 服务消费方(client)以本地调用方式调用服务;
(2). client stub接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体;
(3). client stub找到服务地址,并将消息发送到服务端;
(4). server stub收到消息后进行解码;
(5). server stub根据解码结果 反射调用 本地的服务;
(6). 本地服务执行并将结果返回给server stub;
(7). server stub将返回结果打包成消息并发送至消费方;
(8). client stub接收到消息,并进行解码;
(9). 服务消费方得到最终结果。

2、RPC 框架实现需要使用到的技术

1)服务注册中心(寻址)

服务发现与注册的核心是,服务启动时,将服务名称和服务地址写入到配置中心,客户端调用的时候,先从服务注册中心读取去查询对方服务都有哪些实例,所要调用服务的服务器地址,如果有多个,进一步处理负载均衡问题,连接服务器并调用。

Dubbo 的服务注册中心是可以配置的,官方推荐使用 Zookeeper。

2)动态代理问题

服务调用者用的服务实际是远程服务的本地代理,其实就是通过动态代理来实现。Java里至少提供了两种方式来提供动态代码生成,一种是jdk动态代理,另一种是字节码生成,动态代理相比字节码生成使用起来更方便,但动态代理方式在性能上比字节码要差,而字节码生成在代码可读性上要差很多。

生成 client stub和server stub需要用到 Java 动态代理技术 ,我们可以使用JDK原生的动态代理机制,可以使用一些开源字节码工具框架 如:CgLib、Javassist等。

3)序列化问题

客户端怎么把参数值传给远程的函数呢?在本地调用中,我们只需要把参数压到栈里,然后让函数自己去栈里读就行。但是在远程过程调用时,客户端跟服务端是不同的进程,不能通过内存来传递参数。这时候就需要客户端把参数先转成一个字节流,传给服务端后,再把字节流转成自己能读取的格式。只有二进制数据才能在网络中传输,序列化和反序列化的定义是:

  • 将对象转换成二进制流的过程叫做序列化
  • 将二进制流转换成对象的过程叫做反序列化

这个过程叫序列化和反序列化。同理,从服务端返回的值也需要序列化反序列化的过程。

可以使用Java原生的序列化机制,但是效率非常低,推荐使用一些开源的、成熟的序列化技术,例如:protobuf、Thrift、hessian、Kryo、Msgpack

4)网络通讯问题(NIO )

所有的数据都需要通过网络传输,因此就需要有一个网络传输层。因此,它所使用的协议其实是不限的,能完成传输就行,在 RPC 中可选的网络传输方式有多种,可以选择 TCP 协议、UDP 协议、HTTP 协议。

尽管大部分 RPC 框架都使用 TCP 协议,但其实 UDP 也可以,而 gRPC 干脆就用了 HTTP2。可以自己写 Socket,或者用 Asio,ZeroMQ,Netty 之类。当前很多RPC框架都直接基于netty这一IO通信框架,比如阿里巴巴的HSF、dubbo,Hadoop Avro,推荐使用Netty 作为底层通信框架。

RPC 主要用于公司内部的服务调用,性能消耗低,传输效率高,实现复杂。
HTTP 主要用于对外的异构环境,浏览器接口调用,App 接口调用,第三方接口调用等。
RPC 使用场景(大型的网站,内部子系统较多、接口非常多的情况下适合使用 RPC):
长链接。不必每次通信都要像 HTTP 一样去 3 次握手,减少了网络开销。
注册发布机制。RPC 框架一般都有注册中心,有丰富的监控管理;发布、下线接口、动态扩展等,对调用方来说是无感知、统一化的操作。
安全性,没有暴露资源操作。
在代码可读性上要差很多。

生成 client stub和server stub需要用到 Java 动态代理技术 ,我们可以使用JDK原生的动态代理机制,可以使用一些开源字节码工具框架 如:CgLib、Javassist等。

3)序列化问题

客户端怎么把参数值传给远程的函数呢?在本地调用中,我们只需要把参数压到栈里,然后让函数自己去栈里读就行。但是在远程过程调用时,客户端跟服务端是不同的进程,不能通过内存来传递参数。这时候就需要客户端把参数先转成一个字节流,传给服务端后,再把字节流转成自己能读取的格式。只有二进制数据才能在网络中传输,序列化和反序列化的定义是:

  • 将对象转换成二进制流的过程叫做序列化
  • 将二进制流转换成对象的过程叫做反序列化

这个过程叫序列化和反序列化。同理,从服务端返回的值也需要序列化反序列化的过程。

可以使用Java原生的序列化机制,但是效率非常低,推荐使用一些开源的、成熟的序列化技术,例如:protobuf、Thrift、hessian、Kryo、Msgpack

4)网络通讯问题(NIO )

所有的数据都需要通过网络传输,因此就需要有一个网络传输层。因此,它所使用的协议其实是不限的,能完成传输就行,在 RPC 中可选的网络传输方式有多种,可以选择 TCP 协议、UDP 协议、HTTP 协议。

尽管大部分 RPC 框架都使用 TCP 协议,但其实 UDP 也可以,而 gRPC 干脆就用了 HTTP2。可以自己写 Socket,或者用 Asio,ZeroMQ,Netty 之类。当前很多RPC框架都直接基于netty这一IO通信框架,比如阿里巴巴的HSF、dubbo,Hadoop Avro,推荐使用Netty 作为底层通信框架。

RPC 主要用于公司内部的服务调用,性能消耗低,传输效率高,实现复杂。
HTTP 主要用于对外的异构环境,浏览器接口调用,App 接口调用,第三方接口调用等。
RPC 使用场景(大型的网站,内部子系统较多、接口非常多的情况下适合使用 RPC):
长链接。不必每次通信都要像 HTTP 一样去 3 次握手,减少了网络开销。
注册发布机制。RPC 框架一般都有注册中心,有丰富的监控管理;发布、下线接口、动态扩展等,对调用方来说是无感知、统一化的操作。
安全性,没有暴露资源操作。
微服务支持。就是最近流行的服务化架构、服务化治理,RPC 框架是一个强力的支撑。

;