简体   繁体   English

使用Qt的TCP双向通信

[英]TCP Two-Way Communication using Qt

I am trying to setup a TCP communication framework between two computers. 我试图在两台计算机之间设置TCP通信框架。 I would like each computer to send data to the other. 我希望每台计算机都能将数据发送给另一台计算机。 So computer A would perform a calculation, and send it to computer B. Computer B would then read this data, perform a calculation using it, and send a result back to computer A. Computer A would wait until it receives something from computer B before proceeding with performing another calculation, and sending it to computer B. 因此,计算机A将执行计算,并将其发送到计算机B.计算机B然后将读取该数据,使用它执行计算,并将结果发送回计算机A.计算机A将等到它从计算机B接收到某些东西之前继续执行另一次计算,并将其发送到计算机B.

This seems conceptually straightforward, but I haven't been able to locate an example that details two-way (bidirectional) communication via TCP. 这似乎在概念上很简单,但我无法找到一个通过TCP详细说明双向(双向)通信的示例。 I've only found one-way server-client communication, where a server sends data to a client. 我只找到了单向服务器 - 客户端通信,服务器将数据发送到客户端。 These are some examples that I have looked at closely so far: 这些是我到目前为止密切关注的一些例子:

I'm basically looking to have two "servers" communicate with each other. 我基本上希望有两个“服务器”相互通信。 The synchronized approach above is, I believe, important for what I'm trying to do. 我相信上面的同步方法对于我正在尝试做的事情很重要。 But I'm struggling to setup a two-way communication framework via a single socket. 但我正在努力通过单个插槽设置双向通信框架。

I would appreciate it greatly if someone could point me to examples that describe how to setup bidirectional communication with TCP, or give me some pointers on how to set this up, from the examples I have linked above. 如果有人能指出我描述如何设置与TCP的双向通信的示例,或者从我上面链接的示例中给出一些关于如何设置它的指示,我将非常感激。 I am very new to TCP and network communication frameworks and there might be a lot that I could be misunderstanding, so it would be great if I could get some clear pointers on how to proceed. 我对TCP和网络通信框架都很陌生,可能有很多我可能会误解,所以如果我能得到一些关于如何继续的明确指示,那将是很好的。

This answer does not go into specifics , but it should give you a general idea, since that's what you really seem to be asking for. 这个答案没有详细说明 ,但它应该给你一个大致的想法,因为那是你真正想要的。 I've never used Qt before, I do all my networking code with BSD-style sockets directly or with my own wrappers. 我之前从未使用过Qt,我直接使用BSD风格的套接字或使用自己的包装器来完成所有网络代码。

Stuff to think about: 想一想:

  • Protocol. 协议。 Hand-rolled or existing? 手卷还是现有的?
    • Existing protocols can be heavyweight, depending on what your payload looks like. 现有协议可以是重量级的,具体取决于您的负载的外观。 Examples include HTTP and Google ProtoBuf; 示例包括HTTP和Google ProtoBuf; there are many more. 还有更多。
    • Handrolled might mean more work, but more controlled. 手动可能意味着更多的工作,但更多的控制。 There are two general approaches: length-based and sentinel-based. 有两种通用方法:基于长度和基于哨兵。
      • Length-based means embedding the length into the first bytes. 基于长度意味着将长度嵌入到第一个字节中。 Requires caring about endianness. 需要关心字节序。 Requires thinking about what if a message is longer than can be embedded in the length byte. 需要考虑一条消息是否比长度字节中嵌入的更长的内容。 If you do this, I strongly recommend that you define your packet formats in some data file, and then generate the low-level packet encoding logic. 如果这样做,我强烈建议您在某个数据文件中定义数据包格式,然后生成低级数据包编码逻辑。
      • Sentinel-based means ending the message when some character (or sequence) is seen. 基于Sentinel意味着在看到某个字符(或序列)时结束消息。 Common sentinels are '\\0' , '\\n' , and "\\r\\n" . 常见的哨兵是'\\0''\\n'"\\r\\n" If the rest of your protocol is also text-based, this means it is much easier to debug. 如果您的协议的其余部分也是基于文本的,这意味着它更容易调试。
      • For both designs, you have to think about what happens if the other side tries to send more data than you are willing (or able) to store in memory. 对于这两种设计,您必须考虑如果另一方尝试发送的数据超出您愿意(或能够)存储在内存中的数据,会发生什么。 In either case, limiting the payload size to a 16-bit unsigned integer is probably a good idea; 在任何一种情况下,将有效负载大小限制为16位无符号整数可能是一个好主意; you can stream replies with multiple packets. 您可以使用多个数据包传输回复。 Note that serious protocols (based on UDP + crypto) typically have a protocol-layer size limit of 512-1500 bytes, though application-layer may be larger of course. 请注意, 严重协议(基于UDP +加密)通常具有512-1500字节的协议层大小限制,但应用层当然可能更大。
      • For both designs, EOF on the socket without having a sentinel means you must drop the message and log an error. 对于这两种设计,套接字上的EOF 没有标记意味着您必须删除消息并记录错误。
  • Main loop. 主循环。 Qt probably has one you can use, but I don't know about it. Qt可能有一个你可以使用,但我不知道。
    • It's possible to develop simple operations using solely blocking operations, but I don't recommend it. 使用单独的阻塞操作可以开发简单的操作,但我不推荐它。 Always assume the other end of a network connection is a dangerous psychopath who knows where you live. 总是假设网络连接的另一端是一个危险的精神病患者谁知道你住在哪里。
    • There are two fundamental operations in a main loop: 主循环中有两个基本操作:
      • Socket events: a socket reports being ready for read, or ready to write. 套接字事件:套接字报告已准备好进行读取或准备写入。 There are also other sorts of events that you probably won't use, since most useful information can be found separately in the read/write handlers: exceptional/priority, (write)hangup, read-hangup, error. 还有其他类型的事件你可能不会使用,因为大多数有用的信息可以在读/写处理程序中单独找到:exception / priority,(write)hangup,read-hangup,error。
      • Timer events: when a certain time delta has passed, interrupt the wait-for-socket-events syscall and dispatch to the timer heap. 定时器事件:当某个时间增量已经过去时,中断wait-for-socket-events系统调用并调度到定时器堆。 If you don't have any, either pass the syscalls notion of "infinity". 如果你没有,要么传递系统调用“无限”的概念。 But if you have long sleeps, you might want some arbitrary, relatively number like "10 seconds" or "10 minutes" depending on your application, because long timer intervals can do all sorts of weird things with clock changes, hibernation, and such. 但是如果你长时间睡觉,你可能想要一些任意的,相对数字,如“10秒”或“10分钟”,这取决于你的应用程序,因为长时间间隔可以做各种奇怪的事情,时钟更改,休眠等。 It's possible to avoid those if you're careful enough and use the right APIs, but most people don't. 如果你足够小心并且使用正确的API,那么可以避免这些,但大多数人不这样做。
    • Choice of multiplex syscall: 多路系统调用的选择:
      • The p versions below include atomic signal mask changing. 下面的p版本包括原子信号掩码更改。 I don't recommend using them; 我不建议使用它们; instead if you need signals either add signalfd to the set or else emulate it using signal handlers and a (nonblocking, be careful!) pipe. 相反,如果你需要信号要么将signalfd添加到集合中,要么使用信号处理程序和(非阻塞,小心!)管道来模拟它。
      • select / pselect is the classic, available everywhere. select / pselect是经典的,随处可用。 Cannot have more than FD_SETSIZE file descriptors, which may be very small (but can be #define d on the command-line if you're careful enough. Inefficient with sparse sets. Timeout is microseconds for select and nanonseconds for pselect , but chances are you can't actually get that. Only use this if you have no other choice. 不能超过FD_SETSIZE文件描述符,这可能非常小(但如果你足够小心,可以在命令行上#define d。对于稀疏集合效率低。 select超时为微秒, pselect超时为纳秒,但机会是你实际上无法做到这一点。只有你别无选择才能使用它。
      • poll / ppoll solves the problems of sparse sets, and more significantly the problem of listening to more than FD_SETSIZE file descriptors. poll / ppoll解决了稀疏集的问题,更重要的是收听了超过FD_SETSIZE文件描述符的问题。 It does use more memory, but it is simpler to use. 它确实使用了更多内存,但使用起来更简单。 poll is POSIX, ppoll is GNU-specific. poll是POSIX, ppoll是GNU特有的。 For both, the API provides nanosecond granularity for the timeout, but you probably can't get that. 对于两者,API为超时提供纳秒粒度,但您可能无法实现。 I recommend this if you need BSD compatibility and don't need massive scalability, or if you only have one socket and don't want to deal with epoll 's headaches. 如果您需要BSD兼容性并且不需要大规模可扩展性,或者如果您只有一个套接字并且不想处理epoll的麻烦,我建议这样做。
      • epoll solves the problem of having to respecify the file descriptor and event list every time. epoll解决了每次都必须重新指定文件描述符和事件列表的问题。 by keeping the list of file descriptors. 通过保留文件描述符列表。 Among other things, this means that when, the low-level kernel event occurs, the epoll can immediately be made aware, regardless of whether the user program is already in a syscall or not. 除此之外,这意味着当发生低级内核事件时,无论用户程序是否已经在系统调用中,都可以立即使用epoll Supports edge-triggered mode, but don't use it unless you're sure you understand it. 支持边缘触发模式,但除非您确定理解它,否则不要使用它。 Its API only provides millisecond granularity for the timeout, but that's probably all you can rely on anyway. 它的API只为超时提供毫秒级的粒度,但这可能是你无论如何都可以依赖的。 If you are able to only target Linux, I strongly suggest you use this, except possibly if you can guarantee only a single socket at once, in which case poll is simpler. 如果你只能定位Linux,我强烈建议你使用它,除非你可以一次只保证一个套接字,在这种情况下poll更简单。
      • kqueue is found on BSD-derived systems, including Mac OS X. It is supposed to solve the same problems as epoll , but instead of keeping things simple by using file descriptors, it has all sorts of strange structures and does not follow the "do only one thing" principle. kqueue是在BSD派生系统上找到的,包括Mac OS X.它应该解决与epoll相同的问题,但不是通过使用文件描述符保持简单,它有各种奇怪的结构,不遵循“做”只有一件事“原则。 I have never used it. 我从来没用过它。 Use this if you need massive scalability on BSD. 如果您需要在BSD上进行大规模可扩展性,请使
      • IOCP. IOCP。 This only exists on Windows and some obscure Unixen. 这只存在于Windows和一些不起眼的Unixen上。 I have never used it and it has significantly different semantics. 我从未使用它,它具有明显不同的语义。 Use this, but be aware that much of this post is not applicable because Windows is weird. 使用此,但要注意,这篇文章的大部分内容都不适用,因为Windows很奇怪。 But why would you use Windows for any sort of serious system? 但是为什么你会在任何严肃的系统中使用Windows?
      • io_uring. io_uring。 A new API in Linux 5.1. Linux 5.1中的新API。 Significantly reducing the number of syscalls and memory copies. 显着减少系统调用和内存副本的数量。 Worth it if you have a lot of sockets, but since it's so new, you must provide a fallback path. 值得拥有的是,如果你有很多套接字,但由于它是如此新的,你必须提供一个后备路径。
    • Handler implementation: 处理程序实现:
      • When the multiplex syscall signifies an event, look up the handler for that file number (some class with virtual functions) and call the relevant events (note there may be more than one). 当Multiplex系统调用表示事件时,查找该文件编号的处理程序(某些具有虚函数的类)并调用相关事件(注意可能有多个)。
      • Make sure all your sockets have O_NONBLOCK set and also disable Nagle's algorithm (since you're doing buffering yourself), except possibly connect 's before the connection is made, since that requires confusing logic, especially if you want to play nice with multiple DNS results. 确保所有的插座都O_NONBLOCK集,并禁用Nagle算法(因为你在做缓冲自己),除了可能connect的是连接之前,因为这需要混乱的逻辑,特别是如果你要玩多个DNS不错结果。
      • For TCP sockets, all you need is accept in the listen ing socket's handler, and read/write family in the accept / connect ed handler. 对于TCP套接字,你需要的是acceptlisten荷兰国际集团插座的处理器,和read/write在家庭accept / connect编辑处理。 For other sorts of sockets, you need the send/recv family. 对于其他类型的套接字,您需要send/recv系列。 See the "see also" in their man pages for more info - chances are one of them will be useful to you sometimes, do this before you hard-code too much into your API design. 有关详细信息,请参阅其手册页中的“另请参阅” - 有时候其中一个对您有用, 您对API设计进行过多硬编码之前 ,请执行此操作。
      • You need to think hard about buffering. 你需要认真考虑缓冲。 Buffering reads means you need to be able to check the header of a packet to see if there are enough bytes to do anything with it, or if you have to store the bytes until next time. 缓冲读取意味着您需要能够检查数据包的标头,以查看是否有足够的字节来对其执行任何操作,或者您是否必须将字节存储到下次。 Also remember that you might receive more than one packet at once (I suggest you rethink your design so that you don't mandate blocking until you get the reply before sending the next packet). 还要记住,您可能会同时收到多个数据包(我建议您重新考虑您的设计,以便在发送下一个数据包之前得到回复之前不要强制阻止)。 Buffering writes is harder than you think, since you don't want to be woken when there is a "can write" even on a socket for which you have no data to write. 缓冲写入比你想象的更难,因为你希望在有一个“能写”哪怕是对你没有数据来写一个插座被唤醒 The application should never write itself, only queue a write. 应用程序永远不应该自己编写,只编写一个写入队列。 Though TCP_CORK might imply a different design, I haven't used it. 虽然TCP_CORK可能意味着不同的设计,但我还没有使用它。
    • Do not provide a network-level public API of iterating over all sockets. 提供遍历所有插座的网络级的公共API。 If needed, implement this at a higher level; 如果需要,在更高级别实施; remember that you may have all sorts of internal file descriptors with special purposes. 请记住,您可能拥有各种具有特殊用途的内部文件描述符。
  • All of the above applies to both the server and the client. 以上所有内容适用于服务器和客户端。 As others have said, there is no real difference once the connection is set up. 正如其他人所说,一旦建立连接就没有真正的区别。

Edit 2019: 编辑2019:

The documentation of D-Bus and 0MQ are worth reading, whether you use them or not. 无论您是否使用,D-Bus和0MQ的文档都值得一读。 In particular, it's worth thinking about 3 kinds of conversations: 特别值得考虑3种对话:

  • request/reply: a "client" makes a request and the "server" does one of 3 things: 1. replies meaningfully, 2. replies that it doesn't understand the request, 3. fails to reply (either due to a disconnect, or due to a buggy/hostile server). 请求/回复:“客户端”发出请求,“服务器”执行以下三项操作之一:1。有意义地回复,2。回复它不理解请求,3。未能回复(由于断开连接) ,或由于有缺陷/恶意服务器)。 Don't let un-acknowledged requests DoS the "client"! 不要让未经承认的请求DoS成为“客户”! This can be difficult, but this is a very common workflow. 这可能很困难,但这是一个非常常见的工作流程。
  • publish/subscribe: a "client" tells the "server" that it is interested in certain events. 发布/订阅:“客户端”告诉“服务器”它对某些事件感兴趣。 Every time the event happens, the "server" publishes a message to all registered "clients". 每次事件发生时,“服务器”都会向所有已注册的“客户”发布消息。 Variations: , subscription expires after one use. 变化:,订阅在一次使用后到期。 This workflow has simpler failure modes than request/reply, but consider: 1. the server publishes an event that the client didn't ask for (either because it didn't know, or because it doesn't want it yet , or because it was supposed to be a oneshot, or because the client sent an unsubscribe but the server didn't process it yet), 2. this might be a magnification attack (though that is also possible for request/reply, consider requiring requests to be padded), 3. the client might have disconnected, so the server must take care to unsubscribe them, 4. (especially if using UDP) the client might not have received an earlier notification. 该工作流具有比请求/应答简单的故障模式,但考虑:1,服务器将发布一个事件,客户没有要求(可能是因为它不知道,或者是因为它不希望 ,或者是因为它应该是一个单击,或者因为客户端发送了取消订阅,但服务器还没有处理它),2。这可能是放大攻击(尽管这也可能是请求/回复,考虑要求请求填充),3。客户端可能已断开连接,因此服务器必须注意取消订阅它们,4。(特别是如果使用UDP)客户端可能没有收到先前的通知。 Note that it might be perfectly legal for a single client to subscribe multiple times; 请注意,单个客户端多次订阅可能是完全合法的; if there isn't naturally discriminating data you may need to keep a cookie to unsubscribe. 如果没有自然歧视的数据,您可能需要保留一个cookie来取消订阅。
  • distribute/collect: a "master" distributes work to multiple "slaves", then collects the results, aka map/reduce any many other reinvented terms for the same thing. 分发/收集:“主人”将工作分配给多个“奴隶”,然后收集结果,即为同一事物映射/减少任何其他重新发明的术语。 This is similar to a combination of the above (a client subscribes to work-available events, then the server makes a unique request to each clients instead of a normal notification). 这类似于上述的组合(客户端订阅工作可用事件,然后服务器向每个客户端发出唯一请求而不是正常通知)。 Note the following additional cases: 1. some slaves are very slow, while others are idle because they've already completed their tasks and the master might have to store the incomplete combined output, 2. some slaves might return a wrong answer, 3. there might not be any slaves, 4. 请注意以下附加情况:1。某些从站非常慢,而其他从站已经完成任务,而主站可能必须存储不完整的组合输出,2。某些从站可能会返回错误的答案,3。可能没有任何奴隶,4。

D-Bus in particular makes a lot of decisions that seem quite strange at first, but do have justifications (which may or may not be relevant, depending on the use case). D-Bus特别做出了很多看起来很奇怪的决定,但确实有正当理由(根据用例情况可能相关或不相关)。 Normally, it is only used locally. 通常,它仅在本地使用。

0MQ is lower-level and most of its "downsides" are solved by building on top of it. 0MQ是较低级别的,其大部分“缺点”都是通过构建它来解决的。 Beware of the MxN problem; 小心MxN问题; you might want to artificially create a broker node just for messages that are prone to it. 您可能希望人为地创建一个代理节点,仅用于容易发生的消息。

TCP is inherently bidirectional. TCP本质上是双向的。 Get one way working (client connects to server). 获得一种方式(客户端连接到服务器)。 After that both ends can use send and recv in exactly the same way. 之后,两端都可以完全相同的方式使用send和recv。

#include <QAbstractSocket>
#include <QtNetwork>
#include <QTcpServer>
#include <QTcpSocket>

QTcpSocket*   m_pTcpSocket;

Connect to host: set up connections with tcp socket and implement your slots. 连接到主机:使用tcp套接字建立连接并实现您的插槽。 If data bytes are available readyread() signal will be emmited. 如果数据字节可用,则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);
}
}

Write: 写:

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

Read: 读:

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

看看QWebSocket,它基于HTTP,它也允许使用HTTPS

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM