如何在Java中等待TCP端口真正(本地)关闭?

12

为什么我要问这个问题?

如果您想的话,可以跳过故事部分。但是有些人可能会感兴趣。

我在Java中嵌入了一个ZooKeeper服务器。在单元测试中,我动态地为测试服务器分配端口。在分配端口之前,我通过打开ServerSocket并关闭它来检查它是否未使用。

在单元测试中,有时候会出现BindException,当我启动我的服务器时(我不可能将相同的端口分配给两个服务器,因为我还使用文件锁进行互斥)。原因是对于端口检查,我打开端口,然后关闭它,并等待一段时间,直到端口可以重新打开。

然而,Java套接字有一个选项(StandardSocketOptions.SO_REUSEADDR),可以告诉它在TIMED_WAIT状态下可以重用旧套接字。在检查了ZooKeeper代码之后,发现它实际上被设置为true(参见org.apache.zookeeper.server.NIOServerCnxnFactory.configure(InetSocketAddress, int)):

@Override
public void configure(InetSocketAddress addr, int maxcc) throws IOException {
    configureSaslLogin();

    thread = new Thread(this, "NIOServerCxn.Factory:" + addr);
    thread.setDaemon(true);
    maxClientCnxns = maxcc;
    this.ss = ServerSocketChannel.open();
    ss.socket().setReuseAddress(true);
    LOG.info("binding to port " + addr);
    ss.socket().bind(addr);
    ss.configureBlocking(false);
    ss.register(selector, SelectionKey.OP_ACCEPT);
}

我的测试结果表明它不起作用。我在Linux JDK 1.7.0_60下遇到了BindException错误。

经过检查ServerSocketChannel实现(JDK 1.7.0_60)后,我意识到这在Linux下永远不会起作用。请参见sun.nio.ch.ServerSocketChannelImpl.setOption(SocketOption<T>, T)

public <T> ServerSocketChannel setOption(SocketOption<T> paramSocketOption, T paramT) throws IOException
{
    if (paramSocketOption == null)
        throw new NullPointerException();
    if (!(supportedOptions().contains(paramSocketOption)))
        throw new UnsupportedOperationException("'" + paramSocketOption + "' not supported");
    synchronized (this.stateLock) {
        if (!(isOpen()))
            throw new ClosedChannelException();
        if ((paramSocketOption == StandardSocketOptions.SO_REUSEADDR) && (Net.useExclusiveBind()))
        {
            this.isReuseAddress = ((Boolean)paramT).booleanValue();
        }
        else {
            Net.setSocketOption(this.fd, Net.UNSPEC, paramSocketOption, paramT);
        }
        return this;
    }
}

很遗憾,在Linux下,Net.useExclusiveBind()永远不会返回true。如果您查看其源代码(在类似OpenJDK中),它依赖于Net.isExclusiveBindAvailable(),而在Linux下,该值为-1。

你有解决方法吗?

除了打开一个没有SO_REUSEADDR选项的ServerSocket并检查是否收到BindException以外,Java中是否有一种等待端口真正本地关闭的方法?当然,这不是一个解决方案,因为我还必须再次关闭该ServerSocket。 为什么Java中没有像阻塞模式下关闭套接字那样的东西,只有在套接字在操作系统级别上真正关闭时才返回?


1
这个问题可能会对你有兴趣,特别是TwentyMiles的回答:https://dev59.com/IHRC5IYBdhLWcg3wCMc6#13826145 - Gimby
惊讶 - 只需打开和关闭套接字即可将其置于TIME_WAIT状态。这相当奇怪!或者您是openbindlistenaccept,然后再close吗?在您的特定情况下,我认为SO_REUSEADDR本身并不是坏事。因为很少有任何流浪未确认的数据会滞留。 - gabhijit
1
你的测试调用了 ss.setReuseAddress(false);,所以自然会失败(所有套接字都必须调用 setReuseAddress(true);,而不仅仅是其中一个)。尽管你引用的 sun.nio.ch.ServerSocketChannelImpl.setOption 代码仍然调用了 Net.setSocketOption(this.fd, Net.UNSPEC, paramSocketOption, paramT);,这应该正确设置了 SO_REUSEADDR - 你不需要让 Net.useExclusiveBind() 返回 true。你的单元测试需要调用 ss.setReuseAddress(true);,并且你肯定应该关闭你已经接受的套接字。 - nos
1
@GáborLipták ss.setReuseAddress(false) 会导致您的Zookeeper服务器无法绑定到该端口,因此我认为这非常有趣。我也不确定为什么您认为在ServerSocketChannelImpl下不能使用SO_REUSEADDR。您引用的代码调用了Net.setSocketOption(this.fd, Net.UNSPEC, paramSocketOption, paramT);,最终会产生一个本地的setsockopt()调用——这就是您需要在Linux上启用SO_REUSEADDR的全部内容。(Net.isExclusiveBindAvailable()源自sun.net.useExclusiveBind系统属性,仅在Windows上相关) - nos
1
@GáborLipták 这是100%正确的,它不应该进入this.isReuseAddress = ((Boolean)paramT).booleanValue(),这是一个仅适用于Windows的代码路径。else子句处理了Linux的情况。而且它与Zookeeper有关,您的服务器套接字和Zookeeper创建的套接字使用相同的端口。正如我所提到的,所有绑定到相同端口的套接字必须启用SO_REUSEADDR才能产生任何效果。 - nos
显示剩余9条评论
1个回答

1

很抱歉,您对 sun.nio.ch.ServerSocketChannelImpl.setOption(SocketOption<T>, T) 实现的分析是不正确的。

"独占绑定" 只在 Windows 平台上使用,并且与其他平台无关。您可以在 sun.nio.ch.Net 类的源代码 中进行验证。具体来说,请参阅 isExclusiveBindAvailable() 的注释。

当独占绑定不可用时,ServerSocketChannelImpl.setOption 仅调用 Net.setSocketOption,最终将调用本机的 setsockopt 函数。

顺便提一下,服务器端套接字可能不仅处于 TIME_WAIT 状态,还可能处于 FIN_WAIT_1 或 FIN_WAIT_2 状态(等待客户端确认关闭)。这个 TCP 状态机的深入描述 可能会有所帮助。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接