初学者记录学习内容,如有错误请各位前辈指点。
此次工程完成过程借鉴了下面得两个帖子,附上链接,并致以感谢:
qt 写的tcp客户端程序实现简单的连接接受和发送消息
qt写的一个简单的tcp服务器程序,可以接受消息发送数据
好了闲话少说进入正题。
了解C语言的盆友们应该知道实现Socket程序传递消息需要以下几点:
在服务器server端:①创建套接字SOCKET;②bind()函数绑定套接字(和IP,端口Port绑定);③Listen()进入监听状态;④accept()进入接收客户端请求;⑤send()向客户端发送数据;⑥close()关闭套接字。
在客户端Client端:①创建套接字SOCKET;②connect()向服务器发起请求;③recv()接收服务器传回的数据;④printf()打印传回的数据;⑤close()关闭套接字。
而在Qt实现Socket的过程中,也与此过程有很多相似之处。
在QtDesigner中绘制界面:
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缓存中,之后就是客户端的工作了。
在QtDesigner中绘制界面:
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中。结束。
关于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类型的变量中进行转化。
有概念性的疑问一定去找Qt帮助文档,这才是权威。
最后说一点:blockSize的问题,在传输字符串时,由于我们要传输的数据比较短,我们就当作记录字符串长度的变量一次性传入成功,其实这是存在问题的。数据较多时,并不一定能一次成功的,应该使用传输图片时使用的while循环比对判断。因为要使用blockSize==0作为判断条件,在循环体的结尾处要将blockSize再置为0,①处是传输异常情况下走不到下面的if结构中时的情况,②处是传输正常情况下的置0。
最后将图片读入到image,自适应Label的大小并显示。
有点啰嗦,如有错误还望指正,谢谢。