资源描述
.
河北金融学院信息管理与工程系课程设计报告
题 目: 局域网聊天工具
学生姓名: 张依 学 号: 20111811016
系别班级:信息管理与工程系11级计算机科学
与技术本科班
专业(方向): 计算机科学与技术
指导者教师: 姜志旺
完 成 时 间: 2013年 5 月 16 日
目录
1. 任务与要求………………………………………………………………………3
1.1设计任务……………………………………………………………………… 3
1.2具体要求……………………………………………………………………… 3
2. 系统总体设计……………………………………………………………………4
2.1 设计目标及完成功能…………………………………………………………4
2.2 系统结构设计…………………………………………………………………4
3. 系统详细设………………………………………………………………………6
3.1窗口设计………………………………………………………………………7
3.2接收消息模块…………………………………………………………………16
3.3用户信息………………………………………………………………………18
4.测试………………………………………………………………………………20
5.课程设计与总结…………………………………………………………………25
6.参考文献…………………………………………………………………………25
1 任务与要求
1.1设计任务
本课程设计是利用可视化编程库QT进行一个可视化的网络通信程序设计,主要采用QUdpSocket、QTcpSocket、QTcpServer等类。
1.2具体要求
1.2.1系统的需求分析
① 能够发送消息到不同的用户程序。
② 用户程序能够接收来自不同用户程序的消息,并能识别出是哪个用户。
③ 通过协议可以找到局域网里面的用户。
④ 根据具体情况进行发挥,努力完善网络通讯程序。
1.2.2 完成系统设计
找出系统的对象,抽象完成分析类图的创建,根据情况画出顺序图,协作图,状态图,部署图,组件图,活动图。针对具体的思想语言要求写出具体的实现类图,类的属性和服务,标出类之间的关系。
1.2.3编码
完成需要编码完成的模块。
1.2.4测试
编写合适的测试用例完成系统的测试工作并分析结果
2 系统总体设计
2.1系统设计目标及完成功能
本项目的设计目标为一个高性能的,易于使用的,面向校园内部通信需求的局域网即时通信软件。它应具有如下特征:
1、具有高性能,可同时处理多个连接请求。
2、对硬件要求低,适应范围广,运行稳定。
3、具有一定的容错性能。
当用户登入聊天室时,用户输入的内容直接发送到其他有登入此聊天室的用户,用户与用户直接通信不需要经过服务器。
最终的软件产品应具有如下功能:
(1) 能够随时改变自己的昵称。
(2) 能够自动更新其他用户的名单及在线人数。
(3) 随时获取系统的当前时间。
(4) 能够向其他用户传输文件。
(5) 能够保存或者删除聊天记录。
(6) 能够改变聊天的字体。
(7) 能够多人聊天。
(8) 只限于局域网内聊天。
(9) 美观的操作界面。
(10) 主界面显示聊天信息,在线用户信息。
2.2 系统结构设计
①进入用户界面
软件构架
处理新用户加入
显示用户信息
显示新用户信息
发送文件
进入聊天室
退出
②用户操作系统
显示用户离开
删除用户信息
显示新用户信息
用户处理
处理用户离开
处理新用户加入
接受文件
保存聊天记录
文件处理
聊天记录
发送文件
删除聊天记录
发送信息
接受信息
发送信息
③聊天室活动图
处理用户离开
处理新用户加入
处理文字
消息处理
下划线
颜色
加粗
接受消息
发送消息
3 系统详细设计
3.1窗口设计
私聊窗口设计
群聊窗口设计
3.1.1聊天室窗口设计
聊天室窗口分为三个模块,模块一:发送消息;模块二:接收消息;模块三:接收在线用户的信息如(用户名、主机名、IP地址)。
1、模块一也可以称为用户发言区。专门用来处理用户所输入的发言等,可以对发言的字体大小,字体和颜色,粗体,下划线进行更改以及保存聊天记录、清屏等功能。用户发言后直接点击发送按钮,此时就会调用发送函数sendMessage(),将messageTextEdit组件中的内容发送出去。通过QByteArray型局部变量datagram中构建待发送的数据包,然后通过QUdpSocket类的 writeDatagram ( const QByteArray & datagram, const QHostAddress & host, quint16 port );函数将数据包发出。值得注意的是,这里的地址使用了QHostAddress::Broadcast值,它对应IPv4下的广播地址,如果将该值更换成单机地址(如本机地址QHostAddress::LocalHost),将变成一个普通的点对点的UDP程序。
发送函数实现的主要代码如下:
void siliao::sendMessage(MessageType type , QString serverAddress) //发送信息
{
QByteArray data; //定义一个存储数据的容器
QDataStream out(&data,QIODevice::WriteOnly); //将数据转化为同一类型
QString localHostName = QHostInfo::localHostName(); //获取主机名
QString address = getIP(); //获取IP地址
out << type << getUserName() << localHostName;
switch(type)
{
case ParticipantLeft:
{
break;
}
case Message :
{
if(ui->textEdit->toPlainText() == "")
{
QMessageBox::warning(0,tr("警告"),tr("发送内容不能为空"),QMessageBox::Ok);
return;
}
message = getMessage();
out << address << message; //将IP和消息发送到data容器中 ui->textBrowser->verticalScrollBar()->setValue(ui->textBrowser->verticalScrollBar()->maximum());
break;
}
}
xsiliao->writeDatagram(data.data(),data.size(),QHostAddress::QHostAddress(xpasvuserip), 45457);
} //将data容器中的数据发送出去
}
字体设置实现
字体样式转变设置实现的主要代码如下:
void qunliao1::currentFormatChanged(const QTextCharFormat &format)
{
ui->fontComboBox->setCurrentFont(format.font());
if (format.fontPointSize() < 9) {
ui->comboBox->setCurrentIndex(3);
} else {
ui->comboBox->setCurrentIndex(ui->comboBox ->findText(QString::number(format.fontPointSize())));
}
color = format.foreground().color();
}
字体加粗样式设置实现的主要代码如下:
void qunliao1::on_boldToolBtn_clicked(bool checked)
{
if(checked)
ui->messageTextEdit->setFontWeight(QFont::Bold);
else ui->messageTextEdit->setFontWeight(QFont::Normal);
ui->messageTextEdit->setFocus();
}
字体添加下划线样式设置实现的主要代码如下:
void qunliao1::on_underlineToolBtn_clicked(bool checked)
{
ui->messageTextEdit->setFontUnderline(checked);
ui->messageTextEdit->setFocus();
}
清屏功能实现的主要代码如下:
void qunliao1::on_clearToolBtn_clicked()
{
ui->messageBrowser->clear();
}
改变字体颜色实现的主要代码如下:
void qunliao1::on_toolButton_clicked()
{
color = QColorDialog::getColor(color, this);
if (color.isValid())
//判断是否创建对象实例,创建了就进入{
ui->messageTextEdit->setTextColor(color);
ui->messageTextEdit->setFocus();
}
}
保存聊天记录的主要代码如下:
void qunliao1::on_saveToolBtn_clicked()
{
if (ui->messageBrowser->document()->isEmpty()) {
QMessageBox::warning(0, tr("警告"), tr("聊天记录为空,无法保存!"), QMessageBox::Ok);
} else {
QString fileName = QFileDialog::getSaveFileName(this,
tr("保存聊天记录"), tr("聊天记录"), tr("文本(*.txt);;All File(*.*)"));
if(!fileName.isEmpty())
saveFile(fileName);
}
}bool qunliao1::saveFile(const QString &fileName)
{
QFile file(fileName);
if (!file.open(QFile::WriteOnly | QFile::Text)) {
QMessageBox::warning(this, tr("保存文件"),
tr("无法保存文件 %1:\n %2").arg(fileName)
.arg(file.errorString()));
return false;
}
QTextStream out(&file);
out << ui->messageBrowser->toPlainText();
return true;
}
3.1.2发送文件窗体设计
发送文件窗体中包涵了一些简单的组件,其中最主要的就是发送按钮,他实现了文件的传输。
1、用户在界面按下"打开"按钮后,openFile()槽函数将被调用。该函数通过Qt文件选择对画框QFileDialog所提供的静态函数getOpenFileName(),能够很容易地返回用户所选取的文件名,这里将其保存在私有成员变量fileName中。如果选中返回的文件名非空,将激活"发送"按钮。单击发送按钮就开启监听,并且等待接收者接受。当接收者接受时就开始建立连接。
一旦连接建立成功,QTcpServer类将发出newConnection ()消息,继而调用sendMessage()槽函数。该函数首先向接收方发送一个文件头结构。
文件头结构由三个字段组成,分别是64位的总长度(包括文件数据长度和文件头自身长度),64位的文件名长度和文件名。
函数sendMessage()首先以只读方式打开选中的文件,然后通过QFile类的size()函数获取待发送文件的大小,并将该值暂存于TotalBytes变量中。
接下来将发送缓冲区outBlock封装在一个QDataStream类型的变量中,这样做可以很方便的通过重载的"<<"操作符填写文件头结构。
设置文件头结构的操作有些小技巧,这里首先通过QString类的right()函数去掉文件的路径部分,仅将文件部分保存在currentFile变量中,然后通过sendOut << qint64(0) << qint64(0) << currentFile操作构造一个临时的文件头,将该值追加到TotalBytes字段,从而完成实际需发送字节数的记录。
接着通过sendOut.device()->seek(0)函数将读写操作指向从头开始,并且调用类似操作sendOut << TotalBytes << qint64((outBlock.size() - sizeof(qint64) * 2)),填写实际的总长度和文件长度。
需要注意的是,不能错误地通过QString::size()函数获取文件名的大小,该函数返回的是QString类型文件名所包含的字节数,而不是实际所占存储空间的大小,由于字节编码和QString类存储管理的原因,两者往往并不相等。
完成了文件头结构的填写后,调用tcpClient.write(outBlock)函数将该文件头发出,同时修改待发送字节数bytesToWrite。最后,调用outBlock.resize(0)函数清空发送缓冲区以备下次使用。
一旦数据发出,QTcpSocket类将会产生bytesWritten()信号,继而调用updateClientProgress(qint64)槽函数,参数表示实际已发出的字节数。如果待发送数据计数bytesToWritten大于0,将尽可能地从发送文件中读取数据,并将其发送,否则发送完毕关闭文件。还需要在此更新亦发和待发数据计数,并以此更新发送进度条和状态显示。
发送文件的代码如下:
clientConnection = tcpServer->nextPendingConnection();
//nextPendingConnection获得socket 连接;
connect(clientConnection, SIGNAL(bytesWritten(qint64)),
this, SLOT(updateClientProgress(qint64)));
ui->serverStatusLabel->setText(tr("开始传送文件 %1 !").arg(theFileName));
localFile = new QFile(fileName);
if(!localFile->open((QFile::ReadOnly)))
//以只读方式打开
{
QMessageBox::warning(this, tr("应用程序"), tr("无法读取文件 %1:\n%2")
.arg(fileName).arg(localFile->errorString()));
return;
}
TotalBytes = localFile->size(); //此时总大小为实际文件大小
QDataStream sendOut(&outBlock, QIODevice::WriteOnly);
//将数据写入outBlock缓存;
sendOut.setVersion(QDataStream::Qt_4_7);
time.start(); // 开始计时
QString currentFile = fileName.right(fileName.size()
- fileName.lastIndexOf('/')-1);
sendOut << qint64(0) << qint64(0) << currentFile; //依次写入总大小信息空间,文件大小信息空间,文件名
TotalBytes += outBlock.size(); //此时总大小为实际文件大小加上文件名大小等信息
sendOut.device()->seek(0); //返回到数据缓冲区outBlock的起始地址
sendOut << TotalBytes << qint64((outBlock.size() - sizeof(qint64)*2));
bytesToWrite = TotalBytes - clientConnection->write(outBlock);
outBlock.resize(0);
}
更新进度条的代码如下:
qApp->processEvents();
bytesWritten += (int)numBytes;
if (bytesToWrite > 0) {
outBlock = localFile->read(qMin(bytesToWrite, payloadSize)); //每次发送payloadSize大小的数据
bytesToWrite -= (int)clientConnection->write(outBlock);
outBlock.resize(0); //清空发送缓冲区
} else {
localFile->close();
}
ui->progressBar->setMaximum(TotalBytes);
ui->progressBar->setValue(bytesWritten);
float useTime = time.elapsed();
double speed = bytesWritten / useTime;
ui->serverStatusLabel->setText(tr("已发送 %1MB (%2MB/s) "
"\n共%3MB 已用时:%4秒\n估计剩余时间:%5秒")
.arg(bytesWritten / (1024*1024))
.arg(speed*1000 / (1024*1024), 0, 'f', 2)
.arg(TotalBytes / (1024 * 1024))
.arg(useTime/1000, 0, 'f', 0)
.arg(TotalBytes/speed/1000 - useTime/1000, 0, 'f', 0));
if(bytesWritten == TotalBytes) {
localFile->close();
tcpServer->close();
ui->serverStatusLabel->setText(tr("传送文件%1成功").arg(theFileName));
}
1、添加所需要的组件
主要组件名称及用途如下:
progressBar进度条,用于显示数据传输的过程
serverOpenBtn打开本地文件
serverSendBtn发送文件
serverCloseBtn关闭窗体
2、重要组件QTcpServer的使用
(1) 创建对象tcpServer。
(2) 通过tcpServer关联信号和槽。
(3) 每当发现新连接时信号newConnection就会触发槽函数sendMessage()实现文件的发送。tcpServer组件中的nextPendingConnection()方法是用来获得socket 连接的。
3.1.3接收窗体设计
接收窗体中也是一些简单的组件,其主要的功能还是在于文件接收的函数中。
1、当接收到其他用户发来的文件接收请求时,用户可以接受请求,也可以拒绝请求。当接收请求后,newConnect()函数将被调用。该函数的主要功能是连接服务器,它使用了QTcpSocket类的connectToHost()函数,其中的两个参数分别是服务器主机地址及其监听端口。
接收程序tcpclient完成的功能与发送程序恰恰相反,它负责从TCP连接上接收数据,并将其写入当前目录下的指定文件中。
其界面也是一个简单的对话框,上面布置一个QProgressBar进度条,一个用来显示状态的QLabel,两个QPushButton按钮分别用来取消接收和退出程序。
当建立的连接有新的可供读取的数据时,QTcpSocket类会发出readyRead()信号,从而触发readMessage()槽函数。该函数完成数据的接收、存储,并更新进度显示。
首先将返回的TCP连接tcpClient封装的QDataStream类型变量in中,同时设置流化数据格式类型为QDataStream::Qt_4_6,与客户端保持一致。现在可以很方便的通过重载后的"<<"操作符读取TCP连接上的数据了。
由于流数据是没有结构的,为了知道接收的文件名以及文件何时接收完毕,必须首先获取文件头结构,这里还有个小问题,由于开始时所传输文件名的长度是未知的,导致文件头结构的长度也是未知的,因此无法知道TCP数据流中前多少字节属于文件头结构部分。实际上文件头结构的接收可分两布完成:
(1)从TCP数据流中接收前16个字节(两个qint64结构长),用来确定总共需接收的字节数和文件名长度,并将这两个值保存在私有成员TotalBytes和fileNameSize中,然后根据fileNameSize值接收文件名。值得注意的是,无法保证在上述接收文件头结构过程中,TCP连接上总是有足够的数据,因此在第一步中,需要通过tcpServerConnection->bytesAvailable() >= sizeof(qint64)*2) && (fileNameSize ==0)操作确保至少有16字节的可用数据且文件名长度为0(表示未从TCP连接接收文件名长度字段,仍处于第一步操作),然后调用in >> TotalBytes >> fileNameSize操作读取总共需接收的数据和文件名长度。
(2)类似的通过(tcpServerConnection->bytesAvailable() >= fileNameSize) && (fileNameSize !=0)操作确保连接上的数据已包含完整的文件名且文件名长度不为0(表示已从TCP连接接收文件名长度字段,处于第二步操作中),然后调用in >> fileName操作读取文件名,并根据该文件名在本地以只写方式打开一个同名文件localFile,用来保存接收到的数据。
接下来的工作是读取实际的文件数据并保存,以及更新进度显示,直到接收到完全的数据。由于所发送的文件内容自身也是无格式的流,因此在接收文件内容时,只要TCP连接上有数据,就调用tcpClient->readAll()操作将当前全部可读数据读入接收缓冲inBlock中,随后再将该缓冲中的数据写入文件localFile中。当已收到的数据bytesReceived等于TotalBytes时,接收完毕,这时通过tcpClient->close()操作关闭连接。
接收文件的主要代码如下:
QDataStream in(tcpClient);
in.setVersion(QDataStream::Qt_4_7);
float useTime = time.elapsed();
if (bytesReceived <= sizeof(qint64)*2) {
if ((tcpClient->bytesAvailable()
>= sizeof(qint64)*2) && (fileNameSize == 0))
{
in>>TotalBytes>>fileNameSize;
bytesReceived += sizeof(qint64)*2;
}
if((tcpClient->bytesAvailable() >= fileNameSize) && (fileNameSize != 0)){
in>>fileName;
bytesReceived +=fileNameSize;
if(!localFile->open(QFile::WriteOnly)){
QMessageBox::warning(this,tr("应用程序"),tr("无法读取文件 %1:\n%2.")
.arg(fileName).arg(localFile->errorString()));
return;
}
} else {
return;
}
}
if (bytesReceived < TotalBytes) {
bytesReceived += tcpClient->bytesAvailable();
inBlock = tcpClient->readAll();
localFile->write(inBlock);
inBlock.resize(0);
}
ui->progressBar->setMaximum(TotalBytes);
ui->progressBar->setValue(bytesReceived);
double speed = bytesReceived / useTime;
ui->tcpClientStatusLabel->setText(tr("已接收 %1MB (%2MB/s) "
"\n共%3MB 已用时:%4秒\n估计剩余时间:%5秒")
.arg(bytesReceived / (1024*1024))
.arg(speed*1000/(1024*1024),0,'f',2)
.arg(TotalBytes / (1024 * 1024))
.arg(useTime/1000,0,'f',0)
.arg(TotalBytes/speed/1000 - useTime/1000,0,'f',0));
if(bytesReceived == TotalBytes)
{
localFile->close();
tcpClient->close();
ui->tcpClientStatusLabel->setText(tr("接收文件 %1 完毕")
.arg(fileName));
}
1、 添加所需要的组件
一个lable,一个progressBar,两个pushButton。
主要组件名称及用途如下:
progressBar进度条,用于显示数据接收的过程
tcpClientCancleBtn停止接收
tcpClientCloseBtn关闭窗体
2、 重要组件QTcpSocket的使用
(1) 创建对象tcpClient。
(2) 通过tcpClient关联信号和槽。
(3) 每当发送端将文件发送出来时,接收端就会设置服务器地址并通过tcpClient组件的connectToHost方法连接服务器,连接成功并发现有可接收的数据时,信号readyRead()就会触发槽函数readMessage(
开始接收数据。每读取一个字节时tcpClient组件中的 bytesAvailable 属性增加一
Tcpclient.ui
tcpserver.ui
3.2接收消息模块
messageBrowser组件实现,接受数据函数首先调用QUdpSocket类的成员函数hasPendingDatagrams()以判断是否有可供读取的数据。如果有则通过pendingDatagramSize()获取当前可供读取的UDP报文大小,并据此大小分配接收缓冲区,最后读取相应数据。
接收函数中几条主要语句如下:
void qunliao1::qlprocessPendingDatagrams()
{
while(udpSocket->hasPendingDatagrams()) //判断是否有可供读取的数据
{
QByteArray datagram; //用于存放接收的数据报
datagram.resize(udpSocket->pendingDatagramSize());
//让datagram的大小为等待处理的数据报的大小,这样才能接收到完整的数据
udpSocket->readDatagram(datagram.data(), datagram.size());
//接收数据报,将其存放到datagram中;
QDataStream in(&datagram, QIODevice::ReadOnly);
// 从文件datagram中读取串行化的数据;
int messageType;
in >> messageType;
QString userName,localHostName,ipAddress,message;
QString time = QDateTime::currentDateTime()
.toString("yyyy-MM-dd hh:mm:ss");
//获取系统时间
switch(messageType)
{
case QLMessage:
in >> userName >> localHostName >> ipAddress >> message;
//数据的流向
ui->messageBrowser->setTextColor(Qt::blue);
ui->messageBrowser->setCurrentFont(QFont("Times New Roman",12));
ui->messageBrowser->append("[ " +userName+" ] "+ time);
//显示用户名和接收到数据的时间
ui->messageBrowser->append(message);
//显示接收的消息
break;
}
}
}
3.3用户信息
用userTableWidget组件实现,在userTableWidget中显示了用户的,用户名、主机名、IP地址。有新用户加入时就调用newParticipant()函数处理新用户,有用户离开时就会调用participantLeft()函数处理离开的用户。用户加入时会把自己的用户名、主机名、IP地址发送出去,其他用户就会接收到并将其内容显示在userTabl
展开阅读全文