广播通信设计——WinSock编程(QT界面)

广播通信设计

一、设计要求

  • 设计要求是通过学习winSock API编程,实现局域网消息广播的应用程序。
  • 系统采用CS架构的方式,具有服务端和客户端。完成的功能有私聊、群聊和私聊时的文件传输,私聊和文件传输采用TCP协议,群聊采用UDP协议。服务端只作为私聊信息的记录和转发,以及在线用户的更新。文件传输和聊天功能工作方式不同。文件传输的服务器由客户端提供,文件传输的客户端由系统客户端的私聊模块提供。而聊天功能的服务器由系统服务端提供,聊天功能的客户端由系统客户端提供。

二、开发环境与工具

  • 开发系统:Win10
  • 开发工具:Qt 5.6
  • 开发语言:C++

三、设计原理

  • 系统的设计采用了TCP协议和UDP协议,TCP作为面向连接的传输,传输的信息是可靠的,所以在系统的设计中选择TCP协议来作为用户一对一私聊和文件传输。UDP是面向无连接的协议,它本身是适合广播信息的,所以使用UDP协议来进行群聊的实现。
  • 本系统的设计是使用Qt实现的,而非纯粹的c++语言。这么说的原因在于Qt里的一些函数是对C++语言进行封装过的,就拿这次要使用的WinSock编程来说,函数的使用是有区别的,在这里只介绍Qt中的WinSock编程方法(包括TCP协议编程和UDP协议编程)。
  • 先说TCP编程,分为服务端和客户端。在服务端需要设置两个套接字,一个是监听套接字QTcpServer,一个是通信套接字QTcpSocket。监听套接字在设置为监听状态后可以和客户端连接,连接后服务端可以在连接中取出通信套接字来进行数据的传送。在客户端需要设置通信套接字QTcpSocket,该套接字要主动与服务端连接才能进行通信。编程原理如图3.1所示。
    在这里插入图片描述
  • 其次是UDP编程,也分为服务端与客户端,相对于TCP编程来说,UDP编程要简单些,他没有面向连接的过程,只需要接收广播的信号即可。客户端和服务端都需要设置一个通信套接字QUdpSocket,不同的在于服务端的套接字需要绑定才能工作,然后才能发送接收数据。UDP编程原理如图3.2所示。

在这里插入图片描述

四、系统功能描述及软件模块划分

  • 系统所实现的主要功能是用户一对一的私聊和文件传输,以及用户之间的群聊。并设定每台pc机只能注册一个用户,使用的是本地IP地址,一台pc机多次申请连接会报错(IP地址重复)。
  • 软件包含两大主要模块,一是客户端模块 chat_client,二是服务端模块chat_server,分别说下两大模块。
  • 首先是客户端模块 chat_client,该模块下面有四个小模块,分别是主模块 widget、私聊模块 chat、文件发送进度显示模块 filesend 和文件接收进度显示模块 filercv,主要说主模块 widget 和私聊模块 chat。主模块的功能有文件传输服务端TCP配置(用来接收其他用户发来的文件)、与服务端模块chat_server的定时连接(防止连接不上的情况)、处理UDP收到的广播消息(包括处理服务端返回的信息处理以及群聊信息的处理)、接收服务器返回的私聊TCP信息(服务端中转私聊信息)、获取本地IP地址(系统采用本地唯一IP地址进行用户注册)、以及私聊群聊方式的设置和群聊信息的发送功能。接着是私聊模块 chat,私聊模块可以说是主模块的子模块,实现的功能包括发送自己的私聊消息和接收对方的私聊消息并显示,除此之外,就是作为文件传输的客户端。
  • 然后是服务端模块 chat_server,该模块没有子模块,就一个主模块 widget。主模块的功能除了服务端界面的布局外,还有作为私聊TCP信息的服务端(接收用户传来的私聊信息并解析显示出内容,作为中转站将信息转发给目的IP)、处理UDP信息(包括处理新加用户和退出用户信息)、更新现有用户信息并广播该信息给客户端模块更新数据。

五、设计步骤
本次系统的设计主要是为了实现三个功能,一是使用TCP协议完成用户一对一的私聊;二是使用UDP协议完成广播信息的处理;三是使用TCP协议完成用户一对一之间的文件传输。

  • 先说使用UDP协议实现群聊广播信息的处理。在本系统的设计中,UDP协议除了处理广播数据外,还为单播数据工作。设计中有三个地方用到了UDP协议,一是用户名的返回(系统的用户名由服务端自动产生发送给客户端);二是服务端广播更新后的用户数据;三是群聊广播信息的处理,对三种信息的处理是通过标志位来实现判断的。系统中使用UDP协议的只有客户端的主模块widget和服务端的主模块widget。UDP协议实现的服务端就在系统服务端主模块中。该协议实现的功能主要流程图如图5.1和图5.2所示。

在这里插入图片描述
在这里插入图片描述
图5.1:UDP协议在客户端的主要函数流程

在这里插入图片描述
图5.2:UDP协议在服务端的主要函数流程

  • 再说使用TCP协议实现用户一对一私聊的功能实现。私聊信息是在客户端模块 chat_client里的子模块chat中发出的(由子模块widget创建的chat对象实现),然后发给服务端模块 chat_server进行中转,最后由客户端模块 chat_client里的子模块widget接收(接收的内容也是在由子模块widget创建的chat对象中显示)。该功能实现的主要流程图如图5.3所示。

在这里插入图片描述
图5.3:实现私聊功能的主要函数流程

  • 最后是使用TCP协议进行文件数据的传输。文件传输的客户端是在客户端模块下的子模块chat中定义的,服务端是在客户端模块下的主模块widget中定义的,也就是说文件传输功能没有用到系统的服务端,所有功能都是在系统客户端中实现的。直接作用体现在当用户1在给用户2发送文件时,用户1使用私聊模块chat选择文件进行发送,用户2使用主模块widget对对方发送的文件进行确认和接收。该功能实现的主要函数流程如图5.4所示:
    在这里插入图片描述
    图5.4:文件传输主要函数流程

六、关键问题及其解决方法

  • 设计过程中主要有两个关键问题。第一个是UDP广播信息的处理,因为在设计过程中需要广播几类信息,需要将每类信息加上标记位,也就是封装不同的信息,除此之外用户更新信息是一次性全部发出,用户接受后需要对信息进行判断,以及解析,就如同要封装成数据报一样先广播在接受一点点解析。如何封装和解析成了一个难题,首先是所有用户信息的储存(每个用户对应一个用户名和一个IP地址),我查资料找到了QT中“#include ”容器头文件的用法,他可以将每个用户按照“键:值”的方式一起存储。接着是找到了“#include ”流头文件的用法,他可以将没一个信息按照流的方式存储,然后在解析,实现了UDP广播信息的处理。
  • 第二个关键问题是文件传输中出现了问题,文件传输有时候文件内容过多(文件太大)或出现内容丢失导致接收的文件出现损坏,然后我想到了将文件按每份4k的内容块进行传输,知道传完。
  • 代码实现后,虽说文件传输的最大内容有所提升,但还是在接收大文件时出现了文件内容丢失文件损坏的情况,这个是个遗憾,未能解决。

七:部分源码

  • 服务器源码(widget.h):
#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include <QTcpServer>        //监听套接字头文件
#include <QTcpSocket>
#include <QUdpSocket>        //通信套接字头文件
#include <QMap>              //容器头文件

namespace Ui {
class Widget;
}

class Widget : public QWidget
{
    Q_OBJECT

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

    void upDateMsg();       //更新用户信息

private:
    Ui::Widget *ui;
private slots:

    void udp_ReadyRead();
    void process_Data(void);                 //tcp 消息处理函数
    void tcp_ReadyRead();

private:

    QString getIP();        //得到本地IP地址

private:

    int usrNum;         //用户总数

    QTcpServer *tcpServer;
    QTcpSocket  *tcpSocket;
    QUdpSocket *udpSocket;

    int tPort;          //TCP端口

    QMap<QString,QString>  usrMap;
    QMap<QString,QTcpSocket*> tcpusr;

};

#endif // WIDGET_H

  • 服务器源码(widget.cpp):
#include "widget.h"
#include "ui_widget.h"
#include <QDebug>
#include <QList>
#include <QNetworkInterface>
#include <QTableWidget>
#include <QMessageBox>
#include <QHostAddress>
#include <QDataStream>      //数据流头文件

Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget)
{
    ui->setupUi(this);
    ui->textBrowser->setText("开始!");

    usrNum = 0;

    //处理用户表格
    QStringList header;
    header<<tr("用户名")<<tr("IP地址");
    ui->tableWidget->setHorizontalHeaderLabels(header);
    ui->tableWidget->setSelectionMode(QAbstractItemView::SingleSelection);
    ui->tableWidget->setSelectionBehavior(QAbstractItemView::SelectRows);
    ui->tableWidget->show();

    //udp初设设置,允许其他服务绑定同样的地址和端口
    udpSocket =new QUdpSocket(this);
    udpSocket->bind(5555,QUdpSocket::ShareAddress|QUdpSocket::ReuseAddressHint);

    //udp消息处理函数
    connect(udpSocket,SIGNAL(readyRead()),this,SLOT(udp_ReadyRead()));

    //QTcpserver设置
    tcpSocket =new QTcpSocket(this);
    tcpServer=new QTcpServer(this);

    //有新连接请求时执行
    connect(tcpServer, SIGNAL(newConnection()),this,SLOT(process_Data()));
    tPort = 6666;
    if (!tcpServer->listen(QHostAddress::Any,tPort))
    {
        qDebug() << tcpServer->errorString();
        close();
        return;
    }

}

//!析构函数,释放申请的内存
Widget::~Widget()
{
    delete ui;
    usrMap.clear();
    tcpusr.clear();
}

//*消息处理函数
//.完成得功能有:
//1.新用户注册处理
//2.用户离开处理
//3.用户转发消息处理


//tcp 连接成功触发newConnection ,取出套接字
void Widget::process_Data()
{
    //获得当前套接字
    tcpSocket = tcpServer->nextPendingConnection();
    //qDebug()<<"收到一个新的客户连接\r\n客户端的IP:"<<tcpSocket->peerAddress().toString();
    QString ip = tcpSocket->peerAddress().toString();
    quint16 port = tcpSocket->peerPort();
    QString str = QString("收到一个新的客户连接客户端的IP:[%1],端口号:[%2]").arg(ip).arg(port);
    ui->textBrowser->append(str);

    //获得对方IP并储存套接字
    tcpusr.insert(tcpSocket->peerAddress().toString().mid(7),tcpSocket);

    connect(tcpSocket,SIGNAL(readyRead()),this,SLOT(tcp_ReadyRead()));
}


//连接请求处理函数
void Widget::tcp_ReadyRead()
{
    QTcpSocket * tcp ;
    QTcpSocket * tcp_to ;

    //获得那个信号过来的
    tcp=qobject_cast <QTcpSocket*>(sender());
    //qDebug()<<"即将转发一条消息\r\n"<<"消息来自:"<<tcp->localAddress();
    QString ip = tcp->peerAddress().toString();
    QString str = QString("即将转发一条消息,消息来自:[%1]").arg(ip);
    ui->textBrowser->append(str);

    //消息可读
    if(tcp->isReadable())
    {
        quint16 blockSize = 0;
        QByteArray array = tcp->readAll();
        array.resize(array.size());
        QDataStream in(&array,QIODevice::ReadOnly);

        QByteArray data;
        QDataStream out(&data,QIODevice::WriteOnly);

        if (blockSize == 0) {

            in >> blockSize;
        }

        /*  MsgType:信息类型
        *   fromname:源用户名称
        *   fromip:源IP地址
        *   msg:转发内容
        *   toname:目标用户名称
        *   toip:目的IP地址
        */
        QString MsgType,fromname,fromip,msg,toname,toip;
        in>>MsgType>>fromname>>fromip>>msg>>toname>>toip;


        if(MsgType == "transfer")
        {
            //可能是fromip
            tcp_to = tcpusr.value(fromip);

            //把来源和信息发送过去
            out<<fromname<<fromip<<msg<<toname<<toip;
            tcp_to->write(data);

            //qDebug()<<"消息转发完成\r\n"<<"内容:"<<msg<<"转发的目的地址:"<<toip;
            QString str = QString("消息转发完成,内容:[%1]转发的目的地址:[%2]").arg(msg).arg(toip);
            ui->textBrowser->append(str);
        }
    }
}



//udp消息处理函数
//功能:处理新加如的用户和断开连接的用户
void Widget::udp_ReadyRead()
{
    while(udpSocket->hasPendingDatagrams())
    {
        QByteArray datagram;
        QByteArray data;
        QDataStream out(&data,QIODevice::WriteOnly);
        out.setVersion(QDataStream::Qt_5_6);

        datagram.resize(udpSocket->pendingDatagramSize());

        //读取UDP消息
        udpSocket->readDatagram(datagram.data(),datagram.size());

        //读io流管理
        QDataStream in(&datagram,QIODevice::ReadOnly);

        //取出信息类型
        qint16 msgType;
        in>>msgType;

        switch(msgType)
        {
            //处理新用户加入信息
            case (qint16)2:
            {
                usrNum++;
                QString ip;
                QString usrName;
                in>>ip;

                //防止重复IP加入
                foreach (QString value,  usrMap)
                {
                   if(value==ip)
                   {
                      if (QMessageBox::Yes==QMessageBox::warning(this,tr("提示"),tr("有重复的IP地址,请检查!")
                       ,QMessageBox::Yes))
                      {
                        return;
                      }

                   }
                }

                //分配用户名
                usrName =tr("Usr_") +QString::number(usrNum);

                //返还服务器信息  QByteArray data
                out<<(qint16)3<<usrName<<getIP();

                //存入容器
                usrMap.insert(usrName,ip);

                //获得在线用户的总数,放入数据
                out<<usrMap.size();

                //Map的迭代器
                QMapIterator<QString, QString> i(usrMap);

                //迭代器返回所有用户
                while (i.hasNext())
                {
                    i.next();
                    out << i.key() << i.value();
                }

                QHostAddress addr;
                addr.setAddress(ip);

                //发送数据
                udpSocket->writeDatagram(data,data.length(),QHostAddress::Broadcast,5555);
                //udpSocket->writeDatagram(data,data.length(),addr,5555);

                //服务器数据存储
                ui->tableWidget->insertRow(0);
                ui->tableWidget->setItem(0,0,new QTableWidgetItem(usrName));
                ui->tableWidget->setItem(0,1,new QTableWidgetItem(ip));
                upDateMsg();
                break;
             }

            //客户断开连接,清除注册的信息
             case (qint16)4:
            {
                 QString name;
                 in>>name;

                 tcpusr[usrMap.value(name)]->disconnectFromHost();
                 tcpusr[usrMap.value(name)]->close();
                 tcpusr[usrMap.value(name)]->abort();
                 tcpusr.remove(usrMap.value(name));
                 usrMap.remove(name);

                 //清空用户列表
                 ui->tableWidget->setRowCount(0);

                 //Map的迭代器
                 QMapIterator<QString, QString> i(usrMap);

                 //刷新用户信息
                 while (i.hasNext())
                 {
                     i.next();

                     //服务器重新数据存储
                     ui->tableWidget->insertRow(0);
                     ui->tableWidget->setItem(0,0,new QTableWidgetItem(i.key()));
                     ui->tableWidget->setItem(0,1,new QTableWidgetItem(i.value()));
                 }
                 upDateMsg();
                 break;

            }
               default:{break;}
            }
        }
}


//更新现有的用户
void Widget::upDateMsg()
{
    QByteArray data;
    QDataStream out(&data,QIODevice::WriteOnly);

    //获得在线用户的总数,放入数据
    out<<(qint16)6<<(qint16)usrMap.size();

    //Map的迭代器
    QMapIterator<QString, QString> i(usrMap);

    //刷新用户信息
    while (i.hasNext())
    {
        i.next();
        out << i.key() << i.value();
    }

    //Map的迭代器
    QMapIterator<QString, QString> j(usrMap);

    //发送数据
    udpSocket->writeDatagram(data,data.length(),QHostAddress::Broadcast,5555);

    //刷新用户信息
    while (j.hasNext())
    {
        j.next();
        QHostAddress addr;
        addr.setAddress(j.value());
        udpSocket->writeDatagram(data,data.length(),addr,5555);
    }
}

//本地IP地址
QString  Widget:: getIP()
{
    QList<QHostAddress> addrs = QNetworkInterface::allAddresses();
    for(int i = 0; i < addrs.size(); i++)
    {
        if (addrs.at(i).protocol() == QAbstractSocket::IPv4Protocol &&
                addrs.at(i) != QHostAddress::Null &&
                addrs.at(i) != QHostAddress::LocalHost &&
                !addrs.at(i).toString().contains(QRegExp("^169.*$")))
        {
            QString a;
             a = addrs.at(i).toString();
            return addrs.at(i).toString();
        }
    }
}
  • 客户端私聊窗口源码(chat.h):
#ifndef CHAT_H
#define CHAT_H

#include <QDialog>
#include<QTcpSocket>
#include<widget.h>
#include<chat.h>
#include<QTime>
#include<QFile>
#include"filesend.h"

namespace Ui {
class chat;
}

class chat : public QDialog
{
    Q_OBJECT
public:
    explicit chat(QWidget *parent = 0);
    ~chat();

    void showMsg();             //将接受到的消息弹出显示在窗口

public:

    QString chat_ip;            //对方的IP
    QString chat_name;          //对方的名称
    QString server_ip;          //服务器的IP
    QString ipself;             //自己的IP
    QString nameself;           //自己的名称
    QTcpSocket  *tClnt;         //私聊用的TCP通信套接字
    QString msg_receive;        //接收的对方聊天内容
    QTcpSocket *fileTcpSocket;  //发送文件用的通信套接字

private:

    qint16 totalBytes;          //选择传送文件的大小
    qint64 fileNameSize;        //文件名字长度
    qint64 bytesTowrite;        //剩余未发送的文件大小
    qint64 byteWritten;         //已传送的文件大小
    qint64 loadSize;            //文件每次传送的大小

    QFile *locFile;             //文件操作
    QByteArray outBlock;        //发送数据缓存区
    QString fileName;           //传送的文件名字
    FileSend *filesnd;          //文件传输界面

private:
    Ui::chat *ui;

private slots:
    void on_sendBtn_clicked();
    void on_exitBtn_clicked();
    void on_FileSendBtn_clicked();
    void updateData(qint64);        //发送剩余文件
    void isOK();                    //是否收到回复信息
};

#endif
  • 客户端私聊窗口源码(chat.cpp):
#include "chat.h"
#include "ui_chat.h"
#include<widget.h>
#include<QMessageBox>
#include<QDebug>
#include<QFile>
#include<QFileDialog>


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

    //给输入框输入箭头
    ui->textInput->setFocus();

    //4K大小的文件
    loadSize =200*1024;
    setWindowFlags(Qt::WindowMinimizeButtonHint);

}


//将接受到的消息弹出显示在窗口
void chat::showMsg()
{
    QString time = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");
    ui->MsgBrowser->setAlignment(Qt::AlignLeft);
    ui->MsgBrowser->setTextColor(Qt::blue);
    ui->MsgBrowser->setCurrentFont(QFont("Times New Roman",10));
    ui->MsgBrowser->append("["+chat_name+"--"+time+"]");
    ui->MsgBrowser->append(msg_receive+"\r\n");
    this->setWindowTitle(chat_name);
}

chat::~chat()
{
    delete ui;

}


//发送按钮
//获取编译区的内容并封装,发给服务器端
void chat::on_sendBtn_clicked()
{
    QByteArray outBlock;
    QString time = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");
    QDataStream out(&outBlock,QIODevice::WriteOnly);
    out.setVersion(QDataStream::Qt_5_6);

    //读取信息
    QString msg =ui->textInput->toPlainText();
    if(msg.isEmpty())
    {
        QMessageBox::warning(0,tr("warning"),tr("发送的消息不能为空!"),
        QMessageBox::Ok);
        return ;
    }

    //马上清空方面下一次输入
    ui->textInput->clear();

    //将发送内容显示在内容显示栏的右边
    ui->MsgBrowser->setAlignment(Qt::AlignRight);
    ui->MsgBrowser->setTextColor(Qt::green);
    ui->MsgBrowser->setCurrentFont(QFont("Times New Roman",10));
    ui->MsgBrowser->append("["+nameself+"--"+time+"]");
    ui->MsgBrowser->append(msg+"\r\n");



    //设置发送的数据的长度
    out.device()->seek(0);
    out << (quint16) 0;

    //设置发送内容
    //消息类型为消息转发
    QString a("transfer");
    out<<a;
    //自己的昵称
    out<<nameself;
    //自己得IP
    out<<ipself ;
    //发送的消息
    out<<msg;
    //发送的目的昵称
    out<<chat_name;
    //送的目的ip
    out<<chat_ip;

    //回到字节流起始位置  重置字节流长度
    out.device()->seek(0);
    out << (quint16) (outBlock.size()-sizeof(quint16));

    //发送消息给服务器让其中转
    tClnt->write(outBlock);


}

//文件传输按钮
void chat::on_FileSendBtn_clicked()
{

    QString fileTosendName;

    //创建文件传输的TcpSocket,文件的端口为7777
    fileTcpSocket =new QTcpSocket;

    fileName = QFileDialog::getOpenFileName(this);


    //获得文件按名字后开始传送
    if (!fileName.isEmpty())
    {
        fileTosendName = fileName.right(fileName.size()-fileName.lastIndexOf('/')-1);


       locFile =new QFile(fileName);
       if(!locFile->open(QFile::ReadOnly))
       {
           QMessageBox::warning(this,tr("应用程序"),tr("无法读取文件%1:\n%2.")
                                .arg(fileTosendName)
                                .arg(locFile->errorString()));
       }

       totalBytes =locFile->size();
       QDataStream sendOut(&outBlock,QIODevice::WriteOnly);
       QString currentFile =fileName.right(fileName.size()-
                                           fileName.lastIndexOf('/')-1);
       sendOut<<qint64(0)<<qint64(0)<<currentFile;

       qDebug()<<"namesize:"<<currentFile;

       totalBytes += outBlock.size();
       qDebug()<<"totalBytes:"<<totalBytes<<qint64((outBlock.size()-sizeof(qint64)*2));

       sendOut.device()->seek(0);
       sendOut<<(qint64)totalBytes<<qint64((outBlock.size()-sizeof(qint64)*2));

       //开始连接客户端的IP,等待对方响应
       fileTcpSocket->connectToHost(chat_ip,7777);

       //最多等待20s
       if(fileTcpSocket->waitForConnected(20000))
       {
           qDebug()<<"文件传输端口已经连接上对方服务器!";
           connect(fileTcpSocket,SIGNAL(bytesWritten(qint64)),this,SLOT(updateData(qint64)));
           connect(fileTcpSocket,SIGNAL(readyRead()),this,SLOT(isOK()));
       }

       else
       {
           qDebug()<<"无法连接对方的服务器!";
           return;
       }
}

}

//收到确认接收信息后开始发送文件
void chat::isOK()
{
    qDebug()<<"收到了确认消息:";
    QString msg;
    QDataStream in(fileTcpSocket);
    in>>msg;
    if(msg =="IamReady")
    {
        qDebug()<<"收到了确认消息:"<<"IamReady";

        //发送出去,等待响应
        bytesTowrite=totalBytes - fileTcpSocket->write(outBlock);
        qDebug()<<"bytesTowrite:"<<bytesTowrite;
        outBlock.resize(0);

        //建立文件传输界面
        filesnd =new FileSend;
        filesnd->LabelState("等待对方接收……");
        filesnd->show();
    }
    if(msg =="Refuse")
    {
         QMessageBox::information(this,tr("提示"),tr("对方拒绝了您的文件请求"),QMessageBox::Yes);
    }

}


//发送后续的文件
void chat::updateData(qint64 numBytes)
{
    byteWritten += (int)numBytes;
    qDebug()<<"bytesTowrite:"<<bytesTowrite;
    if(bytesTowrite>0)
    {
        outBlock = locFile->read(qMin(bytesTowrite,loadSize));
        bytesTowrite -= (int)fileTcpSocket->write(outBlock);
        outBlock.resize(0);

        //更新进度条
        filesnd->showProcess(totalBytes,byteWritten);
        filesnd->LabelState(tr("已发送 %1Mb")
                            .arg(byteWritten/(1024*1024)));
        filesnd->show();
    }
    else
    {
        locFile->close();
        filesnd->close();
        QMessageBox::information(this,tr("提示"),tr("文件传送成功"),QMessageBox::Yes);
        fileTcpSocket->disconnectFromHost();
        fileTcpSocket->close();
    }
}

//退出按钮槽函数
void chat::on_exitBtn_clicked()
{

    accept();
    close();
}

  • 客户端主窗口源码(widget.cpp):
#include "widget.h"
#include "ui_widget.h"
#include<QMessageBox>
#include"chat.h"
#include<QDebug>
#include<QFileDialog>
#include<QTime>

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

    tClnt = new QTcpSocket(this);

    //初始化变量和窗口状态
    flag = 0;
    fileNameSize = 0;
    bytesReceived = 0;

    //udp端口
    port = 5555;
    setWindowTitle(tr("聊天工具"));
    this->resize(650,550);
    ui->groupChatBtn->setText(tr("开始群聊"));
    ui->usrNameLine->setText(tr("正在连接服务器……"));
    ui->groupBox->hide();
    ui->groupBox->hide();

    setWindowFlags(Qt::WindowMinimizeButtonHint);

    //定时器
    timer = new QTimer(this);

    // 两秒钟后重连接服务器
    timer->start(2000);
    connect(timer,SIGNAL(timeout()),this,SLOT(recall(void)));

    //客户端udp初始化
    udpSocket = new QUdpSocket;
    udpSocket->bind(port,QUdpSocket::ShareAddress|QUdpSocket::ReuseAddressHint);
    connect(udpSocket,SIGNAL(readyRead()),this,SLOT(udp_ReadyRead()));

    //文件传输服务器配置
    fileTcpServer = new QTcpServer(this);
    connect(fileTcpServer, SIGNAL(newConnection()),this,SLOT(file_NewConc()));

    //占用端口7777
    if (!fileTcpServer->listen(QHostAddress::Any,7777))
    {
        qDebug() << fileTcpServer->errorString();
        close();
        return;
    }

}


//!析构函数,清除内存
Widget::~Widget()
{
    delete ui;

    delete udpSocket;
    delete timer;
    delete fileTcpServer;
    delete fileSocket;
    dialogChat.clear();
    delete locFile;
}


//文件接收连接
void Widget::file_NewConc()
{
     fileSocket = fileTcpServer->nextPendingConnection();
     int btn = QMessageBox::information(this,tr("接收文件"),tr("来自 %1的文件 ,是否接收?")
                                        .arg(fileSocket->peerAddress().toString().mid(7)),
                                        QMessageBox::Yes,QMessageBox::No);
      if(btn == QMessageBox::Yes)
      {
          QByteArray block;
          QDataStream out(&block,QIODevice::WriteOnly);

          //定义文件消息标志
          QString msg = "IamReady";
          out<<msg;

          //发送确认消息,准备接收
          fileSocket->write(block);
          qDebug()<<"确认消息已经发出!";
          connect(fileSocket,SIGNAL(readyRead()),this,SLOT(fileRcv()));

          //新建一个发送显示窗口
          filercv =new FileRcv;

      }
      else
      {
          //拒绝文件传输请求
          QByteArray block;
          QDataStream out(&block,QIODevice::WriteOnly);
          QString msg = "Refuse";
          out<<msg;
          fileSocket->write(block);
          qDebug()<<"确认消息已经发出!";
          fileSocket->disconnectFromHost();
          fileSocket->close();

      }
}


//文件接受槽函数
void Widget::fileRcv()
{
    qDebug()<<"开始接收文件";
    QDataStream in(fileSocket);
    if(fileSocket->bytesAvailable() >= sizeof(qint64)*2
            &&fileNameSize==0)
    {

        in>>TotalByte>>fileNameSize;
        qDebug()<<"文件大小"<<TotalByte<<"文件名长度"<<fileNameSize;

        bytesReceived += sizeof(qint64)*2;

    }
    if(fileSocket->bytesAvailable() >= fileNameSize
            &&fileNameSize != 0)
    {

        in>>fileName;
        qDebug()<<"文件名字"<<fileName;
        bytesReceived += fileNameSize;
        QString name = QFileDialog::getSaveFileName(0,tr("保存文件"),fileName);
        locFile = new QFile(name);

        if(!locFile->open(QFile::WriteOnly))
        {
            QMessageBox::warning(this,tr("应用程序"),tr("无法写入文件 %1:\n%2.")
                                 .arg(fileName)
                                 .arg(locFile->errorString()));
            return;
        }
    }

    //接收过程
    if(bytesReceived < TotalByte)
    {
        bytesReceived += fileSocket->bytesAvailable();
        inBlock = fileSocket->readAll();
        locFile->write(inBlock);
        inBlock.resize(0);
    }

    //实时更新进度条
    filercv->showProcess(TotalByte,bytesReceived);
    filercv->showMsg(tr("已接收%1MB")
                     .arg(bytesReceived/(1024*1024)));

    filercv->show();

    if(bytesReceived == TotalByte)
    {
        qDebug()<<"接收完成!";
        QMessageBox::information(this,tr("提示"),tr("文件接收成功"),QMessageBox::Yes);
        locFile->close();
        filercv->close();
        fileSocket->disconnectFromHost();
        fileSocket->close();
    }

}

//连接服务器重连接函数
//每次重播时间为2秒
void Widget::recall()
{
     static int times = 0;
     QByteArray data;
     QDataStream out(&data,QIODevice::WriteOnly);
     QString iplocal =getIP();

     //写入自己得IP地址
     out<<(qint16)2<<iplocal;
     times++;
     qDebug()<<tr("进入中断");
     if(0 == flag)
     {
         udpSocket->writeDatagram(data,data.length(),QHostAddress::Broadcast,port);
     }
     else
     {
          timer->stop();
          times = 0;
     }

     if(2 == times)
     {
         timer->stop();
         times = 0;
         if(QMessageBox::Yes == QMessageBox::warning(this,tr("提示"),tr("请先检查服务器是否连接,或者本地网络接入,是否重新连接?"),
                              QMessageBox::Yes,QMessageBox::No))
         {
            timer->start(2000);

         }
         else
         {
             close();
         }
    }
}


//处理udp收到的广播消息
//1.对服务器返回的信息进行处理
//2.处理群聊信息
//3.接受服务器各种广播消息
void Widget::udp_ReadyRead()
{
    qDebug()<<"收到UDP返回的广播消息";
    while(udpSocket->hasPendingDatagrams())
    {
        QByteArray datagram;
        //QByteArray data;
        //QDataStream out(&data,QIODevice::WriteOnly);

        datagram.resize(udpSocket->pendingDatagramSize());
        udpSocket->readDatagram(datagram.data(),datagram.size());
        QDataStream in(&datagram,QIODevice::ReadOnly);
        in.setVersion(QDataStream::Qt_5_6);

        qint16 msgType;
        in>>msgType;

        switch(msgType)
        {
        //该类型为服务器的返回消息类型
        case 3:
        {
            //获得服务器分配的用户名
            in>>usrName;
            ui->usrNameLine->setText(usrName);

            //获得服务器地址
            in>>serverIP;
            qDebug()<<tr("服务器IP地址:")<<serverIP;

            //标志位置1,通知重播窗口连接上服务器,关闭定时器
            flag = 1;
            timer->stop();

            //获得所有在线用户信息
            int num =0;
            in>>num;

            //建立Tcp连接
            tClnt->connectToHost(serverIP,6666);

            if(tClnt->waitForConnected(2000))
            {
                qDebug()<<"客户端以Tcp方式连接上服务器";
                connect(tClnt,SIGNAL(readyRead()),this,SLOT(tcp_ReadyRead()));
            }
            else
            {
                qDebug()<<"无法连接连接上服务器!";
            }
            break;
        }

        //提交时注意加上来自自己消息的屏蔽
        case 5:
        {
            QString msg,name;
            in>>msg;
            in>>name;
            if(name != usrName){
                QString time = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");
                if(!(ui->groupBox->isHidden())){
                    ui->groupMsg->setAlignment(Qt::AlignLeft);
                    ui->groupMsg->setTextColor(Qt::blue);
                    ui->groupMsg->setCurrentFont(QFont("Times New Roman",10));
                    ui->groupMsg->append("["+name+"--"+time+"]");
                    ui->groupMsg->append(msg+"\r\n");

                }
            }
            break;
        }

        //更新在线用户
        case 6:
        {
            qint16 size;
            in>>size;
            ui->allUsrTable->setRowCount(0);
            for(int i=0; i<size; i++)
            {
                QString usr,ip;
                in>>usr;
                in>>ip;

                //将在线用户信息显示在表格
                ui->allUsrTable->insertRow(0);
                ui->allUsrTable->setItem(0,0,new QTableWidgetItem(usr));
                ui->allUsrTable->setItem(0,1,new QTableWidgetItem(ip));
            }

        }
        }
    }
}



//接受服务器返回的Tcp消息处理函数
void Widget::tcp_ReadyRead()
{
    // QTextCodec *tc=QTextCodec::codecForName("UTF-8");
    qDebug()<<tr("接受到了服务器返回信息!");
    QByteArray data = tClnt->readAll();
    QDataStream in(&data,QIODevice::ReadOnly);

    QString fromUsr,ip,msg,toUsr,toip;
    in>>fromUsr>>ip>>msg>>toUsr>>toip;

    chat *mychat;
    char flag = 1;

    //首先检查要传达的窗口数据是否存在
    QList< chat *>::iterator i;
    for(i = dialogChat.begin(); i != dialogChat.end(); ++i)
    {
        if((*i)->chat_ip == ip)
        {
           mychat = *i;
           flag = 0;
           qDebug()<<"已经存在窗口";
           mychat->msg_receive = msg;
           mychat->showMsg();
           mychat->show();
           break;
        }
    }

    //如果窗口不存在就自己新建
    if(flag)
    {
        qDebug()<<"新建窗口";
        mychat = new chat(this);
        mychat->chat_ip = ip;
        mychat->chat_name = fromUsr;
        mychat->server_ip = serverIP;
        mychat->nameself = usrName;
        mychat->ipself = getIP();
        mychat->tClnt = tClnt;
        mychat->msg_receive = msg;
        mychat->showMsg();
        dialogChat.append(mychat);
        mychat->show();

    }

}



//.获得本机IP地址
QString  Widget:: getIP()
{
    QList<QHostAddress> addrs = QNetworkInterface::allAddresses();
    for(int i = 0; i < addrs.size(); i++)
    {
        if (addrs.at(i).protocol() == QAbstractSocket::IPv4Protocol &&
                addrs.at(i) != QHostAddress::Null &&
                addrs.at(i) != QHostAddress::LocalHost &&
                !addrs.at(i).toString().contains(QRegExp("^169.*$")))//正则表达式,不包含169开头的默认无网络连接IP
        {
             qDebug() << addrs.at(i).toString();
            return addrs.at(i).toString();
        }
    }
}


//双击表格后弹出聊天窗口
void Widget::on_allUsrTable_cellDoubleClicked(int row, int column)
{
    QString usr;
    QString IP;
    usr = ui->allUsrTable->item(row,0)->text();
    IP = ui->allUsrTable->item(row,1)->text();

    //新对话框需要的参数信息
    chat *mychat = new chat;
    mychat->chat_ip = IP;
    mychat->chat_name = usr;
    mychat->server_ip = serverIP;
    mychat->nameself = usrName;
    mychat->ipself = getIP();
    mychat->tClnt = tClnt;
    mychat->msg_receive = " ";
    mychat->showMsg();
    dialogChat.append(mychat);
    mychat->show();
//    if(mychat->exec()==QDialog::Accepted)
//    {
//        dialogChat.removeOne(mychat);
//        delete mychat;
//        mychat =NULL;
//    }
}



//退出按钮槽函数
void Widget::on_exitBtn_clicked()
{
    QByteArray data;
    QDataStream out(&data,QIODevice::WriteOnly);
    out<<(qint16)4<<usrName;//写入自己得IP地址
    qDebug()<<tr("用户退出");
    udpSocket->writeDatagram(data,data.length(),QHostAddress::Broadcast,port);
    tClnt->abort();
    tClnt->disconnectFromHost();
    tClnt->close();
    fileTcpServer->close();
    close();
}


//群聊功能
void Widget::on_groupChatBtn_clicked()
{
    static int flag =1;
    flag = 1 - flag;
    if(flag == 1){
    ui->groupBox->hide();

    ui->groupChatBtn->setText(tr("开始群聊"));
    }
    else
    {
        ui->groupBox->show();

        ui->groupChatBtn->setText(tr("关闭群聊"));
    }

}

//群聊发送按钮槽函数
void Widget::on_SendBtn_clicked()
{
    QByteArray outBlock;

    QDataStream out(&outBlock,QIODevice::WriteOnly);
    //out.setVersion(QDataStream::Qt_5_6);
    QString msg =ui->groupChatInput->toPlainText();

    //信息发送不能为空判断
    if(msg.isEmpty())
    {
        QMessageBox::warning(0,tr("warning"),tr("发送的消息不能为空!"),
                             QMessageBox::Ok);
        return ;
    }
    ui->groupChatInput->clear();

    QString time = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");

    //将发送内容显示在内容显示栏的右边
    ui->groupMsg->setAlignment(Qt::AlignRight);
    ui->groupMsg->setTextColor(Qt::green);
    ui->groupMsg->setCurrentFont(QFont("Times New Roman",10));
    ui->groupMsg->append("["+usrName+"--"+time+"]");
    ui->groupMsg->append(msg+"\r\n");

    //加上消息类型后发送出去
    out<<qint16(5)<<msg<<usrName;
    //  QHostAddress addr;
    //  addr.setAddress(tr("192.168.43.12"));//这里必须用指定的IP不然不可以发出消息,不知为何
    // udpSocket->writeDatagram(outBlock,outBlock.length(),addr,5555);//发送数据

    udpSocket->writeDatagram(outBlock,outBlock.length(),QHostAddress::Broadcast,5555);
}

完整项目源码文件:https://download.csdn.net/download/WXY19990803/12450932

八、设计结果
软件正常运行是先打开服务端,再打开客户端,服务端界面如图7.1所示,客户端界面如图7.2所示。
在这里插入图片描述
图7.1:服务端界面
在这里插入图片描述
图7.2:客户端界面
在先后打开服务端和客户端后,服务端会有消息显示已有用户连接,并返回自动生成的用户名在客户端显示(Usr_1)。私聊功能只需要在客户端双击你想私聊的用户即可进入私聊界面,如图7.3所示。
在这里插入图片描述
图7.3:私聊界面

思考题:简述单播、广播、多播的区别与联系,以及各自的优缺点和适应范围。

  • 单播:主机之间一对一的通讯方式,网络中的交换机和路由器对数据只进行转发不进行复制。如果10个客户机需要相同的数据,则服务器需要逐一传送,重复10次相同的工作。但由于其能够针对每个客户的及时响应,所以现在的网页浏览全部都是采用单播模式,具体的说就是IP单播协议。网络中的路由器和交换机根据其目标地址选择传输路径,将IP单播数据传送到其指定的目的地。
  • 单播优点:服务器能够及时响应客户的请求,能够对不同的用户实现个性化服务。
  • 单播缺点:当用户量大时,容易造成网络阻塞
  • 广播:主机之间一对所有的通讯方式,网络对其中每一台主机发出的信号都进行无条件复制并转发,所有主机都可以接收到所有信息(不管你是否需要),由于其不用路径选择,所以其网络成本可以很低廉。有线电视网就是典型的广播型网络。
  • 广播优点:网络建立和维护简单,服务器流量负载低。
  • 广播缺点:无法实现对每个用户进行及时的个性化服务。
  • 多播(组播):主机之间一对一组的通讯模式,也就是加入了同一个组的主机可以接受到此组内的所有数据,网络中的交换机和路由器只向有需求者复制并转发其所需数据。
  • 多播优点:具备单播和广播的优点,最具有发展前景。
  • 多播缺点:与单播相比没有纠错机制,发送丢包错包后难以弥补。

八、软件使用说明
软件中有两个工程文件,一个是客户端工程文件chat_client,一个是服务端工程文件chat_server,使用QT打开各个工程文件的.pro文件即可。在正常使用软件时需要先打开服务端在打开客户端,否则先打开客户端会出现出错提示,注意,软件设计时是采用一台PC机只能注册一个用户的原则,采用的是唯一IP地址进行注册,但没有限制自己与自己私聊和传输文件,目的在于测试。在正常打开软件后,如果要私聊,双击用户列表中的用户即可进入私聊界面,在私聊界面中可以进行文件传输。如果要群聊,点击“开始群聊”按钮即可进入群聊界面。退出软件点击“退出”即可。服务端打开后是不需要任何操作的,他自动显示用户一对一之间的私聊信息,但是不能显示文件传输信息和群聊信息。

九、参考资料
书籍资料:
[1]张会勇. WinSock网络编程经络. 电子工业出版社,2012.
[2]王艳平. Windows网络与通信程序设计. 人民邮电出版社,2009
[3]唐文超. Visual C++网络编程. 清华大学出版社,2013

视频资料:
[1]网址:https://www.bilibili.com/video/av20446734?p=1


看完觉得有帮助就顺手点个赞呗^_^ !!!
在这里插入图片描述

  • 5
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值