2011-06-30 75 views
5

我有一個使用Java NIO使用TCP套接字連接到C++服務器的Java客戶機。這適用於Linux,AIX和HP/UX,但在Solaris下,OP_CONNECT事件不會觸發。Java Solaris NIO OP_CONNECT問題

進一步的細節:

  • Selector.select()將返回0,而 '選擇的密鑰集' 是空的。
  • 只有在連接到本地計算機(通過回送或以太網接口)時纔會出現此問題,但連接到遠程計算機時會發生此問題。
  • 我已經在兩個不同的Solaris 10計算機下確認了該問題;物理SPARC和使用JDK 1.6.0_21和_26版本的虛擬x64(VMWare)。

下面是一些測試代碼,演示了這個問題:

import java.io.IOException; 
import java.net.InetSocketAddress; 
import java.nio.ByteBuffer; 
import java.nio.channels.SelectionKey; 
import java.nio.channels.Selector; 
import java.nio.channels.SocketChannel; 
import java.util.HashSet; 
import java.util.Iterator; 
import java.util.Set; 

public class NioTest3 
{ 
    public static void main(String[] args) 
    { 
     int i, tcount = 1, open = 0; 
     String[] addr = args[0].split(":"); 
     int port = Integer.parseInt(addr[1]); 
     if (args.length == 2) 
      tcount = Integer.parseInt(args[1]); 
     InetSocketAddress inetaddr = new InetSocketAddress(addr[0], port); 
     try 
     { 
      Selector selector = Selector.open(); 
      SocketChannel channel; 
      for (i = 0; i < tcount; i++) 
      { 
       channel = SocketChannel.open(); 
       channel.configureBlocking(false); 
       channel.register(selector, SelectionKey.OP_CONNECT); 
       channel.connect(inetaddr); 
      } 
      open = tcount; 
      while (open > 0) 
      { 
       int selected = selector.select(); 
       System.out.println("Selected=" + selected); 
       Iterator<SelectionKey> it = selector.selectedKeys().iterator(); 
       while (it.hasNext()) 
       { 
        SelectionKey key = it.next(); 
        it.remove(); 
        channel = (SocketChannel)key.channel(); 
        if (key.isConnectable()) 
        { 
         System.out.println("isConnectable"); 
         if (channel.finishConnect()) 
         { 
          System.out.println(formatAddr(channel) + " connected"); 
          key.interestOps(SelectionKey.OP_WRITE); 
         } 
        } 
        else if (key.isWritable()) 
        { 
         System.out.println(formatAddr(channel) + " isWritable"); 
         String message = formatAddr(channel) + " the quick brown fox jumps over the lazy dog"; 
         ByteBuffer buffer = ByteBuffer.wrap(message.getBytes()); 
         channel.write(buffer); 
         key.interestOps(SelectionKey.OP_READ); 
        } 
        else if (key.isReadable()) 
        { 
         System.out.println(formatAddr(channel) + " isReadable"); 
         ByteBuffer buffer = ByteBuffer.allocate(1024); 
         channel.read(buffer); 
         buffer.flip(); 
         byte[] bytes = new byte[buffer.remaining()]; 
         buffer.get(bytes); 
         String message = new String(bytes); 
         System.out.println(formatAddr(channel) + " read: '" + message + "'"); 
         channel.close(); 
         open--; 
        } 
       } 
      } 

     } 
     catch (IOException e) 
     { 
      e.printStackTrace(); 
     } 
    } 

    static String formatAddr(SocketChannel channel) 
    { 
     return Integer.toString(channel.socket().getLocalPort()); 
    } 
} 

爲此,可以使用命令行運行:

java -cp . NioTest3 <ipaddr>:<port> <num-connections> 

凡口應該是7,如果你對正在運行的真正的回聲服務;即:

java -cp . NioTest3 127.0.0.1:7 5 

如果你不能得到真實回波服務運行,那麼源頭之一是here。在Solaris下編譯回聲服務器:

$ cc -o echoserver echoserver.c -lsocket -lnsl 

像這樣運行:

$ ./echoserver 8007 > out 2>&1 & 

這已被報告給孫爲bug

回答

1

我曾-解決此bug使用下列內容:(,如果使用,並沒有超時超時版)

如果Selector.select()返回0,則:

  1. 遍歷註冊的鑰匙與選擇通過selector.keys().iterator()(記住而不是撥打iterator.remove())。
  2. 如果OP_CONNECT興趣已被設置爲關鍵,那麼請致電channel.finishConnect()並做任何事情已經完成,如果isConnectable()已返回true

例如:

if (selected == 0 && elapsed < timeout) 
{ 
    keyIter = selector.keys().iterator(); 
    while (keyIter.hasNext()) 
    { 
     key = keyIter.next(); 
     if (key.isValid()) 
     { 
      channel = (SocketChannel)key.channel(); 
      if (channel != null) 
      { 
       if ((key.interestOps() & SelectionKey.OP_CONNECT) != 0) 
       { 
        if (channel.finishConnect()) 
        { 
         key.interestOps(0); 
        } 
       } 
      } 
     } 
    } 
}   

這已被報告給孫爲bug

9

您的錯誤報告已被封閉爲「不是錯誤」,並附有解釋。您忽略了connect()的結果,如果爲true,則意味着OP_CONNECT將永遠不會觸發,因爲通道已連接。如果返回false,則只需要整個OP_CONNECT/finishConnect() megillah。所以,你應該甚至不應該註冊OP_CONNECT除非connect()返回false,更不用說註冊之前,你甚至稱connect().

而且備註:

引擎蓋下,OP_CONNECT和OP_WRITE是一樣的,這也解釋了部分的。

由於您有一個單線程,解決方法是在阻塞模式下進行連接,然後切換到非阻塞的I/O。

您是否在之後使用Selector註冊了select()

處理正確的方法非阻塞連接如下:

channel.configureBlocking(false); 
if (!channel.connect(...)) 
{ 
    channel.register(sel, SelectionKey.OP_CONNECT, ...); // ... is the attachment, or absent 
} 
// else channel is connected, maybe register for OP_READ ... 
// select() loop runs ... 
// Process the ready keys ... 
if (key.isConnectable()) 
{ 
    if (channel.finishConnect()) 
    { 
    key.interestOps(0); // or SelectionKey.OP_READ or OP_WRITE, whatever is appropriate 
    } 
} 

審覈您的擴展的代碼後,一些非詳盡的評論:

  1. 關閉通道取消鍵。你不需要這樣做。

  2. 非靜態removeInterest()方法未正確實現。

  3. TYPE_DEREGISTER_OBJECT也關閉通道。不知道這是否是你真正想要的。我會認爲它應該取消密鑰,並且應該有關閉通道的單獨操作。

  4. 你已經走到了小方法和異常處理的方向。 addInterest()和removeInterest()是很好的例子。它們捕獲異常並記錄它們,然後繼續進行,就像異常沒有發生時一樣,當它們實際執行的所有操作都被設置或清除一點時:一行代碼。最重要的是,它們中的很多都有靜態和非靜態版本。所有調用key.cancel(),channel.close()等的小方法也是如此。沒有任何意義,它只是對代碼行進行計時。它只會增加默默無聞,讓你的代碼更難理解。只需進行內聯操作,並在選擇循環的底部有一個捕捉器。

  5. 如果finishConnect()返回false,它不是連接失敗,它還沒有完成。如果它拋出一個異常,這是一個連接失敗。

  6. 您正在同時註冊OP_CONNECT和OP_READ。這沒有意義,它可能會導致問題。在OP_CONNECT觸發之前沒有什麼可讀的。首先註冊OP_CONNECT。

  7. 您正在爲每次讀取分配一個ByteBuffer。這非常浪費。在連接的生命中使用相同的一個。

  8. 你忽略了read()的結果。它可以是零。它可以是-1,表示EOS,您必須關閉該通道。您還假設您將在一次讀取中獲得完整的應用程序消息。你不能假設這一點。這是爲什麼您應該在連接的整個生命週期中使用單個ByteBuffer的另一個原因。

  9. 您忽略了write()的結果。當你調用它時,它可能比buffer.remaining()少。它可以是零。

  10. 通過將NetSelectable作爲關鍵附件,您可以大大簡化此操作。然後,您可以取消幾件事情,包括例如頻道圖和斷言,因爲密鑰的頻道必須始終等於密鑰的附件頻道。

  11. 我也肯定會將finishConnect()代碼移動到NetSelector,並讓connectEvent()只是成功/失敗通知。你不想傳播這種東西。對readEvent()做同樣的事情,即在NetSelector中用NetSelectable提供的緩衝區進行讀取,然後通知NetSelectable讀取結果:count或-1或 異常。同上寫:如果通道可寫,請從NetSelectable寫入內容,將其寫入NetSelector並通知結果。您可以讓通知回調函數返回一些內容以指示接下來要做什麼,例如關閉頻道。

但這真的是所有五倍複雜,因爲它需要是,並且你有這個錯誤的事實證明了它。簡化你的頭。

+0

是的,一旦打開了頻道並在「選擇器」中註冊了該頻道,並且在調用connect之後設置了對OP_CONNECT的興趣(否則引發ConnectionPendingException)。阻塞連接會導致我的問題 - 連接發生在特定的時間,如果它干擾其他線程的I/O,那就很糟糕。 – trojanfoe

+0

@trojanfoe該通道需要註冊並在connect()之前設置爲OP_CONNECT,否則可能會錯過該事件。在你調用connect()之前,不能拋出ConnectPendingException * - 我不明白。這個例外的原因在Javadoc中是非常明確的,但事實並非如此。 – EJP

+0

我已經做了這個改變,並在Linux下進行了測試(它的工作原理),但是它並沒有解決Solaris下的問題。我提到了錯誤的異常 - 它是NoConnectionPendingException,但不再發生,所以請忽略它。 – trojanfoe