Netty学习(一)
Java1.4版本引入NIO概念,实现了对“多路复用IO”的支持,Java1.7版本引入AIO概念。AIO是最晚提出的,理应是更先进的技术,但是并没有大规模的在商业领域应用。Unix提供了五种参考网络模型,在linux领域,并没有异步IO网络模型的成熟方案(linux发行版内核不支持AIO,需要自己安装扩展包)。JAVA的新版本的AIO,还是用采用多路复用IO实现。在Windows平台通过IOCP协议实现,可参考 IOCP浅析。
Netty是一个简单却不失强大的架构。这个架构由三部分组成——缓冲(Buffer)、通道(Channel)、事件模型(Event Model)——所有的高级特性都构建在这三个核心组件之上。
一个Netty程序开始于Bootstrap类,Bootstrap类是Netty提供的一个可以通过简单配置来设置或"引导"程序的一个很重要的类。启动Sever端,需要初始化ServerBootStrap。定义两个EventLoopGroup,分别处理连接请求和socket数据传输。Netty巧妙的把接收请求和处理请求,都抽象成ChannelHanndler处理,模式上更加统一。通过阅读代码,接收请求的处理如下:接到连接请求后,设置其初始化参数,然后注册到childGroup处理。
SingleThreadEventLoopGroup 一个线程处理所有的Channel
ThreadPerChannelLoopGroup 每个线程处理一个channel
MultiThreadEventLoopGroup 通过线程组处理channel
NIOEventLoopGroup
EpollEventLoopGroup 根据Selecter的不同实现,不同的处理策略。NIOEventLoopGroup,默认采用sellect方式,参考JAVA NIO实现。EpollEventLoopGroup只能在linux平台使用,更高效。
ChannelPipline:内部包含一个ChannelHandlerContext的链表数据结构,每次从header item开始,调用context.invoke函数,开始处理流程。
ChannelHandlerContext:在Channelhandler中,调用context的fire函数;fire函数在pipline中查找需要执行的下一个context,调用context.invoke函数;调用channelhandler的函数。
ChannelHandler:channelChandler分为inbound和outbond,inbound处理接收消息,outbound处理传出。如上所示,channelInboundHandler中定义了一系列的函数,一般情况下,编写Netty程序,只需要在ChannelHandler中处理业务逻辑,编程模型相当简单。
1)、线程模型 Reactor单线程模型;
作为NIO服务端,接收客户端的TCP连接; 作为NIO客户端,向服务端发起TCP连接; 读取通信对端的请求或者应答消息; 向通信对端发送消息请求或者应答消息。 Reactor多线程模型; 有专门一个NIO线程-Acceptor线程用于监听服务端,接收客户端的TCP连接请求; 网络IO操作-读、写等由一个NIO线程池负责,线程池可以采用标准的JDK线程池实现,它包含一个任务队列和N个可用的线程,由这些NIO线程负责消息的读取、解码、编码和发送; 1个NIO线程可以同时处理N条链路,但是1个链路只对应1个NIO线程,防止发生并发操作问题。 主从Reactor多线程模型。 为了解决1个服务端监听线程无法有效处理所有客户端连接的性能不足问题,有一组NIO线程处理服务气短的监听。 如上的几种模式,ServerBootStrap.goup初始化时设置,我们的例子,采用的是主从Reactor多线程模型。
2)、Zero Copy Netty的接收和发送ByteBuffer采用DIRECT BUFFERS,使用堆外直接内存进行Socket读写,不需要进行字节缓冲区的二次拷贝。如果使用传统的堆内存(HEAP BUFFERS)进行Socket读写,JVM会将堆内存Buffer拷贝一份到直接内存中,然后才写入Socket中。相比于堆外直接内存,消息在发送过程中多了一次缓冲区的内存拷贝。 ByteBufAllocator 通过ioBuffer分配堆外内存:
public ByteBuf ioBuffer() {
return PlatformDependent.hasUnsafe() ? this.directBuffer(256) : this.heapBuffer(256);
}
Netty提供了组合Buffer对象,可以聚合多个ByteBuffer对象,用户可以像操作一个Buffer那样方便的对组合Buffer进行操作,避免了传统通过内存拷贝的方式将几个小Buffer合并成一个大的Buffer。
private int addComponent0(boolean increaseWriterIndex, int cIndex, ByteBuf buffer) {
...... wasAdded = this.components.add(c); ...... return var11;
}
Netty的文件传输采用了transferTo方法,它可以直接将文件缓冲区的数据发送到目标Channel,避免了传统通过循环write方式导致的内存拷贝问题。linux零拷贝。
3)、Select VS Epoll
多路复用IO有多种实现方式,其中,select/poll是所有操作系统都支持的方式,提出时间较早。JAVA NIO默认实现是用的Select,windows操作系统只支持Select。Epoll提出较晚,在linux系统提供,是目前比较成熟稳定的方案。 select/poll 函数监视的文件描述符分3类,分别是writefds(可写)、readfds(可读)、和exceptfds(异常)。调用后select函数会阻塞,直到有描述符就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。当select函数返回后,可以通过遍历fdset,来找到就绪的描述符。 select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); epoll是在2.6内核中提出的,是之前的select和poll的增强版本。首先,epoll使用一组函数来完成,而不是单独的一个函数;其次,epoll把用户关心的文件描述符上的事件放在内核里的一个事件表中,无须向select和poll那样每次调用都要重复传入文件描述符集合事件集。 int epoll_create(int size); // 该函数返回的文件描述符指定要访问的内核事件表,是其他所有epoll系统调用的句柄, int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); //告诉操作系统需要关注哪些文件描述符 int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); // 等待事件发生
Netty能做什么?
1)、Rpc框架,代表 Dubbo Http VS TCP 采用Http协议,增加了http协议的解码编码过程 http协议本身无状态的,连接的复用不好;(可通过keep-alive解决一部分连接复用的问题)
Tips: 2)、服务代理,代表 NRedis-Proxy 参考: https://my.oschina.net/u/2608504/blog/787976NRedis-Proxy 是一个Redis中间件服务,第一个Java 版本开源Redis中间件,无须修改业务应用程序任何代码与配置,与业务解耦;以Spring为基础开发自定义标签,让它可配置化,使其更加容易上手;以netty 作为通信传输工具,让它具有高性能,高并发,可分布式扩展部署等特点,单片性能损耗约5%左右。