1. 实验目的
掌握基于TCP协议进行网络编程的方法和技巧。
2.实验内容与实验步骤
TCP协议要求拥有一个客户端和一个服务器,故我们需要在程序中实现客户端和服务端的功能,在本实验中建立两个工程,一个建立为客户端,一个建立为服务端。
1. 建立服务器端。
在QTCreator中建立一个新的GUI APP,选择基类为QWidget。为了使用QT的网络通信模块,在tcpServer.pro中添加一行代码QT += network ,然后保存该文件。
2. 打开UI设计区,更改设计区如下所示:
这里我们添加两个label,一个label是提示信息,提示输入发送的信息。另外一个label是提示连接状态的,可以在程序中判断发送状态继而更改label的状态。为了方便程序编写,更改对应的对象名称。
同时,为了实现文本的输入,我们添加一个文本编辑框,程序先读取文本编辑框的数据再发送出去。
添加一个按键对象实现点击发送的功能。
3. 实现服务器的初始化
在widget的构造函数里面实现对服务器的初始化,首先建立一个指QTcpServer类对象的指针,调用监听方法监听端口号6666的本地端口的状态。如果监听到的端口不是指定端口的话,那么程序将报错。代码如下:
Widget::Widget(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
{ //在widget构造函数中调用自己的私有对象实现服务器的功能
ui->setupUi(this);//设置ui文件
tcpServer = new QTcpServer(this);//建立服务器变量
if(!tcpServer->listen(QHostAddress::LocalHost,6666))//设置端口号是6666
{ //本地主机的6666端口,如果出错就输出错误信息,并关闭
qDebug() << tcpServer->errorString();
close();//如果出错就会关闭程序
}
}
4. 在UI设计界面里,右键点击发送按钮,可以看到添加槽的选项,我们点击进去,编辑器自动帮我们建立相应的响应程序。可以观察到,这个响应函数被声明在widget的私有槽里。
在按键响应函数里面我们希望能够发送数据,所以我们在响应里调用发送数据的函数。而发送数据的函数中,我们需要读取对应的文本框的信息,然后利用套接字将其发送出去,利用套接字就必须使用数据流的方法。我们首先定义一个数组,为了保证在客户端能接收到完整的文件,我们都在数据流的最开始写入完整文件的大小信息,这样客户端就可以根据大小信息来判断是否接受到了完整的文件。而在服务器端,在发送数据时就要首先发送实际文件的大小信息先使用了out<<(quint16) 0;在block的开始添加了一个quint16大小的空间,也就是两字节的空间,它用于后面放置文件的大小信息。然后out<<tr("hello Tcp!!!");输入实际的文件,这里是字符串。当文件输入完成后我们再使用out.device()->seek(0);返回到block的开始,加入实际的文件大小信息,也就是后面的代码,它是实际文件的大小:out<<(quint16) (block.size() - sizeof(quint16));具体代码如下所示:
void Widget::sendMessage()
{
//用于暂存我们要发送的数据
QByteArray block;
//使用数据流写入数据
QDataStream out(&block,QIODevice::WriteOnly);//定义输出流对象,这里相当于将输入的东西都往block里面写
//设置数据流的版本,客户端和服务器端使用的版本要相同
out.setVersion(QDataStream::Qt_4_6);
out<<(quint16) 0;//先想block写入两个字节放置文件大小的关系
//out<<tr("hello Tcp!!!");
out<<ui->m_message->toPlainText();//将更新的数据发送过去
out.device()->seek(0);
out<<(quint16) (block.size() - sizeof(quint16));//当文件输入完成后我们再使用out.device()->seek(0);返回到block的开始,加入实际的文件大小信息,也就是后面的代码,它是实际文件的大小:out<<(quint16) (block.size() - sizeof(quint16));
//我们获取已经建立的连接的子套接字
QTcpSocket *clientConnection = tcpServer->nextPendingConnection();//获取已经建立的连接的Tcp套接字
//connect(clientConnection,SIGNAL(disconnected()),clientConnection,
//SLOT(deleteLater()));
clientConnection->write(block);//利用套接字来发送内容
//clientConnection->disconnectFromHost();
//发送数据成功后,显示提示
ui->statusLabel->setText("send message successful!!!");
}
通过该程序,一旦按下按钮,那么就出发发送程序,在发送程序里会读取文本框的内容,然后就可以发送文本框的内容到客户端了。
5.建立客户端
客户端向服务器发送连接的请求,之后服务器才能向客户端发送数据。新建QT GUI APP,基类基类选择QWidget,类名为Widget。完成后打开项目文件tcpClient.pro并添加一行代码:QT += network ,然后保存该文件。
打开UI设计区,向其中添加几个标签以说明主机输入位置以及端口号输入位置,同时添加两个Line Edit以便实现用户向程序输入主机和端口号信息。同时添加一个按钮实现连接的操作。
5. 实现客户端的初始化
首先在头文件中添加响应的私有变量以及私有槽如下所示:
class Widget : public QWidget
{
Q_OBJECT
public:
explicit Widget(QWidget *parent = 0);
~Widget();
private:
Ui::Widget *ui;
QTcpSocket *tcpSocket;
QString message; //存放从服务器接收到的字符串
quint16 blockSize; //存放文件的大小信息
private slots:
void newConnect(); //连接服务器
void readMessage(); //接收数据
void displayError(QAbstractSocket::SocketError); //显示错误
void on_pushButton_clicked();
};
然后再对应的构造函数中添加代码如下所示:
Widget::Widget(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
{
tcpSocket = new QTcpSocket(this);
connect(tcpSocket,SIGNAL(readyRead()),this,SLOT(readMessage()));
connect(tcpSocket,SIGNAL(error(QAbstractSocket::SocketError)),
this,SLOT(displayError(QAbstractSocket::SocketError)));
ui->setupUi(this);
}
在构造函数中,我们建立了一个QTTCPSocket类型的指针,同时连接了对应的信号和槽:一个是一旦接受到数据就开始执行读取函数,另一个是出现错误的信号的话就调用处理错误的程序。
6. 在UI设计界面里,右键点击发送按钮,可以看到添加槽的选项,我们点击进去,编辑器自动帮我们建立相应的响应程序。在这个响应程序中,我们希望能够实现连接的功能。我们编写连接函数如下所示:
void Widget::newConnect()
{
// blockSize = 0; //初始化其为0
tcpSocket->abort(); //取消已有的连接
//连接到主机,这里从界面获取主机地址和端口号
tcpSocket->connectToHost(ui->hostLineEdit->text(),
ui->portLineEdit->text().toInt());
}
为了保证程序的稳定性,在每次连接之前,我们都取消之前的连接,然后调用连接方法,连接到主机上,我们是通过读取对应的文本编辑框来实现我们主机地址和端口号的选择的。
7. 信息的读取和错误的处理
在为了能够在接受到数据的时候能够完成数据的读取,我们将接收到数据的信息和读取信息的槽连接起来。编写读取信息的槽如下所示:
void Widget::readMessage()
{
QDataStream in(tcpSocket);
in.setVersion(QDataStream::Qt_4_6);
//设置数据流版本,这里要和服务器端相同
if(blockSize==0) //如果是刚开始接收数据
{
//判断接收的数据是否有两字节,也就是文件的大小信息
//如果有则保存到blockSize变量中,没有则返回,继续接收数据
if(tcpSocket->bytesAvailable() < (int)sizeof(quint16)) return;
in >> blockSize;
}
if(tcpSocket->bytesAvailable() < blockSize) return;
//如果没有得到全部的数据,则返回,继续接收数据
in >> message;
//将接收到的数据存放到变量中
ui->messageLabel->setText(message);
//显示接收到的数据
blockSize = 0;
}
在服务器端我们是利用数据流的方法处理数据,在客户端我们利用同样的方法来读取接收到的数据,通过判断对应的数据的大小来判断数据是否传送完成。
在TCP传输中可能会因为各种原因出现错误,所以将对应的出错消息和错误处理程序连接起来,我们建立的错误处理程序比较简单,就是将错误信息打印出来方便程序的修正与更改。编写代码如下所示:
void Widget::displayError(QAbstractSocket::SocketError)
{
qDebug() << tcpSocket->errorString(); //输出错误信息
}
就此,我们的客户端和服务端就都已经建立完成,运行程序,可以看到程序运行效果如下所示:
3.实验环境
QTCreator4.6.1+windows7系统
4. 实验过程与分析
1. 因为之前在用VS2017的时候使用MFC出现了错误较多,所以选用了比较流行的UI设计软件QT,实验之初是在VS2017的环境下通过下载QT插件来进行代码编辑的,后来发现QT原生的编辑工具QTCreator功能也很强大,故选择了QTCreator编程。
2. 在使用QT网络相关的程序的时候需要在pro文件里添加QT=+network代码否则程序会出现错误。
3. 在建立此次工程的时候,出现了找不到头文件的问题,这是因为版本的不兼容导致的。我们用了QT4的习惯写法#include”QTGUI”,但在QT5中应该写作QTWidget.
4. 在使用QT的过程中,我们体会到了现代UI设计工具的优越性,代码编辑方便简单,可拓展性强。