【Java手写RPC框架系列-1】—— 基础知识准备:RPC+Netty
代码随想录知识星球介绍
https://articles.zsxq.com/id_m76jd72243bi.html
基于Netty手写实现RPC
https://www.cnblogs.com/mic112/p/15565795.html
项目背景与介绍
- RPC:
- 远程过程调用协议:客户端在不知道调用细节的情况下,调用存在于远程计算机上的某个对象,就像调用本地应用程序中的对象一样;
- 要点
- RPC是协议
- 网络协议或者网络IO模型对其透明;RPC客户端认为自己在调用本地对象;不关心传输层协议
- 信息格式透明:参数以某种信息格式传递给网络上另一台计算机,信息格式是怎么构成的,调用者不关心
- 跨语言能力:不清楚远程服务器的应用程序是使用什么语言运行;对调用方来说,无论服务器方使用什么语言,本次调用都应该成功;返回值也应该按照调用方程序语言所能理解的形式进行描述。
- 常用RPC技术或框架
- 应用级
- 远程通信协议:RMI、Socket、SOAP(HTTP XML)、REST(HTTP JSON)
- 通信框架:MINA和Netty
- 目的:仿照市场主流的RPC框架设计思想,使用java手动实现一个高性能、高可用性的RPC框架
业内主流RPC
- Thrift:thrift是一个软件框架,用来进行可扩展且跨语言的服务的开发。它结合了功能强大的软件堆栈和代码生成引擎,以构建在 C++, Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, JavaScript, Node.js, Smalltalk, and OCaml 这些编程语言间无缝结合的、高效的服务。
- Dubbo:Dubbo是一个分布式服务框架,以及SOA治理方案。其功能主要包括:高性能NIO通讯及多协议集成,服务动态寻址与路由,软负载均衡与容错,依赖分析与降级等。 Dubbo是阿里巴巴内部的SOA服务化治理方案的核心框架,Dubbo自2011年开源后,已被许多非阿里系公司使用。
基础需求
- 场景:
服务端B:有一个用户表
1.UserService 里有一个功能: getUserByUserId(Integer id)
2.UserServiceImpl 实现了UserService接口和方法
客户端A:- 调用getUserByUserId方法, 内部传一个Id给服务端,服务端查询到User对象返回给客户端
- 实现过程:
-
客户端A
- 调用getUserByUserId方法时,内部将调用信息处理后发送给服务端B,告诉B我要获取User
- 外部调用方法,内部进行其它的处理——这种场景我们可以使用动态代理的方式,改写原本方法的处理逻辑。比如,当我们正常调用getUserByUserId方法,代码逻辑是去数据库中找user,但是在当前远程调用的场景下肯定不能走这样的逻辑;我们通过动态代理的方式,绕过【去数据库查询】这原始的逻辑,改成封装信息发送到B中请求调用服务
-
服务器B:
- 监听A请求,接收A的调用信息,并根据信息得到A想调用的服务与方法
- 根据信息找到对应的服务,进行调用后将结果发送回给A
-
A和B之间通信
- Java的Socket网络编程通信
- 为了方便A,B直接对信息进行处理,将请求信息和返回信息封装成统一的消息格式
-
网络IO通信
- 传统BIO模式/同步阻塞IO:客户端向服务端发起一个数据读取请求,客户端在收到服务端返回数据之前,一直处于阻塞状态,直到服务端返回数据后完成本次会话。
- 一个连接一个线程
- 客户端有连接请求时,服务器端启动一个线程进行处理
- 适合场景:HTTP请求
- 阻塞点:
- 服务端接收客户端连接时的阻塞;
- 客户端和服务端IO通信时,数据未就绪情况下阻塞;
- NIO/非阻塞IO:客户端向服务端发起请求时,如果服务端的数据未就绪的情况下, 客户端请求不会被阻塞,而是直接返回。
- 客户端只能通过轮询的方式来获得请求结果
- NIO仍然有一个弊端,就是轮询过程中会有很多空轮询,而这个轮询会存在大量的系统调用(发起内核指令从网卡缓冲区中加载数据,用户空间到内核空间的切换),随着连接数量的增加,会导致性能问题。
- 多路复用机制
- 单个进程监视多个文件描述符(fd)
- 一旦某个描述符就绪(一般是读就绪或写就绪),能够通知程序进行相应的读写操作
- 常见多路复用方式:
- 【select】:进程可以通过把一个或者多个fd传递给select系统调用,进程会阻塞在select操作上,这样select可以帮我们检测多个fd是否处于就绪状态,这个模式有两个缺点
- 当前进程需要线性轮询所有的fd,也就是监听的fd越多,性能开销越大
- 在单个进程中能打开的fd是有限制的,默认是1024
- 【poll】:
- 【epoll】:inux还提供了epoll的系统调用,epoll是基于事件驱动方式来代替顺序扫描,因此性能相对来说更高
- 当被监听的fd中,有fd就绪时,会告知当前进程具体哪一个fd就绪,那么当前进程只需要去从指定的fd上读取数据即可,另外,epoll所能支持的fd上线是操作系统的最大文件句柄,这个数字要远远大于1024
- 【select】:进程可以通过把一个或者多个fd传递给select系统调用,进程会阻塞在select操作上,这样select可以帮我们检测多个fd是否处于就绪状态,这个模式有两个缺点
什么是fd:在linux中,内核把所有的外部设备都当成是一个文件来操作,对一个文件的读写会调用内核提供的系统命令,返回一个fd(文件描述符)。而对于一个socket的读写也会有相应的文件描述符,成为socketfd。
【使用NIO的api来完成多路复用机制,实现伪异步IO。】
【Netty的I/O模型是基于非阻塞IO实现的,底层依赖的是JDK NIO框架的多路复用器Selector来实现。一个多路复用器Selector可以同时轮询多个Channel,采用epoll模式后,只需要一个线程负责Selector的轮询,就可以接入成千上万个客户端连接。】
- 异步IO
- 数据就绪后,客户端不需要发送内核指令从内核空间读取数据,而是系统会异步把这个数据直接拷贝到用户空间,应用程序只需要直接使用该数据即可;
- 数据就绪后,客户端不需要发送内核指令从内核空间读取数据,而是系统会异步把这个数据直接拷贝到用户空间,应用程序只需要直接使用该数据即可;
Netty —— 高性能通信框架
Netty提供了上述三种Reactor模型的支持,我们可以通过Netty封装好的API来快速完成不同Reactor模型的开发,这也是为什么大家都选择Netty的原因之一,除此之外,Netty相比于NIO原生API,它有以下特点:
- 提供了高效的I/O模型、线程模型和时间处理机制
- 提供了非常简单易用的API
- 对数据协议和序列化提供了很好的支持
- 稳定性,Netty修复了JDK NIO较多的问题
- 可扩展性在同类型的框架中都是做的非常好的
- 性能层面的优化
- 对象池复用
- 零拷贝技术