读书笔记:NIO的使用总结

概述:

NIO包是JDK1.4引入的新的I/O类库,目的是为了提高文件读写的速度。NIO的读写模式和旧的I/O有一些不同,NIO是通过缓冲器和通道来对文件进行操作的,当我们需要对文件进行数据的读取时,我们先将数据读取到缓冲器中,再从缓冲器上读取我们所要的数据,当我们想往文件中写入数据时,那么我们先朝着缓冲器写入数据,再利用缓冲器往文件里进行写入。在整个文件的读写操作时,我们并没有与文件进行直接的交互。
我们先来看一个NIO读写文件的例子

public static void testChannel() throws IOException {
        //1.利用RandomAccessFile打开文件data.txt
        RandomAccessFile aFile = new RandomAccessFile(dir + "data.txt","rw");
        //2.获取文件的通道
        FileChannel inChannel = aFile.getChannel();

        //3.创建一个字节缓存器
        ByteBuffer buf = ByteBuffer.allocate(48);
        //4.根据缓存器的大小将文件内容读取到缓存中
        int bytesRead = inChannel.read(buf);

        //5.根据读取到的字节数做判断
        while (bytesRead != -1){
            System.out.println("Read " + bytesRead);
            //切换缓冲区的模式
            buf.flip();

            //6.打印缓存中的内容
            while (buf.hasRemaining()){
                System.out.print((char) buf.get());
            }
            System.out.println("");

            //7.清空缓存的内容,假如不执行这一步,read(buf)将会返回0,因为无法读入到buf中
            buf.clear();
            bytesRead = inChannel.read(buf);
        }
        aFile.close();
    }

如上所示,这是一个利用Channel和缓冲器从文件中读取数据的代码,上面的代码分为这几个步骤:

  1. 打开一个文件
  2. 获取文件的Channel
  3. 创建一个固定大小的缓冲器
  4. 将文件内容读入到缓冲器中
  5. 读出缓冲器的内容,读完之后清空缓冲器以便接下来继续读

NIO的组成

如下所示,一个NIO的系统的组成大致包含下面的三大部分,分别是通道(Channel)、缓冲器(Buffers)以及Selector


NIO包的主要组成部分

其中Selector的作用是可以在单个线程中管理多个通道的读写

Buffers(缓冲器)

缓冲器是文件数据读取和写入的一段内存,当我们需要从通道中取数据时,我么先将通道中的数据读到缓冲器中,再从缓冲器中获取数据。当我们需要往通道中写入数据时,我们先将数据写到缓冲器中,再把数据写入通道。

缓冲器的三个标识

capacitylimitposition,下面我们来分别介绍一下
capacity
表示缓存的容量,比如说我们使用下面的代码创建了一个缓冲器

ByteBuffer buf = ByteBuffer.allocate(48);//分配48个Byte的缓冲器

那么缓冲器的capacity就是48,注意capacity的值与缓冲器的类型无关

limit

  1. 在读模式下,limit表示缓冲器还有多少数据可以读
  2. 在写模式下,limit表示缓冲器还有多少空间可以往里写

position
表示缓冲区的当前位置

  1. 在缓冲区切到写模式时,position的值被设为0
  2. 在缓冲区切换到读模式时,position的值被设为缓冲区中第一个能写入的空间位置
buffer一些操作的方法
往buffer里面写入数据
  1. 通过Channel往buffer里写入数据,如下所示
 int byteRead = inChannel.read(buf);

上面是往缓存buffer里面写入数据,并返回写入到缓存中的字节个数

  1. 通过buffer的put()方法往缓存中写入数据
buf.put(127);

往缓存中写入ASCII编码为127的数,写入之后,limit减1,position加1

flip()方法

这个方法是将缓存切换到读模式以读取缓存中的数据,具体的变化是将position的值设为0,limit的值设为原先position的值,在这样的情况下,就可以读取buffer中从positionlimit之间的所有数据

从buffer中读取数据
  1. 从buffer中读取数据到Channel
 int bytesWritten = inChannel.write(buf);

上面是将buffer中的数据读取到Channel中,并返回读取数据的个数

  1. 使用get()方法读取数据,如下所示
byte aByte = buf.get();

获取buffer中当前位置的值,并将结果赋值给aByte

rewind()方法

这个方法的作用是重新读取buffer里面的内容,做法是,将position置为0,而limit保持不变

clear()compact()方法

clear()方法是对buffer进行清空操作,但是并不是真正意义上的清空,而是修改其中的标识的值,将position的值设为0,而将limit的值设为capacity,这样再往buffer里面写入数据时就会覆盖原先的内容,已达到清空的目的,但是如果随后没有执行写操作,那么原来的数据还是能读的出来
compact()则是将buffer中所有未读的数据拷贝到buffer的起始位置,再将position的值设置到最后一个未读元素的后面

mark()reset()方法
mark()方法将buffer中的position的值记录下来(赋值给mark),假如我们继续往后面读取数据,这时候我们调用reset()方法时,我们会回到我们之前标记的位置,实际上是将mark的值回赋给position

equals()compareTo()方法
这两个方法都是对两个buffer之间的比较
对于equals(),满足下面的几个条件时,我们会返回true值,表示两个buffer相同

  1. 两个buffer有相同的类型
  2. Buffer中剩余的byte、char等的个数相等
  3. Buffer中所有剩余的byte、char等都相同

从上面的条件我们可以看出,equals()方法实际上是比较从positionlimit之间的数据是否相等,而对position之前的数据则并不关心

compareTo()方法在满足下列所有条件时,表示一个Buffer小于另一个Buffer

  1. 第一个不相等的元素小于另一个Buffer中对应的元素
  2. 所有元素都相等,但第一个Buffer比另一个先耗尽

compareTo()方法返回的是第一个buffer和第二个buffer中第一个不相等值的差

scatter和gather

scatter

scatter是指在读操作时将一个Channel中的数据读出到多个buffer中去,代码如下所示

  ByteBuffer header = ByteBuffer.allocate(128);
  ByteBuffer body   = ByteBuffer.allocate(1024);
  ByteBuffer[] bufferArray = { header, body };
  channel.read(bufferArray);

如上的代码是一个将一个Channel中的数据读出到多个buffer中的例子,在读出的过程中,会沿着buffer数组的下标依次进行填满,这样的做法不适合于动态的信息

gather

gather指在写操作时将多个buffer的数据写入到同一个Channel,代码如下所示

  ByteBuffer header = ByteBuffer.allocate(128);
  ByteBuffer body   = ByteBuffer.allocate(1024);
  ByteBuffer[] bufferArray = { header, body };
  channel.write(bufferArray);

如上的代码是将不同的buffer沿着buffer的下标依次写入到Channel中去,这样的操作很适合处理动态的信息

Channel之间的数据传输

利用Channel的transferFrom()transferTo()方法可以在不借助额外的buffer而完成两个Channel之间的数据传输

transferFrom()

将任何Channel中的数据传输到FileChannel中去,示例代码如下所示

  RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");
  FileChannel      fromChannel = fromFile.getChannel();
  RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");
  FileChannel      toChannel = toFile.getChannel();
  long position = 0;
  long count = fromChannel.size();
  toChannel.transferFrom(position, count, fromChannel);
transferTo()

将FileChannel中的数据传输到任意的Channel中去,示例代码如下

  RandomAccessFile fromFile = new RandomAccessFile(dir + "data.txt","rw");
  FileChannel fromChannel = fromFile.getChannel();

  RandomAccessFile toFile = new RandomAccessFile(dir + "data2.txt","rw");
  FileChannel toChannel = toFile.getChannel();

  long position = 0;
  long count = fromChannel.size();

  fromChannel.transferTo(position,count,toChannel);

Selector

Selector的作用是在单一的线程下对多个Channel中的数据进行管理,这样就很有利于对于单个Channel数据量少,但是Channel的总数多的情形进行管理

创建一个Selector
  Selector selector = Selector.open();
向Selector注册通道
  channel.configureBlocking(false);//将Channel设为非阻塞
  SelectionKey key = channel.register(selector,SelectionKey.OP_READ);//将Channel注册到Selector对象上去

注册在Selector中的Channel必须处于非阻塞模式,这意味着不能将Selector和FileChannel一起使用,因为FileChannel是阻塞通道

interest集合

Channel的register方法的第二个参数是interest的集合,意味着第二个参数是多个类型的叠加,类型包括:

  Connect(OP_CONNECT)
  Accept(OP_ACCEPT)
  Read(OP_READ)
  Write(OP_WRITE)
  第二个参数不仅可以是上面4个单独的类型,而且可以是上面4个类型的叠加,即使用按位或的方式,例如: SelectionKey.OP_READ | SelectionKey.OP_ACCEPT,这表示对这个Channel的连接和读取感兴趣
SelectionKey

当Channel向Selector注册时,会返回一个SelectionKey的对象,该对象包含以下的属性

  interest集合(int interestSet = selectionKey.readOps();)
  ready集合(int readySet = selectionKey.readyOps();)
  Channel(Channel channel = selectionKey.channel();)
  Selector(Selector selector = selectionKey.selector();)
  附加的对象
select()

这个方法表示对注册在Selector上的Channel进行选择,select()的方法有几种变体,如下
int select():阻塞并一直等到通道上有一个Channel中发生了其在注册时指定的事件,比如定义了OP_READ属性且出现了Channel的读取行为
int select(long timeout):阻塞并且等到设置的时间后自动返回
int selectNow():立即返回,不管有没有选择到正在发生注册操作的Channel

selectedKeys()

在使用select()方法后会知道有一个或多个通道已经处于就绪状态了,那么此时可以使用selectedKeys()来获取SelectionKey对象的集合,如下所示

  Set selectedKeys = selector.selectedKeys();

对这些结果集的操作如下列代码所示

  Set selectedKeys = selector.selectedKeys();
  Iterator keyIterator = selectedKeys.iterator();
  while(keyIterator.hasNext()) {
      SelectionKey key = keyIterator.next();
      if(key.isAcceptable()) {
         // a connection was accepted by a ServerSocketChannel.
      } else if (key.isConnectable()) {
        // a connection was established with a remote server.
      } else if (key.isReadable()) {
        // a channel is ready for reading
      } else if (key.isWritable()) {
        // a channel is ready for writing
      }
      keyIterator.remove();//Selector不会自己移除SelectionKey实例,因此需要利用这个方法来进行移除操作
  }
wakeUp()

如果一个线程正在执行select()并且因此而处于阻塞的状态,那么可以让其他线程在第一个处于调用select()而阻塞的线程上调用Selector.WakeUp()而立即返回

close()

用完Selector后可以利用close()方法来关闭Selector,这样会使得注册在其上的Channel无效,但是这并不能关闭Channel

FileChannel

FileChannel是文件操作的通道,其操作的方法见下面

打开FileChannel
  RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
  FileChannel inChannel = aFile.getChannel();
从FileChannel读取数据
  ByteBuffer buf = ByteBuffer.allocate(48);
  int bytesRead = inChannel.read(buf);
向FileChannel中写入数据
  String newData = "New String to write to file..." + System.currentTimeMillis();
  ByteBuffer buf = ByteBuffer.allocate(48);
  buf.clear();
  buf.put(newData.getBytes());
  buf.flip();
  while(buf.hasRemaining()) {
      channel.write(buf);
  }
用完FileChannel之后关闭Channel
  channel.close();
position方法

position方法有两种形式,带参数和不带参数,例子如下

  long pos = channel.position();//无参形式为获取当前通道的当前位置
  channel.position(pos + 123);//带参形式为设置通道的当前位置

需注意的是,在设置通道当前位置的时候,假如设置的位置超出了文件长度,并且会在设置的该位置处写下数据,那么就会造成文件的空洞

size()

获取通道关联的文件的大小

  long fileSize = channel.size();
truncate(int)

从头截取通道关联文件的大小,并且丢弃掉后面的数据

  channel.truncate(1024);//只需要关联文件的头1024个字节
force()

将通道中尚未写入到磁盘里的数据写入到磁盘里

  channel.force(true);

SocketChannel

SocketChannel是一个连接到TCP套接字上的通道,创建一个SocketChannel的方式有下面两种

  1. 打开一个SocketChannel并连接到互联网上的某台服务器。
  2. 一个新连接到达ServerSocketChannel时,会创建一个SocketChannel
打开SocketChannel
  SocketChannel socketChannel = SocketChannel.open();
  socketChannel.connect(new InetSocketAddress("http://www.baidu.com",80));
关闭SocketChannel
  socketChannel.close();
从SocketChannel中读取数据
  ByteBuffer buf = ByteBuffer.allocate(48);
  int bytesRead = socketChannel.read(buf);
写入SocketChannel
  String newData = "New String to write to file..." + System.currentTimeMillis();
  ByteBuffer buf = ByteBuffer.allocate(48);
  buf.clear();
  buf.put(newData.getBytes());
  buf.flip();
  while(buf.hasRemaining()) {
      channel.write(buf);
  }
非阻塞模式
  socketChannel.configureBlocking(false);//将SocketChannel对象设为非阻塞
  socketChannel.connect(new InetSocketAddress("http://jenkov.com",80));
  while(! socketChannel.finishConnect()){
  }
write()

在非阻塞模式下,write()方法在尚未写出任何内容时就可能返回了,所以在循环中调用write()

read()

非阻塞模式下,read()方法在尚未读取到任何数据时就可能返回了,所以要关注其返回的字节数

DatagramChannel

DatagramChannel的打开方式
  DatagramChannel channel = DatagramChannel.open();
  channel.socket().bind(new InetSocketAddress(9999));

打开一个DatagramChannel,并将这个Channel绑定到UDP的9999端口

接收数据
  ByteBuffer buf = ByteBuffer.allocate(48);
  buf.clear();
  channel.receive(buf);//利用分配的buffer进行接收数据,超出buffer大小的数据将被丢弃
发送数据
  String newData = "New String to write to file..." +   System.currentTimeMillis();
  ByteBuffer buf = ByteBuffer.allocate(48);
  buf.clear();
  buf.put(newData.getBytes());
  buf.flip();
  int bytesSent = channel.send(buf, new InetSocketAddress("fanwn.com", 80));

以上一个发送数据的代码,由于其发送的地址并没有处于监听状态,因此这样不会发生任何反应

连接到特定的地址
  channel.connect(new InetSocketAddress("fanwn.com",80));
  int bytesRead = channel.read(buf);
  int bytesWritten = channel.write(buf);

将Channel连接到特定的地址,可以读取通道中的数据到buffer中,也可以将buffer中的数据读取到通道中

Pipe

管道是对两个线程之间交换数据的方式,一个管道包含了两个通道类:sink通道和source通道,其中sink是往通道里面写入数据,source是从通道内读出数据

打开管道
  Pipe pipe = Pipe.open();
向管道写入数据
  Pipe.SinkChannel sinkChannel = pipe.sink();
  String newData = "New String to write to file..." + System.currentTimeMillis();
  ByteBuffer buf = ByteBuffer.allocate(48);
  buf.clear();
  buf.put(newData.getBytes());
  buf.flip();
  while(buf.hasRemaining()) {
      sinkChannel.write(buf);
  }
从管道中读出数据
  Pipe.SourceChannel sourceChannel = pipe.source();
  ByteBuffer buf = ByteBuffer.allocate(48);
  int bytesRead = sourceChannel.read(buf);
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容