Merlin 給 Java 平臺帶來了非阻塞 I/O

引用:http://www-128.ibm.com/developerworks/cn/java/j-javaio/

Java 技術平臺早就應該提供非阻塞 I/O 機制了。幸運的是,Merlin(JDK 1.4)有一根幾乎在各個場合都適用的魔杖,而解除阻塞了的 I/O 的阻塞狀態正是這位魔術師的專長。軟件工程師 Aruna Kalagnanam 和 Balu G 介紹了 Merlin 的新 I/O 包 ― java.nio(NIO)― 的這種非阻塞功能,並且用一個套接字編程示例向您展示 NIO 能做些什麼。請單擊本文頂部或底部的 討論,在 討論論壇與作者及其他讀者分享您關於本文的心得。

服務器在合理的時間之內處理大量客戶機請求的能力取決於服務器使用 I/O 流的效率。同時爲成百上千個客戶機提供服務的服務器必須能夠併發地使用 I/O 服務。Java 平臺直到 JDK 1.4(也就是 Merlin)才支持非阻塞 I/O 調用。用 Java 語言寫的服務器,由於其線程與客戶機之比幾乎是一比一,因而易於受到大量線程開銷的影響,其結果是既導致了性能問題又缺乏可伸縮性。

爲了解決這個問題,Java 平臺的最新發行版引入了一組新的類。Merlin 的 java.nio 包充滿了解決線程開銷問題的技巧,包中最重要的是新的 SelectableChannel 類和 Selector 類。 通道(channel)是客戶機和服務器之間的一種通信方式。 選擇器(selector)與 Windows 消息循環類似,它從不同客戶機捕獲各種事件並將它們分派到相應的事件處理程序。在本文,我們將向您展示這兩個類如何協同工作,從而爲 Java 平臺創建非阻塞 I/O 機制。

Merlin 之前的 I/O 編程
我們將從考察基礎的、Merlin 之前的服務器-套接字(server-socket)程序開始。在 ServerSocket 類的生存期中,其重要功能如下:

  • 接受傳入連接
  • 從客戶機讀取請求
  • 爲請求提供服務

我們來考察一下以上每一個步驟,我們用代碼片段來說明。 首先,我們創建一個新的 ServerSocket


ServerSocket s = new ServerSocket();

接着,我們要接受傳入調用。這裏,調用 accept() 應該可以完成任務,但其中有個小陷阱您得當心:


Socket conn = s.accept( );

accept() 的調用將一直阻塞,直到服務器套接字接受了一個請求連接的客戶機請求。一旦建立了連接,服務器就使用 LineNumberReader 讀取客戶機請求。因爲 LineNumberReader 要到緩衝區滿時才成批地讀取數據,所以這個調用在讀時阻塞。 下面的片段顯示了工作中的 LineNumberReader (阻塞等等)。


InputStream in = conn.getInputStream();
InputStreamReader rdr = new InputStreamReader(in);
LineNumberReader lnr = new LineNumberReader(rdr);
Request req = new Request();
while (!req.isComplete() )
{
   String s = lnr.readLine();
   req.addLine(s);
}

InputStream.read() 是另一種讀取數據的方式。不幸的是, read 方法也要一直阻塞到數據可用爲止, write 方法也一樣,。

圖 1 描繪了服務器的典型工作過程。黑體線表示處於阻塞的操作。

圖 1. 典型的工作中的服務器
阻塞的 I/O 圖

在 JDK 1.4 之前,自由地使用線程是處理阻塞問題最典型的辦法。但這個解決辦法會產生它自己的問題 ― 即線程開銷,線程開銷同時影響性能和可伸縮性。不過,隨着 Merlin 和 java.nio 包的到來,一切都變了。

在下面的幾個部分中,我們將考察 java.nio 的基本思想,然後把我們所學到的一些知識應用於修改前面描述的服務器-套接字示例。

反應器模式(Reactor pattern)
NIO 設計背後的基石是反應器設計模式。 分佈式系統中的服務器應用程序必須處理多個向它們發送服務請求的客戶機。然而,在調用特定的服務之前,服務器應用程序必須將每個傳入請求多路分用並分派到各自相應的服務提供者。反應器模式正好適用於這一功能。它允許事件驅動應用程序將服務請求多路分用並進行分派,然後,這些服務請求被併發地從一個或多個客戶機傳送到應用程序。

反應器模式的核心功能

  • 將事件多路分用
  • 將事件分派到各自相應的事件處理程序

反應器模式與觀察者模式(Observer pattern)在這個方面極爲相似:當一個主體發生改變時,所有依屬體都得到通知。不過,觀察者模式與單個事件源關聯,而反應器模式則與多個事件源關聯。

請參閱 參考資料瞭解關於反應器模式的更多信息。

通道和選擇器
NIO 的非阻塞 I/O 機制是圍繞 選擇器通道構建的。 Channel 類表示服務器和客戶機之間的一種通信機制。與反應器模式一致, Selector 類是 Channel 的多路複用器。 Selector 類將傳入客戶機請求多路分用並將它們分派到各自的請求處理程序。

我們將仔細考察 Channel 類和 Selector 類的各個功能,以及這兩個類如何協同工作,創建非阻塞 I/O 實現。

通道做什麼
通道表示連到一個實體(例如:硬件設備、文件、網絡套接字或者能執行一個或多個不同 I/O 操作(例如:讀或寫)的程序組件)的開放連接。可以異步地關閉和中斷 NIO 通道。所以,如果一個線程在某條通道的 I/O 操作上阻塞時,那麼另一個線程可以將這條通道關閉。類似地,如果一個線程在某條通道的 I/O 操作上阻塞時,那麼另一個線程可以中斷這個阻塞線程。

圖 2. java.nio.channels 的類層次結構
java.nio 包的類層次結構

如圖 2 所示,在 java.nio.channels 包中有不少通道接口。我們主要關心 java.nio.channels.SocketChannel 接口和 java.nio.channels.ServerSocketChannel 接口。 這兩個接口可用來分別代替 java.net.Socketjava.net.ServerSocket 。儘管我們當然將把注意力放在以非阻塞方式使用通道上,但通道可以以阻塞方式或非阻塞方式使用。

創建一條非阻塞通道
爲了實現基礎的非阻塞套接字讀和寫操作,我們要處理兩個新類。它們是來自 java.net 包的 InetSocketAddress 類,它指定連接到哪裏,以及來自 java.nio.channels 包的 SocketChannel 類,它執行實際的讀和寫操作。

這部分中的代碼片段顯示了一種經過修改的、非阻塞的辦法來創建基礎的服務器-套接字程序。請注意這些代碼樣本與第一個示例中所用的代碼之間的變化,從添加兩個新類開始:


String host = ......;
   InetSocketAddress socketAddress = new InetSocketAddress(host, 80);
	
SocketChannel channel = SocketChannel.open();
   channel.connect(socketAddress);

緩衝區的角色
Buffer 是包含特定基本數據類型數據的抽象類。從本質上說,它是一個包裝器,它將帶有 getter/setter 方法的固定大小的數組包裝起來,這些 getter/setter 方法使得緩衝區的內容可以被訪問。 Buffer 類有許多子類,如下:

  • ByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer
ByteBuffer 是唯一支持對其它類型進行讀寫的類,因爲其它類都是特定於類型的。一旦連接上,就可以使用 ByteBuffer 對象從通道讀數據或將數據寫到通道。請參閱 參考資料瞭解關於 ByteBuffer 的更多信息。

爲了使通道成爲非阻塞的,我們在通道上調用 configureBlockingMethod(false) ,如下所示:


channel.configureBlockingMethod(false);

在阻塞模式中,線程將在讀或寫時阻塞,一直到讀或寫操作徹底完成。如果在讀的時候,數據尚未完全到達套接字,則線程將在讀操作上阻塞,一直到數據可用。

在非阻塞模式中,線程將讀取已經可用的數據(不論多少),然後返回執行其它任務。如果將真(true)傳遞給 configureBlockingMethod() ,則通道的行爲將與在 Socket 上進行阻塞讀或寫時的行爲完全相同。唯一的主要差別,如上所述,是這些阻塞讀和寫可以被其它線程中斷。

單靠 Channel 創建非阻塞 I/O 實現是不夠的。要實現非阻塞 I/O, Channel 類必須與 Selector 類配合進行工作。

選擇器做什麼
在反應器模式情形中, Selector 類充當 Reactor 角色。 Selector 對多個 SelectableChannels 的事件進行多路複用。每個 ChannelSelector 註冊事件。當事件從客戶機處到來時, Selector 將它們多路分用並將這些事件分派到相應的 Channel

創建 Selector 最簡單的辦法是使用 open() 方法,如下所示:


Selector selector = Selector.open();

通道遇上選擇器
每個要爲客戶機請求提供服務的 Channel 都必須首先創建一個連接。下面的代碼創建稱爲 ServerServerSocketChannel 並將它綁定到本地端口:


ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
InetAddress ia = InetAddress.getLocalHost();
InetSocketAddress isa = new InetSocketAddress(ia, port );
serverChannel.socket().bind(isa);

每個要爲客戶機請求提供服務的 Channel 都必須接着將自己向 Selector 註冊。 Channel 應根據它將處理的事件進行註冊。例如,接受傳入連接的 Channel 應這樣註冊,如下:


SelectionKey acceptKey = 
    channel.register( selector,SelectionKey.OP_ACCEPT);

ChannelSelector 的註冊用 SelectionKey 對象表示。滿足以下三個條件之一, Key 就失效:

  • Channel 被關閉。
  • Selector 被關閉。
  • 通過調用 Keycancel() 方法將 Key 本身取消。

Selectorselect() 調用時阻塞。接着,它開始等待,直到建立了一個新的連接,或者另一個線程將它喚醒,或者另一個線程將原來的阻塞線程中斷。

註冊服務器
Server 是那個將自己向 Selector 註冊以接受所有傳入連接的 ServerSocketChannel ,如下所示:


SelectionKey acceptKey = serverChannel.register(sel, SelectionKey.OP_ACCEPT);

   while (acceptKey.selector().select() > 0 ){

     ......

Server 被註冊後,我們根據每個關鍵字(key)的類型以迭代方式對一組關鍵字進行處理。一個關鍵字被處理完成後,就都被從就緒關鍵字(ready keys)列表中除去,如下所示:


Set readyKeys = sel.selectedKeys();
    Iterator it = readyKeys.iterator();
while (it.hasNext()) 
{

SelectionKey key = (SelectionKey)it.next();
  it.remove();
  ....
  ....
  ....
 }

如果關鍵字是可接受(acceptable)的,則接受連接,註冊通道,以接受更多的事件(例如:讀或寫操作)。 如果關鍵字是可讀的(readable)或可寫的(writable),則服務器會指示它已經就緒於讀寫本端數據:


SocketChannel socket;
if (key.isAcceptable()) {
    System.out.println("Acceptable Key");
    ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
    socket = (SocketChannel) ssc.accept();
    socket.configureBlocking(false);
    SelectionKey another = 
      socket.register(sel,SelectionKey.OP_READ|SelectionKey.OP_WRITE);
}
if (key.isReadable()) {
    System.out.println("Readable Key");
    String ret = readMessage(key);
    if (ret.length() > 0) {
      writeMessage(socket,ret);
    }
		    
}
if (key.isWritable()) {
    System.out.println("Writable Key");
    String ret = readMessage(key);
    socket = (SocketChannel)key.channel();   
    if (result.length() > 0 ) {
      writeMessage(socket,ret);
    }
    }

唵嘛呢叭咪吽 — 非阻塞服務器套接字快顯靈!
對 JDK 1.4 中的非阻塞 I/O 的介紹的最後一部分留給您:運行這個示例。

在這個簡單的非阻塞服務器-套接字示例中,服務器讀取發送自客戶機的文件名,顯示該文件的內容,然後將內容寫回到客戶機。

這裏是您運行這個示例需要做的事情:

  1. 安裝 JDK 1.4(請參閱 參考資料)。
  2. 將兩個 源代碼文件複製到您的目錄。
  3. 編譯和運行服務器, java NonBlockingServer
  4. 編譯和運行客戶機, java Client
  5. 輸入類文件所在目錄的一個文本文件或 java 文件的名稱。
  6. 服務器將讀取該文件並將其內容發送到客戶機。
  7. 客戶機將把從服務器接收到的數據打印出來。(由於所用的 ByteBuffer 的限制,所以將只讀取 1024 字節。)
  8. 輸入 quit 或 shutdown 命令關閉客戶機。

結束語
Merlin 的新 I/O 包覆蓋範圍很廣。Merlin 的新的非阻塞 I/O 實現的主要優點有兩方面:線程不再在讀或寫時阻塞,以及 Selector 能夠處理多個連接,從而大幅降低了服務器應用程序開銷。

我們已經着重論述了新的 java.nio 包的這兩大優點。我們希望,您將把在這裏所學到的知識應用到自己的實際應用程序開發工作中。

=======================================================================================

package nonblock;

/**
 * <li>Title:</li>
 * <li>Description:</li>
 * <li>Company: GuanDa Technology</li>
 * <li>Copyright: 2005-9-1</li>
 * @author: ChenLiang
 * @version 1.0
 */

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;


public class Client
{
    public SocketChannel client = null;
    public InetSocketAddress isa = null;
    public RecvThread rt = null;

    public Client()
    {
    }
   
 public void makeConnection()
    {
  int result = 0;
  try
  {
   
   client = SocketChannel.open();
    isa = new InetSocketAddress(InetAddress.getLocalHost(),4900);
   client.connect(isa);
   client.configureBlocking(false);
   receiveMessage();   
  }
  catch(UnknownHostException e)
  {
   e.printStackTrace();
  }
  catch(IOException e)
  {
   e.printStackTrace();
  }
  while ((result = sendMessage()) != -1)
  {
  }

  try
  {
   client.close();
   System.exit(0);
  }
  catch(IOException e)
  {
   e.printStackTrace();
  }
    }
   
 public int sendMessage()
    {
  System.out.println("Inside SendMessage");
        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
  String msg = null;
  ByteBuffer bytebuf = ByteBuffer.allocate(1024);
  int nBytes = 0;
  try
  {
   msg = in.readLine();
   System.out.println("msg is "+msg);
   bytebuf = ByteBuffer.wrap(msg.getBytes());
   nBytes = client.write(bytebuf);
   System.out.println("nBytes is "+nBytes);
   if (msg.equals("quit") || msg.equals("shutdown")) {
    System.out.println("time to stop the client");
    interruptThread();
    try
    {
     Thread.sleep(5000);
    }
    catch(Exception e)
    {
     e.printStackTrace();
    }
    client.close();
    return -1;
   }
    
  }
        catch(IOException e)
  {
   e.printStackTrace();
  }
  System.out.println("Wrote "+nBytes +" bytes to the server");
  return nBytes;
    }

    public void receiveMessage()
    {
  rt = new RecvThread("Receive THread",client);
  rt.start();

    }

    public void interruptThread()
    {
  rt.val = false;
    }

    public static void main(String args[])
    {
  Client cl = new Client();
  cl.makeConnection();
    }

    public class RecvThread extends Thread
    {
  public SocketChannel sc = null;
  public boolean val = true;
 
  public RecvThread(String str,SocketChannel client)
  {
   super(str);
   sc = client;
  }
 
  public void run() {

   System.out.println("Inside receivemsg");
   int nBytes = 0;
   ByteBuffer buf = ByteBuffer.allocate(2048);
   try
   {
    while (val)
    {
     while ( (nBytes = nBytes = client.read(buf)) > 0){
      buf.flip();
      Charset charset = Charset.forName("us-ascii");
      CharsetDecoder decoder = charset.newDecoder();
      CharBuffer charBuffer = decoder.decode(buf);
      String result = charBuffer.toString();
         System.out.println(result);
      buf.flip();
      
     }
    }
   
   }
   catch(IOException e)
   {
    e.printStackTrace();
   
   }
           

  }
    }
}

=================================================================================

NonBlockingServer.java

package nonblock;

/**
 * <li>Title:</li>
 * <li>Description:</li>
 * <li>Company: GuanDa Technology</li>
 * <li>Copyright: 2005-9-1</li>
 * @author: ChenLiang
 * @version 1.0
 */

import java.io.*;
import java.nio.*;
import java.nio.channels.*;
import java.net.*;
import java.util.*;
import java.nio.charset.*;


public class NonBlockingServer
{
    public Selector sel = null;
    public ServerSocketChannel server = null;
    public SocketChannel socket = null;
    public int port = 4900;
    String result = null;


    public NonBlockingServer()
    {
  System.out.println("Inside default ctor");
    }
   
 public NonBlockingServer(int port)
    {
  System.out.println("Inside the other ctor");
  this.port = port;
    }

    public void initializeOperations() throws IOException,UnknownHostException
    {
  System.out.println("Inside initialization");
  sel = Selector.open();
  server = ServerSocketChannel.open();
  server.configureBlocking(false);
  InetAddress ia = InetAddress.getLocalHost();
  InetSocketAddress isa = new InetSocketAddress(ia,port);
  server.socket().bind(isa);
    }
   
 public void startServer() throws IOException
    {
  System.out.println("Inside startserver");
        initializeOperations();
  System.out.println("Abt to block on select()");
  SelectionKey acceptKey = server.register(sel, SelectionKey.OP_ACCEPT ); 
 
  while (acceptKey.selector().select() > 0 ){
//  while(true)
//  {
//      int num = sel.select();
//      if(num == 0)
//          continue;
    
   Set readyKeys = sel.selectedKeys();
   Iterator it = readyKeys.iterator();

   while (it.hasNext()) {
    SelectionKey key = (SelectionKey)it.next();
    it.remove();
               
    if (key.isAcceptable()) {
     System.out.println("Key is Acceptable");
     ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
     socket = (SocketChannel) ssc.accept();
     socket.configureBlocking(false);
     SelectionKey another = socket.register(sel,SelectionKey.OP_READ|SelectionKey.OP_WRITE);
//     SelectionKey another = socket.register(sel,SelectionKey.OP_READ);
    }
    if (key.isReadable()) {
     System.out.println("Key is readable");
     String ret = readMessage(key);
     if (ret.length() > 0) {
      writeMessage(socket,ret);
     }
    }
    if (key.isWritable()) {
     System.out.println("THe key is writable");
     String ret = readMessage(key);
     socket = (SocketChannel)key.channel();
     if (result.length() > 0 ) {
      writeMessage(socket,ret);
     }
    }
   }
  }
    }

    public void writeMessage(SocketChannel socket,String ret)
    {
  System.out.println("Inside the loop");

  if (ret.equals("quit") || ret.equals("shutdown")) {
   return;
  }
  File file = new File("d:/nonblock.txt");
  try
  {
  
   RandomAccessFile rdm = new RandomAccessFile(file,"r");
   FileChannel fc = rdm.getChannel();
   ByteBuffer buffer = ByteBuffer.allocate(1024);
   fc.read(buffer);
   buffer.flip();
   
   Charset set = Charset.forName("us-ascii");
   CharsetDecoder dec = set.newDecoder();
   CharBuffer charBuf = dec.decode(buffer);
   System.out.println(charBuf.toString());
   buffer = ByteBuffer.wrap((charBuf.toString()).getBytes());
   int nBytes = socket.write(buffer);
   System.out.println("nBytes = "+nBytes);
    result = null;
  }
  catch(Exception e)
  {
   e.printStackTrace();
  }

    }
 
    public String readMessage(SelectionKey key)
    {
  int nBytes = 0;
  socket = (SocketChannel)key.channel();
        ByteBuffer buf = ByteBuffer.allocate(1024);
  try
  {
            nBytes = socket.read(buf);
   buf.flip();
   Charset charset = Charset.forName("us-ascii");
   CharsetDecoder decoder = charset.newDecoder();
   CharBuffer charBuffer = decoder.decode(buf);
   result = charBuffer.toString();
    
        }
  catch(IOException e)
  {
   e.printStackTrace();
  }
  return result;
    }

    public static void main(String args[])
    {
  NonBlockingServer nb = new NonBlockingServer();
  try
  {
   nb.startServer();
  }
  catch (IOException e)
  {
   e.printStackTrace();
   System.exit(-1);
  }
  
 }
}

發佈了1 篇原創文章 · 獲贊 0 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章