广播通信设计
一、设计要求
- 设计要求是通过学习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
看完觉得有帮助就顺手点个赞呗^_^
!!!