Netty介绍及基于Netty手写简易版Tomcat

一.netty简介

1.是什么

高性能网络通信底层框架,封装了java的nio操作,且进行了优化。(高性能rpc框架

2.解决了什么问题

  • 提供了高效的I/O模型、线程模型和时间处理机制
  • 提供了非常简单易用的API,相比NIO来说,针对基础的Channel、Selector、Sockets、Buffers等 api提供了更高层次的封装屏蔽了NIO的复杂性
  • 数据协议和序列化提供了很好的支持
  • 稳定性,Netty修复了JDK NIO较多的问题,比如select空转导致的cpu消耗100%、TCP断线重连、 keep-alive检测等问题。
  • 可扩展性在同类型的框架中都是做的非常好的,比如一个是可定制化的线程模型,用户可以在启动 参数中选择Reactor模型可扩展的事件驱动模型,将业务和框架的关注点分离。
  • 性能层面的优化,作为网络通信框架,需要处理大量的网络请求,必然就面临网络对象需要创建和 销毁的问题,这种对JVM的GC来说不是很友好,为了降低JVM垃圾回收的压力,引入了两种优化机制:对象池复用, 零拷贝技术

3.什么场景使用

任何通信场景;dubbo、mq底层都在使用

4.集成及重要组件的介绍

4.1集成

        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.6.Final</version>
        </dependency>

4.2重要组件介绍

  • Bootstrap 和 ServerBootstrap
  • Channel
  • ChannelHandler
  • ChannelPipeline
  • EventLoop
  • ChannelFuture

1)Bootstrap或者ServerBootstrap,一个Netty应用通常由一个Bootstrap开始,它主要作用是配置整个Netty程序,串联起各个组件

2)Channel:Channel 是 Netty 网络操作抽象类,它除了包括基本的 I/O 操作,如 bind、connect、read、write 之外,还包括了 Netty 框架相关的一些功能,如获取该 Channel的 EventLoop。在传统的网络编程中,作为核心类的 Socket ,它对程序员来说并不是那么友好,直接使用其成本还是稍微高了点。而Netty 的 Channel 则提供的一系列的 API :它大大降低了直接与 Socket 进行操作的复杂性。而相对于原生 NIO 的 Channel,Netty 的 Channel 具有如下优势:

  1. 在 Channel 接口层,采用 Facade 模式进行统一封装,将网络 I/O 操作、网络 I/O 相关联的其他操作封装起来,统一对外提供

  2. Channel 接口的定义尽量大而全,为 SocketChannel 和 ServerSocketChannel 提供统一的视图,由不同子类实现不同的功能,公共功能在抽象父类中实现,最大程度地实现功能和接口的重用。

  3. 具体实现采用聚合而非包含的方式,将相关的功能类聚合在 Channel 中,有 Channel 统一负责和调度,功能实现更加灵活。

  Channel 与 socket 的关系:

  在 Netty 中 Channel 有两种,对应客户端套接字通道NioSocketChannel,内部管理java.nio.channels.SocketChannel 套接字,对应服务器端监听套接字通道NioServerSocketChannel,其内部管理自己的 java.nio.channels.ServerSocketChannel 套接字。也就是 Channel 是对 socket 的装饰或者门面,其封装了对socket 的原子操作。

3)ChannelHandler:ChannelHandler 为 Netty 中最核心的组件,它充当了所有处理入站和出站数据的应用程序逻辑的容器。ChannelHandler 主要用来处理各种事件,这里的事件很广泛,比如可以是连接、数据接收、异常、数据转换等。ChannelHandler 有两个核心子类 ChannelInboundHandler 和 ChannelOutboundHandler,其中ChannelInboundHandler 用于接收、处理入站数据和事件,而 ChannelOutboundHandler 则相反

4)ChannelPipeline:ChannelPipeline 为 ChannelHandler 链提供了一个容器并定义了用于沿着链传播入站和出站事件流的 API。一个数据或者事件可能会被多个 Handler 处理,在这个过程中,数据或者事件经流 ChannelPipeline,由 ChannelHandler 处理。在这个处理过程中,一个 ChannelHandler 接收数据后处理完成后交给下一个 ChannelHandler,或者什么都不做直接交给下一个 ChannelHandler。

  当一个数据流进入 ChannlePipeline 时,它会从 ChannelPipeline 头部开始传给第一个 ChannelInboundHandler ,当第一个处理完后再传给下一个,一直传递到管道的尾部。与之相对应的是,当数据被写出时,它会从管道的尾部开始,先经过管道尾部的 “最后” 一个ChannelOutboundHandler,当它处理完成后会传递给前一个ChannelOutboundHandler 。当 ChannelHandler 被添加到 ChannelPipeline 时,它将会被分配一个 ChannelHandlerContext,它代表了 ChannelHandler 和 ChannelPipeline之间的绑定。其中 ChannelHandler 添加到 ChannelPipeline 过程如下:

  1. 一个 ChannelInitializer 的实现被注册到了 ServerBootStrap中

  2. 当 ChannelInitializer.initChannel() 方法被调用时,ChannelInitializer 将在 ChannelPipeline 中安装一组自定义的 ChannelHandler

  3. ChannelInitializer 将它自己从 ChannelPipeline 中移除

5)EventLoop:Netty 基于事件驱动模型,使用不同的事件来通知我们状态的改变或者操作状态的改变。它定义了在整个连接的生命周期里当有事件发生的时候处理的核心抽象。Channel 为Netty 网络操作抽象类,EventLoop 主要是为Channel 处理 I/O 操作,两者配合参与 I/O 操作。下图是Channel、EventLoop、Thread、EventLoopGroup之间的关系(摘自《Netty In Action》):

  • 一个 EventLoopGroup(Boos线程池,work线程池的分组概念) 包含一个或多个 EventLoop。
  • 一个 EventLoop 在它的生命周期内只能与一个Thread绑定。
  • 所有有 EnventLoop 处理的 I/O 事件都将在它专有的 Thread 上被处理。
  • 一个 Channel 在它的生命周期内只能注册与一个 EventLoop。
  • 一个 EventLoop 可被分配至一个或多个 Channel 。

  当一个连接到达时,Netty 就会注册一个 Channel,然后从 EventLoopGroup 中分配一个 EventLoop 绑定到这个Channel上,在该Channel的整个生命周期中都是有这个绑定的 EventLoop 来服务的。

6)ChannelFuture:Netty 为异步非阻塞,即所有的 I/O 操作都为异步的,因此,我们不能立刻得知消息是否已经被处理了。Netty 提供了 ChannelFuture 接口,通过该接口的 addListener() 方法注册一个 ChannelFutureListener,当操作执行成功或者失败时,监听就会自动触发返回结果

5.Netty线程模型:

  了解了Netty 基础服务的构建,我们对Netty服务有了一定的认识,Netty的整体工作机制如下,整体设计就是前面我们讲过的多线程Reactor模型分离请求监听和请求处 理,通过多线程分别执行具体的handler

网络通信层:

  网络通信层主要的职责是执行网络的IO操作,它支持多种网络通信协议和I/O模型的链接操作。当网络 数据读取到内核缓冲区后,会触发读写事件,这些事件在分发给时间调度器来进行处理。

  在Netty中,网络通信的核心组件以下三个组件

  • Bootstrap, 客户端启动api,用来链接远程netty server,只绑定一个EventLoopGroup
  • ServerBootStrap,服务端监听api,用来监听指定端口,会绑定两个EventLoopGroup, bootstrap组件可以非常方便快捷的启动Netty应用程序
  • Channel,Channel是网络通信的载体,Netty自己实现的Channel是以JDK NIO channel为基础, 提供了更高层次的抽象,同时也屏蔽了底层Socket的复杂性,为Channel提供了更加强大的功能。

  下图表示的是Channel的常用实现实现类关系图,AbstractChannel是整个Channel实现的基 类,派生出了AbstractNioChannel(非阻塞io)、AbstractOioChannel(阻塞io),每个子类代表了不同 的I/O模型和协议类型。

  随着连接和数据的变化,Channel也会存在多种状态,比如连接建立、连接注册、连接读写、连接销 毁。随着状态的变化,Channel也会处于不同的生命周期,每种状态会绑定一个相应的事件回调。以下 是常见的时间回调方法。

  • channelRegistered, channel创建后被注册到EventLoop上
  • channelUnregistered,channel创建后未注册或者从EventLoop取消注册
  • channelActive,channel处于就绪状态,可以被读写
  • channelInactive,Channel处于非就绪状态
  • channelRead,Channel可以从源端读取数据
  • channelReadComplete,Channel读取数据完成

  Bootstrap和ServerBootStrap分别负责客户端和服务端的启动,Channel是网络通信的 载体,它提供了与底层Socket交互的能力。 而当Channel生命周期中的事件变化,就需要触发进一步处理,这个处理是由Netty的事件调度器来完 成。

事件调度器:

  事件调度器是通过Reactor线程模型对各类事件进行聚合处理,通过Selector主循环线程集成多种事件 (I/O时间、信号时间),当这些事件被触发后,具体针对该事件的处理需要给到服务编排层中相关的 Handler来处理。

  事件调度器核心组件:

  • EventLoopGroup。相当于线程池
  • EventLoop。相当于线程池中的线程

  EventLoopGroup本质上是一个线程池,主要负责接收I/O请求,并分配线程执行处理请求。下图展示了Netty接受请求的处理过程、EventLoopGroup、EventLoop、Channel之间的关系

  从图中可知

  • 一个EventLoopGroup可以包含多个EventLoop,EventLoop用来处理Channel生命周期内所有的 I/O事件,比如accept、connect、read、write等
  • EventLoop同一时间会与一个线程绑定,每个EventLoop负责处理多个Channel
  • 每新建一个Channel,EventLoopGroup会选择一个EventLoop进行绑定,该Channel在生命周期 内可以对EventLoop进行多次绑定和解绑。

  表示的是EventLoopGroup的类关系图,可以看出Netty提供了EventLoopGroup的多种实现,如 NioEventLoop、EpollEventLoop、NioEventLoopGroup等。

  从图中可以看到,EventLoop是EventLoopGroup的子接口,我们可以把EventLoop等价于 EventLoopGroup,前提是EventLoopGroup中只包含一个EventLoop。

  EventLoopGroup是Netty的核心处理引擎,它和前面我们讲解的Reactor线程模型有什么关系呢?其 实,我们可以简单的把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只会绑定一个线程,ChannelPipeline中包含入站ChannelInBoundHandler和出站 ChannelOutboundHandler,前者是接收数据,后者是写出数据,其实就是InputStream和 OutputStream。为了更好的理解,我们来看图。

  ChannelHandler, 针对IO数据的处理器,数据接收后,通过指定的Handler进行处理。

  ChannelHandlerContext,ChannelHandlerContext用来保存ChannelHandler的上下文信息,也 就是说,当事件被触发后,多个handler之间的数据,是通过ChannelHandlerContext来进行传递 的。ChannelHandler和ChannelHandlerContext之间的关系,如图所示。 每个ChannelHandler都对应一个自己的ChannelHandlerContext,它保留了ChannelHandler所 需要的上下文信息,多个ChannelHandler之间的数据传递,是通过ChannelHandlerContext来实 现的。

  以上就是Netty中核心的组件的特性和工作机制的介绍,后续的内容中还会详细的分析这几个组件。可 以看出,Netty的架构分层设计是非常合理的,它屏蔽了底层NIO以及框架层的实现细节,对于业务开 发者来说,只需要关心业务逻辑的编排和实现即可。

组件关系及原理总结:

  • 服务单启动初始化Boss和Worker线程组,Boss线程组负责监听网络连接事件,当有新的连接建立 时,Boss线程会把该连接Channel注册绑定到Worker线程
  • Worker线程组会分配一个EventLoop负责处理该Channel的读写事件,每个EventLoop相当于一 个线程。通过Selector进行事件循环监听。
  • 当客户端发起I/O事件时,服务端的EventLoop讲就绪的Channel分发给Pipeline,进行数据的处理
  • 数据传输到ChannelPipeline后,从第一个ChannelInBoundHandler进行处理,按照pipeline链逐 个进行传递
  • 服务端处理完成后要把数据写回到客户端,这个写回的数据会在ChannelOutboundHandler组成 的链中传播,最后到达客户端。

二.基于netty写一个tomcat熟悉netty的api

tomcat设计思路:

服务端设计思路:ip地址,端口号,请求的具体服务(暴露的服务映射服务端的具体哪个类),数据的接收和数据的写出

public class TccTomcat {

    private int port = 8080;

    private ServerSocket server;

    private Properties webxml = new Properties();

    private Map<String, GPServlet> servletMapping = new HashMap<String, GPServlet>();

    public static void main(String[] args) {
        new GPTomcat().start();
    }

    //Tomcat的启动入口
    private void start() {
        //1、加载web.properties文件,解析配置
        init();

        //创建两个EventLoopGroup对象,创建boss线程组 ⽤于服务端接受客户端的连接
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        //创建 worker 线程组 ⽤于进⾏ SocketChannel 的数据读写
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        //启动类初始化
        ServerBootstrap server = new ServerBootstrap();

        try {
            //3、配置服务端参数
            server.group(bossGroup, workerGroup)
                    //配置主线程的处理逻辑
                    .channel(NioServerSocketChannel.class)
                    //子线程的回调处理,Handler
                    .childHandler(new ChannelInitializer() {
                        @Override
                        protected void initChannel(Channel client) throws Exception {
                            //处理回调的逻辑

                            //链式编程,责任链模式

                            //处理响应结果的封装
                            client.pipeline().addLast(new HttpResponseEncoder());
                            //用户请求过来,要解码
                            client.pipeline().addLast(new HttpRequestDecoder());
                            //用户自己的业务逻辑
                            client.pipeline().addLast(new GPTomcatHandler());

                        }
                    })
                    //配置主线程分配的最大线程数
                    .option(ChannelOption.SO_BACKLOG, 128)
                    //保持长连接
                    .childOption(ChannelOption.SO_KEEPALIVE, true);

            //启动服务
            ChannelFuture f = server.bind(this.port).sync();

            System.out.println("GP Tomcat 已启动,监听端口是: " + this.port);

            f.channel().closeFuture().sync();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }

    }


    private void init() {
        try {
            String WEB_INF = this.getClass().getResource("/").getPath();
            FileInputStream fis = new FileInputStream(WEB_INF + "web-nio.properties");

            webxml.load(fis);

            for (Object k : webxml.keySet()) {

                String key = k.toString();

                if(key.endsWith(".url")){

                    //将 servlet.xxx.url 的 .url 替换,只剩下 servlet.xxx当成  servletName
                    String servletName = key.replaceAll("\\.url$","");
                    String url = webxml.getProperty(key);

                    //拿到Serlvet的全类名
                    String className = webxml.getProperty(servletName + ".className");

                    //反射创建Servlet的实例
                    GPServlet obj = (GPServlet) Class.forName(className).newInstance();
                    //将URL和Servlet建立映射关系
                    servletMapping.put(url,obj);
                }

            }

        }catch (Exception e){
            e.printStackTrace();
        }
    }

    public class TccTomcatHandler extends ChannelInboundHandlerAdapter{
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

            if(msg instanceof HttpRequest){
                HttpRequest req = (HttpRequest) msg;

                GPRequest request = new GPRequest(ctx,req);
                GPResponse response = new GPResponse(ctx,req);

                String url = request.getUrl();

                if(servletMapping.containsKey(url)){
                    servletMapping.get(url).service(request,response);
                }else{
                    response.write("404 - Not Found!!");
                }

            }


        }
    }

}

 

posted @ 2021-12-11 15:38  武魂95级蓝银草  阅读(417)  评论(0编辑  收藏  举报