NIO基本介绍:

6.png7.png


buffer:
  1. public class BasicBuffer {
  2. public static void main(String[] args) {
  3. //举例说明buffer的使用(简单说明)
  4. //创建一个buffer
  5. IntBuffer intBuffer = IntBuffer.allocate(5); //可以存放5个int
  6. //向buffer中存放数据
  7. for (int i = 0; i < intBuffer.capacity(); i++) {
  8. intBuffer.put(i + 1);
  9. }
  10. //将buffer进行读写切换
  11. intBuffer.flip();
  12. //取数据
  13. while (intBuffer.hasRemaining()) { //是否还存在数据
  14. //get操作维护一个索引,每次调用索引后移一位
  15. System.out.println(intBuffer.get());
  16. }
  17. }
  18. }

NIO和BIO比较:

1.BIO以流的方式处理数据,而NIO以块的方式处理数据,块IO的效率比流IO高很多
2.BIO是阻塞的,NIO是非阻塞的
3.BIO基于字节流和字符流进行操作,而NIO基于Channel和Buffer进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入通道中。Selector(选择器)用于监听多个通信的时间(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道


描述NIO 的 Selector 、Channel 和 Buffer 的关系

8.png

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

9.png

缓冲区中的四个属性含义:

103.png

常用API:
  1. public abstract class Buffer {
  2. //JDK1.4时,引入的api
  3. public final int capacity()//返回此缓冲区的容量
  4. public final int position()//返回此缓冲区的位置
  5. public final Buffer position(int newPositio)//设置此缓冲区的位置
  6. public final int limit()//返回此缓冲区的限制
  7. public final Buffer limit(int newLimit)//设置此缓冲区的限制
  8. public final Buffer mark()//在此缓冲区的位置设置标记
  9. public final Buffer reset()//将此缓冲区的位置重置为以前标记的位置
  10. public final Buffer clear()//清除此缓冲区, 即将各个标记恢复到初始状态,但是数据并未真正擦除
  11. public final Buffer flip()//反转此缓冲区
  12. public final Buffer rewind()//重绕此缓冲区
  13. public final int remaining()//返回当前位置与限制之间的元素数
  14. public final boolean hasRemaining()//告知在当前位置和限制之间是否有元素
  15. public abstract boolean isReadOnly();//告知此缓冲区是否为只读缓冲区
  16. // JDK1.6时引入的api
  17. public abstract boolean hasArray();//告知此缓冲区是否具有可访问的底层实现数组
  18. public abstract Object array();//返回此缓冲区的底层实现数组
  19. public abstract int arrayOffset();//返回此缓冲区的底层实现数组中第一个缓冲区元素的
  20. public abstract boolean isDirect();//告知此缓冲区是否为直接缓冲区
  21. }

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通道类似于流,区别如下:
通道可以同时进行读写,而流只能读或写
通道可以实现异步读写数据
通道可以从缓冲区读数据,也可以写数据到缓冲区

11.png
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:

12.png


利用Buffer和Channel操作文件的两个Demo

  1. /**
  2. * 将数据写入本地文件
  3. */
  4. public static void write() throws Exception {
  5. String str = "hello";
  6. //创建输出流 --> Channel
  7. FileOutputStream fos = new FileOutputStream("f:\\01.txt");
  8. //获取对应的FileChannel
  9. FileChannel fileChannel = fos.getChannel();
  10. //创建缓冲区
  11. ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
  12. //将输入放入到buffer中
  13. byteBuffer.put(str.getBytes());
  14. byteBuffer.flip(); //读写切换
  15. //将bytebuffer中的数据写入到channel中
  16. fileChannel.write(byteBuffer);
  17. fos.close();
  18. }
  1. /**
  2. * 从文件中读取数据打印到控制台
  3. */
  4. public static void read() throws Exception {
  5. //创建文件的输入流
  6. File file = new File("f:\\01.txt");
  7. FileInputStream fis = new FileInputStream(file);
  8. //通过fileInputStream获取对应的FileChannel
  9. FileChannel fisChannel = fis.getChannel();
  10. //创建缓冲区
  11. ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length());
  12. //将通道中的数据写入到buffer
  13. fisChannel.read(byteBuffer);
  14. //将bytebuffer中的字节数据转化为字符串输出
  15. System.out.println(new String(byteBuffer.array()));
  16. fis.close();
  17. }
  1. /**
  2. * 将文件复制1
  3. */
  4. public static void copy1() throws Exception {
  5. FileInputStream fis = new FileInputStream("f:\\01.txt");
  6. FileChannel fisChannel = fis.getChannel();
  7. FileOutputStream fos = new FileOutputStream("f:\\02.txt");
  8. FileChannel fosChannel = fos.getChannel();
  9. ByteBuffer byteBuffer = ByteBuffer.allocate(521);
  10. while (true) {
  11. byteBuffer.clear(); //清空buffer,防止一次没读完
  12. int read = fisChannel.read(byteBuffer);
  13. if (read == -1) break;
  14. //将buffer中的数据写到fosChannel中
  15. byteBuffer.flip();
  16. fosChannel.write(byteBuffer);
  17. }
  18. fis.close();
  19. fos.close();
  20. }
  1. /**
  2. * 文件复制2(api)
  3. */
  4. public static void copy2() throws Exception {
  5. //创建相关的流
  6. FileInputStream fis = new FileInputStream("f:\\01.txt");
  7. FileOutputStream fos = new FileOutputStream("f:\\03.txt");
  8. //获取对应的channel
  9. FileChannel fisChannel = fis.getChannel();
  10. FileChannel fosChannel = fos.getChannel();
  11. //使用TransferForm
  12. fosChannel.transferFrom(fisChannel, 0, fisChannel.size());
  13. //关闭资源
  14. fis.close();
  15. fos.close();
  16. }

关于Buffer和Channel的使用细节

1.ByteBuffer支持类型化的put和get,put放入的是什么数据类型,get就应该使用想应的数据类型,否则可能会有BufferUnderflowException异常
2.可以将一个普通的Buffer转换成只读Buffer

  1. ByteBuffer readOnlyBuffer = buffer.asReadOnlyBuffer();

3.NIO还提供了MappedByteBuffer,可以让文件直接在内存中进行修改

  1. public static void main(String[] args) throws Exception {
  2. RandomAccessFile randomAccessFile = new RandomAccessFile("f:\\01.txt", "rw");
  3. //获取信道
  4. FileChannel fisChannel = randomAccessFile.getChannel();
  5. /*
  6. * 参数一:使用读写模式
  7. * 参数二:0代表可以直接修改起始位置
  8. * 参数三:映射到内存的大小(即最多可将文件的5个字节映射到内存中) (范围是0-5)
  9. * */
  10. MappedByteBuffer mappedByteBuffer =
  11. fisChannel.map(FileChannel.MapMode.READ_WRITE, 0, 5);
  12. mappedByteBuffer.put(0, (byte) 'a');
  13. mappedByteBuffer.put(1, (byte) 'b');
  14. randomAccessFile.close();
  15. }

4.NIO还支持多个Buffer(数组)完成读写操作,即Scattering和Gatering

  1. public static void test1() throws Exception {
  2. /*
  3. Scattering:将数据写入到buffer时可以采用buffer数组 [分散]
  4. Gathering:从buffer读取数据时可以采用buffer数组依次读 [聚合]
  5. */
  6. //使用ServerSocketChannel和SocketChannel
  7. ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
  8. InetSocketAddress inetSocketAddress = new InetSocketAddress(7000);
  9. //绑定端口到socket
  10. serverSocketChannel.socket().bind(inetSocketAddress);
  11. //创建buffer数组
  12. ByteBuffer[] byteBuffers = new ByteBuffer[2];
  13. byteBuffers[0] = ByteBuffer.allocate(5);
  14. byteBuffers[1] = ByteBuffer.allocate(3);
  15. //等待客户端连接
  16. SocketChannel socketChannel = serverSocketChannel.accept();
  17. int messageLength = 8; //假设从客户端接收8个字节
  18. //循环读取
  19. while (true) {
  20. int byteRead = 0;
  21. while (byteRead < messageLength) {
  22. long l = socketChannel.read(byteBuffers);
  23. byteRead++;
  24. System.out.println("byteRead=" + byteRead);
  25. //使用流打印, 看看当前的这个buffer的position 和 limit
  26. Arrays.asList(byteBuffers).stream().map(buffer ->
  27. "postion=" + buffer.position() + ", limit="
  28. + buffer.limit()).forEach(System.out::println);
  29. }
  30. //将所有的buffer进行读写转换
  31. Arrays.asList(byteBuffers).forEach(Buffer::flip);
  32. //将数据读取到客户端
  33. long byteWrite = 0;
  34. while (byteWrite < messageLength) {
  35. long l = socketChannel.write(byteBuffers);
  36. byteWrite++;
  37. }
  38. //将所有的buffer进行clear防止没读取完
  39. Arrays.asList(byteBuffers).forEach(Buffer::clear);
  40. System.out.println("byteRead:=" + byteRead +
  41. " byteWrite=" + byteWrite + ", messagelength" + messageLength);
  42. }
  43. }

Seletor(选择器)

基本介绍

1.JAVA的NIO,用非阻塞IO的方式,可以用一个线程,处理多个客户端连接,就会用到Seletor选择器
2.Selector能够检测多个注册的通道上是否有事件发生(注意:多个Channel以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然后对每个事件进行相应的处理,这样就可以只用一个线程去管理多个通道,也就是管理多个连接和请求
3.只有在连接真正有读写事件发生时,才会进行读写,就大大的减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程
4.避免了多线程之间的上下文切换导致的开销
image.png

Selector相关API:

1.NIO中的ServerSocketChannel功能类似于ServerSocket,SocketChannel功能类似于Socket
2.selector相关方法说明
selector.select() //阻塞
selector.select(1000) //阻塞1000ms,在1000ms后返回
selector.wakeup() //唤醒
selector.selecctNow() //不阻塞,立刻返回

NIO非阻塞网络编程原理

image.png
说明:
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,完成业务的处理

代码实现
  1. public static void Test() throws Exception {
  2. //创建ServerSocketChannel
  3. ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
  4. //得到Selector对象
  5. Selector selector = Selector.open();
  6. //绑定端口在服务器端监听
  7. serverSocketChannel.socket().bind(new InetSocketAddress(6666));
  8. //设置为非阻塞
  9. serverSocketChannel.configureBlocking(false);
  10. //把ServerSocketChannel注册到selector关心 事件为OP_ACCEPT
  11. serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
  12. //循环等待客户端连接
  13. while (true) {
  14. int select = selector.select(1000);
  15. if (select == 0) { //没有事件发生
  16. System.out.println("服务器等待了1s,无连接");
  17. continue;
  18. }
  19. //如果返回的大于0
  20. //获取到相关的selectionKey集合
  21. Set<SelectionKey> selectionKeys = selector.selectedKeys();
  22. Iterator<SelectionKey> iterator = selectionKeys.iterator();
  23. while (iterator.hasNext()) {
  24. //获取到SelectionKey
  25. SelectionKey key = iterator.next();
  26. //根据key 对应的通道发生的事件做相应的处理
  27. if (key.isAcceptable()) { //如果时OP_ACCEPT,有新的客户端来连接
  28. //给该客户端生成一个SocketChannel
  29. SocketChannel socketChannel = serverSocketChannel.accept();
  30. //设置为非阻塞
  31. socketChannel.configureBlocking(false);
  32. System.out.println("连接成功,生成了socketChannel" + socketChannel.hashCode());
  33. //将当前的socketChannel注册到selector上,关注读事件,并关联一个Buffer
  34. socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
  35. }
  36. if (key.isReadable()) { //读事件 OP_READ
  37. //通过key反向获取到对应的Channel
  38. SocketChannel channel = (SocketChannel) key.channel();
  39. //获取到该channel关联的Buffer
  40. ByteBuffer buffer = (ByteBuffer) key.attachment();
  41. channel.read(buffer);
  42. System.out.println("form 客户端" + new String(buffer.array()));
  43. }
  44. //手动从集合中移除当前的SelectionKey,防止多线程出现重复操作
  45. iterator.remove();
  46. }
  47. }
  48. }

SelectionKey—-API

SelectionKey,表示Seletor和网络通道的注册关系
int OP_ACCEPT:表示有新的网络连接可以accept,值为16
int OP_CONNECT:代表连接已经建立,值为8
int OP_READ:代表读操作,值为1
int OP_WRITE:代表写操作,值为4
image.png

ServerSoketchannel

在服务器端监听新的客户端Soket连接
image.png

SocketChannel

网络IO通道,具体负责进行读写操作,NIO把缓冲区的数据写入通道,或者把通道里的数据读取到缓冲区
image.png

生活中NIO一个简单理解


不知道大家有没有用过2010年左右(或许更早)2G时代的手机,可以运行那种基于J2ME的QQ,能聊天,看个空间,偷个菜(文字版)什么的。这种手机一般都有个缺点就是不能后台运行,一旦去做其他事情(玩游戏,看小说等),QQ就掉线了,就不能收到QQ消息了。如果想要实时接收到女神消息,就要一直保持打开着QQ,不能去做其他事情。这就类似于BIO,阻塞的。
后来QQ出了一个手机业务,叫超级QQ(每月10块呢),可以伪实时在线,同时更快的升级(太阳月亮我的最爱)。之所以叫他伪实时在线,是因为它的实现方式是:当QQ收到消息时,腾讯会以短信的形式发到手机上,告诉你某某给你发消息了,请及时处理之类的(也可以直接回复短信,QQ上也会自动转发过去,不太相关暂时忽略)。此时再去登录QQ,就能立刻收到消息了。虽然手机同一时刻依然只能做一件事情,但是在没有QQ消息的时候也无需一直等待了,从而从容不铺去做别的事情。也就是非阻塞的了。
这个超级QQ的业务就像是NIO:人就是Selector,监听事件。短信就像是一个事件。QQ就像是Channel,建立沟通通道。人看到短信,根据短信内容,从而决定要不要打开QQ,处理消息