【轉載】Java非同步通訊收藏

kekele647發表於2009-11-15

程式碼號為”Merlin”J2SE1.4帶來了一些激動人心的新特性,諸如對正規表示式的支援,非同步輸入輸出流,通道(Channel),字符集等.雖然該版本還處在測試階段,但這些新特性早已讓開發人員們躍躍欲試.Merlin釋出之前,非同步輸入輸出流的應用還只是C,C++程式設計師的特殊武器; Merlin中引入非同步輸入輸出機制之後,Java程式設計師也可以利用它完成很多簡潔卻是高質量的程式碼了.本文將介紹怎樣使用非同步輸入輸出流來編寫 Socket程式通訊程式.

同步?非同步輸入輸出機制的引入

Merlin之前,編寫Socket程式是比較繁瑣的工作.因為輸入輸出都必須同步.這樣,對於多客戶端客戶/伺服器模式,不得不使用多執行緒.即為每個連線的客戶都分配一個執行緒來處理輸入輸出.由此而帶來的問題是可想而知的.程式設計師不得不為了避免死鎖,執行緒安全等問題,進行大量的編碼和測試.很多人都在抱怨為什麼不在Java中引入非同步輸入輸出機制.比較官方的解釋是,任何一種應用程式介面的引入,都必須相容任何操作平臺.因為Java是跨平臺的.而當時支援非同步輸入輸出機制的操作平臺顯然不可能是全部.Java 2 Platform以後,分離出J2SE,J2ME,J2EE三種不同型別的應用程式介面,以適應不同的應用開發.Java標準的制訂者們意識到了這個問題,並且支援非同步輸入輸出機制的操作平臺在當今操作平臺中處於主流地位.於是,Jdk(J2SE) 的第五次釋出中引入了非同步輸入輸出機制.

以前的Socket程式通訊程式設計中,一般客戶端和伺服器端程式設計如下:

  1. 伺服器端:

   //伺服器端監聽執行緒
    while (true) {
                .............
                Socket clientSocket;
                clientSocket = socket.accept(); //
取得客戶請求Socket,如果沒有//客戶請求連線,執行緒在此處阻塞
                //
用取得的Socket構造輸入輸出流
                PrintStream s = new PrintStream(new
                BufferedOutputStream(clientSocket.getOutputStream(),
                1024), false);
                BufferedReader is = new BufferedReader(new
                InputStreamReader(clientSocket.getInputStream()));
                //
建立客戶會話執行緒,進行輸入輸出控制,為同步機制
                new ClientSession();
                .......
                 }

  1. 客戶端:

  ............
   clientSocket = new Socket(HOSTNAME, LISTENPORT);//
連線伺服器套接字

   //
用取得的Socket構造輸入輸出流
   PrintStream s = new PrintStream(new
                BufferedOutputStream(clientSocket.getOutputStream(),
                1024), false);
                BufferedReader is = new BufferedReader(new
                InputStreamReader(clientSocket.getInputStream()));
   //
進行輸入輸出控制
    .......

  1. 以上程式碼段只是用同步機制編寫Socket程式通訊的一個框架,實際上要考慮的問題要複雜的多(有興趣的讀者可以參考我的一篇文章《Internet 實時通訊系統設計與實現》)。將這樣一個框架列出來,只是為了與用非同步機制實現的Socket程式通訊進行比較。下面將介紹使用非同步機制的程式設計。

 


 

用非同步輸入輸出流編寫Socket程式通訊程式

Merlin中加入了用於實現非同步輸入輸出機制的應用程式介面包:java.nio(新的輸入輸出包,定義了很多基本型別緩衝(Buffer)), java.nio.channels(通道及選擇器等,用於非同步輸入輸出)java.nio.charset(字元的編碼解碼)。通道 (Channel)首先在選擇器(Selector)中註冊自己感興趣的事件,當相應的事件發生時,選擇器便通過選擇鍵(SelectionKey)通知已註冊的通道。然後通道將需要處理的資訊,通過緩衝(Buffer)打包,編碼/解碼,完成輸入輸出控制。

通道介紹:

這裡主要介紹ServerSocketChannel SocketChannel.它們都是可選擇的(selectable)通道,分別可以工作在同步和非同步兩種方式下(注意,這裡的可選擇不是指可以選擇兩種工作方式,而是指可以有選擇的註冊自己感興趣的事件)。可以用channel.configureBlocking(Boolean )來設定其工作方式。與以前版本的API相比較,ServerSocketChannel就相當於ServerSocket (ServerSocketChannel封裝了ServerSocket),SocketChannel就相當於Socket SocketChannel封裝了Socket)。當通道工作在同步方式時,程式設計方法與以前的基本相似,這裡主要介紹非同步工作方式。

所謂非同步輸入輸出機制,是指在進行輸入輸出處理時,不必等到輸入輸出處理完畢才返回。所以非同步的同義語是非阻塞(None Blocking)。在伺服器端,ServerSocketChannel通過靜態函式open()返回一個例項serverChl。然後該通道呼叫 serverChl.socket().bind()繫結到伺服器某埠,並呼叫registerSelector sel, SelectionKey.OP_ACCEPT)註冊OP_ACCEPT事件到一個選擇器中(ServerSocketChannel只可以註冊 OP_ACCEPT事件)。當有客戶請求連線時,選擇器就會通知該通道有客戶連線請求,就可以進行相應的輸入輸出控制了;在客戶端,clientChl例項註冊自己感興趣的事件後(可以是OP_CONNECT,OP_READ,OP_WRITE的組合),呼叫clientChl.connect (InetSocketAddress )連線伺服器然後進行相應處理。注意,這裡的連線是非同步的,即會立即返回而繼續執行後面的程式碼。

選擇器和選擇鍵介紹:

選擇器(Selector)的作用是:將通道感興趣的事件放入佇列中,而不是馬上提交給應用程式,等已註冊的通道自己來請求處理這些事件。換句話說,就是選擇器將會隨時報告已經準備好了的通道,而且是按照先進先出的順序。那麼,選擇器是通過什麼來報告的呢?選擇鍵(SelectionKey)。選擇鍵的作用就是表明哪個通道已經做好了準備,準備幹什麼。你也許馬上會想到,那一定是已註冊的通道感興趣的事件。不錯,例如對於伺服器端serverChl來說,可以呼叫key.isAcceptable()來通知serverChl有客戶端連線請求。相應的函式還有: SelectionKey.isReadable(),SelectionKey.isWritable()。一般的,在一個迴圈中輪詢感興趣的事件(具體可參照下面的程式碼)。如果選擇器中尚無通道已註冊事件發生,呼叫Selector.select()將阻塞,直到有事件發生為止。另外,可以呼叫 selectNow()或者select(long timeout)。前者立即返回,沒有事件時返回0值;後者等待timeout時間後返回。一個選擇器最多可以同時被63個通道一起註冊使用。

應用例項:

下面是用非同步輸入輸出機制實現的客戶/伺服器例項程式�D�D程式清單1(限於篇幅,只給出了伺服器端實現,讀者可以參照著實現客戶端程式碼):



程式類圖



程式清單1

public class NBlockingServer {
    int port = 8000;
    int BUFFERSIZE = 1024;
    Selector selector = null;
    ServerSocketChannel serverChannel = null;
    HashMap clientChannelMap = null;//
用來存放每一個客戶連線對應的套接字和通道


    public NBlockingServer( int port ) {
        this.clientChannelMap = new HashMap();
        this.port = port;
    }

    public void initialize() throws IOException {
      //
初始化,分別例項化一個選擇器,一個伺服器端可選擇通道
      this.selector = Selector.open();
      this.serverChannel = ServerSocketChannel.open();
      this.serverChannel.configureBlocking(false);
      InetAddress localhost = InetAddress.getLocalHost();
      InetSocketAddress isa = new InetSocketAddress(localhost, this.port );
      this.serverChannel.socket().bind(isa);//
將該套接字繫結到伺服器某一可用埠
    }
    //
結束時釋放資源
    public void finalize() throws IOException {
        this.serverChannel.close();
        this.selector.close();
    }
    //
將讀入位元組緩衝的資訊解碼
    public String decode( ByteBuffer byteBuffer ) throws
CharacterCodingException {
        Charset charset = Charset.forName( "ISO-8859-1" );
        CharsetDecoder decoder = charset.newDecoder();
        CharBuffer charBuffer = decoder.decode( byteBuffer );
        String result = charBuffer.toString();
        return result;
    }
    //
監聽埠,當通道準備好時進行相應操作
    public void portListening() throws IOException, InterruptedException {
      //
伺服器端通道註冊OP_ACCEPT事件
      SelectionKey acceptKey =this.serverChannel.register( this.selector,
                                           SelectionKey.OP_ACCEPT );
        //
當有已註冊的事件發生時,select()返回值將大於0
        while (acceptKey.selector().select() > 0 ) {
            System.out.println("event happened");
            //
取得所有已經準備好的所有選擇鍵

            Set readyKeys = this.selector.selectedKeys();
            //
使用迭代器對選擇鍵進行輪詢
            Iterator i = readyKeys.iterator();
            while (i.hasNext()) {
                SelectionKey key = (SelectionKey)i.next();
                i.remove();//
刪除當前將要處理的選擇鍵
                if ( key.isAcceptable() ) {//
如果是有客戶端連線請求
                    System.out.println("more client connect in!");
                    ServerSocketChannel nextReady =
                        (ServerSocketChannel)key.channel();
                    //
獲取客戶端套接字
                    Socket s = nextReady.accept();
                    //
設定對應的通道為非同步方式並註冊感興趣事件
                    s.getChannel().configureBlocking( false );
                    SelectionKey readWriteKey =
                        s.getChannel().register( this.selector,
                            SelectionKey.OP_READ|SelectionKey.OP_WRITE  );
                    //
將註冊的事件與該套接字聯絡起來
readWriteKey.attach( s );
//
將當前建立連線的客戶端套接字及對應的通道存放在雜湊表//clientChannelMap
                    this.clientChannelMap.put( s, new
ClientChInstance( s.getChannel() ) );
                    }
                else if ( key.isReadable() ) {//
如果是通道讀準備好事件
                    System.out.println("Readable");
                    //
取得選擇鍵對應的通道和套接字
                    SelectableChannel nextReady =
                        (SelectableChannel) key.channel();
                    Socket socket = (Socket) key.attachment();
                    //
處理該事件,處理方法已封裝在類ClientChInstance
                    this.readFromChannel( socket.getChannel(),
                    (ClientChInstance)
this.clientChannelMap.get( socket ) );
                }
                else if ( key.isWritable() ) {//
如果是通道寫準備好事件
                    System.out.println("writeable");
                    //
取得套接字後處理,方法同上
                    Socket socket = (Socket) key.attachment();
                    SocketChannel channel = (SocketChannel)
socket.getChannel();
                    this.writeToChannel( channel,"This is from server!");
                }
            }
        }
    }
    //
對通道的寫操作
    public void writeToChannel( SocketChannel channel, String message )
throws IOException {
        ByteBuffer buf = ByteBuffer.wrap( message.getBytes()  );
        int nbytes = channel.write( buf );
    }
     //
對通道的讀操作
    public void readFromChannel( SocketChannel channel, ClientChInstance clientInstance )
    throws IOException, InterruptedException {
        ByteBuffer byteBuffer = ByteBuffer.allocate( BUFFERSIZE );
        int nbytes = channel.read( byteBuffer );

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/7389553/viewspace-619657/,如需轉載,請註明出處,否則將追究法律責任。

相關文章