IO | NIO |
---|---|
阻塞式 | 非阻塞式、选择器selectors |
面向流:单向流动,直接将数据从一方流向另外一方 | 面向缓存:将数据放到缓存区中进行存取,经通道进行数据的传输 |
根据数据类型的不一样,提供了对应的类型缓冲区(boolean类型除外),每个Buffer类都是Buffer接口的一个实例。经过Buffer类.allocate()方法获取缓冲区;对缓冲区的数据进行操做可使用put方法和get方法。java
四个核心属性程序员
// Invariants: mark <= position <= limit <= capacity private int mark = -1; private int position = 0; private int limit; private int capacity;
经常使用方法(以ByteBuffer为例)数组
public static ByteBuffer allocateDirect(int capacity):分配一个直接缓冲区public static ByteBuffer allocate(int capacity):分配一个间接缓冲区缓存
当分配一个缓冲区时,capacity=capacity,mark=-1, position=0, limit=capacity,源码分析以下:服务器
public static ByteBuffer allocate(int capacity) { ... return new HeapByteBuffer(capacity, capacity); } // class HeapByteBuffer extends ByteBuffer HeapByteBuffer(int cap, int lim) { // 调用ByteBuffer的构造函数传入默认参数:mark=-1, position=0, limit=capacity super(-1, 0, lim, cap, new byte[cap], 0); }; // public abstract class ByteBuffer extends Buffer ByteBuffer(int mark, int pos, int lim, int cap, byte[] hb, int offset) { super(mark, pos, lim, cap); this.hb = hb; // final byte[] hb; this.offset = offset; // final int offset; } Buffer(int mark, int pos, int lim, int cap) { ... this.capacity = cap; limit(lim); // 设置limit position(pos); // 设置position if (mark >= 0) { ... this.mark = mark; } }
public final ByteBuffer put(byte[] src):将一个字节数组放入缓冲区。
每当放置一个字节时,position将会+1,保证position的值就是下一个可插入数据的buffer单元位置。源码分析以下:网络
public final ByteBuffer put(byte[] src) { return put(src, 0, src.length); } // 由allocate方法调用分配缓冲区可知,返回的是Buffer的实现类HeapByteBuffer对象 public ByteBuffer put(byte[] src, int offset, int length) { checkBounds(offset, length, src.length); // 检查是否下标越界 if (length > remaining()) // 检查是否超出了可操做的数据范围= limit-position throw new BufferOverflowException(); System.arraycopy(src, offset, hb, ix(position()), length); position(position() + length); // 重设position return this; }
public ByteBuffer get(byte[] dst):从缓冲区中读取数据到 dst中。应在 flip() 方法后调用。
获取数据,是在缓冲区字节数组中的position位置处开始,读取一次完毕后,并会记录当前读取的位置,即position,以便于下一次调用get方法继续读取。app
public ByteBuffer get(byte[] dst) { return get(dst, 0, dst.length); } // 调用HeapByteBuffer对象的get方法 public ByteBuffer get(byte[] dst, int offset, int length) { ... // 从缓冲区的字节数组final byte[] hb中,拷贝从 hb的 offset+position(注:offset=0) 处的长度为length的数据到 dst中 System.arraycopy(hb, ix(position()), dst, offset, length); position(position() + length); // 设置position return this; }
经过源码分析可知,当put操做后,position记录的是下一个可用的buffer单元,而get会从position位置处开始获取数据,这显然是没法得到的,所以须要从新设置 position, 即 flip()方法。dom
public final Buffer flip() :翻转缓冲区,在一个通道读取或PUT操做序列以后,调用此方法以准备一个通道写入或相对获取操做的序列
将此通道的缓冲区的界限设置为当前position,保证了有可操做的数据。异步
public final Buffer flip() { limit = position; position = 0; mark = -1; return this; }
public final Buffer mark():标记当前position
可用于在put操做转get操做时标记当前的position位置,以便于调用reset方法从该位置继续操做socket
public final Buffer mark() { mark = position; return this; }
public final Buffer reset():回到mark标记的位置
public final Buffer reset() { int m = mark; if (m < 0) throw new InvalidMarkException(); position = m; return this; }
public final Buffer clear():清除缓冲,重置初始化原始状态
public final Buffer clear() { position = 0; limit = capacity; mark = -1; return this; }
public final Buffer rewind():倒回,用于从新读取数据
public final Buffer rewind() { position = 0; mark = -1; return this; }
直接缓冲区与间接缓冲区
间接缓冲:经过allocate方法分配的缓冲区。当程序发起read请求获取磁盘文件时,该文件首先被OS读取到内核地址空间中,并copy一份原始数据传入JVM用户地址空间,再传给应用程序。增长了一个copy操做,致使效率下降。
直接缓冲:经过allocateDirecr方法分配的缓冲区,此缓冲区创建在物理内存中。直接在两个空间中开辟内存空间,建立映射文件,去除了在内核地址空间和用户地址空间中的copy操做,使得直接经过物理内存传输数据。虽然有效提升了效率,可是分配和销毁该缓冲区的成本高于间接缓冲,且对于缓冲区中的数据将交付给OS管理,程序员没法控制。
用于源节点与目标节点之间的链接,负责对缓冲区中的数据提供传输服务。
经常使用类
FileChannel:用于读取、写入、映射和操做文件的通道。
SocketChannel:经过 TCP 读写网络中的数据。
ServerSocketChannerl:经过 UDP 读写网络中的数据通道。
DatagramChannel:经过 UDP 读写网络中的数据通道。
本地IO:FileInputStream、FileOutputStream、RandomAccessFile
网络IO:Socket、ServerSocket、DatagramSocket
获取Channel方式(以FileChannel为例)
1. Files.newByteChannel工具类静态方法
2. getChannel方法:经过对象动态获取,使用间接缓冲区。
FileInputStream fis = new FileInputStream(ORIGINAL_FILE); FileOutputStream fos = new FileOutputStream(OUTPUT_FILE); // 获取通道 FileChannel inChannel = fis.getChannel(); FileChannel outChannel = fos.getChannel(); // 提供缓冲区(间接缓冲区) ByteBuffer buffer = ByteBuffer.allocate(1024); while (inChannel.read(buffer) != -1) { buffer.flip(); outChannel.write(buffer); buffer.clear(); }
3. 静态open方法:使用open获取到的Channel通道,使用直接缓冲区。
FileChannel inChannel = FileChannel.open(Paths.get(ORIGINAL_FILE), StandardOpenOption.READ); FileChannel outChannel = FileChannel.open(Paths.get(OUTPUT_FILE), StandardOpenOption.READ, StandardOpenOption.CREATE, StandardOpenOption.WRITE); // 使用物理内存 内存映射文件 MappedByteBuffer inBuffer = inChannel.map(MapMode.READ_ONLY, 0, inChannel.size()); MappedByteBuffer outBuffer = outChannel.map(MapMode.READ_WRITE, 0, inChannel.size()); byte[] dst = new byte[inBuffer.limit()]; inBuffer.get(dst); outBuffer.put(dst);
// 使用DMA 直接存储器存储 inChannel.transferTo(0, inChannel.size(), outChannel); outChannel.transferFrom(inChannel, 0, inChannel.size());
public static FileChannel open(Path path, OpenOption... options):从path路径中以某种方式获取文件的Channel
StandardOpenOption | 描述 |
---|---|
CREATE | 建立一个新的文件,若是存在,则覆盖。 |
CREATE_NEW | 建立一个新的文件,若是该文件已经存在则失败。 |
DELETE_ON_CLOSE | 关闭时删除。 |
DSYNC | 要求将文件内容的每次更新都与底层存储设备同步写入。 |
READ | 读方式 |
SPARSE | 稀疏文件 |
SYNC | 要求将文件内容或元数据的每次更新都同步写入底层存储设备。 |
TRUNCATE_EXISTING | 若是文件已经存在,而且打开 wirte访问,则其长度将截断为0。 |
WRITE | 写方式 |
APPEND | 若是文件以wirte访问打开,则字节将被写入文件的末尾而不是开头。 |
public abstract MappedByteBuffer map(MapMode mode, long position, long size):将通道的文件区域映射到内从中。当操做较大的文件时,将数据映射到物理内存中才是值得的,由于映射到内存是须要开销的。
FileChannel.MapMode | 描述 |
---|---|
PRIVATE | 专用映射模式(写入时拷贝) |
READ_ONLY | 只读模式 |
READ_WRIT | 读写模式 |
public abstract long transferFrom(ReadableByteChannel src, long position, long count):从给定的可读取通道src,传输到本通道中。直接使用直接存储器(DMA)对数据进行存储。public abstract long transferTo(long position, long count, WritableByteChannel target):将本通道的文件传输到可写入的target通道中。
分散(Scatter)与汇集(Gather)
分散读取:将通道中的数据分散到多个缓冲区中。 public final long read(ByteBuffer[] dsts)
汇集写入:将多个缓冲区中的数据汇集到一个Channel通道中。public final long write(ByteBuffer[] srcs)
字符集(Charset)
public final ByteBuffer encode(CharBuffer cb):编码public final CharBuffer decode(ByteBuffer bb):解码
阻塞是相对网络传输而言的。传统的IO流都是阻塞的,在网络通讯中,因为 IO 阻塞,须要为每个客户端建立一个独立的线程来进行数据传输,性能大大下降;而NIO是非阻塞的,当存在空闲线程时,能够转去操做其余通道,所以没必要非要建立一个独立的线程来服务每个客户端请求。
选择器(Selector)
SelectableChannle对象的多路复用器,可同时对多个SelectableChannle对象的 IO 状态监听,每当建立一个Channel时,就向Selector进行注册,交由Selector进行管理,只有Channel准备就绪时,Selector可会将任务分配给一个或多个线程去执行。Selector能够同时管理多个Channel,是非阻塞 IO 的核心。
NIO 阻塞式
服务器Server不断监听客户端Client的请求,当创建了一个Channel时,服务器进行read操做,接收客户端发送的数据,只有当客户端断开链接close,或者执行shutdownOutput操做时,服务器才知晓没有数据了,不然会一直进行read操做;当客户端在read操做获取服务器的反馈时,若服务器没有关闭链接或者shutdownInput时也会一直阻塞。示例代码以下:
static final String ORIGINAL_FILE = "F:/1.png"; static final String OUTPUT_FILE = "F:/2.jpg";
public void server() throws Exception { // 打开TCP通道,绑定端口监听 ServerSocketChannel serverChannel = ServerSocketChannel.open(); serverChannel.bind(new InetSocketAddress(9988)); ByteBuffer buf = ByteBuffer.allocate(1024); // 获取链接 SocketChannel accept = null; while ((accept= serverChannel.accept()) != null) { FileChannel fileChannel = FileChannel.open( Paths.get(OUTPUT_FILE), StandardOpenOption.CREATE, StandardOpenOption.WRITE); // 读取客户端的请求数据 while (accept.read(buf) != -1) { buf.flip(); fileChannel.write(buf); buf.clear(); } // 发送执行结果 buf.put("成功接收".getBytes()); buf.flip(); accept.write(buf); buf.clear(); fileChannel.close(); // 关闭链接,不然客户端会一直等待读取致使阻塞,可以使用shutdownInput,但任务已结束,该close accept.close(); } serverChannel.close(); }
public void client() throws Exception { // 打开一个socket通道 SocketChannel clientChannel = SocketChannel.open( new InetSocketAddress("127.0.0.1", 9988)); // 建立缓冲区和文件传输通道 FileChannel fileChannel = FileChannel.open(Paths.get(ORIGINAL_FILE), StandardOpenOption.READ); ByteBuffer buf = ByteBuffer.allocate(1024); while ( fileChannel.read(buf) != -1) { buf.flip(); clientChannel.write(buf); buf.clear(); } // 关闭输出(不关闭通道),告知服务器已经发送完毕,去掉下面一行代码服务区将一直读取致使阻塞 clientChannel.shutdownOutput(); int len = 0; while ((len = clientChannel.read(buf)) != -1) { buf.flip(); System.out.println(new String(buf.array(), 0, len)); buf.clear(); } fileChannel.close(); clientChannel.close(); }
NIO 非阻塞式
经过在通道Channel中调用configureBlocking将blocking设置为false,让Channel能够进行异步 I/O 操做。
public void client() throws Exception { // 打开一个socket通道 SocketChannel clientChannel = SocketChannel.open( new InetSocketAddress("127.0.0.1", 9988)); ByteBuffer buf = ByteBuffer.allocate(1024); // 告知服务器,已经发送完毕 // clientChannel.shutdownOutput(); // 设置非阻塞 clientChannel.configureBlocking(Boolean.FALSE); buf.put("哈哈".getBytes()); buf.flip(); clientChannel.write(buf); clientChannel.close(); }
public void server() throws Exception { // 打开TCP通道,绑定端口监听 ServerSocketChannel serverChannel = ServerSocketChannel.open(); serverChannel.configureBlocking(Boolean.FALSE); serverChannel.bind(new InetSocketAddress(9988)); // 建立一个Selector用于管理Channel Selector selector = Selector.open(); // 将服务器的Channel注册到selector中,并添加 OP_ACCEPT 事件,让selector监听通道的请求 serverChannel.register(selector, SelectionKey.OP_ACCEPT); // 一直判断是否有已经准备就绪的Channel while (selector.select() > 0) { // 存在一个已经准备就绪的Channel,获取SelectionKey集合中获取触发该事件的全部key Iterator<SelectionKey> keys = selector.selectedKeys().iterator(); while (keys.hasNext()) { SelectionKey sk = keys.next(); SocketChannel accept = null; ByteBuffer buffer = null; // 针对不一样的状态进行操做 if (sk.isAcceptable()) { // 可被链接,设置非阻塞并注册到selector中 accept = serverChannel.accept(); accept.configureBlocking(Boolean.FALSE); accept.register(selector, SelectionKey.OP_READ); } else if (sk.isReadable()) { // 可读,获取该选择器上的 Channel进行读操做 accept = (SocketChannel) sk.channel(); buffer = ByteBuffer.allocate(1024); int len = 0; while ((len = accept.read(buffer)) != -1) { buffer.flip(); System.out.println(new String(buffer.array(), 0, len)); buffer.clear(); } } } // 移除本次操做的SelectionKey keys.remove(); } serverChannel.close(); }
方法使用说明
Pipe管道
Channel都是双向通道传输,而Pipe就是为了实现单向管道传送的通道对,有一个source通道(Pipe.SourceChannel)和一个sink通道(Pipe.SinkChannel)。sink用于写数据,source用于读数据。直接使用Pipe.open()获取Pipe对象,操做和FileChannel同样。