上一篇:前端也能懂的RPC(上)
这一节解释,在RPC调用中到底发生了什么。
由于我用的RPC框架是HSF,其他的RPC框架也一样,都大同小异了,看架构图再思考RPC更容易理解一些。如下:
HSF架构
HSF作为一个纯客户端架构的RPC框架,没有服务端集群,所有HSF服务调用均是通过服务消费方(Consumer)与服务提供方(Provider)点对点进行。为了实现整套分布式服务体系,HSF还需要依赖以下外部系统。
一、以传输对象为视角的调用流程
1、动态代理
上一节讲到,在midway项目中调用hsf服务,其实就是调用hsfClient.createConsumer.consumer.invoke.call......
。
这个代理的作用是屏蔽调用细节,让使用者调用hsf服务时,拥有像调用本地已有调用远程的体验。
2、序列化
序列化 (Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程。
上节提到,数据在网络中以二进制的形式进行传输,那么在RPC的调用里,数据是怎么从一个对象变成二进制数据的呢?
a. 序列化格式
前端最熟悉的格式就是JSON了,除了JSON之外,服务端的序列化格式还有Hessian、Protobuf等等。这里以JSON为例讲解。
b. 序列化流程
- 假设我们有一个入参对象
{
id: 123,
name: "apple",
arr: [1,2,3,'b']
}
这个对象不是一个标准的json对象,先把它序列化成标准的JSON对象
- JSON
{
id: 123,
name: "apple",
arr: ["1","2","3","b"]
}
-
以某种编码方式将其转换成二进制数据
IO线程
二进制数据将会经过TCP传输给服务提供方,服务提供方从TCP通道里收到二进制数据。
这个转变成二进制数据从而封装成TCP包传输的工作发生在IO线程中。额外的信息
有了二进制数据,还需要知道调用的方法名;
为了让接收方能够反序列化成功,需要传输序列化方式;
二进制数据过大时,需要分包,那么需要字段用以描述包序号;
为了确保数据完整性,需要整体长度……
那么这些信息应该怎么样去放置呢?
使用协议来把上述提到的协议体和协议头内容通过一定的方式组合在一起。
- 协议
协议的作用就是用于分割二进制数据流。
可以使用现有的协议,如HTTP2.0协议,或者自己设计一个协议
至此,一个入参对象就成功转变成了可以在网络间传输的RPC数据包。
3、网络通信——IO多路复用
网络通信是整个RPC调用的基础,选择哪种网络IO模型更适合这个场景呢?
IO 密集型系统大部分时间都在执行 IO 操作,这个 IO 操作主要包括网络 IO 和磁盘 IO,以及与计算机连接的一些外围设备的访问。
我们开发的绝大多数业务系统,都是IO密集型系统,很少有非常耗时的计算,更多的是网络收发数据,读写磁盘和数据库这些 IO 操作。这样的系统基本上都是 IO 密集型系统,特别适合使用异步的设计来提升系统性能。
- 复习一下node TCP发送请求
var net = require('net');
var tcp_server = net.createServer(); // 创建 tcp server
var Sockets = {};
var SocketID = 1;
// 监听 端口
tcp_server.listen(8888,function (){
console.log('tcp_server listening 8888');
});
// 处理客户端连接
tcp_server.on('connection',function (socket){
console.log(socket.address());
Sockets[SocketID] =socket;
SocketID++;
DealConnect(socket)
})
tcp_server.on('error', function (){
console.log('tcp_server error!');
})
tcp_server.on('close', function () {
console.log('tcp_server close!');
})
// 处理每个客户端消息
function DealConnect(socket){
socket.on('data',function(data){
data = data.toString();
// 向所有客户端广播消息
for(var i in Sockets){
Sockets[i].write(data);
}
// socket.write(data);
console.log('received data %s',data);
})
// 客户端正常断开时执行
socket.on('close', function () {
console.log('client disconneted!');
})
// 客户端正异断开时执行
socket.on("error", function (err) {
console.log('client error disconneted!');
});
}
上面代码使用server.listen去监听端口8888,这个端口就是一个通道。如果有请求接收过来,则执行传入'connect'的回调函数。使用 socket.write往通道写数据传回去。
一个 TCP 连接建立后,用户代码会获得一个用于收发数据的通道,每个通道会在内存中开辟两片区域用于收发数据的缓存。
同步阻塞IO
同步阻塞IO就是应用进程发起IO系统调用后,应用进程被阻塞直到等待到数据,经过操作系统通知,从缓存拿到接收来的数据拷贝到用户内存,然后接触阻塞状态,运行业务逻辑。
这样一来,每一个IO操作都要占用线程,直到IO操作结束。
如果有大量的请求涌入,那些等待着的IO线程因为阻塞不能及时处理,就是极大的资源浪费了。
IO多路复用
因为同步阻塞IO对于高并发场景并不适用,IO多路复用是怎么解决这个场景的?
相较于同步阻塞IO,IO多路复用就是将多个通道复用在一个复用器上,接收到任意一个数据后,线程会将这个数据从缓存拷到内存中处理,处理完成后线程再重新回到等待状态。这样一个线程就能处理多个socket了。
参考:
1、https://help.aliyun.com/document_detail/149498.html?spm=5176.22414175.sslink.1.5c6c4515nPbJT9
2、https://time.geekbang.org/column/article/118322
3、https://time.geekbang.org/column/article/204696
4、https://www.cnblogs.com/ay-a/p/9822057.html