很早之前写过关于Netty的使用。最近发现有网友在看之前写的Netty文章。个人感觉当时写的很粗糙。怕影响同行的阅读质量,所以决定重写一些关于Netty的文章,补充一下之前的不足。图片来自Pexels。Netty能做的就是简单的处理网络编程,编写一个可以通过网络进行通信的服务端和客户端程序。如果没有Netty,在Java的世界里如何应对网络编程?Java自带的工具有:java.net包,用来处理网络通信。后来,Java提供了NIO工具包来提供非阻塞通信。与Netty同级别的第三方工具包:Mina在设计上与Netty略有不同,但核心是提供网络通信能力。传统的网络通信模型在讲Netty之前,先说说传统的网络编程长什么样子。传统的Socket编程开发步骤非常简单,只需要使用Socket类创建客户端和服务端即可。但是为什么现在没人用了呢?主要原因是它基于同步阻塞IO的线程模型,在当今时代根本无法满足生产需求,自然就Out了。同步阻塞IO模型同步阻塞线程模型的问题是一个请求必须绑定一个线程来处理,所有的请求都是同步操作,也就是说在请求处理完之前不会释放连接。如果并发量高,势必会导致系统压力过大。Netty的新线程模型就是基于此。Java增加了一个非阻塞IO操作包NIO。NIO的线程模型采用了Reactor模式,是一种异步非阻塞方式,解决了之前同步阻塞带来的问题。NIO的全称是NoneBlockingIO,非阻塞IO,与BIO不同,BIO的全称是BlockingIO,阻塞IO。那么这个块是什么意思呢?Accept是阻塞的,只有当有新的连接到来时,Accept才会返回,主线程才能继续。读取被阻止。只有请求消息来了,Read才能返回,子线程才能继续处理。Write是阻塞的,只有当客户端收到消息,Write才能返回,子线程才能继续读取下一个请求。服务器响应设计模式目前主要有两种:线程驱动事件驱动同步阻塞是一种线程驱动模式,最明显的例子是Tomcat;对于事件驱动,没有必要为每个连接创建一个线程来维护。参考观察者模式,可以设置一个事件池,用单线程循环监听当前池中是否有完成的事件,如果有则取出事件。简单说说Reactor模式是如何解决线程等待问题的:在等待IO的时候,线程可以先退出,而不用等待IO操作。但是如果不等待,那么IO处理完成后归还给谁呢?Reactor模型采用事件驱动机制,要求线程在退出前向事件循环注册回调函数,以便事件循环在IO完成后调用回调函数完成数据返回。Reactor中有4个角色。所有的数据流入处理统称为Channel,就像水管一样。Reactor模型将每个事件拆分为一个事件,同一类型的事件归为一类。统一的处理逻辑称为处理程序。那么如何让一个或多个线程监听所有的Channel呢?所以有一个选择器。选择器就像一个经理。您可以将多个Channels注册到一个Selector线程。它会使用阻塞的方式来捕捉当前Channel是否有事件。如果有,就会取出事件交给对应的Handler处理。Netty是建立在NIO之上的,Netty在NIO上提供了更高级的API封装。为什么不用JDK提供的NIO呢?JDK已经为我们提供了NIO包,这也是一种使用Reactor模型实现的异步非阻塞模式。那为什么我们在日常开发中没有听说过有人直接使用NIO来开发网络编程呢?其实人们不使用它的原因是因为它太难控制了。JavaNIO类库中提供的主要功能包括:BufferBufferChannelChannelMultiplexerSelectorBufferBuffer实际上是一个对象,即所有传入或传出的数据都存在于Buffer中。newIO和oldstream-orientedIO的区别在于,oldIO直接针对字节流进行处理,newIO针对buffer进行处理,读写数据都是先读写数据到buffer.缓冲区本质上是一个字节数组,NIO提供了维护缓冲区数据读写位置的能力。Buffer和Channel的关系Channel通道,Buffer中的所有数据都会向上流到Channel,数据通过Channel流向处理逻辑,处理后的数据通过Channel返回给客户端。所以Channel是全双工的,可以支持读写,这是它和Stream的区别。如果使用Stream,则只能使用InputStream读取数据,只能使用OutputStream写入数据。用现实世界中的事物来类比,传统IO就像一根水管,水只能顺着管子往下流;蔚来就像一条双向高速公路,双向行驶。另外,也正是因为Buffer的引入,我们可以自由控制每次传输读取多少数据。如果最后一次读取失败,应该从多少个offset重新读取,这是传统I/O流无法比拟的。Selector选择器,这是NIO的核心,一个Selector就是一个线程,NIO允许一个Selector管理多个Channel,即将Channel注册到Selector中。Selector会监听注册的Channel上是否有事件就绪,如果是就取出来处理。Selector轮询监控的NIO代码我就不写了。好大一堆,百度一下就知道了。总之,基于这种思想的网络编程,绝对是面对当今流量高峰的最佳方式。而恰好Netty底层基于NIO的封装,给你屏蔽了这个大操作。网络编程的另一个问题是跨平台。NIO底层依赖于系统的IOAPI。不同的系统可能有不同的IOAPI实现。在这里,如何使用NIO需要考虑系统兼容性。另一个问题是NIO有一个非常著名的bug。JDK的NIO底层是通过epoll实现的。如果Selector的轮询结果为空,没有唤醒和新消息处理,就会发生空轮询,CPU占用率为100%。.官方声明此错误已修复。其实并没有固定,只是发生的概率会降低。Netty也对这个bug进行了处理:统计Selector的Select操作周期,每完成一个空的Select操作就统计一次。如果某个周期内连续出现N个空轮询,就会触发epoll死循环bug。然后此时重建Selector,判断是否是其他线程发起的重建请求。如果没有,就把原来的SocketChannel从旧的Selector中移除,重新注册到新的Selector中,关闭原来的Selector。网络编程要注意什么?既然说要学习Netty,Netty是基于NIO封装进行网络通信的,那么在写一段网络通信的代码时应该注意什么呢?要搞清楚这些问题,我们大概知道Netty做了什么。说到网络,就免不了要说说OSI7层模型和TCP/IP4层模型:OSI模型和对应的网络协议Java网络编程主要使用Socket套接字编程,网络编程基于4-层协议,即基于TCP/UDP协议的封装。写一个Socket通信的步骤是什么?创建一个ServerSocket,监听并绑定一个端口。一系列客户端请求此端口。服务器使用Accept从客户端获取Socket连接对象。启动一个新线程处理连接:①读取Socket,获取字节流;②解码协议,得到HTTP请求对象;③处理HTTP请求,得到结果,封装成HTTPResponse对象;④对协议进行编码,将结果序列化为bytesStream;④编写Socket,将字节流发送给客户端。继续循环第3步,根据上面的数据传输过程,我们可以提出一些问题:如何约定字节流长度格式,从而保证每次读取的字节流都是最新的,不会重复最后一次。传输字节流的编解码问题。一个服务器必须有多个客户端连接,如何管理众多的客户端连接,比如如何保持断线重连、连接超时和关闭机制。我们会在接下来的Netty学习中找到这些问题的答案。Netty核心组件在开始使用Netty之前,我们先来了解一下Netty中都有哪些类,这样我们才能有的放矢,后面借助这些关键信息进行学习。①Bootstrap、ServerBootstrap一个Netty应用程序通常以一个Bootstrap启动。它的主要功能是配置整个Netty程序,串联各个组件。Netty中的Bootstrap类是客户端程序的引导类,ServerBootstrap是服务端的引导类。②Netty中Future和ChannelFuture的所有IO操作都是异步的,并不能立即知道某个事件是否处理完毕。但是你可以稍等片刻完成或者直接注册一个监视器。具体实现是通过Future和ChannelFutures注册一个monitor。当操作成功或失败时,监视器会自动触发注册的监视器事件。③ChannelNetty是网络通信的一个组件,可以用来进行网络I/O操作。Channel为用户提供:当前网络连接的通道状态(如是否开启,是否连接)。网络连接的配置参数(例如接收缓冲区大小)。提供异步网络I/O操作(如建立连接、读写、绑定端口等)。异步调用意味着任何I/O调用都会立即返回,并且不能保证请求的I/O操作在调用结束时已经完成。结束。该调用立即返回一个ChannelFuture实例。通过向ChannelFuture注册监听器,可以在I/O操作成功、失败或取消时通知调用者。支持将I/O操作与相应的处理程序相关联。不同协议、不同阻塞类型的连接,有不同的Channel类型与之对应。以下是一些常用的Channel类型:NioSocketChannel,异步客户端TCPSocket连接。NioServerSocketChannel,异步服务端TCPSocket连接。NioDatagramChannel,一个异步UDP连接。NioSctpChannel,异步客户端Sctp连接。NioSctpServerChannel,异步Sctp服务器端连接,这些通道涵盖UDP和TCP网络IO和文件IO。④SelectorNetty基于Selector对象实现I/O多路复用。通过Selector,一个线程可以监听多个连接的Channel事件。在一个Selector中注册一个Channel后,Selector的内部机制可以自动不断查询(Select)这些注册的Channel是否有就绪的I/O事件(如可读、可写、网络连接完成等),所以程序可以轻松使用一个线程来高效管理多个Channel。⑤NioEventLoopNioEventLoop维护了一个线程和任务队列,支持异步提交执行任务。当线程启动时,会调用NioEventLoop的run方法执行I/O任务和非I/O任务:I/O任务,即selectionKey中的ready事件,如accept、connect、read、write等,由processSelectedKeys方法触发。非IO任务,加入到taskQueue中的任务,如register0、bind0等任务,由runAllTask??s方法触发。两个任务的执行时间比例由变量ioRatio控制,默认为50,即允许非IO任务的执行时间与IO任务的执行时间相等。⑥NioEventLoopGroupNioEventLoopGroup主要管理eventLoop的生命周期。可以理解为线程池,内部维护着一组线程。每个线程(NioEventLoop)负责处理多个Channel上的事件,一个Channel只对应一个线程。⑦ChannelHandlerChannelHandler是处理I/O事件或拦截I/O操作并转发给其ChannelPipeline(业务处理链)中的下一个handler的接口。ChannelHandler本身并没有提供很多方法,因为这个接口有很多方法需要实现,方便使用时可以继承它的子类:ChannelInboundHandler用于处理入站I/O事件。ChannelOutboundHandler用于处理出站I/O操作。或者使用以下适配器类:ChannelInboundHandlerAdapter用于处理入站I/O事件。ChannelOutboundHandlerAdapter用于处理出站I/O操作。ChannelDuplexHandler用于处理入站和出站事件。⑧ChannelHandlerContext保存了所有与Channel相关的上下文信息,同时关联了一个ChannelHandler对象。⑨ChannelPipline保存ChannelHandler的List,用于处理或拦截Channel的入站事件和出站操作。它实现了一种高级形式的拦截过滤器模式,使用户可以完全控制事件的处理方式以及Channel中的各个ChannelHandler之间的交互方式。在Netty中,每个Channel都有一个且只有一个ChannelPipeline与之对应。Netty的介绍到此为止。后面我会结合Socket通信应该解决的问题以及上面提到的Netty的关键组件来讲解Netty是如何实现高性能网络通信的。作者:rickiyang编辑:陶家龙来源:https://www.cnblogs.com/rickiyang/
