QT中实现TCP通信

1.实现原理

在Qt中,要使用TCP通信必须要在.pro文件中加入network模块。在客户端部分,主要是用到QTcpsocket类创建socket对象去连接服务器端口,连接成功后即可正常传输数据。在判断连接状态时,主要会用到connected()、disconnected()、stateChanged()等相关信号 ;在数据读写的过程中,主要会用到类QIODevice中的bytesWritten()、readyRead()等信号。

在创建socket对象后, 可以绑定信号后直接连接服务器。

QTcpsocket *socket = new QTcpsocket(this);
connect(socket, SIGNAL(readyRead()), this, SLOT(SlotReadyRead()));
connect(socket, SIGNAL(bytesWritten(qint64)), this, SLOT(SlotBytesWritten(qint64)));
QHostAddress addr = g_controller_address;
socket->abort();    //这里是用来复位
socket->connectToHost(addr, 5566);
if(socket->waitForConnected(500))
{
    //作连接成功的处理
}
else
{
    //作连接失败的处理
}

2. 自己封装数据进行传输

在TCP传输的过程中,可以自己指定对应的数据包进行传输。

void NetClient::SlotSendCommand(qint64 cmd)
{
    m_total_bytes = 0;
    m_cur_command = cmd;
    QByteArray m_in_out_block;
    QDataStream send_cmd(&m_in_out_block, QIODevice::WriteOnly);
    send_cmd.setVersion(QDataStream::Qt_4_5);
    //这里进行数据包的定义
    send_cmd<<qint64(0)<<qint64(0)<<qint64(0)<<QString("");
    m_total_bytes += m_in_out_block.size();
    semd_cmd.device()->seek(0);    //将设备的指针位置置为0,方便后面重新给值

    //打包数据
    send_cmd<<cmd<<m_total_bytes<<qint64(m_in_out_block.size()-sizeof(qint64)*3)<<QString("test");
    //发送数据
    socket->write(m_in_out_block);
    //清空缓冲区
    m_in_out_block.resize(0);
}

3.用TCP传输文件

在传送文件的过程中,要用到几个变量用来统计传送的进度,包括:文件句柄、传送的总大小、文件名、已发送数据的大小、待发送数据的大小、每次发送的最大数据大小等。

bool IsHaveFile(const QString &file_path, const QString &file_name)
{
    QStringList file_list;     //将找到的文件存放于此
    if(file_path.isEmpty() || file_name.isEmpty())
        return false;
    file_list.clear();
    QDir dir;
    QStringList filters;
    filters<<file_name;    //这是过滤条件,可以添加多个选项,这里指定文件名字
    dir.setPath(file_path);
    dir.setNameFilters(filters);
    QDirIterator iter(dir, QDirIterator::Subdirectories);   //目录递归迭代器
    while(iter.hasNext())
    {
        iter.next();
        QFileInfo info = iter.fileInfo();
        if(info.isFile() && (info.size()>0))   //若是文件且文件不为空
            file_list.append(info.fileName());
    }
    //判断文件file_list中文件的个数
    if(file_list.isEmpty())
        return false;
    else
        return true;
}
void NetClient::SendFile()
{
    QString file_path("./config.xml");
    m_local_file = new QFile(file_path);
    if(!m_local_file->open(QFile::ReadOnly))
    {
        //打开失败的处理
    }
    m_cur_command = _TCP_COMMIT_FILE;
    m_total_bytes = m_local_file->size();
    QDataStream send_out(&m_in_out_block, QIODevice::WriteOnly);
    send_out.setVersion(QDataStream::Qt:4_5);
    m_file_name = file_path.right(file_path.size() - file_path.lastIndexOf('/')-1);
    
    //设置文件头的结构
    send_out<<qint64(0)<<qint64(0)<<qint64(0)<<QString(m_file_name);
    m_total_bytes += m_in_out_block.size();
    send_out.device()->seek(0)
    
    //打包数据
    send_out<<qint64(_TCP_COMMIT_FILE_)<<m_total_bytes<<qint64(m_in_out_block.size()-sizeof(qint64)*3)
    m_bytes_to_write = m_total_bytes - socket->write(m_in_out_block);
    m_in_out_block.resize(0);

    //上面已经将文件头发送,下面可以来显示进度条相关的操作
}

在向socket中写入数据时,会触发bytesWritten()信号,这里为了保证是写文件触发的,在其槽函数中做如下处理:

void NetClient::SlotBytesWritten(qint64 numBytes)
{
    if(m_cur_command == _TCP_COMMIT_FILE_)
        SlotSendUpdateProgress(numBytes);      //直接处理接下来的文件发送过程
    else
        return;
}

下面是文件内容部分的发送过程:

void NetClient::SlotSendUpdateProgress(qint64 numBytes)
{
    m_bytes_written += numBytes;
    if(m_bytes_to_write > 0)
    {
        m_in_out_block = m_local_file->read(qMin(m_bytes_to_write, m_pay_load_size));  //m_pay_load_size是最大传输负载
        m_bytes_to_write -= socket->write(m_in_out_block);
        m_in_out_block.resize(0);
    }
    else
        m_local_file->close();     //关闭发送的配置文件
    
    //这里可以设置进度条的进度
    m_progress_bar->setValue(m_bytes_written);

    if(m_bytes_written == m_total_bytes)   //传输完成
    {
        m_local_file->close();
        //这里可以设置传输完成的相关操作
        m_bytes_written = 0;
    }
}

4. 文件的接收

在TCP数据传输文件时,如何封装发送的,接收时就如何解包,数据格式一定要一致,否则就会出错。

void NetClient::SlotReadyRead()
{
    QDataStream recv_in(socket);
    recv_in.setVersion(QDataStream::Qt_4_5);
    //若已接收的字节小于24字节
    if(m_bytes_received <= sizeof(qint64)*3)
    {
        if((socket->bytesAvailable()>=sizeof(qint64)*3 && (m_file_name_size == 0))
        {
            //接收相应的包字段
            recv_in>>command>>m_total_bytes>>m_file_name_size;
            m_bytes_received += sizeof(qint64)*3;
            if(command == _TCP_ACCEPT_FILE_)
            //可以用来设置接收文件的进度
        }
        if(socket->bytesAvailable()>=m_file_name_size) && (m_file_name_size != 0))
        {
            //接收文件名及其它相关命令
            recv_in>>m_file_name;
            m_bytes_received += m_file_name_size;
            if(command == _TCP_ACCEPT_FILE_)   //返回的是文件
            {
                 m_local_file = new QFile("./" + m_file_name);   //文件句柄
                 if(!m_local_file->open(QFile::WriteOnly)
                       //做相应的处理
             }
        }
    }
    //若已接收的数据小于总数据,则写入文件
    if(m_bytes_received < m_total_bytes)
    {
        m_bytes_received += socket->bytesAvailable();
        m_in_out_block = socket->readAll();    //读取缓冲区的数据
        m_local_file->write(m_in_out_block);
        m_in_out_block.resize(0);
        //下面可以设置接收进度条的数据
     }
     //当接收数据完成时
    if(m_bytes_received == m_total_bytes)
    {
        m_local_file->close();
        //设置接收完成的相关信息
        m_bytes_received = 0;
        m_file_name_size = 0;
        m_total_bytes = 0;
        command = 0;
        m_file_name = "";    
    }
}

在TCP通信过程中,传输数据一定要定义好格式,以什么样的格式发送就以什么样的格式接收,否则就会出现数据错误。

5.用大量数据的接收方式

在传输大量数据时,发送要定义特定的格式,接收时也要注意相应的粘包问题

void NetClient::SlotReadyDataCar()
{
    //socket缓冲区没有数据直接返回
    if(data_socket->bytesAvailable()<=0)
        return;
    QByteArray buffer = data_socket->readAll();
    m_buffer_car.append(buffer);        //将读取的数据放入包缓冲区
    qint64 tune_cmd;
    qint64 struct_count;
    qint64 total_bytes;

    int total_length = m_buffer_car.size();
    while(total_length)
    {
        QDataStream recv_data(m_buffer_car);
        recv_data.setVersion(QDataStream::Qt_4_5);
        
        if(total_length<sizeof(qint64)*3)
            break;         //不够包头长度下次解析
        recv_data>>tune_cmd>>struct_count>>total_bytes;
        if(total_length < total_bytes + sizeof(qint64)*3)
            break;       //不够数据长度下次解析
        
        if(tune_cmd ==_TCP_TUNE_CAR_)
        {
            for(int i=0; i<struct_count; ++i)
            {
                 recv_data>>st_car_monitor_info;    //这里是接收的数据结构体信息
                 //这里可以对数据进行处理
             }
         }
         //一次处理结束后,缓存多余的数据
         buffer = m_buffer_car.right(total_length - total_bytes - sizeof(qint64)*3);
         //更新长度
         total_length = buffer.size();
         m_buffer_car = buffer;
    }
}