BIO
网络IO中比较经典的场景就是http服务器,在java中通过socket也就是BIO实现过程大致如下:
1.创建一个ServerSocket监听一个端口
2.通过accept方法阻塞服务器并等待客户端的连接
3.客户端发起请求,服务器通过accept方法获取一个客户端的socket
4.启动一个新线程来处理我们客户端的请求
5.处理请求的线程通过socket获取输入流并读取流中的数据
6.获取字节数据根据http协议解码数据,获取http请求
7.处理http请求,根据请求构建响应数据
8.根据http的编码协议对响应数据编码并写入客户端的socket
9.socket调用系统write函数将数据发送到物理设备网卡返回给客户端
10.重复接收请求从第3步执行
大概代码实现如下:
package com.crs.echo; import java.io.*; import java.net.ServerSocket; import java.net.Socket; /** * @author administrator * @version 1.0 * @date 2021/7/2 22:29 **/ public class BIOServer implements Server{ @Override public void start(int port) throws IOException { // 创建serversocket并监听端口 final ServerSocket serverSocket = new ServerSocket(port); System.out.println("打开socket连接并绑定端口"+port); try { while (true){ // 利用accept函数阻塞并接收前端请求 Socket socket =serverSocket.accept(); System.out.println("接收到请求来自: " + socket.getInetAddress()); // 有请求到来时开启新线程处理请求 new Thread(()->{ OutputStream out; InputStream in; try { // socket的输出流,向socket写入数据 out = socket.getOutputStream(); // socket的输入流,从socket中读取数据 in = socket.getInputStream(); DataInputStream dis = new DataInputStream(in); // 读取请求头,http的请求头:Get / HTTP/1.1 int len; String response = ""; byte[] bytes = new byte[124]; while ((len=dis.read(bytes))!=-1&&dis.available()>0){ response=response+new String(bytes); } System.out.println("准备将数据写回前端,返回的数据为"); System.out.println(response); // 通过OutputStream构建字符输出流 PrintWriter writer = new PrintWriter(socket.getOutputStream()); // 利用字符输出流向前端返回数据 writer.println("HTTP/1.0 200 OK"); writer.println("Content-Type:application/json"); writer.println(); writer.println(response); // 将流中的数据从缓冲区刷新到内存中 writer.flush(); // 关闭流 writer.close(); socket.close(); } catch (IOException e) { e.printStackTrace(); } finally { try { socket.close(); } catch (IOException e) { e.printStackTrace(); } } }).start(); } } catch (IOException e){ e.printStackTrace(); } } }
上述代码描述的就是使用BIO的形式来实现http服务器的过程,缺点是每当有新的请求时服务器都要创建一个新的线程来处理请求,创建线程是一个比较消耗资源的事情
NIO
在NIO模型中不需要创建多个线程,利用操作系统的IO多路复用模型来建立连接和读取数据
1.创建一个服务器socket通道并绑定监听端口
2.创建选择器Selector
3.将服务器socket通道注册到Selector中
4.通过选择器选择就绪事件并处理
5.通过就绪事件的通道键值获取客户端socket通道
6.从客户端socket通道中读取数据并处理
7.处理完数据后将响应数据写回客户端socket通道
8.在数据处理过程必须使用缓冲区ByteBuffer
通过java的NIO模式编写一个回显服务器
package com.crs.echo; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.*; import java.util.Iterator; import java.util.Set; /** * @author 刘小江 * @version 1.0 * @date 2021/8/10 23:26 **/ public class NIOServer implements Server{ @Override public void start(int port) throws IOException { // 初始化服务器socket通道 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); // 绑定主机端口 serverSocketChannel.socket().bind(new InetSocketAddress("localhost", port)); // 设置通道为非阻塞的 serverSocketChannel.configureBlocking(false); // 创建selector Selector selector =Selector.open(); // 将通道的连接事件注册到selector中 serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); // 通过无线循环处理连接 while (true){ // selector选择就绪事件,此处会阻塞 int num = selector.select(); if(num == 0){ continue; } // 获取到已准备就绪的通道的选择键值,返回的是一个就绪事件集合 Set<SelectionKey> selectKeys=selector.selectedKeys(); // 迭代处理就绪事件 Iterator iter = selectKeys.iterator(); while (iter.hasNext()){ SelectionKey key = (SelectionKey) iter.next(); iter.remove(); // 处理连接请求 if(key.isAcceptable()){ SocketChannel socketChannel = serverSocketChannel.accept(); socketChannel.configureBlocking(false); // 将客户端socket通道注册到读事件中 socketChannel.register(selector,SelectionKey.OP_READ); } // 处理读通道请求 else if(key.isReadable()){ SocketChannel channel = (SocketChannel) key.channel(); // 创建缓冲区默认为写模式 ByteBuffer buffer = ByteBuffer.allocate(1024); // 将通道中的数据读取到缓冲区中 channel.read(buffer); // 翻转缓冲区将缓冲区切换为读模式 buffer.flip(); byte[] bts = new byte[buffer.limit()]; buffer.get(bts); System.out.println("接收到的前端数据为"+new String(bts,"utf8")); // 构建返回的数据 // 构建请求头响应码 String head1="HTTP/1.0 200 OK"+"\r\n"; // 构建请求头返回数据格式 String head2="Content-Type:application/json"+"\r\n"; // 构建返回的数据为前端请求的数据 String content = new String(bts,"utf8"); String resStr = head1 +"\r\n"+ head2+"\r\n"+content; buffer = ByteBuffer.allocate(resStr.length()); buffer.put(head1.getBytes()); buffer.put(head2.getBytes()); // http协议中响应头和响应数据中有一个空行 buffer.put("\r\n".getBytes()); buffer.put(content.getBytes()); buffer.flip(); channel.write(buffer); channel.close(); } } } } }
AIO
package com.crs.echo; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.AsynchronousServerSocketChannel; import java.nio.channels.AsynchronousSocketChannel; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; /** * @author 刘小江 * @version 1.0 * @date 2021/8/15 13:47 **/ public class AIOServer implements Server{ @Override public void start(int port) throws IOException { // 打开aio套接字通道 AsynchronousServerSocketChannel serverSocketChannel = AsynchronousServerSocketChannel.open(); // 绑定端口 serverSocketChannel.bind(new InetSocketAddress(port)); System.out.println("服务器打开连接,端口为:"+port+",等待客户端的连接"); while (true){ // 服务器等待连接 Future<AsynchronousSocketChannel> accept = serverSocketChannel.accept(); try { AsynchronousSocketChannel socketChannel = accept.get(); System.out.println("服务器与" + socketChannel.getRemoteAddress() + "建立连接"); ByteBuffer buffer = ByteBuffer.allocate(1024); Future<Integer> read = socketChannel.read(buffer); while (!read.isDone()){ // 此处可以继续执行其他的业务逻辑 Thread.sleep(10); } // 数据准备好后线程继续处理网络数据 buffer.flip(); byte[] bts = new byte[buffer.limit()]; buffer.get(bts); System.out.println("接收到的前端数据为"+new String(bts,"utf8")); // 构建返回的数据 // 构建请求头响应码 String head1="HTTP/1.0 200 OK"+"\r\n"; // 构建请求头返回数据格式 String head2="Content-Type:application/json"+"\r\n"; // 构建返回的数据为前端请求的数据 String content = new String(bts,"utf8"); String resStr = head1 +"\r\n"+ head2+"\r\n"+content; buffer = ByteBuffer.allocate(resStr.length()); buffer.put(head1.getBytes()); buffer.put(head2.getBytes()); // http协议中响应头和响应数据中有一个空行 buffer.put("\r\n".getBytes()); buffer.put(content.getBytes()); buffer.flip(); Future<Integer> write = socketChannel.write(buffer); while (!write.isDone()){ Thread.sleep(0); } socketChannel.close(); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } } }
BIO指的是同步阻塞IO,就是在读取数据时必须阻塞,一个线程只能处理一个连接,要实现多连接时就必须利用多线程的方式来处理,但是在IO的这一块的处理只能是阻塞的方式,不能高效的利用计算机资源,且连接数过多创建更多线程时也需要消耗大量计算机资源
NIIO指的是同步非阻塞IO,在socket建立连接时不需要阻塞,通过selector选择来轮询就绪事件,多个连接无需再创建多个线程,读取数据方面通过通道channel和缓冲区bytebuffer来读取不用进行数据阻塞,在内核交互角度来说BIO和NIO都是同步的,在编程角度来说同步阻塞的IO的操作就是执行连接和读取数据都只能顺序完成,在执行IO操作时程序不能执行其他事情只能等待,而使用非阻塞的操作则在内核执行IO操作时应用程序可以先做其他事情
AIO指的是异步IO,异步IO下不再是应用程序去向系统内核要数据了,而是内核将数据准备好之后通知应用进程,因此在执行过程中也就没有了阻塞的概念,异步IO在程序代码上的体现就是用socketChannel.read读取数据时是一个异步操作,内核立即返回给你一个结果,数据没有准备好时你可以先做其他事情,内核告诉你数据准备好了你再继续处理
需要强调的是我们的java程序和操作系统之间还隔着java虚拟机,在理解IO模型的时候需要考虑IO模型是针对jvm的操作模型,而jvm封装对IO模型的使用并提供给我们比较方便使用的api