如何正确关闭Java NIO中的SocketChannel?

时间:2012-08-07 21:26:00

标签: java nio socketchannel

我有一个带主循环的简单非阻塞服务器:

try {
    while (selector.select() > -1) {

        // Wait for an event one of the registered channels

        // Iterate over the set of keys for which events are available
        Iterator selectedKeys = selector.selectedKeys().iterator();
        while (selectedKeys.hasNext()) {
            SelectionKey key = (SelectionKey) selectedKeys.next();
            selectedKeys.remove();
            try {
                if (!key.isValid()) {
                    continue;
                }

                if (key.isConnectable()) {
                    connect(key);
                }

                // Check what event is available and deal with it
                if (key.isAcceptable()) {
                    accept(key);
                }

                if (key.isReadable()) {
                    read(key);
                }

                if (key.isWritable()) {
                    write(key);
                }
            } catch (Exception e) {
                e.printStackTrace();
                close(key);
            }
        }
    }
} catch (Exception e) {
    e.printStackTrace();
}

在读/写部分,我会检查是否有东西要读/写 - 然后我尝试关闭频道:

if (channel.read(attachment.buffer) < 1) 
    close(key);

关闭方法:

private void close(SelectionKey key) throws IOException {
    key.cancel();
    key.channel().close();
}

但是在处理这段代码的过程中,我在主循环中遇到异常(它被捕获但我认为有些错误)我得到了这个堆栈跟踪:

java.nio.channels.CancelledKeyException
    at sun.nio.ch.SelectionKeyImpl.ensureValid(Unknown Source)
    at sun.nio.ch.SelectionKeyImpl.readyOps(Unknown Source)
    at java.nio.channels.SelectionKey.isWritable(Unknown Source)

因此,当进入写入部分,关闭通道并在“可写”部分中返回到主循环时,它在主循环上失败,并且失败并出现此类异常。有什么建议吗?

1 个答案:

答案 0 :(得分:9)

错误非常简单。

if (!key.isValid()) {
    continue;
}

if (key.isConnectable()) {
    connect(key);
}

// Check what event is available and deal with it
if (key.isAcceptable()) {
    accept(key);
}

if (key.isReadable()) {
    read(key);
}

if (key.isWritable()) {
    write(key);
}

您的read方法是取消SelectionKey的方法。但是,从read返回后,您再次测试通道是否可写的关键 - 可能在取消相同的键之后!你的初步检查在这里无济于事。


一种解决方案是检查密钥在可能被取消的任何地方是否有效:

...
if (key.isValid() && key.isWritable()) {
  write(key);
}
...

或者,您也可以尝试在任何特定频道上一次只注册一个兴趣,因此所有准备事件都是互斥的:

if (!key.isValid()) {
  continue;
}

if (key.isConnectable()) {
  connect(key);
} else if (key.isAcceptable()) {
  accept(key);
} else if (key.isReadable()) {
  read(key);
} else if (key.isWritable()) {
  write(key);
}

这可能对情况有益;因为通常一个通道几乎总是可以写入就绪,保持对沿着读取就绪的写入准备感兴趣可能会使Selector循环保持旋转,这很可能。在大多数情况下,通常仅在底层套接字输出缓冲区已满时注册写入就绪感兴趣。


作为旁注,请注意SocketChannel.read可以返回值< 1而不会出错。

  

读操作可能无法填充缓冲区,实际上它根本不会读取任何字节。它是否这样做取决于渠道的性质和状态。例如,处于非阻塞模式的套接字通道不能读取比套接字输入缓冲区中立即可用的字节数更多的字节;

此外,Selector.select未说明有关返回< -1以表明其已关闭的任何内容。

  

返回:已更新就绪操作集的键数(可能为零)