Netty基本概念

Netty是什么?

是一个网络通信框架

能做什么?

绝大多数的网络通信Netty都能做,BIO的服务端与客户端通信,NIO的服务端网络通信

为什么要用Netty?

一般对于NIO来说,jdk提供的NIO实现是真的有点复杂,很原生,还有点bug,其中最难顶的就是空selector导致的cpu过高问题,Netty是指在减少NIO的开始技术难度,即使不太熟悉的NIO的开发者,只要按照Netty的规范去写代码也能写出很棒的并发通信应用。

Netty的几个基本组件:

ByteBuf:对标JDK的ByteBuffer,是一个操作序列,可以说是缓冲区,零拷贝实现的一个重要组件

Channel:对标JDK的Channel,通道,通信的核心类包装了一个unsafe,操作IO的工具或方法需要注意的是IO不是写进channel的,而是缓冲区

ChannelHandler:用户自定义的IO数据处理实现,有channelRead、register等IO通知方法,方便我们在各种IO事件后做相应的业务操作。

如:

1. 注册事件 fireChannelRegistered。

2. 连接建立事件 fireChannelActive。

3. 读事件和读完成事件 fireChannelRead、fireChannelReadComplete。

4. 异常通知事件 fireExceptionCaught。

5. 用户自定义事件 fireUserEventTriggered。

6. Channel 可写状态变化事件 fireChannelWritabilityChanged。

7. 连接关闭事件 fireChannelInactive。

EventLoop:还有另一个概念EventLoopGroup,在初始化netty时我们会定义两个线程组,一个负责IO事件的分发,另一个负责IO的事的处理,但要注意的是每个Channel都会绑定一个EventLoop。确保在单线程中完成一次IO事件

bootstrap:netty的引导器,负责引导Netty程序的启动

还有一个概念引自一位大佬的知乎:面试系列 深入理解NIO select&epoll - 知乎

为什么会出现epoll,它的背景是什么

select和poll函数的缺陷是:

  1. 函数调用参数拷贝问题:这两系统函数每次调用都需要我们提供给他所有需要监听的socket文件描述符集合,而且我们主线程是死循环调用select/poll函数,这涉及到用户空间数据到内核空间的拷贝过程,这个操作比较耗费性能。其实我们的fd集合是比较稳定的,可能它每次只有1-2个socket_fd需要更改,但是我们每次都需要传整个。因为select/poll没有在kernel层面保留任何数据信息,所以说每次调用都需要进行数据拷贝。
  2. 系统调用返回后不知道哪些socket就绪问题:select和poll函数它的返回值是个int整型,只能代表有几个socket就绪了,导致我们程序被唤醒之后,还需要新一轮的系统调用,去检查哪个socket是就绪状态的

怎么解决,epoll怎么设计的呢?

需要epoll函数在内核空间内,它有一个对应的数据结构去存储一些数据。这个数据结构实际上就是eventpoll对象,eventpoll的结构,主要是三块重要的区域,一块是检查列表,存放需要监听的socket_fd监听符,红黑树,因为这个socket集合信息经常会有增删改查的需求,保持了时间复杂度为O(logN)

另一块就是就绪列表,存放就绪状态的socket信息,双向链表。 另一块是等待队列,把调用epollwait函数的进程放到这里面去。

它主要提供了两个函数,epoll_ctl函数,它负责维护检查列表,可以根据eventpoll-id去增加删除改动需要检查的socket文件描述符。 epoll_wait函数它主要参数是eventpoll-id,表示此次系统调用需要监测的socket_fd集合是eventpoll里面的信息。默认情况下它会阻塞调用线程,直到eventpoll中关联的某个socket就绪以后,epoll_wait函数才会返回。

怎么维护就绪队列

靠的是中断程序,对等待队列和就绪队列进行操作

epoll_wait返回0表示没有就绪socket,大于0表示有几个就绪socket,它怎么解决第二个识别就绪socket问题的呢。

epollwait函数调用的时候,会传入一个epoll_event事件数组指针,epoll_wait函数正常返回之前会把就绪的socket事件信息拷贝到这个数组指针里面。上层程序就可以通过这个数组拿到就绪的socket列表。所以它的时间复杂度是O(1)

epoll_wait函数可以设置成非阻塞的 吗?

默认是阻塞的,但可以设置阻塞时间,如果设置为0表示非阻塞,每次调用都会去检查就绪列表

检查队列采用的数据结构是什么?

红黑树,因为这个socket集合信息经常会有增删改查的需求

epoll_wait阻塞期间另一个线程执行epoll_ctl是否安全

  • 添加:没问题,另一个线程增加fd进监听列表,epoll_wait是可以监听到的
  • 删除:官方描述的话,是部分unix系统关闭socket会被认为是就绪,从而读写失败报错,部分是没问题
  • 删除再打开:如果关闭了socket就相当于把它从红黑树监听列表移除了,必须再次添加才能被监听

边缘触发和水平触发的区别

  • 水平触发:如果文件描述符已经就绪可以非阻塞的执行IO操作了,此时会触发通知.允许在任意时刻重复检测IO的状态.select,poll就属于水平触发.
    • 简单来说只要缓冲区还有内容,就会返回读就绪。
  • 边缘触发:如果文件描述符自上次状态改变后有新的IO活动到来,此时才会触发通知.在收到一个IO事件通知后要尽可能多的执行IO操作,因为如果在一次通知中没有执行完IO那么就需要等到下一次新的IO活动到来才能获取到就绪的描述符.
    • 简单来说,只有当缓冲区内容发生变化时,才会返回读就绪

使用场景

epoll默认是水平触发,优点是保证数据完整性,会一直通知你,缺点是这种通知涉及内核态到用户态的切换,会浪费一定性能。

边缘触发,优点是每次内核只会通知一次,提高效率,缺点是不能保证数据的完整性,不一定能及时取出所有数据。一般来说如果处理大数据的情况下会使用边缘触发。

一个管道收到了1kb的数据,epoll会立即返回,此时读了512字节数据,然后再次调用epoll.这时如果是水平触发的,epoll会立即返回,因为有数据准备好了.如果是边缘触发的不会立即返回,因为此时虽然有数据可读但是已经触发了一次通知,在这次通知到现在还没有新的数据到来,直到有新的数据到来epoll才会返回,此时老的数据和新的数据都可以读取到(当然是需要这次你尽可能的多读取).

上一篇:【C# 线程】线程池 epoll和IOCP之比较


下一篇:Flutter Chanel通信流程