【问题标题】:TCP Two-Way Communication using Qt使用 Qt 的 TCP 双向通信
【发布时间】:2019-04-03 11:00:33
【问题描述】:

我正在尝试在两台计算机之间设置 TCP 通信框架。我希望每台计算机向另一台计算机发送数据。因此计算机 A 将执行计算,并将其发送到计算机 B。然后计算机 B 将读取此数据,使用它执行计算,并将结果发送回计算机 A。计算机 A 将等待直到它从计算机 B 接收到某些内容继续执行另一个计算,并将其发送到计算机 B。

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

我基本上希望有两个“服务器”相互通信。我相信,上面的同步方法对于我正在尝试做的事情很重要。但我正在努力通过单个套接字设置双向通信框架。

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

【问题讨论】:

  • 为什么要区分“服务器”和“客户端”?一侧必须启动连接。一旦 TCP 连接打开,任何一方都可以通过它发送和接收。
  • 我不知道 Qt 或 C++,但这看起来很有希望:stackoverflow.com/questions/16520744/…
  • “服务器-客户端”通信是双向的。服务器和客户端都能够发送和接收数据。你想做什么但不起作用?

标签: c++ qt sockets tcp


【解决方案1】:

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

要考虑的事情:

  • 协议。手卷的还是现有的?
    • 现有协议可能是重量级的,具体取决于您的有效负载。示例包括 HTTP 和 Google ProtoBuf;还有很多
    • 手卷可能意味着更多的工作,但更受控制。有两种通用方法:基于长度和基于标记。
      • 基于长度意味着将长度嵌入到第一个字节中。需要关心字节序。需要考虑如果消息的长度超过了可以嵌入长度字节的长度怎么办。如果你这样做,我强烈建议你在一些数据文件中定义你的数据包格式,然后生成低级数据包编码逻辑。
      • 基于标记的意思是当看到某个字符(或序列)时结束消息。常见的哨兵是'\0''\n'"\r\n"。如果您的协议的其余部分也是基于文本的,这意味着它更容易调试。
      • 对于这两种设计,您都必须考虑如果对方尝试发送的数据超出您愿意(或能够)存储在内存中的数据会发生什么。在任何一种情况下,将有效负载大小限制为 16 位无符号整数可能是一个好主意。您可以使用多个数据包流式回复。请注意,serious 协议(基于 UDP + 加密)通常具有 512-1500 字节的协议层大小限制,尽管应用层当然可能更大。
      • 对于这两种设计,套接字上的 EOF没有标记意味着您必须删除消息并记录错误。
  • 主循环。 Qt 可能有一个你可以使用,但我不知道。
    • 可以使用单独的阻塞操作来开发简单的操作,但我不建议这样做。 始终假设网络连接的另一端是一个危险的精神病患者,他知道你住在哪里。
    • 主循环中有两个基本操作:
      • 套接字事件:套接字报告已准备好读取或准备写入。还有其他类型的事件您可能不会使用,因为大多数有用的信息可以在读/写处理程序中单独找到:异常/优先级、(写)挂断、读挂断、错误。
      • 定时器事件:当某个时间增量过去时,中断等待套接字事件系统调用并分派到定时器堆。如果您没有,请传递“无限”的系统调用概念。但是如果你的睡眠时间很长,你可能想要一些任意的、相对的数字,比如“10 秒”或“10 分钟”,具体取决于你的应用程序,因为长时间的定时器间隔可以做各种奇怪的事情,比如时钟变化、休眠等等。如果您足够小心并使用正确的 API,则可以避免这些情况,但大多数人不会。
    • 多路复用系统调用的选择:
      • 下面的p 版本包括原子信号掩码更改。我不建议使用它们;相反,如果您需要信号,请将 signalfd 添加到集合中,或者使用信号处理程序和(非阻塞,小心!)管道来模拟它。
      • select/pselect 是经典,随处可见。不能超过FD_SETSIZE 文件描述符,这可能非常小(但如果你足够小心,可以在命令行上使用#defined。稀疏集效率低下。超时为select 的微秒和纳秒pselect,但你可能无法真正得到它。只有在别无选择时才使用它。
      • poll/ppoll 解决了稀疏集的问题,更显着地解决了比FD_SETSIZE 文件描述符多听的问题。它确实使用更多内存,但使用起来更简单。 poll 是 POSIX,ppoll 是 GNU 特定的。对于这两者,API 都为超时提供了纳秒粒度,但您可能无法获得。如果您需要 BSD 兼容性并且不需要大规模可扩展性,或者如果您只有一个套接字并且不想处理 epoll 的问题,我建议您这样做。
      • epoll 解决了每次都必须重新指定文件描述符和事件列表的问题。通过保留文件描述符列表。除此之外,这意味着当低级内核事件发生时,epoll 可以立即被告知,无论用户程序是否已经在系统调用中。支持边沿触发模式,但除非你确定确定你理解它,否则不要使用它。它的 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 套接字,您只需要 listening 套接字的处理程序中的 acceptaccept/connected 处理程序中的 read/write 系列。对于其他类型的套接字,您需要send/recv 系列。请参阅他们手册页中的“另请参阅”以获取更多信息 - 有时其中之一可能对您有用,请在您将过多的硬编码到 API 设计中之前执行此操作
      • 您需要认真考虑缓冲问题。缓冲读取意味着您需要能够检查数据包的标头以查看是否有足够的字节来处理它,或者您是否必须将字节存储到下一次。另请记住,您可能一次收到多个数据包(我建议您重新考虑您的设计,以便在发送下一个数据包之前得到回复之前不会强制阻塞)。缓冲写入比您想象的要难,因为您希望即使在没有数据可写入的套接字上也有“可写入”时被唤醒。应用程序不应该自己写,只排队写。虽然TCP_CORK 可能意味着不同的设计,但我没有使用它。
    • 提供迭代所有套接字的网络级公共 API。如果需要,请在更高级别实施;请记住,您可能拥有各种具有特殊用途的内部文件描述符。
  • 以上所有内容都适用于服务器和客户端。正如其他人所说,一旦建立连接,就没有真正的区别。

2019 年编辑:

D-Bus 和 0MQ 的文档值得一读,不管你是否使用它们。尤其值得考虑 3 种对话:

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

特别是 D-Bus 做出了很多起初看起来很奇怪的决定,但确实有理由(这可能是相关的,也可能是不相关的,具体取决于用例)。一般只在本地使用。

0MQ 是较低级别的,它的大部分“缺点”都是通过在它之上构建来解决的。注意 MxN 问题;您可能想人为地为容易出现的消息创建代理节点。

【讨论】:

    【解决方案2】:
    #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();       
    }
    }
    

    【讨论】:

    • 谢谢!这很有帮助。我实现了所有插槽,现在我正在尝试测试我的代码。我在我的计算机上运行同一应用程序的两个实例,并且我正在连接到同一主机(即 QHostAddress::Broadcast [255.255.255.255])。当我连接时,我收到以下消息序列:“套接字已开始建立连接。” ->“套接字未连接。” ->“权限被拒绝。”您知道为什么会发生这种情况吗?如果可能有帮助,我可以发布我的代码。再次感谢!
    • 我注意到上面的代码 sn-p 包括 QTcpServer... 我的代码中是否需要它?我目前只有一个插座...
    • 您需要在 tcp 服务器上监听传入连接以建立连接。服务器 = 新 QTcpServer(this);连接(服务器,信号(新连接()),这个,插槽(新连接())); server->listen(QHostAddress::Any, 端口); void newConnection() { QTcpSocket *socket = server->nextPendingConnection(); }
    【解决方案3】:

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

    【讨论】:

    • 感谢您的帮助。当我从服务器端连接时,我按如下方式“连接”: connect(this, &LocalServer::newConnection, [&]() {mSocket = nextPendingConnection();});而从客户端:connect(mLocalServer->mSocket, &QTcpSocket::readyRead, [&]() {QTextStream T(mLocalServer->mSocket); ui->listWidget->addItem(T.readAll());}) ;我是否有两个“连接”语句指定服务器和客户端的功能?我认为这是我对如何实现发送/接收功能感到困惑的地方......再次感谢。
    • 你有一个 (Q)TcpSocket,你可以从 readwrite 获取。建立后,您可以根据需要对其进行读写。定义一些协议,例如您在问题中描述的协议并实施它。通过查看您链接的文章,bogotobogo.com/Qt/Qt5_QTcpSocket_Signals_Slots.php 可能会有所帮助。
    【解决方案4】:

    看看QWebSocket,这是基于HTTP的,它也允许HTTPS

    【讨论】:

      猜你喜欢
      • 2010-10-26
      • 1970-01-01
      • 2013-12-01
      • 1970-01-01
      • 2015-08-14
      • 1970-01-01
      • 1970-01-01
      • 2011-09-21
      • 1970-01-01
      相关资源
      最近更新 更多