Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序

Netty特性

  1. 设计

    1. 统一的API,适用于不同的协议(阻塞和非阻塞)

    2. 基于灵活、可扩展的事件驱动模型(SEDA)

    3. 高度可定制的线程模型

    4. 可靠的无连接数据Socket支持(UDP)

  2. 性能

    1. 更好的吞吐量,低延迟

    2. 更省资源

    3. 尽量减少不必要的内存拷贝

  3. 安全

    1. 完整的SSL/TLS和STARTTLS的支持
  4. 易用

    1. 完善的Javadoc,用户指南和样例

    2. 仅依赖于JDK1.6(netty 4.x)

1 Netty主要组件:

  1. TransportChannel —— 对应NIO中的channel

  2. EventLoop—— 对应于NIO中的while循环

  3. EventLoopGroup: 多个EventLoop

  4. ChannelHandler和ChannelPipeline—-对应于NIO中的客户逻辑实现handleRead/handleWrite(interceptor pattern)

  5. ByteBuf—— 对应于NIO 中的ByteBuffer

  6. Bootstrap和 ServerBootstrap —-对应NIO中的Selector、ServerSocketChannel等的创建、配置、启动等

2 Netty Server启动主要流程

  1. 设置服务端ServerBootStrap启动参数

    1. group(parentGroup,childGroup):

    2. channel(NioServerSocketChannel):设置通道类型

    3. handler():设置NioServerSocketChannel的ChannelHandlerPipeline

    4. childHandler():设置NioSocketChannel的ChannelHandlerPipeline

  2. 通过ServerBootStrap的bind方法启动服务端,bind方法会在parentGroup中注册NioServerScoketChannel,监听客户端的连接请求

    1. 会创建一个NioServerSocketChannel实例,并将其在parentGroup中进行注册

Netty - 图1
Netty - 图2
Netty - 图3
Netty - 图4
Netty - 图5

3 Netty Server执行主要流程:

  1. Client发起连接CONNECT请求,parentGroup中的NioEventLoop不断轮循是否有新的客户端请求,如果有,ACCEPT事件触发

  2. ACCEPT事件触发后,parentGroup中NioEventLoop会通过NioServerSocketChannel获取到对应的代表客户端的NioSocketChannel,并将其注册到childGroup中

  3. childGroup中的NioEventLoop不断检测自己管理的NioSocketChannel是否有读写事件准备好,如果有的话,调用对应的ChannelHandler进行处理

Netty - 图6

4 组件

4.1 传输层

提供了统一的API,支持不同类型的传输层:

  1. OIO -阻塞IO

  2. NIO -JavaNIO

  3. Epoll - Linux Epoll(JNI)

  4. Local Transport - IntraVM调用

  5. Embedded Transport - 供测试使用的嵌入传输

  6. UDS- Unix套接字的本地传输

4.2 EventLoop

  1. EventLoopGroup

    1. 包括多个EventLoop

    2. 多个EventLoop之间不交互

  2. EventLoop:

    1. 每个EventLoop对应一个线程

    2. 所有连接(channel)都将注册到一个EventLoop,并且只注册到一个,整个生命周期中都不会变化

    3. 每个EventLoop管理着多个连接(channel)

    4. EventLoop来处理连接(Channel)上的读写事件

  3. ServerBootstrap

    1. 包括2个不同类型的EventLoopGroup:

      1. ParentEventLoop:负责处理Accept事件,接收请求

      2. ChildEventLoop:负责处理读写事件

Netty - 图7

4.3 Buffers

相比JDKByteBuffer, 更加易于使用:

  1. 为读/写分别维护单独的指针,不需要通过flip()进行读/写模式切换

  2. 容量自动伸缩(类似于ArrayList,StringBuilder)

  3. FluentAPI (链式调用)

更好的性能:

  1. 通过内置的CompositeBuffer来减少数据拷贝(Zerocopy)

  2. 支持内存池,减少GC压力

主要操作

  1. ByteBuf通过两个索引(readerindex、writer index)划分为三个区域:

    1. reader index前面的数据是已经读过的数据,这些数据可以丢弃

    2. 从reader index开始,到writerindex之前的数据是可读数据

    3. 从writer index开始,为可写区域

Netty - 图8

顺序读/写:-改变reader/writerindex

  • writeByte()

  • writeLong()

  • writeXXX()- 增加write index

  • readByte()

  • readLong()

  • readXXX()- 增加read index

随机读/写:- 不改变read/write index

  • getXXX(index)

  • setXXX(index,byte)

mark/reset:

  • markReaderIndex()

  • markWriterIndex()

  • resetReaderIndex()

  • resetWriterIndex()

  • writerIndex(index)

  • readerIndex(index)

discardReadBytes方法 -compact
clear方法
Netty - 图9

查询方法:

  • indexOf

  • bytesBefore

  • forEachByte(ByteBufProcessor)

DerivedBuffers(衍生缓冲区):

  • duplicate()

  • slice()

  • slice(start,stop)

  • unmodifiableBuffer(…),

DerivedBuffers(衍生缓冲区)

  • 具有各自的index和mark

  • 返回的ByteBuf 与原ByteBuf 共享底层存储

缓冲区拷贝

  • copy()或 copy(int, int)

  • 返回的ByteBuf 有数据的独立副本

Bytebuf 类型
根据内存的位置

  • HeapByteBuf

    • 基于数组-内部为一个字节数组(byte array)

    • hasArray()返回True

    • array()返回其内部的数组,可以对数组进行直接操作

  • DirectByteBuf

    • 堆外内存

    • 具有更好的性能

    • 创建和释放开销更大

根据是否使用内存池
Pooledvs Unpooled

根据是否使用Unsafe操作(Unsafe)
Safe vs Unsafe

CompositeByteBuf

  • •复合缓冲区(CompositeByteBuf)

    • 多个ByteBuf组合的视图

    • 一个ByteBuf列表,可动态的添加和删除其中的ByteBuf

    • 可能既包含堆缓冲区,也包含直接缓冲区

Netty - 图10
ByteBufHolder
除ByteBuf外,还另外存储除有效的实际数据各种属性值
Netty - 图11

ByteBuf分配
不直接通过new来创建,而是通过ByteBufAllocator来创建
UnpooledByteBufAllocator
PooledByteBufAllocator
Netty - 图12

Unpooled工具类
它提供了静态的辅助方法来创建未池化的ByteBuf实例
Netty - 图13

ChannelHandler

业务处理核心逻辑,用户自定义
Netty提供2个重要的 ChannelHandler 子接口:
ChannelInboundHandler- 处理进站数据和所有状态更改事件
ChannelOutboundHandler- 处理出站数据,允许拦截各种操作

Channel状态及转换
Netty - 图14
Netty - 图15

当 ChannelHandler 添加到 ChannelPipeline,或者从ChannelPipeline 移除后,对应的方法将会被调用
Netty - 图16

ChannelInboundHandler
当接收到数据或者与之关联的Channel 状态改变时调用
与 Channel 的生命周期接近
Netty - 图17

ChannelOutboundHandler
Netty - 图18

ChannelPipeline
ChannelPipeline是ChannelHandler容器

  • 包括一系列的ChannelHandler 实例,用于拦截流经一个Channel 的入站和出站事件

  • 每个Channel都有一个其ChannelPipeline

  • 可以修改 ChannelPipeline 通过动态添加和删除ChannelHandler

  • 定义了丰富的API调用来回应入站和出站事件

Netty - 图19

ChannelHandlerContext
ChannelHandlerContext表示ChannelHandler 和ChannelPipeline 之间的关联
在ChannelHandler 添加到 ChannelPipeline 时创建
ChannelHandlerContext表示ChannelHandler 和ChannelPipeline 之间的关联
Netty - 图20

Reactor模式 - Douglas
Netty - 图21

Reactor模式 - Doug Lea
三种形式
单线程Reactor
多线程Reactor
Multiple Reactor

单线程
Netty - 图22
多线程

Netty - 图23
多reactor

Netty - 图24
其他模式 — 主从Reactor
Netty - 图25
EvenLoop

Netty - 图26

EventExecutor视图

  1. EventExecutorGroup里面有一个EventExecutor数组,保存了多个EventExecutor;

  2. EventExecutorGroup是不干什么事情的,当收到一个请后,他就调用next()获得一个它里面的EventExecutor,再调用这个executor的方法;

  3. next(): EventExecutorChooser.next()定义选择EventExecutor的策略;

问题:
1、channelRead方法是多线程调用吗?
channelRead方法不会被多线程调用,只会在channel对应的Eventloop所在的线程所调用。

2、一个serversocketchannel可以注册到多个selector上吗? serversocket只能注册到一个端口,怎么做到eventLoopgroup的?
Serversocketchannel不能注册到多个Selector上去,只会注册在一个Selector上。(注册到多个Selector, accept()在哪个线程上返回了?)

Serversocketchannel绑定一个端口,并且注册到Boss EventloopGroup上去,他也只会使用BossEventloopGroup中的某一个Eventloop,也就是一个线程。


3、ByteBuf 中 array() 在什么场景下能取到值?
bossgroup 如何多线程工作完成accept?

当ByteBuf.hasArray()返回True时,array()就能够返回底层对应的数组。只有当ByteBuf是HeapByteBuf时才能够返回其底层array,为DirectByteBuf时则是无法取得。

Accept只会在Boss EventLoopGroup中的一个线程上运行,不会在多线程中同时accept。


4、那个衍生缓冲区和符合缓冲区有什么区别?

衍生缓冲区和复合缓冲区都是一个视图,自身并没有真正的底层存储空间。

衍生缓冲层是一个原缓冲区的视图,与原缓冲区共享底层存储空间,但是维护自己的索引(readerIndex和writerIndex)。
复合缓冲层则是多个缓冲区的一个整体视图。

5、是不是换行符和定长这些方案就能解决半包粘包问题对吧,然后真正项目也是这么用的对吧?
换行符(或基于其他分隔符)和定长是解决半包和粘包的一种方案。在有些项目中有用到。用得最广的应该还是基于长度的解码器,也是就协议中有一个字段指明数据的长度。


6、socket编程,如果是聊天室类型的,页面连接socket,除了websocket之外,还有其他长连接方式么?
网页的话目前主要是Websocket。



7、EventLoopGroup包含多个EventLoop,每个EventLoop包含一个selector,这个selector会管理多个channel,一个channel可以有多个客服端连接(一个Channel只表示一个服务器和客户端连接),
每一个channel都有一个ChannelPipeline,ChannelPipeline包含多个channelHandler实例列表,
每一个channelHandler对应多(一个)channelHandlerContentext,channelHandlerContentext只能对应一个channelHandler

8、 8.1、ChannelPipeline通过高级截取过滤器模式来掌控ChannelHandler处理事件(入站[inbound]和出站[outbound])的流程;
8.2、ChannelHandler并没有方法处理事件(嗯,只有一些通用的生命周期方法,handlerAdded/handerRemoved等),而需要由子类处理:ChannelInboundHandler拦截和处理入站事件,ChannelOutboundHandler拦截和处理出站事件。
8.3、ChannelHandlerContext,上下文处理,指的是ChannleHandler之间的关系以及ChannelHandler与ChannelPipeline之间的关系,ChannelPipeline中的事件传播主要依赖于ChannelHandlerContext实现,
由于ChannelHandlerContext中有ChannelHandler之间的关系(不是ChannelHandlerContext有ChannelHandler的关系,是各个ChannelHandler的ChannelHandlerContext之间是一条双向链表),所以能得到ChannelHandler的后继节点,从而将事件传播到下一个ChannelHandler。


9、 流程:一个客户端 —》 EventLoop -》 selector -》 channel -》 ChannelPipeline(开始数据处理),
(1)一个channel可以有多个客服端连接,每一个channel都有一个ChannelPipeline,所有可以处理多个客户端连接后的数据处理,(一个Channel只表示一个服务器和客户端连接)
因为Netty提供了Sharable注解,如果一个ChannelHandle状态无关,那么就标注为Sharable,这样就可以实现一个实例处理所有客户端的事件 ;(如果一个ChannelHandle无状态,有Sharable注册,那在所有Channel的ChannelPipeline中就可以使用同一个实例,而不需要为每个channel创建一个单独的实例)

(2)入站[inbound]:处理服务端接受客户端过来的数据;出站[outbound]:处理服务端发送客户端的数据
这样说不准确。你是从服务器程序角度来说的。而对于客户端程序角度来说,则相反:入站处理客户端接受服务端过来的数据;出站则是处理客户端发送服务端的数据。

入站则主要处理从Channel进来的事件,像读;出站主要处理从channel出去的事件,像写,连接。

1、netty怎么处理客户端或服务端异常关闭后,缓冲区的数据未及时处理造成丢失的问题,BlockingQueue是否可以运用?
连接异常关闭后,缓冲区的数据未能及时处理就只能丢弃,或者存储起来,等目标用户重连后再重新发送(像微信一样,某个用户的连接断了,等他重新连过来时重发),但是这时知道目标用户(因为连接重建后是一个新的,不能通过连接来判断要重发的目标)
2、netty怎么做到同步调用,对于客户端可以在同一channel中实现同步返回,还是需要单独建立一个用于接收返回消息的channel.
这里的同步调用是什么意思? 因为channel本身是双向的,可读可写,服务端写数据,客户端就能够同步返回。
3、客户端消息异常重发,基于exceptionCaught异常事件处理,怎么得到异常的消息。
异常发生后,就会捕获异常信息,并调用ChannelHandler中的exceptionCaught,把异常信息传递给他。至于要实现异常重发,可以有exceptionCaught中加入重发逻辑。
1. selector内部是一个线程循环维护注册在其中的channel状态,还是操作系统回调selector,通知channel状态改变?

不是selector自己维护,也不是操作系统回调。Selector实际上是通过JNI调用到操作系统的select系统调用( linux下的Epoll,或freebsd下的kqueue 等 ),由操作系统内核负责监控channel的状态改变,并返回给selector。

2. NIO任然是阻塞的,是说阻塞的从内核态到用户态吗?对应JAVA中就是从channel中读取数据到buffer吗

对,当内核态数据准备就绪,NIO select就会返回,但是数据从内核态到用户态数据拷贝仍然是阻塞的,也就是 Java中channel读取数据到buffer中的过程。

3. 为什么从内核态复制到堆内存上需要先复制到堆外用户态内存,再复制一次到JVM?

弄反了。是当要通过网络发送数据时,需要把JVM 堆内存上的数据复制到内核态,此时无法直接复制,而是需要创建一个临时的native的内存,先把数据从JVM堆上复制到临时native内存上,再从临时native内存复制到内核态进行发送。
至于为什么要有这一次中间临时复制,是因为JVM堆内存是动态的,可能会发生GC, 而GC操作会涉及到数据的移动操作,移动数据使得其地址发生变化,会导致数据复制出错。所以JVM堆内存不能直接往内核态复制。

Java nio基于事件驱动的四个事件,能不能分别解释一下这四个事件的具体情况么,视频里面直接一笔带过了,我不太明白accept和connect的区别,一个是接收到连接的事件和建立连接的事件,一般后者用在什么场景呢? 另外read可读事件,指的是数据已经到用户态了么,还是说才到核心态,如果是到了核心态的话,为什么我不可以等到用户态再响应呢,这样更可以减少阻塞时间,如果是用户态的话,对应第一节课的io分类,那不就是异步io了,和老师说的nio使用的是io复用有些矛盾,还是说老师说的是io复用指的是数据到用户态的阻塞,以及数据处理的阻塞,这两个过程的复用呢?不知道我想的对不对,非常期待您的解惑。

OP_ACCEPT事件就绪:当收到一个客户端的连接请求时,该事件就绪。这是ServerSocketChannel上唯一有效的操作。(服务器端)
OP_CONNECT事件就绪:只有客户端SocketChannel会注册该操作,当客户端调用SocketChannel.connect()时,该事件就绪。(客户端)
OP_READ事件就绪:该操作对客户端和服务端的SocketChannel都有效,当OS的读缓冲区中有数据可读时,该事件就绪。
OP_WRITE事件就绪:该操作对客户端和服务端的SocketChannel都有效,当OS的写缓冲区中有空闲的空间时,该事件就绪。

accept和connect的区别:accept是服务器端接收到连接,connect则是客户端发起连接成功。后者用在客户端,当连接成功后注册相关读写事件。

对,read就指数据已经到内核缓冲区了,将其拷贝到应用缓冲区的过程。如果等到用户态再响应就是异步IO了。目前Linux异步IO接口还不成熟,因此使用较少。

IO复用更多的是指一个线程监控多个IO通道,这个监控线程的复用。
3、针对图中这里实现多线程reactor模式,这里多线程是如何配置的呢?
就是在netty程序的ChannelHandler中的channelRead()方法中,创建新的线程(池)来执行处理操作。


4、如果根据字节长度分割数据流,那么接收到的数据长度不一致怎么办呢?(多个不同类型的物联网设备)
在发送时通过LengthFieldPrepender添加数据长度前缀,然后通过LengthFieldBasedFrameDecoder拆包即可。