Java NIO 基础学习

零、概述

1、传统IO的问题

 

2、非阻塞IO

    NIO的核心精神是针对块编程。而不是流。

 

 

 

一、NIO核心框架

1、缓存区Buffer

    缓冲区是一个数据容器,可以看作内存中一个大的数组。用来存储Channel的同一类型的所有数据。

    每个非布尔基本类型都有一个缓存区: ByteBuffer、MappedBuffer、CharBuffer、DoubleBuffer、FloatBuffer、ShortBuffer、IntBuffer、LongBuffer

    每个类定义了一系列用于将数据移出或移入缓存去的get()put()方法,用于压缩、复制和切片缓冲区的方法,以及用于分配新缓存区和将现有数组包装到缓冲区的静态方法。

 

    所有缓存区类都有一个统一的抽象父类Buffer,它代表了一块内存区域,可以执行一些与内存有关的操作。

    Buffer缓冲区的4个基本属性:

  • capacity:缓存区容量。
  • limit:Buffer上进行的读写操作都不能越过这个下标。Limit一般和capacity相等。Limit代表Buffer中有效数据的长度。
  • position:读/写操作的当前下标。当使用Buffer的相对位置进行读写时,读写会从这个下标进行,操作完成后,Buffer会更新posititon的值。
  • mark:一个临时存放位置的下标。调用mark()方法会将mark设为当前position的值。以后调用reset()会将position属性设置为mark的值。

     这些属性总是满足:0<=mark<=position<=limit<=capacity。对于初始化的Buffer来说,postion=0,limit=capacity。

 

    Buffer缓冲区的三个数据操作:

  • Buffer clear() 清除此缓冲区。把position设为0,把limit设为capacity,一般在把数据写入Buffer前调用。
  • Buffer flip() 反转此缓冲区。把limit设为当前position,把position设为0,一般在从Buffer读出数据前调用。
  • Buffer rewind() 重绕此缓冲区。把position设为0,limit不变,一般在把数据重写入Buffer前调用。

 

链式调用

    buffer支持链式编程。如下代码:

buffer.flip();
buffer.position(23);
buffer.limit(42);

 

    等价于

buffer.flip().position(23).limit(42);

 

 

线程安全

    多个当前线程使用缓冲区是不安全的。如果一个缓冲区由不止一个线程使用,则应该通过适当的同步来控制对该缓冲区的访问。

 

 

只读缓冲区

    Buffer有可能是只读的。对只读Buffer进行写操作将抛出 ReadOnlyBufferException。调用isReadOnly()方法确定Buffer是否为只读。

  • abstract boolean isReadOnly()  告知此Buffer是否为只读。

 

 

 

Buffer的8个实现类

Java NIO 基础学习

 

 

     Java基本数据类型的映射关系:

Java NIO 基础学习

说明:

    MappedByteBuffer表示直接字节缓存区,其内容是文件的内存映射区域。

    映射的字节缓冲区通过FileChannel.map(int mode,long position,int size)方法创建的。此类用特定于内存映射文件区域的操作扩展ByteBuffer类。

    直接(direct)Buffer在内存中分配一段连续的块,并使用本地访问方法读写数据。

    非直接(nondirect)Buffer通过使用Java中的数组访问代码读写数据。

 

 

获取Buffer对象

MappedByteBuffer对象的创建比较特殊。其余的7个Buffer的实现类,都与Java的一种基本类型对应。

都可以通过调用各自类的静态方法allocate(int capacity)来分配一个Buffer。

例:

ByteBuffer byteBuffer = ByteBuffer.allocate(1024);    //创建byte缓存区

CharBuffer charBuffer = CharBuffer.allocate(1024);    //创建char缓存区

 

对于ByteBuffer类,还可以使用allcateDirect(int caoacity)方法分配一个直接缓存区,并可以使用isDirect()方法判断此字节缓存区是否为直接的:

ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024);   //创建直接缓存区

 

也可以通过ByteBuffer的asCharBuffer()等方法,得到其他基本类型的Buffer。

例:

CharBuffer charBuffer = byteBuffer.asCharBuffer();    //取得char缓存区

IntBuffer intBuffer = byteBuffer.asIntBuffer();    //取得int缓存区

 

 

根据Java数组创建缓存区对象

    除了直接分配缓存区外,可以根据各种基本数据数据类型的数组创建,缓存区的修改将导致数组修改,反之亦然。新缓存区的容量和界限将为数组的length,其位置为0。

    根据数组创建缓存区:

  • static ByteBuffer wrap(byte[] array) 将 byte 数组包装到缓冲区中。
  • static ByteBuffer wrap(byte[] array, int offset, int length) 将 byte 数组包装到缓冲区中。
  • static CharBuffer wrap(CharSequence csq) 将字符序列包装到缓冲区中。
  • static CharBuffer wrap(CharSequence csq, int start, int end) 将字符序列包装到缓冲区中。
  • static FloatBuffer wrap(float[] array) 将 float 数组包装到缓冲区中。
  • static FloatBuffer wrap(float[] array, int offset, int length) 将 float 数组包装到缓冲区中。
  • static DoubleBuffer wrap(double[] array) 将 double 数组包装到缓冲区中。
  • static DoubleBuffer wrap(double[] array, int offset, int length) 将 double 数组包装到缓冲区中。
  • static IntBuffer wrap(int[] array) 将 int 数组包装到缓冲区中。
  • static IntBuffer wrap(int[] array, int offset, int length) 将 int 数组包装到缓冲区中。
  • static LongBuffer wrap(long[] array) 将 long 数组包装到缓冲区中。
  • static LongBuffer wrap(long[] array, int offset, int length) 将 long 数组包装到缓冲区中。
  • static ShortBuffer wrap(short[] array) 将 short 数组包装到缓冲区中。
  • static ShortBuffer wrap(short[] array, int offset, int length) 将 short 数组包装到缓冲区中。

    特别的对于CharBuffer可以包装字符串:

  • CharBuffer wrap(String str)

 

每一个类的第1个函数都或许数组直接创建,第二个函数从数组的offset开始,取length长度的数组创建。

例:

byte[] b = new byte[10];
ByteBuffer byteBuffer = ByteBuffer.wrap(b,2,5);    //从数组第2位开始取5个值创建
 

 

 

相反,它们也提供了一个array()方法返回对应的Java类型。

例:

byte[] b = bufferBuffer.array();    //返回bye数组
 

 

 

 

添加数据

对于7个实现类,都提供了5个方法来往缓存区去添加数据。

  • TypeBuffer put(Type t) 追加一个Java基本数据类型。
  • TypeBuffer put(Type[] src) 追加一个数组。
  • TypeBuffer put(Type[] src,int offset,int length) 追加一个数组从offset到length长度的部分。
  • TypeBuffer put(TypeBuffer src) 追加一个缓存区数据
  • TypeBuffer put(int index,byte b) 将数据覆盖在指定的绝对位置上。

 

取得数据

  • Type  get()     取得当前位置的一个Java基本数据类型的数据
  • TypeBuffer get(Type[] dst) 取得一个数组
  • TypeBuffer get(Type[] dst,int offset,int length)  取得一个数组,并赋值到从offset到length长度的部分。
  • Type  get(int index)  取得固定位置的一个Java基本数据类型的数据。

 

 

 

 

2、字符集Charset

字符集Chartset:java.nio.charset包中定义了字符集API,包括字符集Chartset、编码器ChartsetEncoder、解码器ChartsetDecoder。


Java NIO 基础学习

  • Chartset创建出ChartsetDecoder或ChartsetEncoder。
  • ChartsetDecoder,将ByteBuffer解码为CharBuffer。
  • ChartsetEncoder,将CharBuffer编码为ByteBuffer。

解码是面向计算机的,编码是面向人的。

 

ByteBuffer的asCharBuffer()方法可以直接将ByteBuffer里的字节数据转换成字符数据,但Chartset类提供各种字符集,比如UTF-8,ISO-8859-1等映射,可以将CharBuffer里的数据编码成各种格式的编码,然后再由CharsetDecoder解码。

 

从CharBuffer到ByteBuffer的编码转换

    Char使用的是Unicode编码。ByteBuffer编码时,需要指定编码的字符集。常用字符集有UTF-8,GB2312,GBK等。

    创建字符集,使用Chartset的静态方法forName()创建一个Chartset实例:

Chartset chartset = Chartset.forName(“UTF-8”);

 

    然后使用Chartset对象chartset的newEncoder()方法创建一个ChartsetEncoder编码器的实例,如下所示:

ChartsetEncoder encoder = chartset.newEncoder();
 

    最后使用编码器encoder的encode()方法创建一个ChartsetEncoder编码器的实例:

ByteBuffer byteBuffer = encoder.encode(charBuffer);
 

 

从ByteBuffer到CharBuffer的解码转换

    ByteBuffer到CharBuffer进行解码时,也需要首先使用Chartset创建字符集:

Chartset chartset = Chart.forName(“UTF-8”);
 

    使用Chartset对象chartset的newDecoder()方法创建一个ChartsetDecoder解码器的实例,如下所示

ChartsetDecoder decoder = chartset.newDecoder();
 

    使用编码器对象decoder的decode()方法即可进行编码转换,如下所示:

ChartBuffer charBuffer = decoder.decode(byteBuffer);
 

 

编码与字符集

Unicode是编码;UTF-8、GB2312、GBK、ISO-8859-1等为字符集。

国标码系列:

国际码系列:

 

 

 

 

3、通道Channel

    java.nio.channels包中定义了通道API,包括FileChannel、Socket通道SocketChannel、ServerSocket通道ServerSocketChannel、数据报通道DatagramChannel。

    java.io是无法读写Buffer的。使用NIO的Channel通道类可以读写Buffer。Channel是一个连接,可以用于接收或发送数据。Channel连接的是底层的物理设备,它可直接支持设备的读/写,或提供文件锁。对于文件、管道、套接字都存在相应的Channel类。


Java NIO 基础学习
 

7个接口类

    Channel是最顶层的接口。
    ReadableByteChannelWritableByteChannel分别提供对通道读取和写入ByteBuffer数据的功能。
    ByteChannel用来将读取和写入的功能合并。
    ScatteringByteChannelGatheringByteChannel分别提供了批量读取和写入ByteBuffer数组的能力。
    InterruptibleChannel提供了多线程异步关闭的能力。

 

    Channel代表一个可以进行IO操作的通道,该接口定义了以下方法。

  •  void close() 关闭此通道。
  •  boolean isOpen() 判断此通道是否处于打开状态。

    ReadableByteChannel定义了一个可从中读取byte数据的Channel接口,该接口定义了方法read(),用来将字节序列从此通道中读入给定的缓存区:

 

 

    WritableByteChannel

 

    ByteChannel没有新方法,它只是把ReadableByteChannel和WritableByteChannel合并在一起。

 

    ScatteringByteChannel可以一次将数据从通道读入多个ByteBuffer中。

    GatheringByteChannel可以一次将多个Buffer写入通道中。

    InterruptibleChannel用来提供一个被异步关闭的Channel,它覆盖了Channel接口中的关闭方法close()。

    实现此接口的通道是可异步关闭的:如果某个线程阻塞与可中断通道上的IO操作中,则另一个线程可以调用该通道的close方法,这将导致已阻塞线程节后到AsynchronouseCloseException。  

    实现此接口的通道也是中断的:如果某个线程阻塞于可中断通道上的IO操作上,则另一个线程可调用该阻塞线程的interrupt方法。这将导致该通道被关闭,已阻塞线程接收到ClosedByInterruptException,并且设置已阻塞线程的中断状态。

 

3个抽象类

    AbstractInterruptibleChannel:提供了可中断通道的基本实现。

    SelectableChannel:提供了可通过Selector实现多路复用的通道。

    AbstractSelectableChannel:提供了SelectableChannel中抽象函数的实现。

这三个抽象类都实现了InterruptibleChannel接口,所以也拥有多线程下异步关闭的能力。

 

(1)AbstractInterruptibleChannel

此类封装了实现通道异步关闭和中断所需的低级别机制。在调用可能无限期阻塞的 I/O 操作之前和之后,具体的通道类必须分别调用 begin 和 end 方法。为了确保始终能够调用 end 方法,应该在 try ... finally 块中使用这些方法:

 

 boolean completed = false;

 try {

     begin();

     completed = ...;    // Perform blocking I/O operation

return ...;         // Return result

 } finally {

     end(completed);

 }

end 方法的 completed 参数告知 I/O 操作实际是否已完成,也就是说它是否有任何对调用者可见的效果。例如,在读取字节的操作中,当且仅当确实将某些字节传输到调用者的目标缓冲区时此参数才应该为 true。

 

具体的通道类还必须实现 implCloseChannel 方法,其方式为:如果调用此方法的同时,另一个线程阻塞在该通道上的本机 I/O 操作中,则该操作将立即返回,要么抛出异常,要么正常返回。如果某个线程被中断,或者异步地关闭了阻塞线程所处的通道,则该通道的 end 方法会抛出相应的异常。

 

此类执行实现 Channel 规范所需的同步。implCloseChannel 方法的实现不必与其他可能试图关闭通道的线程同步。

SelectableChannel

可通过 Selector 实现多路复用的通道。

 

为了与选择器一起使用,此类的实例必须首先通过 register 方法进行注册。此方法返回一个表示该通道已向选择器注册的新 SelectionKey 对象。

 

向选择器注册后,通道在注销 之前将保持注册状态。注销涉及释放选择器已分配给该通道的所有资源。

 

不能直接注销通道;相反,必须取消 表示通道注册的键。取消某个键要求在选择器的下一个选择操作期间注销通道。可通过调用某个键的 cancel 方法显式地取消该键。无论是通过调用通道的 close 方法,还是中断阻塞于该通道上 I/O 操作中的线程来关闭该通道,都会隐式地取消该通道的所有键。

 

如果选择器本身已关闭,则将注销该通道,并且表示其注册的键将立即无效。

 

一个通道至多只能在任意特定选择器上注册一次。

 

可通过调用 isRegistered 方法来确定是否向一个或多个选择器注册了某个通道。

 

多个并发线程可安全地使用可选择的通道。

 

阻塞模式

 

可选择的通道要么处于阻塞 模式,要么处于非阻塞 模式。在阻塞模式中,每一个 I/O 操作完成之前都会阻塞在其通道上调用的其他 I/O 操作。在非阻塞模式中,永远不会阻塞 I/O 操作,并且传输的字节可能少于请求的数量,或者可能根本不传输字节。可通过调用可选择通道的 isBlocking 方法来确定其阻塞模式。

新创建的可选择通道总是处于阻塞模式。在结合使用基于选择器的多路复用时,非阻塞模式是最有用的。向选择器注册某个通道前,必须将该通道置于非阻塞模式,并且在注销之前可能无法返回到阻塞模式。

 

 

(3)AbstractSelectableChannel

SelectableChannel类,以上讲解的方法都是抽象的,这些抽象方法由AbstractSelectableChannel来提供实现。

此类定义了处理通道注册、注销和关闭机制的各种方法。它会维持此通道的当前阻塞模式及其当前的选择键集。它执行实现 SelectableChannel 规范所需的所有同步。此类中所定义的抽象保护方法的实现不必与同一操作中使用的其他线程同步。

 

 

4个实现类

 

 

 

 

 

选择器Selector

    包括选择器Selector和事件对象SelectionKey。SelectableChannel类提供了阻塞和非阻塞的实现。

    Selector是为了知道通道什么时候可以读或写了。

    Selector对象和SelectableChannel对象互为多路复用器。Selector是非阻塞IO的核心,可以同时监控多个SelectableChannel的IO状况,对于每一个监听到的事件都产生一个SelectionKey对象,其流程如下

 
Java NIO 基础学习
 

 

    使用Selector为SelectableChannel所用,需要经理3个步骤:

    创建选择器Selector

 

static Selector open();
 

 

    注册

    SelectableChannel在打开后,可以使用register()将它注册到特定的选择器,实现通道与选择器的事件绑定。

 

    处理监听事件对象SelectionKey

    注册函数将socket的连接时间注册到了Selector中,使用的是SelectionKey对象的静态变量。SelectableChannel在Selector中注册的标记,即监听事件的类型,包括4种类型:

  • static int OP_ACCEPT  用于套接字接受操作的操作集位。
  • static int OP_CONNECT  用于套接字连接操作的操作集位。
  • static int OP_READ  用于读取操作的操作集位。
  • static int OP_WRITE  用于写入操作的操作集位。

    并不是所有的通道都支持所有的事件类:

Java NIO 基础学习

 

 

 

 

    对事件的监听和处理需要如下步骤:

    (1)注册了事件类型后,使用Selector的select()监听该事件;

    可以使用方法有4个:

  • abstract int select()  选择一组键,其相应的通道已为 I/O 操作准备就绪。
  • abstract int select(long timeout)  选择一组键,其相应的通道已为 I/O 操作准备就绪。
  • abstract int selectNow()  选择一组键,其相应的通道已为 I/O 操作准备就绪。
  • abstract Selector wakeup()  使尚未返回的第一个选择操作立即返回。

    (2)一旦有该事件出触发,就可以使用Selector的selectedKeys()方法返回所有该事件的列表。

    (3)可以循环处理该事件列表,在处理前需要删除当前事件,防止重复处理。

    (4)开始去除当前时间的SelectionKey对象,根据对象的时间类型分别进行处理。与上面的4中注册事件类型相呼应,该类共提供了4个isXXX()函数,用来判断当前被注册事件的类型,函数如下:

  •  boolean isAcceptable() 测试此键的通道是否已准备好接受新的套接字连接。
  •  boolean isConnectable() 测试此键的通道是否已完成其套接字连接操作。
  •  boolean isReadable() 测试此键的通道是否已准备好进行读取。
  •  boolean isWritable() 测试此键的通道是否已准备好进行写入。

    对于每一种事件类型需要分别处理,此时可以根据SelectionKey事件对象取出当前发送事件的通道对象。处理完不同事件后,需要注册新的事件,以循环监听下一次事件。

 

 

非阻塞通道

    使用通道的configureBlocking(false)将通道设置为非阻塞,才可以向Selector注册SelectableChannel对象。

 

 

NIO通道编程详解文件通道 FileChannel

文件通道FileChannel

FileChannel实现了类似于输入输出流的功能,用于读取、写入、锁定和映射文件。

 

创建FileChannel对象

通过RandomAccessFile、FileInputStream和FileOutputStream类实例的getChannel()方法来获取实例。

 
Java NIO 基础学习
 

 

File file = new File("D:/test.txt");
//根据RandomAccessFile取得FileChannel实例。
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
FileChannel fileChannel  = randomAccessFile.getChannel();
 
//根据FileInputStream取得FileChannel(只读)实例。
FileInputStream fileInputStream= new FileInputStream(file);
FileChannel fileChannel = fileInputStream.getChannel();

//根据FileOutputStream取得FileChannel实例。
FileOutputStream fileOutputStream= new FileOutputStream(file);
FileChannel fileChannel = fileOutputStream.getChannel();

说明:    

    若通过“rw”创建实例,则获得的通道将允许进行读取和写入操作。

    通过FileInputStream实例的getChannel()获取的通道将允许读取操作。

    通过FileOutputStream实例的getChannel()获取的通道将允许写入操作。

 

关闭

    必须要关闭FileChannel和相应的RandomAccessFile 、FileInputStream、FileOutputStream。

 

fileChannel.close();

 

 

 

从FileChannel读取数据

  •  int    read(ByteBuffer dst)  将字节序列从此通道读入给定的缓冲区。
  •  long read(ByteBuffer[] dsts)   将字节序列从此通道读入给定的缓冲区。
  •  long read(ByteBuffer[] dsts, int offset, int length)   将字节序列从此通道读入给定缓冲区的子序列中。
  •  int    read(ByteBuffer dst, long position)  从给定的文件位置开始,从此通道读取字节序列,并写入给定的缓冲区。

    读取数据从FileChannel的当前位置开始。可以使用position()返回当前文件的位置,也可以通过size()返回当前文件的大小,还可以通过position(long newPosition)设置文件的位置。因此,若你要在文件末尾进行追加,可以使用size()取得文件大小,再将文件位置设置在size()大小处。

 

 

 

向FileChannel写入数据

  •  int    write(ByteBuffer src)  将字节序列从给定的缓冲区写入此通道。
  •  long write(ByteBuffer[] srcs)   将字节序列从给定的缓冲区写入此通道。
  •  long write(ByteBuffer[] srcs, int offset, int length)  将字节序列从给定缓冲区的子序列写入此通道。
  •  int    write(ByteBuffer src, long position)   从给定的文件位置开始,将字节序列从给定缓冲区写入此通道。

    执行写入操作后,可以使用force(true)函数强制将所有对此通道的文件更新写入包含该文件中,防止缓存。

 

    写入文件从FileChannel的当前文职开始。对于从RandomAccessFile创建的FileChannel对象,可以使用fc.postion(fc.size())来设置到文件末尾;而对于FileOutputStream对象,这种做法无效,如果要追加内容,必须创建可追加的FileOutputStream

FileOutputStream fos = new FileOutputStream(file,true);

 

 

 

 

文件锁

    文件锁机制主要在多线程同时读写某个文件资源时使用。

    FileChannel提供了两种记载机制,分别对应函数lock()tryLock()。两者区别在于,lock()是同步的,直至成功才返回,tryLock()是异步的,无论成不成功都会立即返回。

    共4个方法用来锁定文件:

  •  FileLock lock()  获取对此通道的文件的独占锁定。
  •  FileLock lock(long position, long size, boolean shared)  获取此通道的文件给定区域上的锁定。
  •  FileLock tryLock()  试图获取对此通道的文件的独占锁定。
  •  FileLock tryLock(long position, long size, boolean shared)  试图获取对此通道的文件给定区域的锁定。

    因此,若需要锁定文件操作,可以使用上面的方法来锁定,保证独有的操作。

 

 

内存映射

    MappedByteBuffer是通过FileChannel创建的文件到内存的映射。MappedByteBuffer是一个直接缓存区。

    相比于ByteBuffer来说,它有更多的优点:

  •     内存映射I/O是对Channel缓存区技术的改进。当传输大量的数据时,内存映射I/O速度相对较快。因为它使用虚拟内存把文件传输到进程的地址空间中。
  •     映射内存也称为共享内存,因此可以用于相关进程(均映射同意文件)之间的整块数据传输,这些基础甚至可以不必位于同一系统上,只要每个都可以访问同一文件即可。
  •     当对FileChannel执行映射操作时,把文件映射到内存中时,得到的是一个连接到文件的字节缓存区。当输出缓冲区的内容时,数据将出现在文件中,当读入缓存区时,相当于得到文件中的数据。

    FileChannel提供的映射函数为map(),将文件的部分或全部映射到内存中:

    MappedByteBuffer map(FileChannel.MapMode mode, long position, long size)    将此通道的文件区域直接映射到内存中。

    position表示文件中的位置,映射区域从此位置开始,必须大于等于0;

    size表示要映射的区域大小,必须大于等于0;

    mode用以设定通过下列3种模式将文件区域映射到内存中。

  • static FileChannel.MapMode PRIVATE    专用(写入时拷贝)映射模式。对得到缓存区的更改不会将传播到文件;该更改对映射到同一文件的其他程序是可见的。相反,会创建缓存区已修改部分的专用副本。
  • static FileChannel.MapMode READ_ONLY     只读映射模式。修改缓存区将导致ReadOnlyException
  • static FileChannel.MapMode READ_WRITE 读取/写入映射模式。对得到缓存区的更改最终将传播到文件;该更改对映射到同一文件的其他程序不一定是可见的。

    注意:对于只读映射关系,此通道必须可以进行读取操作;对于读取/写入或专用映射关系,此通道必须可以进行读取和写入操作。

 

例:内存映射打开一个文件,然后将读取的内存映射数据对象直接写入第二个文件:

public class MappedByteBufferTest {
    public static void main(String[] args) {
        try {
            FileChannel fc1 = new FileInputStream(new File("E:/nio1.txt")).getChannel();
            FileChannel fc2 = new FileOutputStream(new File("E:/nio2.txt")).getChannel();
 
            MappedByteBuffer buffer = fc1.map(FileChannel.MapMode.READ_ONLY, 0, fc1.size());
            fc2.write(buffer);

            fc1.close();
            fc2.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

 

 

 

Socket通道 SocketChannel

InetSocketAddress地址类

InetSocketAddress地址类于InetAddress类似,用于创建指向某一个主机和端口的地址对象,有3个方法可以构造一个InetSocketAddress对象:

InetSocketAddress(InetAddress addr, int port) 

          根据 IP 地址和端口号创建套接字地址。

InetSocketAddress(int port) 

          创建套接字地址,其中 IP 地址为通配符地址,端口号为指定值。

InetSocketAddress(String hostname, int port) 

          根据主机名和端口号创建套接字地址。

 

创建的对象可以通过下面的方法取得其中的属性,并可以转换为InetAddress对象。

InetAddress getAddress() 

          获取 InetAddress。

String getHostName() 

          获取 hostname。

int getPort() 

          获取端口号。

 

 

 

套接字类SocketChannel

SocketChannel类似于Socket,可以用于创建套接字对象。SocketChannel具有非阻塞功能。

 

创建SocketChannel对象

SocketChannel socket = SocketChannel.open();

 

设置为非阻塞模式

socket.configureBlocking(false);

 

注册到Selector

socket.register(selector,SelectionKey.OP_CONNECT);

 

开始连接到远程地址

InetSocketAddress ip = new InetSocketAddress(“localhose”,10001);

socket.connect(ip);

 

开始处理读写事件

使用selector的select()开始监听后,第一个监听到的事件就是连接事件。

 

关闭连接

在代码的finally块关闭selector和socket对象。

selector.close();

socket.close();

 

 

SocketServer通道 ServerSocketChannel

SocketServerChannel类似于SocketServer,可以创建服务端套接字对象。SocketServerChannel具有非阻塞功能。

 

创建SocketServerChannel对象

SocketServerChannel server = SocketServerChannel.open();

 

设置为非阻塞模式

server.configureBlocking(false);

 

注册到Selector

server.register(selector,SelectionKey.OP_ACCEPT);

 

开始启动端口监听

使用SocketChannel的socket()函数取得ServerSocket对象,然后再使用ServerSocket的bind()函数绑定指定的地址端口。

InetSocketAddress ip = new InetSocketAddress(“localhose”,10001);

server.socket().bind(ip);

 

开始处理客户端连接事件和读写事件

使用selector的select()开始监听后,第一个监听到的事件客户端的连接事件。

 

 

关闭连接

在代码的finally块关闭selector和socket对象。

selector.close();

socket.close();

 

 

 

数据报通道DatagramChannel

    DatagramChannel于DatagramSocket类似,用于实现非阻塞的数据报通信。与DatagramSocket的使用过程相似。

 

    创建一个DatagramChannel对象

socket = DatagramChannel.open();

 

    开始连接到远程地址

    InetSocketAddress地址对象,使用SocketChannel的connect()函数连接到该地址:

InetSocketAddress ip = new InetSocketAddress(“localhost”,10001);
socket.connect(ip);

 

    发送或接收数据

    对于负责发送数据的客户端来说,使用write()来发送Buffer对象的数据。对于负责接收数据的服务器来说,使用receive()来接收Buffer数据。

socket.write(buffer);    //发送buffer
socket.receive(buffer);  //接受buffer

 

 

 

 

NIO初步 实例代码

1、TCP通讯的服务器与客户端

    使用NIO的服务器端不会发生阻塞,所以多个客户端也连接服务器。

    (a)使用NIO的Server:

 

public class Server {
	public static void main(String[] args) {
		Selector selector = null;
		ServerSocketChannel serverSocketChannel = null;
		try {
			// 创建一个Selector
			selector = Selector.open();

			// 创建一个ServerSocketChannel
			serverSocketChannel = ServerSocketChannel.open();

			// 启动端口监听
			InetSocketAddress ip = new InetSocketAddress(10001);
			serverSocketChannel.socket().bind(ip);

			serverSocketChannel.configureBlocking(false);
			serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

			// 监听事件
			while (true) {
				selector.select();

				// 事件来源列表
				Set<SelectionKey> selectionKeys = selector.selectedKeys();
				Iterator<SelectionKey> iterator = selectionKeys.iterator();
				while (iterator.hasNext()) {
					SelectionKey selectionKey = iterator.next();
					// 删除当前事件
					iterator.remove();

					// 判断事件类型
					if (selectionKey.isAcceptable()) {
						// 连接事件
						ServerSocketChannel server = (ServerSocketChannel) selectionKey.channel();
						SocketChannel socketChannel = server.accept();
						socketChannel.configureBlocking(false);
						socketChannel.register(selector, SelectionKey.OP_READ);
						System.out.println("客户端连接:" + socketChannel.socket().getInetAddress().getHostName() + ":" + socketChannel.socket().getPort());

					} else if (selectionKey.isReadable()) {
						// 读取数据事件
						SocketChannel socketChannel = (SocketChannel) selectionKey.channel();

						// 读取数据
						Charset charset = Charset.forName("UTF-8");

						CharsetDecoder decoder = charset.newDecoder();
						ByteBuffer byteBuffer = ByteBuffer.allocate(50);
						socketChannel.read(byteBuffer);
						byteBuffer.flip();

						String msg = decoder.decode(byteBuffer).toString();

						System.out.println("收到" + msg);

						// 写入数据
						CharsetEncoder encoder = Charset.forName("UTF-8").newEncoder();
						socketChannel.write(encoder.encode(CharBuffer.wrap("server" + msg)));

					}
				}
			}

		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			// 关闭
			try {
				selector.close();
				serverSocketChannel.close();
			} catch (IOException e) {
				e.printStackTrace();
			}

		}
	}
}

 

 

    (b)Client。Client程序有两个线程,一个线程负责监听键盘,第二个线程负责将键盘的输入内容通过NIO发送出去。

 

public class Client {
	/**
	 * 主线程
	 * @param args
	 */
	public static void main(String[] args) {
		ClientRunnable clientRunnable = new ClientRunnable();
		Thread thread = new Thread(clientRunnable);
		thread.start();
		
		//输入,输出流
		BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
		
		String readline = "";
		try {
			while((readline = bufferedReader.readLine())!=null){
				if(readline.equals("byebye-system")){
					clientRunnable.close();
					System.exit(0);
				}
				clientRunnable.sendMessage(readline);
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
		
		
	}
}

 

    

 

public class ClientRunnable implements Runnable{
	private CharsetDecoder decoder = Charset.forName("UTF-8").newDecoder();
	private CharsetEncoder encoder = Charset.forName("UTF-8").newEncoder();
	
	private Selector selector = null;
	private SocketChannel socketChannel = null;
	private SelectionKey selectionKey = null;
	
	public ClientRunnable() {
		//创建selector
		try {
			selector = Selector.open();
			
			//创建Socket并注册
			socketChannel = SocketChannel.open();
			socketChannel.configureBlocking(false);
			
			selectionKey = socketChannel.register(selector,SelectionKey.OP_CONNECT);
			
			//连接到远程地址
			InetSocketAddress ip = new InetSocketAddress("localhost",10001);
			socketChannel.connect(ip);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	@Override
	public void run() {
		try {
			while(true){
				//监听事件
				selector.select();
				//事件来源列表
				Set<SelectionKey> keySet = selector.keys();
				Iterator<SelectionKey> iterator = keySet.iterator();
				while(iterator.hasNext()){
					SelectionKey key = iterator.next();
					//删除当前事件
					iterator.remove();
					
					//判断事件类型
					if(key.isConnectable()){
						//连接事件
						//取得当前SocketChannel对象
						SocketChannel channel = (SocketChannel)key.channel();
						if(channel.isConnectionPending()){
							channel.finishConnect();
						}
						channel.register(selector, SelectionKey.OP_READ);
						System.out.println("连接服务器端成功!");
					}else if(key.isReadable()){
						//读取数据事件
						SocketChannel channel = (SocketChannel)key.channel();
						
						//读取数据
						ByteBuffer buffer = ByteBuffer.allocate(50);
						channel.read(buffer);
						String msg = decoder.decode(buffer).toString();
						System.out.println("收到:"+msg);
					}
					
				}
			}
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			//关闭
			try {
				selector.close();
				socketChannel.close();
				
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
	
	/**
	 * 发送消息
	 * @param msg
	 */
	public void sendMessage(String msg){
		try {
			SocketChannel client = (SocketChannel)selectionKey.channel();
			client.write(encoder.encode(CharBuffer.wrap(msg)));
		} catch (CharacterCodingException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * 关闭客户端
	 */
	public void close(){
		try {
			selector.close();
			socketChannel.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
}

 

 

 

2、UDP通讯的服务器与客户端

    (a)使用NIO的Server

 

public class UDPServer {
	public static void main(String[] args) {
		DatagramChannel datagramChannel = null;
		try {
			datagramChannel = DatagramChannel.open();
			InetSocketAddress ip = new InetSocketAddress("localhost",10002);
			datagramChannel.socket().bind(ip);
			
			//循环监听
			while(true){
				CharsetDecoder decoder = Charset.forName("UTF-8").newDecoder();
				ByteBuffer buffer = ByteBuffer.allocate(10);
				datagramChannel.receive(buffer);
				buffer.flip();
				System.out.println(decoder.decode(buffer).toString());
			}
			
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			try {
				datagramChannel.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
}

 

 

     (b)使用NIO的Client

 

public class UDPClient {
	public static void main(String[] args) {
		DatagramChannel datagramChannel = null;
		try {
			datagramChannel = DatagramChannel.open();
			InetSocketAddress ip = new InetSocketAddress("localhost",10002);
			datagramChannel.connect(ip);
			
			//发送数据
			CharsetEncoder encoder = Charset.forName("UTF-8").newEncoder();
			datagramChannel.write(encoder.encode(CharBuffer.wrap("Hello!!!")));
			
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			try {
				datagramChannel.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
}

 

 

 

 

 

你可能感兴趣的:(java,nio)