Netty入门教程——认识Netty

作者: 追那个小女孩 | 来源:发表于2017-07-16 15:20 被阅读46237次
    Netty

    什么是Netty?

    Netty 是一个利用 Java 的高级网络的能力,隐藏其背后的复杂性而提供一个易于使用的 API 的客户端/服务器框架。
    Netty 是一个广泛使用的 Java 网络编程框架(Netty 在 2011 年获得了Duke's Choice Award,见https://www.java.net/dukeschoice/2011)。它活跃和成长于用户社区,像大型公司 Facebook 和 Instagram 以及流行 开源项目如 Infinispan, HornetQ, Vert.x, Apache Cassandra 和 Elasticsearch 等,都利用其强大的对于网络抽象的核心代码。

    以上是摘自《Essential Netty In Action》这本书,本文的内容也是本人读了这本书之后的一些整理心得,如有不当之处欢迎大虾们指正

    Netty和Tomcat有什么区别?

    Netty和Tomcat最大的区别就在于通信协议,Tomcat是基于Http协议的,他的实质是一个基于http协议的web容器,但是Netty不一样,他能通过编程自定义各种协议,因为netty能够通过codec自己来编码/解码字节流,完成类似redis访问的功能,这就是netty和tomcat最大的不同。

    有人说netty的性能就一定比tomcat性能高,其实不然,tomcat从6.x开始就支持了nio模式,并且后续还有arp模式——一种通过jni调用apache网络库的模式,相比于旧的bio模式,并发性能得到了很大提高,特别是arp模式,而netty是否比tomcat性能更高,则要取决于netty程序作者的技术实力了。

    为什么Netty受欢迎?

    如第一部分所述,netty是一款收到大公司青睐的框架,在我看来,netty能够受到青睐的原因有三:

    1. 并发高
    2. 传输快
    3. 封装好

    Netty为什么并发高

    Netty是一款基于NIO(Nonblocking I/O,非阻塞IO)开发的网络通信框架,对比于BIO(Blocking I/O,阻塞IO),他的并发性能得到了很大提高,两张图让你了解BIO和NIO的区别:

    阻塞IO的通信方式
    非阻塞IO的通信方式
    从这两图可以看出,NIO的单线程能处理连接的数量比BIO要高出很多,而为什么单线程能处理更多的连接呢?原因就是图二中出现的Selector
    当一个连接建立之后,他有两个步骤要做,第一步是接收完客户端发过来的全部数据,第二步是服务端处理完请求业务之后返回response给客户端。NIO和BIO的区别主要是在第一步。
    在BIO中,等待客户端发数据这个过程是阻塞的,这样就造成了一个线程只能处理一个请求的情况,而机器能支持的最大线程数是有限的,这就是为什么BIO不能支持高并发的原因。
    而NIO中,当一个Socket建立好之后,Thread并不会阻塞去接受这个Socket,而是将这个请求交给Selector,Selector会不断的去遍历所有的Socket,一旦有一个Socket建立完成,他会通知Thread,然后Thread处理完数据再返回给客户端——这个过程是阻塞的,这样就能让一个Thread处理更多的请求了。
    下面两张图是基于BIO的处理流程和netty的处理流程,辅助你理解两种方式的差别:
    BIO的处理流程
    NIO的处理流程

    除了BIO和NIO之外,还有一些其他的IO模型,下面这张图就表示了五种IO模型的处理流程:


    五种常见的IO模型
    • BIO,同步阻塞IO,阻塞整个步骤,如果连接少,他的延迟是最低的,因为一个线程只处理一个连接,适用于少连接且延迟低的场景,比如说数据库连接。
    • NIO,同步非阻塞IO,阻塞业务处理但不阻塞数据接收,适用于高并发且处理简单的场景,比如聊天软件。
    • 多路复用IO,他的两个步骤处理是分开的,也就是说,一个连接可能他的数据接收是线程a完成的,数据处理是线程b完成的,他比BIO能处理更多请求,但是比不上NIO,但是他的处理性能又比BIO更差,因为一个连接他需要两次system call,而BIO只需要一次,所以这种IO模型应用的不多。
    • 信号驱动IO,这种IO模型主要用在嵌入式开发,不参与讨论。
    • 异步IO,他的数据请求和数据处理都是异步的,数据请求一次返回一次,适用于长连接的业务场景。

    以上摘自Linux IO模式及 select、poll、epoll详解

    Netty为什么传输快

    Netty的传输快其实也是依赖了NIO的一个特性——零拷贝。我们知道,Java的内存有堆内存、栈内存和字符串常量池等等,其中堆内存是占用内存空间最大的一块,也是Java对象存放的地方,一般我们的数据如果需要从IO读取到堆内存,中间需要经过Socket缓冲区,也就是说一个数据会被拷贝两次才能到达他的的终点,如果数据量大,就会造成不必要的资源浪费。
    Netty针对这种情况,使用了NIO中的另一大特性——零拷贝,当他需要接收数据的时候,他会在堆内存之外开辟一块内存,数据就直接从IO读到了那块内存中去,在netty里面通过ByteBuf可以直接对这些数据进行直接操作,从而加快了传输速度。
    下两图就介绍了两种拷贝方式的区别,摘自Linux 中的零拷贝技术,第 1 部分

    传统数据拷贝 零拷贝

    上文介绍的ByteBuf是Netty的一个重要概念,他是netty数据处理的容器,也是Netty封装好的一个重要体现,将在下一部分做详细介绍。

    为什么说Netty封装好?

    要说Netty为什么封装好,这种用文字是说不清的,直接上代码:

    • 阻塞I/O
    public class PlainOioServer {
    
        public void serve(int port) throws IOException {
            final ServerSocket socket = new ServerSocket(port);     //1
            try {
                for (;;) {
                    final Socket clientSocket = socket.accept();    //2
                    System.out.println("Accepted connection from " + clientSocket);
    
                    new Thread(new Runnable() {                        //3
                        @Override
                        public void run() {
                            OutputStream out;
                            try {
                                out = clientSocket.getOutputStream();
                                out.write("Hi!\r\n".getBytes(Charset.forName("UTF-8")));                            //4
                                out.flush();
                                clientSocket.close();                //5
    
                            } catch (IOException e) {
                                e.printStackTrace();
                                try {
                                    clientSocket.close();
                                } catch (IOException ex) {
                                    // ignore on close
                                }
                            }
                        }
                    }).start();                                        //6
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    
    • 非阻塞IO
    public class PlainNioServer {
        public void serve(int port) throws IOException {
            ServerSocketChannel serverChannel = ServerSocketChannel.open();
            serverChannel.configureBlocking(false);
            ServerSocket ss = serverChannel.socket();
            InetSocketAddress address = new InetSocketAddress(port);
            ss.bind(address);                                            //1
            Selector selector = Selector.open();                        //2
            serverChannel.register(selector, SelectionKey.OP_ACCEPT);    //3
            final ByteBuffer msg = ByteBuffer.wrap("Hi!\r\n".getBytes());
            for (;;) {
                try {
                    selector.select();                                    //4
                } catch (IOException ex) {
                    ex.printStackTrace();
                    // handle exception
                    break;
                }
                Set<SelectionKey> readyKeys = selector.selectedKeys();    //5
                Iterator<SelectionKey> iterator = readyKeys.iterator();
                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    iterator.remove();
                    try {
                        if (key.isAcceptable()) {                //6
                            ServerSocketChannel server =
                                    (ServerSocketChannel)key.channel();
                            SocketChannel client = server.accept();
                            client.configureBlocking(false);
                            client.register(selector, SelectionKey.OP_WRITE |
                                    SelectionKey.OP_READ, msg.duplicate());    //7
                            System.out.println(
                                    "Accepted connection from " + client);
                        }
                        if (key.isWritable()) {                //8
                            SocketChannel client =
                                    (SocketChannel)key.channel();
                            ByteBuffer buffer =
                                    (ByteBuffer)key.attachment();
                            while (buffer.hasRemaining()) {
                                if (client.write(buffer) == 0) {        //9
                                    break;
                                }
                            }
                            client.close();                    //10
                        }
                    } catch (IOException ex) {
                        key.cancel();
                        try {
                            key.channel().close();
                        } catch (IOException cex) {
                            // 在关闭时忽略
                        }
                    }
                }
            }
        }
    }
    
    • Netty
    public class NettyOioServer {
    
        public void server(int port) throws Exception {
            final ByteBuf buf = Unpooled.unreleasableBuffer(
                    Unpooled.copiedBuffer("Hi!\r\n", Charset.forName("UTF-8")));
            EventLoopGroup group = new OioEventLoopGroup();
            try {
                ServerBootstrap b = new ServerBootstrap();        //1
    
                b.group(group)                                    //2
                 .channel(OioServerSocketChannel.class)
                 .localAddress(new InetSocketAddress(port))
                 .childHandler(new ChannelInitializer<SocketChannel>() {//3
                     @Override
                     public void initChannel(SocketChannel ch) 
                         throws Exception {
                         ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {            //4
                             @Override
                             public void channelActive(ChannelHandlerContext ctx) throws Exception {
                                 ctx.writeAndFlush(buf.duplicate()).addListener(ChannelFutureListener.CLOSE);//5
                             }
                         });
                     }
                 });
                ChannelFuture f = b.bind().sync();  //6
                f.channel().closeFuture().sync();
            } finally {
                group.shutdownGracefully().sync();        //7
            }
        }
    }
    

    从代码量上来看,Netty就已经秒杀传统Socket编程了,但是这一部分博大精深,仅仅贴几个代码岂能说明问题,在这里给大家介绍一下Netty的一些重要概念,让大家更理解Netty。

    • Channel
      数据传输流,与channel相关的概念有以下四个,上一张图让你了解netty里面的Channel。


      Channel一览
      • Channel,表示一个连接,可以理解为每一个请求,就是一个Channel。
      • ChannelHandler,核心处理业务就在这里,用于处理业务请求。
      • ChannelHandlerContext,用于传输业务数据。
      • ChannelPipeline,用于保存处理过程需要用到的ChannelHandler和ChannelHandlerContext。
    • ByteBuf
      ByteBuf是一个存储字节的容器,最大特点就是使用方便,它既有自己的读索引和写索引,方便你对整段字节缓存进行读写,也支持get/set,方便你对其中每一个字节进行读写,他的数据结构如下图所示:
    ByteBuf数据结构

    他有三种使用模式:

    1. Heap Buffer 堆缓冲区
      堆缓冲区是ByteBuf最常用的模式,他将数据存储在堆空间。
    2. Direct Buffer 直接缓冲区
      直接缓冲区是ByteBuf的另外一种常用模式,他的内存分配都不发生在堆,jdk1.4引入的nio的ByteBuffer类允许jvm通过本地方法调用分配内存,这样做有两个好处
      • 通过免去中间交换的内存拷贝, 提升IO处理速度; 直接缓冲区的内容可以驻留在垃圾回收扫描的堆区以外。
      • DirectBuffer 在 -XX:MaxDirectMemorySize=xxM大小限制下, 使用 Heap 之外的内存, GC对此”无能为力”,也就意味着规避了在高负载下频繁的GC过程对应用线程的中断影响.
    3. Composite Buffer 复合缓冲区
      复合缓冲区相当于多个不同ByteBuf的视图,这是netty提供的,jdk不提供这样的功能。

    除此之外,他还提供一大堆api方便你使用,在这里我就不一一列出了,具体参见ByteBuf字节缓存

    • Codec
      Netty中的编码/解码器,通过他你能完成字节与pojo、pojo与pojo的相互转换,从而达到自定义协议的目的。
      在Netty里面最有名的就是HttpRequestDecoder和HttpResponseEncoder了。

    Netty入门教程2——动手搭建HttpServer
    Netty入门教程3——Decoder和Encoder
    Netty入门教程4——如何实现长连接

    以上就是我对《Netty实战》这本书的一些心得和书外的一些相关知识整理,如果有不同的见解,欢迎讨论!

    相关文章

      网友评论

      • 土豆爱上羊肉:写的不错,有用
      • 每个足迹:代码上面能给一些注解吗 :joy:
      • 山东师范大学08中文李倩:你好楼主,我有一个问题要麻烦问你一下,就是客户端突然断电,服务端此时并不会触发inActive方法,这个有好的解决办法吗?
        干杯_0d9f:不依靠netty本身的断开连接,自己做轮询主动断开
        山东师范大学08中文李倩:@追那个小女孩 谢谢你的解答,好像真没有比较简单的解决方案!
        追那个小女孩:按照我的理解,断电跟断网甚至弱网的情况是一样的,服务端是不知道客户端发生什么的,这种情况只能等待超时,如果一定要优化的话,也就只能减少超时时间,但是这样子做也是有代价的,自己权衡
      • RealDog4j:Tomcat 三中Connector模式
        BIO
        NIO
        APR
        不是ARP....
        追那个小女孩:多谢指正
      • 寒食君:nio 是new io的意思吗
        50db5d07171a:一开始叫 new input/output,后来一般称non-blocking IO
        卑微幻想家:同步非阻塞IO
      • 84649e637cd2:netty用的就是五中IO中的多路复用 也叫事件驱动
        草明_957c:对的,java中的nio对应linux中的multiplexing io
      • 2e62580f527f:您好 HttpHeaderValues 类和 HttpHeaderNames 里面定义的都是什么 麻烦帖出来下哈
        facetoface_bd06:作者,您好,感谢你贡献这篇文章,我对netty也有些更深入的了解了,但是我在使用的过程中,netty经常会抛出connection reset by peer的错误,你知道是什么原因么?知道怎么去重现这个问题么?
        追那个小女孩:这两个是HttpHeader的内部常量类,主要是记录header常用的key-value组合的,而HttpHeader是netty官方sdk自带的,想要深入了解的话可以看netty的api doc:
        https://netty.io/4.1/api/index.html
      • 532c79b692db:《Essential Netty In Action》 这个链接怎么打不开啊

      本文标题:Netty入门教程——认识Netty

      本文链接:https://www.haomeiwen.com/subject/nhbtkxtx.html