Qt实战(2)

TCP 和 UDP 通信

TCP

先通过一张图片了解TCP通信过程,QT的TCP Socket(套接字)通信仍然有服务端、客户端之分。图中左为客户端,右为服务端。服务端通过监听某个端口来监听是否有客户端连接到来,如果有连接到来,则建立的SOCKET连接;客户端通过IP和PORT(端口)连接服务端,当成功建立连接之后,就可进行数据的接收和发送了。数据的收发是通过read()和write()来进行的。Socket,也就是常说的“套接字”。Socket简单地说,就是一个IP地址加一个port端口
TCP通信原理图
TCP通信原理图

先来看运行结果:

服务端

  1. 新建项目,选择Qt Widgets应用,项目名为TCP_connect,类名为ServerWidget。
  2. 在 TCP_connect.pro内联网,添加以下代码,添加后先编译不运行。(之后的所有有关UDP和CDP,都要加上这句,以后不再多说。)

    1
    2
    3
    QT       += core gui network

    CONFIG += C++11 //之后可能会用到c++中的Lambdas表达式,所以把这句也加上
  3. 设计ui界面如生成结果中的服务端

  4. 在ServerWidget.h头文件中,添加头文件并,创建监听套接字和通信套接字对象。
1
2
3
4
5
6
#include<QTcpServer> //监听套接字
#include<QTcpSocket> //通信套接字

private:
QTcpServer *tcpServer;//监听套接字
QTcpSocket *tcpSocket;//通信套接字
  1. 在ServerWidget.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
  setWindowTitle("服务器:8888");

tcpServer=NULL;
tcpSocket=NULL;
//监听套接字分配空间,父窗口自动回收空间
tcpServer=new QTcpServer(this);

//监听并绑定
tcpServer->listen(QHostAddress::Any,8888);

//触发新连接
connect(tcpServer,&QTcpServer::newConnection,
[=](){
//取出建立好连接的套接字并赋值给通信套接字
tcpSocket =tcpServer->nextPendingConnection();//取出下一个连接,即当前最近的一个连接

//获取对方的ip和端口(peer表示对方)
QString ip=tcpSocket->peerAddress().toString();//.toIPv4Address()
qint16 port = tcpSocket->peerPort();

//连接成功
QString temp=QString("[%1:%2]:成功连接").arg(ip).arg(port);
ui->textEditRead->setText(temp);//显示到只读窗口

//接收数据
connect(tcpSocket,&QTcpSocket::readyRead,
[=](){
//从通信套接字中取出内容
QByteArray array= tcpSocket->readAll();
ui->textEditRead->append(array);
});
});
  1. 发送消息(文本)按钮和关闭连接按钮,分别右击转到槽。添加代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

void ServerWidget::on_buttonsend_clicked()
{
if(tcpSocket==NULL){//有套接字连接的情况下,才能进行此操作
return;
}
//获取write编辑区内容
QString str=ui->textEditWrite->toPlainText();

//给客服端发送数据,使用tcpSocket套接字
tcpSocket->write(str.toUtf8().data());
}

void ServerWidget::on_buttonclose_clicked()
{
if(tcpSocket==NULL){
return;
}
//主动和客户端断开连接
tcpSocket->disconnectFromHost();//从服务器取消这个连接到它的服务器
tcpSocket->close();
tcpSocket=NULL;//断开连接后,通信套接字里面就没有内容了
}

客户端

1.右击项目,添加c++class类,类名为clientwidget。设计ui界面如生成结果。
2.在clientwidget.h头文件中添加代码,通信套接字头文件和对象。

1
2
3
4
#include<QTcpSocket> //通信套接字

private:
QTcpSocket *tcpsocket;//通信套接字

3.在clientwidget.cpp文件构造函数中添加代码,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
setWindowTitle("客户端");

tcpsocket=NULL;

//分配空间,指定父对象
tcpsocket=new QTcpSocket(this);

//连接到服务器就会触发connected转到槽
connect(tcpsocket,&QTcpSocket::connected,
[=](){
ui->textEditRead->setText("成功和服务器建立好连接");
});
connect(tcpsocket,&QTcpSocket::readyRead,
[=](){
//获取对方发送的数据
QByteArray array=tcpsocket->readAll();
//显示到read编辑框中
ui->textEditRead->append(array);
});
  1. 三个按钮分别右击转到槽
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
#include<QHostAddress>//之后的操作要添加此头文件



void ClientWidget::on_bottonSend_clicked()
{
//获取write编辑框内容
QString str=ui->textEditWrite->toPlainText();
//发送数据
tcpsocket->write(str.toUtf8().data());

}

void ClientWidget::on_buttonConnect_clicked()
{
//从输入行编辑器获取服务器的ip和端口
QString ip=ui->lineEditIp->text();
qint16 port=ui->lineEditPort->text().toInt();//qint16代表16位的无符号整形。

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

void ClientWidget::on_bottonClose_clicked()
{
//主动和对方断开连接
tcpsocket->disconnectFromHost();
tcpsocket->close();
}

UDP

UDP(User Datagram Protocol即用户数据报协议)是一个轻量级的,不可靠的,面向数据报的无连接协议。就像我们现在使用的QQ,其聊天时就是使用UDP协议进行消息发送的。像QQ那样,当有很多用户,发送的大部分都是短消息,要求能及时响应,并且对安全性要求不是很高的情况下使用UDP协议。即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。

在Qt中提供了QUdpSocket 类来进行UDP数据报(datagrams)的发送(writeDatagram)和接收(readDatagram)。
先来看一下UDP通信过程
udp通信原理图

现在我们来创造下面如图所示的一个服务器:
udp运行结果
新建项目,选择Qt Widgets应用,项目名为udpsocket,类名为Widget。

  1. 先根据上图设计界面
  2. 在头文件中添加通信套接字声明,再定义一个对象
1
2
3
#include<QUdpSocket>

QUdpSocket *udpsocket;
  1. 分配空间和绑定端口,连接通信套接字自动触发readyRead()信号和处理的槽函数

先在头文件声明公有槽函数:

1
void dealMag();//槽函数,处理对方发送过来的信号。

在构造函数添加:

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
Widget::Widget(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
{
ui->setupUi(this);

setWindowTitle("服务端口为:8888");

//分配空间,指定父对象
udpsocket= new QUdpSocket(this);

//绑定端口
udpsocket->bind(8888);
//udpsocket->bind(QHostAddress::AnyIPv4,8888);

//加入某个组播
//组播地址是D类地址
//udpsocket->joinMulticastGroup(QHostAddress("244.0.0.2"));
//udpsocket->leaveMulticastGroup(1);//退出组播

//当对方成功发送数据过来
//自动触发readyRead()信号
connect(udpsocket,&QUdpSocket::readyRead,this,&Widget::dealMag);
}
void Widget::dealMag()
{
//读取对方发送的内容
char buf[1024]={0};
QHostAddress clientAddr; //对方地址
quint16 clientPort; //对方端口
qint64 len=udpsocket->readDatagram(buf,sizeof(buf),&clientAddr,&clientPort);
if(len>0)
{
//接收内容格式化
QString str = QString("[%1:%2] %3")
.arg(clientAddr.toString())
.arg(clientPort)
.arg(buf);
ui->textEdit->setText(str);
}
}

  1. 两个按钮转到槽
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include<QHostAddress>//记得添加这个头文件


void Widget::on_bottonSend_clicked()
{
//获取对方ip和端口
QString ip=ui->lineEditIP->text();
qint16 port=ui->lineEditPort->text().toInt();

//获取编辑区数据
QString str=ui->textEdit->toPlainText();

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

void Widget::on_bottonClose_clicked()
{
//主动和对方断开连接
udpsocket->disconnectFromHost();
udpsocket->close();
}
  1. 根据以上方法就可以创建很多服务端(客户端),通过绑定的端口就可以相互通信了。

UDP组播问题

因为在实际项目中,用户有N个电脑预览实时视频,如果同时有N多个终端去连接服务器,服务器的压力发送数据带宽的压力很大,所以给提出采用组播的方式去解决此类的问题。就像QQ群一样,限定人数,避免广播风暴。组播的原理大致就是服务器往某一组播地址和端口发数据,之后客户端从指定的组播地址和端口去取数据,好处就是减轻了服务器发送的压力

开启多个服务器,用客户端发送数据,所有服务器端(复制的过程)都会收到客户端发送的数据。

组播实现在上个例子代码中注释部分有,需要注意

  • 发送端既可以加入组播,也可以不加入组播;

-吧服务端绑定的ip地址必须是ipv4地址;

  • 组播地址必须是D类地址

TCP传输文件

先来看过传输文件程图:
TCP传输文件运行结果图
运行结果:
TCP传输文件运行结果图

首先看到运行结果如上图所示
新建项目,选择Qt Widgets应用,项目名为tcpfile,类名为ServerWidget。

服务端

  1. 根据运行结果设计界面.
  2. ServerWidget.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
33
34
35
36
37
38
39
40
41
#ifndef SERVERWIDGET_H
#define SERVERWIDGET_H

#include <QWidget>
#include<QTcpServer>//监听套接字
#include<QTcpSocket>//通信套接字
#include<QFile>
#include<QTimer>

namespace Ui {
class ServerWidget;
}

class ServerWidget : public QWidget
{
Q_OBJECT

public:
explicit ServerWidget(QWidget *parent = 0);
~ServerWidget();

void sendData();

private slots:
void on_buttonfile_clicked();//这是转到槽自动添加的

void on_buttonsend_clicked();//这是转到槽自动添加的

private:
Ui::ServerWidget *ui;
QTcpServer *tcpserver;//通信套接字和
QTcpSocket *tcpsocket;//监听套接字

QFile file;//文件对象
QString filename;//文件名字
qint64 filesize;//文件大小
qint64 sendsize;//已经发送文件大小
QTimer timer;//定时器
};

#endif // SERVERWIDGET_H
  1. ServerWidget.cpp中添加代码

    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
    #include "serverwidget.h"
    #include "ui_serverwidget.h"
    #include<QFileDialog>//文件对话框
    #include<QDebug>
    #include<QFileInfo>

    ServerWidget::ServerWidget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::ServerWidget)
    {
    ui->setupUi(this);

    setWindowTitle("服务器端口:8888");

    //分配空间
    tcpserver = new QTcpServer(this);

    //监听
    tcpserver->listen(QHostAddress::Any,8888);

    //没建立连接前是不能对进行操作的
    ui->buttonfile->setEnabled(false);
    ui->buttonsend->setEnabled(false);

    //自动触发newconnection
    connect(tcpserver,&QTcpServer::newConnection,
    [=]()
    {
    //取出建立好的连接套接字,获取ip和端口
    tcpsocket = tcpserver->nextPendingConnection();
    QString ip = tcpsocket->peerAddress().toString();
    quint16 port = tcpsocket->peerPort();//无符号的
    //格式化并显示在文本编辑框中
    QString str = QString("[%1;%2] 连接成功!").arg(ip).arg(port);
    ui->textEdit->setText(str);

    //连接成功后,才能发送文件
    ui->buttonfile->setEnabled(true);

    });
    connect(&timer,&QTimer::timeout,
    [=]()
    {
    //关闭定时器
    timer.stop();

    //发送文件
    sendData();
    });
    }


    void ServerWidget::sendData()
    {
    qint64 len=0;
    do
    {
    //每次发送数据的大小
    char buf[4*1024]={0};//每次发4k
    len=0;

    //往文件中读数据
    len = file.read(buf,sizeof(buf));
    //发送数据,读多少发多少
    len = tcpsocket->write(buf,len);//数据和最大大小

    //发送数据需要累加
    sendsize += len;
    }while(len > 0);

    //是否文件发送完毕
    if(sendsize == filesize)
    {
    ui->textEdit->append("文件发送完毕");

    file.close();

    //把客户端断开
    tcpsocket->disconnectFromHost();
    tcpsocket->close();
    }
    }
  2. 按钮转到槽函数

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
void ServerWidget::on_buttonfile_clicked()
{
//获取打开文件路径
QString filePath = QFileDialog::getOpenFileName(this,"open","../");
if(false == filePath.isEmpty())//选择路径有效
{
filename.clear();
filesize = 0;

//通过路径获取文件信息(文件名和文件大小)
QFileInfo info(filePath);//构造对象
filename = info.fileName();
filesize = info.size();//qint64

//已经发送大小还为0
sendsize=0;
//只读方式打开文件
//指定文件名字
file.setFileName(filePath);

//打开文件
bool isok = file.open(QIODevice::ReadOnly);
if(false == isok)
{
qDebug()<<"只读方式打开文件失败 98";
}

//提示打开的文件路径
ui->textEdit->append(filePath);

//打开文件后,按钮的状态
ui->buttonfile->setEnabled(false);
ui->buttonsend->setEnabled(true);
}
else
{
qDebug()<<"选择文件路径出错";
}

}

void ServerWidget::on_buttonsend_clicked()
{
//先发送文件头信息 文件名##文件大小
QString head = QString("%1##%2").arg(filename).arg(filesize);

//发送头部信息
qint64 len = tcpsocket->write(head.toUtf8());
if(len > 0)//头部信息发送成功
{
//发送真正的文件信息
//防止tcp黏包文件
//需要通过定时器
timer.start(20);//定时器开始就会自动触发timeout信号
}
else
{
qDebug()<<"头部信息发送失败";
file.close();
ui->buttonfile->setEnabled(true);
ui->buttonsend->setEnabled(false);
}
}

客户端

  1. 右击添加,class c++新文件,类名为clientwidget.设计界面
  2. clientwidget.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
33
34
35
#include<QFile>
#include<QTimer>

namespace Ui {
class ServerWidget;
}

class ServerWidget : public QWidget
{
Q_OBJECT

public:
explicit ServerWidget(QWidget *parent = 0);
~ServerWidget();

void sendData();

private slots:
void on_buttonfile_clicked();//这是转到槽自动添加的

void on_buttonsend_clicked();//这是转到槽自动添加的

private:
Ui::ServerWidget *ui;
QTcpServer *tcpserver;//通信套接字和
QTcpSocket *tcpsocket;//监听套接字

QFile file;//文件对象
QString filename;//文件名字
qint64 filesize;//文件大小
qint64 sendsize;//已经发送文件大小
QTimer timer;//定时器
};

#endif // SERVERWIDGET_H
  1. clientwidget.cpp中添加代码
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
#include "clientwidget.h"
#include "ui_clientwidget.h"
#include<QDebug>
#include<QMessageBox>
#include<QHostAddress>

clientWidget::clientWidget(QWidget *parent) :
QWidget(parent),
ui(new Ui::clientWidget)
{
ui->setupUi(this);

setWindowTitle("客户端");

tcpsocket = new QTcpSocket(this);

//ui->progressBar->setValue(0);//当前值

isstart = true;//用来判断接受的是不是头部

//连接成功准备接收文件
connect(tcpsocket,&QTcpSocket::readyRead,
[=]()
{
//取出接受的内容
QByteArray buf = tcpsocket->readAll();
if(true == isstart)//第一次发的头,接收头
{
isstart = false;

//取出接收的内容(拆包)
filename = QString(buf).section("##",0,0);
filesize = QString(buf).section("##",1,1).toInt();
//开始收到的文件大小为0
receivesize=0;

//打开文件
file.setFileName(filename);
bool isok=file.open(QIODevice::WriteOnly);

if(false == isok)
{
qDebug()<<"writeonly error";
}


}
else//取出文件数据
{
qint64 len = file.write(buf);
receivesize += len;

if(receivesize==filesize)//文件接受完成
{
//给服务器发送接收完成信息
tcpsocket->write("file done");

QMessageBox::information(this,"完成","文件接受完成!");

tcpsocket->disconnectFromHost();//断开连接
tcpsocket->close();
}
}

});

}
  1. 按钮转到槽
1
2
3
4
5
6
7
8
void clientWidget::on_bottonconnect_clicked()
{
//获取服务器的ip和端口
QString ip=ui->lineEditip->text();
quint16 port = ui->lineEditport->text().toInt();

tcpsocket->connectToHost(QHostAddress(ip),port);
}
  1. 主函数添加为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include "serverwidget.h"
#include <QApplication>
#include"clientwidget.h"

int main(int argc, char *argv[])
{
QApplication a(argc, argv);
ServerWidget w;
w.show();

clientWidget w2;
w2.show();

return a.exec();
}

心得

在学完上面的四个知识后,我写了一个实现两个pc机之间互相通信的程序,一个简单的聊天室,运行结果如下图,另一端改变绑定端口即可,使用的UDP协议,可以实现发文字消息和传输文件(文件对方接收后可以直接创建,但只能写一部分进去,我一直以为是文件写入有问题,后来才找出是我发文件的这一端发送文件大小小于实际文件大小,然后拖着拖着就难得去改了(优秀)),里面最大的问题是判断你接受的是文件还是文字问题,然后我用了分片(section)加关键字(file 和 text)区分。之后我会把我的代码上传到码云,这里不再说明过程。

TCP传输文件运行结果图

你可以对我进行打赏哦