2011-06-30 53 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