2、RPC框架解析:什么是RPC?

这里抛出几个问题:

  • RPC是什么?
  • 本地函数调用的流程是什么?
  • RPC与HTTP,TCP有什么关系?
  • 一个标准的RPC包含哪些流程?
  • 什么是协议?

2.1、简介

​ RPC框架全称Remote Procedure Call,翻译过来就是远程调用,RPC出现的原因主要是为了解决分布式系统中不同系统之间的接口调用。RPC使子系统接口的实现和调用可以分布在不同的机器上,RPC的调用非常方便,就像本地调用接口实现一样,RPC调用对于调用者来说,基本上属于黑盒。

2.2、本地调用

​ 在本地调用中,比如有一个main.go文件,编译器会通过一系列的操作,比如语法分析等生成目标bin文件,输入,输出编解码的一系列问题,编译器都帮你搞定了。但是在远程调用中这些却是我们自己需要考虑的。

例如以下代码:

1
2
3
4
func main() {
option := 1
fmt.Println("option = ",option)
}

​ 我们一眼就能看出这段demo的意思,但是计算机是肯定不可能直接认识的,通过编译器,编译后的bin文件,计算机就可以执行了。

​ 通过上面的描述,引出第一个概念,就是协议,协议解决了最基本的沟通问题,当调用方与远程接口约定好了协议,就可以实现沟通,就像我们在本地实现一个main.go文件,然后编译器根据约定好的协议(语法,语义)生成bin文件供计算机执行。

2.3、协议

协议是一种程序员通过学习就可以理解的,相信在工作中交流用的最多的就是以下场景。

1
hi,有没有读取视频列表的接口,把协议发我看看。

一般看到协议,就知道如何调用对方接口了,所以协议更像是人类的语言。

协议的三要素:

  • 语法:例如go语言与java语言的写法就不一样,有各自的语言规则。
  • 语义:一段带有意义的文本,例如:”10-1”就是有意义的,”10-你好”就是无意义的。
  • 顺序:先执行1,在执行2,有严格的执行顺序。

所以,通过约定好的rpc协议,就可以实现各个系统服务之间的调用。

2.4、桩代码Stub

​ 桩(Stub / Method Stub)是指用来替换一部分功能的程序段。桩程序可以用来模拟已有程序的行为(比如一个远端机器的过程)或是对将要开发的代码的一种临时替代。因此,打桩技术在程序移植、分布式计算、通用软件开发和测试中用处很大。

例如 googole的protocol buffers协议:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
syntax = "proto3";

option go_package = "io.grpc.examples";

package helloworld;

// The greeter service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
string name = 1;
}

// The response message containing the greetings
message HelloReply {
string message = 1;
}

定义好协议,通过protocol buffer 编译器 protoc 来生成创建grpc应用所需的特定客户端和服务端的代码。protoc如何安装可以直接google。

命令如下:

1
protoc --go_out=plugins=grpc:. demo.proto

通过proto生成的桩代码如下,具体的原理后面分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: demo.proto

package io_grpc_examples

import (
context "context"
fmt "fmt"
proto "github.com/golang/protobuf/proto"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
math "math"
)

// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf

// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package

// The request message containing the user's name.
type HelloRequest struct {
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}

func (m *HelloRequest) Reset() { *m = HelloRequest{} }
func (m *HelloRequest) String() string { return proto.CompactTextString(m) }
func (*HelloRequest) ProtoMessage() {}
func (*HelloRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_ca53982754088a9d, []int{0}
}

func (m *HelloRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_HelloRequest.Unmarshal(m, b)
}
func (m *HelloRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_HelloRequest.Marshal(b, m, deterministic)
}
func (m *HelloRequest) XXX_Merge(src proto.Message) {
xxx_messageInfo_HelloRequest.Merge(m, src)
}
func (m *HelloRequest) XXX_Size() int {
return xxx_messageInfo_HelloRequest.Size(m)
}
func (m *HelloRequest) XXX_DiscardUnknown() {
xxx_messageInfo_HelloRequest.DiscardUnknown(m)
}

var xxx_messageInfo_HelloRequest proto.InternalMessageInfo

func (m *HelloRequest) GetName() string {
if m != nil {
return m.Name
}
return ""
}

// The response message containing the greetings
type HelloReply struct {
Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}

func (m *HelloReply) Reset() { *m = HelloReply{} }
func (m *HelloReply) String() string { return proto.CompactTextString(m) }
func (*HelloReply) ProtoMessage() {}
func (*HelloReply) Descriptor() ([]byte, []int) {
return fileDescriptor_ca53982754088a9d, []int{1}
}

func (m *HelloReply) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_HelloReply.Unmarshal(m, b)
}
func (m *HelloReply) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_HelloReply.Marshal(b, m, deterministic)
}
func (m *HelloReply) XXX_Merge(src proto.Message) {
xxx_messageInfo_HelloReply.Merge(m, src)
}
func (m *HelloReply) XXX_Size() int {
return xxx_messageInfo_HelloReply.Size(m)
}
func (m *HelloReply) XXX_DiscardUnknown() {
xxx_messageInfo_HelloReply.DiscardUnknown(m)
}

var xxx_messageInfo_HelloReply proto.InternalMessageInfo

func (m *HelloReply) GetMessage() string {
if m != nil {
return m.Message
}
return ""
}

func init() {
proto.RegisterType((*HelloRequest)(nil), "helloworld.HelloRequest")
proto.RegisterType((*HelloReply)(nil), "helloworld.HelloReply")
}

func init() {
proto.RegisterFile("demo.proto", fileDescriptor_ca53982754088a9d)
}

var fileDescriptor_ca53982754088a9d = []byte{
// 164 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x4a, 0x49, 0xcd, 0xcd,
0xd7, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0xca, 0x48, 0xcd, 0xc9, 0xc9, 0x2f, 0xcf, 0x2f,
0xca, 0x49, 0x51, 0x52, 0xe2, 0xe2, 0xf1, 0x00, 0xf1, 0x82, 0x52, 0x0b, 0x4b, 0x53, 0x8b, 0x4b,
0x84, 0x84, 0xb8, 0x58, 0xf2, 0x12, 0x73, 0x53, 0x25, 0x18, 0x15, 0x18, 0x35, 0x38, 0x83, 0xc0,
0x6c, 0x25, 0x35, 0x2e, 0x2e, 0xa8, 0x9a, 0x82, 0x9c, 0x4a, 0x21, 0x09, 0x2e, 0xf6, 0xdc, 0xd4,
0xe2, 0xe2, 0xc4, 0x74, 0x98, 0x22, 0x18, 0xd7, 0xc8, 0x93, 0x8b, 0xdd, 0xbd, 0x28, 0x35, 0xb5,
0x24, 0xb5, 0x48, 0xc8, 0x8e, 0x8b, 0x23, 0x38, 0xb1, 0x12, 0xac, 0x4b, 0x48, 0x42, 0x0f, 0x61,
0x9f, 0x1e, 0xb2, 0x65, 0x52, 0x62, 0x58, 0x64, 0x0a, 0x72, 0x2a, 0x95, 0x18, 0x9c, 0x84, 0xa2,
0x04, 0x32, 0xf3, 0xf5, 0xd2, 0x8b, 0x0a, 0x92, 0xf5, 0x52, 0x2b, 0x12, 0x73, 0x0b, 0x72, 0x52,
0x8b, 0x93, 0xd8, 0xc0, 0xae, 0x37, 0x06, 0x04, 0x00, 0x00, 0xff, 0xff, 0x8f, 0x32, 0x71, 0x48,
0xcb, 0x00, 0x00, 0x00,
}

// Reference imports to suppress errors if they are not otherwise used.
var _ context.Context
var _ grpc.ClientConnInterface

// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
const _ = grpc.SupportPackageIsVersion6

// GreeterClient is the client API for Greeter service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
type GreeterClient interface {
// Sends a greeting
SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error)
}

type greeterClient struct {
cc grpc.ClientConnInterface
}

func NewGreeterClient(cc grpc.ClientConnInterface) GreeterClient {
return &greeterClient{cc}
}

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

// GreeterServer is the server API for Greeter service.
type GreeterServer interface {
// Sends a greeting
SayHello(context.Context, *HelloRequest) (*HelloReply, error)
}

// UnimplementedGreeterServer can be embedded to have forward compatible implementations.
type UnimplementedGreeterServer struct {
}

func (*UnimplementedGreeterServer) SayHello(ctx context.Context, req *HelloRequest) (*HelloReply, error) {
return nil, status.Errorf(codes.Unimplemented, "method SayHello not implemented")
}

func RegisterGreeterServer(s *grpc.Server, srv GreeterServer) {
s.RegisterService(&_Greeter_serviceDesc, srv)
}

func _Greeter_SayHello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(HelloRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(GreeterServer).SayHello(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/helloworld.Greeter/SayHello",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(GreeterServer).SayHello(ctx, req.(*HelloRequest))
}
return interceptor(ctx, in, info, handler)
}

var _Greeter_serviceDesc = grpc.ServiceDesc{
ServiceName: "helloworld.Greeter",
HandlerType: (*GreeterServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "SayHello",
Handler: _Greeter_SayHello_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "demo.proto",
}

可以看到我们之前协议里定义的SayHello接口转换成以下的样子。

1
2
3
4
5
// GreeterServer is the server API for Greeter service.
type GreeterServer interface {
// Sends a greeting
SayHello(context.Context, *HelloRequest) (*HelloReply, error)
}

通过proto生成了gRPC框架可以处理的桩代码。

2.5、RPC执行流程

  • 执行client调用,触发请求
  • client调用本地stub,对请求参数进行封装。
  • 将封装好的请求包交给client的rpc框架,rpc框架进行打包发送。
  • client发送请求到server(网络调用)。
  • server端的rpc框架接收到消息包。
  • server将消息报交给server stub对参数进行解包。
  • 获取解包参数,进行逻辑处理生成结果。
  • 最后反向执行以上流程,发送reponse给client。

2.6、RPC、TPC、HTTP

​ RPC最开始介绍过,就是Remote Procedure Call(远程调用),他代表一种调用方式,RPC可以通过HTTP协议实现,也可以通过Socket实现。既然Socket与HTTP都可以实现,为什么不直接用Socket或者HTTP呢?制定私有协议,在封装一下Socket或HTTP真的就香了吗?

​ 首先谈Socket,写过的朋友都知道,想把它弄得很透彻,需要把那几本厚厚的Socket编程好好看看,为了解决门槛高的问题,公司会组建一个通信组,实现一个通用的Socket组件库,业务同学直接调用。调用方和服务提供方拉个会,把入参格式,出参格式,ip,端口都统一定义好,开始通信,如果有一方将之前约得好的规则改变,那整个通信就有可能不通了。

​ 再谈HTTP,虽然HTTP实现调用都很简单,但是当我们的系统拥有上百个接口的时候,RPC的协议管理就体现出优势了。而且RPC的调用非常方便简洁,大部分的RPC框架也都是长连接,有效减少3次握手带来的网络开销。

2.7、总结

整篇基本上围绕之前提出的问题来描述,都是基础常识,也是奔着扫盲的初衷。就是因为这种RPC实现了简单的接口级别调用,现在大量的微服务架构得以推广,实践。