Netty基础学习(一)

最近脑海里一直有个念头,要重新好好复习一下netty的相关理论知识,即补充自己对底层通信的知识盲区,也想把相关的观点写下来,和众多网友分享。文章主要从以下几个方面进行介绍:

  • 为什么要学习netty
  • netty有哪些基础知识点
  • 如何将netty和自己工作打通融合起来
  • netty的高阶使用

1. 为什么要学习netty

Netty是由JBOSS提供的一个Java开源框架,现为Github上的独立项目。Netty本质是一个NIO框架,通过异步、基于事件驱动的IO,用以快速开发高性能、高可靠性的网络IO程序,主要针对在TCP协议下,面向Client端的高并发应用,或者Peer-to-Peer场景下的大量数据持续传输的应用。

作为Java后端研发同学,一定都会接触到框架和网络通信,就我接触过并看过源码的开源项目,可以说基本上都是使用Netty作为网络通信的底层框架,如Dubbo、RocketMQ、Canal、otter、HBase等等。

 另一方面,netty作为高性能网络通信基础,有很多很好的设计可以借鉴到我们的日常研发过程中,比如零拷贝操作,bytebuffer理念,pipeline设计等。当然,netty的相关知识也是我们面试时重点考察的对象,所以好好学习netty一举多得。

2. netty有哪些基础知识点

基于博主对netty的理解认识,对netty高性能分为三部分,即网络IO、线程模型和内存优化:

 所以后续netty的基础知识点也将围绕这这三方面来展开:

  • 网络IO模型
  • 多线程模型
    • 设计模式
  • 内存管理
    • 高性能数据结构

3. netty的IO网络模型

netty是一个java开源框架,其IO模型是在JavaIO的基础上封装改进而来,底层关于Java IO的知识概念是一致的。

3.1 Java中的IO

在Java IO中,流从概念上来说是一个连续的数据流,既可以从流中读取数据,也可以往流中写数据。IO相关的媒介包括:

  • 文件
  • 管道
  • 网络连接
  • 内存缓存
  • System.in, System.out

IO的设计,主要是解决IO相关的操作。从数据传输的方式上,分为字节流和字符流。字节流一次性读取传输一个字节,而字符流则是以字符为单位进行读取传输。

LINUX中进程无法直接操作I/O设备,必须通过系统调用请求kernel来协助完成I/O动作。内核会为每个I/O设备维护一个缓冲区,IO输入时应用进程请求内核,内核会先看缓冲区中有没有相应的缓存数据,有数据则直接复制到进程空间,没有的话再到设备中读取。通常用户进程中的一个完整IO分为两阶段:用户进程空间<-->内核空间、内核空间<- ->设备空间。

由于CPU和内存的速度远远高于外设的速度,所以在IO编程中,就存在速度严重不匹配的问题,所以有了同步/异步,阻塞和非阻塞IO之分。

3.1.1 IO 模型

IO模型分为:BIO、NIO、IO多路复用、信号驱动IO和AIO。

BIO:进程发起IO系统调用后,进程被阻塞,转到内核空间处理,整个IO处理完毕后返回进程,操作成功则进程获取到数据。BIO阻塞时,其它应用进程还能正常执行,所以不消耗CPU时间,这种模型的CPU利用率较高。

NIO:非阻塞IO模型在内核数据没准备好,需要进程阻塞的时候,就返回一个错误,以使得进程不被阻塞;由于CPU需要不断轮询内核数据是否准备好,CPU相比利用率会低一些。

IO多路复用:多个的进程的IO可以注册到一个复用器(selector)上,然后用一个进程调用该select,,select会监听所有注册进来的IO,这一过程会被阻塞,当某一个套接字可读时返回,之后再用recvfrom吧数据从内核拷贝到进程中。 IO多路复用也被称为事件驱动IO。

信号驱动IO:当进程发起一个IO操作,会向内核注册一个信号处理函数,内核立即返回,应用程序可以继续进行。当内核数据就绪时会发送一个信号给进程,进程便在信号处理函数中调用IO读取数据。相比于非阻塞的IO轮询,信号驱动IO的CPU利用率更高。

AIO:当进程发起一个IO操作,应用进程执行aio_read系统调用会立即返回,应用程序可以继续进行,内核会在所有操作完成后向应用程序发送信号。AIO和信号驱动IO的区别是:AIO的信号是通知进程IO完成,而信号驱动IO是通知应用程序可以进行IO。

3.1.2 Java NIO

NIO即new IO,是在JDK1.4引入的,NIO和IO有相同的作用和目的,但实现方式不同,NIO主要用到的是块,所以NIO的效率要比IO高很多。

NIO的核心对象包括:

  • Buffer:在NIO中,所有的数据都是用Buffer处理的,它是NIO读写数据的中转池。Buffer实质上是一个数组,通常是一个字节数据,但也可以是其他类型的数组。
  • Channel:是一个对象,可以通过channel读取和写入数据,是IO中流的抽象。但是channel是双向的,也可以是异步读写,并且channel读写必须通过buffer。
  • Selector:是一个对象,可以同时监听多个channel上发生的事件,并且能够根据事件情况决定Channel读写。
// 打开Selector
Selector selector = Selector.open();
// 将channel注册到selector
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

Selector感兴趣的事件有SelectionKey.OP_CONNECT, SelectionKey.OP_ACCEPT, SelectionKey.OP_READ, SelectionKey.OP_WRITE。

SelectionKey表示通道channel在Selector上的注册,事件的传递是通过SelectionKey,也可以通过selectionKey获取注册的channel和对应绑定的selector。

Channel  channel  = selectionKey.channel();
Selector selector = selectionKey.selector(); 

一旦向selector注册一个或者多个通道后,就可以调用重载的select()方法,select()方法会返回读事件已经就绪的那些通道

  • int select():阻塞到至少有一个通道的事件就绪
  • int select(long timeout):与select一样,多个一个超时时间
  • int selectNow():不会阻塞,不管什么通道就绪都立刻返回,如果没有通道可选择,就返回0.

一旦调用了select()方法,它就会返回一个数值,表示一个或多个通道已经就绪,然后你就可以通过调用selector.selectedKeys()方法返回的SelectionKey集合来获得就绪的Channel。某个线程调用select()方法后阻塞了,即使没有通道就绪,也有办法让其从select()方法返回。

  • 让其它线程在调用select方法的对象上调用Selector.wakeup()方法即可,阻塞在select()方法上的线程会立马返回。
  • 如果其它线程调用了wakeup(),但是当前没有线程阻塞在select上,下一个调用select阻塞的线程会被立即唤醒。

 

3.1.3 Java NIO和netty关系

直接使用Java NIO的缺点:

  • NIO的类库和API繁杂,需要熟练掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer等才能很好使用。
  • 可靠性较弱,需要自行维护,工作量大,例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常码流的处理等问题;
  • JDK NIO的BUG,例如epoll bug,它会导致Selector空轮询,最终导致CPU 100%。

Netty是对Java NIO的封装框架,简化了NIO的使用难度,Netty特性总结如下:

  • API使用简单,开发门槛低
  • 功能强大,预置了多种编解码功能,支持多种主流协议
  • 定制能力强,可以通过ChannelHandler对通信框架进行灵活地扩展
  • 性能高,通过与其他业界主流的NIO框架对比,Netty的综合性能最优
  • 成熟、稳定,Netty修复了已经发现的所有JDK NIO BUG,业务开发人员不需要再为NIO的BUG而烦恼
  • 社区活跃,版本迭代周期短,发现的BUG可以被及时修复,同时更多的新功能会加入
  • 经历了大规模的商业应用考验,质量得到验证。

 

3.2 Netty中的NIO关键组件

3.2.1 Buffer缓冲区

缓冲区本质上是一个可以读写数据的内存块,Netty封装Buffer对象提供了一组方法,可以更轻松地使用内存块,并且跟踪和记录缓冲区的状态变化。从Channel读取或写入的数据都必须经由Buffer。

Buffer的类图结构有:

 

public abstract class ByteBuf implements ReferenceCounted, Comparable<ByteBuf> {
    public ByteBuf() {
    }
    // 存储大小
    public abstract int capacity();
    public abstract ByteBuf capacity(int var1);
    ...
    public abstract int readerIndex();
    public abstract int writerIndex();
    public abstract ByteBuf clear();
    public abstract ByteBuf markReaderIndex();
    public abstract ByteBuf resetReaderIndex();
    public abstract ByteBuf markWriterIndex();
    public abstract ByteBuf resetWriterIndex();
    ...
}

public abstract class AbstractByteBuf extends ByteBuf {
    ...
    int readerIndex;
    int writerIndex;
    private int markedReaderIndex;
    private int markedWriterIndex;
    private int maxCapacity;
    // 可读
    public ByteBuf readerIndex(int readerIndex) {
        if (readerIndex >= 0 && readerIndex <= this.writerIndex) {
            this.readerIndex = readerIndex;
            return this;
        } else {
            throw new IndexOutOfBoundsException(String.format("readerIndex: %d (expected: 0 <= readerIndex <= writerIndex(%d))", readerIndex, this.writerIndex));
        }
    }
    // 可写
    public ByteBuf writerIndex(int writerIndex) {
        if (writerIndex >= this.readerIndex && writerIndex <= this.capacity()) {
            this.writerIndex = writerIndex;
            return this;
        } else {
            throw new IndexOutOfBoundsException(String.format("writerIndex: %d (expected: readerIndex(%d) <= writerIndex <= capacity(%d))", writerIndex, this.readerIndex, this.capacity()));
        }
    }
    // 清理Buffer
    public ByteBuf clear() {
        this.readerIndex = this.writerIndex = 0;
        return this;
    }

    public ByteBuf markReaderIndex() {
        this.markedReaderIndex = this.readerIndex;
        return this;
    }
    public ByteBuf resetReaderIndex() {
        this.readerIndex(this.markedReaderIndex);
        return this;
    }
    public ByteBuf markWriterIndex() {
        this.markedWriterIndex = this.writerIndex;
        return this;
    }
    public ByteBuf resetWriterIndex() {
        this.writerIndex = this.markedWriterIndex;
        return this;
    }
    ...
}

netty的ByteBuffer有几个特性:

  • Netty的ByteBuffer提供两个指针变量用于顺序读和顺序写,ReadIndex和WriteIndex。两个指针将Buffer分成3个区域,读取的数据只能位于readIndex和writeIndex之间,可写数据位于writeIndex和capacity之间。0到readIndex之间的区域是已经读取过的区域,可以调用discardReadBytes来重用这部分区间,以节约内存,防止Buffer的动态扩张。
  • clear操作,并不会清空buffer的存储内容,而是用来充值readIndex和writeIndex,position,Mark和limit的,将他们还原为初始设置值。
  • Mark和reset,Mark操作会将当前的位置指针备份到Mark变量中,调用reset后会将指针恢复到备份的位置,这种操作主要是因为对于某些读写需要回滚。

从存储结构上,buffer分为堆内存和直接内存buffer。

  • 堆内存的特点是分配和回收速度快,也能被JVM自动管理,缺点是做Socket IO 操作时,需要从用户态拷贝到内核态,多一次复制操作。
  • 直接内存buffer在堆外分配,缺点是分配和回收速度慢,但是做Socket IO操作时,会少一次内存拷贝。

结合对内对外内存分配的特点,在IO通信的时候使用直接内存分配,可以减少JVM的内存分配限制,还可以直接使用zeroCopy技术。后端业务编码的时候,可以直接使用HeapByteBuffer。

Netty的容量自动扩展,在小于4M的时候,采用倍增;大于等于4M时,采用每次增加4M。

 补:select, poll和epoll

在OS上,对多个IO事件的监听是通过select/poll和epoll来实现的。他们的区别如下:

select

int select(int n, fd_set *readfds, fd_set *writefds, struct timeval *timeout)

  • fd_set使用数组实现,数组大小使用FD_SETSIZE,所以只能监听少于FD_SETSIZE的描述符
  • timeout为超时参数,调用select会一直阻塞直到描述符事件ready或者超时;
  • 成功调用会返回1,错误返回-1,超时返回0

poll

int poll(struct pollfd *fds, unsigned int nfds, int timeout)

poll的功能和select类似,但poll中描述符是polld类型数组。polld定义如下:

struct pollfd{

        int fd;

        short events;   /* request events */

        short revents;  /* return events */

}

epoll

int epoll_create(int size);

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

int epoll_wait(int epfd, struct epoll_event * enevts, int maxevents, int timeout);

epoll_ctl()用于向内核注重心的描述符或者改变某个文件描述的状态。已注册的描述符会在内核中维护在一颗红黑树上,通过回调函数内核会将IO准备好的描述符加入到一个链表中管理,进程调用epoll_wait()便可以得到事件完成的描述符。

epoll的描述符事件有两种触发模式:LT(level trigger)和ET(edge trigger)。

比较

  • select和poll的功能基本相同,但是select会修改描述符,poll不会;
  • select的描述符使用数组实现,默然大小不超过1024,而poll没有限制;
  • poll提供了更多的事件类型,并且对描述符重复利用率比select高
  • 如果一个线程对某个描述符调用了select或者poll,另一个线程关闭了改描述符,会导致结果不确定。
  • 几乎所有的OS都支持select,但只有新的OS支持poll。
  • epoll比select和poll更叫灵活而且描述符没有限制,对多线程也更友好,一个线程调用epoll_wait(),另一个线程关闭了这个描述符也不会产生不确定情况,但epoll仅适用于linux OS 

场景:

  • select 的timeout参数是微妙,而poll和epoll在毫秒级别;
  • poll没有最大描述符数量限制,如果平台实时性要求不高,可使用poll;
  • 在Linux OS上并且有大量描述符(大于1000)需要同时轮询,选择epoll。
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值