并发编程- Outline of Netty
并发编程- Outline of Netty
在分布式架构中网络通讯是一个非常重要的东西,因为不管架构多么厉害,网络通讯的性能还是直接影响用户体验的一个重要因素。在java中我们有很多可以处理网络的框架,和一些基础的东西,比如NIO、BIO、Socket, 这些我们在前面都有聊过,但是当我们基于这些东西去开发的时候,我们会发现,他们提供的API比较复杂,那有些开源框架就应运而生,比如Mina,Netty等。但凡是中间件就有涉及网络通讯,比如zk、dubbo、redis、rocketmq,etc.他们有的底层就是使用的Netty, 并且在分布式架构下,网络的重要性也可想而知了。所以我就准备和大家聊聊Netty这个东西。
网络IO的发展
【BIO(同步阻塞IO)】:客户端发送一个请求到服务端,服务端收到请求后需要阻塞,直到客户端写到数据到服务端后,服务端才能响应。那么我们服务端能同时处理的数据是有限的。(因为需要排队)
【NIO】:通过线程去轮询,没有连接可以使用的时候就直接返回。这就带来了性能问题(从用户态到内核态的转变)
多路复用(epoll):当客户端连接进到服务端,就把连接注册到selector中,当selector中的任何一个channel(任何一个客户端连接)发生了连接事件,或者io事件的时候,都会响应。响应方式就是我们通过一个工作线程去轮询就绪的channel列表,然后进行相关的处理。
【Reactor模型】:基于Nio的底层实现的一种高性能的设计模式,他的核心点就是把相应的机制和业务进行了分离,这样我们就可以通过一个线程或者多个线程去处理IO事件,这样的话就可以进行灵活的扩展.我们聊redis的时候也聊过这个。但是前面聊的是【单线程单reactor】模型,弊病就是当我们的连接进来的时候,还是要等待处理,在reactor处。、
- 【多线程单reactor模型】:当服务端连接过来后,我们把连接使用线程池进行异步处理,这里就不用阻塞
- 【多线程多reactor模型】:当连接进来的时候,我们的main reactor 去接收客户端的连接,然后把这些连接分发给不同的subreactor进行IO处理
有几个容易混淆的点,同步非阻塞、异步非阻塞 (阻塞不阻塞指的是IO层面的,同步非同步指的是应用层面的)
Netty的底层是针对Nio做的一个封装,提供了更加简单的开发方式,之前我们要完成一个多线程多reacto可能需要多个class,而在netty中可能就十几行代码。
Outline of Netty
Netty提供了上述三种Reactor模型的支持,并且相比于NIO原生API,它有以 下特点:
- 提供了高效的I/O模型、线程模型和时间处理机制 提供了非常简单易用的API,相比NIO来说,针对基础的Channel、Selector、Sockets、Buffers等 api提供了更高层次的封装,屏蔽了NIO的复杂性 对数据协议和序列化提供了很好的支持 稳定性.
- Netty修复了JDK NIO较多的问题,比如select空转导致的cpu消耗100%、TCP断线重连、 keep-alive检测等问题。
- 性能层面的优化,作为网络通信框架,需要处理大量的网络请求,必然就面临网络对象需要创建和 销毁的问题,这种对JVM的GC来说不是很友好,为了降低JVM垃圾回收的压力,引入了两种优化机 制 对象池复用, 零拷贝技术
Usage of Netty
引入Netty的包
View Code<dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> </dependency>使用netty实现一个多线程多reactor模型
整体流程:
- 当客户端进来的时候,首先通过mainGroup去接收客户端的连接
- 然后把连接注册到EventLoopGroup中,这里面有多个eventLoop,每个eventloop都代表的是一个线程,也就是说客户端会注册到其中的某一个eventloop中。
- 当某个eventLoop有IO事件的时候,会把请求发送给channel pipeline 进行处理
- channel pipeline 可以多个,我们可以一直添加,他会按照我们的添加的顺序执行。
- channel pipeline 分为两种入栈(inbound)和出栈(outbound),也就是读取和输出。
View Code// 具体处理IO的handler public class NormalMessageHandler extends ChannelInboundHandlerAdapter { //当请求过来的时候,会调用这个方法对数据进行读取 @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { ByteBuf in = (ByteBuf) msg; byte[] req=new byte[in.readableBytes()]; in.readBytes(req); System.out.println("服务端收到的数据"+new String(req, StandardCharsets.UTF_8)); ByteBuf resp= Unpooled.copiedBuffer("Service receive message success".getBytes()); ctx.write(resp); } //把数据写回到客户端,然后监听客户端关闭事件 @Override public void channelReadComplete(ChannelHandlerContext ctx) { ctx.write(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { super.exceptionCaught(ctx, cause); } }View Codepublic class NettyBasicServerExample { // 开发一个多reactor,多线程的模型 public static void main(String[] args) { // 主线程(相当于我们的main reactor) EventLoopGroup mainGroup=new NioEventLoopGroup(); //表示多个工作线程组 (相当于我们的sub reactor) 注册我们的事件 EventLoopGroup workGroup=new NioEventLoopGroup(4); //构建netty server 的 API ServerBootstrap serverBootstrap=new ServerBootstrap(); // 使用Nio的API并且构建Handler(这个handler会对消息进行处理) serverBootstrap.group(mainGroup,workGroup) // 指定使用的模型 .channel(NioServerSocketChannel.class) //具体的工作处理类,处理相关channel io事件 .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel socketChannel) { //处理io事件 socketChannel.pipeline().addLast(new NormalMessageHandler()); } }); // 同步阻塞等到客户端回调 try { ChannelFuture sync = serverBootstrap.bind(8080).sync(); System.out.println("Netty server started successfully and the port 8080 has been listened"); // 同步等待服务端端口关闭 sync.channel().closeFuture().sync(); } catch (InterruptedException e) { e.printStackTrace(); }finally { // 释放资源 mainGroup.shutdownGracefully(); workGroup.shutdownGracefully(); } } }
more detail about netty
事件调度器(是通过Reactor线程模型对各类事件进行聚合处理,通过Selector主循环线程集成多种事件 (I/O时间、信号时间),当这些事件被触发后,具体针对该事件的处理需要给到服务编排层中相关的 Handler来处理)
事件调度器的核心组件有两个:
- EventLoopGroup:实际上就相当于是线程池
- EventLoop:这就相当于线程池中的线程
总的来说:
- 一个EventLoopGroup中有多个eventloop,这些个eventloop用来处理Channel生命周期内所有的 I/O事件,比如accept、connect、read、write等
- EventLoop同一时间会与一个线程绑定,每个EventLoop负责处理多个Channel
- 每新建一个Channel,EventLoopGroup会选择一个EventLoop进行绑定,该Channel在生命周期 内可以对EventLoop进行多次绑定和解绑。
- 我们可以简单的把EventLoopGroup当成是Netty中Reactor线程模型的具体实现,我们可以通过配 置不同的EventLoopGroup使得Netty支持多种不同的Reactor模型。
- 【单线程模型】:EventLoopGroup只包含一个EventLoop,Boss和Worker使用同一个 EventLoopGroup
- 【多线程模型】:EventLoopGroup包含多个EventLoop,Boss和Worker使用同一个 EventLoopGroup。
- 【主从多线程模型】:EventLoopGroup包含多个EventLoop,Boss是主Reactor,Worker是从 Reactor。他们分别使用不同的EventLoopGroup,主Reactor负责新的网络连接Channel的创 建(也就是连接的事件),主Reactor收到客户端的连接后,交给从Reactor来处理。
服务编排层(就是I/O事件触发后,需要有一个Handler来处 理,服务编排层就可以通过一个Handler处理链来实现网络事件的动态编排和有序的传播)
它包含三个组件:
【ChannelPipeline】:它采用了双向链表将多个Channelhandler链接在一起,当I/O事件触发时, ChannelPipeline会依次调用组装好的多个ChannelHandler,实现对Channel的数据处理。 ChannelPipeline是线程安全的,因为每个新的Channel都会绑定一个新的ChannelPipeline。一个 ChannelPipeline关联一个EventLoop,而一个EventLoop只会绑定一个线程,
【ChannelHandler】: 针对IO数据的处理器,数据接收后,通过指定的Handler进行处理
【ChannelHandlerContext】,ChannelHandlerContext用来保存ChannelHandler的上下文信息,也 就是说,当事件被触发后,多个handler之间的数据,是通过ChannelHandlerContext来进行传递 的。每个ChannelHandler都对应一个自己的ChannelHandlerContext,它保留了ChannelHandler所 需要的上下文信息,多个ChannelHandler之间的数据传递,是通过ChannelHandlerContext来实 现的。
总的工作机制:
服务单启动初始化Boss和Worker线程组,Boss线程组负责监听网络连接事件,当有新的连接建立 时,Boss线程会把该连接Channel注册绑定到Worker线程 Worker线程组会分配一个EventLoop负责处理该Channel的读写事件,每个EventLoop相当于一 个线程。通过Selector进行事件循环监听。 当客户端发起I/O事件时,服务端的EventLoop讲就绪的Channel分发给Pipeline,进行数据的处理 数据传输到ChannelPipeline后,从第一个ChannelInBoundHandler进行处理,按照pipeline链逐 个进行传递 服务端处理完成后要把数据写回到客户端,这个写回的数据会在ChannelOutboundHandler组成 的链中传播,最后到达客户端