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②)
文章图片

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_OBJECTpublic: 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②)
文章图片

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_OBJECTpublic: 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()> 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()return; in>>blockSize; }if(tcpSocketConnection->bytesAvailable()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类型的变量中进行转化。
Qt实现服务器与客户端传输文字和图片(Qt②)
文章图片

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

有概念性的疑问一定去找Qt帮助文档,这才是权威。
最后说一点:blockSize的问题,在传输字符串时,由于我们要传输的数据比较短,我们就当作记录字符串长度的变量一次性传入成功,其实这是存在问题的。数据较多时,并不一定能一次成功的,应该使用传输图片时使用的while循环比对判断。因为要使用blockSize==0作为判断条件,在循环体的结尾处要将blockSize再置为0,①处是传输异常情况下走不到下面的if结构中时的情况,②处是传输正常情况下的置0。
最后将图片读入到image,自适应Label的大小并显示。
【Qt实现服务器与客户端传输文字和图片(Qt②)】有点啰嗦,如有错误还望指正,谢谢。

    推荐阅读