Netty学习(二)----JAVA NIO 以及NIO简易广播群聊

JAVA NIO - Buffer

在之前提到JAVA NIO中,引入了Buffer、Channel 、Selectors.

  • Buffer:
    • 定义:
      Java NIO Buffers用于和NIO Channel交互。 我们从Channel中读取数据到buffers里,从Buffer把数据写入到Channels. Buffer本质上就是一块内存区,可以用来写入数据,并在稍后读取出来。 这块内存被NIO Buffer包裹起来,对外提供一系列的读写方便开发的接口。
    • 属性:
 // Invariants: mark <= position <= limit <= capacity
    private int mark = -1;//标志位
    private int position = 0;//当前游标(指针)位置
    private int limit;//最大容量
    private int capacity;//当前容量
  • 交互步骤:
    1.申明缓存区大小(直接缓冲区(allocateDirect)或非直接缓冲区(allocate))1
    2.把数据写入buffer;
    3.调用flip;
    4.从Buffer中读取数据;
    5.调用buffer.clear()或者buffer.compact()。

理解:

Buffer缓冲区实质上就是一块内存,用于写入数据,也供后续再次读取数据。
这块内存被NIO Buffer管理,并提供一系列的方法用于更简单的操作这块内存。
position和limit的具体含义取决于当前buffer的模式。capacity在两种模式下都表示容量。
(其实这里有点像切片的概念,指针、界限、最大容量)

  • 容量(Capacity)
    作为一块内存,buffer有一个固定的大小,叫做capacit(容量)。也就是最多只能写入容量值得字节,整形等数据。一旦buffer写满了就需要清空已读数据以便下次继续写入新的数据

  • 上限(Limit)
    在写模式,limit的含义是我们所能写入的最大数据量,它等同于buffer的容量。 一旦切换到读模式,limit则代表我们所能读取的最大数据量,他的值等同于写模式下position的位置。换句话说,您可以读取与写入数量相同的字节数(限制设置为写入的字节数,由位置标记)。(0位至 之前写模式将指针挪到的 索引位)

  • 位置(Position)
    当写入数据到Buffer的时候需要从一个确定的位置开始,默认初始化时这个位置position为0,一旦写入了数据比如一个字节,整形数据,那么position的值就会指向数据之后的一个单元,position最大可以到capacity-1. 当从Buffer读取数据时,也需要从一个确定的位置开始。buffer从写入模式变为读取模式时,position会归零,每次读取后,position向后移动。

buffer 中的绝大多数操作都是围绕以上三个属性进行操作的,

举例 :
  1. 标记当前指针位置/回退到指针位
	/**
     * Sets this buffer's mark at its position.
     *
     * @return  This buffer
     */
public final Buffer mark() {
    mark = position;
    return this;
}
/**
 回退都指针位置
 */
public final Buffer reset() {

      int m = mark;
      if (m < 0)
          throw new InvalidMarkException();
      position = m;
      return this;
}
  1. clear
public final Buffer clear() {
    position = 0;
    limit = capacity;
    mark = -1;
    return this;
}

应该明确认知clear只是改变有了游标位置等,并不会真实的清空了数据。
好比之前dubbo泛化调用中的destroy不会清除zk上的节点,而只是不watch,T_T。

  1. flip
public final Buffer flip() {
    limit = position;
    position = 0;
    mark = -1;
    return this;
}

数据存入:

当声明一组缓存区时,pos为0位。
每次读、写时,pos会+1滑动至下一个索引位;
Netty学习(二)----JAVA NIO 以及NIO简易广播群聊_第1张图片
Netty学习(二)----JAVA NIO 以及NIO简易广播群聊_第2张图片


JAVA NIO - Channel

在之前提到JAVA NIO中,引入了Buffer、Channel 、Selectors.

  • Channel:

    • 定义:
      Java NIO Buffers用于和NIO Channel交互,而Channel等价于之前的**DMA**,但不存在总线问题。 Channel表示IO源于目标打开的连接。Channel类似传统的‘流’ ,但Channel本身不能直接访问数据,

    • 交互步骤:
      1.Channel只能与Buffer进行交互,且本身不存储数据。
      2.按照通信目标分为FileChannel(本地,File理解为FD)、SocketChannel(tcp client)、ServerSocketChannel(tcp server)、DatagramChannel(udp)每种类型交互存在差异;

  • 通道获取方式:

    1. 可使用API直接对已有的流、socket进行get(),如本地I/O中的FileInputStream、FileOutputStream、RandomAccessFile合网络IO中的Socket、ServerSocket、DatagramSocket。
    2. 对各个通道进行open()调用。
    3. Files中的newByteChannel()
    4. 通道与缓冲区的交互支持 Scatter(分散读取) 与 Gather (Gather)1

Pipe 不做介绍,简单理解为Go里面的 <-Channel 以及 ->Channel 单向


JAVA NIO - Select(ableChannel)

在之前提到JAVA NIO中,引入了Buffer、Channel 、Selectors.

  • Channel:

    • 定义:
      Java NIO Select(ableChannel)是多路复用器。用于监控相应I/O的状态

    • 分类:

      • java.nio.channels.Channel 接口:
        • SelectableChannel
          • SocketChannel
          • ServerSocketChannel
          • DatagramChannel
          • Pipe.SinkChannel
          • Pipe.SourceChannel
  • 交互方式:

    1. 当调用register(Selector sel , int pos) 将通道组成选择器时,选择器对通道的监听事件,通过params2进行指定。
    2. 可监听到的事件类型有。
      – 1.读 SelectionKey.OP_READ(1)
      – 2.写 SelectionKey.OP_WRITE(4)
      – 3.连接 SelectionKey.OP_CONNECT(8)
      – 4.接收 SelectionKey.OP_ACCEPT(16)
    3. 通道与缓冲区的交互支持 Scatter(分散读取) 与 Gather (Gather)2
    4. 若注册 不止一个监听事件,在可以通过使用“|”操作符拼接
SelectionKey keyWithRW =serverSocketChannel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);

零拷贝(无CPU拷贝)

  • 零拷贝是网络编程的关键,很多性能都是围绕该点。
  • JAVA中,常用的零拷贝有mmap(内存映射)和sendFile。于操作系统而言,在内核缓冲区之间,没有数据是重复的(只有Kernel buffer 一份数据)。
	@Test
    public void testCopy() throws IOException {
        RandomAccessFile file = new RandomAccessFile(new File("test.txt"), "rw");
        byte[] arr = new byte[(int) file.length()];
        file.read(arr);
        Socket socket = new ServerSocket(8080).accept();
        socket.getOutputStream().write(arr);
    }

Netty学习(二)----JAVA NIO 以及NIO简易广播群聊_第3张图片
数据一共经过4次拷贝,线程经过3次切换。


mmap优化

  • mmap通过内存映射,将文件映射都内存缓冲区,同时,用户空间可以共享内核空间的数据。这样,在进行网络传输时,就可以减少内核空间到用户控件的零拷贝次。
  • Netty学习(二)----JAVA NIO 以及NIO简易广播群聊_第4张图片

sendFile 优化

  • Linux2.1 版本提供了sendFile 函数,其基本原理如下:数据根本不通过用户态,直接从内核缓冲区进入到SocketBuffer,同时,由于和用户态无关,就减少了一次上下文切换,2.4中会从kernal buffer直接拷贝到协议栈。
    Netty学习(二)----JAVA NIO 以及NIO简易广播群聊_第5张图片
    mmap与sendFile区别
    1)mmap适合小数据量读写,sendFile适合大文件传输。
    2)mmap需要四次上下文切换,3次数据拷贝;sendFile需要3次上下文切换,最少2次数据拷贝。
  1. sendFile可以利用DMA方式,减少CPU拷贝,mmap则不能(必须由内核拷贝到socket缓冲区)

理论概念整体较为杂乱,构建了相应demo,对ApI进行了尝试调用,并做了一发简易的群聊整理NIO知识,完整代码 https://github.com/LikeElephantintheforest/netty NIO-Group-chat 分支,客户端telnet实现。

简介:

  • 客户端可进行注册至服务端
  • 服务端可打印客户端上行数据
  • 服务端可将该消息准发至其他客户端

Netty学习(二)----JAVA NIO 以及NIO简易广播群聊_第6张图片
较为重要的API使用:

//1--服务端注册Selector , 监听客户端连接事件。
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//2---通过该selector监听OP事件,可得知客户端建连。
boolean haveAccept = selector.select(10000) > 0;
//3----当客户端连接建立成功服务端知晓后,可对当前socket进行可读事件监听
if (e.isAcceptable()) {
	SocketChannel socketChannel = serverSocketChannel.accept();
    socketChannel.configureBlocking(Boolean.FALSE);
    //be notified by OP_ACCEPT event,then register OP_READ on socket
    socketChannel.register(selector, SelectionKey.OP_READ);
    System.out.println(String.format("IP: %s sign up ", socketChannel.getRemoteAddress()));
}
//4-----当客户端有数据写入,服务端判定有可读时,将该消息广播至所有其他已注册的客户端。
 if (acceptedMsg) {
	for (SelectionKey select : selector.keys()) {
	                                        //except self
	   if (e != select) {
	
	       if (select.channel() instanceof SocketChannel) {
	
	           SocketChannel clientSocketChannel = (SocketChannel) select.channel();
	           clientSocketChannel.configureBlocking(Boolean.FALSE);
	
	            try {
	
	                clientSocketChannel.write(ByteBuffer.wrap(receiveMsg.getBytes()));
	
	            } catch (IOException ex) {
	//
	                ex.printStackTrace();
	
	            }
	        }
	    }
	}
}

  1. ·
    1:字节缓冲区要么是直接的,要么是非直接的。
    2:如果为直接字节缓冲区,则java虚拟机会尽最大努力直接在此缓冲区上执行本机I/O操作。再每次调用os的一个本机I/O操作时,虚拟机都会尽量避免将缓冲区的内容复制到中间的缓冲区中。
    3:直接缓冲区的内容可以驻留在常规的垃圾回收堆之外,因为对应用程序造成的内存需求影响并不明显。
    4:非直缓冲区必然存在读写时用户态与内核态的拷贝,程序无法直接通过内核态交交互OS。
    5:直接缓冲区通过形成物理内存映射文件,交互操作系统物理内存,不做拷贝。 ↩︎ ↩︎

  2. ·
    分散读取(Scatter Reads):是指从Channel中读取的数据“分散”到多个buffer中。(有序)
    聚集写入(Gathering Writes):是指将多个Buffer中的数据“聚集”到Channel中。 ↩︎

你可能感兴趣的:(netty学习)