zoukankan      html  css  js  c++  java
  • Netty核心组件介绍及手写简易版Tomcat

    Netty是什么:

    1. 异步事件驱动框架,用于快速开发高i性能服务端和客户端
    2. 封装了JDK底层BIO和NIO模型,提供高度可用的API
    3. 自带编码解码器解决拆包粘包问题,用户只用关心业务逻辑
    4. 精心设计的Reactor线程模型支持高并发海量连接
    5. 自带协议栈,无需用户关心
      Netty 是一款提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。也就是说,Netty 是一个基于 NIO 的客户、服务器端编程框架,使用 Netty 可以确保你快速和简单地开发出一个网络应用,例如实现了某种协议的客户,服务端应用。Netty 相当简化和流线化了网络应用的编程开发过程,例如,TCP 和 UDP 的 socket 服务开发。

    Netty具有如下特性:

    • 设计:统一的API,支持多种传输类型,阻塞和非阻塞的,简单而强大的线程模型,真正的无连接数据报套接字支持,链接逻辑组件以支持复用。
    • 易于使用:详实的 Javadoc 和大量的示例集不需要超过JdK 1.6+的依赖。
    • 性能:拥有比 Java 的核心 API 更高的吞吐量以及更低的延迟,得益于池化和复用,拥有更低的资源消耗,最少的内存复制。
    • 健壮性:不会因为慢速、快速或者超载的连接而导致 OutOfMemoryError ,消除在高速网络中 NIO 应用程序常见的不公平读/写比率。
    • 安全性:完整的 SSL/TLS 以及 StartTLs 支持,可用于受限环境下,如 Applet 和 OSGI。
    • 社区驱动:发布快速而且频繁。

    Netty核心组件:

      为了后期更好地理解和进一步深入 Netty,有必要总体认识一下 Netty 所用到的核心组件以及他们在整个 Netty 架构中是如何协调工作的。Nettty 有如下几个核心组件:

    • 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,当操作执行成功或者失败时,监听就会自动触发返回结果。

      通过了解相应的组件,接下去先简单看一下Netty的基本使用,同样的市服务端与客户端的交互。

      服务端:
    public class NettyServer  {
    
        private static final String IP = "127.0.0.1";
        private static final int port = 6666;
        private static final int BIZGROUPSIZE =  Runtime.getRuntime().availableProcessors() * 2;
        private static final int BIZTHREADSIZE = 100;
        //创建两个EventLoopGroup对象,创建boss线程组 ⽤于服务端接受客户端的连接
        private static final EventLoopGroup bossGroup = new NioEventLoopGroup(BIZGROUPSIZE);
        //创建 worker 线程组 ⽤于进⾏ SocketChannel 的数据读写
        private static final EventLoopGroup workGroup = new NioEventLoopGroup(BIZTHREADSIZE);
    
        public static void start() throws Exception {
    
            //启动类初始化
            ServerBootstrap serverBootstrap = initServerBootstrap();
            // 绑定端⼝,并同步等待成功,即启动服务端
            ChannelFuture channelFuture = serverBootstrap.bind(IP, port).sync();
            //成功绑定到端口之后,给channel增加一个 管道关闭的监听器并同步阻塞,直到channel关闭,线程才会往下执行,结束进程。
            channelFuture.channel().closeFuture().sync();
            System.out.println("server start");
    
        }
    
        private static ServerBootstrap initServerBootstrap() {
            //一个Netty应用通常由一个Bootstrap开始
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            //添加两个组,设置使⽤的EventLoopGroup
            serverBootstrap.group(bossGroup,workGroup)
                    //初始化 channel,设置要被实例化的为 NioServerSocketChannel 类
                    .channel(NioServerSocketChannel.class)
                    //初始化channelHandler,设置连⼊服务端的 Client 的 SocketChannel 的处理器
                    .childHandler(new ChannelInitializer<Channel>() {
                //我们再来设置下相应的过滤条件。 这⾥需要继承Netty中ChannelInitializer 类,
                //然后重写 initChannel 该⽅法,进⾏添加相应的设置,传输协议设置,以及相应的业务实现类
                        @Override
                        protected void initChannel(Channel ch) throws Exception {
                            //配置pipeline相关属性
                            ChannelPipeline pipeline = ch.pipeline();
                            pipeline.addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE,0,4,0,4));
                            pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
                            pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
                            // 相关处理 Handler
                            pipeline.addLast(new TcpServerHandler());
                        }
                    });
            return serverBootstrap;
        }
    
        protected static void shutdown(){
            workGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    
        public static void main(String[] args) throws Exception {
            System.out.println("启动Server...");
            NettyServer.start();
        }
    }

      服务相关的设置的代码写完之后,我们再来编写主要的业务代码。 使⽤Netty编写 [业务层 ]的代码,我们需要继承 ChannelInboundHandlerAdapter 或 SimpleChannelInboundHandler 类,在这⾥说下它们两的区别吧。

      继承 SimpleChannelInboundHandler 类之后,会在接收到数据后会⾃动 release 掉数据占⽤的 Bytebuffer 资源。并且继承该类需要指定数据格式。

      ⽽继承ChannelInboundHandlerAdapter 则不会⾃动释放,需要⼿动调⽤ReferenceCountUtil.release() 等⽅法进⾏释放。继承该类不需要指定数据格式。 所以在这⾥,个⼈推荐服务端继承 ChannelInboundHandlerAdapter ,⼿动进⾏释放,防⽌数据未处理完就⾃动释放了。⽽且服务端可能有多个客户端进⾏连接,并且每⼀个客户端请求的数据格式都不⼀致,这时便可以进⾏相应的处理。

      客户端根据情况可以继承 SimpleChannelInboundHandler 类。好处是直接指定好传输的数据格式,就不需要再进⾏格式的转换了。

      TcpServerHandler :

    public class TcpServerHandler extends ChannelInboundHandlerAdapter {
        //建⽴连接时,发送⼀条庆祝消息
        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {
            System.out.println("chanelActive>>>>>>>");
        }
        //业务逻辑处理
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            System.out.println("server receive message:" + msg);
            ctx.channel().writeAndFlush("accept message "+ msg);
            ctx.close();
        }
        //异常相关处理
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
    
            System.out.println("get server exception :"+cause.getMessage());
        }
    }
    

      客户端:客户端过滤其这块基本和服务端⼀致。不过需要注意的是,传输协议、编码和解码应该⼀致.

    public class NettyClient implements Runnable {
    
        @Override
        public void run() {
            EventLoopGroup group = new NioEventLoopGroup();
            try {
                Bootstrap bootstrap = new Bootstrap();
                bootstrap.group(group);
                bootstrap.channel(NioSocketChannel.class)
                        .option(ChannelOption.TCP_NODELAY, true)
                        .handler(new ChannelInitializer<SocketChannel>() {
                            @Override
                            protected void initChannel(SocketChannel ch) throws Exception {
                                ChannelPipeline pipeline = ch.pipeline();
                                pipeline.addLast("frameEncoder", new LengthFieldPrepender(4));
                                pipeline.addLast("decoder", new StringDecoder(CharsetUtil.UTF_8));
                                pipeline.addLast("encoder", new StringEncoder(CharsetUtil.UTF_8));
                                pipeline.addLast("handler", new MyClient());
                            }
                        });
                for (int i=0;i<10;i++){
                    ChannelFuture f = bootstrap.connect("127.0.0.1",6666).sync();
                    f.channel().writeAndFlush("hello service !" + Thread.currentThread().getName()+ ":---->"+i);
                    f.channel().closeFuture().sync();
                }
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                group.shutdownGracefully();
            }
    
        }
    
        public static void main(String[] args) {
            for (int i = 0;i < 3 ;i++ ){
                new Thread(new NettyClient(),">>> this thread "+i).start();
            }
        }
    }
    

      MyClient :这⾥有个注解, 该注解 Sharable 主要是为了多个handler可以被多个channel安全地共享,也就是保证线程安全。

    public class MyClient extends ChannelInboundHandlerAdapter {
        //@Sharable
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            System.out.println("client receieve message: "+msg);
        }
    
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            System.out.println("get client exception :"+cause.getMessage());
        }
    }

      启动服务器,客户端即可看到演示效果。

    Netty线程模型:

      了解了Netty 基础服务的构建,我们对Netty服务有了一定的认识,最后来一张线程模型图(Reactor主从多线程模型):

    Netty实现简易版Tomcat:

      之前我们通过手写springMvc,实现了自己的Mvc的调用流程,其中最本质的东西是通过Servlert,将我们对应的Controller对应的请求路径及controller给映射缓存起来,Tomcat我们称之为Servlet容器,所以我们将自己实现的Servlet交由其管理是理所当然的,既然现在我们自己有映射关系,同时现在也有了Netty这么强大的通信框架,也了解了他的基本使用,那么我们如何将其与我们的程序关联起来,实现自己 容器呢?

      在手写之前,我们需要明白的是在这个过程中非常重要的几个对象,Servlet是必不可少的,Request,Response,另外一个就是我们的容器本身,我们按照我们的思路,就是通过Netty对外暴露一个端口,同时在启动的时候初始化映射关系,在有请求进来的时候调用对应的Servlet进行业务处理,最后进行响应。

      主类:

    //Netty就是一个同时支持多协议的网络通信框架
    public class WuzzTomcat {
        //打开Tomcat源码,全局搜索ServerSocket
    
        private int port = 8080;
    
        private Map<String, WuzzServlet> servletMapping = new HashMap<String, WuzzServlet>();
    
        private Properties webxml = new Properties();
    
        private void init() {
            //加载web.xml文件,同时初始化 ServletMapping对象
            try {
                String WEB_INF = this.getClass().getResource("/").getPath();
                FileInputStream fis = new FileInputStream(WEB_INF + "web.properties");
           //加载配置文件 webxml.load(fis);
    for (Object k : webxml.keySet()) { String key = k.toString(); if (key.endsWith(".url")) { String servletName = key.replaceAll("\.url$", ""); String url = webxml.getProperty(key); String className = webxml.getProperty(servletName + ".className"); WuzzServlet obj = (WuzzServlet) Class.forName(className).newInstance(); servletMapping.put(url, obj); } } } catch (Exception e) { e.printStackTrace(); } } public void start() { init(); //Netty封装了NIO,Reactor模型,Boss,worker // Boss线程 EventLoopGroup bossGroup = new NioEventLoopGroup(); // Worker线程 EventLoopGroup workerGroup = new NioEventLoopGroup(); try { // Netty服务 //ServetBootstrap ServerSocketChannel ServerBootstrap server = new ServerBootstrap(); // 链路式编程 server.group(bossGroup, workerGroup) // 主线程处理类,看到这样的写法,底层就是用反射 .channel(NioServerSocketChannel.class) // 子线程处理类 , Handler .childHandler(new ChannelInitializer<SocketChannel>() { // 客户端初始化处理 protected void initChannel(SocketChannel client) throws Exception { // 无锁化串行编程 //Netty对HTTP协议的封装,顺序有要求 // HttpResponseEncoder 编码器 client.pipeline().addLast(new HttpResponseEncoder()); // HttpRequestDecoder 解码器 client.pipeline().addLast(new HttpRequestDecoder()); // 业务逻辑处理 client.pipeline().addLast(new WuzzTomcatHandler()); } }) // 针对主线程的配置 分配线程最大数量 128 .option(ChannelOption.SO_BACKLOG, 128) // 针对子线程的配置 保持长连接 .childOption(ChannelOption.SO_KEEPALIVE, true); // 启动服务器 ChannelFuture f = server.bind(port).sync(); System.out.println("Wuzz Tomcat 已启动,监听的端口是:" + port); f.channel().closeFuture().sync(); } catch (Exception e) { e.printStackTrace(); } finally { // 关闭线程池 bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } }public static void main(String[] args) { new WuzzTomcat().start(); } }

       配置文件:web.properties主要是模仿web工程中webxml中对Servlet的映射关系的配置:

    servlet.one.url=/firstServlet.do
    servlet.one.className=com.wuzz.demo.netty.tomcat.servlet.FirstServlet
    
    servlet.two.url=/secondServlet.do
    servlet.two.className=com.wuzz.demo.netty.tomcat.servlet.SecondServlet

      所以我们这里需要定义自己的Servlet,这里主要定义自己一个抽象的Servlet类,采用模板方法的模式来编写代码。:

    public abstract class WuzzServlet {
      public void service(WuzzRequest request, WuzzResponse response) throws Exception{
            
            //由service方法来决定,是调用doGet或者调用doPost
            if("GET".equalsIgnoreCase(request.getMethod())){
                doGet(request, response);
            }else{
                doPost(request, response);
            }
        }
        
        public abstract void doGet(WuzzRequest request, WuzzResponse response) throws Exception;
        
        public abstract void doPost(WuzzRequest request, WuzzResponse response) throws Exception;
    }
    public class FirstServlet extends WuzzServlet { @Override public void doGet(WuzzRequest request, WuzzResponse response) throws Exception { this.doPost(request, response); } @Override public void doPost(WuzzRequest request, WuzzResponse response) throws Exception { response.write("This is First Serlvet"); } }
    public class SecondServlet extends WuzzServlet { @Override public void doGet(WuzzRequest request, WuzzResponse response) throws Exception { this.doPost(request, response); } @Override public void doPost(WuzzRequest request, WuzzResponse response) throws Exception { response.write("This is Second Serlvet"); } }

      到目前为止,从初始化工作到接受请求的流程已经都可以了,那么现在就是处理这个请求的过程,那么这里需要定义Request ,Response.在Netty中进行响应的类是需要继承 ChannelInboundHandlerAdapter 或 SimpleChannelInboundHandler 类,我们采用前者,那么我们就可以定义出这样的两个类:

      Request:

    public class WuzzRequest {
    
        private ChannelHandlerContext ctx;
    
        private HttpRequest req;
    
        public WuzzRequest(ChannelHandlerContext ctx, HttpRequest req) {
            this.ctx = ctx;
            this.req = req;
        }
    
        public String getUrl() {
            return req.uri();
        }
    
        public String getMethod() {
            return req.method().name();
        }
    }

      Response:

    public class WuzzResponse {
        //SocketChannel的封装
        private ChannelHandlerContext ctx;
    
        private HttpRequest req;
    
        public WuzzResponse(ChannelHandlerContext ctx, HttpRequest req) {
            this.ctx = ctx;
            this.req = req;
        }
    
        public void write(String out) throws Exception {
            try {
                if (out == null || out.length() == 0) {
                    return;
                }
                // 设置 http协议及请求头信息
                FullHttpResponse response = new DefaultFullHttpResponse(
                    // 设置http版本为1.1
                    HttpVersion.HTTP_1_1,
                    // 设置响应状态码
                    HttpResponseStatus.OK,
                    // 将输出值写出 编码为UTF-8
                    Unpooled.wrappedBuffer(out.getBytes("UTF-8")));
    
                response.headers().set("Content-Type", "text/html;");
                // 当前是否支持长连接
    //            if (HttpUtil.isKeepAlive(r)) {
    //                // 设置连接内容为长连接
    //                response.headers().set(CONNECTION, HttpHeaderValues.KEEP_ALIVE);
    //            }
                ctx.write(response);
            } finally {
                ctx.flush();
                ctx.close();
            }
        }
    }

      最后我们需要定义自己的业务处理类,这里为了方便,我们直接在主类中新建一个内部类来处理:

     //业务处理handler
        public class WuzzTomcatHandler extends ChannelInboundHandlerAdapter {
            @Override
            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                if (msg instanceof HttpRequest) {
                    HttpRequest req = (HttpRequest) msg;
                    // 转交给我们自己的request实现
                    WuzzRequest request = new WuzzRequest(ctx, req);
                    // 转交给我们自己的response实现
                    WuzzResponse response = new WuzzResponse(ctx, req);
                    // 实际业务处理
                    String url = request.getUrl();
    
                    if (servletMapping.containsKey(url)) {
                        servletMapping.get(url).service(request, response);
                    } else {
                        response.write("404 - Not Found");
                    }
                }
            }
         //异常处理
            @Override
            public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
    
            }
        }

      这样子就完成了我们整个容器的编写,启动容器,通过 http://localhost:8080/firstServlet.do 访问可以看到拿到响应:

  • 相关阅读:
    Zookeeper搭建集群及协同
    无限可能,Elasticsearch(一)
    如何打造一款m3u8视频爬虫
    垃圾如何进行自救
    必知必会的JavaJDK工具
    如何在局域网内开一家电影院
    谈谈网络游戏中的延迟解决方案
    简述一致性哈希算法
    Python网络爬虫实战(五)批量下载B站收藏夹视频
    Python网络爬虫实战(四)模拟登录
  • 原文地址:https://www.cnblogs.com/wuzhenzhao/p/10418650.html
Copyright © 2011-2022 走看看