Java NIO 详解

来源:互联网 发布:手机号码人肉搜索软件 编辑:程序博客网 时间:2024/06/10 17:42
Java NIO 

   由以下几个核心部分组成: 
  虽然Java NIO 中除此之外还有很多类和组件,但在我看来,Channel,Buffer 和 Selector 构成了核心的API。其它组件,如Pipe和FileLock,只不过是与三个核心组件共同使用的工具类 
(1)Channels  -> 都是从一个Channels开始的,像io中的流,可以读到Buffer中  实现: FileChannel  DatagramChannel  SocketChannel  ServerSocketChannel
(2)Buffers   -> 包含了8种基本类型的Buffer实现  ByteBuffer CharBuffer DoubleBuffer FloatBuffer IntBuffer LongBuffer ShortBuffer 还有  MappedByteBuffer(直接模式,表示内存映射文件 )  HeapByteBuffer(间接模式,操作堆内存)
(3)Selectors -> 允许单线程处理多个 Channel
   与io的区别:
    NIO和传统IO(一下简称IO)之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。
    NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。


    IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。 NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,
如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 
             线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。
    
 
(一) Channels 详解
 1.特点:(1)双向读取写(2)异步读写 
 2.实现:
    (1)FileChannel 从文件中读取数据(不能切换成非阻塞的状态)
     A) 实现过程:
     a)打开FileChannel -> 我们无法直接打开一个FileChannel,需要通过使用一个InputStream、OutputStream或RandomAccessFile来获取一个FileChannel实例
     RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
     FileChannel inChannel = aFile.getChannel();
 b)从FileChannel读取数据 -> 调用多个read()方法之一从FileChannel中读取数据。
     ByteBuffer buf = ByteBuffer.allocate(48);
         int bytesRead = inChannel.read(buf);//如果返回-1,表示到了文件末尾。
     c)向FileChannel写数据 -> 使用FileChannel.write()方法向FileChannel写数据,该方法的参数是一个Buffer。
        String newData = "nanhu Nio study";
ByteBuffer buf = ByteBuffer.allocate(48); //定义Buffer
buf.clear();
buf.put(newData.getBytes());//将数据放入buffer
buf.flip();//切换到写就绪
while(buf.hasRemaining()) {
   channel.write(buf);//写入通道
}
 d)关闭FileChannel -> 用完FileChannel后必须将其关闭
   channel.close();
B) 其他方法:
     a)position方法 -> 有时可能需要在FileChannel的某个特定位置进行数据的读/写操作。可以通过调用position()方法获取FileChannel的当前位置。 也可以通过调用position(long pos)方法设置FileChannel的当前位置。
       long pos = channel.position();//获取当前通道的位置
            channel.position(pos +123);//设置通道的位置
            //如果将位置设置在文件结束符之后,然后向通道中写数据,文件将撑大到当前位置并写入数据。这可能导致“文件空洞”,磁盘上物理文件中写入的数据间有空隙。
          b)size方法 -> FileChannel实例的size()方法将返回该实例所关联文件的大小。
            long fileSize = channel.size();
          c)truncate方法 -> FileChannel.truncate()方法截取一个文件。截取文件时,文件将中指定长度后面的部分将被删除。
            channel.truncate(1024);//将1024之后的数据全部删除,也就是截取文件的前1024个字节
          d)force方法 -> ileChannel.force()方法将通道里尚未写入磁盘的数据强制写到磁盘上。
            channel.force(true); //出于性能考虑可能将数据缓存在内存,调用此方法将数据写到磁盘
    (2)DataGramChannel 通过UDP读取网络中的数据
    A)实现过程:
      a) 打开 DatagramChannel
          DatagramChannel channel = DatagramChannel.open();
          channel.socket().bind(new InetSocketAddress(9999));
      b) 接收数据
          ByteBuffer buf = ByteBuffer.allocate(48);
 buf.clear();
 channel.receive(buf);// 如果Buffer容不下收到的数据,多出的数据将被丢弃。
 c) 发送数据
     String newData = "nanhu is dean";
 ByteBuffer buf = ByteBuffer.allocate(48);
 buf.clear();
 buf.put(newData.getBytes());
 buf.flip();
 int bytesSent = channel.send(buf, new InetSocketAddress("localhost", 80)); 
    B)其他方式:
      a)connect() -> 可以将DatagramChannel“连接”到网络中的特定地址的。由于UDP是无连接的,连接到特定地址并不会像TCP通道那样创建一个真正的连接。而是锁住DatagramChannel ,让其只能从特定地址收发数据。
        channel.connect(new InetSocketAddress("jenkov.com", 80));
        int bytesRead = channel.read(buf); //数据传送方面没有任何保证
        int bytesWritten = channel.write(but);
    (3)SocketChannel 通过TCP读写网络中的数据
    A)实现过程:
      a) 打开 SocketChannel
          SocketChannel socketChannel = SocketChannel.open();
          socketChannel.connect(new InetSocketAddress("http://jenkov.com", 80));
      b) 从 SocketChannel 读取数据
          ByteBuffer buf = ByteBuffer.allocate(48);
          int bytesRead = socketChannel.read(buf);//如果返回的是-1,表示已经读到了流的末尾(连接关闭了)
      c) 写入 SocketChannel
          String newData = "nanhu is dean";
 ByteBuffer buf = ByteBuffer.allocate(48);
 buf.clear();
 buf.put(newData.getBytes());
 buf.flip();
 while(buf.hasRemaining()) {
     channel.write(buf);
 }
 d) 关闭 SocketChannel
          socketChannel.close();
    B)其他方法:
      a) 非阻塞模式  -> 可以设置 SocketChannel 为非阻塞模式(non-blocking mode).设置之后,就可以在异步模式下调用connect(), read() 和write()了。
          socketChannel.configureBlocking(false);
      b) connect() -> 如果SocketChannel在非阻塞模式下,此时调用connect(),该方法可能在连接建立之前就返回了。为了确定连接是否建立,可以调用finishConnect()的方法。
          socketChannel.configureBlocking(false);
 socketChannel.connect(new InetSocketAddress("http://jenkov.com", 80));
 while(! socketChannel.finishConnect() ){
     //wait, or do something else...
 }
    (4) ServerSocketChannel 监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个  
    A)代码示例:
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); //打开连接
serverSocketChannel.socket().bind(new InetSocketAddress(9999));//绑定端口号
serverSocketChannel.configureBlocking(false);//设置非阻塞模式
while(true){
   SocketChannel socketChannel = serverSocketChannel.accept();//立即返回,如果没有连接返回null
   if(socketChannel != null){
       //do something with socketChannel...
   }
}
       
 3.分散和聚集:
    (1)分散(scatter)从Channel中读取是指在读操作时将读取的数据写入多个buffer中。因此,Channel将从Channel中读取的数据“分散(scatter)”到多个Buffer中。
代码示例如下(写入时按着数据的顺序,一个写满了,写下一个):
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body   = ByteBuffer.allocate(1024);
 
ByteBuffer[] bufferArray = { header, body };
channel.read(bufferArray); 
    (2)聚集(gather)  写入Channel是指在写操作时将多个buffer的数据写入同一个Channel,因此,Channel 将多个Buffer中的数据“聚集(gather)”后发送到Channel。
    (3)scatter / gather经常用于需要将传输的数据分开处理的场合,例如传输一个由消息头和消息体组成的消息,你可能会将消息体和消息头分散到不同的buffer中,这样你可以方便的处理消息头和消息体。
 4.通道之间的数据传输:
   (1)特点:
                    如果两个通道中有一个是FileChannel,那你可以直接将数据从一个channel传输到另外一个channel.
  (2)transferFrom()
      a)FileChannel的transferFrom()方法可以将数据从源通道传输到FileChannel中.
      b)代码示例:
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();//最多传输的字节,如果源通道的剩余空间小于 count 个字节,则所传输的字节数要小于请求的字节数
toChannel.transferFrom(fromChannel,position, count);
  (3)transferTo()
      a)transferTo()方法将数据从FileChannel传输到其他的channel中.
      b 代码示例:
            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();//最多传输的字节,如果源通道的剩余空间小于 count 个字节,则所传输的字节数要小于请求的字节数
toChannel.transferFrom(position, count, fromChannel); //只有这句和transferFrom不同,调用关系相反


 (二)Buffer 详解
 1.特点:Channel提供从文件、网络读取数据的渠道,但是读写的数据都必须经过Buffer
 2.实现:
    (1) 向Buffer中写数据:
从Channel写到Buffer (fileChannel.read(buf))
通过Buffer的put()方法 (buf.put(…))
     从Buffer中读取数据: 
从Buffer读取到Channel (channel.write(buf))
使用get()方法从Buffer中读取数据 (buf.get())
    (2)索引 说明
   capacity 缓冲区数组的总长度
position 写模式:下一个要操作的数据元素的位置 ,读模式:下一个可读元素的位置
limit    写模式:缓冲区数组中不可操作的下一个元素的位置:limit<=capacity ,读模式:最多可读到的位置(就是写模式时position的位置)
mark     用于记录当前position的前一个位置或者默认是0
     (3) 使用Buffer读写数据一般遵循以下四个步骤: 
1.写入数据到Buffer
2.调用flip()方法 -> 将position设置为0,limit设置成之前的position(从写模式切换到读模式)
3.从Buffer中读取数据
4.调用clear()方法或者compact()方法 
 a)capacity() 数据不会被遗忘(只会遗忘已读的数据,compact()方法将所有未读的数据拷贝到Buffer起始处。然后将position设到最后一个未读元素正后面) 从读切换到写模式
 b)clear()    数据会被遗忘,在进新数据时从0开始(遗忘所有的数据) 从读切换到写模式
 
 
 (三) Selectors 详解
 1.特点:Selector(选择器)是Java NIO中能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接。
 2.实现:
   (1)创建: 
       Selector selector = Selector.open();
   (2)注册通道:
  channel.configureBlocking(false);//通道必须是非阻塞的,这意味着不能将FileChannel与Selector一起使用,因为FileChannel不能切换到非阻塞模式。
  SelectionKey key = channel.register(selector, Selectionkey.OP_READ);//第二个参数是个“interest集合”,意思是在通过Selector监听Channel时对什么事件感兴趣
 3.SelectionKey:
       当向Selector注册Channel时,register()方法会返回一个SelectionKey对象.(interest集合,ready集合,Channel,Selector,附加的对象(可选)) 
 4.监听类型分析(interest集合):
   (1)Connect -> SelectionKey.OP_CONNECT(连接)
   (2)Accept -> SelectionKey.OP_ACCEPT(接受)
   (3)Read -> SelectionKey.OP_READ(读)
   (4)Write -> SelectionKey.OP_WRITE(写)
   (5)组合 -> int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
   (6)代码示例:
int interestSet = selectionKey.interestOps(); //得到注册的集合
//下面是判断集合是什么类型(用“位与”操作interest 集合和给定的SelectionKey常量,可以确定某个确定的事件是否在interest 集合中)
boolean isInterestedInAccept  = (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
boolean isInterestedInRead    = interestSet & SelectionKey.OP_READ;
boolean isInterestedInWrite   = interestSet & SelectionKey.OP_WRITE;
 5.ready集合:
   (1)ready 集合是通道已经准备就绪的操作的集合。在一次选择(Selection)之后,你会首先访问这个ready set。
   (2)实现:
selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();
 6.Channel + Selector
   (1)从SelectionKey访问Channel和Selector:
Channel channel  = selectionKey.channel();
Selector selector = selectionKey.selector();
 7.附加的对象:
   (1)作用: 可以将一个对象或者更多信息附着到SelectionKey上,这样就能方便的识别某个给定的通道。例如,可以附加 与通道一起使用的Buffer,或是包含聚集数据的某个对象。
   (2)实现:
a) selectionKey.attach(theObject);
  Object attachedObj = selectionKey.attachment();
b) SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);//注册时附加对象
 8.Selector的通道选择:
   (1)特点:一旦向Selector注册了一或多个通道,就可以调用几个重载的select()方法。这些方法返回你所感兴趣的事件(如连接、接受、读或写)已经准备就绪的那些通道。换句话说,如果你对“读就绪”的通道感兴趣,select()方法会返回读事件已经就绪的那些通道。
   (2)实现:
        a) int select() 阻塞到至少有一个通道在你注册的事件上就绪了。
        b) int select(long timeout) 和select()一样,除了最长会阻塞timeout毫秒(参数)。
        c) int selectNow()不会阻塞,不管什么通道就绪都立刻返回,没有就返回0。
 9.selectedKeys()
   (1)作用:一旦调用了select()方法,并且返回值表明有一个或更多个通道就绪了,然后可以通过调用selector的selectedKeys()方法,访问“已选择键集(selected key set)”中的就绪通道。
   (2)实现:Set selectedKeys = selector.selectedKeys();
   (3)代码示例:
        Set selectedKeys = selector.selectedKeys();
        //SelectionKey.channel();方法返回的通道需要转型成你要处理的类型,如ServerSocketChannel或SocketChannel等。
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(); //注意每次迭代末尾的keyIterator.remove()调用。Selector不会自己从已选择键集中移除SelectionKey实例。
 }
 10.wakeUp()
   (1)作用:
        a)某个线程调用select()方法后阻塞了,即使没有通道已经就绪,也有办法让其从select()方法返回。
        b)只要让其它线程在第一个线程调用select()方法的那个对象上调用Selector.wakeup()方法即可。阻塞在select()方法上的线程会立马返回。
        c)如果有其它线程调用了wakeup()方法,但当前没有线程阻塞在select()方法上,下个调用select()方法的线程会立即“醒来(wake up)”。
 11.close()
   (1)作用:用完Selector后调用其close()方法会关闭该Selector,且使注册到该Selector上的所有SelectionKey实例无效。通道本身并不会关闭。
 12.使用代码示例:
         打开一个Selector,注册一个通道注册到这个Selector上(通道的初始化过程略去),然后持续监控这个Selector的四种事件(接受,连接,读,写)是否就绪。
Selector selector = Selector.open();//打开一个选择器
channel.configureBlocking(false);//设置为非阻塞模式
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);//注册
while(true) { //监听
 int readyChannels = selector.select(); //调用select方法,会阻塞
 if(readyChannels == 0) continue;
 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(); //清除key
 }
}
    
  
   
 注:文章部分内容借鉴于并发编程网
     
    
      
   
     
   


   
  
  
   









1 0