Java 套接字和断开的连接

发布于 2024-08-17 05:07:14 字数 3258 浏览 10 评论 0原文

检测套接字是否已断开的最合适方法是什么?或者数据包是否真的被发送了?

我有一个用于通过 Apple 网关向 iPhone 发送 Apple 推送通知的库(可在 GitHub 上找到)。客户端需要打开一个套接字并发送每条消息的二进制表示;但不幸的是苹果没有回复任何确认信息。该连接也可以重复使用来发送多条消息。我正在使用简单的 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. 插入一个额外的 socket.getInputStream().read() 操作,超时时间为 250 毫秒。这会强制读取操作在连接断开时失败,但会挂起 250 毫秒。

  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");
}

[顺便说一下,我还有一个并发测试库,这使得设置更好更清晰。也请检查示例的要点]。

运行时我得到以下输出:

Closed socket
writing new packets
Finished writing
Run without any errors

What's the most appropriate way to detect if a socket has been dropped or not? Or whether a packet did actually get sent?

I have a library for sending Apple Push Notifications to iPhones through the Apple gatways (available on GitHub). Clients need to open a socket and send a binary representation of each message; but unfortunately Apple doesn't return any acknowledgement whatsoever. The connection can be reused to send multiple messages as well. I'm using the simple Java Socket connections. The relevant code is:

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);

In some cases, if a connection is dropped while a message is sent or right before; Socket.getOutputStream().write() finishes successfully though. I expect it's due to the TCP window isn't exhausted yet.

Is there a way that I can tell for sure whether a packet actually got in the network or not? I experimented with the following two solutions:

  1. Insert an additional socket.getInputStream().read() operation with a 250ms timeout. This forces a read operation that fails when the connection was dropped, but hangs otherwise for 250ms.

  2. set the TCP sending buffer size (e.g. Socket.setSendBufferSize()) to the message binary size.

Both of the methods work, but they significantly degrade the quality of the service; throughput goes from a 100 messages/second to about 10 messages/second at most.

Any suggestions?

UPDATE:

Challenged by multiple answers questioning the possibility of the described. I constructed "unit" tests of the behavior I'm describing. Check out the unit cases at Gist 273786.

Both unit tests have two threads, a server and a client. The server closes while the client is sending data without an IOException thrown anyway. Here is the main method:

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");
}

[Incidentally, I also have a concurrency testing library, that makes the setup a bit better and clearer. Checkout the sample at gist as well].

When run I get the following output:

Closed socket
writing new packets
Finished writing
Run without any errors

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

御弟哥哥 2024-08-24 05:07:14

这对您没有太大帮助,但从技术上讲,您提出的两个解决方案都是不正确的。 OutputStream.flush() 以及您能想到的任何其他 API 调用都不会满足您的需要。

确定对等方是否已收到数据包的唯一可移植且可靠的方法是等待对等方的确认。此确认可以是实际响应,也可以是正常的套接字关闭。故事结束 - 确实没有其他方法,而且这不是 Java 特有的 - 它是基本的网络编程。

如果这不是持久连接 - 也就是说,如果您只是发送一些内容然后关闭连接 - 您执行此操作的方式是捕获所有 IOException(其中任何一个都表示错误)并执行正常的套接字关闭:

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

This not be of much help to you, but technically both of your proposed solutions are incorrect. OutputStream.flush() and whatever else API calls you can think of are not going to do what you need.

The only portable and reliable way to determine if a packet has been received by the peer is to wait for a confirmation from the peer. This confirmation can either be an actual response, or a graceful socket shutdown. End of story - there really is no other way, and this not Java specific - it is fundamental network programming.

If this is not a persistent connection - that is, if you just send something and then close the connection - the way you do it is you catch all IOExceptions (any of them indicate an error) and you perform a graceful socket shutdown:

1. socket.shutdownOutput();
2. wait for inputStream.read() to return -1, indicating the peer has also shutdown its socket
烟雨凡馨 2024-08-24 05:07:14

在遇到连接断开的麻烦后,我将代码移至 使用增强格式,这几乎意味着您将包更改为如下所示:

如果发生错误,Apple 不会断开连接,而是会向套接字写入反馈代码。

After much trouble with dropped connections, I moved my code to use the enhanced format, which pretty much means you change your package to look like this:

enter image description here

This way Apple will not drop a connection if an error happens, but will write a feedback code to the socket.

咽泪装欢 2024-08-24 05:07:14

如果您使用 TCP/IP 协议向 Apple 发送信息,则必须收到确认。然而你说:

Apple 不予退货
无论如何致谢

这是什么意思? TCP/IP 保证传送,因此接收方必须确认收到。但是,它不保证何时交货。

如果您向 Apple 发送通知,并且在收到 ACK 之前断开连接,则无法判断您是否成功,因此您必须再次发送。如果两次推送相同的信息出现问题或者设备未正确处理,则存在问题。解决方案是修复重复推送通知的设备处理:在推送端您无能为力。

@评论澄清/问题

好的。你所理解的第一部分就是你对第二部分的答案。只有收到ACKS的数据包才被正确发送和接收。我确信我们可以想出一些非常复杂的方案来自己跟踪每个单独的数据包,但 TCP 应该将这一层抽象出来并为您处理。最后,您只需处理可能发生的大量故障(在 Java 中,如果发生任何故障,就会引发异常)。如果没有异常,您刚刚尝试发送的数据将由 TCP/IP 协议保证发送。

是否存在数据看似“发送”但不保证收到且不引发异常的情况?答案应该是否定的。

@Examples

很好的例子,这让事情变得很清楚。我本以为会抛出错误。在发布的示例中,第二次写入时会引发错误,但第一次写入时不会引发错误。这是有趣的行为......我无法找到太多信息来解释它为什么会这样。然而,它确实解释了为什么我们必须开发自己的应用程序级协议来验证交付。

看来您是正确的,如果没有确认协议,则无法保证 Apple 设备会收到通知。苹果也只对最后一条消息进行排队。稍微看了一下服务,我可以确定这种服务更多的是为了方便客户,但不能用来保证服务,必须与其他方法结合起来。我从以下来源读到了这篇文章。

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

似乎答案是否定的可以肯定地说。您也许可以使用像 Wireshark 这样的数据包嗅探器来判断数据是否已发送,但由于服务的性质,这仍然不能保证数据已被接收并发送到设备。

If you're sending information using the TCP/IP protocol to apple you have to be receiving acknowledgements. However you stated:

Apple doesn't return any
acknowledgement whatsoever

What do you mean by this? TCP/IP guarantees delivery therefore receiver MUST acknowledge receipt. It does not guarantee when the delivery will take place, however.

If you send notification to Apple and you break your connection before receiving the ACK there is no way to tell whether you were successful or not so you simply must send it again. If pushing the same information twice is a problem or not handled properly by the device then there is a problem. The solution is to fix the device handling of the duplicate push notification: there's nothing you can do on the pushing side.

@Comment Clarification/Question

Ok. The first part of what you understand is your answer to the second part. Only the packets that have received ACKS have been sent and received properly. I'm sure we could think of some very complicated scheme of keeping track of each individual packet ourselves, but TCP is suppose to abstract this layer away and handle it for you. On your end you simply have to deal with the multitude of failures that could occur (in Java if any of these occur an exception is raised). If there is no exception the data you just tried to send is sent guaranteed by the TCP/IP protocol.

Is there a situation where data is seemingly "sent" but not guaranteed to be received where no exception is raised? The answer should be no.

@Examples

Nice examples, this clarifies things quite a bit. I would have thought an error would be thrown. In the example posted an error is thrown on the second write, but not the first. This is interesting behavior... and I wasn't able to find much information explaining why it behaves like this. It does however explain why we must develop our own application level protocols to verify delivery.

Looks like you are correct that without a protocol for confirmation their is no guarantee the Apple device will receive the notification. Apple also only queue's the last message. Looking a little bit at the service I was able to determine this service is more for convenience for the customer, but cannot be used to guarantee service and must be combined with other methods. I read this from the following source.

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

Seems like the answer is no on whether or not you can tell for sure. You may be able to use a packet sniffer like Wireshark to tell if it was sent, but this still won't guarantee it was received and sent to the device due to the nature of the service.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文