分享

QTcpServer、QTcpSocket、QUdpSocket在聊天程序上的应用

 QomoIT 2018-03-28

一、TCP/UDP通信编程在Qt中的实现方法:

与Linux的TCP/UDP通信接口(Socket套接字)类似,相当于是对LinuxSocket套接字进行了一部分合并与包装,其具体图解分析如下:
这里写图片描述

1、详解Qt TCP通信(QTcpSocket、QTcpServer):
针对上图进行一些简单的分析:
QTcpServer用来创建服务器对象,服务器对象创建以后,调用成员函数listen()进行连接监听,其中listen()包含了绑定IP和Port的操作,具体的参数Prot指定端口,IP则可为以下参数:

QHostAddress::Null      //空地址对象
QHostAddress::LocalHost     //监听本地主机IPv4,相当于QHostAddress("127.0.0.1")
QHostAddress::LocalHostIPv6 //监听本地主机IPv6,相当于QHostAddress("::1")
QHostAddress::Broadcast     //IPv4广播地址,相当于QHostAddress("255.255.255.255")
QHostAddress::AnyIPv4       //监听当前网卡所有IPv4,相当于QHostAddress("0.0.0.0")
QHostAddress::AnyIPv6       //监听当前网卡所有IPv6,相当于QHostAddress("::")
QHostAddress::Any       //监听当前网卡的所有IP
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

服务器就一直阻塞监听,而客户端用QTcpSocket创建Tcp通信对象,使用成员函数connectToHost()进行发起连接操作,当服务器接收到连接请求完成三次握手之后,连接成功,服务器的QTcpServer类对象会触发一个newConnection()信号,而对该信号,我们加以处理,在其槽函数中取出建立连接后服务器端创建的TcpSocket用于通信的对象(用nextPendingConnection()成员来获取监听队列中第一个建立连接完成的套接字),注意:此时服务器有两个对象,QTcpServer和QTcpSocket的对象,一个用来监听一个用来通信。连接成功之后,客户端也会触发一个connected()连接成功的信号。这样就可以开始进行数据传输。

服务器的QTcpSocket对象和客户端的QTcpSocket对象进行数据交换,那么发送方发送数据(write()),对端检测信号readyRead(),如果发送成功,则readyRead()信号就会被触发,此时我们只要在readyRead()信号的槽函数中实现数据的接受读取(read()、readAll()等)即可。

如果一方要断开连接,需要使用QTcpSocket类对象的disconnectFromHost()成员,对于disconnectFromHost()成员函数,描述如下:

void QAbstractSocket::disconnectFromHost()

Attempts to close the socket. If there is pending data waiting to be written, QAbstractSocket will enter ClosingState and wait until all data has been written. Eventually, it will enter UnconnectedState and emit the disconnected() signal.
  • 1
  • 2
  • 3

该关闭连接操作,是试图关闭连接操作,如果关闭连接请求时还有数据没有写完,则需要等待所有数据被写入,最终完成后会修改套接字状态(UnconnectedState未连接状态),并发送一个disconnected()信号表示连接已经断开。关于套接字的状态枚举如下:

QAbstractSocket::UnconnectedState   //The socket is not connected
QAbstractSocket::HostLookupState    //The socket is performing a host name lookup.
QAbstractSocket::ConnectingState    //The socket has started establishing a connection.
QAbstractSocket::ConnectedState     //A connection is established
QAbstractSocket::BoundState     //The socket is bound to an address and port
QAbstractSocket::ClosingState       //The socket is about to close (data may still be waiting to be written).
QAbstractSocket::ListeningState     //For internal use only.
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

2、Qt Udp通信(QUdpSocket):
Udp比Tcp简单的多,因为服务器不需要进行监听,显得和客户端并无两样,只需要知道对端的IP和端口就能进行数据传输,但是却不保证一定能完成消息的抵达(这就是TCP与UDP的一个主要区别:TCP会粘包但是不会丢包,而UDP会丢包不会粘包),(我们会在测试中用同一段代码来测试客户端与服务器,因为对于单线程/进程来说其实就是没有区别)。其次,TCP与UDP的数据形式不同,所UDP的数据接收成员函数,自然不能用TCP的简单的read、write(流式数据),而要使用readDatagram、writeDatagram。

二、代码简单测试:

注意:“.pro”工程文件中需要加上“QT += network”和“CONFIG += C++11”

1、TCP点对点聊天:
服务器:

/*severwidget.h*/
#ifndef SERVERWIDGET_H
#define SERVERWIDGET_H

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

namespace Ui {
class ServerWidget;
}

class ServerWidget : public QWidget
{
    Q_OBJECT

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

private slots:
    void on_buttonClose_clicked();
    void on_buttonSend_clicked();
private:
    Ui::ServerWidget *ui;
    QTcpServer * tcpServer; //监听套接字对象
    QTcpSocket * tcpSocket; //通信套接字对象
};

#endif // 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
/*serverwidget.cpp*/
#include "serverwidget.h"
#include "ui_serverwidget.h"

#define PORT_SERVER 8888

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

    //监听套接字
    setWindowTitle("Server:8888");
    tcpServer = new QTcpServer(this);   //指定父对象,自动回收空间
    //指定绑定的IP(Any指当前网卡的所有IP)和port
    tcpServer->listen(QHostAddress::Any, PORT_SERVER);

    //获取连接
    connect(tcpServer, &QTcpServer::newConnection,
            [=](){
                //取出建立好连接的套接字,用于通信
                //取监听队列中下一个(第一个)连接
                tcpSocket = tcpServer->nextPendingConnection();

                //获取对方的IP:port
                QString ipCli = tcpSocket->peerAddress().toString();
                qint16 portCli = tcpSocket->peerPort();
                QString temp = QString("[%1:%2]:连接成功").arg(ipCli).arg(portCli);
                ui->textRead->setText(temp);        //显示连接信息

                //进行通信套接字的读取
                connect(tcpSocket, &QTcpSocket::readyRead,
                        [=](){
                            //从通信套接字中取出内容
                            QByteArray array = tcpSocket->readAll();
                            ui->textRead->append(array);
                        }
                        );
                connect(tcpSocket, &QTcpSocket::disconnected,
                        [=](){ui->textRead->setText("连接断开");});
            }
            );

}

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

void ServerWidget::on_buttonClose_clicked()
{
    //主动和客户端断开连接
    if(tcpSocket != NULL){
        ui->textRead->setText("连接断开");
        tcpSocket->disconnectFromHost();
        tcpSocket->close();
        tcpSocket = NULL;
    }
}

void ServerWidget::on_buttonSend_clicked()
{
    //获取编辑器内容
    if(tcpSocket != NULL){
        QString str = ui->textWrite->toPlainText();
        //给对方发送数据
        tcpSocket->write(str.toUtf8().data());
    }
}
  • 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

客户端:
头文件与服务器基本一致,仅少一个“QTcpServer * tcpServer”指针。

/*clientwidget.cpp*/
#include "clientwidget.h"
#include "ui_clientwidget.h"
#include <QHostAddress>
#include <QByteArray>

ClientWidget::ClientWidget(QWidget *parent) :
    QDialog(parent),
    ui(new Ui::ClientWidget)
{
    ui->setupUi(this);
    setWindowTitle("Client");
    tcpSocket = NULL;

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

    connect(tcpSocket, QTcpSocket::connected,
            [=](){ui->textRead->setText("和服务器建立连接成功");});
    connect(tcpSocket, QTcpSocket::readyRead,
            [=](){
                //获取对方发送的内容
                QByteArray array = tcpSocket->readAll();
                //追加到编辑区中
                ui->textRead->append(array);
            }
            );
    connect(tcpSocket, &QTcpSocket::disconnected,
            [=](){ui->textRead->setText("连接断开");});
}

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

void ClientWidget::on_buttonConnect_clicked()
{
    QString ipSer = ui->lineEditIP->text();
    qint16 portSer = ui->lineEditPort->text().toInt();

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

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

void ClientWidget::on_buttonClose_clicked()
{
    //主动和对方断开连接
    ui->textRead->setText("连接断开");
    tcpSocket->disconnectFromHost();
    tcpSocket->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

由于在同一个工程中,所以需要main中为服务器与客户端均创建一个对象并且调用show()即可:
这里写图片描述

测试结果:
这里写图片描述

2、UDP点对点聊天:
服务器与客户端通用:
头文件也是基本和TCP相同,TcpSocket改为UdpSocket。

#include "widget.h"
#include "ui_widget.h"
#include <QHostAddress>
#include <QKeyEvent>
#include <QEvent>

#define BUFFER_SIZE 1024

Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget)
{
    ui->setupUi(this);
    QString str = QString("<h2>请先绑定端口!</h2>");
    ui->textEditRead->setText(str);
    //socket初始化
    udpSocket = NULL;
}

Widget::~Widget()
{
    delete ui;
}
void Widget::dealMessage()
{
    char buf[BUFFER_SIZE]={0};
    QHostAddress cliAddr;
    quint16 cliPort;
    qint64 ret =  udpSocket->readDatagram(buf, BUFFER_SIZE, &cliAddr, &cliPort);
    if(ret > 0){
        QString str = QString("[%1:%2]:%3")
                .arg(cliAddr.toString())
                .arg(cliPort)
                .arg(buf);
        ui->textEditRead->append(str);
    }
}

void Widget::on_buttonSend_clicked()
{
    if(udpSocket == NULL){
        return;
    }//没绑定端口之前不能发送数据
    QString peer_ip = ui->lineEditIP->text();
    qint64 peer_port = ui->lineEditPeerPort->text().toInt();

    QString str = ui->textEditWrite->toPlainText();
    udpSocket->writeDatagram(str.toUtf8(),QHostAddress(peer_ip),peer_port);
    ui->textEditWrite->clear();
}

void Widget::on_buttonBindOwnPort_clicked()
{
    //分配空间,指定父对象
    quint64 port = ui->lineEditOwnPort->text().toInt();
    udpSocket = new QUdpSocket(this);
    udpSocket->bind(port);
    QString str = QString("<h3>端口绑定成功!</h3>");
    ui->textEditRead->setText(str);
    setWindowTitle(QString("Port:%1").arg(port));

    connect(udpSocket, &QUdpSocket::readyRead, this, &dealMessage);
}

void Widget::on_buttonClose_clicked()
{
    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

UI布局:
这里写图片描述

测试结果:
我们在关闭后是直接退出界面窗口,而不是像TCP那样断开连接不退出程序,因为UDP不需要断开连接(从没有建立连接),并且这里对TCP的GUI聊天进行简单优化,每次发送之后编辑窗口清空。可以看到在绑定端口后(即使在绑定端口前)服务器、客户端这一概念已经基本不区分了。注意:在ui界面文件中,将显示接收内容的textEditRead的属性设置为只读:

这里写图片描述

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多