如何让客户端套接字等待服务器套接字

8
如果客户端套接字在服务器套接字之前打开,Java将生成一个ConnectionException异常。因此,在客户端线程中执行socketChannel.open(hostname, port)之前,我必须检查服务器是否可用并保持等待状态。
我已经找到了一个相关的API:InetAddress.getByName(hostname).isReachable()。然而,这仍然不能告诉我们特定端口上的套接字是否打开。我认为这个问题应该很常见,但是我从谷歌和其他地方没有得到非常有用的信息。

5
捕获异常。暂停一段时间。再次调用该方法。 - vishnu viswanath
3个回答

14
boolean scanning=true;
while(scanning) {
    try {
        socketChannel.open(hostname, port);
        scanning=false;
    } catch(ConnectionException e) {
        System.out.println("Connect failed, waiting and trying again");
        try {
            Thread.sleep(2000);//2 seconds
        } catch(InterruptedException ie){
            ie.printStackTrace();
        }
    } 
}

这是音速评论的代码


+1 你可以不断尝试连接,直到成功,或者放弃。 - Peter Lawrey
1
@PeterLawrey,我想一个故障计数器可能会很有用,或者允许通过公共方法访问扫描,以便另一个线程可以停止这个。 - Cruncher
只有在尝试了X次后才放弃,这种情况比较少见。超时是更常见的情况。如果你需要连接服务器才能正常运行,我不会轻易放弃。 - Peter Lawrey
您可以在FitNesse中看到一个带有超时的示例,它从命令行启动“slim”服务器,然后在客户端上轮询连接:https://github.com/unclebob/fitnesse/blob/30b496e330add41ab36b7fa04b21f1a6e8fefecd/src/fitnesse/testsystems/slim/SlimCommandRunningClient.java#L87 - drrob
你可以使用“秒表”而不是重试计数器-在重试循环开始时获取时间,并在超过时间差后失败循环。这提供了一个漂亮的“总超时”语义,不依赖于算术运算(即最大重试次数*每次休眠时间),并且在休眠被中断时正确标记时间。当然,您可以结合两个限制,但是这样最大计数方面更多地用于捕获本地中断噪声,而秒表则是实际的“等待服务的时间”。 - Stevel
在回来编辑我的评论之前,我花了太长时间来写我的答案...(这不是很糟糕的用户体验吗??)总之,如果你喜欢我的评论(上面的),那么你可能也会喜欢我的答案,它可能在这个下面(直到你投票支持它,如果你认为它值得)。 - Stevel

4
我将为您提供这种处理程序,我在我的小游戏中使用它。
几次尝试后,它也会放弃。
private static int MAX_CONNECTION = 10;
private int reconnections = 0;

public void connect() {

            try {
                    this.socket = new Socket();
                    InetSocketAddress sa = new InetSocketAddress(this.server, this.port);
                    this.socket.connect(sa,500);
                    this.connected = true;

                    this.in = new InputStreamReader(this.socket.getInputStream());
                    this.out = new OutputStreamWriter(this.socket.getOutputStream());
            } catch (ConnectException e) {
                    System.out.println("Error while connecting. " + e.getMessage());
                    this.tryToReconnect();
            } catch (SocketTimeoutException e) {
                    System.out.println("Connection: " + e.getMessage() + ".");
                    this.tryToReconnect();
            } catch (IOException e) {
                    e.printStackTrace();
            }

    }
private void tryToReconnect() {
            this.disconnect();

            System.out.println("I will try to reconnect in 10 seconds... (" + this.reconnections + "/10)");
            try {
                    Thread.sleep(10000); //milliseconds
            } catch (InterruptedException e) {
            }

            if (this.reconnections < MAX_RECONNECTIONS) {
                    this.reconnections++;
                    this.connect();

            } else {
                    System.out.println("Reconnection failed, exeeded max reconnection tries. Shutting down.");
                    this.disconnect();
                    System.exit(0);
                    return;
            }

    }

以下是代码的解释:
private static final int MAX_CONNECTION = 10;
private int reconnections = 0;

首先,我声明了两个变量,其中一个是固定的,在运行时不能更改,它是我希望客户端在关闭之前尝试的最大次数。第二个变量是当前重新连接的尝试次数。

公共方法connect()用于连接套接字。我将跳过异常处理:

} catch (ConnectException e) {
    System.out.println("Error while connecting. " + e.getMessage());
    this.tryToReconnect();
} catch (SocketTimeoutException e) {
    System.out.println("Connection: " + e.getMessage() + ".");
    this.tryToReconnect();
}

当连接异常抛出时,捕获器会调用重新连接方法。
重新连接方法在每次尝试之间等待10秒,并且如果连接失败,则由connect()每次调用此方法。
如果连接已建立,则connect()不会再次调用tryToReconnect()。
如果在100秒内(即10次尝试,每10秒一次)无法连接,则程序退出。

2
递归在这里不是一个合适的技术。只需在原始方法中循环即可。 - user207421
为什么您认为循环更好?这样,如果发生断开连接的情况,我可以在connect()中放置reconnections = 0来重新启动10的计数。如果使用循环,如果连接成功,重新连接会更加困难... - Gianmarco
@EJP 它几乎不是递归。所有递归调用都在它们各自的方法的尾部。这里的大问题是,所有重新连接都停留在堆栈上,直到您进行连接。如果MAX_CONNECTIONS足够大,即堆栈大小的一半左右,则会达到最大递归深度。它确实像goto一样使用,但会消耗堆栈。Goto的邪恶孪生兄弟。 - Cruncher
我担心这段代码会抛出一个未处理的异常,即“StackoverflowError”。就像EJP所说的那样,递归在这里不合适。最好在原始方法中循环。 - Polar
我同意“递归不是最佳方法”的评估。除非你本质上是递归思维,否则这种复杂性并不值得,也根本不需要。而且,如果你有很多次重试(例如,使用一个根本不存在的服务器进行非常高的MAX_CONNECTIONS),你真的会耗尽线程的堆栈。如果你喜欢“重试计数”方法,请一定选择@Cruncher的答案;如果你喜欢“最大等待时间”的方式,请选择我的答案。(并点赞你喜欢的答案。) - Stevel

1

我来到这个对话框是为了寻找答案,然后我发表了一个“怎么样这样”的评论。现在,分享我的答案,复制并粘贴从我的工作(绿色单元测试)代码中未更改的答案,似乎是公平和社区意识。

private void makeSocketWithRetries(long maxWaitMS) throws IOException {
    Stopwatch retryWatch = new Stopwatch().reset();
    final long retryWaitMS = 500;

    do {
        try {
            socket = new Socket(host, port);
        }
        catch (ConnectException e) {
            logger.info("failed to connect tp [" + host + ":" + port + "]: retrying in " + retryWaitMS + "(ms)");
            try {
                Thread.sleep(retryWaitMS);
            } catch (InterruptedException interruptedException) {
                // swallow this exception
            }
        }
    }
    while ((socket == null) && (retryWatch.mark().deltaMS() <= maxWaitMS));

    if (socket == null) {
        String msg = "failed to connect tp [" + host + ":" + port + "] total wait time exceeded: " + retryWatch.deltaMS() + "(ms)";
        logger.info(msg);
        throw new ConnectException(msg);
    }
}

话虽如此,这里有几点需要注意:

  1. 日志记录有点多,但它显示出了正在发生的事情。对于重试来说,跟踪日志更有意义,也许对于“超时”异常也是如此。或者干脆取消日志记录。
  2. retryWait 应该是一个类常量(或者是一个带有验证/范围限制/如果很大则记录警告日志等参数)。至少它是一个本地final #8-)__
  3. 我的 Stopwatch 实用工具类没有显示出来,但它应该是不言自明的。
  4. 没有参数检查,尽管负的 maxWaitMS 等待仍然会进行至少一次尝试。

另外,我不统计(也不限制)重试的次数。根据我对 @Cruncher 解决方案的评论,我认为它通常并不是一个有用的功能。


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