Netty 源码分析之 Java NIO

Java NIO(新的IO)是Java(来自Java 1.4)的可选IO API,意味着可选标准Java IO和Java连网API。Java NIO提供了一种不同于标准IO API的IO工作方式。为什么会出现 Java NIO 呢?下面我们来比较一下 Java IO 也 JDK 1.4 之后出现的 Java NIO 有什么区别.

1、IO 与 NIO

下面就是 IO 与 NIO 的对比:

IO NIO
面向流 面向缓冲
阻塞 IO 非阻塞 IO
选择器

1.1 面向流与面向缓冲

Java NIO 和 IO之间第一个最大的区别是,IO 是面向流的,NIO 是面向缓冲区的。 Java IO 面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。 Java NIO 的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。

1.2 阻塞与非阻塞IO

Java IO 的各种流是阻塞的。这意味着,当一个线程调用 read()write() 时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。 Java NIO 的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。

1.3 选择器(Selectors)

Java NIO 的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。

2、概述

Java Nio 全称java non-blocking IO,是指jdk1.4 及以上版本里提供的新api(New IO) ,为所有的原始类型(boolean类型除外)提供缓存支持的数据容器,使用它可以提供非阻塞式的高伸缩性网络。Java NIO 有更多的类和组件,但在我看来,Channel、Buffer 和Selector 构成了 API 的核心。其他组件,如 Pipe 和 FileLock,只是与这三个核心组件一起使用的实用程序类。

2.1 Channels and Buffers

通常,NIO 中的所有 IO 都从一个通道开始。河道有点像小溪。从通道数据可以读入缓冲区。数据也可以从缓冲区写入通道。这里有一个例子:
Netty 源码分析之 Java NIO_第1张图片
在 Java NIO 中 Channel 可以从 Buffer 中读取数据,Buffer 也可以写入数据到 Channle 当中。

有几种通道和缓冲区类型。下面是Java NIO中主要通道实现的列表:

  • FileChannel
  • DatagramChannel
  • SocketChannel
  • ServerSocketChannel

正如您所看到的,这些通道覆盖UDP + TCP网络IO和文件IO。

这些类还附带了一些有趣的接口,但为了简单起见,我将这些接口排除在Java NIO概述之外。在本Java NIO教程的其他文本中,将在相关的地方解释它们。

下面是Java NIO中核心缓冲区实现的列表:

  • ByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer

这些缓冲区涵盖了你可以通过IO发送的基本数据类型:byte, short, int, long, float, double and characters.。

Java NIO还有一个MappedByteBuffer ,它与内存映射文件一起使用。不过,在本文的概述中,我将不考虑这个缓冲区。

2.2 Selectors

Selector(选择器) 允许一个线程处理多个 Channel(通道) 的线程。如果您的应用程序打开了许多连接(Channels),但每个连接上的流量都很低,那么这很方便。例如,在聊天服务器中。

这里是一个例子,一个线程使用选择器处理 3 通道的:

Netty 源码分析之 Java NIO_第2张图片
如果你需要使用 Selector 你需要把 Channel 注册到它上面去。当你调用 Selector#select 方法时,这个方法会阻塞直到 Selector 里面注册的 Channel 之中有一个事件来临。比如一个连接事件或者数据可读事件。

3、Java NIO Channel

Java NIO通道类似于流,但有一些区别:

  • 可以对通道进行读写操作。流通常是单向的(读或写)。
  • 通道可以异步读取和写入。
  • 通道总是读取或写入缓冲区。

如上所述,您将数据从通道读入缓冲区,并将数据从缓冲区写入通道。这里有一个例子:

Netty 源码分析之 Java NIO_第3张图片

Channel 的实现:

以下是 Java NIO 中最重要的通道实现:

  • FileChannel:从文件和文件读取数据。
  • DatagramChannel:可以通过UDP在网络上读取和写入数据。
  • SocketChannel:可以通过TCP在网络上读写数据。
  • ServerSocketChannel:允许您侦听传入的TCP连接,就像web服务器那样。为每个传入连接创建SocketChannel。

Channel 的简单例子:

下面是一个使用FileChannel读取一些数据到缓冲区的基本例子:

    RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
    FileChannel inChannel = aFile.getChannel();

    ByteBuffer buf = ByteBuffer.allocate(48);

    int bytesRead = inChannel.read(buf);
    while (bytesRead != -1) {

      System.out.println("Read " + bytesRead);
      buf.flip();

      while(buf.hasRemaining()){
          System.out.print((char) buf.get());
      }

      buf.clear();
      bytesRead = inChannel.read(buf);
    }
    aFile.close();

注意 buf.flip() 调用。首先读入一个缓冲区。然后翻转。然后读出来。将在下一篇关于缓冲区的文章中更详细地讨论这个问题。

4、Java NIO Buffer

在与 NIO 通道交互时使用 Java NIO 缓冲区。如您所知,数据是从通道读取到缓冲区,然后从缓冲区写入到通道中。

缓冲区本质上是一个内存块,您可以将数据写入其中,然后再读取数据。这个内存块包装在一个 NIO 缓冲区对象中,该对象提供了一组方法,使使用内存块变得更容易。

4.1 Buffer 使用的基本例子

使用缓冲区读取和写入数据通常遵循以下 4 个步骤:

  • 将数据写入缓冲区
  • 调用buffer.flip ()
  • 从缓冲区中读取数据
  • 调用 buffer.clear() 或 buffer.compact()

当您将数据写入缓冲区时,缓冲区会跟踪您已写入的数据量。一旦需要读取数据,就需要使用flip()方法调用将缓冲区从写入模式切换到读取模式。在读取模式下,缓冲区允许您读取写入到缓冲区中的所有数据。

一旦您读取了所有数据,您需要清除缓冲区,使其为再次写入做好准备。您可以通过两种方式完成此操作:通过调用clear()或调用compact()。方法清除整个缓冲区。compact()方法只清除您已经读取的数据。所有未读数据都被移动到缓冲区的开头,现在数据将在未读数据之后写入缓冲区。

下面是一个简单的缓冲区使用示例,其中写入、翻转、读取和清除操作用粗体显示:

RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();

//create buffer with capacity of 48 bytes
ByteBuffer buf = ByteBuffer.allocate(48);

int bytesRead = inChannel.read(buf); //read into buffer.
while (bytesRead != -1) {

  buf.flip();  //make buffer ready for read

  while(buf.hasRemaining()){
      System.out.print((char) buf.get()); // read 1 byte at a time
  }

  buf.clear(); //make buffer ready for writing
  bytesRead = inChannel.read(buf);
}
aFile.close();

4.2 Buffer 的 Capacity, Position 和 Limit

缓冲区本质上是一个内存块,您可以将数据写入其中,然后再读取数据。这个内存块包装在一个 NIO 缓冲区对象中,该对象提供了一组方法,使使用内存块变得更容易。

为了理解缓冲区是如何工作的,缓冲区有三个您需要熟悉的属性。这些都是:

  • capacity
  • position
  • limit

position(位置) 和 limit(限制) 的意义取决于缓冲区是处于读模式还是写模式。capacity(容量) 总是意味着相同的,无论缓冲模式。

这里是写和读模式的容量、位置和限制的说明。解释将在插图之后的部分中进行。

Netty 源码分析之 Java NIO_第4张图片

Capacity

作为一个内存块,缓冲区有一定的大小,也称为它的“容量”。您只能将容量字节、长度、字符等写入缓冲区。一旦缓冲区满了,就需要清空它(读取数据或清除数据),然后才能将更多数据写入其中。

Position

当您将数据写入缓冲区时,您是在某个位置写入数据的。初始位置是0。当一个字节,长等已写入缓冲区的位置是先进的指向下一个单元在缓冲区中插入数据。Position 可以最大限度地变成 Capacity- 1

从缓冲区读取数据时,也要从给定位置读取数据。当您将缓冲区从写入模式翻转到读取模式时,其位置将重置为0。当您从缓冲区读取数据时,您是从position读取数据,并且position被提升到下一个要读取的位置。

Limit

在写模式下,缓冲区的限制是指可以写入缓冲区的数据量的限制。在写模式下,限制等于缓冲区的容量。

当将缓冲区翻转到读模式时,limit 表示可以从数据中读取的数据量的限制。因此,当将缓冲区翻转到读模式时,将limit设置为写模式的写位置。换句话说,您可以读取写入的字节数(限制设置为写入的字节数,由位置标记)。

4.3 Buffer Types

Java NIO 提供了以下缓冲区类型:

  • ByteBuffer
  • MappedByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer

可以看到,这些缓冲区类型表示不同的数据类型。换句话说,它们允许您将缓冲区中的字节改为char、short、int、long、float或double。

MappedByteBuffer 有点特殊,将在它自己的文本中介绍。

4.4 分配 Buffer

要获得一个 Buffer 对象,您必须首先分配它。每个缓冲区类都有一个执行此操作的 allocate() 方法。下面是一个例子,显示了字节缓冲区的分配,容量为48字节:

ByteBuffer buf = ByteBuffer.allocate(48);

下面是一个分配 CharBuffer 的例子,它有1024个字符的空间:

CharBuffer buf = CharBuffer.allocate(1024);

4.5 写入数据到 Buffer

你可以用两种方式将数据写入缓冲区:

  • 将数据从通道写入缓冲区
  • 通过缓冲区的put()方法,自己将数据写入缓冲区。

这里是一个例子,显示如何一个通道可以写入数据到缓冲区:

int bytesRead = inChannel.read(buf); //read into buffer.

下面是一个通过put()方法将数据写入缓冲区的例子:

buf.put(127); 

put()方法还有许多其他版本,允许您以许多不同的方式将数据写入缓冲区。例如,在特定位置写入,或将字节数组写入缓冲区。

4.6 flip

flip()方法将缓冲区从写入模式切换到读取模式。调用flip()将位置设置回0,并将限制设置为位置原来的位置。

换句话说,position现在标记读取位置,而limit标记有多少字节、字符等写入到缓冲区中——可以读取的字节、字符等的限制。

4.7 从缓冲区中读取数据

有两种方法可以从缓冲区读取数据。

  • 从缓冲区读取数据到通道中。
  • 自己从缓冲区读取数据,使用get()方法之一。

这里有一个例子,你可以读取数据从缓冲区到一个通道:

//从缓冲区读取到通道。
int byteswrite = inChannel.write(buf);

下面是一个使用get()方法从缓冲区读取数据的示例:

byte aByte = buf.get();

get()方法还有许多其他版本,允许您以许多不同的方式从缓冲区读取数据。例如,在特定位置读取,或从缓冲区中读取字节数组。有关具体的缓冲区实现的更多细节,请参阅JavaDoc。

4.8 rewind

rewind()将该位置设置为0,这样您就可以重新读取缓冲区中的所有数据。这个限制保持不变,因此仍然标记了多少元素(字节、字符等)可以从缓冲区中读取。

4.9 clear() and compact()

一旦完成了从缓冲区读取数据的工作,就必须使缓冲区为再次写入做好准备。可以通过调用clear()或compact()来实现。

如果调用clear(),则位置被设置回0并限制容量。换句话说,缓冲区被清除。缓冲区中的数据未被清除。只有告诉您可以将数据写入缓冲区的位置的标记才是。

如果在调用clear()时缓冲区中有任何未读数据,那么这些数据将被“遗忘”,这意味着不再有任何标记来说明哪些数据已被读取,哪些数据尚未被读取。

如果缓冲区中仍有未读数据,并且您希望稍后读取它,但需要先进行一些写入操作,请调用compact()而不是clear()。

compact()将所有未读数据复制到缓冲区的开头。然后它将position设置在最后一个未读元素的右侧。limit属性仍然设置为容量,就像clear()所做的那样。现在缓冲区已经准备好写入,但是您不会覆盖未读数据。

4.10 mark() and reset()

可以通过调用Buffer.mark()方法标记缓冲区中的给定位置。然后,您可以通过调用Buffer.reset()方法将位置重置回标记的位置。下面是一个例子:

buffer.mark();

//call buffer.get() a couple of times, e.g. during parsing.

buffer.reset();  //set position back to mark.  

4.11 equals()和compareTo ()

可以使用equals()和compareTo()来比较两个缓冲区。

equals ()

两个缓冲区是相等的,如果:

  • 它们是相同类型的(byte, char, int etc.)
  • 它们在缓冲区中有相同数量的剩余字节、字符等。
  • 所有剩余的字节、字符等都是相等的。

可以看到,equals只比较缓冲区的一部分,而不是其中的每个元素。实际上,它只是比较缓冲区中剩余的元素。

compareTo ()

方法比较两个缓冲区的剩余元素(字节,字符等),用于排序例程。一个缓冲区被认为比另一个缓冲区“小”,如果:

  • 第一个元素等于另一个缓冲区中对应的元素,小于另一个缓冲区中的元素。
  • 所有的元素都是相等的,但是第一个缓冲区比第二个缓冲区早耗尽元素(它的元素更少)。

5、Java NIO Selector

Java NIO 选择器是一个组件,它可以检查一个或多个Java NIO 通道实例,并确定哪些通道可以读取或写入。通过这种方式,一个线程可以管理多个通道,从而实现多个网络连接。

5.1 为什么使用选择器?

使用单个线程处理多个通道的优点是,处理通道所需的线程更少。实际上,您可以使用一个线程来处理所有的通道。对于操作系统来说,线程之间的切换非常昂贵,而且每个线程也会占用操作系统中的一些资源(内存)。因此,使用的线程越少越好。

但是请记住,现代操作系统和CPU在多任务处理方面越来越好,因此多线程的开销会随着时间的推移而减小。实际上,如果一个CPU有多个核心,那么不执行多任务可能会浪费CPU能量。无论如何,设计讨论属于不同的文本。这里只需说明,您可以使用一个选择器,用一个线程处理多个通道。

这里是一个例子,一个线程使用选择器处理3通道的:

Netty 源码分析之 Java NIO_第5张图片

5.2 创建一个 Selector

通过调用Selector.open()方法创建一个选择器,如下所示:

Selector selector = Selector.open();

5.3 向选择器注册通道

为了使用带有选择器的通道,您必须向选择器注册通道。这是通过selectablechanner .register()方法完成的,如下所示:

channel.configureBlocking(false);

SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

通道必须处于非阻塞模式才能与选择器一起使用。这意味着你不能使用 FileChannel 的选择器,因为 FileChannel 的不能切换到非阻塞模式。套接字通道可以正常工作。

请注意register()方法的第二个参数。这是一个“兴趣集”,意思是您希望通过选择器在通道中侦听的事件。你可以收听四种不同的事件:

  • Connect
  • Accept
  • Read
  • Write

“触发事件”的通道也被称为为该事件“准备好”。因此,一个已成功连接到另一个服务器的通道是“连接就绪”。接收传入连接的服务器套接字通道已准备就绪。一个有数据准备被读的通道是“读”准备。一个为您准备好写入数据的通道是“写”就绪。

这四个事件由四个SelectionKey常量表示:

  • SelectionKey.OP_CONNECT
  • SelectionKey.OP_ACCEPT
  • SelectionKey.OP_READ
  • SelectionKey.OP_WRITE

如果你对多个事件或常量感兴趣,像这样:

int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;

5.4 SelectionKey

正如您在上一节中看到的,当您使用选择器注册一个通道时,register()方法返回一个SelectionKey对象。这个SelectionKey对象包含了一些有趣的属性:

  • The interest set
  • The ready set
  • The Channel
  • The Selector
  • An attached object (optional)

将在下面描述这些属性。

5.5 Interest Set

兴趣集是您想要“选择”的事件集,如“向选择器注册通道”一节所述。你可以像这样通过SelectionKey读取和写入兴趣设置:

int interestSet = selectionKey.interestOps();

boolean isInterestedInAccept = interestSet & SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
boolean isInterestedInRead = interestSet & SelectionKey.OP_READ;
boolean isInterestedInWrite = interestSet & SelectionKey.OP_WRITE;

如您所见,您可以使用给定的 SelectionKey 常量和兴趣集来确定某个事件是否在兴趣集中。

5.6 Ready Set

就绪集是通道准备就绪的操作集。您将主要在选择之后访问准备集。选择将在后面的部分进行解释。您可以像这样访问ready集:

int readySet = selectionKey.readyOps();

您可以使用与兴趣集相同的方法测试通道准备好了什么事件/操作。但是,你也可以使用这四种方法来代替,它们都是布尔值:

selectionKey.isAcceptable ();
selectionKey.isConnectable ();
selectionKey.isReadable ();
selectionKey.isWritable ();

5.7 Channel + Selector

从 SelectionKey 访问 Channel + Selector 非常简单。以下是如何做到的:

Channel  channel  = selectionKey.channel();

Selector selector = selectionKey.selector(); 

5.8 Attaching Objects

可以将对象附加到 SelectionKey 上,这是识别给定通道的一种方便方法,也可以将进一步的信息附加到通道上。例如,可以将正在使用的缓冲区与通道连接,或将包含更多聚合数据的对象连接。这里是如何附加对象:

selectionKey.attach(theObject);

Object attachedObj = selectionKey.attachment();

在register()方法中,您还可以在向选择器注册通道时附加一个对象。这是它的样子:

SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);

5.9 通过 Selector 选择 Channel

一旦您用选择器注册了一个或多个通道,您就可以调用select()方法之一。这些方法返回为您感兴趣的事件(连接、接受、读或写)“准备”的通道。换句话说,如果您对准备读取的通道感兴趣,那么您将接收准备从select()方法读取的通道。

下面是select()方法:

  • int select()
  • int select(long timeout)
  • int selectNow()

select() 一直阻塞,直到至少有一个通道为您注册的事件准备好。

select(long timeout)的操作与select()相同,只是它阻塞的时间最长为超时毫秒(参数)。

selectNow() 完全不阻塞。它会立即返回任何准备好的通道。

select()方法返回的 int 表示有多少通道已经准备好了。也就是说,自上次调用 select()以来已经准备好了多少个通道。如果您调用 select(),它返回1,因为一个通道已经准备好了,如果您再次调用select(),又有一个通道已经准备好了,它将再次返回1。如果您没有对第一个准备好的通道做任何操作,那么现在有两个准备好的通道,但是在每次select()调用之间只有一个通道准备好了。

5.10 selectedKeys ()

一旦您调用了一个select()方法,并且它的返回值表明一个或多个通道已经准备好了,您就可以通过调用选择器selectedKeys()方法,通过“selected key set”访问准备好的通道。这是它的样子:

 selectedKeys = selector.selectedKeys();

当您使用选择器注册一个通道时,channel .register()方法返回一个SelectionKey对象。此键表示使用该选择器注册的通道。您可以通过selectedKeySet()方法访问这些键。从SelectionKey。

您可以迭代这个选定的键集来访问就绪通道。这是它的样子:

Set selectedKeys = selector.selectedKeys();

Iterator keyIterator = selectedKeys.iterator();

while(keyIterator.hasNext()) {
    
    SelectionKey key = keyIterator.next();

    if(key.isAcceptable()) {
        // a connection was accepted by a ServerSocketChannel.

    } else if (key.isConnectable()) {
        // a connection was established with a remote server.

    } else if (key.isReadable()) {
        // a channel is ready for reading

    } else if (key.isWritable()) {
        // a channel is ready for writing
    }

    keyIterator.remove();
}

这个循环迭代所选键集中的键。对于每个键,它测试键以确定键引用的通道已经准备好了。

注意每次迭代结束时的 keyIterator.remove() 调用。选择器不会从所选键集本身移除SelectionKey实例。当您处理完通道时,您必须这样做。下次通道变为“ready”时,选择器将再次将其添加到选中的键集。

由SelectionKey.channel()方法返回的通道应该被转换为您需要使用的通道,例如ServerSocketChannel或SocketChannel等。

5.11 wakeUp()

调用了被阻塞的select()方法的线程可以离开select()方法,即使还没有通道准备好。这是通过让另一个线程调用选择器上的Selector.wakeup()方法来实现的,第一个线程在选择器上调用了select()。然后,在select()中等待的线程将立即返回。

如果另一个线程调用了wakeup(),并且当前在select()中没有阻塞线程,那么下一个调用select()的线程将立即“唤醒”。

5.12 close ()

当您完成选择器时,您调用它的close()方法。这将关闭选择器,并使使用此选择器注册的所有SelectionKey实例无效。通道本身并没有关闭。

5.13 完整的 Selector 的例子

下面是一个完整的示例,它打开一个选择器,用它注册一个通道(忽略了通道实例化),并持续监视选择器对四个事件(accept, connect, read, write)的“准备就绪”。

Selector selector = Selector.open();

channel.configureBlocking(false);

SelectionKey key = channel.register(selector, SelectionKey.OP_READ);


while(true) {

  int readyChannels = selector.selectNow();

  if(readyChannels == 0) continue;


  Set selectedKeys = selector.selectedKeys();

  Iterator keyIterator = selectedKeys.iterator();

  while(keyIterator.hasNext()) {

    SelectionKey key = keyIterator.next();

    if(key.isAcceptable()) {
        // a connection was accepted by a ServerSocketChannel.

    } else if (key.isConnectable()) {
        // a connection was established with a remote server.

    } else if (key.isReadable()) {
        // a channel is ready for reading

    } else if (key.isWritable()) {
        // a channel is ready for writing
    }

    keyIterator.remove();
  }
}

6 Java NIO 完整示例

使用 NIO 的三个核心概念写一个银行叫号系统,帮助大家更加理解 NIO 的概念及它的 API 使用。

6.1 NIOServer

使用 NIO 进行 Socket 服务端编程,绑定端口为 8080。初始 ServerSocketChannel 对象,并且设置为非阻塞的,接着把 Selector 注册到 ServerSocketChannel 监听服务端SelectionKey.OP_ACCEPT事件。

通过 selector.select() 阻塞直到 Selector 有 Channel 来临。做为服务端,分别处理:

  • SelectionKey.OP_ACCEPT: 服务端接收事件,设置 Channel 为非阻塞并向 Selector 注册 SelectionKey.OP_READ
  • SelectionKey.OP_READ:可读事件,通过 ByteBuffer 读取客户端发送过来的消息并向Selector 注册 SelectionKey.OP_WRITE
  • SelectionKey.OP_WRITE:可写事件,向客户端发送消息并向Selector 注册 SelectionKey.OP_READ

NIOServer.java

/**
 *
 * SelectionKey.OP_ACCEPT//服务端,只要客户端连接,就触发
 * SelectionKey.OP_CONNECT//客户端,只要连上了服务端,就触发
 * SelectionKey.OP_READ//只要发生读操作,就触发
 * SelectionKey.OP_WRITE//只要发生写操作,就触发
 *
 */
public class NIOServer {
     
    int port = 8080;
    ServerSocketChannel server;
    Selector selector;
    ByteBuffer receiveBuffer = ByteBuffer.allocate(1024);
    ByteBuffer sendBuffer = ByteBuffer.allocate(1024);
    Map<SelectionKey, String> sessionMsg = new HashMap<>();

    public NIOServer(int port) throws IOException {
     
        this.port = port;
        //就相当于是高速公路,同时开几排车,带宽,带宽越大,车流量就越多
        server = ServerSocketChannel.open();
        //关卡也打开了,可以多路复用了
        server.socket().bind(new InetSocketAddress(this.port));
        //默认是阻塞的,手动设置为非阻塞,它才是非阻塞
        server.configureBlocking(false);
        //叫号系统开始工作
        selector = Selector.open();
        //高速管家,BOSS已经准备就绪,等会有客人来了,要通知我一下
        //我感兴趣的事件
        server.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("NIO服务已经启动,监听端口是:" + this.port);
    }

    public void listener() throws IOException {
     
        //轮询
        while (true) {
     
            //判断一下,当前有没有客户来注册,有没有排队的,有没有取号的
            //没有你感兴趣的事件触发的时候,它还是阻塞在这个位置
            //只要有你感兴趣的事件发生的时候,程序才会往下执行
			//这个位置,还是一个阻塞的
            int i = selector.select();
            if (i == 0) {
     
                continue;
            }
            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = keys.iterator();
            while (iterator.hasNext()) {
     
                //来一个处理一个
                process(iterator.next());
                //处理完之后打发走
                iterator.remove();
            }
        }
    }

    private void process(SelectionKey key) throws IOException {
     
        //判断客户有没有跟我们BOSS建立好连接
        if (key.isAcceptable()) {
     
            SocketChannel client = server.accept();
            client.configureBlocking(false);
            //继续告诉叫号系统,下一步我要开始读数据了,记得通知我
            client.register(selector, SelectionKey.OP_READ);
        }
        //判断是否可以读数据了
        else if (key.isReadable()) {
     
            receiveBuffer.clear();
            //这条管道是交给我们NIO API内部去处理的
            SocketChannel client = (SocketChannel) key.channel();
            int len = client.read(receiveBuffer);
            if (len > 0) {
     
                String msg = new String(receiveBuffer.array(), 0, len);
                sessionMsg.put(key, msg);
                System.out.println("获取客户端发送来的消息:" + msg);
            }
            //告诉叫号系统,下一个我感兴趣的事件就是要写数据了
            client.register(selector, SelectionKey.OP_WRITE);
        }
        //是否可以写数据
        else if (key.isWritable()) {
     
            if (!sessionMsg.containsKey(key)) {
     
                return;
            }
            SocketChannel client = (SocketChannel) key.channel();
            sendBuffer.clear();
            sendBuffer.put((sessionMsg.get(key) + ",你好,您的请求已处理完成").getBytes());
            sendBuffer.flip();//
            client.write(sendBuffer);
            //再告诉我们叫号系统,下一个我要关心的动作,又是读了
            //如此反复,我们的程序就陷入到了一个银行取款的同步陷阱里面去了的
            client.register(selector, SelectionKey.OP_READ);
        }
    }

    public static void main(String[] args) throws IOException {
     
        new NIOServer(8080).listener();
    }
}

6.2 NIOClient

使用 NIO 进行 Socket 客户端编程,连接本地服务器端口 8080。初始 SocketChannel 对象,并且设置为非阻塞的,接着把 Selector 注册到 SocketChannel 监听服务端SelectionKey.OP_CONNECT事件。

通过 selector.select() 阻塞直到 Selector 有 Channel 来临。做为服务端,分别处理:

  • SelectionKey.OP_CONNECT: 客户端连接事件,设置连接 SocketChannel 并向 Selector 注册 SelectionKey.OP_CONNECT
  • SelectionKey.OP_READ:可读事件,读取服务端发送过来的消息并向Selector 注册 SelectionKey.OP_WRITE
  • SelectionKey.OP_WRITE:可写事件,向服务端发送消息并向Selector 注册 SelectionKey.OP_READ

NIOClient.java

public class NIOClient {
     

    SocketChannel client;
    InetSocketAddress serverAddress = new InetSocketAddress("localhost", 8080);
    Selector selector;
    ByteBuffer receiveBuffer = ByteBuffer.allocate(1024);
    ByteBuffer sendBuffer = ByteBuffer.allocate(1024);

    public NIOClient() throws IOException {
     
        //先开路
        client = SocketChannel.open();
        client.configureBlocking(false);
        client.connect(serverAddress);
        selector = Selector.open();
        client.register(selector, SelectionKey.OP_CONNECT);
    }

    public void session() throws IOException {
     
        //先要判断是否已经建立连接
        if (client.isConnectionPending()) {
     
            client.finishConnect();
            System.out.println("请在控制台登记姓名");
            client.register(selector, SelectionKey.OP_WRITE);
        }
        Scanner scan = new Scanner(System.in);
        while (scan.hasNextLine()) {
     
            String name = scan.nextLine();
            if ("".equals(name)) {
     
                continue;
            }
            process(name);
        }

    }

    public void process(String name) throws IOException {
     
        boolean unFinish = true;
        while (unFinish) {
     
            //判断一下,当前有没有客户来注册,有没有排队的,有没有取号的
            int i = selector.select();
            if (i == 0) {
     
                continue;
            }
            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = keys.iterator();
            while (iterator.hasNext()) {
     
                SelectionKey key = iterator.next();
                if (key.isWritable()) {
     
                    sendBuffer.clear();
                    sendBuffer.put(name.getBytes());
                    sendBuffer.flip();
                    client.write(sendBuffer);
                    client.register(selector, SelectionKey.OP_READ);
                } else if (key.isReadable()) {
     
                    receiveBuffer.clear();
                    int len = client.read(receiveBuffer);
                    if (len > 0) {
     
                        receiveBuffer.flip();
                        System.out.println("获取到服务端反馈的信息:" + new String(receiveBuffer.array(), 0, len));
                        client.register(selector, SelectionKey.OP_WRITE);
                        unFinish = false;
                    }
                }
            }
        }
    }

    public static void main(String[] args) throws IOException {
     
        new NIOClient().session();
    }
    
}

6.3 测试

启动 NIOServer:
Netty 源码分析之 Java NIO_第6张图片
启动 NIOClient 并在控制台输入信息:
Netty 源码分析之 Java NIO_第7张图片
此时服务端控制台打印的信息为:

Netty 源码分析之 Java NIO_第8张图片
这样一个基于 NIO 简单的 Socket 编程就完成了。

参考文章:Java NIO

你可能感兴趣的:(Java,Algorithms)