TCP Two-Way Communication using Qt

TCP Two-Way Communication using Qt

本文关键字:Qt using Communication Two-Way TCP      更新时间:2023-10-16

我正在尝试在两台计算机之间设置TCP通信框架。我希望每台计算机都能向另一台发送数据。因此,计算机A将进行计算,并将其发送给计算机B。然后,计算机B将读取这些数据,使用它进行计算,然后将结果发送回计算机A。计算机A将等待,直到它从计算机B接收到一些东西,然后再进行另一次计算,并发送给计算机B.

这在概念上似乎很简单,但我还没有找到一个详细说明通过TCP进行双向(双向)通信的示例。我只发现了单向服务器-客户端通信,即服务器向客户端发送数据。以下是我迄今为止仔细研究过的一些例子:

  • 服务器客户端通信
  • 同步服务器客户端通信

我基本上希望有两个"服务器"相互通信。我相信,上面的同步方法对我要做的事情很重要。但我很难通过单个套接字建立双向通信框架。

如果有人能给我举一些例子来描述如何用TCP建立双向通信,或者从我上面链接的例子中给我一些关于如何建立双向通信的建议,我将不胜感激。我对TCP和网络通信框架非常陌生,可能会有很多误解,所以如果我能得到一些关于如何进行的明确指示,那就太好了。

这个答案不涉及细节,但它应该给你一个大致的想法,因为这似乎是你真正想要的。我以前从未使用过Qt,我直接使用BSD风格的套接字或使用自己的包装器来编写所有的网络代码。

需要考虑的事项:

  • 协议。手工轧制还是现有?
    • 现有协议可能是重量级的,这取决于您的有效负载。示例包括HTTP和Google ProtoBuf;还有很多
    • 手动可能意味着更多的工作,但更可控。一般有两种方法:基于长度的方法和基于哨点的方法。
      • 基于长度意味着将长度嵌入到第一个字节中。需要关心终结性。需要考虑如果消息的长度超过了可以嵌入长度字节的长度,该怎么办。如果你这样做,我强烈建议你在一些数据文件中定义你的数据包格式,然后生成低级别的数据包编码逻辑
      • 基于哨兵意味着当看到某个字符(或序列)时结束消息。常见的哨兵有'''n'"rn"。如果协议的其余部分也是基于文本的,这意味着调试起来要容易得多
      • 对于这两种设计,你都必须考虑如果对方试图发送比你愿意(或能够)存储在内存中的数据更多的数据,会发生什么。在任何一种情况下,将有效载荷大小限制为16位无符号整数都可能是一个好主意;您可以使用多个数据包流式传输回复。请注意,严重协议(基于UDP+加密)通常具有512-1500字节的协议层大小限制,当然应用层可能更大
      • 对于这两种设计,套接字上的EOF如果没有sentinel,则意味着您必须丢弃消息并记录错误
  • 主回路。Qt可能有一个你可以使用的,但我不知道。
    • 只使用阻塞操作可以开发简单的操作,但我不建议这样做。总是假设网络连接的另一端是一个危险的精神病患者,他知道你住在哪里
    • 主循环中有两个基本操作:
      • 套接字事件:套接字报告准备读取或准备写入。还有其他类型的事件可能不会使用,因为最有用的信息可以在读/写处理程序中单独找到:异常/优先级、(写)挂起、读挂起、错误
      • 计时器事件:当某个时间增量过去时,中断等待套接字事件syscall并将其分派到计时器堆。如果没有,可以传递syscalls中"无穷大"的概念。但是,如果你睡得很长,根据你的应用程序,你可能想要一些任意的、相对的数字,比如"10秒"或"10分钟",因为长时间的定时器间隔会对时钟变化、休眠等产生各种奇怪的影响。如果你足够小心并使用正确的API,就有可能避免这些问题,但大多数人不会
    • 多路传输系统调用的选择:
      • 下面的p版本包括原子信号掩码更改。我不建议使用它们;相反,如果您需要信号,请将signalfd添加到集合中,或者使用信号处理程序和(非阻塞,请小心!)管道来模拟它
      • select/pselect是经典,随处可见。不能有超过FD_SETSIZE的文件描述符,它可能很小(但如果你足够小心的话,命令行上可以是#defined。稀疏集效率低下。select的超时是微秒,pselect的超时是纳秒,但很可能你实际上无法获得。只有在别无选择的情况下才使用它
      • poll/ppoll解决了稀疏集的问题,更重要的是解决了侦听多于FD_SETSIZE的文件描述符的问题。它确实使用了更多的内存,但使用起来更简单。poll是POSIX,ppoll是GNU特定的。对于这两种情况,API都为超时提供了纳秒级的粒度,但您可能无法做到这一点。如果您需要BSD兼容性并且不需要巨大的可扩展性,或者如果您只有一个套接字并且不想处理epoll的头痛问题,我建议您这样做
      • CCD_ 18解决了每次都必须重新指定文件描述符和事件列表的问题。通过保留文件描述符的列表。除其他外,这意味着当发生低级内核事件时,无论用户程序是否已经在系统调用中,epoll都可以立即被感知。支持edge-triggered模式,但除非你确信你理解它,否则不要使用它。它的API只提供毫秒级的超时粒度,但这可能是你所能依赖的。如果您只能针对Linux,我强烈建议您使用它,除非您可以保证一次只能使用一个套接字,在这种情况下poll更简单
      • kqueue出现在BSD衍生的系统上,包括Mac OS X。它本应解决与epoll相同的问题,但它没有通过使用文件描述符来保持简单,而是具有各种奇怪的结构,并且不遵循"只做一件事"的原则。我从未使用过它。如果你需要在BSD上进行大规模的可伸缩性,请使用它
      • IOCP。这只存在于Windows和一些不知名的Unixen上。我从来没有用过它,而且它有明显不同的语义。使用这个,但要注意,这篇文章的大部分内容都不适用,因为Windows很奇怪。但是,你为什么要把Windows用于任何一种严肃的系统呢
      • io_uring。Linux 5.1中新的API。显著减少了系统调用和内存副本的数量。如果你有很多套接字,这是值得的,但由于它是如此新,你必须提供一个后备路径
    • 处理程序实现:
      • 当多路复用系统调用表示一个事件时,查找该文件号的处理程序(一些具有虚拟函数的类)并调用相关事件(注意可能有多个)
      • 确保所有套接字都设置了O_NONBLOCK,并禁用Nagle的算法(因为你自己在缓冲),但在连接之前可能会禁用connect的算法,因为这需要混乱的逻辑,尤其是如果你想处理多个DNS结果
      • 对于TCP套接字,您只需要listen套接字处理程序中的accept,以及accept/connect套接字处理程序的read/write族。对于其他类型的套接字,您需要send/recv族。有关更多信息,请参阅他们的手册页中的"See also"-很可能其中一个会对您有用。有时,在您对API设计进行过多硬编码之前,请先执行
      • 你需要认真考虑缓冲。缓冲读取意味着您需要能够检查数据包的标头,看看是否有足够的字节来处理它,或者您是否必须将字节存储到下次。还要记住,你可能一次收到多个数据包(我建议你重新考虑你的设计,这样你就不会强制阻止,直到你在发送下一个数据包之前得到回复)。缓冲写入比您想象的要困难,因为即使在没有数据可写的套接字上,当存在"可写"时,您也不希望被唤醒。应用程序永远不应该自己写入,只应该对写入进行排队。虽然TCP_CORK可能意味着不同的设计,但我没有使用过
    • 请不要提供在所有套接字上迭代的网络级公共API。如果需要,在更高的级别上实施;请记住,您可能有各种具有特殊用途的内部文件描述符
  • 以上所有内容都适用于服务器和客户端。正如其他人所说,一旦建立了连接,就没有真正的区别

编辑2019:

D-Bus和0MQ的文档值得一读,无论您是否使用它们。特别值得思考的是3种对话:

  • 请求/回复:"客户端"发出请求,"服务器"执行以下三件事之一:1。有意义地回答,2。回复说它不理解请求,3。无法回复(可能是由于断开连接,也可能是由于错误/恶意服务器)。不要让未经确认的请求拒绝"客户端"!这可能很困难,但这是一个非常常见的工作流
  • 发布/订阅:"客户端"告诉"服务器"它对某些事件感兴趣。每次事件发生时,"服务器"都会向所有注册的"客户端"发布一条消息。变体:,订阅在一次使用后过期。此工作流具有比请求/回复更简单的失败模式,但请考虑:1。服务器发布客户端没有请求的事件(可能是因为它不知道,也可能是因为不希望,或者因为它应该是oneshot,或者因为客户端发送了取消订阅,但服务器尚未处理它),2。这可能是一种放大攻击(尽管这对于请求/回复也是可能的,请考虑要求填充请求),3。客户端可能已断开连接,因此服务器必须小心取消订阅,4。(尤其是在使用UDP的情况下)客户端可能没有收到更早的通知。请注意,单个客户端多次订阅可能是完全合法的;如果没有天生的辨别数据,你可能需要保留一个cookie来取消订阅
  • distribute/collect:"master"将工作分配给多个"slave",然后收集结果,也就是映射/减少同一事物的任何其他重新发明的术语。这类似于上面的组合(客户端订阅工作可用事件,然后服务器向每个客户端发出唯一的请求,而不是普通的通知)。注意以下附加情况:1。有些从程序非常慢,而另一些则处于空闲状态,因为它们已经完成了任务,而主程序可能必须存储不完整的组合输出2。一些奴隶可能会给出错误的答案。可能没有任何从机,4

D-Bus尤其做出了很多起初看起来很奇怪的决定,但确实有正当理由(根据用例的不同,这些决定可能相关,也可能不相关)。通常,它只在本地使用。

0MQ是较低级别的,它的大多数"缺点"都是通过在其上构建来解决的。小心MxN问题;您可能想要人为地创建一个代理节点,只用于容易出现这种情况的消息。

#include <QAbstractSocket>
#include <QtNetwork>
#include <QTcpServer>
#include <QTcpSocket>
QTcpSocket*   m_pTcpSocket;

连接到主机:使用tcp套接字建立连接并实现您的插槽。如果数据字节可用,则会发出readyread()信号。

void connectToHost(QString hostname, int port){
if(!m_pTcpSocket)
{
m_pTcpSocket = new QTcpSocket(this);
m_pTcpSocket->setSocketOption(QAbstractSocket::KeepAliveOption,1);
}
connect(m_pTcpSocket,SIGNAL(readyRead()),SLOT(readSocketData()),Qt::UniqueConnection);
connect(m_pTcpSocket,SIGNAL(error(QAbstractSocket::SocketError)),SIGNAL(connectionError(QAbstractSocket::SocketError)),Qt::UniqueConnection);
connect(m_pTcpSocket,SIGNAL(stateChanged(QAbstractSocket::SocketState)),SIGNAL(tcpSocketState(QAbstractSocket::SocketState)),Qt::UniqueConnection);
connect(m_pTcpSocket,SIGNAL(disconnected()),SLOT(onConnectionTerminated()),Qt::UniqueConnection);
connect(m_pTcpSocket,SIGNAL(connected()),SLOT(onConnectionEstablished()),Qt::UniqueConnection);
if(!(QAbstractSocket::ConnectedState == m_pTcpSocket->state())){
m_pTcpSocket->connectToHost(hostname,port, QIODevice::ReadWrite);
}
}

写入:

void sendMessage(QString msgToSend){
QByteArray l_vDataToBeSent;
QDataStream l_vStream(&l_vDataToBeSent, QIODevice::WriteOnly);
l_vStream.setByteOrder(QDataStream::LittleEndian);
l_vStream << msgToSend.length();
l_vDataToBeSent.append(msgToSend);
m_pTcpSocket->write(l_vDataToBeSent, l_vDataToBeSent.length());
}

读取:

void readSocketData(){
while(m_pTcpSocket->bytesAvailable()){
QByteArray receivedData = m_pTcpSocket->readAll();       
}
}

TCP本质上是双向的。实现单向工作(客户端连接到服务器)。之后,两端可以完全相同的方式使用send和recv。

看看QWebSocket,它基于HTTP,还允许HTTPS