• 给大家讲讲netty如何使用吧!


    今天给大家讲讲家喻户晓的netty!

    netty的诞生

    Netty的创始人是韩国人trustin lee,他现在韩国line公司工作。netty 目前的项目leader 是德国人Norman maurer,也是的作者,他目前是苹果公司高级工程师,同时也经常参加netty相关的技术会议。至于他俩为啥不全职搞Netty,题主可以到他俩的Twitter 下留言问他们,运气好应该会回答你的。他俩的Twitter地址: https://twitter.com/normanmaurerhttps://twitter.com/trustin 补充一点 ,Norman maurer 之前在redhat工作时,是全职开发Netty的,那时候netty项目属于redhat旗下,相当于redhat付工资让Norman全职开发Netty ,这比题主说的那种全职是不是更爽一点?)

    java另一款网络编程框架mina跟netty都源自韩国这位大佬之手,膜拜!!!!!!!

    两位大神

    什么是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 等,都利用其强大的对于网络抽象的核心代码。Netty 是一个基于 JAVA NIO(Nonblocking I/O,非阻塞同步IO)类库的异步通信框架,它的架构特点是:异步非阻塞、基于事件驱动、高性能、高可靠性和高可定制性。对比于BIO(Blocking I/O,阻塞同步IO),他的并发性能得到了很大提高,两张图让你了解BIO和NIO的区别:

    BIO网络模型

    对于同一个socket采用单线程出处理,处理整个读写以及

    阻塞同步IO

    NIO网络模型

    非阻塞同步IO

    从这两图可以看出,NIO的单线程能处理连接的数量比BIO要高出很多,而为什么单线程能处理更多的连接呢?原因就是图二中出现的Selector。
    当一个连接建立之后,他有两个步骤要做,第一步是接收完客户端发过来的全部数据,第二步是服务端处理完请求业务之后返回response给客户端。NIO和BIO的区别主要是在第一步。在BIO中,等待客户端发数据这个过程是阻塞的,这样就造成了一个线程只能处理一个请求的情况,而机器能支持的最大线程数是有限的,这就是为什么BIO不能支持高并发的原因。而NIO中,当一个Socket建立好之后,Thread并不会阻塞去接受这个Socket,而是将这个请求交给Selector,Selector会不断的去遍历所有的Socket,一旦有一个Socket建立完成,他会通知Thread,然后Thread处理完数据再返回给客户端——这个过程是不阻塞的,这样就能让一个Thread处理更多的请求了。然而Bio通常使用线程池的方法去接入更多的链接,工作效率相对较低。

    Netty为什么高性能

    零拷贝是网络编程的关键,很多性能优化都离不开零拷贝。在 Java 程序中,常用的零拷贝有 mmap(内存映射) 和 sendFile。
     Java 中传统的 IO 和网络编程:
    传统意义NIO的文件写入socket拷贝

    java中应用:

    File file = new File("test.txt"); 
    RandomAccessFile raf = new RandomAccessFile(file, "rw"); 
    byte[] arr = new byte[(int) file.length()]; 
    raf.read(arr); 
    Socket socket = new ServerSocket(8080).accept(); 
    socket.getOutputStream().write(arr);
      
    

    内存拷贝过程

    mmap 通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据。这样,在进行网络传输时,就可以减少内核空间到用户空间的拷贝次数。如下图:

    java中应用:

    // 从标准输入获取数据
    Scanner sc = new Scanner(System.in);
    System.out.println("请输入:");
    String str = sc.nextLine();
    byte[] bytes = str.getBytes();
    
    RandomAccessFile raf = new RandomAccessFile("map.txt", "rw");
    FileChannel channel = raf.getChannel();
    
    // 获取内存映射缓冲区,并向缓冲区写入数据
    MappedByteBuffer mappedBuffer = channel.map(MapMode.READ_WRITE, 0, bytes.length);
    mappedBuffer.put(bytes);
    
    raf.close();
    raf.close();
    
    // 再次打开刚刚的文件,读取其中的内容
    raf = new RandomAccessFile("map.txt", "rw");
    channel = raf.getChannel();
    System.out.println("
    文件内容:")
    System.out.println(readChannel(channel));
    
    raf.close();
    raf.close();
    

    mmap内存拷贝过程

    Linux 2.1 版本提供了 sendFile 函数,其基本原理是:数据根本不经过用户态,直接从内核缓冲区进入到 Socket Buffer,由于和用户态完全无关,就减少了一次上下文切换。

    sendFile内存拷贝过程

    在java中FileChannel的transferTo方法使用的就是sendFile函数,需要操作系统的支持

    java中应用:
    
    public class ZeroCopyClient {
        public static void main(String[] args) throws Exception {
            SocketChannel socketChannel = SocketChannel.open();
            socketChannel.connect(new InetSocketAddress("localhost", 7001));
            String filename = "E:\a.pdf";
            FileChannel fileChannel = new FileInputStream(filename).getChannel();
            long startTime = System.currentTimeMillis();
            // 在 linux 下一个 transferTo 方法就可以完成传输,但 windows 下调用一次 transferTo 只能发送 8m,就需要分段传输而且要注意传输的位置
            long transferCount = fileChannel.transferTo(0, fileChannel.size(), socketChannel);
            System.out.println("发送的总字节数:" + transferCount + ",耗时:" + (System.currentTimeMillis() - startTime));
            fileChannel.close();
            Thread.sleep(1000);
        }
    }
    

    Netty针对这种情况,使用了nio的零拷贝机制,而在Netty中还有另一种形式的零拷贝,即Netty允许我们将多段数据合并为一整段虚拟数据供用户使用,而过程中不需要对数据进行拷贝操作,这也是我们今天要讲的重点关于ByteBuffer,关于ByteBuffer,Netty提供了两个接口:

    1. ByteBuf
    2. ByteBufHolder

    对于ByteBuf,Netty提供了多种实现:

    1. Heap ByteBuf:直接在堆内存分配
    2. Direct ByteBuf:直接在内存区域分配而不是堆内存
    3. CompositeByteBuf:组合Buffer

    Netty核心概念讲解

    首先看下如何启动一个netty服务

       NioEventLoopGroup boss = new NioEventLoopGroup(1);
            NioEventLoopGroup work = new NioEventLoopGroup(2);
            try {
                ServerBootstrap serverBootstrap = new ServerBootstrap();
                serverBootstrap.group(boss, work).channel(Epoll.isAvailable() ? EpollServerSocketChannel.class : NioServerSocketChannel.class)
                        .localAddress(new InetSocketAddress(port))
                        .handler(new ServerFatherHandler())
                        .childHandler(new NettyServerHandlerInitializer())
                        .option(ChannelOption.SO_BACKLOG, 1024) //用于临时存放已完成三次握手的请求的队列的最大长度。如果未设置或所设置的值小于1,Java将使用默认值50。
                        .option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
                        .childOption(ChannelOption.TCP_NODELAY, true) //禁用nagle算法
                        .childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
                        .childOption(ChannelOption.SO_KEEPALIVE, true) //连接会测试链接的状态,这个选项用于可能长时间没有数据交流的连接。当设置该选项以后,如果在两小时内没有数据的通信时,TCP会自动发送一个活动探测数据报文。
                        .childOption(ChannelOption.SO_RCVBUF, 1024 * 100)
                        .childOption(ChannelOption.SO_SNDBUF, 1024 * 100);//设置链接的channel的option
                if (Epoll.isAvailable()) {
                    serverBootstrap.option(EpollChannelOption.EPOLL_MODE, EpollMode.EDGE_TRIGGERED)
                            .option(EpollChannelOption.TCP_QUICKACK, Boolean.TRUE);
                }
                ChannelFuture future = serverBootstrap.bind().sync();
                logger.info("nettyServer服务端启动成功=====");
                future.channel().closeFuture().sync();
    

    EventLoop详解

    由下图所示,NioEventLop是EventLoop的一个具体实现,EventLoop是EventLoopGroup的一个属性,NioEventLoopGroup是EventLoopGroup的具体实现,都是基于ExecutorService进行的线程池管理,因此EventLoop、EventLoopGroup组件的核心作用就是进行Selector的维护以及线程池的维护。一个EventLoop绑定一个Selector,同时管理多个SelectKeys,


    由上图可知,一个NioEventLop可以理解成一个线程,他维护的一个Selector,同时会去管理多个Channel,因此当连接建立后,Channel对应的EventLoop已经确定!

    UML类图

    ChannelHandler详解

    从应用程序开发人员的角度来看,Netty的主要组件是ChannelHandler,让用户自定义添加ChannelHandler去处理读写事件,整个设计是基于责任链模式去实现。

    ChannelHandler类型

    ChannelHandler主要分为两大类,ChannelInboundHandler(入)和ChannelOutboundHandler(出),netty中使用ChannelPipeline去保存ChannelHandler双向链表的维护,链表在初始化的时候会生成一个Tail跟Header节点,当一个channel触发写事件时候是从Tail->Head触发,匹配当前handler是ChannelOutboundHandler类型,相反写事件则是Head->Tail匹配当前handler类型为ChannelInboundHandler。

    ByteBuf设计

    网络数据的基本单位总是字节,java NIO提供ByteBuffer作为字节的容器,但是ByteBuffer使用起来过于复杂和繁琐。
    ByteBuf是netty的Server与Client之间通信的数据传输载体(Netty的数据容器),它提供了一个byte数组(byte[])的抽象视图,既解决了JDK API的局限性,又为网络应用程序的开发者提供了更好的API。

    ByteBuffer缺点

    ByteBuffer长度固定,一旦分配完成,它的容量不能动态扩展和收缩,当需要编码的POJO对象大于ByteBuffer的容量时,会发生索引越界异常;
    ByteBuffer只有一个标识位置的指针position,读写的时候需要手工调用flip()和rewind()等,使用者必须小心谨慎地处理这些API,否则很容易导致程序处理失败;

    ByteBuffer的API功能有限,一些高级和实用的特性它不支持,需要使用者自己编程实现。

    ByteBuf优点

    • 容量可以按需增长
    • 读写模式切换不需要调用flip()
    • 读写使用了不同的索引
    • 支持方法的链式调用
    • 支持引用计数
    • 支持池化
    • 可以被用户自定义的缓冲区类型扩展
    • 通过内置的复合缓冲区类型实现透明的零拷贝

    使用用例

    使用Unpooled工具类来创建非池化的ByteBuf

      @Test
        public void testHeapByteBuf() {
            ByteBuf heapBuf = Unpooled.buffer(10);
            if (heapBuf.hasArray()) {
                byte[] array = heapBuf.array();
                int offset = heapBuf.arrayOffset() + heapBuf.readerIndex();
                int length = heapBuf.readableBytes();
                //0,0        
                logger.info("offset:{},length:{}", offset, length);
            }
        }
      
       @Test
        public void testGetSet(){
            ByteBuf byteBuf = Unpooled.copiedBuffer("jannal", Charset.forName("utf-8"));
            System.out.println((char)byteBuf.getByte(0));
            int readIndex = byteBuf.readerIndex();
            int writerIndex= byteBuf.writerIndex();
            byteBuf.setByte(0,(byte)'J');
            System.out.println((char)byteBuf.getByte(0));
            assert readIndex== byteBuf.readerIndex();
            assert writerIndex== byteBuf.writerIndex();
        }
        
        @Test
        public void testReadWrite(){
            ByteBuf byteBuf = Unpooled.copiedBuffer("jannal", Charset.forName("utf-8"));
            System.out.println((char)byteBuf.readByte());
            int readIndex = byteBuf.readerIndex();
            int writerIndex= byteBuf.writerIndex();
            byteBuf.writeByte((byte)'J');
            assert readIndex== byteBuf.readerIndex();
            assert writerIndex!= byteBuf.writerIndex();
        }
    
    

    Channel详解

    Channel是由netty抽象出来的网络I/O操作的接口,作为Netty传输的核心,负责处理所有的I/O操作。Channel提供了一组用于传输的API,主要包括网络的读/写,客户端主动发起连接、关闭连接,服务端绑定端口,获取通讯双方的网络地址等;同时,还提供了与netty框架相关的操作,如获取channel相关联的EventLoop、pipeline等。

    一个Channel可以拥有父Channel,服务端Channel的parent为空,对客户端Channel来说,它的parent就是创建它的ServerSocketChannel。当一个Channel相关的网络操作完成后,请务必调用ChannelOutboundInvoker.close()或ChannelOutboundInvoker.close(ChannelPromise)来释放所有资源,如文件句柄。

    每个Channel都会被分配一个ChannelPipeline和ChannelConfig。ChannelConfig主要负责Channel的所有配置,并且支持热更新。此外,每个Channel都会绑定一个EventLoop,该通道整个生命周期内的事件都将由这个特定EventLoop负责处理。

    Channel是独一无二的,所以为了保证顺序将Channel声明为Comparable的一个子接口。如果两个Channel实例返回了相同的hashcode,那么AbstractChannel中compareTo()方法的实现将会抛出一个Error。

    在netty中,所有的I/O操作都是异步的,这些操作被调用将立即返回一个ChannelFuture实例,用户通过ChannelFuture实例获取操作的结果。下面主要介绍下Channel中的传输API及其作用。

    Channel的Api

    请求将当前的msg通过ChannelPipeline写入到目标Channel中。注意,write操作只是将消息存入到消息发送环形数组中,并没有被真正发送,只有调用flush操作才会将消息写入到Channel,发送给对方。区别在于方法2提供了ChannelPromise实例,用于设置写入操作的结果。该操作会触发outbound事件,该事件会级联触发ChannelPipeline中ChannelHandler.write(ChannelHandlerContext,msg,ChannelPromise)方法被调用。

    ChannelFuture write(Object msg);
    ChannelFuture write(Object msg, ChannelPromise promise);
    

    将write操作写入环形数组中的消息全部写入到Channel中,发送给通信对方。该操作会触发outbound事件,该事件会级联触发ChannelPipeline中ChannelHandler.flush(ChannelHandlerContext)方法被调用。

    Channel flush();
    

    write操作和flush操作的组合。

    ChannelFuture writeAndFlush(Object msg);
    ChannelFuture writeAndFlush(Object msg, ChannelPromise promise);
    

    主动关闭当前连接,close操作会触发链路关闭事件,该事件会级联触发ChannelPipeline中ChannelOutboundHandler.close(ChannelHandlerContext,ChannelPromise)方法被调用;区别在于方法2提供了ChannelPromise实例,用于设置close操作的结果,无论成功与否。

    ChannelFuture close();
    ChannelFuture close(ChannelPromise promise);
    

    用于绑定指定的本地Socket地址localAddress,触发outbound事件,该事件会级联触发ChannelPipeline中ChannelHandler.bind(ChannelHandlerContext,SocketAddress,ChannelPromise)方法被调用。

    ChannelFuture connect(SocketAddress remoteAddress)
    ChannelFuture connect(SocketAddress remoteAddress,ChannelPromise promise);
    

    其他api请参考 https://netty.io/4.1/api/io/netty/channel/Channel.html

    结束

    识别下方二维码!回复: 入群 ,扫码加入我们交流群!

    点赞是认可,在看是支持

    欢迎关注我的公众号!里面可以加入微信技术交流群!
  • 相关阅读:
    类和对象代码练习1
    搬迁MSSQL出现的问题
    QQ群空间是我见过最烂的系统
    导出表数据脚本的存储过程 [转自CSDN]
    plesk for Linux 没有默认首页的优先级别设置
    传说中的otag开源是假的。BS!!!!!
    FTP关闭mput上传确认
    刚装的vs2008不能生成silverlight,郁闷中
    怎么查域名反向解析是否成功
    现在的黑客太有才了。。。/不喜欢开防火墙的要小心
  • 原文地址:https://www.cnblogs.com/1ssqq1lxr/p/15090992.html
Copyright © 2020-2023  润新知