为啥写入此套接字会丢弃读取缓冲区(这真的是发生了啥)?

Posted

技术标签:

【中文标题】为啥写入此套接字会丢弃读取缓冲区(这真的是发生了啥)?【英文标题】:Why does writing to this socket discard the read buffer (and is that really what's happening)?为什么写入此套接字会丢弃读取缓冲区(这真的是发生了什么)? 【发布时间】:2015-07-07 20:19:36 【问题描述】:

考虑以下 sn-p:

QTcpServer server;
server.listen(QHostAddress::LocalHost);

QTcpSocket clientSocket;
clientSocket.connectToHost(server.serverAddress(), server.serverPort());

// ...wait for connection to succeed...

QTcpSocket *serverSocket = server.nextPendingConnection();
serverSocket->write("test");
serverSocket->close();

这个 sn-p 的目标是通过创建一个 QTcpServer 来创建两个连接的套接字,该 QTcpServer 侦听传入连接并与另一个 QTcpSocket 连接到它。建立连接后,数据将写入其中一个套接字并关闭。

QAbstractSocket::close() invokes disconnectFromHost() documentation for QAbstractSocket::disconnectFromHost() 明确指出:

如果有等待写入的未决数据,QAbstractSocket 将进入 ClosingState 并等待直到所有数据都已写入。

下一个 sn-p 短暂进入事件循环处理挂起的事件,然后尝试从另一个套接字读取字符串:

QCoreApplication::processEvents();
qDebug() << clientSocket.readAll();

这会打印出"test",一切都很好。或者也许不是。如果我通过预先调用write() 来修改之前的sn-p,则无法再从套接字读取数据:

clientSocket.write("test");
QCoreApplication::processEvents();
qDebug() << clientSocket.readAll();

这将打印一个空字符串 (""),而不是上一次运行的预期值。为什么socket这次不能读取值了?

注意:在 Windows 平台上体现。 Qt 的 Linux 和 Mac OS X 版本都没有表现出这种行为,而是在两种情况下都打印预期值。

第二点:如果你想玩代码,这里有一个工作要点:https://gist.github.com/nathan-osman/ee6116d120903db84384

第三点:这是 Wireshark 捕获 TCP 交换的屏幕截图:

【问题讨论】:

我的猜测:由于套接字的另一端已关闭,我希望尝试写入它会使套接字处于错误状态,并可能导致它自动关闭。之后是否仍然可以从套接字读取数据可能取决于实现。 @HarryJohnston 第一次写入和关闭操作直到QCoreApplication::processEvents(); 行才发生(我用调试器验证了这一点)。不过,它们可能仍然是乱序处理的。 如果不能保证排序,它可能仅在 UNIX 上工作,因为 Qt 恰好选择在处理 serverSocket.close() 之前处理 clientSocket.write()。但这也可能是由于底层套接字实现的差异。 @HarryJohnston 我添加了 TCP 交换的屏幕截图,以防有助于阐明事件的顺序。第一个write() 立即得到确认,然后在客户端套接字写入任何内容之前发送并确认FIN 数据包。 这与 Linux / MacOSX 上的事件序列相比如何? 【参考方案1】:

我对 Qt 不是很熟悉,但是在 Windows 上,没有 Fork。您正在尝试在同一线程上适应并行任务,但您应该让客户端在与服务器不同的线程上运行。

【讨论】:

OP 也没有在其他平台上使用 fork()。 我从未在 Linux 上工作过,也许 API 正在这样做? Windows 支持异步 I/O,我希望 Qt 能够利用它,因此您不一定需要多个线程。【参考方案2】:

您正在以同步方式编写异步代码,但无法确保您等待足够长的时间让事情发生。调用processEvents 仅仅意味着“做任何当前可以做的工作”而不是“做工作和/或等待工作,直到没有更多的工作可能到达”。你把它当作后者,但它的意思是前者。

最简单的解决方法是不要这样做。

将槽或函子连接到相关的套接字信号,并将控制权返回给事件循环。永远不要使用waitForXxx 函数,它们根本没有被充分指定以允许在所有情况下使用。不要介意重新进入事件循环时将面临的可怕的重入问题。

如果您的代码运行的线程中没有事件循环,请自行启动:

QEventLoop loop;

...
connect(serverSocket, &QAbstractSocket::disconnected,
        &loop, &QEventLoop::quit);
loop.exec(); // this will keep executing until the socked is disconnected

【讨论】:

好点。我有一个例子here 虽然它使用QTRY_COMPARE 而不是processEvents(),但它仍然表现出相同的行为。 QTRY_COMPARE() 创建一个事件循环并运行它,直到两个值相等或达到超时。 @NathanOsman 尝试完全不关闭发送套接字,看看是否接收到数据。如果是这样,那么在 Windows 上可能存在某个地方的实现错误或平台限制。不过,您确实需要在您的问题中提供一个完整的、独立的、单文件的测试用例。 但我做到了。链接就在问题的底部。

以上是关于为啥写入此套接字会丢弃读取缓冲区(这真的是发生了啥)?的主要内容,如果未能解决你的问题,请参考以下文章

为啥在可重复读取中会发生写入偏斜?

如何解决TCP拆包粘包问题

尝试读取或写入受保护的内存。这通常表明其他内存已损坏

读取时的套接字行为

为啥使用 OS 和磁盘缓冲区写入文件后读取操作更快?

CFReadStreamRead 读取时如何区分数据包?