Java NIO 学习笔记

摘要:
Java基本数据类型的映射关系:Buffer类基本数据类型说明java.nio.ByteBufferbyte字节Bufferjava.nio.MappedBufferbyte直接字节Buffer,文件内容在缓存区的映射java.nio.CharBufferchar字符Bufferjava.nio.DoubleBufferdoubledoubleBufferjava.nio.FloatBufferfloatfloatBufferjava.nio.ShortBuffershortshortBufferjava.nio.IntBufferintintBufferjava.nio.LongBufferlonglongBuffer这8种类都是抽象类。非直接Buffer通过使用Java中的数组访问代码读写数据。

为了防止无良网站的爬虫抓取文章,特此标识,转载请注明文章出处。LaplaceDemon/SJQ。

http://www.cnblogs.com/shijiaqi1066/p/3344148.html

0 概述

0.1 Socket的问题

传统socket由于需要等待资源,所以会出现阻塞现象。服务器端一般只能使用一个客户端socket对应一个处理线程。

但是有以下局限:

  1. Java虚拟机会为每个线程分配独立的堆栈空间,工作线程数目越多,系统开销越大,而且增加了Java虚拟机调度线程的负担,增加了线程之间同步的复杂性,提高了线程死锁的可能性;
  2. 工作线程的许多时间都浪费在阻塞I/O操作上,Java虚拟机需要频繁的转让CPU使用权。

所以,工作线程不是越多越好,保持适量的工作线程,会提高服务器的并发性能,但是当工作线程的数目到达某个极限,超出了系统的负荷时,反而会降低并发性能,使得多数客户无法快速得到服务器的响应。

Java NIO 学习笔记第1张

0.2 非阻塞IO

JDK 1.4开始,Java提供NIO API来开发高性能网络服务器。NIO API可以让服务器使用一个几个有限的线程来同时处理连接到服务器上的所有客户端。

1 NIO核心框架

1.1 Buffer

缓冲区是一个数据容器,可以看作内存中一个大的数组。

缓冲区从两个方面提高I/O操作效率:

  1. 减少实际的物理读写次数;
  2. 缓冲区在创建时被分配内存,这块内存区域一直被重用,这可以减少动态内存分配和回收内存区域的次数。

1.1.1 Buffer的实现类

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

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

Java NIO 学习笔记第2张

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

Buffer类

基本数据类型

说明

java.nio.ByteBufferbyte字节Buffer
java.nio.MappedBufferbyte直接字节Buffer,文件内容在缓存区的映射
java.nio.CharBufferchar字符Buffer
java.nio.DoubleBufferdoubledouble Buffer
java.nio.FloatBufferfloatfloatBuffer
java.nio.ShortBuffershortshortBuffer
java.nio.IntBufferintintBuffer
java.nio.LongBufferlonglongBuffer

这8种类都是抽象类。 不可以直接new。(下面使用 "TypeBuffer" 统一代表着八种实现类。)

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

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

  • 直接(direct)Buffer在内存中分配一段连续的块,并使用本地访问方法读写数据。
  • 非直接(nondirect)Buffer通过使用Java中的数组访问代码读写数据。

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

1.1.2 Buffer的基本属性

capacity:缓存区容量

position:读/写操作的当前位置。当使用Buffer的相对位置进行读写时,读写会从这个下标进行,操作完成后,Buffer会更新posititon的值。

当写数据到Buffer中时,position表示当前的位置。初始的position值为0。当一个byte、long等数据写到Buffer后, position会向前移动到下一个可插入数据的Buffer单元。position值最大可为capacity – 1 。

当读取数据时,也是从某个特定位置读。当将Buffer从“写”模式切换到“读”模式,position会被重置为0。当从Buffer的position处读取数据时,position向前移动到下一个可读的位置。

position会在每次读写操作是发生变化。

limit:Buffer上进行的读写操作都不能越过该限制。limit一般和capacity相等。limit代表Buffer中有效数据的长度。

在“写”模式下,Buffer的limit表示最多能往Buffer里写多少数据。 写模式下,limit等于Buffer的capacity。

当切换Buffer到“读”模式时, limit表示最多能读到多少数据。因此,当切换Buffer到读模式时,limit会被设置成写模式下的position值。换句话说,能读到之前写入的所有数据(limit被设置成已写数据的数量,这个值在写模式下就是position)

limit会在读写模式切换下发生变化。

mark:一个临时存放位置的标记。调用mark()方法会将mark设为当前position的值。以后调用reset()会将position属性设置为mark的值。

这些属性总是满足:0<=mark<=position<=limit<=capacity

Java NIO 学习笔记第3张

对于初始化的Buffer来说,postion=0,limit=capacity。

Java NIO 学习笔记第4张

Buffer缓冲区的属性方法:

  • int capacity() 返回此缓冲区的容量。
  • int limit() 返回此缓冲区的限制。
  • Buffer limit(int newLimit) 设置此缓冲区的限制。
  • int position() 返回此缓冲区的位置。
  • Buffer position(int newPosition) 设置此缓冲区的位置。
  • Buffer mark() 在此缓冲区的位置设置标记。

Buffer缓冲区的操作方法:

  • Buffer flip() 反转此缓冲区。把limit设为当前position,把position设为0,一般在从Buffer读出数据前调用。此方法的意义在于将Buffer从“写”模式切换为“读”模式。一般filp()方法只能被执行一次。
  • Buffer rewind() 重绕此缓冲区。把position设为0,limit不变,一般在重读Buffer之前调用。此方法的意义在于将Buffer设置为重读状态。
  • Buffer clear() 清除此缓冲区。把position设为0,把limit设为capacity,一般在数据重写入Buffer前调用。此方法的意义在于将Buffer设置为重写状态。若Buffer中有一些未读的数据,那么这些数据将被“遗忘”。

以上方法在Buffer类中就被定义。compact()方法被定义于8个子类中。

  • Buffer compact() 该将所有未读的数据拷贝到Buffer起始处。然后将position设到最后一个未读元素正后面。limit属性依然像clear()方法一样,设置成capacity。现在Buffer准备好写数据了,但是不会覆盖未读的数据。该方法的意义在于继续写入数据,但不会丢失未读数据。

1.1.3 链式调用

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

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

等价于

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

1.1.4 线程不安全与只读

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

Buffer有可能是只读的。对只读Buffer进行写操作将抛出 ReadOnlyBufferException。

调用isReadOnly()方法确定Buffer是否为只读。

  • abstract boolean isReadOnly()

1.1.5 分配缓冲区对象

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

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

  • static TypeBuffer allocate(int capacity) 分配一个新的字节缓冲区。

例:

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

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

  • static TypeBuffer allocateDirect(int capacity) 分配新的直接字节缓冲区。
  • boolean isDirect() 判断此字节缓冲区是否为直接的。

例:

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

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

  • static ByteBufferasTypeBuffer()

例:

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

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

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

根据数组创建缓存区:

API方法说明
static ByteBufferwrap(byte[] array)将 byte 数组包装到缓冲区中。
static ByteBufferwrap(byte[] array, int offset, int length)将 byte 数组包装到缓冲区中。
static CharBufferwrap(CharSequence csq)将字符序列包装到缓冲区中。
static CharBufferwrap(CharSequence csq, int start, int end)将字符序列包装到缓冲区中。
static FloatBufferwrap(float[] array)将 float 数组包装到缓冲区中。
static FloatBufferwrap(float[] array, int offset, int length)将 float 数组包装到缓冲区中。
static DoubleBufferwrap(double[] array)将 double 数组包装到缓冲区中。
static DoubleBufferwrap(double[] array, int offset, int length)将 double 数组包装到缓冲区中。
static IntBufferwrap(int[] array)将 int 数组包装到缓冲区中。
static IntBufferwrap(int[] array, int offset, int length)将 int 数组包装到缓冲区中。
static LongBufferwrap(long[] array)将 long 数组包装到缓冲区中。
static LongBufferwrap(long[] array, int offset, int length)将 long 数组包装到缓冲区中。
static ShortBufferwrap(short[] array)将 short 数组包装到缓冲区中。
static ShortBufferwrap(short[] array, int offset, int length)将 short 数组包装到缓冲区中。

特别的由于String继承了CharSequence,所以CharBuffer可以包装字符串。

CharBuffer  charBuffer = CharBuffer.wrap("abcd");

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

例:

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

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

例:

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

1.1.7 put()与get()

使用put方法向缓存区添加数据:

  • TypeBuffer put(Type t) 相对写。追加一个Java基本数据类型。
  • TypeBuffer put(int index,Tyte b) 绝对写。参数index指定的位置写入一个单元的数据。
  • TypeBuffer put(Type[] src) 追加一个数组。
  • TypeBuffer put(Type[] src,int offset,int length) 追加一个数组从offset到length长度的部分。
  • TypeBuffer put(TypeBuffer src) 追加一个缓存区数据

使用get方法从缓冲区取得数据:

  • Type get() 相对读。从缓冲区的当前位置读取一个单元的数据,读完后把位置+1。
  • Type get(int index) 绝对读。从参数index指定的位置读取一个单元的数据。
  • TypeBuffer get(Type[] dst) 取得一个数组。
  • TypeBuffer get(Type[] dst,int offset,int length) 取得一个数组,并赋值到从offset到length长度的部分。

1.1.8 equals()与compareTo()方法

可以使用equals()和compareTo()方法比较两个Buffer。

equals()

当满足下列条件时,表示两个Buffer相等:

  • 有相同的类型(byte、char、int等)。
  • Buffer中剩余的byte、char等的个数相等。
  • Buffer中所有剩余的byte、char等都相同。

equals()方法只是比较Buffer的一部分,不是每一个在它里面的元素都比较。实际上,它只比较Buffer中的剩余元素。

compareTo()方法

compareTo()方法比较两个Buffer的剩余元素(byte、char等), 如果满足下列条件,则认为一个Buffer“小于”另一个Buffer:

  • 第一个不相等的元素小于另一个Buffer中对应的元素 。
  • 所有元素都相等,但第一个Buffer比另一个先耗尽(第一个Buffer的元素个数比另一个少)。

说明:剩余元素是从 position到limit之间的元素。

1.1.8 基于Buffer的基本读写

使用Buffer读写数据一般遵循以下四个步骤:

  1. 写入数据到Buffer
  2. 调用flip()方法
  3. 从Buffer中读取数据
  4. 调用clear()方法或者compact()方法

当向buffer写入数据时,buffer会记录下写了多少数据。一旦要读取数据,需要通过flip()方法将Buffer从写模式切换到读模式。在读模式下,可以读取之前写入到buffer的所有数据。

一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。有两种方式能清空缓冲区:调用clear()或compact()方法。

API说明
clear()方法会清空整个缓冲区。
compact()方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面。

向Buffer中写数据

写数据到Buffer有两种方式:

  • 通过Buffer的put()方法写到Buffer里。
  • 从Channel写到Buffer。

例:通过put方法写Buffer。

buf.put(127);

put方法有很多版本,允许你以不同的方式把数据写入到Buffer中。例如, 写到一个指定的位置,或者把一个字节数组写入到Buffer。 更多Buffer实现的细节参考JavaDoc。

例:从Channel写到Buffer。

int bytesRead = inChannel.read(buf); //read into buffer.

Channel的读,表示把数据读到Buffer中,本质也就是对Buffer的写。

从Buffer中读取数据
从Buffer中读取数据有两种方式:

  • 使用get()方法从Buffer中读取数据。
  • 从Buffer读取数据到Channel。

例:使用get()方法从Buffer中读取数据的例子

byte aByte = buf.get();

get方法有很多版本,允许你以不同的方式从Buffer中读取数据。例如,从指定position读取,或者从Buffer中读取数据到字节数组。更多Buffer实现的细节参考JavaDoc。

例:从Buffer读取数据到Channel的例子:

int bytesWritten = inChannel.write(buf);

Channel的写,表示把Buffer中的数据写出去,本质也就是对Buffer的读。

1.2 字符集Charset

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

Java NIO 学习笔记第5张

Chartset:创建出ChartsetDecoder或ChartsetEncoder。

ChartsetDecoder:将ByteBuffer解码为CharBuffer。

ChartsetEncoder:将CharBuffer编码为ByteBuffer。

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

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

构造字符集

Charset获取指定的字符集的Charset实例。

static Charset forName(String charsetName) 返回指定字符集的Charset对象。

例:

Charset charset = Charset.forName("GBK");

获取平台默认字符编码:

static Charset defaultCharset() 返回此 Java 虚拟机的默认编码的Charset对象。

Charset类提供了编码和解码的方法:

API说明
ByteBuffer encode(String str)对参数str指定的字符串进行编码,把得到的字节序列存放在一个ByteBuffer对象中,并将其返回。
ByteBuffer encode(CharBuffer cb)对参数cb指定的字符缓冲区中的字符进行编码,把得到的字节序列存放在一个ByteBuffer对象中,并将其返回。
CharBuffer decode(ByteBuffer bb)对参数bb指定的ByteBuffer中的字节序列进行解码,把得到的字符串存放在CharBuffer对象中,并将其返回。

ChartsetEncoder编码器

从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);

ChartsetDecoder 解码器

从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);

1.3 通道Channel

Buffer无法使用Java IO类来进行读写,需要使用Channel来操作数据的流转。Channel是一个连接,用于接收或发送数据。

Channel连接的是底层的物理设备,它可直接支持设备的读/写,或提供文件锁。对于文件、管道、套接字都存在相应的Channel类。

image

java.nio.channels包中定义了Channel类。包括

  • FileChannel(文件通道)
  • SocketChannel(客户端Socket通道)
  • ServerSocketChannel(ServerSocket通道)
  • DatagramChannel(数据报通道)

这些类的抽象结构如下:

Java NIO 学习笔记第7张

Channel接口

Channel是最顶层的接口。Channel代表一个可以进行IO操作的通道,该接口仅定义了两个方法:

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

通道在创建时被打开,一旦关闭就不能重新打开了。

ReadableByteChannel与WritableByteChannel

Channel接口的两个最重要的子接口是ReadableByteChannel和WritableByteChannel。这两个类分别提供对通道读取和写入ByteBuffer数据的功能。

ReadableByteChannel定义了一个可从中读取byte数据的Channel接口,该接口定义了read(ByteBuffer dst)方法,该方法把数据源的数据读入指定的ByteBuffer缓冲区中。

WritableByteChannel接口声明了write(ByteBuffer dst)方法,该方法把参数指定的ByteBuffer缓冲区中的数据写到数据汇中。

ByteChannel

ByteChannel没有新方法,它只是合并了ReadableByteChannel和WritableByteChannel。

ScatteringByteChannel与GatheringByteChannel

ScatteringByteChannel接口扩展了ReadableByteChannel接口。ScatteringByteChannel接口可以一次将数据从通道读入多个ByteBuffer中。

ScatteringByteChannel接口定义的方法:

  • long read(ByteBuffer[] dsts) 将字节序列从此通道读入给定的缓冲区。
  • long read(ByteBuffer[] dsts, int offset, int length) 将字节序列从此通道读入给定缓冲区的子序列中。

GatheringByteChannel接口扩展了WritableByteBuffer接口,允许集中地写入数据。GatheringByteChannel接口可以一次将多个Buffer写入通道中。

GatheringByteChannel接口定义的方法:

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

InterruptibleChannel接口

InterruptibleChannel用来表示一个可以被异步关闭的Channel。

  • InterruptibleChannel是可异步关闭的:如果某个线程阻塞与可中断通道上的IO操作中,则另一个线程可以调用该通道的close方法,这将导致已阻塞线程节后到AsynchronouseCloseException。
  • InterruptibleChannel是可中断的:如果某个线程阻塞于可中断通道上的IO操作上,则另一个线程可调用该阻塞线程的interrupt方法。这将导致该通道被关闭,已阻塞线程接收到ClosedByInterruptException,并且设置已阻塞线程的中断状态。

3个抽象类

  • AbstractInterruptibleChannel提供了可中断通道的基本实现。
  • SelectableChannel提供了可通过Selector实现多路复用的通道。
  • AbstractSelectableChannel提供了SelectableChannel中抽象函数的实现。

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

(1)AbstractInterruptibleChannel

此类封装了实现InterruptibleChannel的异步关闭和中断所需的低级别机制。在调用可能无限期阻塞的 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 方法的实现不必与其他可能试图关闭通道的线程同步。

(2)SelectableChannel

SelectableChannel是一种支持阻塞I/O和非阻塞I/O的通道。在非阻塞模式下,读写数据不会阻塞,并且SlectableChannel可以向Selector注册IO就绪事件。Selector负责监控这些事件,等到事件触发时,比如发送了读就绪事件,SlectableChannel就可以执行读操作了。

SelectableChannel主要有如下方法:

abstract SelectableChannelconfigureBlocking(boolean block)

  • 参数block为true时,表示把SelectableChannel设为阻塞模式;
  • 参数block为false时,表示把SelectableChannel设为非阻塞模式。

默认情况下,SelectableChannel采用阻塞模式。该方法返回SelectableChannel对象本身的引用。

abstract booleanisBlocking()

该方法判断SelectableChannel是否处于阻塞模式。true表示阻塞模式,false表示非阻塞模式。

SelectionKeyregister(Selector sel, int ops)

向给定的选择器注册此通道,返回一个选择键。

abstract SelectionKeyregister(Selector sel, int ops, Object attachement)

向给定的选择器注册此通道,返回一个选择键。attachement是一个与返回的SelectionKey对象关联的附件。

例:socketChannel(客户端Channel,SelectableChannel的一个子类)向Selector注册读就绪写就绪事件。

SelectionKey key = socketChannel.register(selector, SelectionKey.OP_READ|SelectionKey.OP_WRITE);

register()方法返回一个SelectionKey对象,SelectionKey用来跟踪被注册的事件。

第二个register()方法还有一个Object类型的参数attachement,该参数用于为SelectionKey关联一个附件。当被注册事件发生后,需要处理该事件时,可以从SelectionKey中获得这个附件,该附件可用来包含与处理这个事件相关的信息。

MyHandle handle = newMyHandle();

SelectionKey key = socketChannel.regisrer(selector,SelectionKey.OP_READ|SelectionKey.OP_WRITE,handler);
//等价于
SelectionKey key  = socketChannel.register(selector,SelectionKey.OP_READ|SelectionKey.OP_WRITE);
key.attach(handler);    //为SelectionKey关联一个附件。比如可以附加一个事件处理函数。

(3)AbstractSelectableChannel

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

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

选择器Selector

NIO是一种多路复用机制,即一条线程处理多个连接。选择器Selector就是用来处理多个连接的,通常一个Selector对应了多条连接。

Selector是NIO的核心。可以同时监控多个SelectableChannel的IO状况,对于每一个监听到的事件都产生一个SelectionKey对象,其流程如下

Java NIO 学习笔记第8张

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

创建选择器Selector

static Selector open();

SelectableChannel的register方法负责向Selector注册事件,该方法返回一个SelectionKey对象,即事件对象。Selector会监控这些事件是否发生。该监控机制由OS底层提供。

一个Selector对象中会包含了的SelectionKey集合。对于一个新建的Selector 对象,SelectionKey集合为空。

Selector对这些SelectionKey进行了简单分类:

  • all-keys 集合:当前所有向Selector 注册的 SelectionKey 的集合,Selector 的keys() 方法返回该集合。
    •   selected-keys 集合:相关事件已经被Selector 捕获的SelectionKey 的集合。Selector 的selectedKeys() 方法返回该集合。
    •   cancelled-keys 集合:已经被取消的 SelectionKey 的集合。Selector 没有提供访问这种集合的方法。

当执行SelectableChannel 的 register() 方法时,该方法新建一个 SelectionKey, 并把它加入到 Selector 的all-keys 集合中。

如果关闭了与SelectionKey 对象关联的 Channel 对象,或者调用了 SelectionKey 对象的cancel() 方法,这个 SelectionKey 对象就会被加入到 cancelled-keys 集合中,表示这个 SelectionKey 对象已经被取消,在程序下一次执行 Selector 的 select() 方法时,被取消的 SelectionKey 对象将从所有的集合(包括 all-keys 集合, selected-keys集合和cancelled-keys 集合)中删除。

在执行 Selector 的 select() 方法时,如果与 SelectionKey 相关的事件发生了,这个SelectionKey 就被加入到 selected-keys 集合中。程序直接调用 selected-keys 集合的 remove() 方法,或者调用它的 Iterator 的 remove() 方法,都可以从 selected-keys 集合中删除一个 SelectionKey 对象。

程序不允许直接通过集合接口的 remove() 方法删除 all-keys 集合中的 SelectionKey 对象。如果程序试图这样做, 那么会导致 UnsupportedOperationException。

(all-keys 应该是一个内部类, 并且不实现remove()的方法, 继承的对象也是没实现这些方法的;还有可能是new HashSet(){重写remove()方法,直接抛出异常},

如:

private static HashSet keys = new HashSet(){
public boolean remove(Object o){
throw new UnsupportedOperationException();
}
};

Selector 类的主要方法如下:

方法说明
static Selector open() throws IOExceptionSelector 的静态工厂方法,创建一个 Selector 对象。
boolean isOpen()判断 Selector 是否处于打开状态. Selector 对象创建后就处于打开状态, 当调用那个了 Selector 对象的 close() 方法, 它就进入关闭状态.
Set<SelectionKey> keys()返回 Selector 的 all-keys 集合, 它包含了所有与 Selector 关联的 SelectionKey 对象。
int selectNow() throws IOException返回相关事件已经发生的 SelectionKey 对象的数目. 该方法采用非阻塞的工作方式, 返回当前相关时间已经发生的 SelectionKey 对象的数目, 如果没有, 就立即返回 0 。

int select() throws IOException

int select(long timeout) throws IOException

该方法采用阻塞的工作方式,返回相关事件已经发生的 SelectionKey 对象的数目,如果一个也没有,就进入阻塞状态,直到出现以下情况之一,才从 select() 方法中返回。

  • 至少有一个 SelectionKey 的相关事件已经发生;
  • 其他线程调用了 Selector 的 wakeup() 方法, 导致执行 select() 方法的线程立即从 select() 方法中返回.
  • 当前执行 select() 方法的线程被其他线程中断.
  • 超出了等待时间. 该时间由 select(long timeout) 方法的参数 timeout 设定, 单位为毫秒. 如果等待超时, 就会正常返回, 但不会抛出超时异常. 如果程序调用的是不带参数的 select() 方法, 那么永远不会超时, 这意味着执行 select) 方法的线程进入阻塞状态后, 永远不会因为超时而中断。
Selector wakeup()

呼醒执行 Selector 的 select() 方法(也同样设用于 select(long timeout) 方法) 的线程. 当线程A 执行 Selector 对象的 wakeup() 方法时, 如果线程B 正在执行同一个 Selector 对象的 select() 方法, 或者线程B 过一会儿会执行这个 Selector 对象的 select() 方法, 那么线程B 在执行 select() 方法时, 会立即从 select() 方法中返回, 而不会阻塞. 假如, 线程B 已经在 select() 方法中阻塞了, 也会立即被呼醒, 从select() 方法中返回.

wakeup() 方法只能呼醒执行select() 方法的线程B 一次. 如果线程B 在执行 select() 方法时被呼醒后, 以后在执行 select() 方法, 则仍旧按照阻塞方式工作, 除非线程A 再次调用 Selector 对象的 wakeup() 方法.
public void close() throws IOException 关闭 Selector. 如果有其他线程正执行这个Selector 的select() 方法并且处于阻塞状态, 那么这个线程会立即返回. close() 方法使得 Selector 占用的所有资源都被释放, 所有与 Selector 关联的 SelectionKey 都被取消.

SelectionKey 类

SelectionKey 对象用来跟踪注册事件的句柄。在 SelectionKey 对象的有效期间,Selector 会一直监控与 SelectionKey 对象相关的事件,如果事件发生,就会把 SelectionKey 对象加入到 selected-keys 集合中。

在以下情况下,SelectionKey 对象会失效,这意味着 Selector 再也不会监控与它相关的事件了:

  • 程序调用 SelectionKey 的 cancel() 方法;
  • 关闭与 SelectionKey 关联的 Channel;
  • 与 SelectionKey 关联的 Selector 被关闭。

在 SelectionKey 中定义了 4 个具体变量,表示4种事件。

事件常量值说明
static int OP_READ1读就绪事件,表示通道中已经有了可读数据, 可以执行读操作了
static int OP_WRITE4写就绪事件,表示已经可以向通道写数据了。
static int OP_CONNECT8连接就绪事件,表示客户与服务器的连接已经建立成功。
static int OP_ACCEPT16接收连接就绪事件,表示服务器监听到了客户连接,服务器可以接收这个连接了

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

Channel类型OP_ACCEPTOP_CONNECTOP_READOP_WRITE
ServerSocketChannel
SocketChannel
DatagramChannel
Pipe.SourceChannel
Pipe.SinkChannel

以上常量分别占据不同的二进制位,可以通过二进制的或运算 “|” 来将它们进行任意组合。

一个 SelectionKey 对象中包含两种类型的事件。

所有感兴趣的事件: SelectionKey 的 interestOps() 方法返回所有感兴趣的事件。假定返回值为 SelectionKey.OP_WRITE | SelectionKey.OP_READ,就表示这个 SelectionKey 对读就绪事件和写就绪事件感兴趣。与之关联的 Selector 对象会负责监控这些事件。

当通过 SelectableChannel 的 register() 方法注册事件时,可以在参数中指定 SelectionKey 感兴趣的事件。

以下代码表明新建的 SelectionKey 对连接就绪事件和读就绪事件感兴趣:

SelectionKey key = socketChannel.register(selector,  SelectionKey.OP_CONNECT | SelectionKey.OP_READ );

SelectionKey 的 interestOps(int ops) 方法用于为 SelectionKey 对象增加一个感兴趣的事件。

例:关注写就绪事件。

key.interestOps( SelectionKey.OP_WRITE );       

SelectionKey 的 readyOps() 方法返回所有已经发生的事件。

假定返回值为 SelectionKey.OP_WRITE | SelectionKey.OP_READ , 表示读就绪事件和写就绪事件发生了。这意味着与之关联的 SocketChannel 对象可以进行读操作和写操作了。

当程序调用一个 SelectableChannel的register()方法所创建的SelectionKey对象之间的关联关系,SelectionKey的channel()方法返回与之关联的SelectableChannel对象,selector()方法返回与之关联的Selector()对象。

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

(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事件对象取出当前发送事件的通道对象。处理完不同事件后,需要注册新的事件,以循环监听下一次事件。

SelectionKey的主要方法如下:

abstract SelectableChannel channel() 返回与该SelectionKey对象关联的SelectableChannel对象。

abstract Selector selector() 返回与该SelectionKey对象关联的Selector对象。

abstract boolean isValid() 判断该SelectionKey是否有效。当创建

SelectionKey后,就一直处于有效状态。若调用可cancel()方法,或关闭了与之关联的SelectableChannel或Selector对象,即为失效。

abstract void cancel()
使SelectionKey对象失效。该方法把SelectionKey对象加入到与之关联的Selector对象的cancelled-keys集合中。当程序下一次执行Selector的select()方法时,该方法会把SelectionKey对象从Selector对象的all-keys,selected-keys和canncelled-keys这3个集合中删除。

abstract int interestOps() 返回该SelectionKey感兴趣的事件。

abstract SelectionKey interestOps(int ops) 为SelectionKey增加感兴趣的事件。该方法返回当前SelectionKey对象本身的引用。相当于“return this ”

abstract int readyOps() 返回已经就绪的事件。

boolean isReadable() 判断与之关联的SocketChannel的读就绪事件是否已经发生。该方法等价于:

key.readyOps() & OP_READ != 0

boolean isWritable() 判断与之关联的SocketChannel的写就绪事件是否已经发生。该方法等价于:

key.readyOps() & OP_WRITE != 0

boolean isConnectable() 判断与之关联的SocketChannel的连接就绪事件是否已经发生。该方法等价于:

key.readyOps() & OP_CONNECT != 0

boolean isAcceptable() 判断与之关联的ServerSocketChannel的接收就绪事件是否已经发生。该方法等价于:
key.readyOps() & OP_ACCEPT != 0

Object attach(Object ob) 使SelectionKey关联一个附件。一个SelectionKey对象只能关联一个Object类型的附件。若多次调用该方法,则只有最后一个附件与SelectionKey对象关联。调用SelectionKey对象的attachment()方法可获得这个附件。

Object attachment() 获取当前SelectionKey的附加对象。

NIO实例代码

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

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

使用NIO的Server

public classServer {
    public static voidmain(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(); } } } }

使用NIO的Client

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

public classClient {
    /**
     * 主线程
     * @param args
     */
    public static voidmain(String[] args) {
        ClientRunnable clientRunnable = newClientRunnable();
        Thread thread = newThread(clientRunnable);
        thread.start();
        
        //输入,输出流
         BufferedReader bufferedReader = new BufferedReader(newInputStreamReader(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 implementsRunnable{
    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;
    
    publicClientRunnable() {
        //创建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 voidrun() {
        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 voidsendMessage(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 voidclose(){
        try{
            selector.close();
            socketChannel.close();
        } catch(IOException e) {
            e.printStackTrace();
        }
    }
    
}

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

文件通道FileChannel

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

创建FileChannel对象

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

Java NIO 学习笔记第9张

File file = new File("D:/test.txt");
// 根据 RandomAccessFile 取得 FileChannel 实例。 RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw"); FileChannel fileChannel =randomAccessFile.getChannel();
// 根据 FileInputStream 取得 FileChannel (只读)实例。 FileInputStream fileInputStream= newFileInputStream(file); FileChannel fileChannel =fileInputStream.getChannel();
// 根据 FileOutputStream 取得 FileChannel 实例。 FileOutputStream fileOutputStream= newFileOutputStream(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 classMappedByteBufferTest {
    public static voidmain(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

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

使用NIO的Server

public classUDPServer {
    public static voidmain(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();
            }
        }
    }
}

使用NIO的Client

public classUDPServer {
    public static voidmain(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();
            }
        }
    }
}

以下是一篇关于NIO的PPT,非常精彩

http://wenku.baidu.com/view/ad3c0f4a59eef8c75fbfb3b0.html

为了防止无良网站的爬虫抓取文章,特此标识,转载请注明文章出处。LaplaceDemon/SJQ。

http://www.cnblogs.com/shijiaqi1066/p/3344148.html

免责声明:文章转载自《Java NIO 学习笔记》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇Python的路径引用Ubuntu查看crontab运行日志下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

Linux 多线程应用中如何编写安全的信号处理函数

http://blog.163.com/he_junwei/blog/static/1979376462014021105242552/ http://www.ibm.com/developerworks/cn/linux/l-cn-signalsec/ Linux 多线程应用中编写安全的信号处理函数 在开发多线程应用时,开发人员一般都会考虑线程安全,会...

Tomcat性能调优方案

web应用的并发提升,除了负载均衡。在小企业中也可以通过一些软件的上的设置来进行一些优化。下面是一些在服务器上修改tomcat参数的优化方法,非常简单实用!(这些方法通过网络整理的) 1,让Tomcat6 中支持Java语言的特性 NIO( New I/O) 引用 使用NIO在服务器端会有更好的性能,加强服务器端对并发处理的性能。 请注意:很抱歉,在to...

【ES6】迭代器与可迭代对象

ES6 新的数组方法、集合、for-of 循环、展开运算符(...)甚至异步编程都依赖于迭代器(Iterator )实现。本文会详解 ES6 的迭代器与生成器,并进一步挖掘可迭代对象的内部原理与使用方法 一、迭代器的原理 在编程语言中处理数组或集合时,使用循环语句必须要初始化一个变量记录迭代位置,而程序化地使用迭代器可以简化这种数据操作 如何设计一个迭代器...

iframe中positioin:fixed失效问题

页面中嵌套的iframe 内的 position:fixed元素定位失效fixed正常页面 此时position:fixed是根据浏览器窗口定位的,下拉一直位于左上角;以iframe形式嵌入后 此时position:fixed是根据iframe窗口定位的,下拉fixed元素消失;这种问题应该用什么思路解决呢 当时做的是在iframe页面中点击分享图片,弹窗...

史上最全 Java 中各种锁的介绍

什么是锁 在计算机科学中,锁(lock)或互斥(mutex)是一种同步机制,用于在有许多执行线程的环境中强制对资源的访问限制。锁旨在强制实施互斥排他、并发控制策略。      锁通常需要硬件支持才能有效实施。这种支持通常采取一个或多个原子指令的形式,如"test-and-set", "fetch-and-add" or "compare-and-swap...

C#细说多线程(下)

本文主要从线程的基础用法,CLR线程池当中工作者线程与I/O线程的开发,并行操作PLINQ等多个方面介绍多线程的开发。 其中委托的BeginInvoke方法以及回调函数最为常用。而 I/O线程可能容易遭到大家的忽略,其实在开发多线程系统,更应该多留意I/O线程的操作。特别是在ASP.NET开发当中,可能更多人只会留意在客户端使用Ajax或者在服务器端使用U...