Java NIO的原理和使用
NIO是面向缓存的非阻塞IO模型,其有三大核心组件:Buffer、Channel、Selector,如下图:
原理都好理解,接下来从Java api来看下三大核心组件的简单使用。
1、Buffer
Buffer有几大子类:ByteBuffer(最常用)、ShortBuffer、CharBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer。
Buffer底层维护一个数组,由四个重要参数:
mark(标记)
limit(最大可读或可写的位置)
public static void main(String[] args) { IntBuffer intBuffer = IntBuffer.allocate(5); for (int i=0;i<intBuffer.capacity();i++){ intBuffer.put(i*2); } //读写切换 intBuffer.flip(); while (intBuffer.hasRemaining()){ System.out.println(intBuffer.get()); } }
输出:0、2、4、6、8
Buffer特性:
(1)Buffer支持类型化的put和get
private static void type(){ ByteBuffer byteBuffer = ByteBuffer.allocate(64); byteBuffer.putInt(100); byteBuffer.putLong(3); byteBuffer.putChar('哈'); byteBuffer.flip(); System.out.println(byteBuffer.getInt()); System.out.println(byteBuffer.getLong()); System.out.println(byteBuffer.getChar()); }
但是如果put或是get时超出的Buffer的容量,会抛出java.nio.BufferOverflowException异常。
(2)Buffer可以设置为只读模式
(3)MappedByteBuffer
MappedByteBuffer可以使文件直接在内存(堆外内存)中修改,减少了内核空间和用户空间之间的数据拷贝来提升效率。
private static void mappingBuffer(){ try { RandomAccessFile randomAccessFile = new RandomAccessFile("/Users/jijingyi/Desktop/test.txt","rw"); FileChannel channel = randomAccessFile.getChannel(); /** * 参数1:FileChannel.MapMode.READ_WRITE 表示使用读写模式 * 参数2:文件可以直接在内存中修改的起始位置 * 参数3:这个5指的是可修改的字节长度,即从下标0-4,修改超出下标4会报 IndexOutOfBoundsExpection */ MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE,0,5); mappedByteBuffer.put(0,(byte)'H'); mappedByteBuffer.put(3,(byte)'L'); randomAccessFile.close(); } catch (Exception e) { e.printStackTrace(); } }
(4)Buffer的分散和聚集
/** * scatter(分散):将数据写入到buffer时,可以使用数组,依次写入 * gatter(聚集):从buffer读取数据时,可以使用数组,依次读取 */ private static void serverBoost(){ try { //创建服务端 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); InetSocketAddress inetSocketAddress = new InetSocketAddress(8888); //绑定端口 serverSocketChannel.socket().bind(inetSocketAddress); //监听连接请求 SocketChannel socketChannel = serverSocketChannel.accept(); //缓存数组 ByteBuffer[] byteBuffers = new ByteBuffer[2]; byteBuffers[0] = ByteBuffer.allocate(6); byteBuffers[1] = ByteBuffer.allocate(4); int msgLength = 10; //设定最多写10个字节 while (true){ int readByte = 0; while (readByte < msgLength){ long l = socketChannel.read(byteBuffers); readByte += l; //输出一下每个buffer的position和limit Arrays.asList(byteBuffers).stream().map(buffer -> "position="+buffer.position()+",limit="+buffer.limit()).forEach(System.out::println); } //buffer读写反转 Arrays.asList(byteBuffers).forEach(buffer -> buffer.flip()); //再将客服端发送的数据写回去 int writeByte = 0; while (writeByte < msgLength){ long l = socketChannel.write(byteBuffers); writeByte += l; } //清空缓存,这里并不是把缓存中的数据删除了,而是重置position和limit Arrays.asList(byteBuffers).forEach(buffer -> buffer.clear()); } } catch (Exception e) { e.printStackTrace(); } }
2、Channel
NIO的channel类似于BIO的stream,但是区别是:
• channel既可以从buffer中读,也可以向buffer中写,是双向的,而stream是单向的
• channel可以实现异步读写
常用的Channel类:FileChannel(文件读写)、DatagramChannel(UDP协议)、ServerSocketChannel和SocketChannel(TCP协议)
我们来看一下FileChannel的几个api案例,ServerSocketChannel和SocketChannel在介绍netty的时候再说。
(1)文件读取
private static void readData(){ try { File file = new File("/Users/jijingyi/Desktop/test.txt"); FileInputStream inputStream = new FileInputStream(file); FileChannel fileChannel = inputStream.getChannel(); ByteBuffer byteBuffer = ByteBuffer.allocate((int)file.length()); fileChannel.read(byteBuffer); System.out.println(new String(byteBuffer.array())); inputStream.close(); } catch (Exception e) { e.printStackTrace(); } }
(2)文件写入
private static void writeData(){ String str = "hello world"; //创建一个输出流 try { //FileOutputStream中持有一个channel属性 FileOutputStream outputStream = new FileOutputStream("/Users/jijingyi/Desktop/test.txt"); FileChannel fileChannel = outputStream.getChannel(); ByteBuffer byteBuffer = ByteBuffer.allocate(1024); byteBuffer.put(str.getBytes()); byteBuffer.flip(); //注意读和写是相对缓冲区来说的 fileChannel.write(byteBuffer); outputStream.close(); } catch (Exception e) { e.printStackTrace(); } }
(3)实现文件拷贝
private static void copyFile(){ FileInputStream inputStream = null; FileOutputStream outputStream = null; try { inputStream = new FileInputStream("/Users/jijingyi/Desktop/test.txt"); FileChannel fileChannel01 = inputStream.getChannel(); outputStream = new FileOutputStream("test.txt"); FileChannel fileChannel02 = outputStream.getChannel(); ByteBuffer byteBuffer = ByteBuffer.allocate(5); while (true){ //如果文件读完返回-1 int read = fileChannel01.read(byteBuffer); if (read == -1){ break; } //读写反转 byteBuffer.flip(); fileChannel02.write(byteBuffer); //这里写完要重置channel,否则会由于position=limit出现死循环(因为再次读取时read会一直等于0) byteBuffer.clear(); } } catch (Exception e) { e.printStackTrace(); } finally { try { if (inputStream != null){ inputStream.close(); } if (outputStream != null){ outputStream.close(); } }catch (Exception e){ e.printStackTrace(); } } }
(4)使用transfFrom实现文件拷贝
private static void copyFileByTransf(){ FileInputStream inputStream = null; FileOutputStream outputStream = null; try { inputStream = new FileInputStream("/Users/jijingyi/Desktop/17岁.mp3"); FileChannel channelSrc = inputStream.getChannel(); outputStream = new FileOutputStream("/Users/jijingyi/Desktop/100岁.mp3"); FileChannel channelDest = outputStream.getChannel(); channelDest.transferFrom(channelSrc,0,channelSrc.size()); } catch (Exception e) { e.printStackTrace(); } finally { try { if (inputStream != null){ inputStream.close(); } if (outputStream != null){ outputStream.close(); } }catch (Exception e){ e.printStackTrace(); } } }
3、Selector
Selector就是选择器,也叫多路复用器,可以同时并发处理多个连接(就是监听连接对应的channel,通过事件机制来触发响应的操作)。
3.1、Selector常用的方法
public static Selector open(); //获得一个选择器对象
public int select(); //监听所有注册的通道,将有事件发生的channel对应的selectionKey加入到集合中,返回有事件触发的通道的个数。这个方法是阻塞的。
public int select(long timeout); //作用同select,但是带超时时间
public int selectNow(); //作用同select,但是不阻塞,不管是否有可处理的channel都返回
public Selector wakeup(); //立刻唤醒选择器对象
public Set<SelectionKey> selectedKeys(); //返回所有有事件触发的selectionKey的集合
3.2、NIO非阻塞网络编程原理
NIO非阻塞网络编程主要涉及4个核心类:Selector、SelectionKey、ServerSocketChannel、SocketChannel,原理如下图
对上图的几点说明:
• 当有客户端连接时,会通过 ServerSocketChannel 得到 SocketChannel
• Selector 通过 select() 进行监听,返回有事件发生的通道个数
• 将 SocketChannel 通过 register(Selector sel, int ops) 注册到Selector上,一个 Selector 上可以注册多个通道
• 注册后返回一个 selectionKey ,它会和该 Selector 关联(Selector 维护一个 selectionKey 的集合)
• 进一步得到各个有事件发生的 selectionKey
• SelectionKey 通过 channel() 反向获取 SocketChannel
• 最后通过得到的 channel 处理相应的事件
NIOServer:
private static void startNioServer(){ try { //创建服务端 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); InetSocketAddress inetSocketAddress = new InetSocketAddress(8888); serverSocketChannel.socket().bind(inetSocketAddress); //设置为非阻塞,否则会报 IllegalBlockingModeException 异常 serverSocketChannel.configureBlocking(false); //创建选择器对象 Selector selector = Selector.open(); //将服务端通道注册到Selector serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); while (true){ //Selector监听所有注册的通道,这里选择带超时的监听(这里是阻塞的),返回的是有事件触发的通道个数 if (selector.select(5000) == 0){ System.out.println("服务器等待5秒,没有连接"); continue; } //获取有事件触发的通道关联的 SelectionKey,然后通过 SelectionKey 反向获取 SocketChannel Set<SelectionKey> selectionKeys = selector.selectedKeys(); //遍历集合 Iterator<SelectionKey> iterator = selectionKeys.iterator(); while (iterator.hasNext()){ SelectionKey key = iterator.next(); //客户端请求连接事件 if (key.isAcceptable()){ //注意这里的 accept() 虽然是阻塞的,但是因为已经明确了是连接事件,所以会立刻执行 SocketChannel socketChannel = serverSocketChannel.accept(); System.out.println("客户端连接成功,生成SocketChannel=" + socketChannel.hashCode()); //设置为非阻塞 socketChannel.configureBlocking(false); //将客户端通道注册到 Selector,并关联一个 buffer socketChannel.register(selector,SelectionKey.OP_READ, ByteBuffer.allocate(1024)); } //读事件 else if (key.isReadable()){ //通过 SelectionKey 反向获取 SocketChannel SocketChannel socketChannel = (SocketChannel)key.channel(); //获取关联的 buffer ByteBuffer buffer = (ByteBuffer)key.attachment(); //读取数据客户端发送的数据 socketChannel.read(buffer); System.out.println("客户端发送数据:" + new String(buffer.array())); } //手动从集合中删除 SelectionKey,避免重复操作 iterator.remove(); } } } catch (Exception e) { e.printStackTrace(); } }
NIOClient:
private static void nioClient(){ try { //创建一个 SocketChannel SocketChannel socketChannel = SocketChannel.open(); //设置为非阻塞 socketChannel.configureBlocking(false); //提供服务端 ip和port,并连接服务端 InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1",8888); socketChannel.connect(inetSocketAddress); //这里是非阻塞的,如果还没有连接成功,可以处理其他业务 if (!socketChannel.isConnected()){ while (!socketChannel.finishConnect()){ System.out.println("客户端还未完成连接,处理其他业务"); } } //向服务端发送数据 String data = "哈哈哈"; ByteBuffer byteBuffer = ByteBuffer.wrap(data.getBytes()); socketChannel.write(byteBuffer); System.in.read(); } catch (Exception e) { e.printStackTrace(); } }
这段程序有一个问题:客户端如果不加 System.in.read(),服务端会一直触发读事件,一直打印客户端发送的数据,应该是服务端代码有点问题。