惠州网站建设 英语6,手机最新发布,中国建筑网校,网站 技术方案1.Netty 是什么#xff1f;
Netty是 一个异步事件驱动的网络应用程序框架#xff0c;用于快速开发可维护的高性能协议服务器和客户端。Netty是基于nio的#xff0c;它封装了jdk的nio#xff0c;让我们使用起来更加方法灵活。
2.Netty 的特点是什么#xff1f;
高并发
Netty是 一个异步事件驱动的网络应用程序框架用于快速开发可维护的高性能协议服务器和客户端。Netty是基于nio的它封装了jdk的nio让我们使用起来更加方法灵活。
2.Netty 的特点是什么
高并发Netty 是一款基于 NIONonblocking IO非阻塞IO开发的网络通信框架对比于 BIOBlocking I/O阻塞IO他的并发性能得到了很大提高。传输快Netty 的传输依赖于零拷贝特性尽量减少不必要的内存拷贝实现了更高效率的传输。封装好Netty 封装了 NIO 操作的很多细节提供了易于使用调用接口。
3.Netty 的优势有哪些
使用简单封装了 NIO 的很多细节使用更简单。功能强大预置了多种编解码功能支持多种主流协议。定制能力强可以通过 ChannelHandler 对通信框架进行灵活地扩展。性能高通过与其他业界主流的 NIO 框架对比Netty 的综合性能最优。稳定Netty 修复了已经发现的所有 NIO 的 bug让开发人员可以专注于业务本身。社区活跃Netty 是活跃的开源项目版本迭代周期短bug 修复速度快。
4.Netty 的应用场景有哪些
典型的应用有阿里分布式服务框架 Dubbo默认使用 Netty 作为基础通信组件还有 RocketMQ 也是使用 Netty 作为通讯的基础。
5.Netty 高性能表现在哪些方面
IO 线程模型同步非阻塞用最少的资源做更多的事。内存零拷贝尽量减少不必要的内存拷贝实现了更高效率的传输。内存池设计申请的内存可以重用主要指直接内存。内部实现是用一颗二叉查找树管理内存分配情况。串形化处理读写避免使用锁带来的性能开销。高性能序列化协议支持 protobuf 等高性能序列化协议。
6.BIO、NIO和AIO的区别
BIO一个连接一个线程客户端有连接请求时服务器端就需要启动一个线程进行处理。线程开销大。 伪异步IO将请求连接放入线程池一对多但线程还是很宝贵的资源。
NIO一个请求一个线程但客户端发送的连接请求都会注册到多路复用器上多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。
AIO一个有效请求一个线程客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理
BIO是面向流的NIO是面向缓冲区的BIO的各种流是阻塞的。而NIO是非阻塞的BIO的Stream是单向的而NIO的channel是双向的。
NIO的特点事件驱动模型、单线程处理多任务、非阻塞I/OI/O读写不再阻塞而是返回0、基于block的传输比基于流的传输更高效、更高级的IO函数zero-copy、IO多路复用大大提高了Java网络应用的可伸缩性和实用性。基于Reactor线程模型。
在Reactor模式中事件分发器等待某个事件或者可应用或个操作的状态发生事件分发器就把这个事件传给事先注册的事件处理函数或者回调函数由后者来做实际的读写操作。如在Reactor中实现读注册读就绪事件和相应的事件处理器、事件分发器等待事件、事件到来激活分发器分发器调用事件对应的处理器、事件处理器完成实际的读操作处理读到的数据注册新的事件然后返还控制权。
7.NIO的组成
Buffer与Channel进行交互数据是从Channel读入缓冲区从缓冲区写入Channel中的
flip方法 反转此缓冲区将position给limit然后将position置为0其实就是切换读写模式
clear方法 清除此缓冲区将position置为0把capacity的值给limit。
rewind方法 重绕此缓冲区将position置为0
DirectByteBuffer可减少一次系统空间到用户空间的拷贝。但Buffer创建和销毁的成本更高不可控通常会用内存池来提高性能。直接缓冲区主要分配给那些易受基础系统的本机I/O 操作影响的大型、持久的缓冲区。如果数据量比较小的中小应用情况下可以考虑使用heapBuffer由JVM进行管理。
Channel表示 IO 源与目标打开的连接是双向的但不能直接访问数据只能与Buffer 进行交互。通过源码可知FileChannel的read方法和write方法都导致数据复制了两次
Selector可使一个单独的线程管理多个Channelopen方法可创建Selectorregister方法向多路复用器器注册通道可以监听的事件类型读、写、连接、accept。注册事件后会产生一个SelectionKey它表示SelectableChannel 和Selector 之间的注册关系wakeup方法使尚未返回的第一个选择操作立即返回唤醒的
原因是注册了新的channel或者事件channel关闭取消注册优先级更高的事件触发如定时器事件希望及时处理。
Selector在Linux的实现类是EPollSelectorImpl委托给EPollArrayWrapper实现其中三个native方法是对epoll的封装而EPollSelectorImpl. implRegister方法通过调用epoll_ctl向epoll实例中注册事件还将注册的文件描述符(fd)与SelectionKey的对应关系添加到fdToKey中这个map维护了文件描述符与SelectionKey的映射。
fdToKey有时会变得非常大因为注册到Selector上的Channel非常多百万连接过期或失效的Channel没有及时关闭。fdToKey总是串行读取的而读取是在select方法中进行的该方法是非线程安全的。
Pipe两个线程之间的单向数据连接数据会被写到sink通道从source通道读取
NIO的服务端建立过程Selector.open()打开一个SelectorServerSocketChannel.open()创建服务端的Channelbind()绑定到某个端口上。并配置非阻塞模式register()注册Channel和关注的事件到Selector上select()轮询拿到已经就绪的事件
8.Netty的线程模型
Netty通过Reactor模型基于多路复用器接收并处理用户请求内部实现了两个线程池boss线程池和work线程池其中boss线程池的线程负责处理请求的accept事件当接收到accept事件的请求时把对应的socket封装到一个NioSocketChannel中并交给work线程池其中work线程池负责请求的read和write事件由对应的Handler处理。
单线程模型所有I/O操作都由一个线程完成即多路复用、事件分发和处理都是在一个Reactor线程上完成的。既要接收客户端的连接请求,向服务端发起连接又要发送/读取请求或应答/响应消息。一个NIO 线程同时处理成百上千的链路性能上无法支撑速度慢若线程进入死循环整个程序不可用对于高负载、大并发的应用场景不合适。
多线程模型有一个NIO 线程Acceptor 只负责监听服务端接收客户端的TCP 连接请求NIO 线程池负责网络IO 的操作即消息的读取、解码、编码和发送1 个NIO 线程可以同时处理N 条链路但是1 个链路只对应1 个NIO 线程这是为了防止发生并发操作问题。但在并发百万客户端连接或需要安全认证时一个Acceptor 线程可能会存在性能不足问题。
主从多线程模型Acceptor 线程用于绑定监听端口接收客户端连接将SocketChannel 从主线程池的Reactor 线程的多路复用器上移除重新注册到Sub 线程池的线程上用于处理I/O 的读写等操作从而保证mainReactor只负责接入认证、握手等操作
9.TCP 粘包/拆包的原因及解决方法
TCP是以流的方式来处理数据一个完整的包可能会被TCP拆分成多个包进行发送也可能把小的封装成一个大的数据包发送。
TCP粘包/分包的原因
应用程序写入的字节大小大于套接字发送缓冲区的大小会发生拆包现象而应用程序写入数据小于套接字缓冲区大小网卡将应用多次写入的数据发送到网络上这将会发生粘包现象
进行MSS大小的TCP分段当TCP报文长度-TCP头部长度MSS的时候将发生拆包 以太网帧的payload净荷大于MTU1500字节进行ip分片。
解决方法
消息定长FixedLengthFrameDecoder类
包尾增加特殊字符分割
行分隔符类LineBasedFrameDecoder或自定义分隔符类 DelimiterBasedFrameDecoder
将消息分为消息头和消息体LengthFieldBasedFrameDecoder类。分为有头部的拆包与粘包、长度字段在前且有头部的拆包与粘包、多扩展头部的拆包与粘包。
10.什么是 Netty 的零拷贝
Netty 的零拷贝主要包含三个方面
Netty 的接收和发送 ByteBuffer 采用 DIRECT BUFFERS使用堆外直接内存进行 Socket 读写不需要进行字节缓冲区的二次拷贝。如果使用传统的堆内存HEAP BUFFERS进行 Socket 读写JVM 会将堆内存 Buffer 拷贝一份到直接内存中然后才写入 Socket 中。相比于堆外直接内存消息在发送过程中多了一次缓冲区的内存拷贝。Netty 提供了组合 Buffer 对象可以聚合多个 ByteBuffer 对象用户可以像操作一个 Buffer 那样方便的对组合 Buffer 进行操作避免了传统通过内存拷贝的方式将几个小 Buffer 合并成一个大的 Buffer。Netty 的文件传输采用了 transferTo 方法它可以直接将文件缓冲区的数据发送到目标 Channel避免了传统通过循环 write 方式导致的内存拷贝问题。
11.Netty 中有哪种重要组件
ChannelNetty 网络操作抽象类它除了包括基本的 I/O 操作如 bind、connect、read、write 等。EventLoop主要是配合 Channel 处理 I/O 操作用来处理连接的生命周期中所发生的事情。ChannelFutureNetty 框架中所有的 I/O 操作都为异步的因此我们需要 ChannelFuture 的 addListener()注册一个 ChannelFutureListener 监听事件当操作执行成功或者失败时监听就会自动触发返回结果。ChannelHandler充当了所有处理入站和出站数据的逻辑容器。ChannelHandler 主要用来处理各种事件这里的事件很广泛比如可以是连接、数据接收、异常、数据转换等。ChannelPipeline为 ChannelHandler 链提供了容器当 channel 创建时就会被自动分配到它专属的 ChannelPipeline这个关联是永久性的。
12.Netty 发送消息有几种方式
Netty 有两种发送消息的方式
直接写入 Channel 中消息从 ChannelPipeline 当中尾部开始移动写入和 ChannelHandler 绑定的 ChannelHandlerContext 中消息从 ChannelPipeline 中的下一个 ChannelHandler 中移动。
13.默认情况 Netty 起多少线程何时启动
Netty 默认是 CPU 处理器数的两倍bind 完之后启动。
14.了解哪几种序列化协议
序列化编码是将对象序列化为二进制形式字节数组主要用于网络传输、数据持久化等而反序列化解码则是将从网络、磁盘等读取的字节数组还原成原始对象主要用于网络传输对象的解码以便完成远程调用。
影响序列化性能的关键因素序列化后的码流大小网络带宽的占用、序列化的性能CPU资源占用是否支持跨语言异构系统的对接和开发语言切换。
Java默认提供的序列化无法跨语言、序列化后的码流太大、序列化的性能差
XML优点人机可读性好可指定元素或特性的名称。缺点序列化数据只包含数据本身以及类的结构不包括类型标识和程序集信息只能序列化公共属性和字段不能序列化方法文件庞大文件格式复杂传输占带宽。适用场景当做配置文件存储数据实时数据转换。
JSON是一种轻量级的数据交换格式优点兼容性高、数据格式比较简单易于读写、序列化后数据较小可扩展性好兼容性好、与XML相比其协议比较简单解析速度比较快。缺点数据的描述性比XML差、不适合性能要求为ms级别的情况、额外空间开销比较大。适用场景可替代跨防火墙访问、可调式性要求高、基于Web browser的Ajax请求、传输数据量相对小实时性要求相对低例如秒级别的服务。
Fastjson采用一种“假定有序快速匹配”的算法。优点接口简单易用、目前java语言中最快的json库。缺点过于注重快而偏离了“标准”及功能性、代码质量不高文档不全。适用场景协议交互、Web输出、Android客户端
Thrift不仅是序列化协议还是一个RPC框架。优点序列化后的体积小, 速度快、支持多种语言和丰富的数据类型、对于数据字段的增删具有较强的兼容性、支持二进制压缩编码。缺点使用者较少、跨防火墙访问时不安全、不具有可读性调试代码时相对困难、不能与其他传输层协议共同使用例如HTTP、无法支持向持久层直接读写数据即不适合做数据持久化序列化协议。适用场景分布式系统的RPC解决方案
AvroHadoop的一个子项目解决了JSON的冗长和没有IDL的问题。优点支持丰富的数据类型、简单的动态语言结合功能、具有自我描述属性、提高了数据解析速度、快速可压缩的二进制数据形式、可以实现远程过程调用RPC、支持跨编程语言实现。缺点对于习惯于静态类型语言的用户不直观。适用场景在Hadoop中做Hive、Pig和MapReduce的持久化数据格式。
Protobuf将数据结构以.proto文件进行描述通过代码生成工具可以生成对应数据结构的POJO对象和Protobuf相关的方法和属性。优点序列化后码流小性能高、结构化数据存储格式XML JSON等、通过标识字段的顺序可以实现协议的前向兼容、结构化的文档更容易管理和维护。缺点需要依赖于工具生成代码、支持的语言相对较少官方只支持Java 、C 、python。适用场景对性能要求高的RPC调用、具有良好的跨防火墙的访问属性、适合应用层对象的持久化
其它
protostuff 基于protobuf协议但不需要配置proto文件直接导包即可 Jboss marshaling 可以直接序列化java类 无须实java.io.Serializable接口 Message pack 一个高效的二进制序列化格式 Hessian 采用二进制协议的轻量级remoting onhttp工具 kryo 基于protobuf协议只支持java语言,需要注册Registration然后序列化Output反序列化Input
15.如何选择序列化协议
具体场景 对于公司间的系统调用如果性能要求在100ms以上的服务基于XML的SOAP协议是一个值得考虑的方案。 基于Web browser的Ajax以及Mobile app与服务端之间的通讯JSON协议是首选。对于性能要求不太高或者以动态类型语言为主或者传输数据载荷很小的的运用场景JSON也是非常不错的选择。 对于调试环境比较恶劣的场景采用JSON或XML能够极大的提高调试效率降低系统开发成本。 当对性能和简洁性有极高要求的场景ProtobufThriftAvro之间具有一定的竞争关系。 对于T级别的数据的持久化应用场景Protobuf和Avro是首要选择。如果持久化后的数据存储在hadoop子项目里Avro会是更好的选择。 对于持久层非Hadoop项目以静态类型语言为主的应用场景Protobuf会更符合静态类型语言工程师的开发习惯。由于Avro的设计理念偏向于动态类型语言对于动态语言为主的应用场景Avro是更好的选择。 如果需要提供一个完整的RPC解决方案Thrift是一个好的选择。 如果序列化之后需要支持不同的传输层协议或者需要跨防火墙访问的高性能场景Protobuf可以优先考虑。
protobuf的数据类型有多种bool、double、float、int32、int64、string、bytes、enum、message。protobuf的限定符required: 必须赋值不能为空、optional:字段可以赋值也可以不赋值、repeated: 该字段可以重复任意次数包括0次、枚举只能用指定的常量集中的一个值作为其值
protobuf的基本规则每个消息中必须至少留有一个required类型的字段、包含0个或多个optional类型的字段repeated表示的字段可以包含0个或多个数据[1,15]之内的标识号在编码的时候会占用一个字节常用[16,2047]之内的标识号则占用2个字节标识号一定不能重复、使用消息类型也可以将消息嵌套任意多层可用嵌套消息类型来代替组。
protobuf的消息升级原则不要更改任何已有的字段的数值标识不能移除已经存在的required字段optional和repeated类型的字段可以被移除但要保留标号不能被重用。新添加的字段必须是optional或repeated。因为旧版本程序无法读取或写入新增的required限定符的字段。
编译器为每一个消息类型生成了一个.java文件以及一个特殊的Builder类该类是用来创建消息类接口的。如UserProto.User.Builder builder UserProto.User.newBuilder();builder.build()
Netty中的使用ProtobufVarint32FrameDecoder 是用于处理半包消息的解码类ProtobufDecoder(UserProto.User.getDefaultInstance())这是创建的UserProto.java文件中的解码类ProtobufVarint32LengthFieldPrepender 对protobuf协议的消息头上加上一个长度为32的整形字段用于标志这个消息的长度的类ProtobufEncoder 是编码类
将StringBuilder转换为ByteBuf类型copiedBuffer()方法
16.Netty 支持哪些心跳类型设置
readerIdleTime为读超时时间即测试端一定时间内未接受到被测试端消息。writerIdleTime为写超时时间即测试端一定时间内向被测试端发送消息。allIdleTime所有类型的超时时间。
17.Netty 和 Tomcat 的区别
作用不同Tomcat 是 Servlet 容器可以视为 Web 服务器而 Netty 是异步事件驱动的网络应用程序框架和工具用于简化网络编程例如TCP和UDP套接字服务器。协议不同Tomcat 是基于 http 协议的 Web 服务器而 Netty 能通过编程自定义各种协议因为 Netty 本身自己能编码/解码字节流所有 Netty 可以实现HTTP 服务器、FTP 服务器、UDP 服务器、RPC 服务器、WebSocket 服务器、Redis 的 Proxy 服务器、MySQL 的 Proxy 服务器等等。
18.NIOEventLoopGroup源码
NioEventLoopGroup(其实是MultithreadEventExecutorGroup) 内部维护一个类型为 EventExecutor children [], 默认大小是处理器核数 * 2, 这样就构成了一个线程池初始化EventExecutor时NioEventLoopGroup重载newChild方法所以children元素的实际类型为NioEventLoop。
线程启动时调用SingleThreadEventExecutor的构造方法执行NioEventLoop类的run方法首先会调用hasTasks()方法判断当前taskQueue是否有元素。如果taskQueue中有元素执行 selectNow() 方法最终执行selector.selectNow()该方法会立即返回。如果taskQueue没有元素执行 select(oldWakenUp) 方法
select ( oldWakenUp) 方法解决了 Nio 中的 bugselectCnt 用来记录selector.select方法的执行次数和标识是否执行过selector.selectNow()若触发了epoll的空轮询bug则会反复执行selector.select(timeoutMillis)变量selectCnt 会逐渐变大当selectCnt 达到阈值默认512则执行rebuildSelector方法进行selector重建解决cpu占用100%的bug。
rebuildSelector方法先通过openSelector方法创建一个新的selector。然后将old selector的selectionKey执行cancel。最后将old selector的channel重新注册到新的selector中。rebuild后需要重新执行方法selectNow检查是否有已ready的selectionKey。
接下来调用processSelectedKeys 方法处理I/O任务当selectedKeys ! null时调用processSelectedKeysOptimized方法迭代 selectedKeys 获取就绪的 IO 事件的selectkey存放在数组selectedKeys中, 然后为每个事件都调用 processSelectedKey 来处理它processSelectedKey 中分别处理OP_READOP_WRITEOP_CONNECT事件。
最后调用runAllTasks方法非IO任务该方法首先会调用fetchFromScheduledTaskQueue方法把scheduledTaskQueue中已经超过延迟执行时间的任务移到taskQueue中等待被执行然后依次从taskQueue中取任务执行每执行64个任务进行耗时检查如果已执行时间超过预先设定的执行时间则停止执行非IO任务避免非IO任务太多影响IO任务的执行。
每个NioEventLoop对应一个线程和一个SelectorNioServerSocketChannel会主动注册到某一个NioEventLoop的Selector上NioEventLoop负责事件轮询。
Outbound 事件都是请求事件, 发起者是 Channel处理者是 unsafe通过 Outbound 事件进行通知传播方向是 tail到head。Inbound 事件发起者是 unsafe事件的处理者是 Channel, 是通知事件传播方向是从头到尾。
内存管理机制首先会预申请一大块内存ArenaArena由许多Chunk组成而每个Chunk默认由2048个page组成。Chunk通过AVL树的形式组织Page每个叶子节点表示一个Page而中间节点表示内存区域节点自己记录它在整个Arena中的偏移地址。当区域被分配出去后中间节点上的标记位会被标记这样就表示这个中间节点以下的所有节点都已被分配了。大于8k的内存分配在poolChunkList中而PoolSubpage用于分配小于8k的内存它会把一个page分割成多段进行内存分配。
ByteBuf的特点支持自动扩容4M保证put方法不会抛出异常、通过内置的复合缓冲类型实现零拷贝zero-copy不需要调用flip()来切换读/写模式读取和写入索引分开方法链引用计数基于AtomicIntegerFieldUpdater用于内存回收PooledByteBuf采用二叉树来实现一个内存池集中管理内存的分配和释放不用每次使用都新建一个缓冲区对象。UnpooledHeapByteBuf每次都会新建一个缓冲区对象。
Netty简介
Netty是 一个异步事件驱动的网络应用程序框架用于快速开发可维护的高性能协议服务器和客户端。
JDK原生NIO程序的问题
JDK原生也有一套网络应用程序API但是存在一系列问题主要如下
NIO的类库和API繁杂使用麻烦你需要熟练掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer等需要具备其它的额外技能做铺垫例如熟悉Java多线程编程因为NIO编程涉及到Reactor模式你必须对多线程和网路编程非常熟悉才能编写出高质量的NIO程序可靠性能力补齐开发工作量和难度都非常大。例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常码流的处理等等NIO编程的特点是功能开发相对容易但是可靠性能力补齐工作量和难度都非常大JDK NIO的BUG例如臭名昭著的epoll bug它会导致Selector空轮询最终导致CPU 100%。官方声称在JDK1.6版本的update18修复了该问题但是直到JDK1.7版本该问题仍旧存在只不过该bug发生概率降低了一些而已它并没有被根本解决
Netty的特点
Netty的对JDK自带的NIO的API进行封装解决上述问题主要特点有
设计优雅 适用于各种传输类型的统一API - 阻塞和非阻塞Socket 基于灵活且可扩展的事件模型可以清晰地分离关注点 高度可定制的线程模型 - 单线程一个或多个线程池 真正的无连接数据报套接字支持自3.1起使用方便 详细记录的Javadoc用户指南和示例 没有其他依赖项JDK 5Netty 3.x或6Netty 4.x就足够了高性能 吞吐量更高延迟更低 减少资源消耗 最小化不必要的内存复制安全 完整的SSL / TLS和StartTLS支持社区活跃不断更新 社区活跃版本迭代周期短发现的BUG可以被及时修复同时更多的新功能会被加入
Netty常见使用场景
Netty常见的使用场景如下
互联网行业 在分布式系统中各个节点之间需要远程服务调用高性能的RPC框架必不可少Netty作为异步高新能的通信框架,往往作为基础通信组件被这些RPC框架使用。 典型的应用有阿里分布式服务框架Dubbo的RPC框架使用Dubbo协议进行节点间通信Dubbo协议默认使用Netty作为基础通信组件用于实现各进程节点之间的内部通信。游戏行业 无论是手游服务端还是大型的网络游戏Java语言得到了越来越广泛的应用。Netty作为高性能的基础通信组件它本身提供了TCP/UDP和HTTP协议栈。 非常方便定制和开发私有协议栈账号登录服务器地图服务器之间可以方便的通过Netty进行高性能的通信大数据领域 经典的Hadoop的高性能通信和序列化组件Avro的RPC框架默认采用Netty进行跨界点通信它的Netty Service基于Netty框架二次封装实现 有兴趣的读者可以了解一下目前有哪些开源项目使用了 NettyRelated projects(opens new window)
Netty高性能设计
Netty作为异步事件驱动的网络高性能之处主要来自于其I/O模型和线程处理模型前者决定如何收发数据后者决定如何处理数据
I/O模型
用什么样的通道将数据发送给对方BIO、NIO或者AIOI/O模型在很大程度上决定了框架的性能
阻塞I/O
传统阻塞型I/O(BIO)可以用下图表示
Blocking I/O 特点
每个请求都需要独立的线程完成数据read业务处理数据write的完整操作
问题
当并发数较大时需要创建大量线程来处理连接系统资源占用较大连接建立后如果当前线程暂时没有数据可读则线程就阻塞在read操作上造成线程资源浪费
I/O复用模型 在I/O复用模型中会用到select这个函数也会使进程阻塞但是和阻塞I/O所不同的的这两个函数可以同时阻塞多个I/O操作而且可以同时对多个读操作多个写操作的I/O函数进行检测直到有数据可读或可写时才真正调用I/O操作函数
Netty的非阻塞I/O的实现关键是基于I/O复用模型这里用Selector对象表示
Nonblocking I/O Netty的IO线程NioEventLoop由于聚合了多路复用器Selector可以同时并发处理成百上千个客户端连接。当线程从某客户端Socket通道进行读写数据时若没有数据可用时该线程可以进行其他任务。线程通常将非阻塞 IO 的空闲时间用于在其他通道上执行 IO 操作所以单独的线程可以管理多个输入和输出通道。
由于读写操作都是非阻塞的这就可以充分提升IO线程的运行效率避免由于频繁I/O阻塞导致的线程挂起一个I/O线程可以并发处理N个客户端连接和读写操作这从根本上解决了传统同步阻塞I/O一连接一线程模型架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。
基于buffer
传统的I/O是面向字节流或字符流的以流式的方式顺序地从一个Stream 中读取一个或多个字节, 因此也就不能随意改变读取指针的位置。
在NIO中, 抛弃了传统的 I/O流, 而是引入了Channel和Buffer的概念. 在NIO中, 只能从Channel中读取数据到Buffer中或将数据 Buffer 中写入到 Channel。
基于buffer操作不像传统IO的顺序操作, NIO 中可以随意地读取任意位置的数据
线程模型
数据报如何读取读取之后的编解码在哪个线程进行编解码后的消息如何派发线程模型的不同对性能的影响也非常大。
事件驱动模型
通常我们设计一个事件处理模型的程序有两种思路
轮询方式 线程不断轮询访问相关事件发生源有没有发生事件有发生事件就调用事件处理逻辑。事件驱动方式 发生事件主线程把事件放入事件队列在另外线程不断循环消费事件列表中的事件调用事件对应的处理逻辑处理事件。事件驱动方式也被称为消息通知方式其实是设计模式中观察者模式的思路。
以GUI的逻辑处理为例说明两种逻辑的不同
轮询方式 线程不断轮询是否发生按钮点击事件如果发生调用处理逻辑事件驱动方式 发生点击事件把事件放入事件队列在另外线程消费的事件列表中的事件根据事件类型调用相关事件处理逻辑
这里借用O’Reilly 大神关于事件驱动模型解释图(opens new window)
事件驱动模型 主要包括4个基本组件
事件队列event queue接收事件的入口存储待处理事件分发器event mediator将不同的事件分发到不同的业务逻辑单元事件通道event channel分发器与处理器之间的联系渠道事件处理器event processor实现业务逻辑处理完成后会发出事件触发下一步操作
可以看出相对传统轮询模式事件驱动有如下优点
可扩展性好分布式的异步架构事件处理器之间高度解耦可以方便扩展事件处理逻辑高性能基于队列暂存事件能方便并行异步处理事件
Reactor线程模型
Reactor是反应堆的意思Reactor模型是指通过一个或多个输入同时传递给服务处理器的服务请求的事件驱动处理模式。 服务端程序处理传入多路请求并将它们同步分派给请求对应的处理线程Reactor模式也叫Dispatcher模式即I/O多了复用统一监听事件收到事件后分发(Dispatch给某进程)是编写高性能网络服务器的必备技术之一。
Reactor模型中有2个关键组成
Reactor Reactor在一个单独的线程中运行负责监听和分发事件分发给适当的处理程序来对IO事件做出反应。 它就像公司的电话接线员它接听来自客户的电话并将线路转移到适当的联系人Handlers 处理程序执行I/O事件要完成的实际事件类似于客户想要与之交谈的公司中的实际官员。Reactor通过调度适当的处理程序来响应I/O事件处理程序执行非阻塞操作
Reactor模型 取决于Reactor的数量和Hanndler线程数量的不同Reactor模型有3个变种
单Reactor单线程单Reactor多线程主从Reactor多线程
可以这样理解Reactor就是一个执行while (true) { selector.select(); …}循环的线程会源源不断的产生新的事件称作反应堆很贴切。
篇幅关系这里不再具体展开Reactor特性、优缺点比较有兴趣的读者可以参考我之前另外一篇文章《理解高性能网络模型》(opens new window)
Netty线程模型
Netty主要基于主从Reactors多线程模型如下图做了一定的修改其中主从Reactor多线程模型有多个ReactorMainReactor和SubReactor
MainReactor负责客户端的连接请求并将请求转交给SubReactorSubReactor负责相应通道的IO读写请求非IO请求具体逻辑处理的任务则会直接写入队列等待worker threads进行处理
这里引用Doug Lee大神的Reactor介绍Scalable IO in Java (opens new window)里面关于主从Reactor多线程模型的图
主从Rreactor多线程模型 特别说明的是 虽然Netty的线程模型基于主从Reactor多线程借用了MainReactor和SubReactor的结构但是实际实现上SubReactor和Worker线程在同一个线程池中
EventLoopGroup bossGroup new NioEventLoopGroup();
EventLoopGroup workerGroup new NioEventLoopGroup();
ServerBootstrap server new ServerBootstrap();
server.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)上面代码中的bossGroup 和workerGroup是Bootstrap构造方法中传入的两个对象这两个group均是线程池
bossGroup线程池则只是在bind某个端口后获得其中一个线程作为MainReactor专门处理端口的accept事件每个端口对应一个boss线程workerGroup线程池会被各个SubReactor和worker线程充分利用
异步处理
异步的概念和同步相对。当一个异步过程调用发出后调用者不能立刻得到结果。实际处理这个调用的部件在完成后通过状态、通知和回调来通知调用者。
Netty中的I/O操作是异步的包括bind、write、connect等操作会简单的返回一个ChannelFuture调用者并不能立刻获得结果通过Future-Listener机制用户可以方便的主动获取或者通过通知机制获得IO操作结果。
当future对象刚刚创建时处于非完成状态调用者可以通过返回的ChannelFuture来获取操作执行的状态注册监听函数来执行完成后的操常见有如下操作
通过isDone方法来判断当前操作是否完成通过isSuccess方法来判断已完成的当前操作是否成功通过getCause方法来获取已完成的当前操作失败的原因通过isCancelled方法来判断已完成的当前操作是否被取消通过addListener方法来注册监听器当操作已完成(isDone方法返回完成)将会通知指定的监听器如果future对象已完成则理解通知指定的监听器
例如下面的的代码中绑定端口是异步操作当绑定操作处理完将会调用相应的监听器处理逻辑 serverBootstrap.bind(port).addListener(future - {if (future.isSuccess()) {System.out.println(new Date() : 端口[ port ]绑定成功!);} else {System.err.println(端口[ port ]绑定失败!);}});相比传统阻塞I/O执行I/O操作后线程会被阻塞住, 直到操作完成异步处理的好处是不会造成线程阻塞线程在I/O操作期间可以执行别的程序在高并发情形下会更稳定和更高的吞吐量。
Netty架构设计
前面介绍完Netty相关一些理论介绍下面从功能特性、模块组件、运作过程来介绍Netty的架构设计
功能特性
Netty功能特性图 传输服务 支持BIO和NIO容器集成 支持OSGI、JBossMC、Spring、Guice容器协议支持 HTTP、Protobuf、二进制、文本、WebSocket等一系列常见协议都支持。 还支持通过实行编码解码逻辑来实现自定义协议Core核心 可扩展事件模型、通用通信API、支持零拷贝的ByteBuf缓冲对象
模块组件
Bootstrap、ServerBootstrap
Bootstrap意思是引导一个Netty应用通常由一个Bootstrap开始主要作用是配置整个Netty程序串联各个组件Netty中Bootstrap类是客户端程序的启动引导类ServerBootstrap是服务端启动引导类。
Future、ChannelFuture
正如前面介绍在Netty中所有的IO操作都是异步的不能立刻得知消息是否被正确处理但是可以过一会等它执行完成或者直接注册一个监听具体的实现就是通过Future和ChannelFutures他们可以注册一个监听当操作执行成功或失败时监听会自动触发注册的监听事件。
Channel
Netty网络通信的组件能够用于执行网络I/O操作。 Channel为用户提供
当前网络连接的通道的状态例如是否打开是否已连接网络连接的配置参数 例如接收缓冲区大小提供异步的网络I/O操作(如建立连接读写绑定端口)异步调用意味着任何I / O调用都将立即返回并且不保证在调用结束时所请求的I / O操作已完成。调用立即返回一个ChannelFuture实例通过注册监听器到ChannelFuture上可以I / O操作成功、失败或取消时回调通知调用方。支持关联I/O操作与对应的处理程序
不同协议、不同的阻塞类型的连接都有不同的 Channel 类型与之对应下面是一些常用的 Channel 类型
NioSocketChannel异步的客户端 TCP Socket 连接NioServerSocketChannel异步的服务器端 TCP Socket 连接NioDatagramChannel异步的 UDP 连接NioSctpChannel异步的客户端 Sctp 连接NioSctpServerChannel异步的 Sctp 服务器端连接 这些通道涵盖了 UDP 和 TCP网络 IO以及文件 IO.
Selector
Netty基于Selector对象实现I/O多路复用通过 Selector, 一个线程可以监听多个连接的Channel事件, 当向一个Selector中注册Channel 后Selector 内部的机制就可以自动不断地查询(select) 这些注册的Channel是否有已就绪的I/O事件(例如可读, 可写, 网络连接完成等)这样程序就可以很简单地使用一个线程高效地管理多个 Channel 。
#NioEventLoop
NioEventLoop中维护了一个线程和任务队列支持异步提交执行任务线程启动时会调用NioEventLoop的run方法执行I/O任务和非I/O任务
I/O任务 即selectionKey中ready的事件如accept、connect、read、write等由processSelectedKeys方法触发。非IO任务 添加到taskQueue中的任务如register0、bind0等任务由runAllTasks方法触发。
两种任务的执行时间比由变量ioRatio控制默认为50则表示允许非IO任务执行的时间与IO任务的执行时间相等。
NioEventLoopGroup
NioEventLoopGroup主要管理eventLoop的生命周期可以理解为一个线程池内部维护了一组线程每个线程(NioEventLoop)负责处理多个Channel上的事件而一个Channel只对应于一个线程。
ChannelHandler
ChannelHandler是一个接口处理I / O事件或拦截I / O操作并将其转发到其ChannelPipeline(业务处理链)中的下一个处理程序。
ChannelHandler本身并没有提供很多方法因为这个接口有许多的方法需要实现方便使用期间可以继承它的子类
ChannelInboundHandler用于处理入站I / O事件ChannelOutboundHandler用于处理出站I / O操作
或者使用以下适配器类
ChannelInboundHandlerAdapter用于处理入站I / O事件ChannelOutboundHandlerAdapter用于处理出站I / O操作ChannelDuplexHandler用于处理入站和出站事件
ChannelHandlerContext
保存Channel相关的所有上下文信息同时关联一个ChannelHandler对象
ChannelPipline
保存ChannelHandler的List用于处理或拦截Channel的入站事件和出站操作。 ChannelPipeline实现了一种高级形式的拦截过滤器模式使用户可以完全控制事件的处理方式以及Channel中各个的ChannelHandler如何相互交互。
下图引用Netty的Javadoc4.1中ChannelPipline的说明描述了ChannelPipeline中ChannelHandler通常如何处理I/O事件。 I/O事件由ChannelInboundHandler或ChannelOutboundHandler处理并通过调用ChannelHandlerContext中定义的事件传播方法例如ChannelHandlerContext.fireChannelReadObject和ChannelOutboundInvoker.writeObject转发到其最近的处理程序。 入站事件由自下而上方向的入站处理程序处理如图左侧所示。 入站Handler处理程序通常处理由图底部的I / O线程生成的入站数据。 通常通过实际输入操作例如SocketChannel.readByteBuffer从远程读取入站数据。
出站事件由上下方向处理如图右侧所示。 出站Handler处理程序通常会生成或转换出站传输例如write请求。 I/O线程通常执行实际的输出操作例如SocketChannel.writeByteBuffer。
在 Netty 中每个 Channel 都有且仅有一个 ChannelPipeline 与之对应, 它们的组成关系如下: 一个 Channel 包含了一个 ChannelPipeline, 而 ChannelPipeline 中又维护了一个由 ChannelHandlerContext 组成的双向链表, 并且每个 ChannelHandlerContext 中又关联着一个 ChannelHandler。入站事件和出站事件在一个双向链表中入站事件会从链表head往后传递到最后一个入站的handler出站事件会从链表tail往前传递到最前一个出站的handler两种类型的handler互不干扰。
工作原理架构
初始化并启动Netty服务端过程如下 public static void main(String[] args) {// 创建mainReactorNioEventLoopGroup boosGroup new NioEventLoopGroup();// 创建工作线程组NioEventLoopGroup workerGroup new NioEventLoopGroup();final ServerBootstrap serverBootstrap new ServerBootstrap();serverBootstrap // 组装NioEventLoopGroup .group(boosGroup, workerGroup)// 设置channel类型为NIO类型.channel(NioServerSocketChannel.class)// 设置连接配置参数.option(ChannelOption.SO_BACKLOG, 1024).childOption(ChannelOption.SO_KEEPALIVE, true).childOption(ChannelOption.TCP_NODELAY, true)// 配置入站、出站事件handler.childHandler(new ChannelInitializerNioSocketChannel() {Overrideprotected void initChannel(NioSocketChannel ch) {// 配置入站、出站事件channelch.pipeline().addLast(...);ch.pipeline().addLast(...);}});// 绑定端口int port 8080;serverBootstrap.bind(port).addListener(future - {if (future.isSuccess()) {System.out.println(new Date() : 端口[ port ]绑定成功!);} else {System.err.println(端口[ port ]绑定失败!);}});
}基本过程如下
1 初始化创建2个NioEventLoopGroup其中boosGroup用于Accetpt连接建立事件并分发请求 workerGroup用于处理I/O读写事件和业务逻辑2 基于ServerBootstrap(服务端启动引导类)配置EventLoopGroup、Channel类型连接参数、配置入站、出站事件handler3 绑定端口开始工作
结合上面的介绍的Netty Reactor模型介绍服务端Netty的工作架构图
服务端Netty Reactor工作架构图 server端包含1个Boss NioEventLoopGroup和1个Worker NioEventLoopGroupNioEventLoopGroup相当于1个事件循环组这个组里包含多个事件循环NioEventLoop每个NioEventLoop包含1个selector和1个事件循环线程。
每个Boss NioEventLoop循环执行的任务包含3步
1 轮询accept事件2 处理accept I/O事件与Client建立连接生成NioSocketChannel并将NioSocketChannel注册到某个Worker NioEventLoop的Selector上 *3 处理任务队列中的任务runAllTasks。任务队列中的任务包括用户调用eventloop.execute或schedule执行的任务或者其它线程提交到该eventloop的任务。
每个Worker NioEventLoop循环执行的任务包含3步
1 轮询read、write事件2 处I/O事件即read、write事件在NioSocketChannel可读、可写事件发生时进行处理3 处理任务队列中的任务runAllTasks。
其中任务队列中的task有3种典型使用场景
1 用户程序自定义的普通任务
ctx.channel().eventLoop().execute(new Runnable() {Overridepublic void run() {//...}
});2 非当前reactor线程调用channel的各种方法 例如在推送系统的业务线程里面根据用户的标识找到对应的channel引用然后调用write类方法向该用户推送消息就会进入到这种场景。最终的write会提交到任务队列中后被异步消费。
3 用户自定义定时任务
ctx.channel().eventLoop().schedule(new Runnable() {Overridepublic void run() {}
}, 60, TimeUnit.SECONDS);总结
现在稳定推荐使用的主流版本还是Netty4Netty5 中使用了 ForkJoinPool增加了代码的复杂度但是对性能的改善却不明显所以这个版本不推荐使用官网也没有提供下载链接。
Netty 入门门槛相对较高其实是因为这方面的资料较少并不是因为他有多难大家其实都可以像搞透 Spring 一样搞透 Netty。在学习之前建议先理解透整个框架原理结构运行过程可以少走很多弯路。