玛卡巴卡的卡巴卡玛 发表于 2024-8-2 18:55:37

QT学习条记之TCP通信3(TCP服务器与客户端数据互传)

 一、TCP通信小结

   在Qt中实现TCP通信时,通常使用QTcpServer和QTcpSocket两个类。QTcpServer用于创建服务器端应用步伐,而QTcpSocket既可以用于创建客户端,也可以用于服务器端接受新的毗连。
以下是使用这两个类实现TCP通信的基本区别和方法:
1. 使用QTcpServer和QTcpSocket实现TCP服务器:

①创建服务器:

[*]创建一个QTcpServer对象,并设置服务器要监听的端标语。
[*]调用listen()方法,服务器开始监听传入的毗连请求。
②接受毗连:

[*]当有客户端实验毗连时,QTcpServer会发出newConnection信号。
[*]通过毗连newConnection信号到一个槽函数,可以获取新建立的QTcpSocket对象,该对象代表一个新的客户端毗连。
③数据传输:

[*]使用QTcpSocket对象进行数据的发送和吸收。服务器可以使用同一个QTcpSocket与不同的客户端进行通信。
[*]服务器和客户端之间的通信是可靠的,数据传输是有序的,并且包管数据的完整性。
2. 使用QTcpSocket实现TCP客户端:

①建立毗连:

[*]创建一个QTcpSocket对象。
[*]调用connectToHost()方法,客户端实验毗连到服务器。
②毗连确认:

[*]客户端会监听connected信号,当毗连成功建立后,可开始数据传输。
③数据传输:

[*]使用QTcpSocket对象发送和吸收数据。
[*]客户端和服务器之间的通信是双向的,可同时进行发送和吸收操作。
3. 示例代码:

①服务器端:
   1. // 创建服务器对象

2. QTcpServer *server = new QTcpServer(this);

3. // 设置端口号

4. server->listen(QHostAddress::Any, 1234);

5.  

6. // 连接信号

7. connect(server, &QTcpServer::newConnection, this, [=](qintptr socketDescriptor) {

8.     QTcpSocket *clientSocket = server->nextPendingConnection();

9.     connect(clientSocket, &QTcpSocket::readyRead, this, &MyClass::onReadyRead);

10.     connect(clientSocket, &QTcpSocket::disconnected, this, &MyClass::onDisconnected);

11. });

12.  

13. // 槽函数

14. void MyClass::onReadyRead() {

15.     QTcpSocket *clientSocket = qobject_cast<QTcpSocket *>(sender());

16.     if (clientSocket) {

17.         QString data = clientSocket->readAll();

18.         // 处理接收到的数据

19.     }

20. }

21.  

22. void MyClass::onDisconnected() {

23.     QTcpSocket *clientSocket = qobject_cast<QTcpSocket *>(sender());

24.     if (clientSocket) {

25.         // 客户端断开连接的处理

26.     }

27. }    ②客户端:
   1. // 创建客户端对象

2. QTcpSocket *clientSocket = new QTcpSocket(this);

3. // 尝试连接到服务器

4. clientSocket->connectToHost("server.example.com", 1234);

5.  

6. // 连接信号

7. connect(clientSocket, &QTcpSocket::connected, this, &MyClass::onConnected);

8.  

9. // 槽函数

10. void MyClass::onConnected() {

11.     // 连接成功,可以发送数据

12.     clientSocket->write("Hello, Server!");

13. }    在Qt中使用QTcpServer和QTcpSocket类实现TCP通信时,可以很容易地创建服务器和客户端应用步伐,并通过信号和槽机制处理数据传输和毗连事件。这种方式使得网络编程在Qt框架下变得简便而高效。
    二、TCP服务器和TCP客户端运行联调

第一步:打开已经创建好的服务器和客户端工程;

具体请查看:【QT学习条记之TCP通信1(TCP服务器):http://t.csdnimg.cn/VU7kD】
                ​​​​​​​    【QT学习条记之TCP通信2(TCP客户端):http://t.csdnimg.cn/pEvWc】
https://img-blog.csdnimg.cn/direct/d33ed8cb3c8b430ba88c474885c171c8.png
第二步:运行两个工程;

https://img-blog.csdnimg.cn/direct/91e0fc59b78941abbffb50199a165567.pnghttps://img-blog.csdnimg.cn/direct/8f5be7bfdb414a709c7d273895b62c17.png
        成功运行如下图所示:
https://img-blog.csdnimg.cn/direct/704fe62093db4c0caebe6a3a3e833ec2.png
第三步:查看本机地址;

①打开命令提示符

https://img-blog.csdnimg.cn/direct/1e6e8c0338bf482487e97a6a4bc1166b.png
②输入ipconfig,找到IPv4地址

https://img-blog.csdnimg.cn/direct/e8cd5bf5bef4433b9ca13cdad489d6f9.png
第四步:系统联调

①毗连客户端和服务器

https://img-blog.csdnimg.cn/direct/1972a0319f0d4111957a6762acd468e6.png
②完成数据互传

https://img-blog.csdnimg.cn/direct/5fe618ff30ff4d44b41edc52584f9077.png

三、QT学习条记之TCP通信1、2问题总结

解决好的工程代码已经上传至GitHub:  https://github.com/Dake-7/QT
问题1:每次断开再重新打开服务器和客户端的毗连后,客户端吸收框显示的数据后面额外增加两行回车;

解答:问题出现在 connected_Slot 函数中,该函数被设计为在 QTcpSocket 对象成功毗连到服务器时调用。然而,每次调用 connected_Slot 函数时,都会重新毗连信号 readyRead 到槽函数 readyRead_Slot。
这意味着,每次客户端成功毗连到服务器后,readyRead_Slot 函数都会被重新毗连,而不是仅仅在第一次毗连时毗连一次。假如服务器在毗连后发送了数据,每次 readyRead_Slot 被调用时,它都会将吸收到的数据追加到 ui->receiveEdit 控件中。假如每次毗连后都有数据发送,那么每次毗连都会在吸收框中追加一次数据,包括可能的回车符或其他控制字符。
为了解决这个问题,应该只在客户端第一次设置 QTcpSocket 对象时毗连 readyRead 信号到 readyRead_Slot 槽函数。这可以在构造函数中完成,确保只毗连一次,如下所示:
   1. Widget::Widget(QWidget *parent) :

 2.     QWidget(parent),

 3.     ui(new Ui::Widget)

 4. {

 5.     ui->setupUi(this);

 6.  

 7.     tcpSocket = new QTcpSocket(this);

 8.  

 9.     // 连接 readyRead 信号到 readyRead_Slot 槽函数,只连接一次

10.     connect(tcpSocket, SIGNAL(readyRead()), this, SLOT(readyRead_Slot()));

11. }

12. 然后,从 connected_Slot 函数中移除重复的连接代码:

13.  

14. void Widget::connected_Slot()

15. {

16.     // 不要再次连接 signal-slot,因为这已经在上面完成了

17. }这样,无论客户端实验毗连到服务器多少次,readyRead 信号只会在客户端初始化时毗连到 readyRead_Slot 槽函数一次。这将防止每次毗连时都重新毗连信号和槽,从而避免吸收框中出现额外的回车行。
问题2:关闭服务器,但是不关闭客户端,客户端仍旧可以向服务器传输数据;

解答:服务器关闭操作是通过调用 tcpServer->close()实现的。这个操作会导致 QTcpServer 对象停止监听新的毗连请求,并且会关闭所有通过该服务器创建的 QTcpSocket 套接字。然而,这里有一个关键点需要注意:tcpServer->close(); 并不会立刻关闭所有已经建立的客户端毗连,而是会停止接受新的毗连请求。
对于已经建立的客户端毗连,QTcpSocket 套接字在服务器端的关闭操作通常需要一个额外的步骤来完成。当服务器调用 close() 方法时,它会发送一个 TCP 毗连停止(FIN)信号给客户端,告诉客户端毗连即将关闭。客户端收到这个信号后,会启动一个四步握手过程来关闭毗连。在这个过程中,客户端可能仍旧可以或许发送数据,直到它的发送缓冲区被清空并且确认了服务器的关闭请求。这意味着,即使服务器已经调用了 close() 方法,客户端在完成四步握手之前发送的数据仍旧有可能到达服务器。这就是为什么服务器在关闭后仍旧可以吸收到客户端发送的数据的原因。
要确保服务器完全停止吸收数据,需要在服务器端的 QTcpSocket 套接字上调用 disconnectFromHost() 方法,或者在客户端调用 close() 方法来自动关闭毗连。这样,客户端会发送一个 FIN 包给服务器,服务器收到后会确认这个关闭请求,从而完成四步握手过程,确保两边都不再发送数据。在代码中,假如服务器端的 QTcpSocket 套接字在 newConnection_Slot() 中被关闭,客户端可能仍旧在实验发送数据。假如服务器端没有精确处理关闭逻辑,客户端发送的数据可能会在服务器端的吸收缓冲区中被吸收到,即使服务器已经停止监听新的毗连请求。
为了确保服务器关闭后完全停止吸收数据,应该在服务器端的 QTcpSocket 套接字上实现适当的关闭逻辑,确保在关闭套接字之前,所有的数据都已经被处理,并且两边都同意关闭毗连。这通常涉及到以下几个步骤:
1. 停止监听新的毗连:调用 tcpServer->close() 或 tcpServer->listen(QHostAddress::Any, 0) 来停止服务器监听新的毗连请求。
2. 关闭所有现有的毗连:遍历所有已建立的 QTcpSocket 对象,并对每个套接字调用 close() 方法来关闭它们。
3. 等候所有套接字关闭:在关闭套接字后,确保等候直到所有套接字都完成了关闭过程。
以下是一个示例代码,展示了怎样在服务器端实现这个过程:
   1. #include "widget.h"

 2. #include "ui_widget.h"

 3. #include <QTcpSocket>

 4. #include <QList>

 5.  

 6. class Widget : public QWidget {

 7.     Q_OBJECT

 8.  

 9. public:

10.     explicit Widget(QWidget *parent = nullptr) :

11.         QWidget(parent),

12.         ui(new Ui::Widget) {

13.         ui->setupUi(this);

14.  

15.         tcpServer = new QTcpServer(this);

16.         tcpSockets = new QList<QTcpSocket*>();

17.         connect(tcpServer, &QTcpServer::newConnection, this, &Widget::newConnection_Slot);

18.     }

19.  

20.     ~Widget() {

21.         closeAllSockets();

22.         delete ui;

23.     }

24.  

25.     void closeAllSockets() {

26.         // 停止监听新的连接

27.         tcpServer->close();

28.  

29.         // 等待所有套接字关闭

30.         foreach (QTcpSocket *socket, *tcpSockets) {

31.             if (socket->state() == QTcpSocket::ConnectedState) {

32.                 socket->waitForDisconnected(5000); // 等待最多5秒

33.                 socket->close();

34.             }

35.         }

36.         // 清空套接字列表

37.         tcpSockets->clear();

38.     }

39.  

40.     void newConnection_Slot() {

41.         QTcpSocket *newSocket = tcpServer->nextPendingConnection();

42.         connect(newSocket, &QTcpSocket::disconnected, this, (){

43.             tcpSockets->removeOne(newSocket);

44.             delete newSocket;

45.         });

46.         tcpSockets->append(newSocket);

47.         connect(newSocket, &QTcpSocket::readyRead, this, &Widget::readyRead_Slot);

48.     }

49.  

50.     // ... 其他槽函数和成员函数 ...

51.  

52. private:

53.     Ui::Widget *ui;

54.     QTcpServer *tcpServer;

55.     QList<QTcpSocket*> *tcpSockets;

56. };

57.  

58. // ... 其他成员函数实现 ...在这个示例中,我们创建了一个 QList<QTcpSocket*> 来跟踪所有已建立的 QTcpSocket 毗连。当服务器需要关闭时,我们调用 closeAllSockets() 函数,它会停止服务器监听新的毗连,并关闭所有现有的套接字。
在 newConnection_Slot() 函数中,每当有新的毗连建立时,我们将其添加到 tcpSockets 列表中,并毗连其 disconnected 信号到一个 lambda 表达式,该表达式会从列表中移除套接字并删除它。
请注意,这个示例代码假设Widget 类已经精确设置了 Q_OBJECT 宏,并且已经实现了 readyRead_Slot 等槽函数。别的,closeAllSockets() 函数在关闭套接字时使用了 waitForDisconnected() 方法来等候套接字关闭,这样可以确保在关闭套接字之前,所有的数据都已经被处理。注意,可能需要根据现实情况调整等候时间。
问题3:端标语不能为0

解答:在=服务器和客户端的代码中,使用 "0"、"00"、"000"、"0000" 等作为端标语不能传输数据的原因在于 "0"、"00"、"000"、"0000" 不是一个有效的端标语。在网络编程中,端标语通常是一个介于 1 到 65535 之间的整数,用于标识特定的网络服务或应用步伐。使用 "0"、"00"、"000"、"0000" 作为端标语会导致几个问题:
1. 无效的端标语:端标语 "0"、"00"、"000"、"0000" 现实上相称于十进制的 0,这超出了有效端标语的范围。有效的端标语应该在 1 到 65535 之间。
2. 系统限定:操作系统和网络库通常不答应使用 0 或 "00" 作为端标语,因为它不是一个有效的值。实验使用这样的值会导致错误或异常。
3. 绑定失败:当实验使用 QTcpServer 或 QTcpSocket 绑定到端口 "0"、"00"、"000"、"0000" 时,绑定操作会失败,因为没有为该端口分配任何服务。
4. 无法监听:即使绑定操作没有明确失败,服务器也无法在端口 "0"、"00"、"000"、"0000" 上监听传入的毗连,因为没有为该端口分配任何服务。
5. 网络库的行为:大多数网络库,包括 Qt 的网络模块,都会对端标语进行验证,以确保它们在有效的范围内。假如端标语不在有效范围内,库可能会拒绝实验网络操作。
为了解决这个问题,应该确保在代码中使用有效的端标语。通常,可以通过要求用户输入一个介于 1 到 65535 之间的数字,或者在代码中硬编码一个有效的端标语来避免这个问题。比方,在 Qt 应用步伐中,可以使用 QSpinBox 或类似的控件来限定用户输入的端标语范围,确保用户不会输入无效的值。
在代码中,假如用户通过 ui->portEdit 输入端标语,可能需要添加一些验证逻辑来确保输入的值是一个有效的端标语。比方:
    1. // 假设这是在 on_openBt_clicked() 槽函数中

 2. bool portNumber = ui->portEdit->text().toUInt();

 3. if (portNumber < 1 || portNumber > 65535) {

 4.     // 显示错误消息,提示用户输入有效的端口号

 5.     QMessageBox::warning(this, "警告", "请输入一个介于 1 到 65535 之间的端口号。");

 6.     return;

 7. }

 8. // 继续使用有效的端口号进行操作

 9. tcpServer->listen(QHostAddress::Any, portNumber);

10.  通过这样的验证,可以确保只有有效的端标语被用于网络操作,从而避免因使用无效端标语而导致的问题。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: QT学习条记之TCP通信3(TCP服务器与客户端数据互传)