Java套接字和连接丢失问题

13

如何最恰当地检测一个 socket 是否已经断开连接?或者确认一个数据包是否已经被发送?

我有一个用于通过 Apple 网关向 iPhone 发送苹果推送通知的库(可在 GitHub 上获得)。客户端需要打开一个 socket 并发送每个消息的二进制表示;但不幸的是,Apple 不会返回任何确认信息。该连接还可以重复使用以发送多个消息。我正在使用简单的 Java Socket 连接。相关代码如下:

Socket socket = socket();   // returns an reused open socket, or a new one
socket.getOutputStream().write(m.marshall());
socket.getOutputStream().flush();
logger.debug("Message \"{}\" sent", m);
有时,如果在发送消息之前或之后连接断开,尽管Socket.getOutputStream().write()成功完成,但我认为这是由于TCP窗口尚未耗尽。

有没有一种方法可以确定数据包是否真的进入了网络?我尝试了以下两种解决方案:

  1. 插入一个具有250ms超时的附加socket.getInputStream().read()操作。这会强制进行读取操作,当连接被中断时失败,否则会等待250ms。

  2. 将TCP发送缓冲区大小(例如Socket.setSendBufferSize())设置为消息二进制大小。

这两种方法都有效,但它们会极大地降低服务质量;吞吐量从每秒100个消息降至最多约10个消息。

有什么建议吗?

更新:

受到多个答案对所述行为可能性的质疑的挑战。 我构建了描述行为的“单元”测试。 请查看Gist 273786中的单元测试用例。

两个单元测试都有两个线程,一个服务器和一个客户端。 在客户端发送数据时,服务器关闭而没有抛出任何IOException。 下面是主方法:

public static void main(String[] args) throws Throwable {
    final int PORT = 8005;
    final int FIRST_BUF_SIZE = 5;

    final Throwable[] errors = new Throwable[1];
    final Semaphore serverClosing = new Semaphore(0);
    final Semaphore messageFlushed = new Semaphore(0);

    class ServerThread extends Thread {
        public void run() {
            try {
                ServerSocket ssocket = new ServerSocket(PORT);
                Socket socket = ssocket.accept();
                InputStream s = socket.getInputStream();
                s.read(new byte[FIRST_BUF_SIZE]);

                messageFlushed.acquire();

                socket.close();
                ssocket.close();
                System.out.println("Closed socket");

                serverClosing.release();
            } catch (Throwable e) {
                errors[0] = e;
            }
        }
    }

    class ClientThread extends Thread {
        public void run() {
            try {
                Socket socket = new Socket("localhost", PORT);
                OutputStream st = socket.getOutputStream();
                st.write(new byte[FIRST_BUF_SIZE]);
                st.flush();

                messageFlushed.release();
                serverClosing.acquire(1);

                System.out.println("writing new packets");

                // sending more packets while server already
                // closed connection
                st.write(32);
                st.flush();
                st.close();

                System.out.println("Sent");
            } catch (Throwable e) {
                errors[0] = e;
            }
        }
    }

    Thread thread1 = new ServerThread();
    Thread thread2 = new ClientThread();

    thread1.start();
    thread2.start();

    thread1.join();
    thread2.join();

    if (errors[0] != null)
        throw errors[0];
    System.out.println("Run without any errors");
}
< p > [顺带提一下,我也有一个并发测试库,可以使设置变得更好、更清晰。请查看 gist 上的示例。] < p>运行时我得到以下输出:
Closed socket
writing new packets
Finished writing
Run without any errors

正如我在答案中已经提到的,如果你想确保没有错误发生,你必须执行优雅的连接终止。执行shutdownOutput(),然后读取直到接收到EOF,然后关闭socket。优雅的连接终止实质上是从对端接收到一个确认,表明它已经正确接收了你的信息,这正是你想要的。 - Tzvetan Mikov
3个回答

17

这可能对你并没有太大的帮助,但是从技术上讲,你提出的两种解决方案都是不正确的。OutputStream.flush() 和其他你能想到的 API 调用都不能实现你需要的功能。

唯一可移植且可靠的方式来确定数据包是否已被对等方接收,就是等待对等方的确认。这个确认可以是一个实际的响应,也可以是一个优雅的套接字关闭操作。就是这样 - 真的没有其他方法,这与 Java 无关 - 这是基础的网络编程。

如果这不是一个持久连接 - 也就是说,如果你只是发送一些内容然后关闭连接 - 那么你需要捕获所有 IOException 异常(它们中的任意一个都表示错误),并执行优雅的套接字关闭:

1. socket.shutdownOutput();
2. wait for inputStream.read() to return -1, indicating the peer has also shutdown its socket

1
完全正确。如果应用层没有给出任何确认,那么协议就是不可靠的,你只能忍受这一点(毕竟仅知道数据包已经发送到了网络中,并不能告诉你它是否完整地到达了另一端)。 - caf

3
经历了许多断开连接的麻烦后,我将我的代码迁移到使用增强格式,这基本上意味着您需要更改包的外观,使其看起来像这样:

enter image description here

这样,如果出现错误,苹果不会断开连接,而是会向套接字写入反馈代码。

2
如果你使用TCP/IP协议向苹果发送信息,你必须接收确认。然而,你所说的是:
“苹果根本不会返回任何确认。”
那么你的意思是什么?TCP/IP保证传递,因此接收方必须确认收到。它不保证传递何时发生。
如果你向苹果发送通知,并在接收到ACK之前中断连接,那么就无法确定你是否成功,因此你只能再次发送通知。如果重复推送相同的信息是一个问题或设备没有正确处理,那么就存在问题。解决方案是修复设备处理重复推送通知的方式:在推送方面无法做任何事情。
@评论澄清/问题
好的。你理解的第一部分是对第二部分的回答。只有收到ACK的数据包才已经被正确地发送和接收。我们可以想象一些非常复杂的跟踪每个单独数据包的方案,但TCP应该抽象出这个层次并为你处理。在你的端口,你只需要处理可能发生的多种失败情况(如果在Java中发生任何异常,就会引发异常)。如果没有异常,你刚刚尝试发送的数据将由TCP/IP协议保证已发送。

是否存在数据似乎被“发送”但不能保证被接收而没有引发异常的情况?答案应该是否定的。

@示例

很好的例子,这解释了很多问题。我本来以为会抛出一个错误。在发布的示例中,第二次写入时会抛出错误,但第一次不会。这是有趣的行为...我无法找到解释它为什么会这样行为的信息。然而,这解释了为什么我们必须开发自己的应用程序级协议来验证传递。

看起来您是正确的,如果没有确认协议,苹果设备就无法保证收到通知。此外,苹果只会排队最后一条消息。通过对该服务进行稍微了解,我能够确定这个服务更多地是为了方便客户,但不能用于保证服务,必须与其他方法结合使用。我从以下来源中阅读到了这些信息。

http://blog.boxedice.com/2009/07/10/how-to-build-an-apple-push-notification-provider-server-tutorial/

似乎答案是否定的,你无法确定。你可能可以使用类似Wireshark的数据包嗅探器来判断它是否被发送,但由于服务的性质,这仍然不能保证它已被接收并发送到设备。

我的意思是,苹果公司除了 TCP ACK 之外,并没有返回任何特定的协议确认信息。我也不知道如何告诉哪些数据包已经正确发送,哪些没有。 - notnoop
谢谢您的更新。我意识到我应该把问题的重点放在TCP上,并且给出例子,而不是讨论苹果服务。我想现在我会放弃对于断开连接检测支持的追求。 - notnoop

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