NIO基本介绍:


buffer:
public class BasicBuffer {public static void main(String[] args) {//举例说明buffer的使用(简单说明)//创建一个bufferIntBuffer intBuffer = IntBuffer.allocate(5); //可以存放5个int//向buffer中存放数据for (int i = 0; i < intBuffer.capacity(); i++) {intBuffer.put(i + 1);}//将buffer进行读写切换intBuffer.flip();//取数据while (intBuffer.hasRemaining()) { //是否还存在数据//get操作维护一个索引,每次调用索引后移一位System.out.println(intBuffer.get());}}}
NIO和BIO比较:
1.BIO以流的方式处理数据,而NIO以块的方式处理数据,块IO的效率比流IO高很多
2.BIO是阻塞的,NIO是非阻塞的
3.BIO基于字节流和字符流进行操作,而NIO基于Channel和Buffer进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入通道中。Selector(选择器)用于监听多个通信的时间(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道
描述NIO 的 Selector 、Channel 和 Buffer 的关系

1.每个channel 都会对应一个Buffer
2.Selector 对应一个线程, 一个线程对应多个channel(连接)
3.该图反应了有三个channel 注册到 该selector //程序
4.程序切换到哪个channel 是有事件决定的, Event 就是一个重要的概念
5.Selector 会根据不同的事件,在各个通道上切换
6.Buffer 就是一个内存块 , 底层是有一个数组
7.数据的读取写入是通过Buffer, 这个和BIO , BIO 中要么是输入流,或者是输出流, 不能双向,但是NIO的Buffer是可以读也可以写, 需要 flip 方法切换
8.channel 是双向的, 可以返回底层操作系统的情况, 比如Linux , 底层的操作系统通道就是双向的.
缓冲区(Buffer)
最常用的是ByteBuffer(二进制数据)
缓冲区:缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个容器对象(含数组),该对象提供了一组方法,可以更轻松的使用内存块,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况,Channel提供文件,网络读取数据的渠道,但是读取或写入的数据必须经过Buffer

缓冲区中的四个属性含义:
常用API:
public abstract class Buffer {//JDK1.4时,引入的apipublic final int capacity()//返回此缓冲区的容量public final int position()//返回此缓冲区的位置public final Buffer position(int newPositio)//设置此缓冲区的位置public final int limit()//返回此缓冲区的限制public final Buffer limit(int newLimit)//设置此缓冲区的限制public final Buffer mark()//在此缓冲区的位置设置标记public final Buffer reset()//将此缓冲区的位置重置为以前标记的位置public final Buffer clear()//清除此缓冲区, 即将各个标记恢复到初始状态,但是数据并未真正擦除public final Buffer flip()//反转此缓冲区public final Buffer rewind()//重绕此缓冲区public final int remaining()//返回当前位置与限制之间的元素数public final boolean hasRemaining()//告知在当前位置和限制之间是否有元素public abstract boolean isReadOnly();//告知此缓冲区是否为只读缓冲区// JDK1.6时引入的apipublic abstract boolean hasArray();//告知此缓冲区是否具有可访问的底层实现数组public abstract Object array();//返回此缓冲区的底层实现数组public abstract int arrayOffset();//返回此缓冲区的底层实现数组中第一个缓冲区元素的public abstract boolean isDirect();//告知此缓冲区是否为直接缓冲区}
clear()方法用于写模式,其作用为情况Buffer中的内容,所谓清空是指写上限与Buffer的真实容量相同,即limit==capacity,同时将当前写位置置为最前端下标为0处
rewind()在读写模式下都可用,它单纯的将当前位置置0,同时取消mark标记,仅此而已;也就是说写模式下limit仍保持与Buffer容量相同,只是重头写而已;读模式下limit仍然与rewind()调用之前相同,也就是为flip()调用之前写模式下的position的最后位置,flip()调用后此位置变为了读模式的limit位置,即越界位置
flip()函数的作用是将写模式转变为读模式,即将写模式下的Buffer中内容的最后位置变为读模式下的limit位置,作为读越界位置,同时将当前读位置置为0,表示转换后重头开始读,同时再消除写模式下的mark标记
通道(Channel)
1.NIO通道类似于流,区别如下:
通道可以同时进行读写,而流只能读或写
通道可以实现异步读写数据
通道可以从缓冲区读数据,也可以写数据到缓冲区

2.BIO中的Stream是单向的,例如FileInputStream对象只能进行读取数据的操作,而NIO中的通道(Channel)是双向的,可以读操作,也可以写操作
3.Channel在NIO中是一个接口
public interface Channel extends CCloseable{}
4.常用的Channel类有:FileChannel、DatagramChannel、ServerSocketChannel和SocketChannel
5.FileChannel用于文件的数据读写,DatagramChannel用于UDP的数据读写,ServerSocketChannel和SocketChannel用于TCP的数据读写
API:

利用Buffer和Channel操作文件的两个Demo
/*** 将数据写入本地文件*/public static void write() throws Exception {String str = "hello";//创建输出流 --> ChannelFileOutputStream fos = new FileOutputStream("f:\\01.txt");//获取对应的FileChannelFileChannel fileChannel = fos.getChannel();//创建缓冲区ByteBuffer byteBuffer = ByteBuffer.allocate(1024);//将输入放入到buffer中byteBuffer.put(str.getBytes());byteBuffer.flip(); //读写切换//将bytebuffer中的数据写入到channel中fileChannel.write(byteBuffer);fos.close();}
/*** 从文件中读取数据打印到控制台*/public static void read() throws Exception {//创建文件的输入流File file = new File("f:\\01.txt");FileInputStream fis = new FileInputStream(file);//通过fileInputStream获取对应的FileChannelFileChannel fisChannel = fis.getChannel();//创建缓冲区ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length());//将通道中的数据写入到bufferfisChannel.read(byteBuffer);//将bytebuffer中的字节数据转化为字符串输出System.out.println(new String(byteBuffer.array()));fis.close();}
/*** 将文件复制1*/public static void copy1() throws Exception {FileInputStream fis = new FileInputStream("f:\\01.txt");FileChannel fisChannel = fis.getChannel();FileOutputStream fos = new FileOutputStream("f:\\02.txt");FileChannel fosChannel = fos.getChannel();ByteBuffer byteBuffer = ByteBuffer.allocate(521);while (true) {byteBuffer.clear(); //清空buffer,防止一次没读完int read = fisChannel.read(byteBuffer);if (read == -1) break;//将buffer中的数据写到fosChannel中byteBuffer.flip();fosChannel.write(byteBuffer);}fis.close();fos.close();}
/*** 文件复制2(api)*/public static void copy2() throws Exception {//创建相关的流FileInputStream fis = new FileInputStream("f:\\01.txt");FileOutputStream fos = new FileOutputStream("f:\\03.txt");//获取对应的channelFileChannel fisChannel = fis.getChannel();FileChannel fosChannel = fos.getChannel();//使用TransferFormfosChannel.transferFrom(fisChannel, 0, fisChannel.size());//关闭资源fis.close();fos.close();}
关于Buffer和Channel的使用细节
1.ByteBuffer支持类型化的put和get,put放入的是什么数据类型,get就应该使用想应的数据类型,否则可能会有BufferUnderflowException异常
2.可以将一个普通的Buffer转换成只读Buffer
ByteBuffer readOnlyBuffer = buffer.asReadOnlyBuffer();
3.NIO还提供了MappedByteBuffer,可以让文件直接在内存中进行修改
public static void main(String[] args) throws Exception {RandomAccessFile randomAccessFile = new RandomAccessFile("f:\\01.txt", "rw");//获取信道FileChannel fisChannel = randomAccessFile.getChannel();/** 参数一:使用读写模式* 参数二:0代表可以直接修改起始位置* 参数三:映射到内存的大小(即最多可将文件的5个字节映射到内存中) (范围是0-5)* */MappedByteBuffer mappedByteBuffer =fisChannel.map(FileChannel.MapMode.READ_WRITE, 0, 5);mappedByteBuffer.put(0, (byte) 'a');mappedByteBuffer.put(1, (byte) 'b');randomAccessFile.close();}
4.NIO还支持多个Buffer(数组)完成读写操作,即Scattering和Gatering
public static void test1() throws Exception {/*Scattering:将数据写入到buffer时可以采用buffer数组 [分散]Gathering:从buffer读取数据时可以采用buffer数组依次读 [聚合]*///使用ServerSocketChannel和SocketChannelServerSocketChannel serverSocketChannel = ServerSocketChannel.open();InetSocketAddress inetSocketAddress = new InetSocketAddress(7000);//绑定端口到socketserverSocketChannel.socket().bind(inetSocketAddress);//创建buffer数组ByteBuffer[] byteBuffers = new ByteBuffer[2];byteBuffers[0] = ByteBuffer.allocate(5);byteBuffers[1] = ByteBuffer.allocate(3);//等待客户端连接SocketChannel socketChannel = serverSocketChannel.accept();int messageLength = 8; //假设从客户端接收8个字节//循环读取while (true) {int byteRead = 0;while (byteRead < messageLength) {long l = socketChannel.read(byteBuffers);byteRead++;System.out.println("byteRead=" + byteRead);//使用流打印, 看看当前的这个buffer的position 和 limitArrays.asList(byteBuffers).stream().map(buffer ->"postion=" + buffer.position() + ", limit="+ buffer.limit()).forEach(System.out::println);}//将所有的buffer进行读写转换Arrays.asList(byteBuffers).forEach(Buffer::flip);//将数据读取到客户端long byteWrite = 0;while (byteWrite < messageLength) {long l = socketChannel.write(byteBuffers);byteWrite++;}//将所有的buffer进行clear防止没读取完Arrays.asList(byteBuffers).forEach(Buffer::clear);System.out.println("byteRead:=" + byteRead +" byteWrite=" + byteWrite + ", messagelength" + messageLength);}}
Seletor(选择器)
基本介绍
1.JAVA的NIO,用非阻塞IO的方式,可以用一个线程,处理多个客户端连接,就会用到Seletor选择器
2.Selector能够检测多个注册的通道上是否有事件发生(注意:多个Channel以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然后对每个事件进行相应的处理,这样就可以只用一个线程去管理多个通道,也就是管理多个连接和请求
3.只有在连接真正有读写事件发生时,才会进行读写,就大大的减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程
4.避免了多线程之间的上下文切换导致的开销
Selector相关API:
1.NIO中的ServerSocketChannel功能类似于ServerSocket,SocketChannel功能类似于Socket
2.selector相关方法说明
selector.select() //阻塞
selector.select(1000) //阻塞1000ms,在1000ms后返回
selector.wakeup() //唤醒
selector.selecctNow() //不阻塞,立刻返回
NIO非阻塞网络编程原理

说明:
1.当客户端连接时,会通过ServerSocketChannel得到SocketChannel
2.Selector进行监听—select方法—返回有事件发生的Channel的个数
3.将socketChannel注册到Seletor上,reguster(Selector sel,int ops)方法-一个selector上可以注册多个SocketChannel
4.注册后返回一个SelectionKey,会和该Selector关联(集合)
5.进一步得到各个SelectionKey(有事件发生)
6.再通过SelectionKey反向获取SocketChannel —-channel()
7.可以通过得到的Channel,完成业务的处理
代码实现
public static void Test() throws Exception {//创建ServerSocketChannelServerSocketChannel serverSocketChannel = ServerSocketChannel.open();//得到Selector对象Selector selector = Selector.open();//绑定端口在服务器端监听serverSocketChannel.socket().bind(new InetSocketAddress(6666));//设置为非阻塞serverSocketChannel.configureBlocking(false);//把ServerSocketChannel注册到selector关心 事件为OP_ACCEPTserverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);//循环等待客户端连接while (true) {int select = selector.select(1000);if (select == 0) { //没有事件发生System.out.println("服务器等待了1s,无连接");continue;}//如果返回的大于0//获取到相关的selectionKey集合Set<SelectionKey> selectionKeys = selector.selectedKeys();Iterator<SelectionKey> iterator = selectionKeys.iterator();while (iterator.hasNext()) {//获取到SelectionKeySelectionKey key = iterator.next();//根据key 对应的通道发生的事件做相应的处理if (key.isAcceptable()) { //如果时OP_ACCEPT,有新的客户端来连接//给该客户端生成一个SocketChannelSocketChannel socketChannel = serverSocketChannel.accept();//设置为非阻塞socketChannel.configureBlocking(false);System.out.println("连接成功,生成了socketChannel" + socketChannel.hashCode());//将当前的socketChannel注册到selector上,关注读事件,并关联一个BuffersocketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));}if (key.isReadable()) { //读事件 OP_READ//通过key反向获取到对应的ChannelSocketChannel channel = (SocketChannel) key.channel();//获取到该channel关联的BufferByteBuffer buffer = (ByteBuffer) key.attachment();channel.read(buffer);System.out.println("form 客户端" + new String(buffer.array()));}//手动从集合中移除当前的SelectionKey,防止多线程出现重复操作iterator.remove();}}}
SelectionKey—-API
SelectionKey,表示Seletor和网络通道的注册关系
int OP_ACCEPT:表示有新的网络连接可以accept,值为16
int OP_CONNECT:代表连接已经建立,值为8
int OP_READ:代表读操作,值为1
int OP_WRITE:代表写操作,值为4
ServerSoketchannel
SocketChannel
网络IO通道,具体负责进行读写操作,NIO把缓冲区的数据写入通道,或者把通道里的数据读取到缓冲区
生活中NIO一个简单理解
不知道大家有没有用过2010年左右(或许更早)2G时代的手机,可以运行那种基于J2ME的QQ,能聊天,看个空间,偷个菜(文字版)什么的。这种手机一般都有个缺点就是不能后台运行,一旦去做其他事情(玩游戏,看小说等),QQ就掉线了,就不能收到QQ消息了。如果想要实时接收到女神消息,就要一直保持打开着QQ,不能去做其他事情。这就类似于BIO,阻塞的。
后来QQ出了一个手机业务,叫超级QQ(每月10块呢),可以伪实时在线,同时更快的升级(太阳月亮我的最爱)。之所以叫他伪实时在线,是因为它的实现方式是:当QQ收到消息时,腾讯会以短信的形式发到手机上,告诉你某某给你发消息了,请及时处理之类的(也可以直接回复短信,QQ上也会自动转发过去,不太相关暂时忽略)。此时再去登录QQ,就能立刻收到消息了。虽然手机同一时刻依然只能做一件事情,但是在没有QQ消息的时候也无需一直等待了,从而从容不铺去做别的事情。也就是非阻塞的了。
这个超级QQ的业务就像是NIO:人就是Selector,监听事件。短信就像是一个事件。QQ就像是Channel,建立沟通通道。人看到短信,根据短信内容,从而决定要不要打开QQ,处理消息
