Java网络IO模型详解(BIO、NIO、AIO)
作者:沈小洋
简介
Java 支持三种网络 IO 模型:BIO、NIO、AIO。
- Java BIO 是同步阻塞模型,一个连接对应一个线程,客户端有连接请求时服务端就启动一个线程,即使这个连接不做任何事情也会占用线程资源。
- Java NIO 是同步非阻塞模型,一个线程可以处理多个连接,客户端连接请求会注册到多路复用器(Selector),多路复用器检测到连接有 IO 时间就会处理。
- Java AIO 是异步非阻塞模型,AIO 引入了异步通道的概念,读写异步通道会立刻返回,读写的数据由 Future 或 CompletionHandler 进一步处理。
BIO 适用于连接数少的场景,程序编写比较简单,对服务器的资源要求比较高,JDK1.4之前的唯一选择。NIO 适用于连接数多的场景,例如聊天服务器、服务器间通讯等,程序编写比较复杂,JDK1.4开始支持。AIO 也适用于连接数多的场景,但更加偏向于异步操作多的场景。
Java BIO
模型示例
客户端代码示例
import java.io.*; import java.net.InetSocketAddress; import java.net.Socket; import java.net.SocketAddress; public class BIOClient { public static void main(String[] args) { new BIOClient().start("localhost", 6666); } public void start(String host, int port) { // 初始化 socket Socket socket = new Socket(); try { // 设置 socket 连接 SocketAddress remote = new InetSocketAddress(host, port); socket.setSoTimeout(5000); socket.connect(remote); // 发送数据 PrintWriter writer = getWriter(socket); writer.write("hello server"); writer.flush(); // // 发起请求 // PrintWriter writer = getWriter(socket); // writer.write(compositeRequest(host)); // writer.flush(); // // // 读取响应 // String msg; // BufferedReader reader = getReader(socket); // while ((msg = reader.readLine()) != null) { // System.out.println(msg); // } } catch (IOException e) { e.printStackTrace(); } finally { try { socket.close(); } catch (IOException e) { e.printStackTrace(); } } } private BufferedReader getReader(Socket socket) throws IOException { InputStream in = socket.getInputStream(); return new BufferedReader(new InputStreamReader(in)); } private PrintWriter getWriter(Socket socket) throws IOException { OutputStream out = socket.getOutputStream(); return new PrintWriter(new OutputStreamWriter(out)); } private String compositeRequest(String host) { return "GET / HTTP/1.1\r\n" + "Host: " + host + "\r\n" + "User-Agent: curl/7.43.0\r\n" + "Accept: */*\r\n\r\n"; } }
服务端代码示例
import java.io.InputStream; import java.net.ServerSocket; import java.net.Socket; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class BIOServer { public static void main(String[] args) throws Exception { // 创建一个线程池 ExecutorService pool = Executors.newCachedThreadPool(); // 创建 ServerSocket ServerSocket serverSocket = new ServerSocket(6666); while (true) { // 等待客户端连接 final Socket socket = serverSocket.accept(); // 接收到一个客户端连接 放入线程池进行处理 pool.execute(() -> process(socket)); } } static void process(Socket socket) { try { byte[] bytes = new byte[1024]; // 通过 socket 获取输入流 InputStream inputStream = socket.getInputStream(); // 循环读取客户端发送的数据 while (true) { // 没有数据的时候这里会阻塞等待 int read = inputStream.read(bytes); if (read == -1) break; // 输出客户端发送的数据 System.out.println(new String(bytes, 0, read)); } } catch (Exception e) { e.printStackTrace(); } finally { try { socket.close(); } catch (Exception e) { e.printStackTrace(); } } } }
Java NIO
NIO 采用 Reactor 模式,属于 IO 多路复用模型,可以用一个线程处理多个请求。NIO 有三大核心模块,通道(Channel)、缓冲区(Buffer)、选择器(Selector)。NIO 的非阻塞模式,使主线程在未发生数据读写事件时无需阻塞,可以继续做其他事情,这就大大增强了服务器的并发处理能力。
模型示例
Selector 对应一个线程,一个 Selector 可以对应多个 Channel,一个 Channel 对应一个 Buffer。程序切换到哪个 Channel 是由事件决定的,Selector 会根据不同的事件切换不同的 Channel。下图描述了 Channel、Buffer 和 Selector 的关系。
MappedByteBuffer 简介
NIO 提供的 MappedByteBuffer 支持支持在内存(堆外内存)中修改文件,可以减少一次数据拷贝。文件同步的部分,由 NIO 自己完成。
代码示例
import java.io.RandomAccessFile; import java.nio.MappedByteBuffer; import java.nio.channels.FileChannel; /** * 说明 1.MappedByteBuffer 可让文件直接在内存(堆外内存)修改,操作系统不需要拷贝一次 */ public class MappedByteBufferTest { public static void main(String[] args) throws Exception { RandomAccessFile randomAccessFile = new RandomAccessFile("1.txt", "rw"); //获取对应的通道 FileChannel channel = randomAccessFile.getChannel(); /** * 参数 1:FileChannel.MapMode.READ_WRITE 使用的读写模式 * 参数 2:0:可以直接修改的起始位置 * 参数 3:5: 是映射到内存的大小(不是索引位置),即将 1.txt 的多少个字节映射到内存 * 可以直接修改的范围就是 0-5 * 实际类型 DirectByteBuffer */ MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5); mappedByteBuffer.put(0, (byte) 'H'); mappedByteBuffer.put(3, (byte) '9'); mappedByteBuffer.put(5, (byte) 'Y');//IndexOutOfBoundsException randomAccessFile.close(); System.out.println("修改成功~~"); } }
NIO 编程代码原理分析图
关于 NIO 非阻塞网络编程相关的(Selector、SelectionKey、ServerScoketChannel 和 SocketChannel)关系梳理图
服务端代码示例
可以结合上面的原理图观察代码实现细节
import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.*; import java.util.Iterator; public class GroupChatServer { // 定义属性 private Selector selector; private ServerSocketChannel listenChannel; private static final int PORT = 6667; // 构造器执行初始化工作 public GroupChatServer() { try { // 得到选择器 selector = Selector.open(); // 监听端口的主线程 listenChannel = ServerSocketChannel.open(); // 绑定端口 listenChannel.socket().bind(new InetSocketAddress(PORT)); // 设置非阻塞模式 listenChannel.configureBlocking(false); // 将该 listenChannel 注册到 selector listenChannel.register(selector, SelectionKey.OP_ACCEPT); } catch (IOException e) { e.printStackTrace(); } } public void listen() { try { // 循环处理 while (true) { int count = selector.select(); // 有事件处理 if (count > 0) { // 遍历得到 selectionKey 集合 Iterator<SelectionKey> iterator = selector.selectedKeys().iterator(); while (iterator.hasNext()) { // 取出 selectionKey SelectionKey key = iterator.next(); // 监听到 accept if (key.isAcceptable()) { SocketChannel sc = listenChannel.accept(); sc.configureBlocking(false); // 将该 sc 注册到 selector sc.register(selector, SelectionKey.OP_READ); // 提示 System.out.println(sc.getRemoteAddress() + " 上线 "); } if (key.isReadable()) {// 通道发送read事件,即通道是可读的状态 // 处理读(专门写方法..) readData(key); } // 当前的 key 删除,防止重复处理 iterator.remove(); } } else { System.out.println("等待...."); } } } catch (Exception e) { e.printStackTrace(); } finally { // 发生异常处理.... } } // 读取客户端消息 public void readData(SelectionKey key) { SocketChannel channel = null; try { // 得到 channel channel = (SocketChannel) key.channel(); // 创建 buffer ByteBuffer buffer = ByteBuffer.allocate(1024); int count = channel.read(buffer);// NIO这里不会阻塞 因为事件触发时必然已经有数据了 所以叫非阻塞IO // 根据 count 的值做处理 if (count > 0) { // 把缓存区的数据转成字符串 String msg = new String(buffer.array()); // 输出该消息 System.out.println("form客户端:" + msg); // 向其它的客户端转发消息(去掉自己),专门写一个方法来处理 sendInfoToOtherClients(msg, channel); } } catch (IOException e) { try { System.out.println(channel.getRemoteAddress() + "离线了.."); // 取消注册 key.cancel(); // 关闭通道 channel.close(); } catch (IOException e2) { e2.printStackTrace(); } } } // 转发消息给其它客户(通道) private void sendInfoToOtherClients(String msg, SocketChannel self) throws IOException { System.out.println("服务器转发消息中..."); // 遍历所有注册到 selector 上的 SocketChannel,并排除 self for (SelectionKey key : selector.keys()) { // 通过 key 取出对应的 SocketChannel Channel targetChannel = key.channel(); // 排除自己 if (targetChannel instanceof SocketChannel && targetChannel != self) { // 转型 SocketChannel dest = (SocketChannel) targetChannel; // 将 msg 存储到 buffer ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes()); // 将 buffer 的数据写入通道 dest.write(buffer); } } } public static void main(String[] args) { // 创建服务器对象 GroupChatServer groupChatServer = new GroupChatServer(); groupChatServer.listen(); } }
Java AIO
AIO 是异步非阻塞的,引入了异步通道的概念,采用 Proactor 模式,操作系统完成数据拷贝操作后才会通知服务端线程。AIO 本质上还是 IO 多路复用模型,与 NIO 比起来,AIO 只是在非阻塞的前提下增加了异步功能,具体则体现在代码编写以及数据传输两个层面。
- 从代码编写角度来说,原来的同步方法会阻塞等待接口返回,而现在可以异步等待返回结果。
- 从数据传输角度来说,每个请求都需要传输数据,NIO 虽然是非阻塞的,但是事件到达后,NIO 需要自己把数据从内核空间复制到用户空间。AIO 引入异步逻辑后,事件到达后系统不会立刻通知服务端线程,而是会自己把数据从内核空间复制到用户空间,完成这个操作后,才会通知服务端线程去处理。
AIO 的使用场景还是比较少,现在大部分开源框架中应该还是以使用 NIO 为主,AIO 在性能方面的提升还是比较有限,主要的变化还是增加了异步功能。
如何理解 Reactor 和 Proactor 的区别?
Reactor 可以理解为「来了事件操作系统通知应用进程,让应用进程来处理」,而Proactor 可以理解为「来了事件操作系统来处理,处理完再通知应用进程」。
总结
到此这篇关于Java网络IO模型(BIO、NIO、AIO)的文章就介绍到这了,更多相关Java网络BIO、NIO、AIO内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!