1.BIO基本介绍BIO是传统的Java IO编程,其基本的类和接口在java.io包中 BIO(blocking I/O):同步阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销 BIO方式使用于连接数目比较小且固定的架构,这种服务方式对服务器资源要求比价高,并且局限于应用中,JDK1.4以前的唯一选择,程序简单易理解 BIO基本模型:
2.NIO基本介绍 NIO全称 java non-blocking IO。从JDK 1.4开始,java提供了一些列改进的输入/输出(I/O)的新特性,被称为NIO,是同步非阻塞的 NIO相关类都被放在java.nio包及其子包下 NIO三大核心部分:Channel(通道),Buffer(缓冲区),Selector(选择器) NIO是面向缓冲区的,或者面向块编程的。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区内前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞的高伸缩性网络 Java NIO的非阻塞模式,使一个线程从某通道发送或者读取数据,但是它仅能得到目前可用的数据,如果目前没有可用的数据时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可读取之前,该线程可以继续做其他事情。非阻塞就是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情 通俗来讲:NIO是可以做到用一个线程处理多个操作的。假设有10000个请求过来,根据实际情况,可以分配50或100个线程来处理。不想BIO一样需要分配10000个线程来处理 NIO基本模型:
Selector、Channel和Buffer对应关系:
每个Channel都会对应一个Buffer
Selector对应一个线程,一个线程对应多个Channel
程序切换到哪个Channel是有事件决定的,Event就是一个重要的概念
Buffer实际上就是一个地址块,底层是一个数组
数据的读取写入是通过buffer,BIO中要么为输入流要么为输出流,不能为双向。而NIO的buffer是可以双向的,需要filp方法切换,channel也是双向的。
其中Selector(选择器)的作用是循环监听多个客户端连接通道,如果通道中没有数据即客户端没有请求时它可以去处理别的通道或者做其他的事情,如果通道中有数据他就会选择这个通道然后进行处理,这就做到了一个线程处理多个连接。 3.BIO和NIO的区别 NIO以流的方式处理数据,NIO以块的方式处理数据,块IO的效率比流IO高很多。(比如说流IO他是一个流,你必须时刻去接着他,不然一些流就会丢失造成数据丢失,所以处理这个请求的线程就阻塞了他无法去处理别的请求,他必须时刻盯着这个请求防止数据丢失。而块IO就不一样了,线程可以等他的数据全部写入到缓冲区中形成一个数据块然后再去处理他,在这期间该线程可以去处理其他请求) BIO是阻塞的,NIO是非阻塞的 BIO基于字节流和字符流进行操作的,而NIO基于Channel(通道)和Buffer(缓冲区)进行操作的,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道事件,因此使用单个线程就可以监听多个客户端通道
缓冲区(Buffer):缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个容器对象(含数组),该对象提供了一组方法,可以更轻松地使用内存块,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel 提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由Buffer,如图:【后面举例说明】
下图为Buffer类中的字段:
属性 | 描述 |
---|---|
Capacity | 容量,表示最大容纳的最大数据量 |
Limit | 表示缓存区的当前终点,不能对缓存区超过极限的位置进行读写操作 |
Position | 位置,下一个要被读写的元素索引,每次读写缓冲器数据时会发生改变 |
Mark | 标记 |
Buffer类和子类
在NIO中,Buffer是一个顶层的父类,它是一个抽象类,如下图所示:
比较重要的Buffer的Api
NIO中的通道类似于流,但有些区别如下:
通道可以同时进行读写,而流只能读取或者只能写
通道可以实现异步读写数据
通道可以从缓冲读数据,也可以写数据到缓冲;
BIO中的stream是单向的,例如 FilelnputStream对 象只能进行读取数据的操作,而NIO中的通道 (Channel)是双向的,可以读操作,也可以写操作。
Channel在NIO中是一个接口 public interface Channel extends Closeable04)常用的 Channel 类有:FileChannel、 DatagramChannel、ServerSocketChannel和SocketChannel。【 ServerSocketChanne类似ServerSocket , SocketChannel 类似Socket】
FileChannel用于文件的数据读写,DatagramChannel用于UDP的数据读写, ServerSocketChannel 和 SocketChannel用于TCP的粉掘读写
当用户访问Server时channel执行如下:
客户端首先会请求服务端,服务端中会产生一个ServerSocketChannel然后在Channel中创建一个SocketChannel。这一过程实际上都是调用了ServerSocketChannel和SocketChannel的实现类。
public class NIOFileChannel01 { public static void main(String[] args) throws IOException { String context = "hello,lishuai,niochannel"; //创建一个输出流——>channel FileOutputStream fileOutputStream = new FileOutputStream("F:\\li.txt"); //通过fileOutputStream获取对应的FileChannel //这个fileChannel真实类型是FileChannelImpl FileChannel channel = fileOutputStream.getChannel(); //创建一个为byte类型的缓冲区 ByteBuffer byteBuffer = ByteBuffer.allocate(1024); //将context放入缓冲区中 byteBuffer.put(context.getBytes()); //对缓冲区进行反转 byteBuffer.flip(); //将缓冲区中的数据写入channel channel.write(byteBuffer); fileOutputStream.close(); } }
public class NIOFileChannel02 { public static void main(String[] args) throws IOException { String content = "F:\\li.txt"; FileInputStream fileInputStream = new FileInputStream(content); FileChannel channel = fileInputStream.getChannel(); ByteBuffer byteBuffer = ByteBuffer.allocate(1024); byteBuffer.get(content.getBytes()); byteBuffer.flip(); channel.read(byteBuffer); String c = new String(byteBuffer.array()); System.out.println(c); fileInputStream.close(); } }
public class NIOFileChannel03 { public static void main(String[] args) throws IOException { String str1 = "F:\\li.txt"; String str2 = "F:\\li2.txt"; FileInputStream fileInputStream = new FileInputStream(str1);//创建一个文件输入流 FileChannel channel1 = fileInputStream.getChannel();//使用输入流获取channel FileOutputStream fileOutputStream = new FileOutputStream(str2);//创建文件输出流 FileChannel channel2 = fileOutputStream.getChannel();//使用输出流获取channel ByteBuffer buffer = ByteBuffer.allocate(1024);//建立缓冲区 buffer.get(str1.getBytes());//获取 while (true){ buffer.clear();//清理buffer int read = channel1.read(buffer);//将buffer中的内容读取 if(read == -1){ break; } buffer.flip();//反转 channel2.write(buffer);//写入 } fileInputStream.close(); fileOutputStream.close(); } }
如果不加入buffer.clear();那么buffer中的position和limit字段就会冲突。如下图所示。
public class NIOFileChannel04 { public static void main(String[] args) throws Exception{ FileInputStream fileInputStream = new FileInputStream("C:\\Users\\Li\\Desktop\\新建文件夹\\1.jpg"); FileChannel channel1 = fileInputStream.getChannel(); FileOutputStream fileOutputStream = new FileOutputStream("C:\\Users\\Li\\Desktop\\新建文件夹\\1a.jpg"); FileChannel channel2 = fileOutputStream.getChannel(); /** * @param src 原始路径 * @param position 位置 * @param size 大小 */ channel2.transferFrom(channel1,0,channel1.size()); //关闭 channel1.close(); channel2.close(); fileInputStream.close(); fileOutputStream.close(); } }
ByteBuffer支持类型化的put和get,put放入什么类型的数据,get就应该使用对应的数据类型取出来,否则就会有BufferUnderFlowException异常。
public class Buffer01 { public static void main(String[] args) { ByteBuffer buffer = ByteBuffer.allocate(64); ByteBuffer p1 = buffer.putInt(1); ByteBuffer p2 = buffer.putChar('一'); ByteBuffer p3 = buffer.putDouble(20.0); ByteBuffer p4 = buffer.putLong(111111111); ByteBuffer p5 = buffer.putFloat(1); buffer.flip(); System.out.println(p1.getInt()); System.out.println(p2.getChar()); System.out.println(p3.getDouble()); System.out.println(p4.getLong()); System.out.println(p5.getFloat()); } }
在将Buffer指定为只读文件时,如进行put操作则会抛出异常[ReadOnlyBufferException]
public class Buffer02 { public static void main(String[] args) { ByteBuffer byteBuffer = ByteBuffer.allocate(64); for(int i =0;i3.NIO还提供了MappedByteBuffer,可以让文件直接在内存(堆外的内存)中进行修改,而如何同步到文件由NIO来完成。
//可以让文件直接在内存中修改,操作系统不需要再拷贝一次 public class MappedBufferTest{ public static void main(String[] args) throws IOException { RandomAccessFile randomAccessFile = new RandomAccessFile("C:\\Users\\Li\\Desktop\\新建文件夹\\1.txt","rw"); FileChannel channel = randomAccessFile.getChannel(); /** * @param MapMode mode, FileChannel.MapMode.READ_WRITE(使用的为读写模式) 模式 * @param long position,0 可以直接修改的起始位置 * @param long size, 5 是映射到内存的大小,即将1.txt的多少个字节映射到内存 */ MappedByteBuffer map = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5); //put的中修改的位置指的是下标 map.put(0,(byte) 'Z'); map.put(3,(byte)'O'); map.put(5,(byte) '9');//超出下标长度就会抛出IndexOutOfBoundsException randomAccessFile.close(); System.out.println("成功!"); } }1.4 Selector
基本介绍
Java的 NIO,用非阻塞的lO方式。可以用一个线程,处理多个的客户端连 接,就会使用到Selector(选择器)
Selector能够检测多个注册的通道上是否有事件发生(注意:多个Channel以 事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。【示意图】 3)只有在连接真正有读写事件发生时,才会进行读写,就大大地减少了系统 开销,并且不必为每个连接都创建一个线程,不用去维护多个线程 4)避免了多线程之间的上下文切换导致的开销
1.5NIO非阻塞网络编程原理分析图
NIO非阻塞网络编程相关的(Selector、SelectionKey、ServerSocketChannel和SocketChannel)关系梳理
上图说明:
当客户端连接时,会通过ServerSocketChannel得到SocketChannel
将SocketChannel注册到Seelctor上,register(Selector sel,int ops),一个selector上可以注册多个SocketChannel
注册后返回一个SelectionKey,会和该selector关联
Selector进行监听selct方法,返回有事件发送通道个数。
再获取具体方法的SelectionKey,从而反向获取SocketChannel并得到具体channel,再利用channel进行对应的操作