How to wait till a TCP port is really (natively) closed in Java?
Asked Answered
K

1

12

Why am I asking it?

You can skip the story if you want. Still, some might be interested.

I have an embedded ZooKeeper server in Java. In the unit tests, I assign ports to the test servers dynamically. Before assigning the port I check if it is unused by opening up a ServerSocket, then closing it.

It happens from time to time, that in the unit tests I get BindException when I start up my server (it cannot be that I assign the same port to two servers since I use File locks as well for mutual exclusion). It turned out that the reason is, that for the port check I open up the port, then I close it and it waits for a while in operating system level till the port can be reopened.

There is however an option (StandardSocketOptions.SO_REUSEADDR) which can tell for the Java socket, that an old socket in TIMED_WAIT state can be reused. After checking the ZooKeeper code it is actually set to true (see 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);
}

My test however prooves that it does not work. I get BindException (under Linux JDK 1.7.0_60).

After checking the ServerSocketChannel implementation (JDK 1.7.0_60) I realized, that this NEVER works under linux. See 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;
    }
}

Unfortunately Net.useExclusiveBind() will never give back true under linux, if you check its source in the hopefully similar OpenJDK it depends on Net.isExclusiveBindAvailable(), which is -1 under Linux.

Do you have a workaround?

Is there a way in Java to wait till a port is really natively closed apart from opening up a ServerSocket without SO_REUSEADDR and checking if I get BindException? Of course, that is not a solution since then I have to close that ServerSocket again. Why is there nothing in Java like closing a socket in blocking mode, which would return only when the socket is really on the operating system level closed?

Kass answered 1/7, 2015 at 13:59 Comment(14)
This question might be of interest to you, in particular TwentyMiles' answer: https://mcmap.net/q/158058/-sockets-discover-port-availability-using-javaManganin
Surprised - simply opening and closing a socket puts it in TIME_WAIT state. that is rather strange! Or do you open, bind, listen and accept and then close? In your particular case I don't think SO_REUSEADDR isn't bad per se. As it's unlikely there's any stray unacked data that's going to be lingering around.Dulosis
@gabhijit: Take a look at the test code. Yes, there is an accept as well.Tedder
@Manganin : excellent idea. I could simply use a client socket for port testing. Thanks.Tedder
If you can control - client socket - there's indeed no problem (but client socket would then go in TIME_WAIT state). In general TIME_WAIT state isn't a very big problem and in your case it definitely isn't. Also SO_EXCLUSIVEREUSE is a windows only behavior and hence not portable and not expected to work in Linux. (after a quick look at documentation).Dulosis
Your test calls ss.setReuseAddress(false);, so that would naturally fail (ALL sockets have to call setReuseAddress(true);, not just one of them.. Tough the code from sun.nio.ch.ServerSocketChannelImpl.setOption you quote still calls Net.setSocketOption(this.fd, Net.UNSPEC, paramSocketOption, paramT);, which should properly set SO_REUSEADDR - you don't need Net.useExclusiveBind() to return true. Your unit test need to call ss.setReuseAddress(true);, and you certainly should close the socket you've accepted.Shawana
Get rid of the port-testing part altogether. Just do the real open that you need, of the ServerSocket that you actually plan to use, and deal with the BindException at that point. What you're engaged in at the moment is trying to foretell the future.Quintain
@Shawana : The ss.setReuseAddress(false) at the top of my test class is actually not interesting. If you set it to true, the error will still come as far as I think. Give it a try. The real point above is that you have no chance to have SO_REUSEADDR under linux for a ServerSocketChannelImpl instance.Tedder
@EJP : it is not that easy to deal with it if the whole thing should "reserve" ports for JMX, HTTP and other ports of several parallel running ServiceMix instances. I do not have control over the code of ServiceMix, I just configure the instances with PAX Exam. I think waiting till a socket is natively closed is really a missing feature in Java.Tedder
@GáborLipták ss.setReuseAddress(false) will cause you Zookeeper server to not be able to bind to that port, so I would presume that is quite interesting. I am also unsure why you think that you cannot have SO_REUSEADDR under linux for a ServerSocketChannelImpl. The code you quote calls Net.setSocketOption(this.fd, Net.UNSPEC, paramSocketOption, paramT);, which ends up with a native setsockopt() call - which is all you need to enable SO_REUSEADDR on linux. (The Net.isExclusiveBindAvailable() is derived from the sun.net.useExclusiveBind system property, and is relevant only on Windows )Shawana
@nos: I debugged it and it never comes to the line "this.isReuseAddress = ((Boolean)paramT).booleanValue();" on my linux with my JDK version.Tedder
@nos: like I said: ss.setReuseAddress(false) in the test has nothing to do with ZooKeeper. Zookeeper sets reuseaddress to true (and this has no effect under Linux). The ss.setReuseAddress(false) in the test is not interesting.Tedder
@GáborLipták That is 100% correct, it should not go to this.isReuseAddress = ((Boolean)paramT).booleanValue(), that is a Windows only codepath. The else clause handles it for linux. And it HAS something to do with Zookeeper, your server socket and the one zookeeper creates uses the same port. As I mentioned, ALL sockets bound to the same port MUST have enabled SO_REUSEADDR for it to have any effect.Shawana
Your test does not prove any such thing as you are claiming here. It would make more sense, and work, that being exactly and only what the API is for, if it really did what you state in your question , which is to (a) call setReuseAddtess(true) and (b) not do anything at all with Socket or connect() or accwot(), all of which is creating a second socket bound to the same port. It is needless and pointless. Voting to close as 'cannot reproduce'.Quintain
E
1

I am afraid your analysis of the sun.nio.ch.ServerSocketChannelImpl.setOption(SocketOption<T>, T) implementation is not correct.

The "exclusive bind" stuff is used on Windows only, and not relevant for other platforms. You can check this in the sources of the sun.nio.ch.Net class. Specifically, see the comments for isExclusiveBindAvailable().

When exclusive binding is not available, ServerSocketChannelImpl.setOption just calls Net.setSocketOption which at the end will end up calling the native setsockopt function.

BTW the server-side socket might not be in TIME_WAIT state, but also in FIN_WAIT_1 or FIN_WAIT_2 (waiting for the client side to acknowledge the close). This in-depth description of the TCP state machine may be helpful.

Ettore answered 4/2, 2020 at 9:40 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.