非阻隔(Nonblocking)体系结构 在这一部分,我将从理论的角度上来解释非阻隔体系的结构及其工作原理。这部“喜剧(当然,如果你喜欢的话也可以称做戏剧)”的“人物”如下:
●服务器:接收请求的应用
程序。
●客户:一组向服务器端发出请求的应用程序。
●套接字通道:客户端与服务器端之间的通信通道。它能识别服务器端的IP地址和端口号。数据以Buffer中元素的形式通过套接字通道传送。
●选择器:所有非阻隔技术的主要对象。它监视着已注册的套接字通道,并序列化服务器需要应答的请求。
●关键字:选择器用来对对象的请求进行排序。每个关键字代表一个单独的客户端子请求并包含识别客户端和请求类型的信息。
图一:使用非阻隔端口体系的结构图。

图1:非阻隔端口结构
你可能注意到,客户端应用程序同时执行对服务器端的请求,选择器将其集中起来,创建关键字,然后将其发送至服务器端。这看起来像是阻隔(Blocking)体系,因为在一定时间内只处理一个请求,但事实并非如此。实际上,每个关键字不代表从客户端发至服务器端的整个信息流,仅仅只是一部分。我们不要忘了选择器能分割那些被关键字标识的子请求里的数据。因此,如果有更多连续地数据发送至服务器端,那么选择器就会创建更多的根据时间共享策略(Time-sharing policy)来进行处理的关键字。强调一下,在图一中关键字的颜色与客户端的颜色相对应。
服务器端非阻隔(Server Nonblocking) 我以前的部分介绍过的实体都有与其相当的
Java实体。客户端和服务器端是两个Java应用程序。套接字通道是SocketChannel类的实例,这个类允许通过
网络传送数据。它们能被Java程序员看作是一个新的套接字。SocketChannel类被定义在java.nio.channel包中。
选择器是一个Selector类的对象。该类的每个实例均能监视更多的套接字通道,进而建立更多的连接。当一些有意义的事发生在通道上(如客户端试图连接服务器端或进行读/写操作),选择器便会通知应用程序处理请求。选择器会创建一个关键字,这个关键字是SelectionKey类的一个实例。每个关键字都保存着应用程序的标识及请求的类型。其中,请求的类型可以是如下之一:
●尝试连接(客户端)
●尝试连接(服务器端)
●读取操作
●写入操作
一个通用的实现非阻隔服务器的算法如下:
create SocketChannel; create Selector associate the SocketChannel to the Selector for(;;) { waiting events from the Selector; event arrived; create keys; for each key created by Selector { check the type of request; isAcceptable: get the client SocketChannel; associate that SocketChannel to the Selector; record it for read/write operations continue; isReadable: get the client SocketChannel; read from the socket; continue; isWriteable: get the client SocketChannel; write on the socket; continue; } } |
基本上,服务器端的实现是由选择器等待事件和创建关键字的无限循环组成的。根据关键字的类型,及时的执行操作。关键字存在以下4种可能的类型。
Acceptable: 相应的客户端要求连接。
Connectable:服务器端接受连接。
Readable:服务器端可读。
Writeable:服务器端可写。
通常一个表示接受的关键字创建在服务器端。事实上,这种关键字仅仅通知一下服务器端客户端请求连接。在这种环境下,正如你通过算法得到的结论一样,服务器端个性化套接字通道和连接这个通道到选择器以便进行读/写操作。从这一刻起,当接受客户端读或写操作时,选择器将为客户端创建Readable或Writeable关键字。从而,服务器端将截取这些关键字并执行正确的动作。
现在,你可以用下面这个推荐算法和Java语言写服务器端了。用这种方法能成功的创建套接字通道,选择器,和套接字-选择器注册(socket-selector registration)。
// Create the server socket channel ServerSocketChannel server = ServerSocketChannel.open(); // nonblocking I/O server.configureBlocking(false); // host-port 8000 server.socket().bind(new java.net.InetSocketAddress(host,8000)); System.out.println("Server attivo porta 8000"); // Create the selector Selector selector = Selector.open(); // Recording server to selector (type OP_ACCEPT) server.register(selector,SelectionKey.OP_ACCEPT); |
这个静态(static)open类方法创建了一个SocketChannel类的实例。configureBlocking(false)调用设置通道为非阻隔。通过bind方法建立到服务器端的连接。字符串“host”代表服务器的IP地址,8000是通信套接字。你可以调用你可以调用Selector类的静态(static)open方法创建选择器。最后,register方法用来连接选择器和套接字通道。
第二个参数代表注册的类型。在这个例子中,我们使用OP_ACCEPT,意思是选择器仅能报告客户端试图尝试连接服务器端。其他可能的选项是:OP_CONNECT,在客户端下使用;OP_READ; 和OP_WRITE
用Java语言描述的无限for循环的代码如下:
// Infinite server loop for(;;) { // Waiting for events selector.select(); // Get keys Set keys = selector.selectedKeys(); Iterator i = keys.iterator(); // For each keys... while(i.hasNext()) { SelectionKey key = (SelectionKey) i.next(); // Remove the current key i.remove(); // if isAccetable = true // then a client required a connection if (key.isAcceptable()) { // get client socket channel SocketChannel client = server.accept(); // Non Blocking I/O client.configureBlocking(false); // recording to the selector (reading) client.register(selector, SelectionKey.OP_READ); continue; } // if isReadable = true // then the server is ready to read if (key.isReadable()) { SocketChannel client = (SocketChannel) key.channel(); // Read byte coming from the client int BUFFER_SIZE = 32; ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE); try { client.read(buffer); } catch (Exception e) { // client is no longer active e.printStackTrace(); continue; } // Show bytes on the console buffer.flip(); Charset charset=Charset.forName("ISO-8859-1"); CharsetDecoder decoder = charset.newDecoder(); CharBuffer charBuffer = decoder.decode(buffer); System.out.print(charBuffer.toString()); continue; } } } |
循环的第一行是调用select方法,它会阻塞进程执行并等待选择器上记录的事件。在这段代码中,套接字通道由服务器变量指代。实际上,服务器端不是一个SocketChannel对象,但是一个ServerSocketChannel对象。它象SocketChannel一样是SelectableChannel类的一般化,通常用于服务器端的应用程序。
选择器等待的事件是客户端尝试连接。当这样的操作出现时,服务器端的应用程序便获得一个由选择器创建的关键字和检查每个关键字的类型。你也许注意到,当一个关键字被处理时,它不得不调用remove方法从这组关键字中被移出。如果这个关键字的类型是可接受的(isAcceptable()=true),那么服务器端便通过调用accept方法来查找客户端套接字通道,设置它为非阻隔,并将OP_READ选项把它登记进选择器中。我们也可以使用OP_WRITE 或者是OP_READ|OP_WRITE选项,但为了简单,我实现的服务器端仅仅能从通道中读取,不能进入写入操作。
客户端套接字通道现在已经登记入选择器并可进行读取操作。从而,当客户端在套接字通道上写数据时,选择器将通知服务器端应用程序这里有一些数据读。随着可读关键字的创建,从而isReadable()=true。在这个例子中,应用程序从套接字通道上读取数据使用的是32个字节的ByteBuffer,字节译码使用的是ISO-8859-1编码规则,同时读取的数据也会显示在服务器端的控制台上。