Netty与RPC知识点总结

Netty原理

Netty是一个高性能的,异步事件驱动的NIO框架,基于JAVA NIO提供的API实现。它提供了堆TCP,UDP和文件传输的支持,作为一个异步NIO的框架,Netty的所有IO操作都是异步非阻塞的,通过Future-listener机制,用户可以方便的主动获取或者通过通知机制或得IO操作的结果。

Netty高性能

在IO编程过程中,当需要同时处理多个客户端接入请求的时候,可以利用多线程或者IO多路复用技术进行处理,IO多路复用技术通过吧多个IO阻塞复用到同一个select的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求,与传统的多线程/多进程模型相比,I/O多路复用的最大优势就是系统开销较小,系统不需要创建新的额外的进程或者线程,也不需要维护这些进程和线程的运行,降低了系统的维护代码量,节省了系统的资源。
与Socket类和ServerSocket类相对应,NIO也提供了SocketChannel和ServerSocketChannel两种不同的套接字通道实现。

NIO服务端通信序列图

在这里插入图片描述

  • 打开ServerSocketChannel
  • 绑定鉴定地址InetSocketAddress
  • 创建Selector,启动线程
  • 将ServerSocketChannel注册到Selector,监听SelectionKey.OP_ACCEPT
  • Selector轮询就绪的key
  • accpet新连接,HandleAccept()处理新的客户端接入
  • 设置新建客户端连接的Socket
  • 向Selector注册监听读操作,SelectionKey.OP_READ
  • handlerRead()异步读请求到ByteBuffer
  • decode请求
  • 异步写byteBuffer到SocketChannel

NIO客户端通信序列图

在这里插入图片描述

  1. 打开socketChannel
  2. 设置Socketchannel为非阻塞模式,同时设置TCP参数
  3. 异步连接服务器
  4. 判断连接结果,如果连接成功则调到步骤10
  5. 向Reactor线程的多路复用器注册OP_CONNECT事件
  6. 创建Selector启动线程
  7. Selector轮询就绪的Key
  8. handleConnect
  9. 判断连接完成,完成执行步骤10
  10. 向Selector注册OP_READ
  11. handleRead异步请求读取消息到ByteBuffer
  12. decode请求
  13. 异步写ByteBuffer到SocketChannel

Netty的IO线程是NIOEventLoop由于聚合了多路复用器Selector,可以同时并发的处理成百上千客户端channel,由于读写操作都是非阻塞的,这就重复提升IO线程的运行效率,避免由于频繁IO阻塞导致的线程挂起

零拷贝

  • Netty的接受和发送ByteBuffer采用DIRECT BUFFER,使用堆外的内存进行Socket读写,不需要进行字节缓冲区的二次拷贝,如果使用传统的堆内存HEAP BUFFERS进行读写,JVM会将堆内存BUFFER拷贝一份到直接内存中,然后才写入Socket中。相比对堆外直接内存,消息在发送过程中多了一次缓冲区的内存拷贝
  • Netty提供了组合Buffer对象,可以聚合多个ByteBuffer,用户可以像操作一个Buffer那样方便的堆和Buffer进行操作,避免了传统通过内存拷贝的方式将几个小Buffer合并成一个大的BUFFER
  • Netty的文件传输采用了transferTo方法,它可以直接将文件缓冲区的数据发送到目标channel,避免了传统通过循环write方式导致的内存拷贝问题。

Netty内存池

随着JVM虚拟机和JIT即使编译器计数的发展,对象的分配和回收是个非常轻量级的工作,但是对于缓冲区Buffer,情况却稍有不同,特别是对于堆外直接内存的分配和回收,是一件耗时的操作。为了尽量的重用缓冲区,Netty提供了基于内存池的缓冲区重用的机制。

高效的Reactor线程模型

reactor线程模型有三种,Reactor单线程模型,Reactor多线程模型,主从Reactor多线程模型

Reactor单线程模型

Reactor单线程模型,值得是所有的IO操作都是在同一个NIO线程上面完成的,NIO线程的职责如下:

  • 作为NIO服务端,接受客户端的TCP连接
  • 作为NIO客户端,向服务端发起TCP连接
  • 读取通信对端的请求或者应答消息
  • 向通信对端发送请求消息或者应答消息

在这里插入图片描述
一个线程需要监听连接,当连接有读写事件发生的时候,这个线程还要去处理读写事件。

Reactor多线程模型

Reactor多线程模型与单线程最大的区别就是有一组NIO线程处理IO操作,有专门的一个NIO线程-Acceptor线程用于监听服务端,接受客户端的TCP连接请求,网络IO 操作读写等由一个NIO线程池负责。线程池可以采用标准的JDK线程池实现,它包含一个任务队列和N个可用的线程,由这些NIO线程负责消息的读取,编码,解码和发送。

在这里插入图片描述一个线程需要监听连接,当连接有读写事件发生的时候,去交由线程池去处理。

主从Reactor多线程模型

服务端用于接收客户端连接的不再是一个单独的NIO线程,而是一个独立的NIO线程池,Acceptor接收到客户端TCP连接请求处理完成之后,将新创建的SocketChannel注册到IO线程池的某个IO线程上,由它负责SocketChannel的读写和编码的工作。Acceptor线程池仅仅只用于客户端的登录,连接,一旦链路简历成果,就将链路注册到后端的subReactor线程池的IO线程上,由IO线程池负责后续的IO操作。
在这里插入图片描述
简单来说 就是会有两个Reactor,一个主Reactor专门负责监听连接,当有连接接入的时候,生成socketchannel注册到SubReactor上,由SubReactor去处理这个连接的读写事件。

串行无锁化

Netty采用了串行无锁化设计,在IO线程内部进行串行操作,避免了多线程竞争导致的性能下降,表面上看,串行化似乎CPU利用率不高,并发程度不够高,但是通过调整NIO线程池的线程参数,可以同时启动多个串行化的线程并行运行,这种局部无锁化的串行线程设计相比一个队列多个工作线程的模型性能更优。
在这里插入图片描述

Netty的NIOEventLoop(Selector)读取到消息之后,直接调用ChannlePipeLine的firechannelRead(Object msg),期间不进行线程的切换。这种串行化处理方式避免了多线程操作导致的锁的竞争,从性你那个角度看是最优的。

NettyRPC实现

轻量级分布式 RPC 框架

概念

RPC即RemoteProcedureCall(远程过程调用),即调用远程计算机山的服务,就想调用本地服务一样。RPC可以很好的解耦系统,如WebService就是一种基于Http协议的RPC,其具有良好的跨平台性,但是其性能却不如基于TCP洗衣的RPC。会两方面直接影响RPC的性能,一个是传输的方式,一个是序列化。

众所周知,TCP是传输层协议,而HTTP是应用层协议,而传输层较应用层更加的底层,在数据传输方面,越底层愉快,因此在一般情况下,TCP一定比HTTP块,就序列化方式而言,Java提供了默认的序列化方式,但是在高并发的情况下,这种方式会带来一些性能瓶颈,于是市面上出现了一些列优秀的序列化框架。比如protobuf,kryo,hessian,jackson等,他们可以取代Java默认的序列化,从而提供更高效的性能。
我们将服务部署在分布式环境下的不同的节点上,通过服务注册的方式,让客户端来自动发现当前可用的服务,并调用这些服务。这需要一种服务注册表组件,让他来注册分布式环境下所有的服务地址。
在这里插入图片描述

核心流程

  • 服务消费方client调用本地方式调用服务
  • 客户端代理接受到调用后负责讲方法,参数等组装成能够进行网络传输的消息体
  • clientsub找到服务地址,并将消息发送到服务端
  • 服务端代理收到消息后进行解码
  • 服务端代理根据解码结果调用本地的服务
  • 本地服务执行并将结果返回给服务端代理
  • 服务端代理将返回结果打包成消息并发送值消费方
  • 客户端代理接受到消息,并进行解码
  • 服务消费方得到最终的结果。

RPC的目标就是要将 2 - 8 步都封装起来让用户对这些细节透明,Java一般使用动态代理的方式实现远程调用在这里插入图片描述

封装RPC请求类的构成
  • 接口名称:HelloService,如果不穿,服务端就不知道调用哪个接口
  • 方法名:一个接口内可能会有很多方法,如果不传方法名服务端也就不知道调用哪个方法
  • 参数类型和参数值:参数类型有很多,比如有bool,int,long,double,string,map,list,甚至如struct(class)以及相应的参数值
  • 超时时间
  • requestID,表示唯一的请求id
  • 服务端返回的消息,一般包括以下的内容,返回值+状态 code+ requestID

核心问题(线程暂停、消息乱序)

如果使用netty的话,一般会用channel.writeandFlush方法来发送消息的二进制串,这个方法调用后对于整个远程调用来说是一个异步的,即对于当前线程来说,将请求发送出来折后,线程就可以继续往后执行了,对于服务端的结果,是服务端处理完成之后,再以消息的形式发送给客户端。于是出现了以下两个问题。

  • 怎么让当前前程暂停,等结果回来之后,再向后执行

  • 如果有多个线程同时进行远程方法调用,这是建立在clientServer之间的socket连接上会有很多双发发送的消息船体,前后顺序也是可能随机的,server处理完结果折后,将结果消息发送给client,client收到很多消息,怎么知道那个消息结果是原先哪个线程调用的?如下图所示,线程A和线程B同时向client socket发起请求 requestA 和requestB, socket先后将requestB和requestA发送至server,而server可能将responseB先返回,尽管requestB请求到达时间更晚,我们需要一种机制保证responseA丢给ThreadA,responseB丢给responseB。
    在这里插入图片描述

  • RequestID 生成 AtomicLong
    client线程每次通过socket调用一次远程接口之前,生成一个唯一的ID,即requestID(requestID必须保证在一个socket连接里面是唯一的),一般常常使用AtomicLong从0开始累积数字生成唯一ID

  • 存放回调对用callback到全局ConcurrentHashMap
    将处理结果的回调对象callback,存放到全局ConcurrentHashMap里面(requestID,callback)

  • synchronized获取回调对象callback的锁并自旋wait
    当线程调用channel.writeAndFlush发送消息之后,紧接着执行callback的get方法试图获取远程返回的结果,在get的内部,则使用synchronized获取回调对象callback的锁,再先检测是否已经获取到的结果,如果没有,然后调用callback的wait方法,释放callback上的锁,让当前线程处于等待的状态

  • 监听消息的线程收到消息后,找到callback上的锁并唤醒
    服务端收到请求并处理之后,将response结果发送给客户端,客户端socket连接上专门监听消息的线程收到了消息,分析结果,取到requestID,再从前面的ConcurrentHashMap里面get(requestID),从而找到callback对象,再用synchronized获取callback上的锁,讲方法调用的结果设置到callback对象里面,再调用callback.notify唤醒前面处于等待状态的线程。

Proto Buffer

Proto Buffer是google的一个开源项目,它是用于结构化数据串行化的灵活,高效,自动方法的方法,例如XML,不过它比xml更小,更快,也更简单,你可以定义自己的数据结构,然后使用代码生成器生成的代码来读写这个数据结构。
在这里插入图片描述

  • Protocol Buffer序列化反序列化简单速度快的原因是

    1. 编码 解码方式简单(只需要简单的数学运算 = 位移等等)
    2. 采用Protocol Buffer自生的框架代码和编译器共同完成
  • Protocol Buffer的数据压缩效果好(即序列化后的数据量体积小)的原因是:

    1. 采用了独特的编码方法,如Varint,ZigZag的编码方法
    1. 采用T -L - V的数据存储方法:减少了分隔符的使用 & 数据存储的更加紧凑。
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值