Qt实现服务器与客户端传输文字和图片(Qt②)

初学者记录学习内容,如有错误请各位前辈指点。
此次工程完成过程借鉴了下面得两个帖子,附上链接,并致以感谢:
qt 写的tcp客户端程序实现简单的连接接受和发送消息
qt写的一个简单的tcp服务器程序,可以接受消息发送数据
好了闲话少说进入正题。
了解C语言的盆友们应该知道实现Socket程序传递消息需要以下几点:
在服务器server端:①创建套接字SOCKET;②bind()函数绑定套接字(和IP,端口Port绑定);③Listen()进入监听状态;④accept()进入接收客户端请求;⑤send()向客户端发送数据;⑥close()关闭套接字。
在客户端Client端:①创建套接字SOCKET;②connect()向服务器发起请求;③recv()接收服务器传回的数据;④printf()打印传回的数据;⑤close()关闭套接字。
而在Qt实现Socket的过程中,也与此过程有很多相似之处。

传输文字的服务器server实现

在QtDesigner中绘制界面:
Qt实现服务器与客户端传输文字和图片(Qt②)_第1张图片
QDialog中两个的PushButton分别命名为pbtnSend和stopButton,以便后面加入槽函数。
注意进行socket连接之前要在.pro中加入network

QT       += core gui network

贴入代码如下:
sever.h

#ifndef SERVER_H
#define SERVER_H

#include 
#include 
#include 
#include 
#include 

namespace Ui {
class Server;
}

class Server : public QDialog
{
    Q_OBJECT

public:
    explicit Server(QWidget *parent = 0);
    ~Server();

private slots:
    void on_stopButton_clicked();
    void acceptConnection();
    void sendMessage();
    void displayError(QAbstractSocket::SocketError);
private:
    Ui::Server *ui;
    QTcpServer *tcpServer;
    QTcpSocket *tcpSocketConnection;
};

#endif // SERVER_H

server.cpp

#include "server.h"
#include "ui_server.h"

Server::Server(QWidget *parent) :
    QDialog(parent),
    ui(new Ui::Server)
{
    ui->setupUi(this);
    tcpServer=new QTcpServer(this);
    if (!tcpServer->listen(QHostAddress::Any, 7777)) {
            qDebug() << tcpServer->errorString();
            close();
        }
    tcpSocketConnection = NULL;
    connect(tcpServer,SIGNAL(newConnection()),
    this,SLOT(acceptConnection()));
    connect(ui->pbtnSend,SIGNAL(clicked(bool)),
    this,SLOT(sendMessage()));
}

Server::~Server()
{
    delete ui;
}

void Server::acceptConnection()
{
    tcpSocketConnection = tcpServer->nextPendingConnection();
    connect(tcpSocketConnection,SIGNAL(disconnected()),this,SLOT(deleteLater()));
    connect(tcpSocketConnection,SIGNAL(error(QAbstractSocket::SocketError)),this,SLOT(displayError(QAbstractSocket::SocketError)));
}

void Server::on_stopButton_clicked()
{
    tcpSocketConnection->abort();
    QMessageBox::about(NULL,"Connection","Connection stoped");
}

void Server::sendMessage()
{
    if(tcpSocketConnection==NULL)
        return;
    QByteArray block;
    QDataStream out(&block,QIODevice::WriteOnly);
    out.setVersion(QDataStream::Qt_5_8);
    out<<(quint16)0;
    out<<"Hello TCP!@_@!";
    out.device()->seek(0);
    out << (quint16)(block.size() - sizeof(quint16));
    tcpSocketConnection->write(block);
}

void Server::displayError(QAbstractSocket::SocketError)
{
     qDebug() << tcpSocketConnection->errorString();
}

现在对server.cpp进行解释:
引用查到的一段对QTcpServer和QTcpSocket基本操作的描述,如下涉及到的几个函数都是特别重要的。

QTcpServer的基本操作:
1、调用listen监听端口。
2、连接信号newConnection,在槽函数里调用nextPendingConnection获取连接进来的socket。
QTcpSocket的基本能操作:
1、调用connectToHost连接服务器。
2、调用waitForConnected判断是否连接成功。
3、连接信号readyRead槽函数,异步读取数据。
4、调用waitForReadyRead,阻塞读取数据。

在调用这几个函数之前先在.h文件中做声明指针变量:

private:
    QTcpServer *tcpServer;
    QTcpSocket *tcpSocketConnection;

在.cpp文件中

tcpServer=new QTcpServer(this);
tcpSocketConnection = NULL;

注意之后要在tcpSocketConnection的基础上操作数据,所以初始化要保证当前无连接,即赋值为NULL。
然后在构造函数中设置IP地址和端口号:

if (!tcpServer->listen(QHostAddress::Any, 7777)) {
            qDebug() << tcpServer->errorString();
            close();
        }

QTcpServer调用Listen()监听,使用了IPv4的本地主机地址,等价于QHostAddress(“127.0.0.1”),端口号设为”7777”。listen()函数返回的值是bool型,所以如果监听失败时,会把错误原因打印到控制台,并关闭连接。

用QT实现并不需要像C语言那么麻烦,当我们设置好IP和端口进行监听时,用我的理解就是服务器进入了循环,会不断监听检查发来的连接,当有同样的IP和端口的连接申请发来的时候,QTcpSocket会发射newConnection()信号,在代码的槽函数中会触发acceptConnection()函数。

tcpSocketConnection = tcpServer->nextPendingConnection();

当信号发来的时候,通过调用QTcpServer的nextPendingConnection()函数得到的socket连接句柄tcpSocketConnection ,注意之后对于信号和数据的操作都是在这个句柄基础上进行的。
注意到此函数中仅有对tcpSocketConnection进行操作,但是没有在开头new一块内存去保存tcpSocketConnection,这是因为通过nextPendingConnection()得到句柄的过程就会被分配一块内存去保存,所以不用忘记了在之后释放内存空间,即之后的deleteLater()函数。
之后的两个connect语句,断开后删除连接,运行出错将错误信息打印到控制台。

最后来看服务器端最重要的一个槽函数sendMessage()。
当触发pbtnSend按钮的clicked()信号,将固定消息“Hello TCP!@_@!”发送。首先连接句柄tcpSocketConnection是否为空。
然后用到了QByteArray,这在Qt传输数据时非常重要,它可以存储raw bytes即原始字节,将文字或者图片转化为raw bytes发送,在接收端进行解析。
QDataStream则提供了一个二进制的数据流。看代码:

    QByteArray block;
    QDataStream out(&block,QIODevice::WriteOnly);
    out.setVersion(QDataStream::Qt_5_8);
    out<<(quint16)0;
    out<<"Hello TCP!@_@!";
    out.device()->seek(0);
    out << (quint16)(block.size() - sizeof(quint16));
    tcpSocketConnection->write(block);

将仅可写入WriteOnly的数据流out与block进行绑定。
设置数据流out的版本,注意客户端和服务器端使用的版本要相同,这里我们使用的是Qt5.8。然后通过C++的重载操作符<<实现对流的操作。
这里着重说一点,使用<<的时候并不关心输入的类型,换句话说无论是什么类型写入数据流,在读出的时候都会以写入的顺序和类型读出,而不用考虑占几个字节,需要一个一个字节取出来,高位低位组合起来等等情况。
比如此程序中,先以quint16类型(2个字节)的输入”0”占位,然后写入字符串”Hello TCP!@_@!”。seek(0)将指针移动到0所在的那一位处,block.size()得出block的长度15,sizeof(quint16)得出一个quint16类型的长度为2,相减得13覆盖保存到刚才用0占位的保存quint16类型的内存中,这就是传输的数据的长度,与数据一起传给服务器用于比对数据是否传输完整。
注意quint16是QT软件下的一种自定义类型,代表16位的无符号整型,可以存储2^16个数字,就是0-65535,以此储存发送的文字的长度足以。但是如果需要传输图片的大小就需要使用quint32类型来储存,在后面中我们会用到。
最后是QTcpSocket的write函数,将QByteArray类型的block写入Socket缓存中,之后就是客户端的工作了。

传输文字的客户端Client实现

在QtDesigner中绘制界面:
Qt实现服务器与客户端传输文字和图片(Qt②)_第2张图片
QDialog下两个QLineEdit用于填入端口号和IP地址,PushButton命名为sendButton用于触发向服务器的连接申请。QTextEdit命名为messageShow用于显示从服务器传来的字符串。
贴入代码如下:

client.h

#ifndef CLIENT_H
#define CLIENT_H

#include 
#include 
#include 
#include 
#include 
#include 


namespace Ui {
class Client;
}

class Client : public QDialog
{
    Q_OBJECT

public:
    explicit Client(QWidget *parent = 0);
    ~Client();

private slots:
    void on_sendButton_clicked();
    void showMessage();
    void displayError(QAbstractSocket::SocketError);

private:
    Ui::Client *ui;
    QTcpSocket *tcpSocket;
    quint16 blockSize;
};

#endif // CLIENT_H

client.cpp

#include "client.h"
#include "ui_client.h"

Client::Client(QWidget *parent) :
    QDialog(parent),
    ui(new Ui::Client)
{
    ui->setupUi(this);
    tcpSocket = new QTcpSocket(this);
    connect(ui->sendButton,SIGNAL(clicked()),this,SLOT(on_sendButton_clicked()));
    connect(tcpSocket, SIGNAL(readyRead()), this, SLOT(showMessage()));
    connect(tcpSocket, SIGNAL(error(QAbstractSocket::SocketError)),
                        this, SLOT(displayError(QAbstractSocket::SocketError)));
    blockSize = 0;
}

Client::~Client()
{
    delete ui;
}

void Client::on_sendButton_clicked()
{
    if(tcpSocket->state()!=QAbstractSocket::ConnectedState)
    {
        tcpSocket->connectToHost(ui->ipLineEdit->text(),ui->portLineEdit->text().toInt());

        if(tcpSocket->waitForConnected(10000))
        {
            QMessageBox::about(NULL, "Connection", "Connection success");
        }
        else
        {
            QMessageBox::about(NULL,"Connection","Connection timed out");
        }
    }
    else
        QMessageBox::information(NULL,"","Connected!");
}

void Client::showMessage()
{
    QDataStream in(tcpSocket);
    in.setVersion(QDataStream::Qt_5_8);
    if(blockSize==0)
    {
        if(tcpSocket->bytesAvailable()<(int)sizeof(quint16))
            return;
        in >> blockSize;
    }
   if(tcpSocket->bytesAvailable()return;
    char* buf=new char[512];
    in >> buf;
    QDateTime time = QDateTime::currentDateTime();
    QString str = time.toString("yyyy-MM-dd hh:mm:ss");
    ui->messageShow->setText(buf+str);
    if(buf)
        delete buf;
}


void Client::displayError(QAbstractSocket::SocketError)
{
    qDebug() << tcpSocket->errorString();
}

现在对client.cpp进行解释。
多余的东西不再赘述,点击sendButton运行槽函数on_sendButton_clicked()。这里引用一段对QAbstractSocket的描述:

QAbstractSocket都有一个状态,而我们可以通过调用成员函数state返回这个状态,才开始的状态是UnconnectedState,
当程序调用了connectToHost之后,QAbstractSocket的状态会变成HostLookupState,,
如果主机被找到,QAbstaractSocket进入connectingState状态并且发射HostFound()信号,当连接被建立的时候QAbstractSocket 进入了connectedState状态 并且发射connected()信号,如果再这些阶段出现了错误,QAbstractSocket将会发射error()信号,无论在什么时候,如果状态改变了,都会发射stateChanged(),如果套接字准备好了读写数据,isValid()将会返回true。

如上所述调用state()判断QAbstractSocket是否是connectedState,即已经连接的状态。如果尚未连接继续执行,调用随后调用QTcpSocket的connectToHost(从控件中获取的IP和端口号)连接服务器,调用waitForConnected判断是否连接成功,但如果是已经连接成功弹出提示框。
QMessageBox弹出提示语对话框,常用于帮助你判断socket是否连接成功。
在服务器中通过write()将数据写入socket缓冲区,在已经连接成功的情况下客户端当有数据要读的时候,会触发readyRead()信号,随后执行showMessage()槽函数,showMessage()函数是将传过来的数据类型转化后输出。声明数据流in接收数据,设置QT版本。
注意在.h中定义:

private:
 quint16 blockSize;

quint16 类型的blockSize用于从数据流中接收之前保存为quint16 的字符串的大小,用来进行数据是否传输完整的比对,并在.cpp中赋值为blockSize = 0。

tcpSocket->bytesAvailable()返回已经接收到的可以被读出的字节数,(int)sizeof(quint16)返回一个quint16类型变量的大小,只有当前者大于后者说明数据的长度已经传输完整,则保存到blockSize变量中,否则直接返回,继续接收数据。
随后用blockSize值进行比对,只有当接收到的字节数大于blockSize的值才说明数据全部传输成功,否则返回继续接收数据。

传输数据的大小和比对,这两步是基本步骤,必不可少
定义一个暂时保存数据的char型数组的指针,new一段大小为512的内存,将接下来的char型的字符串输入其中。一定记得在最后删除指针。
关于删除指针着重说一点——

关于C++中的析构函数,分配到栈(局部内存管理)的不需要释放,分配到堆(全局内存管理)需要释放。一般局部对象在局部函数结束的时候会自动释放。
New一个内存空间或者malloc()动态分配内存空间一般都需要自己在析构函数中释放。比如在此程序中,函数运行结束之后指向内存空间的指针buf最后会被自动删除,而这一块储存空间并不会被释放,而且也将再无法对其进行操作,长此以往内存会愈来愈小。因此有两条路可选,一是对该指针进行保存以便以后对其指向的内存空间进行管理。二是在函数结束之前释放内存空间。

最后获取当前的时间和日期显示到textEdit中。结束。

传输图片的server和client实现

关于socket的连接,这里将不再累述,只对发送和显示图片的过程进行简单地讲解。
同上,先再QTdesigner中绘制客户端和服务器的界面,同传输文字大致相同,只需将客户端界面中的textEdit换成QLabel用于显示图片。
服务器中发送图片和客户端中接收图片的槽函数如下:

void pictureSever::on_sendPictureButton_clicked()
{
    if(tcpSocket==NULL)
        return;
    QByteArray block;
    QDataStream out(&block,QIODevice::WriteOnly);
    out.setVersion(QDataStream::Qt_5_8);
    out<<(quint32)buffer.data().size();
    block.append(buffer.data());
    tcpSocket->write(block);
}

在.h中声明

private:
QBuffer buffer;

在.cpp的构造函数中,将图片保存在buffer中:

QPixmap(":/new/prefix1/sendPicture/007.bmp").save(&buffer,"BMP");

图片在项目的资源文件中。

此处将字符串读入socket中的方法与上例中的方法大致相同,先读入数据的大小,后读入数据。但过程不同,两个过程并无太大区别,可自行选择。
不过要注意的就是上面已经谈过的,这里使用quint32而不是quint16来存储图片的大小。

void pictureClient::showPicture()
{
    while(tcpSocketConnection->bytesAvailable()>0)
    {
        if(blockSize==0)
        {
            QDataStream in(tcpSocketConnection);
            in.setVersion(QDataStream::Qt_5_8);
            if(tcpSocketConnection->bytesAvailable()<sizeof(quint32))
                return;
            in>>blockSize;
        }

        if(tcpSocketConnection->bytesAvailable()<blockSize)
            return;

        QByteArray array = tcpSocketConnection->read(blockSize);//blockSize作read()的参数。
        QBuffer buffer(&array);
        buffer.open(QIODevice::ReadOnly);

        QImageReader reader(&buffer,"BMP");
        QImage image = reader.read();
        blockSize=0;//①

        if(!image.isNull())
       {
            image=image.scaled(ui->showPicturelabel->size());
            ui->showPicturelabel->setPixmap(QPixmap::fromImage(image));
            blockSize=0;//②
       }
    }
}

同样的方式,先确定用quint32变量保存的图片大小的的值已经传入,然后比对确定图片完全传入。
注意这里要使用QImageReader将图片的数据流转化回BMP格式,必须使用QBuffer,将数据装入QBuffer类型的变量中进行转化。
QImageReader的帮助文档
Qt实现服务器与客户端传输文字和图片(Qt②)_第3张图片
有概念性的疑问一定去找Qt帮助文档,这才是权威。
最后说一点:blockSize的问题,在传输字符串时,由于我们要传输的数据比较短,我们就当作记录字符串长度的变量一次性传入成功,其实这是存在问题的。数据较多时,并不一定能一次成功的,应该使用传输图片时使用的while循环比对判断。因为要使用blockSize==0作为判断条件,在循环体的结尾处要将blockSize再置为0,①处是传输异常情况下走不到下面的if结构中时的情况,②处是传输正常情况下的置0。
最后将图片读入到image,自适应Label的大小并显示。

有点啰嗦,如有错误还望指正,谢谢。

你可能感兴趣的:(qt学习)