实验二:传输文件
1、实验目的:
要求学生掌握Socket编程中流套接字的技术
2、实验内容:
- 要求学生掌握利用Socket进行编程的技术
- 对文件进行分割(每片256字节),分别打包传输
- 发送前,通过协商,发送端告诉接收端发送片数
- 报头为学号、姓名、本次分片在整个文件中的位置
- 报尾为校验和:校验和s的计算:设要发送n字节,bi为第i个字,s=(b0+b1+…+bn) mod 256
- 接收方进行合并
- 必须采用图形界面
- 发送端可以选择文件,本次片数
- 接收端显示总共的片数,目前已经接收到的文件片数,收完提示完全收到
目录
附上代码:(就不教怎么配置了,根据下面文件能反推,比如类名叫啥,实在不会的就去看看qt软件,熟悉熟悉再弄)
写在最前:
csdn这个上传视频弄这个封面的分辨率真的是搞不来了,索性我传到b站了在主页里,自己去找吧。。
计算机-CSDN直播 不知道你们能不能看见,反正我自己可以~~
-
实验环境
win10 +
软件安装教程:
QT从入坑到绝望_不买Huracan不改名的博客-CSDN博客
软件配件工具维护:
成功资料
库:http://mirrors.ustc.edu.cn/qtproject/online/qtsdkrepository/windows_x86/root/qt/
Qt 官网有一个专门的资源下载网站,所有的开发环境和相关工具都可以从这里下载,具体地址是:http://download.qt.io/
更新配件教程:https://blog.csdn.net/liulihuo_gyh/article/details/78583884
关于我眼中的TCP和Socket
1、TCP/IP介绍
TCP/IP协议 q 传输控制/网际协议(Transfer Control Protocol/Internet Protocol) 又称作网络通讯协议
Internet国际互联网络的基础,RFC793
一组协议,通常称它为TCP/IP协议族
四个层次:网络接口层、网际层、传输层、应用层
2、TCP/IP结构
3、TCP传输协议
是一种面向连接的传输层协议,它能提供高可靠性通信(即 数据无误、数据无丢失、数据无失序、数据无重复到达的 通信)
适用情况:
- 适合于对传输质量要求较高,以及传输大量数据的通信。
- 在需要可靠数据传输的场合,通常使用TCP协议
- MSN/QQ等即时通讯软件的用户登录账户管理相关的功能 通常采用TCP
4、Socket
一个编程接口
是一种特殊的文件描述符 (everything in Unix is a file) n
并不仅限于TCP/IP协议 n
面向连接 (Transmission Control Protocol - TCP/IP) n
无连接 (User Datagram Protocol -UDP 和 Inter-network Packet Exchange - IPX)
5、在linux中C语言的使用流程
在这里我就不细讲c/c++中的使用,因为我们有QT这个框架,QT中帮助我们封装了很多socket的库,使用起来很方便。但是我还是要讲一下,在c语言中linux环境下TCP通信息的流程,因为只有知道了底层调用的一些原理,用起框架来才更加的顺手。
6、常用接口函数:
socket() 创建套接字
bind() 绑定本机地址和端口
connect() 建立连接
listen() 设置监听端口
accept() 接受TCP连接
recv(), read(), recvfrom() 数据接收
send(), write(), sendto() 数据发送
close(), shutdown() 关闭套接字
从上图可以很清楚的看到服务器和客户端的运行流程,对于客户端的bind哪一步文档上是说可以bind可以不bind,一般我们都不去将客户端bind
在这里值得注意下,对于系统编程中socket套接字都是默认阻塞的,因此accept这个函数会阻塞在哪里一直等到有客户端连接上来,还有读操作也是阻塞状态的。想要解除套接字阻塞,那就需要用到IO模型的知识了。由于没有用到,暂时就不深究了。
在QT中的Socket
我们知道tcp通信的流程是
1)、服务器:申请套接字 -> 绑定套接字 -> 监听套接字 -> 接收连接 -> 开始发送数据接收数据
2)、客户端:申请套接字 -> 建立连接 -> 发送数据 和 接收数据
而在qt中将,这些操作都被封装成在一个模块中,所以我们就没有这么复杂的操作,大致流程如下
1)、服务器:
1、启动服务器,即监听
mserver.listen(QHostAddress::Any,8080);
2、连接newConnection这个信号判断是否有客户端连接进来
connect(&mserver, &QTcpServer::newConnection, this, &FileRecv::new_client);
3、创建套接字
QTcpSocket *msocket = mserver.nextPendingConnection();
2)、客户端
1、初始化一个套接字对象
2、连接connected这个信号判断是否连接成功
3、对套接字进行读写
1.QT下的服务端
1).socket函数变为QTcpServer
2).bind ,listen 统一为listen
同时没有accept,当有一个链接过来的时候,会产生一个信号:newconnection,可以从对应的槽函数中取出建立好的套接字(对方的)QTcpSocket 如果成功和对方建立好链接,通信套接字会自动触发connected信号
3).read :
对方发送数据过来,链接的套接字(通信套接字)就会触发(本机的)readyRead信号,需要在对应的槽函数中接收数据
4).write,
发送数据,对方的(客户端的)套接字(通信套接字)就会触发readyRead信号,需要在对应的槽函数中接收数据 如果对方主动断开连接,对方的(客户端的)套接字(通信套接字)会自动触发disconnected信号。
2.QT下的客户端:
1).socket函数变为 QTcpSocket
2).connect变为connetToHost()
如果成功和对方建立好链接,就会自动触发connected信号
3).read :
对方发送数据过来,链接的套接字(通信套接字)就会触发(本机的)readyRead信号,需要在对应的槽函数中接收数据
4).write,
发送数据,对方的(服务器的)套接字(通信套接字)就会触发readyRead信号,需要在对应的槽函数中接收数据,如果对方主动断开连接,就会自动触发disconnected信号
-
操作方法与实验步骤
代码正文解释
文件传输的图解分析如下:
1.按照流程过一遍: (演示视频)
2.客户端点击connect后
触发下面的函数: 会去连接服务器
3.服务器收到连接后,会自动触发newConnection函数进入下面操作,建议连接并且
打印相关信息。
4.选择框选择文件 :触发下面的函数,打开相应的文件,做好发送准备
5.点击发送文件按钮: 发送文件头部信息,和即将发送片数等内容,经过短暂延时后开始发送数据(tcp防止粘包问题要求的操作就是如此)
6.上面定时函数触发下面的函数,倒计时结束后调用发送函数
下面是发送函数,根据先前选的报文片数,拼接报头,正文,报尾并且发送出去
7.发送方成功以后,接收方这边检测到有数据过来会自动触发readyRead函数:
根据实际情况,拿到文件头信息,或者是拿到正文数据,然后拆分出报头报尾和正文,存到自己的数据结构,然后存入到文件中,存入的文件名默认是发送过来的名字,路径是在生成的debug文件下。
其他的都是附加的针对的ui界面的设计,可以参考文件:源码文件夹,详细代码,有注释。
-
实验数据记录和结果分析
QT中简单的emit使用:
QT中简单的emit使用_csdn_dyq111的博客-CSDN博客_qt emit
qt字符串与字符串数组操作:qt字符串与字符串数组操作_一只藤井树的博客-CSDN博客_qt 字符串数组
QString 和char数组转换:
QString 和char数组转换_M_Pinery的博客-CSDN博客_qstring char数组
qstring如何初始化_QString介绍:
qstring如何初始化_QString介绍_AMD中国的博客-CSDN博客
Qt之Qfile读取文件操作:
Qt之Qfile读取文件操作_Nina_小哥的博客-CSDN博客_qfile
QT 对QString字符串的操作:
QT 对QString字符串的操作_Sakuya__的博客-CSDN博客_qstring 字符串反转
QByteArray转QString(String-Base64):QByteArray转QString(String-Base64)_于大博的博客-CSDN博客_qbytearray转qstring
QString、Qbytearray、string的相互转换及相关问题:
QString、Qbytearray、string的相互转换及相关问题_HelloEarth_的博客-CSDN博客_string 转q'b'y't
Qt中 字符串的比较和遍历:
Qt中 字符串的比较和遍历_后起乱秀的博客-CSDN博客_qt遍历字符串
对QMap中的key进行自定义排序:
对QMap中的key进行自定义排序_hp_cpp的博客-CSDN博客_qmap排序
Qt 使用QInputDialog弹出输入框获取用户输入数据:
Qt 使用QInputDialog弹出输入框获取用户输入数据_Cqy_Chaos的博客-CSDN博客_qt弹窗输入
C++中int与string的相互转换:
C++中int与string的相互转换_HarryLi的博客-CSDN博客_int转string c++语言
std:;remove_if用法讲解:
std:;remove_if用法讲解_KFLING的博客-CSDN博客_remove_if
Qt 整型与字符串 int与QString互转:
Qt 整型与字符串 int与QString互转_秃顶吧!程序猿的博客-CSDN博客_qt 整形转qstring
Qt TCP网络编程:
Qt TCP网络编程——传输图片(附TCP连接逻辑以及完整代码)_繁星蓝雨的博客-CSDN博客_qt发送图片
附上代码:
(就不教怎么配置了,根据下面文件能反推,比如类名叫啥,实在不会的就去看看qt软件,熟悉熟悉再弄)
(好像没什么用 因为ui 的部分是没办法给的?)
结构如下:
TCPfile.pro
QT += core gui network
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
CONFIG += c++11
# The following define makes your compiler emit warnings if you use
# any Qt feature that has been marked deprecated (the exact warnings
# depend on your compiler). Please consult the documentation of the
# deprecated API in order to know how to port your code away from it.
DEFINES += QT_DEPRECATED_WARNINGS
# You can also make your code fail to compile if it uses deprecated APIs.
# In order to do so, uncomment the following line.
# You can also select to disable deprecated APIs only up to a certain version of Qt.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
SOURCES += \
clientwidget.cpp \
main.cpp \
serverwidget.cpp
HEADERS += \
clientwidget.h \
serverwidget.h
FORMS += \
clientwidget.ui \
serverwidget.ui
CONFIG += C++11
# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target
clientwidget.h
#ifndef CLIENTWIDGET_H
#define CLIENTWIDGET_H
#include"newwindow.h"
#include <QWidget>
#include <QMap>
#include<QTcpSocket>
#include<QFile>
namespace Ui {
class clientwidget;
}
class clientwidget : public QWidget
{
Q_OBJECT
public:
explicit clientwidget(QWidget *parent = nullptr);
~clientwidget();
QString prefix="txt";
QByteArray RecString;//收到的字节数
int rcve;//即将收到的个数
QMap<int,QString>Accept_Data;//存放所有分片的数据结构
private slots:
void on_pushButton_clicked();
private:
Ui::clientwidget *ui;
QTcpSocket *tcpSocket;
QString fileName;
qint64 fileSize;
qint64 receiveSize;
bool isStart;
QFile file;
};
#endif // CLIENTWIDGET_H
clientwidget.cpp
#include "clientwidget.h"
#include "ui_clientwidget.h"
#include <QMessageBox>
#include <QHostAddress>
clientwidget::clientwidget(QWidget *parent) :
QWidget(parent),
ui(new Ui::clientwidget)
{
ui->setupUi(this);
isStart = true;//代表传输的文件头
tcpSocket = new QTcpSocket(this);
connect(tcpSocket,&QTcpSocket::readyRead,
[=]
{
QByteArray buf = tcpSocket->readAll();
//是拿到的文件头部信息的话
if(true == isStart)
{
isStart = false;
fileName = QString(buf).section("##",0,0);
fileSize = QString(buf).section("##",1,1).toInt();
rcve= QString(buf).section("##",2,2).toInt();
receiveSize = 0;
// qDebug()<<"clent接受到的文件名:"<<fileName<<fileSize<<rcve;
file.setFileName(fileName);
bool isOk = file.open(QIODevice::WriteOnly);
if(isOk == false)
{
qDebug()<<"只写方式打开出错";
}
}
//如果是txt文件的话
else if(fileName.contains(prefix,Qt::CaseSensitive))
{
QString temp=QString(buf);
QStringList list = temp.split("|||", QString::SkipEmptyParts);//QString字符串分割函数
int recc=0;//用来显示进度
for (QList<QString>::Iterator it = list.begin();it!=list.end(); it++){
// qDebug()<<(*it);
QString tt= QString(*it).section("##",2,2);//拿到中间正文数据
int pianyiliang = QString(*it).section("##",0,0).toInt();
//显示进度条
recc++;
float f = 100.0*recc/fileSize;
ui->progressBar->setValue(f);
//放入自己的数据结构
Accept_Data.insert(pianyiliang,tt);
}
// 把手里拿到的报文拼接回去
QString tt="";
QMapIterator<int, QString> i(Accept_Data);
while(i.hasNext()) {
i.next();
tt+=i.value();
}
qDebug()<<tt;
//写入文件
qint64 len = file.write(tt.toUtf8());
if(Accept_Data.size()==rcve) {
file.close();
QMessageBox::information(this,"完成","文件接收完成");
qDebug()<<"收到得全部内容"<<RecString;
ui->progressBar->setValue(0);
tcpSocket->disconnectFromHost();
tcpSocket->close();
}
}
//其他文件
else{
qint64 len = file.write(buf);
qDebug()<<"收到de内容"<<buf;
RecString+=buf;
receiveSize +=len;
_sleep(5000);
float f = 100.0*receiveSize/fileSize;
ui->progressBar->setValue(f);
if(receiveSize==fileSize) {
file.close();
QMessageBox::information(this,"完成","文件接收完成");
qDebug()<<"收到得全部内容"<<RecString;
ui->progressBar->setValue(0);
tcpSocket->disconnectFromHost();
tcpSocket->close();
}
}
});
}
clientwidget::~clientwidget()
{
delete ui;
}
/**
* @brief 连接函数点击动作
*/
void clientwidget::on_pushButton_clicked()
{
QString ip = ui->lineEditIP->text();
quint16 port = ui->lineEditPort->text().toInt();
tcpSocket->connectToHost(QHostAddress(ip),port);
}
serverwidget.h
#ifndef SERVERWIDGET_H
#define SERVERWIDGET_H
#include"newwindow.h"
#include <QWidget>
#include<QTcpServer>
#include<QTcpSocket>
#include<QFile>
#include<QVector>
#include<QTimer>
#include<QDebug>
#include<string.h>
#include<string>
#include<QInputDialog>
#include <QPushButton> //Push按钮类
#include <QList> //列表类
QT_BEGIN_NAMESPACE
namespace Ui { class serverWidget; }
QT_END_NAMESPACE
class serverWidget : public QWidget
{
Q_OBJECT
public:
serverWidget(QWidget *parent = nullptr);
int LoadFile();
~serverWidget();
void sendData();
QString STR_Not_Cut;
QVector<int> Checksum;
QVector<int> data;
QString prefix="txt";
int size=100;
int file_to_chose=0;
QVector<QString> DATA;
private slots:
void on_buttonFile_clicked();
void on_buttonSend_clicked();
void on_pushButton_clicked();
void on_pushButton_2_clicked();
private:
Ui::serverWidget *ui;
QTcpServer *tcpServer;
QTcpSocket *tcpSocket;
QFile file;
QString fileName;
qint64 fileSize;
qint64 sendSize;
QTimer timer;
QList<QPushButton*> btnPushlist;//动态创建按钮的列表
QPushButton *btnPush;//动态创建按钮指针
public:
void PRINT()
{
qDebug() << "拿到的所有存放在DATA:"<< "\0";
for ( auto iter:DATA)
qDebug()<<iter;
qDebug() << DATA.size() << "\0";
}
};
#endif // SERVERWIDGET_H
serverwidget.h
#include "serverwidget.h"
#include "ui_serverwidget.h"
#include<QFileDialog>
#include<QDebug>
#include <string>
#include<QFileInfo>
#define flag fileName.contains(prefix,Qt::CaseInsensitive)
serverWidget::serverWidget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::serverWidget)
{
ui->setupUi(this);
tcpServer = new QTcpServer(this);
tcpServer->listen(QHostAddress::Any,8000);
ui->buttonFile->setEnabled(false);
ui->buttonSend ->setEnabled(false);
setWindowTitle("客户端口为:8000");
connect(tcpServer,&QTcpServer::newConnection,
[=]
{
tcpSocket = tcpServer->nextPendingConnection();
QString ip = tcpSocket->peerAddress().toString();
quint16 port = tcpSocket->peerPort();
QString str = QString("[%1:%2]成功连接").arg(ip).arg(port);
ui->textEdit->setText(str);
ui->buttonFile->setEnabled(true);
ui->buttonSend->setEnabled(true);
});
connect(&timer,&QTimer::timeout,
[=]
{
timer.stop();
sendData();
});
}
serverWidget::~serverWidget()
{
delete ui;
}
/**
* @brief 装载数据 同时拿到校验和
*/
int serverWidget::LoadFile()
{
qint64 len = 0;
QByteArray buf;
do
{
len = 0;
buf=file.read(256);
if(buf.size()!=0)
DATA.push_back(buf);
}while(buf.size()>0);
int sum=0;
for(int i=0;i<DATA.size();i++)
{
for (QChar *it=DATA[i].begin(); it!=DATA[i].end(); ++it) {
sum+=(int)(*it).toLatin1();
}
Checksum.push_back(sum%256);
}
return 0;
}
/**
* @brief 当选择文件按钮被点击以后的动作
*/
void serverWidget::on_buttonFile_clicked()
{
QString filePath = QFileDialog::getOpenFileName(this,"open","../");
if(!filePath.isEmpty()){
fileName.clear();
fileSize = 0;
QFileInfo info(filePath);
fileName = info.fileName();
fileSize = info.size();
qDebug()<<"从 文件筐拿到的文件信息<"<<fileName<<fileSize;
sendSize = 0;
file.setFileName(filePath);
bool isOk = file.open(QIODevice::ReadOnly);
if(isOk == false){
qDebug()<<"只读方式打开文件失败了";
}
ui->textEdit->setText(filePath);
ui->buttonFile->setEnabled(false);
ui->buttonSend->setEnabled(true);
}
else{
qDebug() << "选择文件出错 62";
}
//加载数据
if(fileName.contains(prefix,Qt::CaseInsensitive)){
LoadFile();
}
//creat();
}
/**
* @brief 发送按钮被点击以后的动作
*/
void serverWidget::on_buttonSend_clicked()
{
//告诉他要发送的片数
QString head = QString("%1##%2##%3").arg(fileName).arg(fileSize).arg(data.size());
qint64 len = tcpSocket->write(head.toUtf8());
if(len>0){
timer.start(30);
}
else{
qDebug()<<"头部信息发送失败";
file.close();
ui->buttonFile->setEnabled(true);
ui->buttonSend->setEnabled(false);
}
}
void serverWidget::sendData()
{
//确实到了发送的时机
if(flag){
//std::string stm;
QString stm;
//QByteArray stm;
for(int i=0;i<data.size();i++)
{
_sleep(500);
QString head = QString("%1##%2##%3##%4").arg(QString::number(data[i])).arg(QString("162040210-zhangtutu")).arg(DATA[i]).arg(QString::number(Checksum[i]));
head+="|||";
QByteArray sendMessage = head.toUtf8();
tcpSocket->write(sendMessage);
// tcpSocket->waitForBytesWritten();
}
}
//其他命令状态操作
else
{
qint64 len = 0;
do
{
char buf[1024] = {0};
len = 0;
len = file.read(buf,sizeof(buf));
len = tcpSocket->write(buf,len);
sendSize += len;
}while(len>0);
if(sendSize == fileSize){
ui->textEdit->append("文件发送完毕");
file.close();
tcpSocket->disconnectFromHost();
tcpSocket->close();
}
}
}
//全选
void serverWidget::on_pushButton_clicked()
{
for(int i=0;i<DATA.size();i++)
{
data.push_back(i);
}
ui->textEdit->setText("选中全部的报文成功!\n");
}
//选择一次片数
void serverWidget::on_pushButton_2_clicked()
{
bool bOk = false;
int dbWeight = QInputDialog::getInt(this,
"QInputDialog_Weight",
"最大值"+QString::number(DATA.size()),
1, //默认值
-1, //最小值
DATA.size(), //最大值
2, //步长
&bOk);
if (bOk && dbWeight > -1) {
data.push_back(dbWeight);
}
}
main.cpp
#include "serverwidget.h"
#include "clientwidget.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
serverWidget w;
w.show();
clientwidget w1;
w1.show();
return a.exec();
}
ui:
写在最后:
写这个题目真的是百感交集,也可谓是一波三折。
我原来的思路是:
@1 写好文件传输的终端下的demo(也就是上文提到的linux中的通信模式一样)
@2 尝试内嵌一点图形化界面或者交互已完成题目要求。
结果做到后面发现,想要做一个图形化的交互界面可以用:qt、 c# 或者是py。但是无论是上面哪一种他们都有自己的socket套接字通讯函数,qt是基于c++的,不是说自己原来写的函数不能用。 但就是违背了qt本身自带Socket函数这一方便性。所以尝试把原来的代码嵌入到qt中。从c转变成c++。图方便就全部public了。于是就这样跑去验收,然后被痛批,发现只能传txt,对于docx和png,不太行。
跑回来慢慢改,直接全部推倒重来,全部基于qt实现,采用qt下的Socket通讯函数,Qt与原来通讯函数的区别:见上文。采用Qt下的QString和QByteArray存放信息,渐渐也知道了原来失败的原因,由于Qt编译器和本身自带的环境的编码格式可能存在差异,就会导致非acill码的部分出现解析失败,自然传过去的信息就是错误的。
到目前我写下这篇文章开始,我已经全部实现了要求的全部功能,并且可以正确的传输,不经感叹,计算机的神奇,qt的图形强大,未统一编码格式的痛苦。
在这里真的要首先感谢xxx老师对于知识的传授,为我成功实现实验6打下了坚定的基础;其次是负责验收的不知道姓名的助教学长,(其实对于本身强迫症的我来说,一开始验收的版本本就是qt和c的杂糅版本,不太符合我自己的预期,恰好验收未通过),给予我推到重来的动力,完成了目前让我满意的版本;最终也要感谢所有在这次实验中帮助过我了学长学姐和同学以及从未放弃的自己!最后以一句诗结尾吧:千磨万击还坚劲,任尔东西南北风!
官方话术结束:开始真正的心里话: (服务端称之为s端,客户端成为c端)
被这个实验差点搞破防,直接说结论吧,少废话,这个代码的s端和c端是反过来的,有没有发现,直接也就是说:c端找s端建立连接后,s端发送文件给c端.......
另外:针对txt和非txt我做的特殊的处理
txt部分:是可以 片头 报头 正文 报尾 拆分的 然后对应的接受度端(在我代码里是c端)是可以去掉 片头 报头 报尾的,拿到的正文放到了map中(第一个元素是 本次的片数,第二个元素是正文)
非txt部分:读一个 字符就写一个字符,不存在分片处理。(真写不出来了,那个编码问题搞得我直接破防了。)
作为个define处理以防止被看出来。。。。。。。。。。。。。