从零实现系列|RPC
0.序言
1.服务端与消息编码
2.高性能客户端
3.服务注册
这一章是干什么的?
服务端的主要工作:
- 监听端口
- 响应请求
- 解析请求
- 处理请求
- 从请求中获取方法名、参数
- 获取方法
- 调用方法
- 返回结果
这一章主要完成处理请求部分,其中,从请求中获取方法名、参数在上一章已完成。client 在发送请求时,方法名在请求头里 client.header.ServiceMethod
,参数在请求体里 call.Args
。
所以我们重点关注
- 获取方法
- 调用方法
如何获取方法
服务端通过方法名获取实际的方法,需要两步:
在启动服务端服务时,我们可以将所有可调用的方法都加到一个 map 中。
当 client 调用 hello 时,我们从 map 中找到对应的方法皆可。
由于不同类型的结构体,可能有同名的结构体方法。我们的方法名,使用 结构体实例名.方法名
来作为方法名。比如上一章中的,结构体类型 Foo
,有 Sum
方法,客户端实例化 foo
对象后,远程调用 Client.Call 时,传入的就是 foo.Sum
。
如何方法注册到 map 中
首先,先说数据存储方式,简单来说是这样的,
flowchart LR
A[Server.serviceMap] -->|map储存多个service| B[Service<br>一个 service 表示一个变量类型]
B -->|service包含一个 map 变量| C[service.method]
C -->|map 储存多个方法| D[methodType<br>一个 methodType 表示一个该变量类型的方法]
具体数据存储方式设计如下:
服务端保存着一个 serviceMap。使用 serviceMap 保存变量类型及其方法。key 是变量类型名,value 是 service 类型实例。
1 |
|
service 类型定义如下,一个 service 表示一个变量类型。其中,也包含一个 map,保存着这个变量的方法。key 是方法名,value 是 methodType 类型实例。
1 |
|
methodType 类型定义如下,一个 methodType 表示一个方法。
1 |
|
最后,下面将以一个实际例子,演示如何将方法注册到 map 中。
graph LR
A[startServer, 启动服务器] --> B[Server.Register , 为 Foo 类型注册服务]
B --> C[newService, 为 Foo 类型创建 service 结构体]
C --> D[获取 rcvr 值, 类型名和类型]
C --> G[service.registerMethods, 获取方法列表]
G --> K[检查方法的参数和返回值]
K --> L[保存方法到 map 变量 service.method]
- 启动服务,将 Foo 类型注册到 rpc server 中。
1 |
|
- 为 Foo 类型创建 service 对象,并保存到 server 的 map 中。
1 |
|
- 创建 service 对象、获取对象的方法、检查方法参数和返回值,最后将方法保存到 map 中。
1 |
|
如何从 map 获取方法
在 readRequest 时,从请求头中,获取方法名;根据方法名在对应的 service 的 map 中获取到方法、传入参数、传出参数。
graph LR
A[Server.readRequest] -->|解析请求头| B[Server.readRequestHeader]
B-->|返回请求头,包含方法名| A
A -->|获取方法| C[Server.findService]
A -->|获取方法传入参数,传出参数| D[GobCodec.ReadBody]
E[request]
C-->|方法保存到 request| E
D-->|传入参数,传出参数保存到 request| E
- request 储存了一次远程调用请求的信息,方法、传入参数、传出参数指针。它的定义如下。
1 |
|
- readRequest 获取方法名、方法、传入参数、传出参数,并储存在 request 实例中,为方法调用做准备。
1 |
|
另外,需要注意一下,
- 创建传入参数时,需区分值类型还是指针类型。
- 创建传出参数时,需对 map、slice 特殊处理。(因为 reflect.New 初始化时,map、slice 都是 nil)
1 |
|
如何调用方法
在 handleRequest 时,通过 service.call 利用反射来调用函数。这里主要学习这个用法。
graph LR
A[Server.serveCodec]-->|获取请求信息| B[Server.readRequest]
A -->|调用方法| C[Server.handleRequest]
C --> D[request.svc.call, 也就是 Service.call]
D -->|使用反射调用| E[methodType.method.Func.Call]
1 |
|
1 |
|
更深入了解反射
请看这篇 https://www.aimtao.net/go#11-reflect
完整代码
1 |
|
1 |
|
1 |
|
1 |
|
1 |
|
4.超时处理
哪些地方需要超时处理
简单来看整个流程:
客户端:
拨号连接 ✓
远程调用(发送数据)✓
等待服务端处理 ✓
接收返回的结果(接收数据)✓
服务端:
端口监听
接收请求信息(接收数据)✓
调用方法处理请求信息(处理数据)✓
返回请求结果(发送数据)✓
以上打 ✓ 的步骤,均可能出现超时,主要为,
- 客户端建立连接超时
- 发送数据
- 客户端/服务端写报文时超时
- 处理数据
- 服务端调用方法超时
- 接收数据
- 客户端等待服务端响应超时
- 客户端/服务端读取报文超时
基于这个超时的情景,我们可以在以下三个地方设置超时处理机制。
客户端:
- 建立连接
Client.call()
的整个过程(包含发送数据、等待处理、接收数据)
服务端:
Server.handleRequest()
的整个过程(包括接收数据、处理数据、发送数据)
建立连接的超时处理
启动一个 goroutine 来拨号建立连接,使用 select 设置超时器,阻塞等待拨号结果,如果在接收拨号结果之前,先收到了超时信号,则进行超时处理。
1 |
|
超时的时长,设置在 Option struct 内,便于用户自定义。
1 |
|
Call 的超时处理
把选择权交给用户。
1 |
|
用户如何使用?
1 |
|
handleRequest 超时处理
这里先不管读取数据了,仅对 handleRequest 做超时处理。handleRequest 其中又分为两个步骤,处理数据、发送数据,仅对处理数据做超时处理。
设置两个 channel,一个表示 req.svc.call
完成,一个表示 server.sendResponse
完成。当 req.svc.call
完成后,就不用管超时时间是否到达,继续让 server.sendResponse 发送数据。
1 |
|
测试代码
测试客户端处理连接超时的情况
1 |
|
测试客户端处理调用超时和服务端处理超时的情况
1 |
|
完整代码
1 |
|
1 |
|
1 |
|
1 |
|
1 |
|