本章目的:

  • 了解新IO操作与传统IO操作的区别
  • 掌握新IO操作中缓冲区的操作原理
  • 了解子缓冲区、只读缓冲区、直接缓冲区的区别及使用
  • 了解新IO中通道的概念,并可以使用通道进行双向操作
  • 掌握文件锁的使用
  • 掌握编码器的作用
  • 了解Selector实现非阻塞服务器的操作

        程序可以通过System.in来获取用户输入的数据,但是,有一个问题,如果用户没有输入信息,则程序会一直等待用户输入,这样会浪费系统资源。为了提升传统IO的操作性能,在JDK1.4之后引入了新IO处理机制 ---- NIO!!!

20.1 Java新IO简介

        NIO提供了一个全新的底层I/O模型。与最初的java.io包中面向流(stream-oriented)的概念不同, NIO中采用了面向块的概念(block-oriented)。这意味着在尽可能的情况下,I/O操作以大的数据块为单位进行,而不是一次一个字节或字符进行。采用这样的操作方式Java的I/O性能已经有了很大的提高。但是也牺牲了Java操作的简单性。

        NIO中提供了与平台无关的非阻塞I/O(nonblocking I/O)。 与面向线程、阻塞式I/O方式相比,多道通信、非阻塞I/O技术可以使应用程序更有效地处理大量连接的情况!!!

=========================================================================================================================

IO的阻塞操作

        IO操作中,接收键盘数据的操作时,只要执行到readLine()方法,程序就要停止等待用户输入数据;在网络编程中,在服务器端使用ServerSocket类的accept()方法时,服务器也是一直处于等待操作,要等待客户端连接。 这两类操作都输入阻塞操作,因为都会让程序暂停执行。

=========================================================================================================================

        新IO并没有在原来的IO基础上开发,而是采用了全新的类和接口,除了原有的功能之外还提供了以下新的特性:

  • 多路选择的非封锁式I/O设施
  • 支持文件锁和内存映射
  • 支持正则表达式的模式匹配设施
  • 字符集编码器和译码器

        在Java新IO中使用Buffer和Channel支持以上的操作~~

20.2 缓冲区与Buffer

        在基本IO操作中所有的操作都是直接以流的形式完成的;而在NIO中所有的操作都要使用到缓冲区处理,且所有的读写操作都是通过缓冲区完成的。

        缓冲区(Buffer)是一个线性的、有序的数据集,只能容纳某种特定的数据类型!!!!!

   20.2.1 Buffer的基本操作

         java.nio.Buffer本身是一个抽象类。其常用方法如下所示:

java 新IO


         在新IO中针对每一种基本数据类型都有一种对应的缓冲区操作类,如下所示:

java 新IO

      在以上7种数据缓冲操作类中,都提供了以下的几组常用方法:

java 新IO

      以上方法针对于各个缓冲区类型都有效!!!

      下面介绍基本的操作流程,以IntBuffer类操作为例。

范例:演示缓冲区的操作流程,同时观擦position、limit和capacity

[java] view plain copy
  1. package org.forfan06.bufferdemo;  
  2. import java.nio.IntBuffer;  
  3. public class IntBufferDemo01{  
  4.     public static void main(String args[]){  
  5.         IntBuffer buf = IntBuffer.allocate(10);    //开辟10个大小的缓冲区  
  6.         System.out.print("1、写入数据之前的position、limit和capacity: ");  
  7.         System.out.println("position = " + buf.position() + ", limit = " + buf.limit() + ", capacity = " + buf.capacity());  
  8.         int temp[] = {579};    //定义整型数组  
  9.         buf.put(3);           //向缓冲区写入数据  
  10.         buf.put(temp);         //向缓冲区写入一组数据  
  11.         System.out.print("2、写入数据之后的position、limit和capacity: ");  
  12.         System.out.println("position = " + buf.position() + ", limit = " + buf.limit() + ", capacity = " + buf.capacity());  
  13.   
  14.         buf.flip();       //重设缓冲区  (在写入之前调用,改变缓冲的指针)  
  15.         System.out.println("3、 准备输出数据时的position、limit和capacity: ");  
  16.         System.out.println("缓冲区中的内容是:");  
  17.         while(buf.hasRemaining()){    //只要缓冲区有内容输出  
  18.             int x = buf.get();     //取出当前内容  
  19.             System.out.print(x + "、");    //输出内容  
  20.         }  
  21.     }  
  22. }  

      程序运行结果:

[java] view plain copy
  1. 1、写入数据之前的position、limit和capacity: position = 0, limit = 10, capacity = 10  
  2. 2、写入数据之后的position、limit和capacity: position = 4, limit = 10, capacity = 10  
  3. 3、 准备输出数据时的position、limit和capacity:   
  4. 缓冲区中的内容是:  
  5. 3579、  

      程序解析:

以上程序首先开辟了10个长度的缓冲区空间,之后向缓冲区写入了4个元素,在每次写入之后position都会有变化;而当调用flip()方法时,position和limit将同时产生变化。这实际上就是面向块的操作。

=============================================

写入的数据不能超过规定的缓冲区大小。

      如果只开辟了3个缓冲区,但是却写入了5个数据,则操作会出现以下错误提示:

Exception in thread "main" java.nio.BufferOverflowException

     at java.nio.HeapIntBuffer.put(Unknown Source)

=============================================


   20.2.2 深入缓冲区操作

      在Buffer中存在一系列的状态变量,这些状态变量随着写入或读取都有可能被该表。在缓冲区中可以使用3个值表示缓冲区的状态。

  • position: 表示下一个缓冲区读取或写入的操作指针,当向缓冲区中写入数据时此指针会改变;指针永远放到写入的最后一个元素之后。例如,如果写入了4个位置的数据,则position会指向第5个位置
  • limit: 表示还有多少数据需要存储或读取, position <= limit
  • capacity:表示缓冲区的最大容量, limit <= capacity。 此值在分配缓冲区时被设置,一般不会更改

      在之前的例子中,随着数据向缓冲区中的写入或是重设,对应的position和limit也会改变。 下面将逐步分析这些操作的步骤。如下图所示:

              java 新IO

   20.2.3 创建子缓冲区

       可以使用各个缓冲区类的slice()方法从一个缓冲区中创建一个新的子缓冲区,子缓冲区与原缓冲区中的部分数据可以共享。

范例: 观察子缓冲区的创建及数据共享

[java] view plain copy
  1. package org.forfan06.bufferdemo;  
  2. import java.nio.IntBuffer;  
  3. public class IntBufferDemo02{  
  4.     public static void main(String args[]){  
  5.         IntBuffer buf = IntBuffer.allocate(10);  //开辟10个大小的缓冲区  
  6.         IntBuffer sub = null;  //定义缓冲区对象  
  7.         for(int i = 0; i < 10; i++){  
  8.             buf.put(2 * i + 1);   //加入10个奇数  
  9.         }  
  10.         buf.position(2);  //主缓冲区指针设置在第3个元素上  
  11.         buf.limit(6);  //主缓冲区limit为6  
  12.         sub = buf.slice();  //开辟子缓冲区  
  13.         for(int i = 0; i < sub.capacity(); i++){  
  14.             int temp = sub.get(i);   //根据下标取得元素  
  15.             sub.put(temp - 1);  //修改子缓冲区内容  
  16.         }  
  17.         buf.flip();  //重设缓冲区  
  18.         buf.limit(buf.capacity());  //设置limit  
  19.         System.out.print("主缓冲区中的内容: ");  
  20.         while(buf.hasRemaining()){       //只要缓冲区有内容则输出  
  21.             int x = buf.get();       //取出当前内容  
  22.             System.out.print(x + "、");    //输出内容  
  23.         }  
  24.     }  
  25. }  

程序运行结果:

[java] view plain copy
  1. 主缓冲区中的内容: 134681013151719、  
     以上程序中,子缓冲区的内容改变之后主缓冲区的内容也跟着一起变化。 

   20.2.4 创建只读缓冲区

     如果现在需要使用到缓冲区中的内容,但又不希望其内容被修改,则可以通过asReadOnlyBuffer()方法创建一个只读缓冲区。但是创建完毕后,此缓冲区不能变为可写状态!!!

范例:创建只读缓冲区

[java] view plain copy
  1. package org.forfan06.bufferdemo;  
  2. import java.nio.IntBuffer;  
  3. public class IntBufferDemo03{  
  4.     public static void main(String args[]){  
  5.         IntBuffer buf = IntBuffer.allocate(10);  
  6.         IntBuffer read = null;  
  7.         for(int i = 0; i < 10; i++){  
  8.             buf.put(2 * i + 1);  
  9.         }  
  10.         read = buf.asReadOnlyBuffer();  
  11.         read.flip();  
  12.         System.out.print("缓冲区中的内容: ");  
  13.         while(read.hasRemaining()){  
  14.             int x = read.get();  
  15.             System.out.print(x + "、");  
  16.         }  
  17.         System.out.println();  
  18.         read.put(30);  //此行会报错, 不可写!!!!  
  19.     }  
  20. }  

程序运行结果:

[java] view plain copy
  1. 缓冲区中的内容: 135791113151719、  
  2. Exception in thread "main" java.nio.ReadOnlyBufferException  
  3.     at java.nio.HeapIntBufferR.put(HeapIntBufferR.java:166)  
  4.     at IntBufferDemo03.main(Unknown Source)  
  5.   
  6. 执行错误  
      从程序的运行结果中可以发现,从之前的缓冲区中创建了一个新的缓冲区,新创建的缓冲区可以输出里面的内容,但是一旦修改内容就会出现异常!!

   20.2.5 创建直接缓冲区

     在缓冲区操作类中,只有ByteBuffer可以创建直接缓冲区,这样Java虚拟机将尽最大努力直接对其执行本机的IO操作。也就是说,在每次调用基础操作系统的一个本机I/O操作之前(或之后),虚拟机都会尽量避免将缓冲区的内容复制到中间缓冲区中(或从中间缓冲区中复制内容)。

      创建直接缓冲区时直接使用ByteBuffer类定义如下方法即可:

java 新IO


范例: 创建直接缓冲区

[java] view plain copy
  1. package org.forfan06.bufferdemo;  
  2. import java.nio.ByteBuffer;  
  3. public class ByteBufferDemo01{  
  4.     public static void main(String args[]){  
  5.         ByteBuffer buf = null;  
  6.         buf = ByteBuffer.allocateDirect(10);  //开辟直接缓冲区  
  7.         byte temp[] = {13579};  
  8.         buf.put(temp);  
  9.         buf.flip();    //重设缓冲区  
  10.         System.out.print("缓冲区中的内容: ");  
  11.         while(buf.hasRemaining()){  
  12.             int x = buf.get();  
  13.             System.out.print(x + "、");  
  14.         }  
  15.     }  
  16. }  

运行结果:

[java] view plain copy
  1. 缓冲区中的内容: 13579、  


20.3 通道

     通道(Channel)可以用来读取和写入数据。通道类似于之前的输入/输出流,但是,程序不会直接操作通道,所有的内容都是先读到或写入到缓冲区中,再通过缓冲区中取得或写入的。

      通道与传统的流操作不同: 传统的流操作分为输入或输出流;而通道本身是双向操作的,既可以完成输入也可以完成输出(通过缓冲区Buffer) 

      Channel本身是一个接口, 此接口定义了以下方法:

java 新IO

   20.3.1 FileChannel

      FileChannel是Channel接口的子类。可以进行文件的读/写操作。此类的常用方法如下所示:

java 新IO


     如果要使用FileChannel,则可以依靠FileInputStream或FileOutputStream类中的getChannel()方法取得输入或输出的通道。下面是使用通道写入文本操作的实例

范例:使用输出通道输出内容

[java] view plain copy
  1. package org.forfan06.channeldemo;  
  2. import java.io.File;  
  3. import java.io.FileOutputStream;  
  4. import java.nio.ByteBuffer;  
  5. import java.nio.channels.FileChannel;  
  6. public class FileChannelDemo01{  
  7.     public static void main(String args[]) throws Exception{  
  8.         String info[] = {"CSDN""Java Studying""www.csdn.net""forfan06"};   //待输出的数据  
  9.         File file = new File("D:" + File.separator + "out.txt");  
  10.         FileOutputStream output = null;    //文件输出流  
  11.         output = new FileOutputStream(file);   //实例化输出流  
  12.         FileChannel fout = null;   //声明输出的通道对象  
  13.         fout = output.getChannel();   //得到输出的文件通道  
  14.         ByteBuffer buf = ByteBuffer.allocate(1024);   //开辟缓冲区  
  15.         for(int i = 0; i < info.length; i++){   //姜内容写入到缓冲区  
  16.             buf.put(info[i].getBytes());  
  17.         }  
  18.         buf.flip();  //重设缓冲区,准备输出  
  19.         fout.write(buf);   //输出  
  20.         fout.close();   //关闭输出通道  
  21.         output.close();   //关闭输出流  
  22.     }  
  23. }  

       上面程序是使用输出通道将内容全部放到缓冲区中,一次性写入到文件中。实际上FileChannel是双向操作的,同时可以完成输出和输入数据的功能

 范例:使用通道进行读写操作

[java] view plain copy
  1. package org.forfan06.channeldemo;  
  2. import java.io.File;  
  3. import java.io.FileInputStream;  
  4. import java.io.FileOutputStream;  
  5. import java.nio.ByteBuffer;  
  6. import java.nio.channels.FileChannel;  
  7. public class FileChannelDemo02{  
  8.     public static void main(String args[]) throws Exception{  
  9.         File file1 = new File("D:" + File.separator + "note.txt");  
  10.         File file2 = new File("D:" + File.separator + "outnote.txt");  
  11.         FileInputStream input = null;  
  12.         FileOutputStream output = null;  
  13.         input = new FileInputStream(file1);   //实例化输入、输出流  
  14.         output = new FileOutputStream(file2);  
  15.         FileChannel fin = null;   //声明输入、输出的通道对象  
  16.         FileChannel fout = null;  
  17.         fin = input.getChannel();   //得到输入、输出的文件通道  
  18.         fout = output.getChannel();  
  19.         ByteBuffer buf = ByteBuffer.allocate(1024);  //开辟缓冲区  
  20.         int temp = 0;   //声明变量接收内容  
  21.         while((temp = fin.read(buf)) != -1){    //如果没有读到文件结尾  
  22.             buf.flip();   //重设缓冲区  
  23.             fout.write(buf);   //输出缓冲区  
  24.             buf.clear();   //清空缓冲区  
  25.         }  
  26.         fin.close();    //关闭输入、输出通道  
  27.         fout.close();   
  28.         input.close();    //关闭输入/输出流  
  29.         output.close();    
  30.     }  
  31. }  


   20. 内存映射

      内存映射可以把文件映射到内存中,这样文件内的数据就可以用内存读/写指令来访问,而不是用InputStream或OutputStream这样的I/O操作类,采用此种方式读取文件的速度是最快的!!!!

=================================================================================================

Java中访问文件内容的4种方法:

  • RandomAccessFile, 随机读取数据,此种访问速度较慢
  • FileInputStream,文件输入流,使用此种方式速度较慢
  • 缓冲读取(例如BufferedReader),使用此种方式访问速度较快
  • 内存映射(MappedByteBuffer), 使用此种方式读取速度最快!!!!!!

=================================================================================================

       要想将文件映射到内存中,可以使用FileChannel类提供的map()方法,此方法定义如下:

java 新IO

    map()方法在使用时要指定映射模式!!!在内存映射中提供了3种模式,此3种模式分别由FileChannel类中的3个常量表示,如下所示:

java 新IO

范例:内存映射

[java] view plain copy
  1. package org.forfan06.channeldemo;  
  2. import java.io.File;  
  3. import java.io.FileInputStream;  
  4. import java.nio.MappedByteBuffer;  
  5. import java.nio.channels.FileChannel;  
  6. public class  FileChannelDemo03{  
  7.     public static void main(String args[]) throws Exception{  
  8.         File file = new File("D:" + File.separator + "personal.txt");  
  9.         FileInputStream input = null;  //文件输入流  
  10.         input = new FileInputStream(file);   //声明输入的通道对象  
  11.         FileChannel fin = null;   //声明输入的通道对象  
  12.         fin = input.getChannel();    //得到输入文件通道  
  13.         MappedByteBuffer mbb = null;    //声明文件的内存映射  
  14.         mbb = fin.map(FileChannel.MapMode.READ_ONLY, 0, file.length());   //将文件映射到内存中  
  15.         byte data[] = new byte[(int) file.length()];   //开辟字节数组,接收数据  
  16.         int foot = 0;   //定义下标  
  17.         while(mbb.hasRemaining()){   //判断是否有数据  
  18.             data[foot++] = mbb.get();   //取出数据  
  19.         }  
  20.         System.out.println(new String(data));  //显示输入的数据  
  21.         fin.close();   //关闭输入通道  
  22.         input.close();   //关闭输入流  
  23.     }  
  24. }  

     尽管创建内存映射文件非常简单,但是,如果使用MappedByteBuffer写入数据就可能非常危险。 因为仅仅是改变数组中的单个元素的内容这样的简单操作,就有可能直接修改磁盘上的具体文件,因为修改数据与数据重新保存到磁盘是一样的!!!!


20.4 文件锁: FileLock

     在Java新IO中提供了文件锁的功能,这样当一个线程将文件锁定之后,其他线程是无法操作此文件的。
     要想进行文件的锁定操作,则要使用FileLock类完成,此类的对象需要依靠FileChannel类进行实例化操作!!!
    
     FileChannel类中提供了以下几个方法取得FileLock类的实例化对象!!!
java 新IO

      上图中所示的文件锁定方式有2种:
  • 共享锁: 允许多个线程进行文件的读取操作
  • 独占锁: 只允许一个线程进行文件的读/写操作
       文件锁定之后需要依靠FileLock类进行解锁。FileLock类的常用方法如下所示:
java 新IO

范例:将D:\csdn.txt文件锁定
[java] view plain copy
  1. package org.forfan06.filelockdemo;  
  2. import java.io.File;  
  3. import java.io.FileOutputStream;  
  4. import java.nio.channels.FileChannel;  
  5. import java.nio.channels.FileLock;  
  6. public class FileLockDemo01{  
  7.     public static void main(String args[]) throws Exception{  
  8.         File file = new File("D:" + File.separator + "csdn.net");  
  9.         FileOutputStream output = null;  //文件输出流  
  10.         output = new FileOutputStream(file);  //实例化输出流  
  11.         FileChannel fout = null;    //声明输出的通道对象  
  12.         fout = output.getChannel();  //得到输入文件通道  
  13.         FileLock lock = fout.tryLock();   //试图获得此通道的文件锁  
  14.         if(lock != null){  
  15.             System.out.println(file.getName() + "文件锁定300秒");  
  16.             Thread.sleep(300000);   //将文件锁定300秒  
  17.             lock.release();  //释放文件锁  
  18.             System.out.println(file.getName() + "文件解除锁定");  
  19.         }  
  20.         fout.close();    //关闭输出通道  
  21.         output.close();     //关闭输出流  
  22.     }  
  23. }  
         上面程序在运行时,将文件进行独占锁定,这样其他线程在锁定的3000秒内是无法对该文件进行读写操作的。

20.5 字符集: Charset

       在Java语言中所有的信息都是以UNICODE进行编码的,但是在计算机的世界里并不只单单存在一种编码,而是存在多种编码;如果对编码处理不当,就有可能产生乱码。

       Java的新IO包中提供了Charset类来负责编码的问题!!Charset类还包含了创建编码器(CharsetEncoder)和创建解码器(CharsetDecoder)的操作。

==========================================================================================

  • 编码器和解码器

       编码和解码实际上是从最早的电报发展起来的,所有的内容如果需要使用电报传送,则必须变为相应的编码,之后再通过指定的编码进行解码的操作。

       在新IO中为了保证程序可以适应各种不同的编码,所以提供了编码器和解码器!!通过解码器程序可以方便地读取各个平台上不同编码的数据,之后再通过编码器将程序的内容以正确的编码进行输出。

==========================================================================================

        Charset类的常用操作如下图所示:

java 新IO


     CharsetEncoder类和CharsetDecoder类的常用方法:

java 新IO


     如果想要对内容进行编码,则首先应该掌握Charset类中所支持的全部编码。 可以参照下面的范例

范例:取得Charset类的全部编码

[java] view plain copy
  1. package org.forfan06.charsetdemo;  
  2. import java.nio.charset.Charset;  
  3. import java.util.Iterator;  
  4. import java.util.Map;  
  5. import java.util.SortedMap;  
  6. public class GetAllCharsetDemo{  
  7.     public static void main(String args[]){  
  8.         SortedMap<String, Charset> all = null;   //声明SortedMap集合  
  9.         all = Charset.availableCharsets();   //取得全部编码  
  10.         Iterator<Map.Entry<String, Charset>> iter = null;  //声明Iterator对象  
  11.         iter = all.entrySet().iterator();  //实例化Iterator对象  
  12.         while(iter.hasNext()){  
  13.             Map.Entry<String, Charset> me = iter.next();  
  14.             System.out.println(me.getKey() + "--->" + me.getValue());  
  15.         }  
  16.     }  
  17. }  
      程序运行结果: 

[java] view plain copy
  1. Big5--->Big5  
  2. Big5-HKSCS--->Big5-HKSCS  
  3. EUC-JP--->EUC-JP  
  4. EUC-KR--->EUC-KR  
  5. GB18030--->GB18030  
  6. GB2312--->GB2312  
  7. GBK--->GBK  
  8. IBM-Thai--->IBM-Thai  
  9. IBM00858--->IBM00858  

       运行上面程序后,将返回全部支持的字符集,Map集合中的key保存的是每种编码的别名,在实际使用时,可以使用forName()方法根据编码的别名实例化CharSet对象。


     下面演示对d:\csdn.txt中的内容通过CharsetEncoder和CharsetDecoder类来使用ISO-8859-1编码的过程。

范例: 编码-解码操作

[java] view plain copy
  1. package org.forfan06.charsetdemo;  
  2. import java.nio.ByteBuffer;  
  3. import java.nio.CharBuffer;  
  4. import java.nio.charset.Charset;  
  5. import java.nio.charset.CharsetEncoder;  
  6. import java.nio.charset.CharsetDecoder;  
  7. public class CharsetCoderDemo01{  
  8.     public static void main(String args[]) throws Exception{  
  9.         Charset latin1 = Charset.forName("ISO-8859-1");  //以ISO-8859-1编码  
  10.         CharsetEncoder encoder = latin1.newEncoder();   //实例化编码、解码对象  
  11.         CharsetDecoder decoder = latin1.newDecoder();  
  12.         //通过CharBuffer类中的wrap()方法,将一个字符串变为CharBuffer类型  
  13.         //CharBuffer cb = CharBuffer.wrap("欢迎光临小站");   此处如果是中文输入,则会抛出UnmappableCharacterException异常~~  
  14.         //客户端的输入编码不对。设置一下即可 --〉  http://daizuan.iteye.com/blog/1112909  
  15.         /* 
  16.          * 另外下面这块可以正常运行 
  17.          *Charset latin2 = Charset.forName("UTF-8"); 
  18.          *CharBuffer cb = CharBuffer.wrap("欢迎光临小站"); 
  19.         */  
  20.         CharBuffer cb = CharBuffer.wrap("Welcome to My Website!!!");  
  21.         ByteBuffer buf = encoder.encode(cb);   //进行编码、解码操作  
  22.         CharBuffer cbuf = decoder.decode(buf);  
  23.         System.out.println(cbuf);   //输出  
  24.     }  
  25. }  

         程序若以被注释掉的语句块运行将 以ISO-8859-1的方式显示中文,这样肯定是无法显示的,会造成乱码问题(运行时抛出UnmappableCharacterException异常)。

====================================================================================

CharsetEncoder和CharsetDecoder经常使用在文件的读写上,以实现文件编码的转换功能!!!!!

====================================================================================

20.6 Selector

      在新IO中Selector是一个极其重要的概念,在原来使用IO和Socket构造网络服务时,所有的网络服务将使用阻塞的方式进行客户端的连接;而如果使用了新IO则可以构造一个非阻塞的网络服务!!

      Selector类的常用方法如下所示:

java 新IO

       在进行非阻塞网络开发时需要使用SelectableChannel类向Select类注册!!!

       而且在新IO中实现网络程序需要依靠ServerSocketChannel类与SocketChannel类,这两个类都是SelectableChannel的子类,SelectableChannel提供了注册Selector的方法和阻塞模式。

       ServerSocketChannel类的常用方法如下所示:

java 新IO

       在使用register()方法时需要指定一个选择器(Selector对象)以及Select域。 其中, Selector对象可以通过Selector中的open()方法取得;而Selector域则在SelectionKey类中定义,如下所示:

java 新IO

       通过下面代码可以建立一个非阻塞的服务器端:

[java] view plain copy
  1. int ports = 8888;  //定义连接的端口号  
  2. Selector selector = Selector.open();  //打开一个连接器  
  3. ServerSocketChannel iniSer = null;  //声明ServerSocketChannel对象  
  4. initSer = ServerSocketChannel.open();   //打开服务器套接字通道  
  5. initSer.configureBlocking(false);  //服务器配置为非阻塞  
  6. ServerSocket initSock = initSer.socket();  //检索与此通道关联的服务器套接字  
  7. InetSocketAddress address = null;  //表示监听地址  
  8. address = new InetSocketAddress(ports);  //实例化绑定地址  
  9. initSock.bind(address);  //绑定地址  
  10. initSer.register(selector, SelectionKey.OP_ACCEPT);  //注册选择器,相当于使用accept()方法接收  
  11. System.out.println("服务器运行,在" + ports + "端口监听");  

      在上面代码中,首先通过Selector.open()方法打开一个选择器,之后通过ServerSocketChannel类中的open()方法打开一个服务器套接字通道。

      程序中最重要就是configureBlocking()方法,将服务器设置为非阻塞状态,之后通过ServerSocketChannel的socket()方法返回一个ServerSocket对象,并在8888端口绑定服务器的监听端口。

      最后,使用register()方法将选择器注册为accept方法,等待客户端连接。


      如果要使用服务器向客户端发送信息,则需要通过SelectionKey类中提供的方法判断服务器的操作状态。要想取得客户端的连接也需要使用SelectionKey类。下面是SelectionKey类中的常用方法:

java 新IO

      因为SelectionKey中提供了4种操作状态,所以4种状态也对应了4个isXxx()方法!!!取得SelectionKey的操作方法如下:

[java] view plain copy
  1. int keysAdd = 0;  //接收一族SelectionKey  
  2. while((keysAdd = selector.select()) > 0){  //选择一组键,相应的通道已经为IO准备就绪  
  3.     Set<SelectionKey> selectedKeys = selector.selectedKeys();  //取出全部生成的key  
  4.     Iterator<SelectionKey> iterator = selectedKeys.iterator();  //实例化Iterator对象  
  5.     while(iterator.hasNext()){  
  6.         SelectionKey key = (SelectionKey) iter.next();    //取出每一个SelectionKey  
  7.         if(key.isAcceptable()){   //判断客户端是否已经连接上  
  8.             //执行服务器的输出操作  
  9.         }  
  10.     }  
  11.     selectedKeys.clear();  //清除全部的key  
  12. }  

     下面,通过Selector创建一个非阻塞的服务器,此服务器向客户端返回当前的系统时间

范例:取得服务器的时间

[java] view plain copy
  1. package org.forfan06.selectordemo;  
  2. import java.net.InetSocketAddress;  
  3. import java.net.ServerSocket;  
  4. import java.nio.ByteBuffer;  
  5. import java.nio.channels.SelectionKey;  
  6. import java.nio.channels.Selector;  
  7. import java.nio.channels.ServerSocketChannel;  
  8. import java.nio.channels.SocketChannel;  
  9. import java.util.Data;  
  10. import java.util.Iterator;  
  11. import java.util.Set;  
  12. public class DateServer{  
  13.     public static void main(String args[]) throws Exception{  
  14.         int ports[] = {800080018002800380058006};  //定义一组连接端口号  
  15.         Selection selector = Selector.open();    //打开一个连接器  
  16.         for(int i = 0; i < ports.length; i++){   //构造服务器的启动信息  
  17.             ServerSocketChannel initSer = null;   //声明ServerSocketChannel对象  
  18.             initSer = ServerSocketChannel.open();  //打开服务器套接字通道  
  19.             initSer.configureBlocking(false);  //服务器配置为非阻塞  
  20.             ServerSocket initSock = initSer.socke();  //检索与此通道关联的服务器套接字  
  21.             InetSocketAddress address = null//表示监听地址  
  22.             address = new InetSocketAddress(ports[i]);  //实例化绑定地址  
  23.             initSock.bind(address);   //绑定地址  
  24.             //注册选择器,相当于使用accept()方法接收客户端连接  
  25.             initSer.register(selector, SelectionKey.OP_ACCEPT);  
  26.             System.out.println("服务器运行,在" + ports[i] + "端口监听");  
  27.         }  
  28.         int keysAdd = 0;  //接收一组SelectionKey  
  29.         while((keysAdd = selector.select()) > 0){   //选择一组键,相应的通道已为IO准备就绪  
  30.             Set<SelectionKey> selectedKeys = selector.selectedKeys();  //取出全部生成的key  
  31.             Iterator<SelectionKey> iterator = selectedKeys.iterator();  //实例化Iterator对象  
  32.             while(iterator.hasNext()){    //迭代  
  33.                 SelectionKey key = (SelectionKey) iterator.next();  
  34.                 if(key.isAcceptable()){      //判断客户端是否已经连接上  
  35.                     ServerSocketChannel server = (ServerSocketChannel) key.channel();   //取得Channel  
  36.                     SocketChannel client = server.accept();  //接收新连接  
  37.                     client.configureBlocking(false);  //设置成非阻塞状态  
  38.   
  39.                     ByteBuffer outBuf = ByteBuffer.allocateDirect(1024);  //开辟直接缓冲区  
  40.                     outBuf.put(("当前时间为: " + new Date()).getBytes());   //向缓冲区设置内容  
  41.                     outBuf.flip();   //重置缓冲区  
  42.                     client.write(outBuf);   //输出信息  
  43.                     client.close();   //关闭输出流  
  44.                 }  
  45.             }  
  46.             SelectedKeys.clear();   //清除全部的key  
  47.         }  
  48.     }  
  49. }  

     上面程序运行之后,程序将在8000、8001、8002、8003、8005、8006这6个端口进行服务器的监听,等待客户端连接,客户端连接后将返回系统的当前时间。

      客户端可以直接使用普通的Socket闯将或使用telnet命令进行连接。

=====================================================================================

  • 服务器运行后并不会退出

在使用Selector实现的服务器操作代码中,程序执行完后并不会像传统的Socket那样立刻关闭服务器,而是会继续等待下一次的连接!!!!!   

=====================================================================================

20.7 本章要点

  1. 使用Java新IO可以提升传统IO的操作性能

  2. 在新IO中所有的读、写操作都是通过缓冲区完成!!!缓冲区中只能容纳特定的数据类型!!!

  3. 在缓冲区中使用position、limit、capacity表示缓冲区的操作状态

  4. 通道提供了双向的读、写操作

  5. 使用内存映射可以提升输入流的性能

  6. 如果一个线程操作一个文件时,不希望其他线程进行访问,则可以通过FileLock锁定文件

  7. 在Java新IO中可以使用CharsetEncoder和CharsetDecoder完成编码的转换操作

  8. 使用Java新IO中的Selector可以构造非阻塞的服务器网络!!!!!

相关文章:

  • 2021-09-24
  • 2022-12-23
  • 2022-12-23
  • 2021-11-28
  • 2021-06-13
  • 2021-09-04
猜你喜欢
  • 2021-04-10
  • 2021-11-29
  • 2021-12-14
  • 2021-09-05
  • 2021-06-10
  • 2022-12-23
相关资源
相似解决方案