在Java应用程序中,可以使用NIO(New I/O)API来开发高性能网络服务器。当程序执行输入、输出操作后,在这些操作返回之前会一直阻塞该线程,服务器必须为每个客户端都提供一个独立线程进行处理。这说明前面的程序是基于阻塞式API的,当服务器需要同时处理大量客户端请求时,这种做法会降低性能。
在Java应用程序中可以用NIO API使服务器提供一个或有限几个线程来同时处理连接到服务器上的所有客户端。在Java的NIO中,为非阻塞式的Socket通信提供了下面的特殊类。
(1)Selector:是SelectableChannel对象的多路复用器,所有希望采用非阻塞方式进行通信的Channel都应该注册到Selector对象。可通过调用此类的静态open()方法来创建Selector实例,该方法将使用系统默认的Selector来返回新的Selector。Selector可以同时监控多个SelectableChannel的I/O状况,是非阻塞I/O的核心。一个Selector实例有如下3个SelectionKey的集合。
□所有SelectionKey集合:代表了注册在该Selector上的Channel,这个集合可以通过keys()方法返回。
□被选择的SelectionKey集合:代表了所有可通过select()方法监测到、需要进行I/O处理的Channel,这个集合可以通过selectedKeys()方法返回。
□被取消的SelectionKey集合:代表了所有被取消注册关系的Channel,在下一次执行select()方法时,这些Channel对应的SelectionKey会被彻底删除,程序通常无须直接
访问该集合。
除此之外,Selector还提供了如下和select()相关的方法。
□int select():监控所有注册的Channel,当它们中间有需要处理的I/O操作时,该方法返回,并将对应的SelectionKey加入被选择的SelectionKey集合中,该方法返回这些Channel的数量。
□int select(long timeout):可以设置超时的select()操作。
□int selectNow():执行一个立即返回的select()操作,相对于无参数的select()方法而言,该方法不会阻塞线程。
□Selector wakeup():使一个还未返回的select()方法立刻返回。
(2)SelectableChannel:它代表可以支持非阻塞I/O操作的Channel对象,可以将其注册到Selector上,这种注册的关系由SelectionKey实例表示。在Selector对象中,可以使用select()方法设置允许应用程序同时监控多个I/O Channel。Java程序可调用SelectableChannel中的register()方法将其注册到指定Selector上,当该Selector中某些SelectableChannel上有需要处理的I/O操作时,程序可以调用Selector实例的select()方法以获取它们的数量,并通过selectedKeys()方法返回它们对应的SelectKey集合。这个集合的作用巨大,因为通过该集合就可以获取所有需要处理I/O操作的SelectableChannel集。
对象SelectableChannel支持阻塞和非阻塞两种模式,其中所有Channel默认都是阻塞模式,我们必须使用非阻塞式模式才可以利用非阻塞IO操作。
在SelectableChannel中提供了如下两个方法来设置和返回该Channel的模式状态。
□SelectableChannel configureBlocking(boolean block):设置是否采用阻塞模式。
□boolean isBlocking():返回该Channel是否是阻塞模式。
不同的SelectableChannel所支持的操作不一样,例如ServerSocketChannel代表一个ServerSocket,它就只支持OP_ACCEPT操作。在SelectableChannel中提供ValidOps()方法返回一个bit mask,表示这个channel上支持的IO操作来返回它支持的所有操作。
除此之外,SelectableChannel还提供了如下方法获取它的注册状态。(www.xing528.com)
□boolean isRegistered():返回该Channel是否已注册在一个或多个Selector上。
□SelectionKey keyFor(Selector sel):返回该Channel和Selector之间的注册关系,如果不存在注册关系,则返回null。
□SelectionKey:该对象代表SelectableChannel和Selector之间的注册关系。
□ServerSocketChannel:支持非阻塞操作,对应于java.net.ServerSocket类,提供了TCP协议I/O接口,只支持OP_ACCEPT操作。该类也提供了accept()方法,功能相当于ServerSocket提供的accept()方法。
□SocketChannel:支持非阻塞操作,对应于java.net.Socket类,提供了TCP协议I/O接口,支持OP_CONNECT,OP_READ和OP_WRITE操作。这个类还实现了ByteChannel接口、ScatteringByteChannel接口和GatheringByteChannel接口,所以可以直接通过SocketChannel来读写ByteBuffer对象。
服务器上的所有Channel都需要向Selector注册,包括ServerSocketChannel和SocketChannel。该Selector则负责监视这些Socket的I/O状态,当其中任意一个或多个Channel具有可用的I/O操作时,该Selector的select()方法将会返回大于0的整数,该整数值就表示该Selector上有多少个Channel具有可用的I/O操作,并提供了selectedKeys()方法来返回这些Channel对应的SelectionKey集合。正是通过Selector才使得服务端只需要不断地调用Selector实例的select()方法,这样就可以知道当前所有Channel是否有需要处理的I/O操作。当Selector上注册的所有Channel都没有需要处理的I/O操作时,将会阻塞select()方法,此时调用该方法的线程被阻塞。
我们继续以聊天室为例,讲解非阻塞Socket通信在Java应用项目中的实现过程。我们的目标是,在服务端使用循环不断地获取Selector的select()方法返回值,当该返回值大于0时就处理该Selector上被选择SelectionKey所对应的Channel。在具体实现时,服务端使用ServerSocketChannel来监听客户端的连接请求,程序先调用它的socket()方法获得关联ServerSocket对象,再用该ServerSocket对象绑定到指定监听的IP和端口。最后在服务端调用Selector的select()方法来监听所有Channel上的I/O操作。
接下来开始具体编码,其中服务端的主要代码如下。
源码路径:daima\5\tcpudp\src\feizu\feizuServer.java
通过上述代码,可在启动时马上建立一个监听连接请求的ServerSocketChannel,并将该Channel注册到指定的Selector,接着程序直接采用循环方法,不断地监控Selector对象的select()方法的返回值,当该返回值大于0时处理该Selector上所有被选择的SelectionKey。处理指定的SelectionKey之后立即将在Selector中的被选择的SelectionKey集合中删除该SelectionKey。服务端的Selecto仅需要监听连接和读数据这两种操作,在处理连接操作时只需将接受连接后产生的SocketChannel注册到指定Selector对象即可。当处理读数据操作后,系统先从该Socket中读取数据,再将数据写入在Selector上注册的所有Channel中。
接下来开始编写客户端的代码,本应用的客户端程序需要如下两个线程。
□负责读取用户的键盘输入,并将输入的内容写入SocketChannel中。
□不断查询Selector对象的select()方法的返回值。
客户端的主要代码如下。
源码路径:daima\5\tcpudp\src\feizu\feizuClient.java
上述客户端代码只有一条SocketChannel,当此SocketChannel注册到指定的Selector后,程序会启动另一条线程来监测该Selector。
在使用NIO来实现服务器时,甚至无须使用ArrayList来保存服务器中所有的SocketChannel,因为所有的SocketChannel都需要注册到指定的Selector对象。除此之外,当客户端关闭时会导致服务器对应的Channel也抛出异常,而且在使用NIO时只有一条线程,如果该异常得不到处理将会导致整个服务器退出,所以程序捕捉了这种异常,并在处理异常时从Selector删除异常的Channel的注册。
免责声明:以上内容源自网络,版权归原作者所有,如有侵犯您的原创版权请告知,我们将尽快删除相关内容。