技术: Qt 网络编程案例汇总

作品页面的配套讲解页,主要讲解 Qt 网络编程核心代码。

除了第二个阻塞的案例外,基本上新手一看就能懂。
具备网络基础的话,相当于仅仅是熟悉 Qt 网络编程的API。

访问网络(简单)

这里要完成一个简单的事情,访问网络,然后拿到服务器返回的 HTML 代码。

当然获取网页代码(或者显示HTML代码)并不是真正诉求,重点在于 Qt网络编程中,如何利用QNetworkAccessManager发出QNetworkRequest请求,然后处理收到的QNetworkReply响应。以及如何封装网络访问代码,至少是和业务接口解耦。(让业务接口可以直接调用这边儿的接口,而且做到二进制兼容,即连接库,或者远端API修改时,不用修改源码,也不用重新编译)

封装一下网络访问的工具类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
/*networker.h*/
#ifndef NETWORKER_H
#define NETWORKER_H

#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>


class QNetworkReply;
//class NetWorker::Private;


class NetWorker : public QObject
{
Q_OBJECT
public:
/*接口主要给内部类Private使用, 外部也可以用*/
static NetWorker *getInstance();
~NetWorker();
void get(const QString &url);

signals:
void finished(QNetworkReply *reply);//网络请求完成的信号

public slots:


private:
class Private;
friend class Private;
Private *d;

//私有化三大元
explicit NetWorker(QObject *parent = nullptr);
NetWorker(const NetWorker &) = delete;
NetWorker& operator=(NetWorker worker) =delete;//Q_DECL_EQ_DELETE;

};

class NetWorker::Private
{
public:
/*给 Private 构造器传入 NetWorker 主要是因为
* QNetworkAccessManager需要一个 QObject 父类,用来实现自动回收
*/
Private(NetWorker *q) :
manager(new QNetworkAccessManager(q))
{}

/*不必写析构器,因为已经给内部维护的manager指针指定了父类QObject对象了*/

QNetworkAccessManager *manager;
};

#endif // NETWORKER_H

具体实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/*networker.cpp*/
#include "networker.h"

NetWorker::NetWorker(QObject *parent) : QObject(parent),
d(new NetWorker::Private(this))
{
/*直接把 QNetworkAccessManager 的信号转发到 本类*/
connect(d->manager, &QNetworkAccessManager::finished,
this, &NetWorker::finished);
}



/*Private 类实例是本类构造器new的,所以本类负责销毁*/
NetWorker::~NetWorker()
{
delete d;
d = 0;
}

/*返回单例*/
NetWorker *NetWorker::getInstance()
{
static NetWorker netWorker; //这里不用指定 parent 参数
return &netWorker;
}

/*请求方法也是简单封装即可*/
void NetWorker::get(const QString &url)
{
d->manager->get(QNetworkRequest(QUrl(url)));
}

调用代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
networker = NetWorker::getInstance();

/*绑定networker finshed信号*/
connect(networker, &NetWorker::finished, [=](QNetworkReply *reply){
//从解析到的 reply中拿到数据,封装到pojo中&domain class中(略)
qDebug() << reply->readAll().data(); //这里读了一大堆乱七八糟的 HTML 代码

/*删除 reply*/
reply->deleteLater();
});

/*发送网络请求*/
networker->get(QString("http://www.baidu.com/"));
}

运行结果符合预期:

总结一下,如果不带封装,核心代码不过如下:

1
2
3
4
5
6
7
8
QNetworkReply *replay;
QNetworkAccessManager *mgr = new QNetworkAccessManager();

mgr->setNetworkAccessible(QNetworkAccessManager::Accessible);
replay = mgr->get(QNetworkRequest(QUrl(url)));

//显示响应信息
replay->readAll();

或者需要验证的时候,大致是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
QNetworkAccessManager *manager = new QNetworkAccessManager(this);

connect(manager, SIGNAL(proxyAuthenticationRequired(const QNetworkProxy &, QAuthenticator *)),
SLOT(slotProxyAuthenticationRequired(const QNetworkProxy &, QAuthenticator *)));
connect(manager, SIGNAL(authenticationRequired(QNetworkReply *, QAuthenticator *)),
SLOT(slotAuthenticationRequired(QNetworkReply *, QAuthenticator *)));

QNetworkRequest request;

request.setUrl(m_url);
request.setRawHeader("User-Agent", "Qt NetworkAccess 1.3");

m_pReply = manager->get(request);

在发送请求之后 get(),我们进入事件循环并等待finished()来自网络访问管理器的信号。当到达时,请求已经完成 - 成功或失败。接下来,我们应该打电话 reply->error()来查看是否有问题并报告,或者如果一切顺利,请拨打电话reply->readAll()或相似的地方获取我们的数据。

注意在 .pro 文件中添加 DEFINES += QT_NO_SSL.

同步阻塞下载文件(范例)

“How can I use QNetworkManager for synchronous downloads?”. Several times, the first hint is “Use it in asynchronous mode.” This tends to make me angry, not only because the reply is useless. It also sounds to me like “Don’t you know how to use it asynchronously, stupid?”.

异步回调下载可能引发的问题:

  • 该文件将在下载之前完全下载到内存中finished()。对于大文件,这可能需要一段时间,甚至填满所有内存(导致您的应用程序崩溃)。
  • 使用一个不同的信号,如响应对象提前触发readyRead(),QNetworkRequest将会留下一个大文件的第一部分,但是你无法得到其余的部分。
  • 下载时无法向用户显示进度。
  • 流不能被这样处理,就像finish()永远不会被调用 - 应用程序将锁定,最终耗尽内存和崩溃。
  • 在非 GUI 线程中使用此代码 QEventLoop 时,可能导致死锁。

一般的解决方案是:

  • 使用一个QThread来处理传入的数据。
  • 根据运行在GUI还是非GUI模式,使用不同的方法进行线程同步。
  • 对于GUI线程,使用QEventLoop确保我们正在维护一个活泼的用户界面。

但是如果工作线程处理数据不够快,内存还是容易填满;并且线程还有一个缺陷,QTimers must be created from QThread, 也就是说定时器是跟实际工作的线程有关。


下面是实际解决方案(提供一种解决思路):

首先,我们需要创建并开始QThread处理传入的数据,同时保持用户界面的快乐和活跃:

1
2
3
4
5
6
7
8
9
webfile::webfile(QObject *parent /*= 0*/) :
QObject(parent),
m_pNetworkProxy(NULL)
{
// QThread is required, otherwise QEventLoop will block
m_pSocketThread = new QThread(this);
moveToThread(m_pSocketThread);
m_pSocketThread->start(QThread::HighestPriority);
}

在打开和读取操作中,线程将被使用。请注意,我们必须检查我们是否以 GUI 线程运行,并采取不同的行动。调用 exec() 函数 QEventLoop 可能导致在非GUI线程中发生死锁 - QNetworkRequest 由此发送的信号将不被接受,所以 quit() 插槽将永远不会被激活。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/*注意这个方法带有 event loop, 所以在不同线程调用时,采用不同的方法*/
void webfile::slotOpen(void *pReturnSuccess, void *pLoop, qint64 offset /*= 0*/)
{
*(bool*)pReturnSuccess = workerOpen(offset);

if (pLoop != NULL)
{
QMetaObject::invokeMethod((QEventLoop*)pLoop, "quit", Qt::QueuedConnection);
}
}

bool webfile::open(qint64 offset /*= 0*/)
{
bool bSuccess = false;

if (isGuiThread())
{
// For GUI threads, we use the non-blocking call and use QEventLoop to wait and yet keep the GUI alive
QMetaObject::invokeMethod(this, "slotOpen", Qt::QueuedConnection,
Q_ARG(void *, &bSuccess),
Q_ARG(void *, &m_loop),
Q_ARG(qint64, offset));
m_loop.exec();
}
else
{
// For non-GUI threads, QEventLoop would cause a deadlock, so we simply use a blocking call.
// (Does not hurt as no messages need to be processed either during the open operation).
QMetaObject::invokeMethod(this, "slotOpen", Qt::BlockingQueuedConnection,
Q_ARG(void *, &bSuccess),
Q_ARG(void *, NULL),
Q_ARG(qint64, offset));
}

return bSuccess;
// Please note that it's perfectly safe to wait on the return Q_ARG,
// as we wait for the invokeMethod call to complete.
}

参数通过指针方便地传递Q_ARG()。在这种情况下,这是完全合法的,因为我们等待时隙完成任务,所以在线程仍处于活动状态时,它们不会被无意中断开。这样我们也可以得到结果(成功或数据在read()通话的情况下)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
bool webfile::workerOpen(qint64 offset /*= 0*/)
{
qDebug() << "webfile::open(): start offset =" << offset;

clear();
resetReadFails();
close();

QNetworkAccessManager *manager = new QNetworkAccessManager(this);

if (m_pNetworkProxy != NULL)
manager->setProxy(*m_pNetworkProxy);

connect(manager, SIGNAL(proxyAuthenticationRequired(const QNetworkProxy &, QAuthenticator *)),
SLOT(slotProxyAuthenticationRequired(const QNetworkProxy &, QAuthenticator *)));
connect(manager, SIGNAL(authenticationRequired(QNetworkReply *, QAuthenticator *)),
SLOT(slotAuthenticationRequired(QNetworkReply *, QAuthenticator *)));

QNetworkRequest request;

request.setUrl(m_url);
request.setRawHeader("User-Agent", "Qt NetworkAccess 1.3");

m_nPos = offset;
if (m_nPos)
{
QByteArray data;
QString strData("bytes=" + QString::number(m_nPos) + "-");

data = strData.toLatin1();
request.setRawHeader("Range", data);
}

m_pReply = manager->get(request);

if (m_pReply == NULL)
{
qDebug() << "webfile::open(): network error";
m_NetworkError = QNetworkReply::UnknownNetworkError;
return false;
}

// Set the read buffer size
m_pReply->setReadBufferSize(m_nBufferSize);

connect(m_pReply, SIGNAL(error(QNetworkReply::NetworkError)),
SLOT(slotError(QNetworkReply::NetworkError)));
connect(m_pReply, SIGNAL(sslErrors(QList<QSslError>)),
SLOT(slotSslErrors(QList<QSslError>)));

if (!waitForConnect(m_nOpenTimeOutms, manager))
{
qDebug() << "webfile::open(): timeout";
m_NetworkError = QNetworkReply::TimeoutError;
return false;
}

if (m_pReply == NULL)
{
qDebug() << "webfile::open(): cancelled";
m_NetworkError = QNetworkReply::OperationCanceledError;
return false;
}

if (m_pReply->error() != QNetworkReply::NoError)
{
qDebug() << "webfile::open(): error" << m_pReply->errorString();
m_NetworkError = m_pReply->error();
return false;
}

m_NetworkError = m_pReply->error();

m_strContentType = m_pReply->header(QNetworkRequest::ContentTypeHeader).toString();
m_LastModified = m_pReply->header(QNetworkRequest::LastModifiedHeader).toDateTime();
m_nSize = m_pReply->header(QNetworkRequest::ContentLengthHeader).toULongLong();

m_nResponse = m_pReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
m_strResponse = m_pReply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString();
m_bHaveSize = (m_nSize ? true : false);

m_nSize += m_nPos;

if (error() != QNetworkReply::NoError)
{
qDebug() << "webfile::open(): error" << error();
return false;
}

m_NetworkError = response2error(m_nResponse);

qDebug() << "webfile::open(): end response" << response()
<< "error" << error() << "size" << m_nSize;

return (response() == 200 || response() == 206);
}

不要忘记等待功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
bool webfile::waitForConnect(int nTimeOutms, QNetworkAccessManager *manager)
{
QTimer *timer = NULL;
QEventLoop eventLoop;
bool bReadTimeOut = false;

m_bReadTimeOut = false;

if (nTimeOutms > 0)
{
timer = new QTimer(this);

connect(timer, SIGNAL(timeout()), this, SLOT(slotWaitTimeout()));
timer->setSingleShot(true);
timer->start(nTimeOutms);

connect(this, SIGNAL(signalReadTimeout()), &eventLoop, SLOT(quit()));
}

// Wait on QNetworkManager reply here
connect(manager, SIGNAL(finished(QNetworkReply *)), &eventLoop, SLOT(quit()));

if (m_pReply != NULL)
{
// Preferrably we wait for the first reply which comes faster than the finished signal
connect(m_pReply, SIGNAL(readyRead()), &eventLoop, SLOT(quit()));
}
eventLoop.exec();

if (timer != NULL)
{
timer->stop();
delete timer;
timer = NULL;
}

bReadTimeOut = m_bReadTimeOut;
m_bReadTimeOut = false;

return !bReadTimeOut;
}

精华都已经全部讲解了,如果需要全部源码,可以发邮件给我。

Tcp通信案例(简单)

Qt 这边儿核心通信逻辑经过封装,写起来比较简单,其实整个看起来就像是在槽函数里面填空一样。

  • 建立相关连接逻辑
  • 找到相应的信号,绑定槽函数
  • 实现槽函数

这里演示的是一个简单的 Tcp 建立连接,然后通信的过程,整个运行效果如下:

先写 server 端:(可以用 nc 代替客户端进行测试)

多个客户端同时连接:

测试发送数据: (S -> C)(这里客户端是局域网的另外一台电脑)

测试发送数据: (C -> S)

测试分别从 C端,S端断开连接:

客户端界面,略。(直接看代码吧,太简单了)

完整的代码如下:

客户端逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
private slots:
void on_btnConnect_clicked();
void on_btnSend_clicked();
void on_btnClose_clicked();

private:
Ui::clientwidget *ui;
QTcpSocket *tcpSocket;
QString currentInfo;


/******实现如下******/
clientwidget::clientwidget(QWidget *parent) :
QWidget(parent),
ui(new Ui::clientwidget)
{
ui->setupUi(this);
setWindowTitle("客户端");

tcpSocket = nullptr;
tcpSocket = new QTcpSocket(this);

connect(tcpSocket, &QTcpSocket::connected, [=](){
this->currentInfo += "成功和服务器建立连接\n";
ui->txRead->setText(this->currentInfo);
});


connect(tcpSocket, &QTcpSocket::disconnected, [=](){
this->currentInfo += "和服务器连接断开\n";
ui->txRead->setText(this->currentInfo);
});

connect(tcpSocket, &QTcpSocket::readyRead, [=](){
//获取对方发送的内容
QByteArray array = tcpSocket->readAll();
//追加到编辑区中
this->currentInfo += QString(array);
this->currentInfo += "\n";
ui->txRead->setText(this->currentInfo);
});
}

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

//处理连接操作
void clientwidget::on_btnConnect_clicked()
{
//获取服务器的ip 和 端口
QString ip = ui->leIP->text();
quint16 port = ui->lePort->text().toInt();

//如果主动断开连接时已经关闭了,那么重新建立即可
if (!tcpSocket) {
qDebug() << "new socket";
tcpSocket = new QTcpSocket(this);
}

//主动和服务器建立连接
tcpSocket->connectToHost(QHostAddress(ip), port);
}

//设置发送相应
void clientwidget::on_btnSend_clicked()
{
if (!tcpSocket) {
return;
}

//获取编辑框内容
QString str = ui->txSend->toPlainText();
tcpSocket->write(str.toUtf8().data());

//显示出来
this->currentInfo += str;
ui->txRead->setText(this->currentInfo);
ui->txSend->clear();
}

void clientwidget::on_btnClose_clicked()
{
//断开连接
if (tcpSocket) {
qDebug() << "delete" ;
tcpSocket->close(); //自动调用 tcpSocket->disconnectFromHost();

//不要写下面的两句,否则之前绑定的槽函数失效(不再显示信息)
//还是用系统的close()比较好
/*delete tcpSocket;
tcpSocket = nullptr;*/

}
}

注意一下:

  • c和s端注意检查tcp连接是否存活
  • 断开连接并不是要删除原来分配的套接字,只是关闭文件而已,要用close,而不是delete

服务端代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
private slots:
void on_btnClose_clicked();

void on_btnSend_clicked();

private:
Ui::Widget *ui;
QString currentInfo;
QTcpServer *tcpServer; //监听套接字
QTcpSocket *tcpSocket; //通信套接字

Widget::Widget(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
{
ui->setupUi(this);
this->setWindowTitle("服务器");
tcpServer = nullptr;
tcpSocket = nullptr;

//创建监听套接字
tcpServer = new QTcpServer(this); //自动回收空间(指定父对象)

//绑定和Listen
tcpServer->listen(QHostAddress::Any, 9999); //默认绑定当前网卡的所有IP

//绑定 建立连接 的信号
connect(tcpServer, &QTcpServer::newConnection, [=](){
//成功连接的处理
//取出建立好连接的套接字
tcpSocket =tcpServer->nextPendingConnection(); //当前取出一个
//获取对端信息(ip and port) 地址要进行网络地址到本机地址的转换
QString ip = tcpSocket->peerAddress().toString();//.toIPv4Address();
quint16 port = tcpSocket->peerPort();

this->currentInfo += QString("[%1:%2]:成功连接了").arg(ip).arg(port);
this->currentInfo += "\n";
ui->teRead->setText(currentInfo);

// 只有在连接成功的基础上才谈接收(否则 tcpSocket对象为空)
//绑定 接收 的信号 (通信套接字)
connect(tcpSocket, &QTcpSocket::readyRead, [=](){
//有数据可读 (取出来,显示)
QByteArray array = tcpSocket->readAll();
this->currentInfo.append("received from ["
+ ip + ":" + QString::number(port)
+"]"+ array.data() + "\n");
ui->teRead->setText(this->currentInfo);

});


//处理断开 (客户端或者服务器这边儿)
connect(tcpSocket, &QTcpSocket::disconnected, [=](){
this->currentInfo += "断开当前连接\n";
ui->teRead->setText(this->currentInfo);
});

});


}

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

//关闭当前连接
void Widget::on_btnClose_clicked()
{
//主动关闭当前连接
if(tcpSocket) {
tcpSocket->disconnectFromHost(); //服务器主动断开
tcpSocket->close();
tcpSocket = nullptr; //这里写没有关系,因为每次都是建立的新的 tcpSocket;
}
/*
if(tcpServer) {
tcpServer->close();
}*/
//this->close();
}

//处理发送
void Widget::on_btnSend_clicked()
{
if(!tcpSocket) {
return;
}

//获取编辑区内容
QString str = ui->teWrite->toPlainText()+"\n";
//给对方发送数据(只会给当前连接的对端发送)
tcpSocket->write(str.toUtf8().data());

//显示在currentInfo上面
//获取对端信息(ip and port) 地址要进行网络地址到本机地址的转换
QString ip = tcpSocket->peerAddress().toString();//.toIPv4Address();
quint16 port = tcpSocket->peerPort();
this->currentInfo = this->currentInfo + "server sent to [" + ip
+" : " + QString::number(port) +"]" + str.toUtf8().data();
ui->teRead->setText(this->currentInfo);

//发送完毕, 清空输入端
ui->teWrite->clear();
}

服务端写完,可以直接用 nc 工具测试, lsof -i : 9999 查看端口号.

Tcp文件传输(简单)

套接字编程 + 网络文件读写。基本是上一个案例的基础上加上网络文件读写处理。

先说要注意的细节:

  • 服务器发送时注意粘包问题,即发送文件头的时候后面跟着数据呢;(tcp会凑够数再发送, 或者延迟发送)
    • 解决方法先读指定长度字节的头部或者发完头部信息,隔一段时间再发送文件数据或者干脆分两次发送
    • 本案例,采用两段式传输,先传输文件头,后传输文件体,也就是客户端第二次触发 readReay 的时候才读取文件体
  • 传输中途断开连接了,选择文件之后断开连接了等情况, 先检查连接,再传输
  • 进度条显示不要越界等
  • 服务器端写的时候处理的好发送量,例如4K为单位;那么客户端读的时候也方便
  • 绑定 readRead, 一定放在 newConnection 里面(没有连接建立的情况下绑定肯定出异常)

下面看看运行的效果以及需要处理的细节:

没有建立连接的情况下,不能选择传输文件: (更不要提发送文件)

连接建立之后,按钮可用:

不管哪一方断开连接,按钮都不可用:

选择文件后,断开连接,要清理连接信息以及所选文件等:

文件发送完毕,服务器端主动断开连接:(这是没有写客户端的时候测试)

文件发送完毕,客户端发送信息通知服务端,然后服务端收到信息后,显示“客户端”已经收到文件:

(这里其实,选择文件也应该 setEnable(false);)

中途遇到的小问题记载:
1.进度条显示应该是 M 为单位,即 1024 的倍数, 结果写错了:


2.重复连续发送文件,第二次传输文件,发现总是受到的文件少一个字节
应该在第二次传输之前,设置为下一次又应该传输文件头了。


3.粘包问题:
客户端给服务器端发送收到的字节序号,每次服务器是分简单批次字节写出,然后可以看到,服务端每次收到的都是服务端分多次写出的数据。
写就是生产者貌似生产多了,消费者接受太慢了?(其实是客户端这边 TCP 发包时,看到字节太少,这里 delay发送,组包,提高传输效率)

详细代码片段如下:

客户端:

服务端:

两次发送,以避免字节序粘包大致逻辑如下:

核心代码如下:

客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
private slots:
void on_btnConnect_clicked();

protected:
void closeEvent(QCloseEvent *);

private:
Ui::ClientWidget *ui;

QTcpSocket *tcpSocket; //通信套接字

QFile file;
QString fileName; //文件名
quint64 fileSize; //文件大小
quint64 recvSize; //已经发送的大小

bool isHead; //标记本次是否是接收文件头

//实现:
ClientWidget::ClientWidget(QWidget *parent) :
QWidget(parent),
ui(new Ui::ClientWidget)
{
ui->setupUi(this);
setWindowTitle("客户端");
ui->progressBar->setValue(0); //初始化进度条

tcpSocket = new QTcpSocket(this);



//然后就等着连接之后,别人给我发东西
isHead = true;
//绑定读
connect(tcpSocket, &QTcpSocket::readyRead, [=](){

//直接全部读取
QByteArray buf = tcpSocket->readAll(); //第一次肯定是拿到头

if (isHead) { //之所以可以这样做,完全是因为已知服务器端会分两次写
isHead = false;

//解析头信息 (拆包) "filename##size" (初始化数据)
//QString section() 方法, 按段拆包
fileName = QString(buf).section("##", 0, 0);
fileSize = QString(buf).section("##", 1, 1).toInt();
recvSize = 0;

if(fileName.isEmpty() || fileSize == 0){
//做一下异常处理
qDebug() << "收到的文件可能是空文件";
fileName = "NULL";
fileSize = 1;
}

//创建文件并打开
file.setFileName(fileName); //和可执行文件同一路径
bool isOpen = file.open(QIODevice::WriteOnly);
if(!isOpen) {
qDebug() << "创建文件出错了"; //是不是要重试?
tcpSocket->close();
return;
}


//开始设置进度条信息
ui->progressBar->setMinimum(0); //最小值
ui->progressBar->setMaximum(fileSize/1024); //最大值
ui->progressBar->setValue(0); //当前值

//如果创建文件ok, 提示一下准备收取文件
QMessageBox::information(this, "文件信息", QString("即将接收文件: %1, 大小 %2 kb")
.arg(fileName).arg(fileSize/1024));

tcpSocket->write("file");

} else { //并非读取头, 而是读取文件内容
//(注意上面用的是 readAll, 不用担心服务器方会根据链路带宽进行调整发送的大小)
//一般就是会发送多次, 但是经过TCP的组织,满足一定长度才真正发出去
quint64 len = file.write(buf);
//qDebug() << "client : write " + QString::number(len) + " Bytes to file this time " ;
recvSize += len;

/*这里发送长度给服务器打印,为了 演示一下 粘包问题 */
tcpSocket->write( QString::number(recvSize).toUtf8().data());
qDebug() << "粘包问题 Client: " << QString::number(recvSize);


//同时更新进度条
ui->progressBar->setValue(recvSize/1024);

if (recvSize == fileSize) {
QMessageBox::information(this, "完成", "文件接收完成, 通知服务器");
tcpSocket->write("done"); //发给服务器, 服务器收到后显示一下发送完毕.
isHead = true; //避免下次直接又重新读 (如果没有这一句,连续发送就有问题)

file.close(); //已经全部写入文件,现在关闭文件
tcpSocket->close();
}
}
});
}

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

//主动连接过去
void ClientWidget::on_btnConnect_clicked()
{
QString ip = ui->leIP->text();
quint16 port = ui->lePort->text().toInt();

tcpSocket->connectToHost(QHostAddress(ip), port);
ui->progressBar->setValue(0); //初始化进度条

}


void ClientWidget::closeEvent(QCloseEvent *e)
{
if (tcpSocket->isOpen()) {
tcpSocket->close();
}

if(file.isOpen()) {
file.close();
}
}

服务端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
private slots:
void on_btnFile_clicked();

void on_btnSend_clicked();

private:
Ui::ServerWidget *ui;

QTcpServer *tcpServer;
QTcpSocket *tcpSocket;

QFile file;
QString fileName; //文件名
quint64 fileSize; //文件大小
quint64 sendSize; //已经发送的大小

//服务端收到客户端要文件题的信息启动定时器发送文件体
QTimer timer;

/*********实现*********/
ServerWidget::ServerWidget(QWidget *parent) :
QWidget(parent),
ui(new Ui::ServerWidget)
{
ui->setupUi(this);
setWindowTitle("服务器端端口 9999");
ui->btnFile->setEnabled(false); //当前没有连接建立, 不能按按钮
ui->btnSend->setEnabled(false);

//绑定并监听
tcpServer = new QTcpServer(this);
tcpServer->listen(QHostAddress::Any, 9999);

//绑定连接成功槽函数
connect(tcpServer, &QTcpServer::newConnection, [=](){

//取出建立好的连接套接字
tcpSocket = tcpServer->nextPendingConnection();
//获取对方的IP和Port
QString ip = tcpSocket->peerAddress().toString();
quint16 port = tcpSocket->peerPort();

ui->textEdit->setText(QString("[%1:%2] 连接成功").arg(ip).arg(port));

//成功连接后,让选择文件和发送按钮,可用
ui->btnFile->setEnabled(true);

//绑定连接已经断开
connect(tcpSocket, &QTcpSocket::disconnected, [=](){
//可能是对方已经断开连接
qDebug() << "连接已经断开";
if(tcpSocket) {
ui->textEdit->append("当前连接已经断开, 清理有关信息");
qDebug() << "我也主动断开当前连接";
tcpSocket->close();
}
//清理打开的文件信息
fileName.clear();
fileSize = 0;
sendSize = 0;


//disable btn
ui->btnFile->setEnabled(false);
ui->btnSend->setEnabled(false);
});

//绑定一下客户端的发送数据 readRead, 一定放在newConnection里面
connect(tcpSocket, &QTcpSocket::readyRead, [=](){
//取出来客户端发送的信息
QString buf(tcpSocket->readAll());
if (buf == "done") {
ui->textEdit->append("客户端已经完全接受文件");
file.close();
tcpSocket->close(); //一次连接只传递一个文件

ui->btnFile->setEnabled(false); //除非有新的连接
ui->btnSend->setEnabled(false);
} else if (buf == "file") {
//启动定时器,开始发第二波(即文件数据)
timer.start(100);
} else {
//其他情况打印一下从客户端发来的长度信息 --- 演示一下粘包
qDebug() << "粘包问题 Server: " + buf;
}
});

});


//绑定定时器的 timerout事件
connect(&timer, &QTimer::timeout, [=](){
//已经成功触发过来,调用 sendFile, 所以先关闭定时器
timer.stop();
sendFile();
});
}

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

//选择文件按钮
void ServerWidget::on_btnFile_clicked()
{
QString filePath = QFileDialog::getOpenFileName(this, "open", "../");


if (!filePath.isEmpty()) { //路径有效
fileName.clear();
fileSize = 0;
sendSize = 0;

//读文件信息(只读打开) : 文件名, 大小
QFileInfo info(filePath);
fileName = info.fileName();//baseName();
fileSize = info.size();

//只读打开(建立文件关联)
file.setFileName(filePath);
bool isOpen = file.open(QIODevice::ReadOnly);

if (!isOpen) {
qDebug() << "只读模式打开文件失败";
//相关处理
}

//提示您已经打开文件了
ui->textEdit->append(filePath + "已经选定, 文件大小:" + QString::number(fileSize));

ui->btnFile->setEnabled(false); //只能选一次,不能重复选择
ui->btnSend->setEnabled(true); //可以发送

} else { //没有选择文件
qDebug() << "路径无效的处理代码开始执行" ;
}
}

//发送文件按钮
void ServerWidget::on_btnSend_clicked()
{
//先发送文件头信息, 格式: 文件名##文件大小
QString head = QString("%1##%2").arg(fileName).arg(fileSize);

//你发送的时候,对方主动断开了连接
if (tcpSocket->isWritable()) {
quint64 len = tcpSocket->write(head.toUtf8()); //发送头部
if (len > 0) { //头部发送成功, 开始发送文件内容
//防止TCP粘包 (延迟发送文件内容, 借助 QTimer的 timeout 信号槽)

//这种策略不管用, 如果对方确认的时间过长,对方还是会一次性读完小文件。
//---发送文件就交给 readyRead槽函数吧--即等待客户端说能发了才发
//timer.start(100); //定时器的作用就是100ms后调转到 sendFile 开始真正的文件发送工作
} else {
qDebug() << "头部信息发送失败, 直接关闭文件" ;
file.close(); //关闭文件
ui->btnFile->setEnabled(true);
ui->btnSend->setEnabled(false);
}
} else {
qDebug() << "连接已经断开, 不能发送"; //任何时候断开连接了,就不能让用户点击这个按妞
}
}

void ServerWidget::sendFile()
{
quint64 len = 0; //当前读了多少
if (!file.isOpen()) {
qDebug() << "文件都没有打开,发送不了";
ui->btnFile->setEnabled(true);
ui->btnSend->setEnabled(false);
return;
}

//按照 4K 大小来发送
do {
char buf[4*1024] = {0}; //每次发送4K
len = file.read(buf, sizeof(buf)); //先读
//if (lend > 0 ) { //读多少发多少
sendSize += tcpSocket->write(buf, len);
//}
} while(len > 0);//while(sendSize < fileSize); //没有发送完毕或者每次都能读到数据

/* 交给客户端的通知消息来做下面的事儿
//sendSize = fileSize
if (sendSize == fileSize) {
ui->textEdit->append("文件已经发送完毕; 断开当前连接");
file.close();
//断开客户端(一次只发送一个)
tcpSocket->close();

}

ui->btnFile->setEnabled(false);
ui->btnSend->setEnabled(false);
*/
}

Udp通信案例(简单)

一个 peer to peer 的通信程序(相互发送内容, 广播,组播)
但广播需要路由支持.

最终运行效果大致如下: (建立套接字之后就开始发送数据报)

开发过程阶段截图:
1.测试接收:

2.测试发送

3.测试组播 (先加入组播地址才能收到)

核心代码也比较简单,主要是看其流程:
(端对端,只写一份代码即可)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
public:
void dealMsg(); //处理读的槽函数(接收对方发送的数据)

private slots:
void on_btnClose_clicked();
void on_btnSend_clicked();

private:
QUdpSocket *udpSocket;

/********实现部分**********/
Widget::Widget(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
{
ui->setupUi(this);

//分配空间, 指定父对象(自动回收)
udpSocket = new QUdpSocket(this);

//绑定
//(因为只有一个界面,所以先运行一个8888,然后在运行一个9999, 一共两个实例)
//udpSocket->bind(8888); //最好指定一下是绑定的 ipv4的端口,不然绑定组播地址报错
udpSocket->bind(QHostAddress::AnyIPv4, 8888);


this->setWindowTitle("我的端口 8888");

//绑定读操作
connect(udpSocket, &QUdpSocket::readyRead, this, &Widget::dealMsg);


//如果接收组播的话
udpSocket->joinMulticastGroup(QHostAddress("224.0.0.2"));
//退出组播组
//udpSocket->leaveMulticastGroup(QHostAddress("224.0.0.2"));

}

void Widget::dealMsg()
{
//读取对方发送的内容
char buf[1024] = {0};
//拿到对方的IP和port
QHostAddress cliAddr; //局域网广播255.255.255.255(需要权限或者路由允许)
quint16 port;
qint64 len; //读到多少内容
len = udpSocket->readDatagram(buf, sizeof(buf), &cliAddr, &port);

//读到了内容, 开始显示
if (len > 0) {
QString str = QString("[%1:%2] %3")
.arg(cliAddr.toString())
.arg((port))
.arg(buf);
ui->textEdit->setText(str);
}

}

void Widget::on_btnClose_clicked()
{
this->udpSocket->close();
delete this->udpSocket;
this->udpSocket = NULL;

this->close(); //关闭窗口

}

//发送处理
void Widget::on_btnSend_clicked()
{
//先获取指定了的对方的IP和端口
QString ip = ui->leIP->text();
quint16 port = ui->lePort->text().toInt();

//判断一下是否 ip/port 正常
//代码略

//获取编辑区内容,准备发送
QString str = ui->textEdit->toPlainText();

//给指定的IP发送数据
udpSocket->writeDatagram(str.toUtf8(), QHostAddress(ip), port);
}

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

Merlin
2017.2 客户端编程谈什么传输层 : )

文章目录
  1. 1. 访问网络(简单)
  2. 2. 同步阻塞下载文件(范例)
  3. 3. Tcp通信案例(简单)
  4. 4. Tcp文件传输(简单)
  5. 5. Udp通信案例(简单)
|