对于用ServerSocket以及Socket编写的服务器程序和客户程序,它们在运行过程中常常会阻塞。例如当一个线程执行ServerSocket的accept()方法时,假如没有客户连接,该线程就会一直等到有了客户连接才从accept()方法返回。再例如当线程执行Socket的输入流的read()方法时,如果输入流中没有数据,该线程就会一直等到读入了足够的数据才从read()方法返回。
假如服务器程序需要同时与多个客户通信,就必须分配多个工作线程,让它们分别负责与某个客户通信,当然每个工作线程都有可能经常处于长时间的阻塞状态。
从JDK1.4版本开始,引入了非阻塞的通信机制。服务器程序接收客户连接、客户程序请求建立与服务器的连接,以及服务器程序和客户程序收发数据的操作都可以按非阻塞的方式进行。服务器程序只需要创建一个线程,就能完成同时与多个客户通信的任务。
非阻塞的通信机制主要由java.nio包(新I/O包)中的类实现,主要的类包括ServerSocketChannel、SocketChannel、Selector、SelectionKey和ByteBuffer等。
1、线程阻塞的概念
在生活中,最常见的阻塞现象是公路上汽车的堵塞。汽车在公路上快速行驶,如果前方交通受阻,就只好停下来等待,等到公路顺畅,才能恢复行驶。
线程在运行中也会因为某些原因而阻塞。所有处于阻塞状态的线程的共同特征是:放弃CPU,暂停运行,只有等到导致阻塞的原因消除,才能恢复运行;或者被其他线程中断,该线程会退出阻塞状态,并且抛出InterruptedException。
1.1、线程阻塞的原因
导致线程阻塞的原因主要有以下方面:
- 线程执行了Thread.sleep(int n)方法,线程放弃CPU,睡眠n ms,然后恢复运行。
- 线程要执行一段同步代码,由于无法获得相关的同步锁,只好进入阻塞状态,等到获得了同步锁,才能恢复运行。
- 线程执行了一个对象的wait()方法,进入阻塞状态,只有等到其他线程执行了该对象的notify()或notifyAll()方法,才可能将其唤醒。
- 线程执行I/O操作或进行远程通信时,会因为等待相关的资源而进入阻塞状态。例如当线程执行System.in.read()方法时,如果用户没有向控制台输入数据,则该线程会一直等读到了用户的输入数据才从read()方法返回。
进行远程通信时,在客户程序中,线程在以下情况下可能进入阻塞状态:
- 请求与服务器建立连接时,即当线程执行Socket的带参数的构造方法,或执行Socket的connect()方法时,会进入阻塞状态,直到连接成功,此线程才从Socket的构造方法或connect()方法返回。
- 线程从Socket的输入流读入数据时,如果没有足够的数据,就会进入阻塞状态,直到读到了足够的数据,或者到达输入流的末尾,或者出现了异常,才从输入流的read()方法返回或异常中断。输入流中有多少数据才算足够呢?这要看线程执行的read()方法的类。(1)int read():只要输入流中有1字节,就算足够。(2)int read(byte[] buff):只要输入流中的字节数目与参数buff数组的长度相同,就算足够。(3)String readLine():只要输入流中有1行字符串,就算足够。值得注意的是,InputStream类并没有readLine()方法,在过滤流BufferedReader类中才有此方法。
- 线程向Socket的输出流写一批数据时,可能会进入阻塞状态,等到输出了所有的数据,或者出现异常,才从输出流的write()方法返回或异常中断。
- 如果调用Socket的setSoLinger()方法设置了关闭Socket的延迟时间,那么当线程执行Socket的close()方法时,会进入阻塞状态,直到底层Socket发送完所有剩余数据,或者超过了setSoLinger()方法设置的延迟时间,才从close()方法返回。
在服务器程序中,线程在以下情况下可能会进入阻塞状态:
- 线程执行ServerSocket的accept()方法,等待客户的连接,直到接收到了客户连接,才从accept()方法返回。
- 线程从Socket的输入流读入数据时,如果输入流没有足够的数据,就会进入阻塞状态。
- 线程向Socket的输出流写一批数据时,可能会进入阻塞状态,等到输出了所有的数据,或者出现异常,才从输出流的write()方法返回或异常中断。
由此可见,无论是在服务器程序还是客户程序中,当通过Socket的输入流和输出流来读写数据时,都可能进入阻塞状态。这种可能出现阻塞的输入和输出操作被称为阻塞I/O。与此对照,如果执行输入和输出操作时,不会发生阻塞,则称为非阻塞I/O。
1.2、服务器程序用多线程处理阻塞通信的局限
(创建多线程的服务器)已经介绍了服务器程序用多线程来同时处理多个客户连接的方式。服务器程序的处理流程如下图所示。
主线程负责接收客户的连接。在线程池中有若干工作线程,它们负责处理具体的客户连接。每当主线程接收一个客户连接,主线程就会把与这个客户交互的任务交给一个空闲的工作线程去完成,主线程继续负责接收下一个客户连接。
在上图中,用粗体框标识的步骤为可能引起阻塞的步骤。从图中可以看出,当主线程接收客户连接,以及工作线程执行I/O操作时,都有可能进入阻塞状态。
服务器程序用多线程来处理阻塞I/O,尽管能满足同时响应多个客户请求的需求,但是有以下局限:
(1)Java虚拟机会为每个线程都分配独立的堆栈空间,工作线程数目越多,系统开销就越大,而且增加了Java虚拟机调度线程的负担,增加了线程之间同步的复杂性,提高了线程死锁的可能性。
(2)工作线程的许多时间都浪费在阻塞I/O操作上,Java虚拟机需要频繁地转让CPU的使用权,使进入阻塞状态的线程放弃CPU,再把CPU分配给处于可运行状态的线程。
由此可见,工作线程并不是越多越好。如下图所示,保持适量的工作线程,会提高服务器的并发性能,但是当工作线程的数目到达某个极限,超出了系统的负荷时,反而会降低并发性能,使得多数客户无法快速得到服务器的响应。
1.3、非阻塞通信的基本思想
假如同时要做两件事:烧开水和煮粥。烧开水的步骤如下:
煮粥的步骤如下:
为了同时完成两件事,一种方案是同时请两个人分别做其中的一件事,这相当于采用多线程来同时完成多个任务。还有一种方案是让一个人同时完成两件事,这个人应该善于利用一件事的空闲时间去做另一件事,这个人一刻也不应该闲着。为了同时完成两件事,一种方案是同时请两个人分别做其中的一件事,这相当于采用多线程来同时完成多个任务。还有一种方案是让一个人同时完成两件事,这个人应该善于利用一件事的空闲时间去做另一件事,这个人一刻也不应该闲着。
这个人不断监控烧水和煮粥的状态,如果发生了“水烧开”“粥煮开”或“粥煮熟”事件,就去处理这些事件,处理完一件事后继续监控烧水和煮粥的状态,直到所有的任务都完成。
以上工作方式也可以被运用到服务器程序中,服务器程序只需要一个线程就能同时接收客户的连接、接收各个客户发送的数据,以及向各个客户发送响应数据。服务器程序的处理流程如下:
以上处理流程采用了轮询的工作方式,当某一种操作就绪,就执行该操作,否则就查看是否还有其他就绪的操作可以执行。线程不会因为某一个操作还没有就绪,就进入阻塞状态,一直傻傻地在那里等待这个操作就绪。
为了使轮询的工作方式顺利进行,接收客户的连接、从输入流读数据,以及向输出流写数据的操作都应该以非阻塞的方式运行。所谓非阻塞,指当线程执行这些方法时,如果操作还没有就绪,就立即返回,而不会一直等到操作就绪。例如当线程接收客户连接时,如果没有客户连接,就立即返回;再例如当线程从输入流中读数据时,如果输入流中还没有数据,就立即返回,或者如果输入流还没有足够的数据,那么就读取现有的数据,然后返回。值得注意的是,以上while循环条件中的操作还是按照阻塞方式进行的,如果未发生任何事件,就会进入阻塞状态,直到接收连接就绪事件、读就绪事件或写就绪事件中至少有一个事件发生,此时才会执行while循环体中的操作。
2、非阻塞通信API的用法
java.nio.channels包提供了支持非阻塞通信的类,如下所述:
- ServerSocketChannel:ServerSocket的替代类,支持阻塞通信与非阻塞通信。
- SocketChannel:Socket的替代类,支持阻塞通信与非阻塞通信。
- Selector:为ServerSocketChannel监控接收连接就绪事件,为SocketChannel监控连接就绪、读就绪和写就绪事件。
- SelectionKey:代表ServerSocketChannel以及SocketChannel向Selector注册事件的句柄。当一个SelectionKey对象位于Selector对象的selected-keys集合中,就表示与这个SelectionKey对象相关的事件发生了。ServerSocketChannel及SocketChannel都是SelectableChannel的子类,如图下图所示。SelectableChannel类及其子类都能委托Selector来监控它们可能发生的一些事件,这种委托过程也被称为注册事件过程。
ServerSocketChannel向Selector注册接收连接就绪事件的代码如下:
SelectionKey key = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
SelectionKey类的一些静态常量表示事件类型,ServerSocketChannel只可能发生一种事件:
- SelectionKey.OP_ACCEPT:接收连接就绪事件,表示至少有了一个客户连接,服务器可以接收这个连接。
SocketChannel可能发生以下3种事件。
- SelectionKey.OP_CONNECT:连接就绪事件,表示客户与服务器的连接已经建立成功。
- SelectionKey.OP_READ:读就绪事件,表示输入流中已经有了可读数据,可以执行读操作了。
- SelectionKey.OP_WRITE:写就绪事件,表示已经可以向输出流写数据了。
SocketChannel提供了接收和发送数据的方法。
- read(ByteBuffer buffer):接收数据,把它们存放到参数指定的ByteBuffer中。
- write(ByteBuffer buffer):把参数指定的ByteBuffer中的数据发送出去。
ByteBuffer表示字节缓冲区,SocketChannel的read()和write()方法都会操纵ByteBuffer。ByteBuffer类继承于Buffer类。ByteBuffer中存放的是字节,为了把它们转换为字符串,还需要用到Charset类,Charset类代表字符编码,它提供了把字节流转换为字符串(解码过程)和把字符串转换为字节流(编码过程)的实用方法。
ByteBuffer表示字节缓冲区,SocketChannel的read()和write()方法都会操纵ByteBuffer。ByteBuffer类继承于Buffer类。ByteBuffer中存放的是字节,为了把它们转换为字符串,还需要用到Charset类,Charset类代表字符编码,它提供了把字节流转换为字符串(解码过程)和把字符串转换为字节流(编码过程)的实用方法。
2.1、缓冲区
数据输入和输出往往是比较耗时的操作。缓冲区(Buffer)从两个方面提高I/O操作的效率:
- 减少实际的物理读写次数。
- 缓冲区在创建时被分配内存,这块内存区域一直被重用,这可以减少动态分配和回收内存区域的次数。
旧I/O类库(对应java.io包)中的BufferedInputStream、BufferedOutputStream、BufferedReader和BufferedWriter在其实现中都运用了缓冲区。java.nio包公开了Buffer API,使得Java程序可以直接控制和运用缓冲区。下图显示了Buffer类的层次结构:
所有的缓冲区都有以下属性:
- 容量(capacity):表示缓冲区可以保存多少数据。
- 极限(limit):表示缓冲区的当前终点,不能对缓冲区中超过极限的区域进行读写操作。极限是可以被修改的,这有利于缓冲区的重用。例如,假定容量为100的缓冲区已经填满了数据,接着程序在重用缓冲区时,仅仅将10个新的数据写入缓冲区中从位置0到10的区域,这时可以将极限设为10,这样就不能读取位置从11到99的原先的数据了。极限是一个非负整数,不应该大于容量。
- 位置(position):表示缓冲区中下一个读写单元的位置,每次读写缓冲区的数据时,该值都会改变,为下一次读写数据做准备。位置是一个非负整数,不应该大于极限。
如下图所示,以上3个属性的关系为:容量>=极限>=位置>=0:
缓冲区提供了用于改变以上3个属性的方法:
- clear():把极限设为容量,把位置设为0。
- flip():把极限设为位置,把位置设为0。
- rewind():不改变极限,把位置设为0。
Buffer类的remaining()方法返回缓冲区的剩余容量,取值等于极限-位置。Buffer类的compact()方法删除缓冲区内从0到当前位置position的内容,然后把从当前位置position到极限limit的内容拷贝到0到limit-position的区域内,当前位置position和极限limit的取值也做相应的变化,如下图所示:
java.nio.Buffer类是一个抽象类,不能被实例化。它共有8个具体的缓冲区类,其中最基本的缓冲区是ByteBuffer,它存放的数据单元是字节。ByteBuffer类并没有提供公开的构造方法,但是提供了两个获得ByteBuffer实例的静态工厂方法:
- allocate(int capacity):返回一个ByteBuffer对象,参数capacity指定缓冲区的容量。
- directAllocate(int capacity):返回一个ByteBuffer对象,参数capacity指定缓冲区的容量。该方法返回的缓冲区被称为直接缓冲区,它与当前操作系统能够更好地耦合,因此能进一步提高I/O操作的速度。但是分配直接缓冲区的系统开销很大,因此只有在缓冲区较大并且长期存在,或者需要经常重用时,才使用这种缓冲区。
除boolean类型以外,每种基本类型都有对应的缓冲区类,包括CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer和ShortBuffer。这几个缓冲区类都有一个能够返回自身实例的静态工厂方法allocate(int capacity)。在CharBuffer中存放的数据单元为字符,在DoubleBuffer中存放的数据单元为double数据,以此类推。还有一种缓冲区是MappedByteBuffer,它是ByteBuffer的子类。MappedByteBuffer能够把缓冲区和文件的某个区域直接映射。
所有具体缓冲区类都提供了读写缓冲区的方法:
- get():相对读。从缓冲区的当前位置读取一个单元的数据,读完后把位置加1。
- get(int index):绝对读。从参数index指定的位置读取一个单元的数据。
- put(单元数据类型 data):相对写。向缓冲区的当前位置写入一个单元的数据,写完后把位置加1。
- put(int index,单元数据类型 data):绝对写。向参数index指定的位置写入一个单元的数据。
ByteBuffer类不仅可以读取和写入一个单元的字节,还可以读取和写入int、char、float和double等基本类型的数据,例如:
- getInt()
- getInt(int index)
- putInt(int value)
- putInt(int index,int value)
- getChar()·getChar(int index)
- putChar(char value)·
- putChar(int index,char value)
以上不带index参数的方法会在当前位置读取或写入数据,称为相对读写。带index参数的方法会在index参数指定的位置读取或写入数据,称为绝对读写。
ByteBuffer类还提供了用于获得缓冲区视图的方法,例如:
- ShortBuffer asShortBuffer()
- CharBuffer asCharBuffer()
- IntBuffer asIntBuffer()
- FloatBuffer asFloatBuffer()
例如以下程序代码获取ByteBuffer的CharBuffer缓冲区视图:
CharBuffer charBuffer = byteBuffer.asCharBuffer();
以上CharBuffer视图和底层ByteBuffer共享同样的数据,修改CharBuffer视图的数据,会反映到底层ByteBuffer。不过,CharBuffer视图和底层ByteBuffer有各自独立的位置position、极限limit和容量capacity属性。
2.2、字符编码Charset
java.nio.Charset类的每个实例代表特定的字符编码类型。如下图所示:
把字节序列转换为字符串的过程称为解码;把字符串转换为字节序列的过程称为编码。
Charset类提供了编码与解码的方法:
- ByteBuffer encode(String str):对参数str指定的字符串进行编码,把得到的字节序列存放在一个ByteBuffer对象中,并将其返回。
- ByteBuffer encode(CharBuffer cb):对参数cb指定的字符缓冲区中的字符进行编码,把得到的字节序列存放在一个ByteBuffer对象中,并将其返回。
- CharBuffer decode(ByteBuffer bb):对参数bb指定的ByteBuffer中的字节序列进行解码,把得到的字符序列存放在一个CharBuffer对象中,并将其返回。
Charset类的静态forName(String encode)方法返回一个Charset对象,它代表参数encode指定的编码类型。例如以下代码创建了一个代表“GBK”编码的Charset对象:
Charset charset = Charset.forName("GBK");
Charset类还有一个静态方法defaultCharset(),它返回代表本地平台的默认字符编码的Charset对象。
2.3、通道
通道(Channel)用来连接缓冲区与数据源或数据汇(即数据目的地)。如下图所示,数据源的数据经过通道到达缓冲区,缓冲区的数据经过通道到达数据汇。
下图展示了通道的主要层次结构:
java.nio.channels.Channel接口只声明了两个方法:
- close():关闭通道。
- isOpen():判断通道是否打开。
通道在创建时被打开,一旦关闭通道,就不能重新打开它。
Channel接口的两个最重要的子接口是ReadableByteChannel和WritableByteChannel。ReadableByteChannel接口声明了read(ByteBuffer dst)方法,该方法把数据源的数据读入参数指定的ByteBuffer缓冲区中。WritableByteChannel接口声明了write(ByteBuffer src)方法,该方法把参数指定的ByteBuffer缓冲区中的数据写到数据汇中。下图展示了Channel与Buffer的关系。ByteChannel接口是一个便利接口,它扩展了ReadableByteChannel和WritableByteChannel接口,因而同时支持读写操作:
ScatteringByteChannel接口扩展了ReadableByteChannel接口,允许分散地读取数据。分散读取数据指单个读取操作能填充多个缓冲区。ScatteringByteChannel接口声明了read(ByteBuffer[] dsts)方法,该方法把从数据源读取的数据依次填充到参数指定的ByteBuffer数组的各个ByteBuffer中。GatheringByteChannel接口扩展了WritableByteChannel接口,允许集中地写入数据。集中写入数据指单个写操作能把多个缓冲区的数据写到数据汇。GatheringByteChannel接口声明了write(ByteBuffer[] srcs)方法,该方法依次把参数指定的ByteBuffer数组的每个ByteBuffer中的数据写到数据汇。分散读取和集中写数据能够进一步提高输入和输出操作的速度。
FileChannel类是Channel接口的实现类,代表一个与文件相连的通道。该类实现了ByteChannel、ScatteringByteChannel和GatheringByteChannel接口,支持读操作、写操作、分散读操作和集中写操作。FileChannel类没有提供公开的构造方法,因此客户程序不能用new语句来构造它的实例。不过,在FileInputStream、FileOutputStream和RandomAccessFile类中提供了getChannel()方法,该方法返回相应的FileChannel对象。
SelectableChannel也是一种通道,它不仅支持阻塞的I/O操作,还支持非阻塞的I/O操作。SelectableChannel有两个子类:ServerSocketChannel和SocketChannel。SocketChannel还实现了ByteChannel接口,具有read(ByteBuffer dst)和write(ByteBuffer src)方法。
2.4、SelectableChannel类
SelectableChannel是一种支持阻塞I/O和非阻塞I/O的通道。在非阻塞模式下,读写数据不会阻塞,并且SelectableChannel可以向Selector注册读就绪和写就绪等事件。Selector负责监控这些事件,等到事件发生时,比如发生了读就绪事件,SelectableChannel就可以执行读操作了。
SelectableChannel的主要方法如下:
public SelectableChannel configureBlocking(boolean block) throws IOException
当参数block为true时,表示把 SelectableChannel设为阻塞模式,当参数block为false时,表示把 SelectableChannel设为非阻塞模式。在默认情况下,SelectableChannel采用阻塞模式。该方法返回SelectableChannel对象本身的引用,相当于“return this”。SelectableChannel的isBlocking()方法判断SelectableChannel是否处于阻塞模式,如果返回true,则表示处于阻塞模式,否则表示处于非阻塞模式。public SelectionKey register(Selector sel,int ops)throws ClosedChannelException
public SelectionKey register(Selector sel,int ops,Object attachment)throws ClosedChannelException
以上两个方法都向Selector注册事件,例如,以下socketChannel(SelectableChannel的一个子类)向Selector注册读就绪和写就绪事件:
SelectionKey key = socketChannel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
register()方法返回一个SelectionKey对象,SelectionKey被用来跟踪被注册的事件。第2个register()方法还有一个Object类型的参数attachment,它被用于为SelectionKey关联一个附件,当被注册事件发生后,需要处理该事件时,可以从SelectionKey中获得这个附件,该附件可用来包含与处理这个事件相关的信息。以下这两段代码是等价的。
2.5、ServerSocketChannel类
ServerSocketChannel从SelectableChannel中继承了configureBlocking()和register()方法。ServerSocketChannel是ServerSocket的替代类,也具有负责接收客户连接的accept()方法。ServerSocketChannel并没有public类型的构造方法,必须通过它的静态方法open()来创建ServerSocketChannel对象。每个ServerSocketChannel对象都与一个ServerSocket对象关联。ServerSocketChannel的socket()方法返回与它关联的ServerSocket对象。可通过以下方式把服务器进程绑定到一个本地端口:
serverSocketChannel.socket().bind(port);
ServerSocketChannel的主要方法如下:
public static ServerSocketChannel open()throws IOException
这是ServerSocketChannel类的静态工厂方法,它返回一个ServerSocketChannel对象。这个对象没有与任何本地端口绑定,并且处于阻塞模式。public SocketChannel accept()throws IOException
类似于ServerSocket的accept()方法,用于接收客户的连接。如果ServerSocketChannel处于非阻塞模式,当没有客户连接时,该方法就立即返回null。如果ServerSocketChannel处于阻塞模式,当没有客户连接时,它就会一直阻塞下去,直到有客户连接就绪,或者出现了IOException。值得注意的是,该方法返回的SocketChannel对象处于阻塞模式,如果希望把它改为非阻塞模式,就必须执行以下代码:socketChannel.configureBlocking(false);
public final int validOps()
返回ServerSocketChannel所能产生的事件,这个方法总是返回SelectionKey.OP_ACCEPT。public ServerSocket socket()
返回与ServerSocketChannel关联的ServerSocket对象。每个ServerSocketChannel对象都与一个ServerSocket对象关联。
2.6、SocketChannel类
SocketChannel可以被看作是Socket的替代类,但它比Socket具有更多的功能。SocketChannel不仅从SelectableChannel父类中继承了configureBlocking()和register()方法,而且实现了ByteChannel接口,因此具有用于读写数据的read(ByteBuffer dst)和write(ByteBuffer src)方法。SocketChannel没有public类型的构造方法,必须通过它的静态方法open()来创建SocketChannel对象。
SocketChannel的主要方法如下:
- public static SocketChannel open() throws IOException
- public static SocketChannel open(SocketAddress remote)throws IOException
SocketChannel的静态工厂方法open()负责创建SocketChannel对象,第2个带参数的构造方法还会建立与远程服务器的连接。在阻塞模式以及非阻塞模式下,第2个open()方法有不同的行为,这与SocketChannel类的connect()方法类似。
以下两段代码是等价的:
值得注意的是,open()方法返回的SocketChannel对象处于阻塞模式,如果希望把它改为非阻塞模式,就必须执行以下代码:
socketChannel.configureBlocking(false);
public final int validOps()
返回SocketChannel所能产生的事件,这个方法总是返回以下值:SelectionKey.OP_CONNECT|SelectionKey.OP_READ | SelectionKey.OP_WRITE
public Socket socket()
返回与这个SocketChannel关联的Socket对象。每个SocketChannel对象都与一个Socket对象关联。public boolean isConnected()
判断底层Socket是否已经建立了远程连接。public boolean isConnectionPending()
判断是否正在进行远程连接。如果远程连接操作已经开始,但还没有完成,则返回true,否则返回false。也就是说,无论底层Socket还没有开始连接,或者已经连接成功,该方法都会返回false。public boolean connect(SocketAddress remote)throws IOException
使底层Socket建立远程连接。当SocketChannel处于非阻塞模式时,如果立即连接成功,则该方法返回true,如果不能立即连接成功,则该方法返回false,程序稍后必须通过调用finishConnect()方法来完成连接。当SocketChannel处于阻塞模式时,如果立即连接成功,则该方法返回true,如果不能立即连接成功,则进入阻塞状态,直到连接成功,或者出现I/O异常。public boolean finishConnect()throws IOException
试图完成连接远程服务器的操作。在非阻塞模式下,建立连接从调用SocketChannel的connect()方法开始,到调用finishConnect()方法结束。如果finishConnect()方法顺利完成连接,或者在调用此方法之前连接已经被建立,则finishConnect()方法立即返回true。如果连接操作还没有完成,则立即返回false。如果连接操作中遇到异常而失败,则抛出相应的I/O异常。在阻塞模式下,如果连接操作还没有完成,则会进入阻塞状态,直到连接完成,或者出现I/O异常。public int read(ByteBuffer dst)throws IOException
从Channel中读入若干字节,把它们存放到参数指定的ByteBuffer中。假定执行read()方法前,ByteBuffer的位置为p,剩余容量为r,r等于dst.remaining()方法的返回值。假定read()方法实际上读入了n字节,那么0≤n≤r。当read()方法返回后,参数dst引用的ByteBuffer的位置变为p+n,极限保持不变,如下图所示:
在阻塞模式下,read()方法会争取读到r字节,如果输入流中不足r字节,就进入阻塞状态,直到读入了r字节,或者读到了输入流末尾,或者出现了I/O异常。
在非阻塞模式下,read()方法奉行能读到多少数据就读多少数据的原则。read()方法读取当前通道中的可读数据,有可能不足r字节,或者为0字节,read()方法总是立即返回,而不会等到读取了r字节再返回。
read()方法返回实际上读入的字节数,有可能为0。如果返回“-1”,就表示读到了输入流的末尾。
把参数src指定的ByteBuffer中的字节写到Channel中。假定执行write()方法前,ByteBuffer的位置为p,剩余容量为r,r等于src.remaining()方法的返回值。假定write()方法实际上向通道中写了n字节,那么0≤n≤r。当write()方法返回后,参数src引用的ByteBuffer的位置变为p+n,极限保持不变,如下图所示:
在阻塞模式下,write()方法会争取输出r字节,如果底层网络的输出缓冲区不能容纳r字节,就进入阻塞状态,直到输出了r字节,或者出现了I/O异常。
在非阻塞模式下,write()方法奉行能输出多少数据就输出多少数据的原则,有可能不足r字节,或者为0字节,write()方法总是立即返回,而不会等到输出r字节再返回。
write()方法返回实际上输出的字节数,有可能为0。
2.7、Selector类
只要ServerSocketChannel以及SocketChannel向Selector注册了特定的事件,Selector就会监控这些事件是否发生。SelectableChannel的register()方法负责注册事件,该方法返回一个SelectionKey对象,该对象是用于跟踪这些被注册事件的句柄。一个Selector对象中会包含3种类型的SelectionKey的集合。
2.8、SelectionKey类
ServerSocketChannel或SocketChannel通过register()方法向Selector注册事件时,register()方法会创建一个SelectionKey对象,这个SelectionKey对象用来跟踪注册事件的句柄。在SelectionKey对象的有效期间,Selector会一直监控与SelectionKey对象相关的事件,如果事件发生,就会把SelectionKey对象加入selected-keys集合中。在以下情况下,SelectionKey对象会失效,这意味着Selector再也不会监控与它相关的事件。
(1)程序调用SelectionKey的cancel()方法。
(2)关闭与SelectionKey关联的Channel。
(3)与SelectionKey关联的Selector被关闭。
2.9、 Channels类
Channels类是一个简单的工具类,提供了通道与传统的基于I/O的流、Reader和Writer之间进行转换的静态方法。
- ReadableByteChannel newChannel(InputStream in):输入流转换成读通道。
- WritableByteChannel newChannel(OutputStream out):输出流转换成写通道。
- InputStream newInputStream(AsynchronousByteChannel ch):异步通道转换成输入流。
- InputStream newInputStream(ReadableByteChannel ch):读通道转换成输入流。
- OutputStream newOutputStream(AsynchronousByteChannel ch):异步通道转换成输出流。
- OutputStream newOutputStream(WritableByteChannel ch) :写通道转换成输出流。
- Reader newReader(ReadableByteChannel ch,String csName):读通道转换成Reader。参数csName指定字符编码。
- Reader newReader(ReadableByteChannel ch,Charset charset):读通道转换成Reader。参数charset指定字符编码。
- Reader newReader(ReadableByteChannel ch,CharsetDecoder dec,int minBufferCap):读通道转换成Reader。参数dec指定字符解码器。参数minBufferCap指定内部字节缓冲区的最小容量。
- Writer newWriter(WritableByteChannel ch,String csName):写通道转换成Writer。参数csName指定字符编码。
- Writer newWriter(WritableByteChannel ch,Charset charset):写通道转换成Writer。参数charset指定字符编码。
- Writer newWriter(WritableByteChannel ch,CharsetEncoder enc,int minBufferCap):写通道转换成Writer。参数enc指定字符编码器。参数minBufferCap指定内部字节缓冲区的最小容量。
以上方法的参数包括ReadableByteChannel、WritableByteChannel和AsynchronousByteChannel类型。SocketChannel实现了ReadableByteChannel和WritableByteChannel接口,AsynchronousSocketChannel实现了AsynchronousByteChannel接口。
以下程序代码把SocketChannel转换成输入流,接下来就能按照输入流的方式来读取数据:
2.10、Socket选项
从JDK7开始,SocketChannel、ServerSocketChannel、AsynchronousSocketChannel、AsynchronousServerSocketChannel和DatagramChannel都实现了新的NetworkChannel接口。NetworkChannel接口的主要作用是设置和读取各种Socket选项,例如TCP_NODELAY、SO_LINGER、SO_SNDBUF和SO_RCVBUF等。
3、异步通道和异步运算结果
从JDK7开始,引入了表示异步通道的AsynchronousSocketChannel类和AsynchronousServerSocketChannel类,这两个类的作用与SocketChannel类和ServerSocketChannel相似,区别在于异步通道的一些方法总是采用非阻塞模式,并且它们的非阻塞方法会立即返回一个Future对象,用来存放方法的异步运算结果。
AsynchronousSocketChannel类有以下非阻塞方法:
- Future connect(SocketAddress remote):连接远程主机。
- Future read(ByteBuffer dst):从通道中读入数据,存放到ByteBuffer中。Future对象中包含了实际从通道中读到的字节数。
- Future write(ByteBuffer src):把ByteBuffer中的数据写入通道中。Future对象中包含了实际写入通道的字节数。AsynchronousServerSocketChannel类有以下非阻塞方法。·Futureaccept():接受客户连接请求。Future对象中包含了连接建立成功后创建的AsynchronousSocketChannel对象。