• 【QT开发(14)】QT P2P chat 聊天


    【P2P学习(2)】P2P 通信,主要存在四种不同的网络模型的第一阶段:集中式P2P 模式

    最简单的路由方式就是集中式,即存在一个中心节点保存了其他所有节点的索引信息,索引信息一般包括节点 IP 地址、端口、节点资源等。集中式路由的优点就是结构简单、实现容易。但缺点也很明显,由于中心节点需要存储所有节点的路由信息,当节点规模扩展时,就很容易出现性能瓶颈;而且也存在单点故障问题。

    一般是有一个服务器,和一群客户端。各个客户端可以两两相互发送消息。各个客户端用IP地址和TCP/IP监听端口号进行标识。客户端,可执行注册和聊天过程;注册服务器,主要用于注册客户端和分发注册客户端信息。

    技术点:

    • GUI
    • 多线程
    • 套接字编程

    1 QT 项目引入库cmake

    认识Qt5::Network
    Qt中提供的所有的Socket类都是非阻塞的。

    QTcpSocket 用于TCP/IP通信,作为客户端套接字使用。
    QTcpServer 用于TCP/IP通信,作为服务器端套接字使用。
    QUdpSocket 用于UDP通信,服务器,客户端均使用此套接字。

    文档资料:

    官方文档: Qt Network C++ Classes:https://doc.qt.io/qt-5/qtnetwork-module.html
    C++ QTcpSocket::waitForConnected方法代码示例:https://vimsky.com/examples/detail/cpp-ex—QTcpSocket-waitForConnected-method.html
    
    • 1
    • 2

    CMake下添加,在CMakelists.txt文件中添加以下代码

    #设置Qt的支持
    set(QT5_LIBRARIESQt5::Network)
    #自动查找配置构建工程所需的程序库
    find_package(Qt5Network REQUIRED)
    #添加可执行文件所需要的库
    target_link_libraries(${PROJECT_NAME} ${QT5_LIBRARIES} Qt5::Network)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    然后,引入相关类的头文件:

    #include 
    #include 
    #include 
    
    • 1
    • 2
    • 3

    2 TCP/IP协议

    TCP/IP: TCP(Transmission Control Protocol,传输控制协议),IP(Internet Protocol 网络协议)。TCP提供一种机制可以让发送端根据接收端的实际接收能力控制发送的数据量,是一种流控制。认识TCP可通过以下方面。

    • TCP是一种面向连接的可靠的协议,属于传输层。只有在确认通信对端存在时才会发送数据,从而控制通信流量的浪费。
    • TCP特性: 既保证可靠性,又尽可能的提高性能。
      保证可靠性的机制: 校验和,序列号(按序到达),确认应答,超时重传,连接管理,流量控制,拥塞控制。
      提高性能的机制: 滑动窗口,快速重传,延迟应答,捎带应答。
      定时器: 超时重传定时器,保活定时器,TIME_WAIT定时器。
    • 基于 TCP 的应用层协议: HTTP,HTTPS,SSH,Telnet,FTP,SMTP,基于TCP的自定义的应用层协议。
    • TCP 和 UDP 对比: TCP用于可靠传输的情况, 应用于文件传输, 重要状态更新等场景。UDP用于对高速传输和实时性要求较高的通信领域,例如, 早期的QQ, 视频传输等.;UDP可以用于广播。

    2.1 基础:Socket接口

    Socket又称"套接字",用于描述IP地址和端口,是一个通信链的句柄,可以用来实现不同虚拟机或不同计算机之间的通信。

    • TCP/IP提供了封装或者显示数据的具体形式,即可供程序员做网络开发所用的接口。
      socket提供了网络通信的能力,其本质是对TCP/IP的封装的编程接口(API)。
      建立网络通信连接至少要一对Socket端口。
      TCP/IP如果是轿车的话,Socket就是发动机。

    2.2 基础:QTcpServer

    服务器监听客户端流程

    1.创建套接字QTcpServer
    2.监听指定的地址和端口号(周期性线程调用)
    3.监听是否有Client连接
    4.停止监听close
    5.通过监听到的QTcpSocket收发数据
    6.获取服务器监听状态

    创建套接字

    QTcpServer* server = new QTcpServer();
    
    • 1

    监听指定的地址和端口号,即进入Sever启动监听等待Client连接状态

    bool QTcpServer::listen(const QHostAddress &address = QHostAddress::Any, 
    					    quint16 port = 0);
    
    • 1
    • 2

    监听是否有Client连接

    QTcpSocket* QTcpServer::nextPendingConnection();
    
    • 1

    注意,返回的QTcpSocket只能再本线程使用;如果需要在别的线程管理该QTcpSocket,则需要重写QTcpServer::incomingConnection(),将QTcpSocket描述符传递给其他线程并再创建QTcpSocket。

    void QTcpServer::incomingConnection(qintptr handle);
    
    • 1

    停止监听

    void QTcpServer::close();
    
    • 1

    收发数据
    根据第3不中返回的QTcpSocket指针,调用read()和write()接口。

    其他辅助接口

    获取己方和对方的IP和port

    quint16 QTcpServer::serverPort() const;
    QHostAddress QTcpServer::serverAddress() const;
    
    • 1
    • 2

    获取服务器监听状态

    bool QTcpServer::isListening() const;
    
    • 1

    ServerListen 断开重连机制

    static int count = 0;
    void UserServerlient::Listen() {
    		//1.tcpServer的断开重连
        if (!tcpClient) { return; }
        if (isListening() != lastState) {
            count++;
            //服务器尝试重新监听某个地址和端口
            ServerListening();
            //判断当前服务器监听状态
            if (isListening()) {
                std::cout << localIP << "/" << localPort << " 断开重连count: " << count;
                count = 0;
            }
        }
        //数字10为断开重连次数,根据设置的线程周期时间和实际需求设置
        if (count >= 10) {
            lastState = false;
            delete this;
            count = 0;
            return;
        }
        //2.检测连接到的Client
        if (!child_client) {
            child_client = server->nextPendingConnection(); //得到每个连进来的socket
            if (!child_client) {return;}
        }
        //检测到Client断开连接,清理内存
        if (child_client->state() == QAbstractSocket::UnconnectedState) {
        	……
        }
    }    
    
    • 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

    2.3 基础:QTcpSocket

    客户端通信流程

    1.创建套接字QTcpSocket
    2.连接服务器,使用 QTcpSocket::connectToHost()
    3.向服务器发送数据 QTcpSocket::write()
    4.接收服务器数据 QTcpSocket::readAll()
    5.断开Socket连接等接口
    6.获取己方和对方的IP和port,Socket状态等;

    创建套接字

    QTcpSocket* socket = new QTcpSocket();
    
    • 1

    客户端连接到服务器端

    // ### Qt6: de-virtualize connectToHost(QHostAddress) overload
    void QAbstractSocket::connectToHost(const QString &hostName, quint16 port, 
    						 		    OpenMode mode = ReadWrite, 
    						   	  		NetworkLayerProtocol protocol = AnyIPProtocol);
    void QAbstractSocket::connectToHost(const QHostAddress &address, quint16 port, 
    						  		    OpenMode mode = ReadWrite);
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    连接成功或断开服务器端,或触发connected()和disconnected()信号

    Q_SIGNALS:
    	void QAbstractSocket::hostFound();
    	void QAbstractSocket::connected();
    	void QAbstractSocket::disconnected();
    	void QAbstractSocket::stateChanged(QAbstractSocket::SocketState);
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    连接成功后发送数据。返回值:数据信息的长度。

    qint64 QIODevice::write(const char *data, qint64 len);
    qint64 QIODevice::write(const char *data);
    qint64 QIODevice::write(const QByteArray &data);
    
    
    • 1
    • 2
    • 3
    • 4

    连接成功接收数据。返回值:数据信息info

    qint64 QIODevice::read(char *data, qint64 maxlen);
    QByteArray QIODevice::read(qint64 maxlen);
    QByteArray QIODevice::readAll();
    
    
    • 1
    • 2
    • 3
    • 4

    当有新的数据到来时,会触发readyRead()信号。因此,要时刻检测数据有数据发送过来,此处应该放在一个循环中,如timer中。

    Q_SIGNALS:
        void QIODevice::readyRead();
        void QIODevice::aboutToClose();
    
    
    • 1
    • 2
    • 3
    • 4

    关闭TCP连接

    //会等待数据被写完之后关闭套接字连接
    void QAbstractSocket::disconnectFromHost();
    //关闭套接字的I/O设备并调用disconnectFromHost()来关闭套接字的连接。
    void QIODevice::close() override;     
    //调用close(),终止当前连接并重置套接字。与disconnectFromHost()不同,这个函数会立即关闭套接字,丢弃写缓冲区中任何未处理的数据。     
    void QAbstractSocket::abort(); 
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    其他辅助接口

    获取己方和对方的IP和port

    quint16 QAbstractSocket::localPort() const;
    quint16 QAbstractSocket::peerPort() const;
    QHostAddress QAbstractSocket::localAddress() const;
    QHostAddress QAbstractSocket::peerAddress() const;
    QString QAbstractSocket::peerName() const;
    
    • 1
    • 2
    • 3
    • 4
    • 5

    获取Socket状态

    SocketState QAbstractSocket::state();
    enum QAbstractSocket::SocketState {
        UnconnectedState,   //未连接状态
        ConnectedState,     //连接状态
    };
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    ClientListen 断开重连机制

    static int count = 0;
    void UserSocketlient::Listen() {
        if (!tcpClient) { return; }
        if (CurrentSocketConnectState() != lastState) {
            count++;
            //客服端尝试连接服务器
            SocketConnecttoSever();
            //判断当前Socket连接状态
            if (CurrentSocketConnectState()) {
                std::cout << localIP << "/" << localPort << " 断开重连count: " << count;
                count = 0;
            }
        }
        //数字10为断开重连次数,根据设置的线程周期时间和实际需求设置
        if (count >= 10) {
            lastState = false;
            delete this;
            count = 0;
            return;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    3 开发P2P 的中心服务器

    用途:
    1、接受客户端的加盟,然后记录客户端的列表信息
    2、分发列表信息的端口信息给全体客户端,这样客户端就可相互联系了,不需要中心服务器的支持。
    3、实时更新列表,有些下线的,超时的用户踢出去,然后再次广播列表。

    3.1 具体流程

    参考了adomy/P2PChatRoom;
    在这里插入图片描述

    4 客户端

    在这里插入图片描述

    5打包程序

    【QT开发(13)】QT发布到其他ubuntu用户

    5.1 演示

    在这里插入图片描述

    仓库

    https://gitee.com/hiyanyx/p2-pcha

    参考

    链接:https://blog.csdn.net/qq_43572400/article/details/128261772

  • 相关阅读:
    【C基础篇】之数据类型
    微信小程序根据开发环境切换域名
    web3.js:使用eth包
    第二章 进程与线程 十六、信号量机制
    pytest+yaml实现接口自动化框架
    章节三:RASA Domain介绍
    HTML学生个人网站作业设计成品 HTML+CSS肖战明星人物介绍网页 web结课作业的源码
    Idea报错 java: 程序包org.springframework.boot不存在 解决方法
    Flowable 之任务分配
    浅析Java反射机制
  • 原文地址:https://blog.csdn.net/djfjkj52/article/details/134026874