在前面文章中介绍了JAVA中BIO的实现方式以及优化 介绍了Java BIO代码实现以及常见的几种优化方式,在Java后期的迭代中,引入NIO相关的内容, 提高IO的处理效率,这篇文章主要介绍NIO的代码实现。
使用NIO实现BIO
在NIO中,我们也可以实现阻塞IO的功能,功能和BIO保持一致, 同时针对BIO的实现优化方法也是一致的。
package com.jdk.test.demo.java.nio; import java.io.IOException; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.nio.ByteBuffer; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; /** * 使用nio的代码实现bio */ public class NioToBioDemo { public static void main(String[] args) throws IOException { ServerSocketChannel channel = ServerSocketChannel.open(); // 采用阻塞模式, 在等待客户端链接时,会产生阻塞 channel.configureBlocking(true); // 绑定8080端口 channel.bind(new InetSocketAddress(8080)); System.out.println("服务端已经启动, 端口: 8080"); // 循环等待接收客户端请求, 并将客户端的输入写出到channel while (true) { // 会一直阻塞,并等到客户端链接 SocketChannel socketChannel = channel.accept(); socketChannel.configureBlocking(true); printClientInfo(socketChannel); handle(socketChannel); } } private static void handle(SocketChannel socketChannel) throws IOException { // 从channel中读取数据 ByteBuffer byteBuffer = ByteBuffer.allocate(1024); while (socketChannel.read(byteBuffer) != -1) { String msg = readData(byteBuffer); System.out.println("接收到客户端消息: " + msg); // 将消息写出到客户端 byteBuffer.flip(); while (byteBuffer.hasRemaining()) { socketChannel.write(byteBuffer); } byteBuffer.compact(); } } private static void printClientInfo(SocketChannel channel) throws IOException { SocketAddress socketAddress = channel.getRemoteAddress(); System.out.println("客户端已经链接: " + socketAddress.toString()); } /** * 从channel中读取数据 * * @param byteBuffer 字节缓冲对象 * @return 客户端发送的数据 * @throws IOException */ private static String readData(ByteBuffer byteBuffer) throws IOException { byteBuffer.flip(); byte[] bytes = new byte[byteBuffer.limit()]; byteBuffer.get(bytes); return new String(bytes); } }
在以上的代码中,实现BIO的关键在于configureBlocking(true)方法的实现,该方法用于标记当前的channel的工作模式:
- true: 表示blocking(阻塞)工作模式,也就是BIO的实现
- false: 表示non-blocking(非阻塞)工作模式, 也是NIO的实现
因此针对以上的代码,如果要将BIO改为NIO的实现,其实就只是将configureBlocking(false)即可。
阻塞模式的工作,产生阻塞的点在那些地方呢?
- 针对ServerSocketChannel而言,阻塞主要在等待客户端链接(accept())方法上
- 针对SocketChannel而言,阻塞主要在从Socket读取数据(read())和写出数据(write())
NIO non-blocking实现方式
package com.jdk.test.demo.java.nio; import java.io.IOException; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.nio.ByteBuffer; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; /** * 使用nio的代码实现bio */ public class NioServerDemo { public static void main(String[] args) throws IOException { ServerSocketChannel channel = ServerSocketChannel.open(); // 采用非阻塞模式 channel.configureBlocking(false); // 绑定8080端口 channel.bind(new InetSocketAddress(8080)); System.out.println("服务端已经启动, 端口: 8080"); // 循环等待接收客户端请求, 并将客户端的输入写出到channel while (true) { // 当处于non-blocking模式的时候, 该方法不会发生阻塞 SocketChannel socketChannel = channel.accept(); try { if (socketChannel != null) { socketChannel.configureBlocking(false); printClientInfo(socketChannel); // 从channel中读取数据 ByteBuffer byteBuffer = ByteBuffer.allocate(1024); int data; while ((data = socketChannel.read(byteBuffer)) > -1) { if (data > 0) { String msg = readData(byteBuffer); System.out.println("接收到客户端消息: " + data + msg); // 将消息写出到客户端 byteBuffer.flip(); // 异步写出 socketChannel.write(byteBuffer); byteBuffer.clear(); } } } } catch (IOException e) { e.printStackTrace(); if (socketChannel != null) { socketChannel.close(); } } } } private static void printClientInfo(SocketChannel channel) throws IOException { SocketAddress socketAddress = channel.getRemoteAddress(); System.out.println("客户端已经链接: " + socketAddress.toString()); } /** * 从channel中读取数据 * * @param byteBuffer 字节缓冲对象 * @return 客户端发送的数据 * @throws IOException */ private static String readData(ByteBuffer byteBuffer) throws IOException { byteBuffer.flip(); byte[] bytes = new byte[byteBuffer.limit()]; byteBuffer.get(bytes); return new String(bytes); } }
当前的实例为nio的non-blocking工作模式, 这个工作模式最大的优点在于取消的阻塞,通过自身的循环实现获取客户端链接,读入和写出数据。对于上面的代码中,有几个点需要注意:
- 由于是non-blocking工作模式, accept()方法也不是阻塞的等待客户端链接,所以从accept()方法中获取的channel实际上可能为空的,所以需要有判空
- 在从SocketChannel读取数据的时,也不会产生阻塞,如果没有读取到数据,返回的是0,因此需要判断是否读取到数据
从以上的代码可以分析得知,在non-blocking工作模式中,会不停的判断是否有新的连接,是否有数据读取。因此在链接较多,或者空闲时,都可能会导致CPU使用过高等问题。因此NIO中引入了Selector,可以管理多个Channel。
NIO Selector实现
package com.jdk.test.demo.java.nio; 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.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Set; /** * 通过nio实现服务端处理客户端请求 */ public class NioSelectorServerDemo { private static Map<SocketChannel, ByteBuffer> writeCache = new HashMap<>(); public static void main(String[] args) { try { ServerSocketChannel socketChannel = ServerSocketChannel.open(); // non-blocking模式 socketChannel.configureBlocking(false); socketChannel.bind(new InetSocketAddress(8080)); // 将channel与selector进行绑定 Selector selector = Selector.open(); int ops = socketChannel.validOps(); socketChannel.register(selector, ops, null); while (true) { // 当前selector会阻塞, 直到有事件可以处理位置 selector.select(); // 从selector中获取可以处理的事件列表 Set<SelectionKey> selectionKeys = selector.selectedKeys(); Iterator<SelectionKey> iterator = selectionKeys.iterator(); while (iterator.hasNext()) { SelectionKey selectionKey = iterator.next(); // 说明当前的key是一个op_accept事件 if (selectionKey.isAcceptable()) { SocketChannel channel = socketChannel.accept(); channel.configureBlocking(false); System.out.println("客户端已连接: " + channel.getRemoteAddress().toString()); // 绑定op_read事件,等待从客户端读取数据 channel.register(selector, SelectionKey.OP_READ, null); } else if (selectionKey.isReadable()) { SocketChannel channel = (SocketChannel) selectionKey.channel(); // 从channel中读取数据 ByteBuffer byteBuffer = ByteBuffer.allocate(1024); int data = channel.read(byteBuffer); if (data > 0) { String msg = new String(byteBuffer.array(), 0, data); writeCache.put(channel, byteBuffer); System.out.println("接收到客户端消息: " + msg); } byteBuffer.flip(); channel.write(byteBuffer); byteBuffer.compact(); // channel.register(selector, SelectionKey.OP_WRITE, null); } iterator.remove(); } } } catch (IOException e) { e.printStackTrace(); } } }
在上面的实现中,可以看出,本身的NIO ServerSocketChannel还是non-blocking的工作模式,只是本身不需要去判断是否有客户端链接,而是将实现交给了Selector, Selector通过系统的epoll的实现,获取OP_ACCEPT, OP_READ, OP_WRITE事件的Channel, 这样对先前的两个实现有了比较大的提升,主要包含一下方面:
- Selector同时可以管理多个channel, 并且返回已经能够处理的channel列表,避免CPU使用率过高等问题
- 通过事件响应机制,避免了对多个channel扫描的流程,能够精确的处理需要处理的数据流,提高效率
因此通过selector管理多个channel, 可以提高服务端的整体性能,后面将介绍selector是如何实现,如果文章有帮助到你,请问文章点赞!!!