Fdog系列(六):利用Qt通过服务端进行客户端与客户端通信(资料少,建议收藏)

2021-05-06 16:45:52 浏览数 (1)

  • (1)服务端的实现
  • (2)客户端的实现

一. 前言

Fdog已写文章目录:

Fdog系列(一):思来想去,不如写一个聊天软件,那就从仿QQ注册页面开始吧。

Fdog系列(二):html写完注册页面之后怎么办,用java写后台响应呀。

Fdog系列(三):使用腾讯云短信接口发送短信,数据库写入,部署到服务器,web收尾篇。

Fdog系列(四):使用Qt框架模仿QQ实现登录界面,界面篇。

Fdog系列(五):使用Qt模仿QQ实现登录界面到主界面,功能篇

Fdog系列(六):Qt实现客户端与客户端通过服务端进行互相通信 当前篇

所有文章源码已整体打包上传至github,求星星!


先上图

关于QT通信网上能搜到的,基本都是清一色的客户端到服务端的通信,至于客户端与客户端的通信,少之又少,我反正是基本找不到,要有也只是给一张图,博主也是想了很长时间,也算是想出来了,所以当你看到这里,我再次强烈建议你收藏。

关于客户端与服务端之间的通信,这个在csdn倒是有很多资料的,基本内容都一样,如果不了解客户端到服务端之间的通信,可以先去复习一下。


二. 正文

1. 客户端与客户端通信思路

先简单叙述一下单纯的客户端到服务端通信的流程

第一步:服务端:

设置ip 设置端口 开始监听

第二步:客户端:

设置ip 设置端口 发送TCP请求

经过三次握手之后,客户端建立与服务端的通信,这就是单纯的客户端和服务端建立通信过程。

现在我们要做的就是要多个用户可以连接服务端,并且通过服务端进行客户端与客户端的通信。

这里就大大加大了难度,单纯的客户端与服务端通信,无需考虑或者说是识别是那个用户,因为只是作为一个例子出现。

以QQ举例

  1. 一个QQ就是一个客户端,服务端就要考虑识别是哪个用户,要知道一条信息是来自哪个客户端并发送给哪一个客户端的。
  2. 一个QQ又有N个好友,一个客户端收到的消息又要考虑如何到达正确的好友聊天窗口。

这都是本篇接下来将要表达的,但是这篇只考虑双方在线的情况下进行通信,不考虑不在线的情况,关于不在线的情况,客户端如何在上线后继续接受消息,这一功能可以交给数据库来做,本篇暂不在讨论。


在一个简单的客户端与服务端通信例子中,服务端无需识别用户,因为用户唯一,那么多个客户端登录如何识别?

可以使用ip吗,不行,想一想,当两个客户端在同一台电脑登录时,ip将是相同,如何做到唯一值?

使用 账号 IP 端口 确定唯一值,来说一说为什么需要三个组合值才能确定客户端。

A,B作为客户端,S作为服务端

A,B在同一台电脑登录,假设IP值都是10.13.128.122

仅使用IP无法识别,再加一个端口号呢?

这个端口号并不是客户端与服务端通信时设置的端口号,而是服务端为每一个请求连接的客户端分配的闲置端口号。

IP 端口号还是不能识别?接着往下看

要让服务器知道一条信息是来自哪个客户端并发送给哪一个客户端的,可以在客户端发送信息之前给消息加上前缀。

A的账号是12345678,B的账号是11111111

A要给B发一句”你好“,这条信息在发送给服务端之前,被加工成为“1111111112345678你好”。

前8位为目的地账号,再往后8位为发送者账号。

服务端分配的随机端口号只有服务器知道,客户端是无法获取,或者只能让服务器传回去。

所以服务端收到的信息是”1111111112345678你好“,服务端通过解析知道了要发生给11111111这个账号,也就是B,并将前8位截掉。

但是问题来了,服务端怎么知道11111111对应哪个客户端,哪个IP,哪个端口号,所以除了IP,端口,还需要一个账号才能确定唯一用户。

客户端第一次连接服务端,将自己的账号发送,表明身份,服务端将账号,IP,生成的端口保存在数据库,服务端知道要发生给11111111这个账号,就去数据库查询该账号对应的端口号,因为IP可能相同,但是端口号不会相同,至此服务端正确的将信息发送给B。

所以B接受到的信息为“12345678你好”。

B与服务端的连接是存在于主界面的,所以是B的主界面收到一条信息“12345678你好”。

但是B有很多好友,每一个好友都是一个聊天窗口,如何将消息显示在正确对应的窗口,就要用到信息的前8位,表示这条信息要显示在标记为12345678的窗口。

至此整个流程完成。

读完之后读者可能有疑问,服务器怎么连接多个客户端,这里是单纯的连接,普通的连接是服务端监听请求,有请求就创建套接字,所以有几个请求,就创建几个套接字就可以实现多个客户端连接。


2. 代码实现

(1)服务端的实现

省略ui变量操作,头文件

代码语言:javascript复制
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include"usersql.h"
#include<QMainWindow>
#include<QTcpServer>
#include<QTcpSocket>
#include<QList>
namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT
private:
    usersql sqconn;             //连接数据库
    QTcpServer * tcpServer;     //TCP服务器
    QList <QTcpSocket *> tcpSocket;//TCP通信的Socket
    QList <bool> isfrist;       //判断是否第一条消息
    QString getLocalIP();       //获取本机IP地址
public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();

private slots:
    void onNewConnection();  //有新的请求会调用
    
    //void onSocketStateChange(QAbstractSocket::SocketState socketState); 
    //显示连接状态 暂不需要,可通过stateChanged信号调用
    
    void onClientConnected(int);	//连接成功调用
    void onClientDisconnected(int);	//连接断开调用
    void onSocketReadyRead(int);	//有消息调用

private:
    Ui::MainWindow *ui;
};

#endif // MAINWINDOW_H

再来看cpp文件

代码语言:javascript复制
//构造函数
MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
	ui->setupUi(this);
	tcpServer = new QTcpServer(this);
    connect(tcpServer,SIGNAL(newConnection()),this,SLOT(onNewConnection()));//绑定槽函数,当有新的连接请求就会调用该函数
	//开始监听
    QString IP = getLocalIP();//服务器IP
    quint16 port = ui->spinBox->value();//获取界面端口 也就是60
    QHostAddress addr(IP);
    tcpServer->listen(addr,port);
    ui->plainTextEdit->appendPlainText("**开始监听...");
    ui->plainTextEdit->appendPlainText("**服务器地址:"
                    tcpServer->serverAddress().toString());
    ui->plainTextEdit->appendPlainText("**服务器端口:"
                    QString::number(tcpServer->serverPort()));
    ui->pushButton->setEnabled(false);
    ui->pushButton_2->setEnabled(true);
    LabListen->setText("监听状态:正在监听");
};


void MainWindow::onNewConnection()
{
    QTcpSocket * tcpSocket = new QTcpSocket();
    tcpSocket = tcpServer->nextPendingConnection();
    this->tcpSocket.append(tcpSocket);
    this->isfrist.append(false);
    onClientConnected(this->tcpSocket.length()-1);

    QSignalMapper * myMapper1 = new QSignalMapper(this);
    connect(tcpSocket, SIGNAL(disconnected()), myMapper1, SLOT(map()));
    myMapper1->setMapping(tcpSocket,this->tcpSocket.length()-1);
    connect(myMapper1, SIGNAL(mapped(int)), this, SLOT(onClientDisconnected(int)));
    
    //connect(tcpSocket,SIGNAL(stateChanged(QAbstractSocket::SocketState)),
    //        this,SLOT(onSocketStateChange(QAbstractSocket::SocketState)));
            
    QSignalMapper * myMapper2 = new QSignalMapper(this);
    connect(tcpSocket, SIGNAL(readyRead()), myMapper2, SLOT(map()));
    myMapper2->setMapping(tcpSocket,this->tcpSocket.length()-1);
    connect(myMapper2, SIGNAL(mapped(int)), this, SLOT(onSocketReadyRead(int)));
}

//void MainWindow::onSocketStateChange(QAbstractSocket::SocketState socketState)
//{
//    switch (socketState) {
//    case QAbstractSocket::UnconnectedState:
//        LabSocketState->setText("scoket状态:UnconnectedState");
//        break;
//    case QAbstractSocket::HostLookupState:
//        LabSocketState->setText("scoket状态:HostLookupState");
//        break;
//    case QAbstractSocket::ConnectingState:
//        LabSocketState->setText("scoket状态:ConnectingState");
//        break;
//    case QAbstractSocket::ConnectedState:
//        LabSocketState->setText("scoket状态:ConnectedState");
//        break;
//    case QAbstractSocket::BoundState:
//        LabSocketState->setText("scoket状态:BoundState");
//        break;
//    case QAbstractSocket::ClosingState:
//        LabSocketState->setText("scoket状态:ClosingState");
//        break;
//    case QAbstractSocket::ListeningState:
//        LabSocketState->setText("scoket状态:ListeningState");
//        break;
//    default:
//        break;
//    }
//}


void MainWindow::onClientConnected(int i)
{
    ui->plainTextEdit->appendPlainText("**clinet socket connected");
    ui->plainTextEdit->appendPlainText("**peer address" 
                                       tcpSocket[i]->peerAddress().toString());
    ui->plainTextEdit->appendPlainText("**peer port:" 
                                       QString::number(tcpSocket[i]->peerPort()));
    ui->listWidget->addItem(tcpSocket[i]->peerAddress().toString() "  " QString::number(tcpSocket[i]->peerPort()));
}

void MainWindow::onClientDisconnected(int i)
{
    ui->plainTextEdit->appendPlainText("**client socket disconnected");
    tcpSocket[i]->deleteLater();
    this->tcpSocket.removeAt(i);
    this->isfrist.removeAt(i);
    //对列表做删除操作
    ui->listWidget->takeItem(i);
}

void MainWindow::onSocketReadyRead(int i)
{
    if(this->isfrist[i]==false)
    {
        //获取连接账号
        QString account = tcpSocket[i]->readLine();
        //写入数据库
        qDebug()<<"写入数据库";
        sqconn.update(account,QString::number(tcpSocket[i]->peerPort()));
        this->isfrist[i]=true;
    }
    else
    {
        while(tcpSocket[i]->canReadLine())
        {
            //获取内容
            QString data = tcpSocket[i]->readLine();
            //data.mid(0,8);为到达方 data.mid(8,8);为发送方
            //遍历列表ip是否登录
            //遍历数据库查询该帐户
            QString account = data.mid(0,8);
            qDebug()<<"目标帐户为:"<<account;
            //通过该帐户找到ip地址,然后发送信息
            QString port = sqconn.AccountIP(account); //查询数据库
            for(int i =0;i<this->tcpSocket.length();i  )
            {
                if(QString::number(tcpSocket[i]->peerPort())==port)//说明在线
                {   //将数据转发给到达方,并保留发送方帐户,用于窗口识别
                    QByteArray str = data.mid(8).toUtf8();
                    tcpSocket[i]->write(str);
                    qDebug()<<"目标端口号:"<<tcpSocket[i]->peerPort();
                }
            }
        }
    }
}

QString MainWindow::getLocalIP()
{
    //获取本机IPv4地址
    QString hostName = QHostInfo::localHostName();//本机主机名
    QHostInfo hostInfo = QHostInfo::fromName(hostName);
    QString localIP="";
    QList<QHostAddress> addList = hostInfo.addresses();
    if(!addList.isEmpty())
    {
        for(int i = 0;i<addList.count();i  )
        {
            QHostAddress aHost = addList.at(i);
            if(QAbstractSocket::IPv4Protocol==aHost.protocol())
            {
                localIP = aHost.toString();
                break;
            }
        }
    }
   return localIP;
}

(2)客户端的实现

主界面头文件

代码语言:javascript复制
class MainWindow : public QMainWindow
{
    Q_OBJECT

private:
    QList<Chat *>  listchat; //自定义的聊天窗口类
    QTcpSocket * tcpClient;//连接网络
};

主界面cpp文件

代码语言:javascript复制
MainWindow::MainWindow(QString account,QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);
	tcpClient=new QTcpSocket(this);
	//连接到服务器
    QString addr =getLocalIP();
    quint16 port = 60;
    tcpClient->connectToHost(addr,port);

    connect(tcpClient,SIGNAL(connected()),
            this,SLOT(onConnected()));
    connect(tcpClient,SIGNAL(disconnected()),
            this,SLOT(onDisconnected()));
    //connect(tcpClient,SIGNAL(stateChanged(QAbstractSocket::SocketState)),
    //        this,SLOT(onSocketStateChange(QAbstractSocket::SocketState)));
    connect(tcpClient,SIGNAL(readyRead()),
            this,SLOT(onSocketReadyRead()));
}

void MainWindow::onConnected()
{
    //连接成功之后第一次发送自己的帐户
    QByteArray straccount = this->account.toUtf8();
    tcpClient->write(straccount);
}

void MainWindow::onDisconnected() //断开连接
{
    //ui->plainTextEdit->appendPlainText("已断开服务器的连接");

}

void MainWindow::onSocketReadyRead()//收到消息
{
    while (tcpClient->canReadLine()) {
        QString data = tcpClient->readLine();
        for(int i = 0;i<listchat.length();i  )
        {
        	//传给聊天界面,这里要确定传给哪个界面
            if(listchat[i]->getOtheraccount()==data.mid(0,8))
            {
                listchat[i]->setIsread(true); //给聊天界面一个值 其他了解界面根据这个值的真假来判断是否在该窗口显示信息
                emit sendChatData(data);//这个信号的绑定在聊天窗口
            }
        }
    }
}

聊天窗口

代码语言:javascript复制
//构造函数
Chat::Chat(QString account,QString name,MainWindow * main,QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Chat)
{
    ui->setupUi(this);
    this->otheraccount=account;
    this->name=name;
    tcpClient=new QTcpSocket(this);
    //接收主窗口的消息信号
    mainwindow = main;
    connect(mainwindow,SIGNAL(sendChatData(QString)),this,SLOT(onSocketReadyRead(QString)));
}

void Chat::onSocketReadyRead(QString data)
{
    if(this->isread==true) //开始判断isread值
    {
        ui->plainTextEdit->appendPlainText(data);
        this->isread=false;
    }
}

//聊天窗口发送消息
void Chat::on_pushButton_3_clicked()
{
    QDateTime curDateTime=QDateTime::currentDateTime();
    //数据格式为: 到达方账号,发送方账号,内容
    QString msg = this->getOtheraccount() this->getAccount() ui->lineEdit->text();
    //ui->plainTextEdit->appendPlainText(msg); 文本框显示发送消息
    //ui->lineEdit->clear();  清空文本框
    //ui->lineEdit->setFocus();//设置文本框焦点
    QByteArray str = msg.toUtf8();
    //发送信号
    emit sendData(str); //发送信息给主窗口
}

主窗口绑定信号槽

代码语言:javascript复制
connect(a,SIGNAL(sendData(QString)),this,SLOT(MainSendData(QString)));

void MainWindow::MainSendData(QString str)
{
    QByteArray strdata = str.toUtf8();
    strdata.append('n');
    tcpClient->write(strdata);
}

最后说一下代码中的信号槽

无论是父子窗口还是两个独立窗口,信号槽都是可以使用的,

A想调用B的函数,就在A里面发送信息,在B里面连接信号槽即可。


所有文章源码已整体打包上传至github,求星星!

重要的东西说两遍,资料少,建议收藏!

最后IU镇楼


0 人点赞