Skip to main content

Qt中Tcp相关问题定位方法汇总

如果你刚开始学习使用Qt的QTcpServer、QTcpClient编写CS架构的应用程序,或者已经写过一些简单的程序,但当问题出现时不知到该如何定位,这篇文档应该对你有所帮助。

QTcpServer与QTcpClient连接过程介绍#

首先我们总结一下QTcpServer与QTcpClient间建立连接的过程。对整个过程熟悉后,可以帮助我们快速定界问题发生在哪个阶段。然后再针对对应的阶段,详细介绍可能发生的问题及解决方案。

Qt的Tcp通信步骤

问题定位及解决方案#

问题定位与解决的时间往往比写代码的时间要多很多,因此在写代码时就应注意可能出现的错误有哪些,出现错误后怎样处理可以既保证功能的稳定有能支撑快速定位。常见的处理方法有记录日志、抛出断言、退出程序等。

对象创建失败#

在上图中的步骤1、5可能出现。对象创建失败一般出现在操作系统内存不足,这种情况很少遇到,但相应的处理可以在极端场景下帮助提供一些蛛丝马迹。

tcpServer = new QTcpServer(this);
if (tcpServer == nullptr) {
qDebug() << "tcpServer is null.";
return;
}

注:系统内存不足时可能日志都无法打印,有人认为对new返回值的判断只是一个心理安慰,谁知道呢。

QTcpServer调用listen失败#

在上图中的步骤2可能出现。listen失败一般是因为端口被其他应用程序占用。可以通过listen函数的返回值判单是否listen成功,并通过errorString函数打印错误的原因。

if (!tcpServer->listen(QHostAddress::Any, 58586)) {
QMessageBox::critical(this, tr("Fortune Server"),
tr("Unable to start the server: %1.")
.arg(tcpServer->errorString()));
close();
return;
}

当错误信息显示The bound address is already in use时,代表端口被其他应用程序占用,windows下可以在终端工具中输入netstat和tasklist命令确认,并找到是哪个应用程序占用了端口。

C:\Users\tiny>netstat -ano | findstr 58586
协议 本地地址 外部地址 状态 PID
TCP 0.0.0.0:58586 0.0.0.0:0 LISTENING 9408
TCP [::]:58586 [::]:0 LISTENING 9408
C:\Users\tiny>tasklist | findstr 9408
fortuneserver.exe 9408 Console 1 23,424 K

linux下在终端工具中输入netstat命令即可查询到被哪个应用占用

tiny% sudo netstat -anp | grep 58586
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 0.0.0.0:58586 0.0.0.0:* LISTEN 26/nc

信号槽绑定失败#

在上图中的步骤3、6、11可能出现。基于SINGNAL,SLOT宏的老绑定方式,可能出现函数名错误导致绑定不生效的问题,在编辑、链接阶段不报错提示,需要到运行时才有报错提示。

connect(server, SIGNAL(newConnection()), this, SLOT(onNewConnectionXXX()));
connect(server, SIGNAL(newConnectionXXX()), this, SLOT(onNewConnection()));

只需要注意应用程序输出中的错误打印既可。

QObject::connect: No such slot Server::onNewConnectionXXX() in ..\server.cpp:10
QObject::connect: No such signal QTcpServer::newConnectionXXX() in ..\server.cpp:10

建议使用Qt5引入的新绑定方式,绑定函数指针。如果代码不小心敲错了,在静态检查、编译阶段即可报错。

connect(server, &QTcpServer::newConnection, this, &Server::onNewConnection);

另外有一点非常重要,不要我忘记绑定QTcpSocket的error信号了,绑定error信号后一旦发生错误可以快速根据提示信息定位问题。

connect(tcpSocket, QOverload<QAbstractSocket::SocketError>::of(&QAbstractSocket::error),
this, &Client::displayError);

未进入Qt的事件循环#

在上图中的步骤4、9可能出现。一般情况下很少会出现这个问题,但也曾经遇到过在写一个小demo时,觉得写信号槽绑定太麻烦了直接在Server端一个while循环socket连接。

#include <QCoreApplication>
#include <QTcpServer>
#include <QTcpSocket>
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
QTcpServer server;
if (!server.listen(QHostAddress::Any, 58586)) {
qDebug() << "listen failed:" << server.errorString();
}
while (1) {
QTcpSocket *socket = server.nextPendingConnection();
if (socket != nullptr) {
qDebug() << "new client connected";
while (1) {
qDebug() << socket->readAll();
}
}
}
return a.exec();
}

这样写看似没有问题,但实际上nextPendingConnection永远拿不到与Client建立连接的QTcpSocket。因为QTcpServer中调用accept接口获取连接是在Qt的事件循环中完成的。

connectToHost、newConnection信号触发、nextPendingConnection获取QTcpSocket对象失败#

在上图中的步骤7、8、10可能出现。整个连接流程由connectToHost开始触发,其他步骤由Qt框架内部事件循环自动触发。而connectToHost没有返回值,错误原因及后续处理动作需要通过绑定QTcpSocket的error信号实现。

connect(tcpSocket, QOverload<QAbstractSocket::SocketError>::of(&QAbstractSocket::error),
this, &Client::displayError);
void Client::displayError(QAbstractSocket::SocketError socketError)
{
switch (socketError) {
case QAbstractSocket::HostNotFoundError:
QMessageBox::information(this, tr("Fortune Client"),
tr("The host was not found. Please check the "
"host name and port settings."));
break;
case QAbstractSocket::ConnectionRefusedError:
QMessageBox::information(this, tr("Fortune Client"),
tr("The connection was refused by the peer. "
"Make sure the fortune server is running, "
"and check that the host name and port "
"settings are correct."));
break;
default:
QMessageBox::information(this, tr("Fortune Client"),
tr("The following error occurred: %1.")
.arg(tcpSocket->errorString()));
}
}

具体错误可以参考errorString中的描述。这里介绍几种常见的错误。

  1. 防火墙未允许应用进行通信。本地调试时建议关闭防火墙,最终上线时根据应用需求配置防火墙白名单。

  2. 连接域名时报错HostNotFoundError。需要检查网络连接和dns设置,首先保证在终端中可以ping通。

    C:\Users\tiny>ping www.baidu11111.com
    Ping 请求找不到主机 www.baidu11111.com。请检查该名称,然后重试。
  3. IP层连接不通,一般会报SocketTimeoutError。同样的使用ping命令,检查网络是否通畅。

    C:\Users\tiny>ping 192.168.0.111
    正在 Ping 192.168.0.111 具有 32 字节的数据:
    来自 192.168.0.155 的回复: 无法访问目标主机。
    来自 192.168.0.155 的回复: 无法访问目标主机。
    来自 192.168.0.155 的回复: 无法访问目标主机。
    来自 192.168.0.155 的回复: 无法访问目标主机。
    192.168.0.111 的 Ping 统计信息:
    数据包: 已发送 = 4,已接收 = 4,丢失 = 0 (0% 丢失),
  4. Server端没有成功listen,一般会报ConnectionRefusedError。可以在Server端通过netstat命令确认端口状态是否是LISTENING

    C:\Users\tiny>netstat -ano | findstr 58586
    TCP 0.0.0.0:58586 0.0.0.0:0 LISTENING 13192
    TCP [::]:58586 [::]:0 LISTENING 13192
  5. 本地系统开启了代理,还会报一些代理相关的错误,例如:ProxyConnectionRefusedError。请检查本地代理配置。

Write发送、onReadyRead接收错误#

在上图中的步骤12、13、14、15、16、17可能出现。如果前面的步骤都保证了没有错误,一般发送接收不会出现特殊的问题。可以针对write接口的返回值进行判断,确认数据是否成功发送。

if(clientConnection->write(block) <= 0) {
qDebug() << "clientConnection write failed";
// 相应的可靠性处理动作
}

扩展阅读#

前面的介绍都是以单线程接口调用讲解连接建立、首发的流程,及可能出现的错误与解决方案。实际应用中还会有更复杂的场景,但基于接口返回值和error信号绑定来获取错误,定位处理错误的整体思路不变。建议参考Qt官方示例中的Fortune Client Example、Fortune Server Example、Threaded Fortune Server Example。

对于更深层次的问题,通过函数返回值、errorString也无法确认根因的问题,可能需要对系统原生的Socket编程有一定的了解,结合netstat命令中展示的TCP连接状态进一步定位。一般的情况下,Qt友好的封装,都可以帮助我们快速的定位解决问题。