目录

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

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

一、TCP通信小结

在Qt中实现TCP通信时,通常使用QTcpServer和QTcpSocket两个类。QTcpServer用于创建服务器端应用程序,而QTcpSocket既可以用于创建客户端,也可以用于服务器端接受新的连接。

以下是使用这两个类实现TCP通信的基本区别和方法:

1. 使用 QTcpServer QTcpSocket 实现 TCP 服务器:

创建服务器:

  1. 创建一个QTcpServer对象,并设置服务器要监听的端口号。
  2. 调用listen()方法,服务器开始监听传入的连接请求。

接受连接:

  1. 当有客户端尝试连接时,QTcpServer会发出newConnection信号。
  2. 通过连接newConnection信号到一个槽函数,可以获取新建立的QTcpSocket对象,该对象代表一个新的客户端连接。

数据传输:

  1. 使用QTcpSocket对象进行数据的发送和接收。服务器可以使用同一个QTcpSocket与不同的客户端进行通信。
  2. 服务器和客户端之间的通信是可靠的,数据传输是有序的,并且保证数据的完整性。

2. 使用 QTcpSocket 实现 TCP 客户端:

建立连接:

  1. 创建一个QTcpSocket对象。
  2. 调用connectToHost()方法,客户端尝试连接到服务器。

连接确认:

  1. 客户端会监听connected信号,当连接成功建立后,可开始数据传输。

数据传输:

  1. 使用QTcpSocket对象发送和接收数据。
  2. 客户端和服务器之间的通信是双向的,可同时进行发送和接收操作。

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. // 创建客户端对象

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

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

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

31.

32. // 连接信号

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

34.

35. // 槽函数

36. void MyClass::onConnected() {

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

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

39. }

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

二、TCP服务器和TCP客户端运行联调

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

详细请查看:【QT学习笔记之TCP通信1(TCP服务器): 】

​​​​​​​    【QT学习笔记之TCP通信2(TCP客户端): 】

https://i-blog.csdnimg.cn/blog_migrate/815894b439dfbec7f85a67d2f78d4576.png

第二步:运行两个工程;

https://i-blog.csdnimg.cn/blog_migrate/a1fb9f518e74439c7e9ae5b3d37ae8f1.png https://i-blog.csdnimg.cn/blog_migrate/44fd98768a25e98cc082434ea2347618.png

成功运行如下图所示:

https://i-blog.csdnimg.cn/blog_migrate/90e47fe84fb36ce7ac507425f11d9b8c.png

第三步:查看本机地址;

打开命令提示符

https://i-blog.csdnimg.cn/blog_migrate/4f94550d44a29d47db022e9b80de33f6.png

输入 ipconfig ,找到 IPv4 地址

https://i-blog.csdnimg.cn/blog_migrate/99d878e1428d6662e2e4ef2ac21f9d3e.png

第四步:系统联调

连接客户端和服务器

https://i-blog.csdnimg.cn/blog_migrate/e8521f298870f2edb88cabddab28157e.png

完成数据互传

https://i-blog.csdnimg.cn/blog_migrate/c08f7c93e99c808180a2c2ba1f65c4b0.png

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

解决好的工程代码已经上传至 GitHub : 

问题 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, [this, newSocket](){

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. 

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