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

适用读者

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

QTcpServer与QTcpClient连接过程介绍

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

问题定位及解决方案

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

对象创建失败

在上图中的步骤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友好的封装,都可以帮助我们快速的定位解决问题。