Qt - TCP通信

网络编程

编写具有网络功能的程序就要用到Qt Network模块。该模块提供了一系列的接口用于TCP/IP编程。什么HTTP发送/接收请求啊、cookies相关的啊、DNS啊等都有对应的C++类可操作。使用network模块,需要在.pro文件中添加“QT += network”。

Qt5中所有网络相关的C++类的继承关系如下图:

 

1. QAbstractSocket

QAbstractSocket类提供了所有套接字类型通用的基本功能 。

QAbstractSocket是QTcpSocket和QUdpSocket的基类,包含这两个类的所有通用功能。 如果你需要一个套接字,你有两个选择:

  • 实例化QTcpSocket或QUdpSocket。

  • 创建本机套接字描述符,实例化QAbstractSocket,并调用setSocketDescriptor()来包装本机套接字。

TCP(传输控制协议)是一种可靠的、面向流的、面向连接的传输协议。 UDP(用户数据报协议)是一个不可靠的、面向数据报的、无连接的协议。 在实践中,这意味着TCP更适合于数据的连续传输,而更轻量级的UDP可以在可靠性不重要的情况下使用。

QAbstractSocket的API统一了这两种协议之间的大部分差异。 例如,尽管UDP是无连接的,但connectToHost()为UDP套接字建立了一个虚拟连接,使您能够以几乎相同的方式使用QAbstractSocket,而不管底层协议是什么。 在内部,QAbstractSocket记住传递给connectToHost()的地址和端口,read()和write()等函数使用这些值。

在任何时候,QAbstractSocket都有一个状态(由state()返回)。 初始状态为UnconnectedState。 调用connectToHost()后,套接字首先进入HostLookupState(socket正在查找主机名)。 如果找到主机,QAbstractSocket进入ConnectingState并发出hostFound()信号。 当连接建立后,它进入ConnectedState并发出connected()。 如果在任何阶段发生错误,则会触发error()。 每当状态改变时,就会触发stateChanged()。 为了方便起见,如果套接字已经准备好读写,isValid()将返回true,但是请注意,在读写发生之前,套接字的状态必须是ConnectedState。

通过调用Read()或write()读取或写入数据,或使用方便的函数readLine()和readAll()。 QAbstractSocket还从QIODevice继承了getChar()、putChar()和ungetChar(),它们处理单个字节。 当数据被写入套接字时将发出bytesWritten()信号。 注意,Qt不限制写缓冲区的大小。 你可以通过听这个信号来监控它的大小。

readyRead()信号在每次到达一个新的数据块时被触发。 然后bytesAvailable()返回可用于读取的字节数。 通常,您将readyRead()信号连接到一个插槽并读取那里的所有可用数据。 如果您没有一次读取所有数据,其余的数据将在稍后仍然可用,并且任何新的传入数据将被附加到QAbstractSocket的内部读缓冲区。 要限制读缓冲区的大小,可以调用setReadBufferSize()。

要关闭套接字,调用disconnectFromHost()。 QAbstractSocket进入QAbstractSocket:: ClosingState。 在将所有挂起的数据写入套接字之后,QAbstractSocket实际上关闭了套接字,输入QAbstractSocket::UnconnectedState,并发出disconnected()。 如果您想立即中止连接,丢弃所有挂起的数据,可以调用abort()。 如果远程主机关闭连接,QAbstractSocket将发出错误(QAbstractSocket::RemoteHostClosedError),在此期间,套接字状态仍然是ConnectedState,然后将发出disconnected()信号。

通过调用 peerPort() 和 peerAddress() 获取连接的对等方的端口和地址。 peerName() 返回传递给 connectToHost() 的对等方的主机名。 localPort() 和 localAddress() 返回本地套接字的端口和地址。

QAbstractSocket提供了一组函数,用于挂起调用线程,直到发出某些信号。 这些函数可以用来实现阻塞套接字:

  • waitForConnected()将阻塞,直到建立连接。

  • waitForReadyRead()会阻塞,直到有新的数据可以读取。

  • waitForBytesWritten()将阻塞,直到一个数据负载被写入套接字。

  • waitForDisconnected()将阻塞,直到连接关闭。

下面是一段示例:

int numRead = 0, numReadTotal = 0;
char buffer[50];
​
forever 
{
     numRead  = socket.read(buffer, 50);
     // do whatever with array
     numReadTotal += numRead;
     if (numRead == 0 && !socket.waitForReadyRead())
         break;
}

如果waitForReadyRead()返回false,则表示连接已经关闭或发生了错误。

使用阻塞套接字编程与使用非阻塞套接字编程完全不同。 阻塞套接字不需要事件循环,通常会导致更简单的代码。 然而,在GUI应用程序中,阻塞套接字应该只在非GUI线程中使用,以避免冻结用户界面。 请参阅fortuneclient和blockingfortuneclient示例来了解这两种方法的概述。

注意:我们不建议将阻塞函数与信号一起使用。 应该使用两种可能性中的一种。

QAbstractSocket可以与QTextStream和QDataStream的流操作符(操作符<<()和操作符>>())一起使用。 但是有一个问题需要注意:在尝试使用操作符>>()读取数据之前,必须确保有足够的数据可用。

如果想要了解更多信息,可以查阅QNetworkAccessManager和QTcpServer。

QAbstractSocket类继承自 : QIODevice.

 

1.1 信号

void connected()//这个信号是在调用connectToHost()并成功建立连接之后发出的。
void disconnected()//当套接字断开连接时,会发出此信号。
void error(QAbstractSocket::SocketError socketError)//这个信号是在发生错误后发出的。socketError参数描述发生的错误类型。
void hostFound()//这个信号是在调用connectToHost()并且主机查找成功之后发出的。
void proxyAuthenticationRequired(const QNetworkProxy &proxy, QAuthenticator *authenticator)//当使用需要身份验证的代理时,可以发出此信号。然后可以用所需的详细信息填充身份验证者对象,以允许身份验证并继续连接。
void stateChanged(QAbstractSocket::SocketState socketState)//每当QAbstractSocket的状态发生变化时,就会发出这个信号。socketState参数是新的状态。

•从QIODevice继承4个信号

void aboutToClose()//这个信号是在设备即将关闭时发出的。如果你有需要在设备关闭之前执行的操作,连接这个信号(例如,如果你有数据在一个单独的缓冲区,需要写入设备)。
void bytesWritten(qint64 bytes)//每当向设备写入有效数据时,就会发出这个信号。bytes参数设置为写入此有效负载的字节数。
void readChannelFinished()//当该设备中的输入(读取)流关闭时,该信号被发出。一旦检测到关闭,就会触发该事件,这意味着可能仍然有数据可用read()进行读取。
void readyRead()//每当从设备读取新数据时,该信号就会发出一次。只有当有新的数据可用时,它才会再次发出,例如当网络数据的新负载到达您的网络套接字时,或者当一个新的数据块被附加到您的设备时。

•从QObject继承了2个信号

void destroyed(QObject *obj = Q_NULLPTR)//该信号在对象obj被销毁之前立即发出,并且不能被阻塞。所有对象的子对象在信号发出后立即被销毁。
void objectNameChanged(const QString &objectName)//该信号在对象的名称被更改之后发出。新的对象名称作为objectName传递。注意:这是私人信号。它可以用于信号连接,但不能由用户发出。注意:属性objectName的通知符信号。参见QObject::的objectName。

 

1.2 公有函数

函数 描述
void abort() 终止当前连接并重置套接字。与disconnectFromHost()不同,这个函数会立即关闭套接字,丢弃写缓冲区中任何挂起的数据。
bool bind(const QHostAddress &address, quint16 port = 0, BindMode mode = DefaultForPlatform) 使用BindMode模式绑定到端口上的地址。
bool bind(quint16 port = 0, BindMode mode = DefaultForPlatform) 这是一个重载函数。绑定到QHostAddress:任何端口端口,使用BindMode模式。
virtual void connectToHost(const QString &hostName, quint16 port, OpenMode openMode = ReadWrite, NetworkLayerProtocol protocol = AnyIPProtocol) 尝试连接到给定端口上的hostName。protocol参数可用于指定使用哪种网络协议(ea. IPv4或IPv6)。
virtual void connectToHost(const QHostAddress &address, quint16 port, OpenMode openMode = ReadWrite) 这是一个重载函数。尝试连接到端口上的地址。
virtual void disconnectFromHost() 试图关闭套接字。如果有挂起的数据等待写入,QAbstractSocket将进入ClosingState并等待,直到所有数据都已写入。最后,它将进入UnconnectedState并发出disconnected()信号。
SocketError error() const 返回上次发生的错误类型。
bool flush() 该函数尽可能多地从内部写缓冲区写入底层网络套接字,而不会阻塞。如果写入了任何数据,该函数返回true;否则返回false。
bool isValid() const 如果套接字有效并可以使用,则返回true;否则返回false。
QHostAddress localAddress() const 如果可用,返回本地套接字的主机地址;否则返回QHostAddress::零。
quint16 localPort() const 如果可用,返回本地套接字的主机端口号(以本机字节顺序);否则返回啊。
PauseModes pauseMode() const 返回此套接字的暂停模式。
QHostAddress peerAddress() const 如果套接字处于ConnectedState状态,返回连接的对等体的地址;否则返回QHostAddress::零。
QString peerName() const 返回connectToHost()指定的对等体名称,如果connectToHost()没有被调用,则返回一个空QString。
quint16 peerPort() const 如果套接字在ConnectedState中,返回连接的对等体的端口;否则返回0。
QNetworkProxy proxy() const 返回此套接字的网络代理。默认情况下使用QNetworkProxy::DefaultProxy,这意味着这个套接字将查询应用程序的默认代理设置。
qint64 readBufferSize() const 返回内部读缓冲区的大小。这限制了在调用read()或readAll()之前客户端可以接收的数据量。读取缓冲区大小为O(默认值)意味着缓冲区没有大小限制,确保没有数据丢失。
virtual void resume() 在套接字上继续数据传输。此方法仅应在套接字被设置为暂停通知并接收到通知之后使用。目前唯一支持的通知是QSslSocket::sslErrors()。如果套接字没有暂停,调用此方法会导致未定义的行为。
void setPauseMode(PauseModes pauseMode) 控制在收到通知时是否暂停。pauseMode参数指定套接字应该暂停的条件。目前唯一支持的通知是QSslSocket::sslErrors()。如果设置为PauseOnSslErrors,套接字上的数据传输将暂停,需要通过调用resume()再次显式启用。默认情况下,该选项设置为PauseNever。在连接到服务器之前必须调用此选项,否则将导致未定义的行为。
void setProxy(const QNetworkProxy &networkProxy) 将此套接字的显式网络代理设置为networkProxy。要禁用这个套接字的代理,使用QNetworkProxy::NoProxy代理类型:
virtual void setReadBufferSize(qint64 size) 将QAbstractSocket的内部读取缓冲区的大小设置为大小字节。
virtual bool setSocketDescriptor(qintptr socketDescriptor, SocketState socketState = ConnectedState, OpenMode openMode = ReadWrite) 使用本机套接字描述符socketDescriptor初始化QAbstractSocket。如果socketDescriptor被接受为有效的套接字描述符,则返回true;否则返回false。socket以openMode指定的模式打开,并进入socketState指定的socket状态。读取和写入缓冲区将被清除,丢弃任何挂起的数据。
virtual void setSocketOption(QAbstractSocket::SocketOption option, const QVariant &value) 将给定选项设置为由value描述的值。注意:在Windows运行时上,在连接套接字之前必须设置QAbstractSocket::KeepAliveOption。
virtual qintptr socketDescriptor() const 如果可用,则返回QAbstractSocket对象的本机套接字描述符;否则返回1。
virtual QVariant socketOption(QAbstractSocket::SocketOption option) 返回选项的值。
SocketType socketType() const 返回套接字类型(TCP, UDP或其他)。
SocketState state() const 返回套接字的状态。
virtual bool waitForConnected(int msecs = 30000) 等待直到套接字被连接,最长可达msecs毫秒。如果连接已经建立,这个函数返回true;否则返回false。在返回false的情况下,可以调用error()来确定错误的原因。
virtual bool waitForDisconnected(int msecs = 30000) 等待直到套接字断开连接,最长可达msecs毫秒。如果连接已经断开,这个函数返回true;否则返回false。在返回false的情况下,可以调用error()来确定错误的原因。

 

2. QTcpServer(监听套接字)

QTcpServer类提供了一个基于tcp服务器的监听套接字。

这个类使接收传入的TCP连接成为可能。 您可以指定端口或让QTcpServer自动选择一个端口。 您可以监听一个特定的地址或所有机器的地址。

调用listen()让服务器监听传入的连接。 然后,每当客户机连接到服务器时,就会发出newConnection()信号。

调用nextPendingConnection()接受挂起的连接作为已连接的QTcpSocket。 该函数返回一个指向QAbstractSocket::ConnectedState中的QTcpSocket的指针,您可以使用该指针与客户端通信。

如果发生了错误,serverError()返回错误的类型,可以调用errorString()来获得人们可读的关于发生了什么事情的描述。

当监听连接时,服务器监听的地址和端口可用serverAddress()和serverPort()获取。

调用close()会使QTcpServer停止监听传入的连接。

虽然QTcpServer主要是为使用事件循环而设计的,但也可以不使用事件循环。 在这种情况下,必须使用waitForNewConnection(),它会阻塞直到连接可用或超时过期。

 

QTcpServer类继承自: QObject

2.1 信号

void acceptError(QAbstractSocket::SocketError socketError)//当接受新连接导致错误时,会发出此信号。
void newConnection()//每当有新的连接可用时,就会发出这个信号。

 

2.2 公有函数

函数 描述
void close() 关闭服务器。服务器将不再监听传入的连接。
QString errorString() const 返回最近发生的错误的人类可读的描述。
virtual bool hasPendingConnections() const 如果服务器有一个挂起的连接,返回true;否则返回
bool isListening() const 如果服务器当前正在监听传入的连接,则返回true;否则返回false。
bool listen(const QHostAddress &address = QHostAddress::Any, quint16 port = 0) 服务器开始监听指定addres和port上的连接。
int maxPendingConnections() const 返回挂起的已接受连接的最大数目。默认值为30。
virtual QTcpSocket *nextPendingConnection() 返回下一个挂起的连接作为已连接的QTcpSocket对象。
void pauseAccepting() 暂停接受新连接。排队连接将保持在队列中。
QNetworkProxy proxy() const 返回此套接字的网络代理。默认使用QNetworkProxy::DefaultProxy。
void resumeAccepting() 恢复接受新连接。
QHostAddress serverAddress() const 如果服务器正在监听连接,返回服务器地址;否则返回QHostAddress::零。
QAbstractSocket::SocketError serverError() const 返回发生的最后一个错误的错误码。
quint16 serverPort() const 如果服务器正在监听连接,返回服务器的端口;否则返回0。
void setMaxPendingConnections(int numConnections) 将挂起的接受连接的最大数量设置为numConnections。在调用nextPendingConnection()之前,QTcpServer将接受不超过numConnections的传入连接。默认情况下,这个限制是30个挂起的连接。
void setProxy(const QNetworkProxy &networkProxy) 将此套接字的显式网络代理设置为networkProxy。
bool setSocketDescriptor(qintptr socketDescriptor) 设置此服务器在监听到socketDescriptor的传入连接时应该使用的套接字描述符。如果套接字设置成功,返回true;否则返回false。
qintptr socketDescriptor() const 设置此服务器在监听到socketDescriptor的传入连接时应该使用的套接字描述符。如果套接字设置成功,返回true;否则返回false。假定套接字处于监听状态。
bool waitForNewConnection(int msec = 0, bool *timedOut = Q_NULLPTR) 等待最多毫秒或直到传入连接可用。如果连接可用,返回true;否则返回false。如果操作超时且timeout不是O,则* timeout将被设为true。

•从QObject继承了31个公共函数,具体查看Qt帮助文档

 

3. QTcpSocket(通信套接字)

TCP(Transmission Control Protocol,传输控制协议)

TCP是一个用于数据传输的地城网络协议,多个网络协议包括(HTTP和FTP都是基于TCP协议),TCP是面向数据流和面向连接的可靠的传输协议。

QTcpSocket类继承自QAbstractSocket,与QUdpSocket传输的数据报不同的是,QTcpSocket传输的是连续的数据流,尤其适合连续的数据传输,TCP一般分为客户端和服务端,即C/S (Client/Server模型)。

QTcpSocket代表了两个独立的数据流,一个用来读取数据,一个用来写入数据,分别采用QTcpSocket::read()及QTcpSocket::write()操作,读取数据前先调用QTcpSocket::bytesAvailable来确定已有足够的数据可用。

QTcpServer处理客户端的连接,可通过QTcpServer::listen()监听客户端发来的连接请求,每当有客户端连接时会发射newConnection()信号,QTcpSocket可用于读取客户端发来的数

 

3.1 信号

 

3.2 公有函数

 

•从QAbstractSocket继承了37个公共函数,上面有写QAbstractSocket的公有函数

•从QIODevice继承了33个公共函数,具体不详细描述,常用的有以下函数:

qint64 QIODevice::read(char *data, qint64 maxSize)//从设备读取最多maxSize字节到数据,并返回读取的字节数。如果发生错误,例如试图读取以WriteOnly模式打开的设备时,此函数返回-1。
    
QByteArray QIODevice::read(qint64 maxSize)//这是一个重载函数。从设备读取最多maxSize字节,并将读取的数据作为QByteArray返回。这个函数没有方法报告错误;返回一个空的QByteArray可能意味着当前没有数据可供读取,或者发生了错误。
    
QByteArray QIODevice::readAll()//从设备读取所有剩余数据,并以字节数组的形式返回。这个函数没有方法报告错误;返回一个空的QByteArray可能意味着当前没有数据可供读取,或者发生了错误。
    
qint64 QIODevice::write(const char *data, qint64 maxSize)//将数据写入设备的数据最多为maxSize字节。返回实际写入的字节数,如果发生错误则返回-1。
    
qint64 QIODevice::write(const char *data)//这是一个重载函数。将以零结尾的8位字符字符串写入设备。返回实际写入的字节数,如果发生错误则返回-1。
    
qint64 QIODevice::write(const QByteArray &byteArray)//这是一个重载函数。将byteArray的内容写入设备。返回实际写入的字节数,如果发生错误则返回-1。

•从QObject继承了31个公共函数,具体查看Qt帮助文档

 

4. TCP单聊

一对一聊天

Qt Tcp通信和windows的类似,分服务端和客户端,模型如下

服务器通信流程:

  • 1.创建QTcpServer对象
  • 2.启动服务器(监听)调用成员方法listen(QHostAddress::Any,端口号)
  • 3.当有客户端链接时候会发送newConnection信号,触发槽函数接受链接(得到一个与客户端通信的套接字QTcpSocket)
  • 4.QTcpsocket发送数据用成员方法write,
  • 5.读数据当客户端有数据来,QTcpSocket对象就会发送readyRead信号,关联槽函数读取数据

客户端通信流程 :

  • 1.创建QTcpSocket对象
  • 2.链接服务器connectToHost(QHostAddress("ip"),端口号)
  • 3.QTcpsocket发送数据用成员方法write,
  • 4.读数据当对方有数据来,QTcpSocket对象就会发送readyRead信号,关联槽函数读取数据

 

文件结构:

 

4.1 TCP服务器

widget.h代码

#ifndef WIDGET_H
#define WIDGET_H

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

QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE

class Widget : public QWidget
{
    Q_OBJECT

public:
    Widget(QWidget *parent = nullptr);
    ~Widget();

private slots:
    void slotsNewConnection();//有新的客户端来连接
    void slotsReadyRead();//读取客户端发来的数据
    void slotsDisconnected();//客户端断开连接
    void on_btnSend_clicked();
    void on_benClose_clicked();

private:
    Ui::Widget *ui;
    QTcpServer* tcpServer;//监听套接字
    QTcpSocket* tcpSocket;//通信套接字
};
#endif // WIDGET_H

 

widget.cpp代码

#include "widget.h"
#include "ui_widget.h"

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

    setWindowTitle("Server");
    ui->textEditRead->setReadOnly(true);

    tcpServer = nullptr;
    tcpSocket = nullptr;
    tcpServer = new QTcpServer(this);//初始化服务器server对象

    //启动服务器监听
    tcpServer->listen(QHostAddress::Any, 8888);
    connect(tcpServer,SIGNAL(newConnection()),this,SLOT(slotsNewConnection())); //有新的客户端连接
}

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

void Widget::slotsNewConnection()
{
    qDebug()<<"有新客户端连接";

    //接受客户端连接,并取出建立好连接的客户端套接字
    tcpSocket = tcpServer->nextPendingConnection();
    //获取对方的IP和端口
    QString ip = tcpSocket->peerAddress().toString();
    qint16 port = tcpSocket->peerPort();
    QString temp = QString("[%1 : %2]: 成功连接").arg(ip).arg(port);
    ui->textEditRead->setText(temp);

    //关联接收客户端数据信号readyRead信号(客户端有数据就会发readyRead信号)
    connect(tcpSocket,SIGNAL(readyRead()),this,SLOT(slotsReadyRead()));
    //检测掉线信号,客户端断开连接
    connect(tcpSocket,SIGNAL(disconnected()),this,SLOT(slotsDisconnected()));
}

void Widget::slotsReadyRead()
{
    //从通信套接字中取出内容
    QByteArray array = tcpSocket->readAll();
    ui->textEditRead->append(array);
}

void Widget::slotsDisconnected()
{
    QTcpSocket *obj = (QTcpSocket*)sender();//掉线对象
    QString ip = obj->peerAddress().toString();
    quint16 port = obj->peerPort();

    QString temp = QString("客户端:%1 %2断开了连接!").arg(ip).arg(port);
    qDebug()<<temp;//打印出掉线对象的ip
    ui->textEditRead->append(temp);
}

void Widget::on_btnSend_clicked()
{
    if(tcpSocket == nullptr)
    {
        return;
    }

    //获取编辑区内容
    QString str = ui->textEditWrite->toPlainText();

    //给对方发送数据,使用套接字是tcpSocket
    tcpSocket->write(str.toUtf8().data());

    //清空编辑区内容
    ui->textEditWrite->clear();
}

void Widget::on_benClose_clicked()
{
    if(tcpSocket == nullptr)
    {
        return;
    }

    //主动和对方断开连接
    tcpSocket->disconnectFromHost();
    tcpSocket->close();
    tcpSocket = nullptr;
}

 

4.2 TCP客户端

widget.h代码

#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include <QTcpSocket>

QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE

class Widget : public QWidget
{
    Q_OBJECT

public:
    Widget(QWidget *parent = nullptr);
    ~Widget();

private slots:
    void slotsConnected();//成功和服务器建立好连接
    void slotsReadyRead();//读取服务器发来的数据
    void on_btnConnect_clicked();
    void on_btnSend_clicked();
    void on_btnClose_clicked();


private:
    Ui::Widget *ui;

    QTcpSocket* tcpSocket;
};
#endif // WIDGET_H

widget.cpp代码

#include "widget.h"
#include "ui_widget.h"

#include <QHostAddress>

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

    setWindowTitle("Client");
    ui->textEditRead->setReadOnly(true);

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

    connect(tcpSocket,SIGNAL(connected()),this,SLOT(slotsConnected()));
    connect(tcpSocket,SIGNAL(readyRead()),this,SLOT(slotsReadyRead()));
}

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

void Widget::slotsConnected()
{
    ui->textEditRead->setText("成功和服务器建立好连接!");
}

void Widget::slotsReadyRead()
{
    //从通信套接字中取出内容
    QByteArray array = tcpSocket->readAll();
    ui->textEditRead->append(array);
}

void Widget::on_btnConnect_clicked()
{
    //获取服务器Ip和端口
    QString ip = ui->lineEditIP->text();
    qint16 port = ui->lineEditPort->text().toInt();

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

void Widget::on_btnSend_clicked()
{
    //获取编辑区内容
    QString str = ui->textEditWrite->toPlainText();

    //给对方发送数据,使用套接字是tcpSocket
    tcpSocket->write(str.toUtf8().data());

    //清空编辑区内容
    ui->textEditWrite->clear();
}

void Widget::on_btnClose_clicked()
{
    //主动和对方断开连接
    tcpSocket->disconnectFromHost();
    tcpSocket->close();
}

 

5. incomingConnection函数

QTcpServer服务器(incomingConnection方法)

这个函数和之前讲过的newConnection信号功能差不多,只要有新的连接出现,就会自动调用这个函数。

然后我们只需在这个函数中新建一个QTcpSocket对象,并且将这个套接字指定为这个函数的参数socketDescriptor,然后将这个套接字存放到套接字列表中就可以实现多个客户端同时登陆了。

这里我们简单看一下这个函数里的内容

void Server::incomingConnection(int socketDescriptor)
{
    TcpClientSocket *tcpclientsocket = new TcpClientSocket(this);//只要有新的连接就生成一个新的通信套接字
    //将新创建的通信套接字描述符指定为参数socketdescriptor
    tcpclientsocket->setSocketDescriptor(socketDescriptor);
 
    //将这个套接字加入客户端套接字列表中
    tcpclientsocketlist.append(tcpclientsocket);
}

Server这个类是继承于QTcpServer类的,所以我们需要在Server类中重写incomingConnection函数。

 

一对一聊天

5.1 TCP服务端

文件目录

tcpserver.h

#ifndef TCPSERVER_H
#define TCPSERVER_H

#include <QTcpServer>
#include <QObject>

class TcpSocket;

class TcpServer : public QTcpServer
{
    Q_OBJECT //为了实现信号和槽的通信

public:
    TcpServer(QObject *parent = 0, int port = 0);
    void senddata(const QString& str);

signals:
    void sig_updata(QString str);
private slots:
    void on_update(QString str);

public:
    virtual void incomingConnection(qintptr socketDescriptor) override;//只要出现一个新的连接,就会自动调用这个函数

private:
    TcpSocket *tcpsocket;
};

#endif // TCPSERVER_H

tcpserver.cpp

#include "tcpserver.h"
#include "tcpsocket.h"

TcpServer::TcpServer(QObject *parent, int port):QTcpServer(parent)
{
    this->listen(QHostAddress::Any, port); //监听
    if(this->isListening())
    {
        qDebug() << "isListening";
    }
}

void TcpServer::incomingConnection(qintptr socketDescriptor)
{

    tcpsocket = new TcpSocket();//只要有新的连接就生成一个新的通信套接字
    //将新创建的通信套接字描述符指定为参数socketdescriptor
    tcpsocket->setSocketDescriptor(socketDescriptor);
    qDebug() << socketDescriptor << " " << tcpsocket->socketDescriptor();

    connect(tcpsocket,SIGNAL(sig_updata(QString)),this,SLOT(on_update(QString)));

    //获取对方的IP和端口
    QString ip = tcpsocket->peerAddress().toString();
    qint16 port = tcpsocket->peerPort();
    QString temp = QString("[%1 : %2]: 成功连接").arg(ip).arg(port);
    qDebug()<<temp;
}

void TcpServer::senddata(const QString& str)
{
    QString msg = str;
    tcpsocket->write(msg.toUtf8().data());
}

void TcpServer::on_update(QString str)
{
    emit sig_updata(str);
}

tcpsocket.h

#ifndef TCPSOCKET_H
#define TCPSOCKET_H

#include <QTcpSocket>

class TcpSocket: public QTcpSocket
{
    Q_OBJECT //添加这个宏是为了实现信号和槽的通信
public:
    TcpSocket(QObject *parent = 0);

signals:
    void sig_updata(QString str);

protected slots:
    void recvdata();//处理readyRead信号读取数据
    void slotclientdisconnected();//客户端断开连接触发disconnected信号,这个槽函数用来处理这个信号
};

#endif // TCPSOCKET_H

tcpsocket.cpp

#include "tcpsocket.h"
#include <QHostAddress>

TcpSocket::TcpSocket(QObject *parent):QTcpSocket(parent)
{

    //客户端发送数据过来就会触发readyRead信号
    connect(this, &TcpSocket::readyRead, this, &TcpSocket::recvdata);
    connect(this, &TcpSocket::disconnected, this, &TcpSocket::slotclientdisconnected);
}

void TcpSocket::recvdata()
{

    QString array = this->readAll();
    //从通信套接字中取出内容
    //QByteArray array = tcpSocket->readAll();
    //ui->textEditRead->append(array);
    qDebug()<<"接收的数据:"<<array.toUtf8().data();

    emit sig_updata(array);

}

void TcpSocket::slotclientdisconnected()
{
    QTcpSocket *obj = (QTcpSocket*)sender();//掉线对象
    QString ip = obj->peerAddress().toString();
    quint16 port = obj->peerPort();

    QString temp = QString("客户端:%1 %2断开了连接!").arg(ip).arg(port);
    qDebug()<<temp;//打印出掉线对象的ip
}

widget.h

#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include "tcpserver.h"

QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE

class Widget : public QWidget
{
    Q_OBJECT

public:
    Widget(QWidget *parent = nullptr);
    ~Widget();

private slots:
    void on_pushButton_clicked();
    void on_btnSend_clicked();
    void on_update(QString str);

private:
    Ui::Widget *ui;

    int port;
    TcpServer* m_tcpserver;
};
#endif // WIDGET_H

widget.cpp

#include "widget.h"
#include "ui_widget.h"


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

    port = 8888;
    m_tcpserver = NULL;


}

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


void Widget::on_pushButton_clicked()
{
    if(m_tcpserver == NULL)
    {
        m_tcpserver  = new TcpServer(this, port);
        connect(m_tcpserver,SIGNAL(sig_updata(QString)),this,SLOT(on_update(QString)));
    }

    if(m_tcpserver)
    {
        ui->textEditRead->setText("服务器启动成功!");
    }
    else
    {
        ui->textEditRead->setText("服务器启动失败!");
    }
}

void Widget::on_btnSend_clicked()
{
    //获取编辑区内容
    QString str = ui->textEditWrite->toPlainText();

    if(str.isEmpty())
    {
        return;
    }

    //给对方发送数据,使用套接字是tcpSocket
    m_tcpserver->senddata(str);
    //清空编辑区内容
    ui->textEditWrite->clear();
}

void Widget::on_update(QString str)
{
    ui->textEditRead->append(str);
}

main.cpp

#include "widget.h"

#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    Widget w;
    w.show();
    return a.exec();
}

 

5.2 TCP客户端

文件目录结构:

tcpsocket.h

#ifndef TCPSOCKET_H
#define TCPSOCKET_H

#include <QTcpSocket>

class TcpSocket: public QTcpSocket
{
    Q_OBJECT //添加这个宏是为了实现信号和槽的通信
public:
    TcpSocket(QObject *parent = 0);

signals:
    void sig_updata(QString str);

protected slots:
    void recvdata();//处理readyRead信号读取数据
    void slotclientdisconnected();//客户端断开连接触发disconnected信号,这个槽函数用来处理这个信号
};

#endif // TCPSOCKET_H

tcpsocket.cpp

#include "tcpsocket.h"
#include <QHostAddress>

TcpSocket::TcpSocket(QObject *parent):QTcpSocket(parent)
{

    //客户端发送数据过来就会触发readyRead信号
    connect(this, &TcpSocket::readyRead, this, &TcpSocket::recvdata);
    connect(this, &TcpSocket::disconnected, this, &TcpSocket::slotclientdisconnected);
}

void TcpSocket::recvdata()
{
    //从通信套接字中取出内容
    QString array = this->readAll();
    //QByteArray array = tcpSocket->readAll();

    emit sig_updata(array);
    qDebug()<<"接收的数据:"<<array.toUtf8().data();
}

void TcpSocket::slotclientdisconnected()
{
    QTcpSocket *obj = (QTcpSocket*)sender();//掉线对象
    QString ip = obj->peerAddress().toString();
    quint16 port = obj->peerPort();

    QString temp = QString("客户端:%1 %2断开了连接!").arg(ip).arg(port);
    qDebug()<<temp;//打印出掉线对象的ip
}

widget.h

#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include <QTcpSocket>

QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE

class TcpSocket;

class Widget : public QWidget
{
    Q_OBJECT

public:
    Widget(QWidget *parent = nullptr);
    ~Widget();

private slots:
    void slotsConnected();//成功和服务器建立好连接
    void on_btnConnect_clicked();
    void on_btnSend_clicked();
    void on_btnClose_clicked();
    void on_update(QString str);


private:
    Ui::Widget *ui;

    TcpSocket* tcpSocket;
};
#endif // WIDGET_H

widget.cpp

#include "widget.h"
#include "ui_widget.h"
#include <QHostAddress>
#include "tcpsocket.h"

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

    setWindowTitle("Client");
    ui->textEditRead->setReadOnly(true);

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

    connect(tcpSocket,SIGNAL(connected()),this,SLOT(slotsConnected()));
    connect(tcpSocket,SIGNAL(sig_updata(QString)),this,SLOT(on_update(QString)));
}

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

void Widget::slotsConnected()
{
    ui->textEditRead->setText("成功和服务器建立好连接!");
}

void Widget::on_btnConnect_clicked()
{
    //获取服务器Ip和端口
    QString ip = ui->lineEditIP->text();
    qint16 port = ui->lineEditPort->text().toInt();

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

void Widget::on_btnSend_clicked()
{
    //获取编辑区内容
    QString str = ui->textEditWrite->toPlainText();

    if(str.isEmpty())
    {
        return;
    }

    QString msg = str;

    //给对方发送数据,使用套接字是tcpSocket
    tcpSocket->write(msg.toUtf8().data());

    //清空编辑区内容
    ui->textEditWrite->clear();
}

void Widget::on_btnClose_clicked()
{
    //主动和对方断开连接
    tcpSocket->disconnectFromHost();
    tcpSocket->close();
}

void Widget::on_update(QString str)
{
    ui->textEditRead->append(str);
}

main,cpp

#include "widget.h"

#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    Widget w;
    w.show();
    return a.exec();
}

 

6. TCP群聊

多对多聊天(聊天室)

运行效果:

核心思路:把接收到的消息发送给每个客户端,客户端连接服务器后使用tcpSocketList保存起来,方便后续转发。

    //将收到的信息发送给每个客户端,从套接字列表中找到需要接收的套接字
    for(int i = 0; i < tcpSocketList.count(); i++)
    {
        QTcpSocket *item = tcpSocketList.at(i);
//        if(item->write((char*)msg.toUtf8().data(), length) != length)
//        {
//            continue;
//        }
        item->write(msg.toUtf8().data());
    }

 

6.1 TCP服务器

此服务器也是采用(incomingConnection方法)实现。

tcpserver.h

#ifndef TCPSERVER_H
#define TCPSERVER_H

#include <QTcpServer>
#include <QObject>
#include "tcpsocket.h"
#include <QList>

class TcpServer : public QTcpServer
{
    Q_OBJECT //为了实现信号和槽的通信

public:
    TcpServer(QObject *parent = 0, int port = 0);

public:
    virtual void incomingConnection(qintptr socketDescriptor) override;//只要出现一个新的连接,就会自动调用这个函数

private:
    QList<TcpSocket*> tcpSocketList;

signals:
    void updateserver(QString, int);//发送信号给界面,让界面更新信息

public slots:
    void sliotupdateserver(QString, int);//用来处理tcpclient发过来的信号
    void slotclientdisconnect(int);

};

#endif // TCPSERVER_H

tcpserver.cpp

#include "tcpserver.h"
#include "tcpsocket.h"

TcpServer::TcpServer(QObject *parent, int port):QTcpServer(parent)
{
    this->listen(QHostAddress::Any, port); //监听
    if(this->isListening())
    {
        qDebug() << "isListening";
    }
}

void TcpServer::incomingConnection(qintptr socketDescriptor)
{

    TcpSocket *tcpsocket = new TcpSocket();//只要有新的连接就生成一个新的通信套接字
    //将新创建的通信套接字描述符指定为参数socketdescriptor
    tcpsocket->setSocketDescriptor(socketDescriptor);
    qDebug() << socketDescriptor << " " << tcpsocket->socketDescriptor();

    //获取对方的IP和端口
    QString ip = tcpsocket->peerAddress().toString();
    qint16 port = tcpsocket->peerPort();
    QString temp = QString("[%1 : %2]: 成功连接").arg(ip).arg(port);
    qDebug()<<temp;


    //将这个套接字加入客户端套接字列表中
    tcpSocketList.append(tcpsocket);


    //接收到tcpclientsocket发送过来的更新界面的信号
    connect(tcpsocket, &TcpSocket::updateserver, this, &TcpServer::sliotupdateserver);
    connect(tcpsocket, &TcpSocket::clientdisconnected, this, &TcpServer::slotclientdisconnect);
}

void TcpServer::sliotupdateserver(QString msg, int length)
{
    //将这个信号发送给界面
    //emit updateserver(msg, length);

    //将收到的信息发送给每个客户端,从套接字列表中找到需要接收的套接字
    for(int i = 0; i < tcpSocketList.count(); i++)
    {
        QTcpSocket *item = tcpSocketList.at(i);
//        if(item->write((char*)msg.toUtf8().data(), length) != length)
//        {
//            continue;
//        }
        item->write(msg.toUtf8().data());
    }

}

void TcpServer::slotclientdisconnect(int descriptor)
{
    for(int i = 0; i < tcpSocketList.count(); i++)
    {
        QTcpSocket *item = tcpSocketList.at(i);
        if(item->socketDescriptor() == descriptor)
        {
            tcpSocketList.removeAt(i);//如果有客户端断开连接, 就将列表中的套接字删除
            return;
        }
    }
    return;
}

tcpsocket.h

#ifndef TCPSOCKET_H
#define TCPSOCKET_H

#include <QTcpSocket>

class TcpSocket: public QTcpSocket
{
    Q_OBJECT //添加这个宏是为了实现信号和槽的通信
public:
    TcpSocket(QObject *parent = 0);


signals:
    void updateserver(QString, int);//用来告诉tcpserver需要跟新界面的显示
    void clientdisconnected(int); //告诉server有客户端断开连接

protected slots:
    void recvdata();//处理readyRead信号读取数据
    void slotclientdisconnected();//客户端断开连接触发disconnected信号,这个槽函数用来处理这个信号
};

#endif // TCPSOCKET_H

tcpsocket.cpp

#include "tcpsocket.h"
#include <QHostAddress>

TcpSocket::TcpSocket(QObject *parent):QTcpSocket(parent)
{

    //客户端发送数据过来就会触发readyRead信号
    connect(this, &TcpSocket::readyRead, this, &TcpSocket::recvdata);
    connect(this, &TcpSocket::disconnected, this, &TcpSocket::slotclientdisconnected);
}

void TcpSocket::recvdata()
{
    int length = 10;
    QByteArray array = readAll();
    QString msg = array;
    emit updateserver(msg, length);
}

void TcpSocket::slotclientdisconnected()
{
    QTcpSocket *obj = (QTcpSocket*)sender();//掉线对象
    QString ip = obj->peerAddress().toString();
    quint16 port = obj->peerPort();

    QString temp = QString("客户端:%1 %2断开了连接!").arg(ip).arg(port);
    qDebug()<<temp;//打印出掉线对象的ip
}

widget.h

#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include "tcpserver.h"

QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE

class Widget : public QWidget
{
    Q_OBJECT

public:
    Widget(QWidget *parent = nullptr);
    ~Widget();

private slots:
    void on_pushButton_clicked();

private:
    Ui::Widget *ui;

    int port;
    TcpServer* m_tcpserver;
};
#endif // WIDGET_H

widget.cpp

#include "widget.h"
#include "ui_widget.h"


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

    port = 8888;
    m_tcpserver = NULL;
}

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


void Widget::on_pushButton_clicked()
{
    if(m_tcpserver == NULL)
    {
        m_tcpserver  = new TcpServer(this, port);
    }

    if(m_tcpserver)
    {
        ui->textEdit->setText("服务器启动成功!");
    }
    else
    {
        ui->textEdit->setText("服务器启动失败!");
    }
}

6.2 TCP客户端

注:和4.2的客户端代码一样。或者也可以使用5.2的客户端代码。

widget.h

#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include <QTcpSocket>

QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE

class Widget : public QWidget
{
    Q_OBJECT

public:
    Widget(QWidget *parent = nullptr);
    ~Widget();

private slots:
    void slotsConnected();//成功和服务器建立好连接
    void slotsReadyRead();//读取服务器发来的数据
    void on_btnConnect_clicked();
    void on_btnSend_clicked();
    void on_btnClose_clicked();


private:
    Ui::Widget *ui;

    QTcpSocket* tcpSocket;

    QString userName;
};
#endif // WIDGET_H

widget.cpp

#include "widget.h"
#include "ui_widget.h"

#include <QHostAddress>

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

    setWindowTitle("Client");
    ui->textEditRead->setReadOnly(true);

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

    connect(tcpSocket,SIGNAL(connected()),this,SLOT(slotsConnected()));
    connect(tcpSocket,SIGNAL(readyRead()),this,SLOT(slotsReadyRead()));
}

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

void Widget::slotsConnected()
{
    ui->textEditRead->setText("成功和服务器建立好连接!");
}

void Widget::slotsReadyRead()
{
    //从通信套接字中取出内容
    QByteArray array = tcpSocket->readAll();

    ui->textEditRead->append(array);
}

void Widget::on_btnConnect_clicked()
{
    //获取服务器Ip和端口
    QString ip = ui->lineEditIP->text();
    qint16 port = ui->lineEditPort->text().toInt();
    userName = ui->lineEdit_userName->text();

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

void Widget::on_btnSend_clicked()
{
    //获取编辑区内容
    QString str = ui->textEditWrite->toPlainText();

    if(str.isEmpty())
    {
        return;
    }

    QString msg = userName + ":" + str;

    //给对方发送数据,使用套接字是tcpSocket
    tcpSocket->write(msg.toUtf8().data());

    //清空编辑区内容
    ui->textEditWrite->clear();
}

void Widget::on_btnClose_clicked()
{
    //主动和对方断开连接
    tcpSocket->disconnectFromHost();
    tcpSocket->close();
}

 

posted @   [BORUTO]  阅读(922)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示