给大家讲讲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("\n文件内容:")
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

结束

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

点赞是认可,在看是支持

posted on 2021-08-02 17:51  coding途中  阅读(1285)  评论(0编辑  收藏  举报

导航