1 简介
参考视频:https://www.bilibili.com/video/BV1XW411x7NU?p=56
参考视频:https://www.bilibili.com/video/BV1XW411x7NU?p=66
测试1代码github:https://github.com/zhengcixi/Qt_Demo/tree/master/tcp
测试2代码github:https://github.com/zhengcixi/Qt_Demo/tree/master/tcp_file
说明:本文介绍Qt下实现tcp客户端和服务器通信的过程。
Linux下tcp客户端和服务器通信模型可参考我的另一篇博客:https://www.cnblogs.com/mrlayfolk/p/11968446.html。Qt下tcp通信的原理是一样的。
Qt的TCP通信模型如下:
(1)TCP服务器端
1)创建服务器套接字,使用QTcpServer()类;
2)将套接字设置为监听模式;
3)等待客户端连接请求,客户端连接上时会触发newConnection信号,可调用nextPendingConnection函数获取客户端的Socket信息;
4)和客户端进行通信,发送数据可使用write()函数,接收数据可使用read()或readAll函数()。
(2)TCP客户端
1)创建套接字;
2)连接服务器,使用connectToHost()函数;
3)和服务器进行通信,发送数据可使用write()函数,接收数据可使用read()或readAll函数()。
2 代码测试
2.1 基本的tcp通信测试
功能说明:分别创建两个窗口,一个用作TCP服务器端,一个用作TCP客户端,双方进行通信。窗口如下:
服务器窗口: 客户端窗口:
下面分别说明代码实现的步骤:
(1)服务器端
首先创建两个套接字指针。tcpserver用作服务器套接字,tcpsocket用作和客户端通信的通信套接字。
1 QTcpServer *tcpserver = NULL; //监听套接字 2 QTcpSocket *tcpsocket = NULL; //通信套接字
然后,创建套接字并启动监听。
1 //监听套接字,指定父对象,自动回收空间 2 tcpserver = new QTcpServer(this); 3 //启动监听 4 tcpserver->listen(QHostAddress::Any, 8888);
捕捉newConnect信号与槽函数,等待客户端连接:
1 //等待连接 2 connect(tcpserver, &QTcpServer::newConnection, 3 [=]() { 4 //取出建立好连接的套接字 5 tcpsocket = tcpserver->nextPendingConnection(); 6 //获取对方的ip的端口 7 QString ip = tcpsocket->peerAddress().toString(); 8 qint16 port = tcpsocket->peerPort(); 9 QString tmp = QString("[%1:%2]:成功连接").arg(ip).arg(port); 10 //在当前对话框显示谁和我连接了 11 ui->textEdit_recv->setText(tmp); 12 } 13 );
发送数据:
1 //获取编辑区内容 2 QString str = ui->textEdit_send->toPlainText(); 3 //给对方发送数据 4 //QString -> char* 5 tcpsocket->write(str.toUtf8().data());
接收数据:
1 //接收数据 2 connect(tcpsocket, &QTcpSocket::readyRead, 3 [=](){ 4 //从通信套接字中取出内容 5 QByteArray array = tcpsocket->readAll(); 6 ui->textEdit_recv->append(array); 7 } 8 );
关闭连接:
1 //主动和客户端断开连接 2 tcpsocket->disconnectFromHost(); 3 tcpsocket->close(); 4 tcpsocket = NULL;
(2)客户端
客户端只需要创建一个套接字,用于和服务器建立连接并通信:
1 QTcpSocket *tcpsocket = NULL; //通信套接字 2 //分配空间,指定父对象 3 tcpsocket = new QTcpSocket(this);
和服务器端建立连接:
1 //获取服务器ip和端口 2 QString ip = ui->lineEdit_ip->text(); 3 qint16 port = ui->lineEdit_port->text().toInt(); 4 //主动和服务器建立连接 5 tcpsocket->connectToHost(QHostAddress(ip), port);
发送数据:
1 //获取编辑框内容 2 QString str = ui->textEdit_send->toPlainText(); 3 //发送数据 4 tcpsocket->write(str.toUtf8().data());
接收数据:
1 connect(tcpsocket, &QTcpSocket::readyRead, 2 [=](){ 3 //获取对方发送的内容 4 QByteArray array = tcpsocket->readAll(); 5 //追加到编辑区中 6 ui->textEdit_recv->append(array); 7 } 8 );
断开连接:
1 //主动和对方断开连接 2 tcpsocket->disconnectFromHost(); 3 tcpsocket->close();
(3)完整的工程代码:
工程所包含的文件有,serverwidget.cpp和serverwidget.h是服务器端的代码,clientwidget.cpp和clientwidget.h是客户端代码。
serverwidget.cpp代码:
1 #include "serverwidget.h" 2 #include "ui_serverwidget.h" 3 4 ServerWidget::ServerWidget(QWidget *parent) : 5 QWidget(parent), 6 ui(new Ui::ServerWidget) 7 { 8 ui->setupUi(this); 9 setWindowTitle("服务器: 8888"); 10 11 //监听套接字,指定父对象,自动回收空间 12 tcpserver = new QTcpServer(this); 13 //启动监听 14 tcpserver->listen(QHostAddress::Any, 8888); 15 //等待连接 16 connect(tcpserver, &QTcpServer::newConnection, 17 [=]() { 18 //取出建立好连接的套接字 19 tcpsocket = tcpserver->nextPendingConnection(); 20 //获取对方的ip的端口 21 QString ip = tcpsocket->peerAddress().toString(); 22 qint16 port = tcpsocket->peerPort(); 23 QString tmp = QString("[%1:%2]:成功连接").arg(ip).arg(port); 24 //在当前对话框显示谁和我连接了 25 ui->textEdit_recv->setText(tmp); 26 27 //接收数据 28 connect(tcpsocket, &QTcpSocket::readyRead, 29 [=](){ 30 //从通信套接字中取出内容 31 QByteArray array = tcpsocket->readAll(); 32 ui->textEdit_recv->append(array); 33 } 34 ); 35 } 36 ); 37 } 38 39 ServerWidget::~ServerWidget() 40 { 41 delete ui; 42 } 43 44 45 void ServerWidget::on_pushButton_close_clicked() 46 { 47 if (NULL == tcpsocket) { 48 return; 49 } 50 //主动和客户端断开连接 51 tcpsocket->disconnectFromHost(); 52 tcpsocket->close(); 53 tcpsocket = NULL; 54 } 55 56 void ServerWidget::on_pushButton_send_clicked() 57 { 58 if (NULL == tcpsocket) { 59 return; 60 } 61 //获取编辑区内容 62 QString str = ui->textEdit_send->toPlainText(); 63 //给对方发送数据 64 //QString -> char* 65 tcpsocket->write(str.toUtf8().data()); 66 }
serverwidget.h代码:
1 #ifndef SERVERWIDGET_H 2 #define SERVERWIDGET_H 3 4 #include <QWidget> 5 #include <QTcpServer> //监听套接字 6 #include <QTcpSocket> //通信套接字 7 8 namespace Ui { 9 class ServerWidget; 10 } 11 12 class ServerWidget : public QWidget 13 { 14 Q_OBJECT 15 16 public: 17 explicit ServerWidget(QWidget *parent = 0); 18 ~ServerWidget(); 19 20 private slots: 21 void on_pushButton_send_clicked(); 22 23 void on_pushButton_close_clicked(); 24 25 private: 26 Ui::ServerWidget *ui; 27 QTcpServer *tcpserver = NULL; //监听套接字 28 QTcpSocket *tcpsocket = NULL; //通信套接字 29 }; 30 31 #endif // SERVERWIDGET_H
clientwidget.cpp代码:
1 #include "clientwidget.h" 2 #include "ui_clientwidget.h" 3 #include <QHostAddress> 4 5 clientwidget::clientwidget(QWidget *parent) : 6 QWidget(parent), 7 ui(new Ui::clientwidget) 8 { 9 ui->setupUi(this); 10 setWindowTitle("客户端"); 11 //分配空间,指定父对象 12 tcpsocket = new QTcpSocket(this); 13 //建立连接 14 connect(tcpsocket, &QTcpSocket::connected, 15 [=]() { 16 ui->textEdit_recv->setText("成功和服务器建立了连接"); 17 } 18 ); 19 //接收数据 20 connect(tcpsocket, &QTcpSocket::readyRead, 21 [=](){ 22 //获取对方发送的内容 23 QByteArray array = tcpsocket->readAll(); 24 //追加到编辑区中 25 ui->textEdit_recv->append(array); 26 } 27 ); 28 //断开连接 29 connect(tcpsocket, &QTcpSocket::disconnected, 30 [=](){ 31 ui->textEdit_recv->append("和服务器断开了连接"); 32 } 33 ); 34 } 35 36 clientwidget::~clientwidget() 37 { 38 delete ui; 39 } 40 41 void clientwidget::on_pushButton_send_clicked() 42 { 43 //获取编辑框内容 44 QString str = ui->textEdit_send->toPlainText(); 45 //发送数据 46 tcpsocket->write(str.toUtf8().data()); 47 } 48 49 void clientwidget::on_pushButton_close_clicked() 50 { 51 //主动和对方断开连接 52 tcpsocket->disconnectFromHost(); 53 tcpsocket->close(); 54 } 55 56 void clientwidget::on_pushButton_connect_clicked() 57 { 58 //获取服务器ip和端口 59 QString ip = ui->lineEdit_ip->text(); 60 qint16 port = ui->lineEdit_port->text().toInt(); 61 //主动和服务器建立连接 62 tcpsocket->connectToHost(QHostAddress(ip), port); 63 }
clientwidget.h代码:
1 #ifndef CLIENTWIDGET_H 2 #define CLIENTWIDGET_H 3 4 #include <QWidget> 5 #include <QTcpSocket> 6 7 namespace Ui { 8 class clientwidget; 9 } 10 11 class clientwidget : public QWidget 12 { 13 Q_OBJECT 14 15 public: 16 explicit clientwidget(QWidget *parent = 0); 17 ~clientwidget(); 18 19 private slots: 20 void on_pushButton_send_clicked(); 21 22 void on_pushButton_close_clicked(); 23 24 void on_pushButton_connect_clicked(); 25 26 private: 27 Ui::clientwidget *ui; 28 QTcpSocket *tcpsocket = NULL; 29 }; 30 31 #endif // CLIENTWIDGET_H
main.cpp代码,启动两个窗口:
1 #include "serverwidget.h" 2 #include <QApplication> 3 #include "clientwidget.h" 4 5 int main(int argc, char *argv[]) 6 { 7 QApplication a(argc, argv); 8 ServerWidget w; 9 clientwidget w2; 10 w.show(); 11 w2.show(); 12 13 return a.exec(); 14 }
运行进行测试:
2.2 使用tcp传输文件
功能:客户端连接到服务器,服务器端再传输文件给客户端,当文件传输完成时,服务器端等待客户端返回成功接收的信息,然后服务器端关闭客户端的连接。
通信模型如下:
有几点需要说明一下:
(1)服务器端传输文件之前,先传输了一个文件头信息,这个信息是我们自己封装的,格式为:“文件名##文件字节数”,这样客户端就可以根据文件头信息获取文件的信息了,创建相应的文件。
(2)发送文件数据时,如果文件太大,可以一个一个缓冲区的发送,缓冲区的大小是自己设置的,代码中设置的4kb;
代码说明如下:
(1)工程文件有:
clientwidget.cpp和clientwidget.h是客户端的代码,serverwidget.cpp和serverwidget.h是服务器端的代码。
(2)直接给出代码,代码中有注释:
clientwidget.h代码:
1 #ifndef CLIENTWIDGET_H 2 #define CLIENTWIDGET_H 3 4 #include <QWidget> 5 #include <QTcpSocket> 6 #include <QFile> 7 8 namespace Ui { 9 class clientWidget; 10 } 11 12 class clientWidget : public QWidget 13 { 14 Q_OBJECT 15 16 public: 17 explicit clientWidget(QWidget *parent = 0); 18 ~clientWidget(); 19 20 private slots: 21 void on_pushButton_connect_clicked(); 22 23 private: 24 Ui::clientWidget *ui; 25 26 QTcpSocket *tcpsocket = NULL; //通信套接字 27 QFile file; //文件对象 28 QString filename; //文件名 29 qint64 filesize = 0; //文件大小 30 qint64 recvsize = 0; //已发送文件大小 31 bool isStart; //开始接收文件标志位 32 }; 33 34 #endif // CLIENTWIDGET_H
clientwidget.cpp代码
1 #include "clientwidget.h" 2 #include "ui_clientwidget.h" 3 #include <QDebug> 4 #include <QMessageBox> 5 #include <QHostAddress> 6 #include <QIODevice> 7 8 clientWidget::clientWidget(QWidget *parent) : 9 QWidget(parent), 10 ui(new Ui::clientWidget) 11 { 12 ui->setupUi(this); 13 setWindowTitle("客户端"); 14 15 isStart = true; 16 ui->progressBar->setValue(0); //当前值 17 18 tcpsocket = new QTcpSocket(this); 19 connect(tcpsocket, &QTcpSocket::readyRead, 20 [=](){ 21 //取出接收的内容 22 qDebug() << "isstart:" << isStart; 23 QByteArray buf = tcpsocket->readAll(); 24 if (isStart == true) { //接收到头部信息 25 isStart = false; 26 //解析头部信息 初始化 27 filename = QString(buf).section("##", 0, 0); //文件名 28 filesize = QString(buf).section("##", 1, 1).toInt(); //文件大小 29 recvsize = 0; //已经接收文件大小 30 qDebug() << "fileName" << filename << "filesize" << filesize; 31 //打开文件 32 file.setFileName(filename); 33 bool isOK = file.open(QIODevice::WriteOnly); 34 if (false == isOK) { 35 qDebug() << "open error!!!"; 36 //关闭连接 37 tcpsocket->disconnectFromHost(); 38 tcpsocket->close(); 39 return; 40 } 41 //弹出对话框,显示接收文件信息 42 QString str = QString("接收的文件:[%1:%2kb]").arg(filename).arg(filesize/1024); 43 QMessageBox::information(this, "文件信息", str); 44 //设置进度条 45 ui->progressBar->setMinimum(0); 46 ui->progressBar->setMaximum(filesize/1024); 47 ui->progressBar->setValue(0); 48 QString str1 = QString("开始接收文件%1,大小为%2bytes").arg(filename).arg(filesize); 49 ui->textEdit->append(str1); 50 } else { //真正的文件信息 51 qDebug() << "start write data"; 52 qint64 len = file.write(buf); 53 if (len > 0) { 54 recvsize += len; 55 qDebug() << len; 56 } 57 qDebug() << "recvsize" << recvsize << "filesize" << filesize; 58 //更新进度条 59 ui->progressBar->setValue(recvsize/1024); 60 if (recvsize == filesize) { //文件接收完成 61 qDebug() << "recvsize" << recvsize << "filesize" << filesize; 62 //先向服务器发送接收文件完成的信息 63 tcpsocket->write("file done"); 64 QMessageBox::information(this, "完成", "文件接收完成"); 65 file.close(); 66 tcpsocket->disconnectFromHost(); 67 tcpsocket->close(); 68 } 69 } 70 } 71 ); 72 connect(tcpsocket, &QTcpSocket::connected, 73 [=](){ 74 ui->textEdit->clear(); 75 ui->textEdit->append("已经和服务器建立了连接,等待服务器传输文件..."); 76 ui->pushButton_connect->setEnabled(false); 77 } 78 ); 79 connect(tcpsocket, &QTcpSocket::disconnected, 80 [=](){ 81 ui->textEdit->append("已经和服务器断开了连接"); 82 ui->pushButton_connect->setEnabled(true); 83 } 84 ); 85 } 86 87 clientWidget::~clientWidget() 88 { 89 delete ui; 90 } 91 92 void clientWidget::on_pushButton_connect_clicked() 93 { 94 //获取服务器的ip和端口 95 QString ip = ui->lineEdit_ip->text(); 96 quint16 port = ui->lineEdit_port->text().toInt(); 97 //主动和服务器建立连接 98 tcpsocket->connectToHost(QHostAddress(ip), port); 99 isStart = true; 100 //设置进度条 101 ui->progressBar->setValue(0); 102 }
serverwidget.h代码:
1 #ifndef SERVERWIDGET_H 2 #define SERVERWIDGET_H 3 4 #include <QWidget> 5 #include <QTcpServer> 6 #include <QTcpSocket> 7 #include <QFile> 8 #include <QTimer> 9 10 namespace Ui { 11 class ServerWidget; 12 } 13 14 class ServerWidget : public QWidget 15 { 16 Q_OBJECT 17 18 public: 19 explicit ServerWidget(QWidget *parent = 0); 20 ~ServerWidget(); 21 void sendData(); //发送文件数据 22 23 24 private slots: 25 void on_pushButton_send_clicked(); 26 27 void on_pushButton_choose_clicked(); 28 29 private: 30 Ui::ServerWidget *ui; 31 32 QTcpServer *tcpserver = NULL; //监听套接字 33 QTcpSocket *tcpsocket = NULL; //通信套接字 34 QFile file; //文件对象 35 QString filename; //文件名 36 qint64 filesize; //文件大小 37 qint64 sendsize; //已发送文件大小 38 QTimer timer; //定时器 39 40 }; 41 42 #endif // SERVERWIDGET_H
serverwidget.cpp代码:
1 #include "serverwidget.h" 2 #include "ui_serverwidget.h" 3 #include <QFileDialog> 4 #include <QDebug> 5 #include <QFileInfo> 6 #include <QTimer> 7 8 ServerWidget::ServerWidget(QWidget *parent) : 9 QWidget(parent), 10 ui(new Ui::ServerWidget) 11 { 12 ui->setupUi(this); 13 setWindowTitle("服务器"); 14 15 //监听套接字 16 tcpserver = new QTcpServer(this); 17 //启动监听 18 tcpserver->listen(QHostAddress::Any, 8888); 19 20 //两个按钮都不能使用 21 ui->pushButton_choose->setEnabled(false); 22 ui->pushButton_send->setEnabled(false); 23 24 //如果客户端成功和服务器连接 25 connect(tcpserver, &QTcpServer::newConnection, 26 [=](){ 27 //取出建立好连接的套接字 28 tcpsocket = tcpserver->nextPendingConnection(); 29 //获取对方的IP和端口 30 QString ip = tcpsocket->peerAddress().toString(); 31 quint16 port = tcpsocket->peerPort(); 32 QString str = QString("[%1:%2] 成功连接").arg(ip).arg(port); 33 ui->textEdit->append(str); //显示到编辑区 34 //成功连接后,才能选择文件 35 ui->pushButton_choose->setEnabled(true); 36 ui->pushButton_send->setEnabled(false); 37 connect(tcpsocket, &QTcpSocket::readyRead, 38 [=]() { 39 //取客户端的消息 40 QByteArray buf = tcpsocket->readAll(); 41 if (QString(buf) == "file done") { //文件接收完毕 42 ui->textEdit->append("文件接收完成"); 43 file.close(); 44 //断开客户端端口 45 tcpsocket->disconnectFromHost(); 46 tcpsocket->close(); 47 ui->pushButton_choose->setEnabled(false); 48 ui->pushButton_send->setEnabled(false); 49 } 50 } 51 ); 52 } 53 ); 54 //定时器事件 55 connect(&timer, &QTimer::timeout, 56 [=](){ 57 //关闭定时器 58 timer.stop(); 59 //发送数据 60 sendData(); 61 } 62 ); 63 64 } 65 66 ServerWidget::~ServerWidget() 67 { 68 delete ui; 69 } 70 71 //发送文件按钮 72 void ServerWidget::on_pushButton_send_clicked() 73 { 74 //发送文件头信息 75 QString head = QString("%1##%2").arg(filename).arg(filesize); 76 qint64 len = tcpsocket->write(head.toUtf8()); 77 if (len > 0) { //头部信息发送成功 78 //防止TCP粘包问题,需要通过定时器样式 79 timer.start(2000); 80 } else { 81 qDebug() << "send header data error!!!"; 82 file.close(); 83 ui->pushButton_choose->setEnabled(true); 84 ui->pushButton_send->setEnabled(false); 85 } 86 } 87 88 //选择文件按钮 89 void ServerWidget::on_pushButton_choose_clicked() 90 { 91 //打开文件 92 QString filepath = QFileDialog::getOpenFileName(this, "open", "../"); 93 if (false == filepath.isEmpty()) { //选择文件路径有效 94 filename.clear(); 95 filesize = 0; 96 sendsize = 0; 97 //获取文件信息 文件名 文件大小 98 QFileInfo info(filepath); 99 filename = info.fileName(); 100 filesize = info.size(); 101 //只读方式打开文件 102 file.setFileName(filepath); 103 bool isOK = file.open(QIODevice::ReadOnly); 104 if (false == isOK) { 105 qDebug() << "open file error!!!"; 106 } 107 QString str = QString("已选择文件:%1").arg(filepath); 108 ui->textEdit->append(str); 109 //设置按钮属性 110 ui->pushButton_choose->setEnabled(false); 111 ui->pushButton_send->setEnabled(true); 112 } else { 113 qDebug() << "选择的文件路径无效!!!"; 114 } 115 } 116 117 //发送文件数据 118 void ServerWidget::sendData() 119 { 120 ui->textEdit->append("正在发送文件..."); 121 qint64 len = 0; 122 do { 123 //每次发送数据的大小 124 char buf[4*1024] = {0}; 125 //往文件中读数据 126 len = file.read(buf, sizeof(buf)); 127 if (len <= 0) { 128 break; 129 } 130 //发送数据,读多少,发多少 131 len = tcpsocket->write(buf, len); 132 //发送数据大小累加 133 sendsize += len; 134 qDebug() << "have send:" << len; 135 } while (len > 0); 136 }
main.cpp代码:
1 #include "serverwidget.h" 2 #include <QApplication> 3 #include "clientwidget.h" 4 5 int main(int argc, char *argv[]) 6 { 7 QApplication a(argc, argv); 8 ServerWidget w; 9 clientWidget w2; 10 w.show(); 11 w2.show(); 12 13 return a.exec(); 14 }
运行测试:
遇到的问题:
(1)服务器端发送头部信息之后,会定时一段时间,然后再发送文件,主要是为了解决粘包现象(如果不这么做,服务器端发送头部信息和文件时会封装在同一个包中,这样客户端就不能将其区分出来)。视频中的定时时间是20ms,但是如果我们发送的文件太小,测试发现客户端只会接收到一次readyRead信号,无法接收到文件,所以最好把定时器的时间设置大一些。
存在的bug:
(1)服务器端发送数据时,发送了头部之后,等待定时器时间到达之后,就会发送文件,是不会管客户端是否准备好了接收,那么当客户端还未准备好接收,但是服务器端已经把文件发送完毕了,这种情况下客户端是不能正确收到文件的。演示效果如下:
也就是说,我们必须在接收到头部信息之后,服务器发送文件之前客户端把文件创建好。