目录
1、什么是NIO编程?
为什么说Java NIO是非阻塞的?
2、Java NIO 通道(Channel)详解
如何获取Channel对象?
3、Java NIO 缓冲区(Buffer)详解
(1)获取缓冲区对象
(2)将数据写入Buffer以及从Buffer读取数据
(3)通道Channel和缓冲区Buffer的相互读写
(4)图解缓冲区Buffer
4、Java NIO 选择器/Selector详解
(1)将channel注册到Selector上
(2)为什么说 select() 方法是阻塞的?
(3)Selector 的 selectedKeys() 方法
5、为什么说Java NIO是事件驱动的?
6、NIO编程完整的代码示例
NIO(New I/O)是Java中一种提供了非阻塞式I/O操作的编程模型。它引入了一组新的Java类,用于取代传统的Java I/O类(如InputStream和OutputStream),以提供更高效、更灵活的I/O操作。// 部分NIO API实际上是阻塞的,例如File API
Java NIO允许执行非阻塞的IO。例如,线程可以请求通道将数据读入缓冲区。当通道将数据读入缓冲区时,线程可以做其他事情。一旦数据被读入缓冲区,线程就可以继续处理它。将数据写入通道也是如此。// 非阻塞的核心在于选择器
NIO的核心组件包括以下几个方面:
下面是一个Thread使用Selector处理3个Channel的示例:
要使用Selector,首先需要注册Channel,然后调用它的select()方法。select()方法将阻塞,直到为其中一个已注册通道准备好事件(Event)为止。select()方法返回后,线程就可以处理这些事件。
Java NIO提供了一种非阻塞的I/O操作模型,相比传统的阻塞式I/O模型,Java NIO允许应用程序在进行I/O操作时不需要等待数据的到达或发送完成,而可以继续执行其他任务。
在传统的阻塞式I/O模型中,当一个线程执行一个I/O操作(如读取文件或网络数据)时,它将被阻塞,直到操作完成或发生错误。这意味着在阻塞模型下,一个线程只能处理一个I/O操作,如果有多个I/O操作需要处理,就需要使用多个线程,增加了线程的开销和管理复杂性。
而Java NIO使用了非阻塞的I/O操作模型。它引入了Channel(通道)和Selector(选择器)的概念,使得应用程序可以注册多个Channel到一个Selector上,并通过Selector来监控这些Channel的状态。Selector可以轮询已注册的Channel,当某个Channel满足I/O事件时(如可读、可写等),就会通知应用程序进行相应的处理。
在非阻塞模型下,一个线程可以同时处理多个Channel的I/O操作,而不需要为每个Channel创建一个单独的线程。这种方式可以大大提高系统的并发性能和资源利用率。
此外,Java NIO还提供了基于事件驱动的异步I/O操作模型(AIO,Asynchronous I/O),通过使用Future和回调机制,允许应用程序在进行I/O操作时不需要主动等待操作完成,而是在操作完成后由操作系统通知应用程序。// NIO2
Java NIO通道类似于流,但有一些不同:
如上所述,数据总是从通道读入缓冲区,或者从缓冲区写入通道:
下面是Java NIO中最重要的Channel实现:
在Java NIO中,可以通过以下几种方式来获取Channel对象:
(1)文件通道(FileChannel):通过文件输入流(FileInputStream)或文件输出流(FileOutputStream)获取文件通道。示例代码如下:
FileInputStream fis = new FileInputStream("path/to/file");
FileChannel fileChannel = fis.getChannel();
FileOutputStream fos = new FileOutputStream("path/to/file");
FileChannel fileChannel = fos.getChannel();
(2)网络通道(SocketChannel、ServerSocketChannel、DatagramChannel):通过Java NIO提供的网络编程类获取网络通道。示例代码如下:
SocketChannel socketChannel = SocketChannel.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
DatagramChannel datagramChannel = DatagramChannel.open();
(3)管道(Pipe):通过管道获取通道。管道用于在两个线程之间进行通信,可以使用Pipe.open()方法打开一个管道,并通过source()和sink()方法获取通道。示例代码如下:
Pipe pipe = Pipe.open();
Pipe.SinkChannel sinkChannel = pipe.sink();
Pipe.SourceChannel sourceChannel = pipe.source();
这些方法返回的Channel对象可以用于读取或写入数据,执行I/O操作。
另外,需要注意的是,以上方法获取的Channel对象都是阻塞式的。如果想要使用非阻塞模式,可以通过调用configureBlocking(false)方法将通道设置为非阻塞模式,然后可以使用Selector来进行非阻塞I/O操作。// FileChannel无此方法,所以它总是阻塞的
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
这样设置后,可以通过Selector来监控这些非阻塞通道的状态,并进行相应的I/O操作。
下面是一个使用FileChannel复制文件的基本示例:// 通道+缓冲区+阻塞式
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.channels.FileChannel;
import java.nio.ByteBuffer;
public class FileCopyExample {
public static void main(String[] args) {
try {
FileInputStream sourceFile = new FileInputStream("F:\\source.txt");
FileOutputStream destinationFile = new FileOutputStream("F:\\destination.txt");
FileChannel sourceChannel = sourceFile.getChannel();
FileChannel destinationChannel = destinationFile.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (sourceChannel.read(buffer) != -1) {
// 翻转缓冲区,为通道写入序列做准备:limit设置为当前position的值,position设置为0,mark设置为-1(废弃)
buffer.flip();
destinationChannel.write(buffer);
// 在放置数据之前清空缓存,为通道读写入序列做准备:limit设置为capacity的值,position设置为0,mark设置为-1(废弃):
buffer.clear();
}
sourceChannel.close();
destinationChannel.close();
sourceFile.close();
destinationFile.close();
System.out.println("File copied successfully.");
} catch (Exception e) {
e.printStackTrace();
}
}
}
缓冲区本质上是一块内存,你可以向其中写入数据,然后可以再次读取数据。该内存块封装在NIO Buffer对象中,该对象提供了一组方法,让使用内存块变得更容易。
Java NIO附带了以下缓冲区类型:
在Java NIO中,可以通过以下方式来获取Buffer对象:
1)使用分配方法(Allocation Methods):每个缓冲区类(如ByteBuffer、CharBuffer、IntBuffer等)都提供了allocate()方法来分配一个新的缓冲区对象。示例代码如下:
ByteBuffer buffer = ByteBuffer.allocate(1024);
CharBuffer buffer = CharBuffer.allocate(1024);
这种方式会分配一个指定容量的缓冲区对象,可以根据需要进行读写操作。
2)使用包装方法(Wrapping Methods):除了分配方法,缓冲区类还提供了wrap()方法,用于包装现有的数组来创建缓冲区对象。示例代码如下:
byte[] byteArray = new byte[1024];
ByteBuffer buffer = ByteBuffer.wrap(byteArray);
char[] charArray = new char[1024];
CharBuffer buffer = CharBuffer.wrap(charArray);
这种方式会使用现有的数组作为缓冲区的数据存储。
3)使用视图方法(View Buffer):缓冲区类还提供了一些视图方法,可以创建基于现有缓冲区的新缓冲区对象。这些视图缓冲区共享底层缓冲区的数据,并根据视图的不同提供不同类型的访问方式。常见的视图缓冲区有ByteBuffer的子类:CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer和DoubleBuffer等。示例代码如下:
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
CharBuffer charBuffer = byteBuffer.asCharBuffer();
IntBuffer intBuffer = byteBuffer.asIntBuffer();
这种方式可以根据需要创建适合特定数据类型的缓冲区。
1)将数据写入Buffer
在Java NIO中,有多种方式可以将数据写入Buffer对象。以下是几种常用的方式:
使用put()方法:Buffer类提供了多个put()方法的重载形式,可以根据不同数据类型来写入数据。例如:
使用通道(Channel):可以使用通道的read()方法将数据直接写入Buffer。例如:
int bytesRead = inChannel.read(buf); //read into buffer.
2)从Buffer读取数据
从Buffer读取数据的两种方式:使用get()方法和通道(Channel)
使用get()方法从缓冲区读取数据:
byte aByte = buf.get();
还有许多其他版本的get()方法,允许以许多不同的方式从Buffer读取数据。例如,在特定位置读取,或者从缓冲区读取一个字节数组的数据等。
将数据从缓冲区读入通道(Channel):可以使用通道的write()方法将数据从Buffer读入通道。例如:
//read from buffer into channel.
int bytesWritten = inChannel.write(buf);
1)把数据从Channel写入Buffer:channel.read(buffer)
// 创建一个FileChannel
FileChannel channel = new FileInputStream("path/to/file").getChannel();
// 创建一个ByteBuffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 将数据从Channel读取到Buffer中
int bytesRead = channel.read(buffer);
2)把数据从Buffer写入Channel:channel.write(buffer)
// 创建一个FileChannel
FileChannel channel = new FileOutputStream("path/to/file").getChannel();
// 创建一个ByteBuffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 将数据写入Buffer
buffer.put("Hello, World!".getBytes());
buffer.flip(); // 切换到读模式
// 将Buffer中的数据写入Channel
while (buffer.hasRemaining()) {
channel.write(buffer);
}
// 关闭Channel
channel.close();
1)缓冲区的创建
示例代码:
public static void main(String[] args) throws IOException {
//1、创建一个缓冲区
ByteBuffer allocate = ByteBuffer.allocate(10);
System.out.println(allocate.position());//0 获取当前索引所在位置
System.out.println(allocate.limit());//10 最多能操作到哪个索引位置
System.out.println(allocate.capacity());//10 返回缓冲区总长度
System.out.println(allocate.remaining());//10 还有多少个可以操作的个数:limit - position
System.out.println("———————————————————");
//2、添加3个字节
allocate.put("ABC".getBytes());
System.out.println(allocate.position());//3 获取当前索引所在位置
System.out.println(allocate.limit());//10 最多能操作到哪个索引位置
System.out.println(allocate.capacity());//10 返回缓冲区总长度
System.out.println(allocate.remaining());//7 还有多少个可以操作的个数
}
2)缓冲区的读写模式转换
写模式切换到读模式:flip()方法
flip()方法:用于将Buffer从写模式切换到读模式。flip()方法的作用是设置limit为当前位置,然后将position重置为0,以便在读取数据之前将Buffer准备好。
当将数据写入Buffer时,Buffer会跟踪写入了多少数据。一旦需要从Buffer读取数据,就需要调用flip()方法将Buffer从写入模式切换到读取模式。在读取模式下,缓冲区允许读取写入缓冲区的所有数据。// 作用:将缓冲区从写入模式切换到读取模式,读取缓冲区中的数据
读模式切换到写模式:clear()和compact()方法
clear()和compact():一旦从Buffer中读取了所有数据,就需要清除缓冲区,以便为再次写入做好准备。调用clear()或compact()方法可以达到清除缓冲区的效果。clear()方法会清除整个缓冲区,而compact()方法只会清除已经读取的数据,未读的数据都会被移到缓冲区的开头,新进的数据将在未读数据之后进行写入。// clear()和compact() 并不会真正清除数据,只是修改了相关位置数据的指针
切换到写模式:clear()方法将Buffer从读模式切换到写模式,重置位置position为0,限制limit设置为容量capacity,以便重新写入数据。
清空数据:clear()方法不会清除缓冲区的数据,而是将缓冲区标记为可重写状态,之前写入的数据仍然存在,但是在写模式下可以覆盖它们。
在Java NIO中,Selector是一个可用于多路复用(Multiplexing)I/O操作的关键组件。它允许单个线程同时管理多个通道(Channels),监控这些通道的I/O事件(如读就绪、写就绪等),并且在有就绪事件发生时进行响应。
以下是关于Selector的一些重要概念和使用方式:
1)创建和获取Selector对象:
2)注册通道到Selector:
3)选择操作:
4)处理就绪事件:
5)取消注册和关闭:
使用Selector可以在单个线程中同时处理多个通道的I/O操作,避免了为每个通道分配一个线程的开销。这种方式常用于需要同时管理多个通道的高并发应用场景,如网络服务器。
需要注意的是,Selector是基于事件驱动的,所以在处理就绪事件时需要注意及时响应并处理,否则可能会错过事件。
使用Selector,必须首先向选择器注册通道。向选择器注册通道可以使用Channel.register()方法进行注册,如下所示:
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
注意:Channel必须处于非阻塞模式才能与Selector一起使用。所以不能在选择器中使用FileChannel,因为FileChannel不能切换到非阻塞模式。// 所以一般文件的读写还是使用的BIO
向Selector注册channel,也是由操作系统完成的,也调用了底层操作系统的相关实现,这是因为Selector的实现机制基于操作系统提供的底层I/O多路复用机制实现的。
通过register()方法注册channel时,需要指定侦听的 I/O 事件类型(读、写等)。选择器可以侦听四种不同的事件:Connect、Accept、Read、Write,具体如下所示:
SelectionKey中定义的4种事件: // Java NIO基于事件驱动
select()方法是阻塞的主要原因是它的实现机制基于操作系统提供的底层I/O多路复用机制,例如Linux上的select()系统调用或Windows上的select()函数。这些底层机制都是阻塞的,因此select()方法在调用底层机制时也会阻塞。// select()方法是由操作系统实现的
看下源码,点击Selector.select()方法,进入Selector的实现类SelectorImpl,它会调用一个lockAndDoSelect()方法:// 基于 java 8
public int select(long var1) throws IOException {
if (var1 < 0L) {
throw new IllegalArgumentException("Negative timeout");
} else {
return this.lockAndDoSelect(var1 == 0L ? -1L : var1);
}
}
进入lockAndDoSelect()方法后,在该方法中会调用SelectorImpl.doSelect()方法。
protected abstract int doSelect(long var1) throws IOException;
private int lockAndDoSelect(long var1) throws IOException {
synchronized(this) {
if (!this.isOpen()) {
throw new ClosedSelectorException();
} else {
int var10000;
synchronized(this.publicKeys) {
synchronized(this.publicSelectedKeys) {
var10000 = this.doSelect(var1);
}
}
return var10000;
}
}
}
SelectorImpl.doSelect()方法是一个抽象方法,既然是抽象方法就会有对应的实现。继续跟进到该方法的实现类WindowsSelectorImpl.doSelect()方法中,其中有一行代码:this.subSelector.poll(),这行代码就是去调用操作系统的底层实现机制了。
protected int doSelect(long var1) throws IOException {
if (this.channelArray == null) {
throw new ClosedSelectorException();
} else {
// 省略...
if (this.interruptTriggered) {
this.resetWakeupSocket();
return 0;
} else {
// 省略...
try {
this.begin();
try {
// 在这里调用系统的select()方法
this.subSelector.poll();
} catch (IOException var7) {
this.finishLock.setException(var7);
}
if (this.threads.size() > 0) {
this.finishLock.waitForHelperThreads();
}
} finally {
this.end();
}
// 省略...
}
}
}
WindowsSelectorImpl是Java NIO库中用于Windows平台上的Selector的具体实现类。它实现了Selector接口,并提供了在Windows操作系统上使用I/O多路复用机制的功能。
继续跟进this.subSelector.poll()方法,可以看到它调用了一个本地方法:// 该方法阻塞
private int poll() throws IOException {
return this.poll0(WindowsSelectorImpl.this.pollWrapper.pollArrayAddress, Math.min(WindowsSelectorImpl.this.totalChannels, 1024), this.readFds, this.writeFds, this.exceptFds, WindowsSelectorImpl.this.timeout);
}
// 本地方法
private native int poll0(long var1, int var3, int[] var4, int[] var5, int[] var6, long var7);
this.subSelector对象
private final WindowsSelectorImpl.SubSelector subSelector = new WindowsSelectorImpl.SubSelector();
在WindowsSelectorImpl类中,SubSelector是一个内部类,用于管理特定事件类型的I/O事件。SubSelector继承自AbstractPollArrayWrapper类,用于封装I/O事件的轮询和处理。
SubSelector类中的poll()方法是用于执行对已注册通道的轮询操作,并返回就绪的通道数量。
一旦调用了select()方法,并且它的返回值表明一个或多个通道已经准备就绪,接下来就可以调用选择器的selectedKeys()方法,获取已经准备就绪的通道集。:// 这个步骤还挺难理解的,先执行select()方法,然后再执行selectedKeys()方法
在Java NIO中,Selector的selectedKeys()方法用于获取当前已选择键集合(selected key set)。
public Set selectedKeys() {
if (!this.isOpen() && !Util.atBugLevel("1.4")) {
throw new ClosedSelectorException();
} else {
return this.publicSelectedKeys;
}
}
该方法返回一个Set
需要注意的是,selectedKeys()方法返回的是一个可变的集合,即可以直接在返回的集合上进行增删操作。这意味着在处理完就绪事件后,需要手动从集合中移除相应的SelectionKey,以便下次的选择操作能正确更新就绪状态。// 先select(),后执行selectedKeys()
在使用selectedKeys()方法时,一般会结合Iterator来遍历集合,并逐个处理就绪的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();
}
在上述该循环中,会循环判断每一个SelectionKey引用的通道是否已经准备就绪。
在每次迭代结束时都会调用keyIterator.remove(),这是因为选择器自己不会从键集中删除SelectionKey的实例。所以,当你完成对该通道的处理时,就需要手动去移除它。当通道再次准备就绪时,选择器会再次将这个键添加到所选的键集中。// 手动移除键
另外,在处理具体的通道时,SelectionKey.channel()方法返回的通道应该被强制转换为需要使用的通道类型,例如ServerSocketChannel或SocketChannel等。
SocketChannel sc = (SocketChannel) key.channel();
Java NIO(New I/O)是事件驱动的,主要基于以下两个核心组件:选择器(Selector)和选择键(SelectionKey)。
Java NIO的事件驱动模型基于以下原理:
Java NIO的事件驱动模型允许应用程序在单线程中同时处理多个通道的I/O操作,提高了系统的并发性和可扩展性。相比于传统的阻塞I/O模型,事件驱动模型减少了线程的创建和管理开销,提高了系统的效率和响应能力。
// 在Java NIO中,Selector并不会不断轮询注册在其上的所有通道。实际上,Selector使用的是操作系统提供的事件通知机制,如epoll、kqueue等,来监听和等待就绪的通道。
// 验证操作系统提供的事件通知机制,待续...
客户端代码:// 以下代码可直接运行
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
import java.util.UUID;
public class NioClient {
public static void main(String[] args) {
String host = "127.0.0.1";
int port = 8001;
Selector selector = null;
SocketChannel socketChannel = null;
try {
selector = Selector.open();
socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false); // 非阻塞
// 如果直接连接成功,则注册到多路复用器上,发送请求消息,读应答
if (socketChannel.connect(new InetSocketAddress(host, port))) {
socketChannel.register(selector, SelectionKey.OP_READ);
doWrite(socketChannel);
} else {
socketChannel.register(selector, SelectionKey.OP_CONNECT);
}
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
while (true) {
try {
selector.select(1000);
Set selectedKeys = selector.selectedKeys();
Iterator it = selectedKeys.iterator();
SelectionKey key = null;
while (it.hasNext()) {
key = it.next();
it.remove();
try {
//处理每一个channel
handleInput(selector, key);
} catch (Exception e) {
if (key != null) {
key.cancel();
if (key.channel() != null) {
key.channel().close();
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
// 多路复用器关闭后,所有注册在上面的Channel资源都会被自动去注册并关闭
// if (selector != null)
// try {
// selector.close();
// } catch (IOException e) {
// e.printStackTrace();
// }
//
// }
}
public static void doWrite(SocketChannel sc) throws IOException {
byte[] str = UUID.randomUUID().toString().getBytes();
ByteBuffer writeBuffer = ByteBuffer.allocate(str.length);
writeBuffer.put(str);
writeBuffer.flip();
sc.write(writeBuffer);
}
public static void handleInput(Selector selector, SelectionKey key) throws Exception {
if (key.isValid()) {
// 判断是否连接成功
SocketChannel sc = (SocketChannel) key.channel();
if (key.isConnectable()) {
if (sc.finishConnect()) {
sc.register(selector, SelectionKey.OP_READ);
}
}
if (key.isReadable()) {
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int readBytes = sc.read(readBuffer);
if (readBytes > 0) {
readBuffer.flip();
byte[] bytes = new byte[readBuffer.remaining()];
readBuffer.get(bytes);
String body = new String(bytes, "UTF-8");
System.out.println("Server said : " + body);
} else if (readBytes < 0) {
// 对端链路关闭
key.cancel();
sc.close();
} else {
; // 读到0字节,忽略
}
}
Thread.sleep(3000);
doWrite(sc);
}
}
}
服务端代码:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
public class NioServer {
public static void main(String[] args) throws IOException {
int port = 8001;
Selector selector = null;
ServerSocketChannel servChannel = null;
try {
selector = Selector.open();
servChannel = ServerSocketChannel.open();
servChannel.configureBlocking(false);
servChannel.socket().bind(new InetSocketAddress(port), 1024);
servChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("服务器在8001端口守候");
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
while (true) {
try {
selector.select(1000);
Set selectedKeys = selector.selectedKeys();
Iterator it = selectedKeys.iterator();
SelectionKey key = null;
while (it.hasNext()) {
key = it.next();
it.remove();
try {
handleInput(selector, key);
} catch (Exception e) {
if (key != null) {
key.cancel();
if (key.channel() != null) {
key.channel().close();
}
}
}
}
} catch (Exception ex) {
ex.printStackTrace();
}
try {
Thread.sleep(500);
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
public static void handleInput(Selector selector, SelectionKey key) throws IOException {
if (key.isValid()) {
// 处理新接入的请求消息
if (key.isAcceptable()) {
// Accept the new connection
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
// Add the new connection to the selector
sc.register(selector, SelectionKey.OP_READ);
}
if (key.isReadable()) {
// Read the data
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int readBytes = sc.read(readBuffer);
if (readBytes > 0) {
readBuffer.flip();
byte[] bytes = new byte[readBuffer.remaining()];
readBuffer.get(bytes);
String request = new String(bytes, "UTF-8"); //接收到的输入
System.out.println("client said: " + request);
String response = request + " 666";
doWrite(sc, response);
} else if (readBytes < 0) {
// 对端链路关闭
key.cancel();
sc.close();
} else {
; // 读到0字节,忽略
}
}
}
}
public static void doWrite(SocketChannel channel, String response) throws IOException {
if (response != null && response.trim().length() > 0) {
byte[] bytes = response.getBytes();
ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
writeBuffer.put(bytes);
writeBuffer.flip();
channel.write(writeBuffer);
}
}
}
至此,全文结束。
附NIO编程的一些学习资料《Java NIO 教程》