本文实验测试部分可参考基于QT的一款P2P共享文件系统
源码包下载地址基于QT的一款P2P共享文件系统下载,想要免费获取可以私信我
自己动手完成一款简易P2P共享文件软件的制作(一)
4. 客户机设计
客户机设计相较于服务器设计相对繁琐,因为除了需要设计一个GUI界面以及与服务器通信代码外,还需要增加客户端之间对等传输(P2P)相关代码。
4.1 GUI界面与功能设计
我们要为客户机设计的功能如下:
- 登陆
- 注册
- 获取我的共享文件信息
- 删除我的共享文件
- 上传我的共享文件
- 在P2P网络中搜索某个文件
- 从P2P网络中下载某个文件
我们将这些功能集成到一个界面中,我们设计的界面如下:
界面设计代码如下:
新建tcp_client.h文件,添加如下代码:
#ifndef TCP_CLIENT_H
#define TCP_CLIENT_H
#include <QMainWindow>
#include <QWidget>
#include <QListWidget>
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>
#include <QProgressBar>
#include <QGridLayout>
#include <QString>
#include <QTableWidget>
#include <QList>
#include <QTimer>
#include <QtNetwork/QHostAddress>
#include <QtNetwork/QTcpSocket>
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = 0);
~MainWindow();
private:
QWidget *myWidget;
QListWidget *ContentListWidget;
QLabel *UserNameLabel;
QLineEdit *UserNameLineEdit;
QLabel *UserPasswdLabel;
QLineEdit *UserPasswdLineEdit;
QLabel *ServerIPLabel;
QLineEdit *ServerIPLineEdit;
QLabel *PortLabel;
QLineEdit *PortLineEdit;
QPushButton *EnterBtn;
QPushButton *SignBtn;
QTableWidget *ResultTableWidget;
QLineEdit *SearchLineEdit;
QPushButton *SearchBtn;
QProgressBar *ProgressBar;
QPushButton *DownloadBtn;
QTimer *timer;
QTableWidget *ShareTableWidget;
QPushButton *FlushBtn;
QPushButton *DisableBtn;
QPushButton *ShareBtn;
QLineEdit *DirLineEdit;
QPushButton *SelectDirBtn;
QGridLayout *layout1;
QGridLayout *layout2;
QGridLayout *layout3;
QGridLayout *mainLayout;
bool status;
int port;
QHostAddress *ServerIP;
QString UserName;
};
#endif // TCP_CLIENT_H
新建tcp_client.cpp文件,添加如下代码:
#include "tcp_client.h"
#include "tcp_meta.h"
#include <QMessageBox>
#include <QHostInfo>
#include <QByteArray>
#include <QFileDialog>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
setWindowTitle("P2P Client");
setGeometry(250,200,850,500);
ContentListWidget = new QListWidget;
myWidget = new QWidget;
UserNameLabel = new QLabel("用户名:");
UserNameLineEdit = new QLineEdit;
UserPasswdLabel = new QLabel("密码:");
UserPasswdLineEdit = new QLineEdit;
ServerIPLabel = new QLabel("服务器IP:");
ServerIPLineEdit = new QLineEdit("192.168.43.98");
PortLabel = new QLabel("端口:");
PortLineEdit = new QLineEdit;
EnterBtn = new QPushButton("登陆");
SignBtn = new QPushButton("注册");
ShareTableWidget = new QTableWidget;
ShareTableWidget->setColumnCount(6);
QStringList tableHead;
tableHead << "文件名" << "大小" << "文件路径" << "IP" << "端口" << "选择";
ShareTableWidget->setHorizontalHeaderLabels(tableHead);
FlushBtn = new QPushButton("我的共享文件");
DisableBtn = new QPushButton("取消选中项共享");
ShareBtn = new QPushButton("上传我的文件");
ResultTableWidget = new QTableWidget;
ResultTableWidget->setColumnCount(7);
tableHead.clear();
tableHead << "文件名" << "大小" << "文件路径" << "拥有者" << "状态" << "IP" << "选择";
ResultTableWidget->setHorizontalHeaderLabels(tableHead);
SearchBtn = new QPushButton("搜索");
SearchLineEdit = new QLineEdit;
ProgressBar = new QProgressBar;
ProgressBar->setRange(0,100);
DownloadBtn = new QPushButton("下载选中项");
timer = new QTimer();
DirLineEdit = new QLineEdit;
SelectDirBtn = new QPushButton("选择保存位置");
layout1 = new QGridLayout();
layout2 = new QGridLayout();
layout3 = new QGridLayout();
mainLayout = new QGridLayout();
layout1->addWidget(ContentListWidget,0,0,1,2);
layout1->addWidget(UserNameLabel,1,0);
layout1->addWidget(UserNameLineEdit,1,1);
layout1->addWidget(UserPasswdLabel,2,0);
layout1->addWidget(UserPasswdLineEdit,2,1);
layout1->addWidget(ServerIPLabel,3,0);
layout1->addWidget(ServerIPLineEdit,3,1);
layout1->addWidget(PortLabel,4,0);
layout1->addWidget(PortLineEdit,4,1);
layout1->addWidget(EnterBtn,5,0,1,2);
layout1->addWidget(SignBtn,6,0,1,2);
layout2->addWidget(ShareTableWidget,0,0,1,3);
layout2->addWidget(FlushBtn,1,0);
layout2->addWidget(DisableBtn,1,1);
layout2->addWidget(ShareBtn,1,2);
layout3->addWidget(ResultTableWidget,0,0,1,2);
layout3->addWidget(SearchLineEdit,1,0);
layout3->addWidget(SearchBtn,1,1);
layout3->addWidget(ProgressBar,2,0,1,2);
layout3->addWidget(DirLineEdit,3,0);
layout3->addWidget(SelectDirBtn,3,1);
layout3->addWidget(DownloadBtn,4,0,1,2);
mainLayout->addLayout(layout1,0,0,2,2);
mainLayout->addLayout(layout2,0,2,1,1);
mainLayout->addLayout(layout3,1,2,1,1);
myWidget->setLayout(mainLayout);
setCentralWidget(myWidget);
status = false;
port = 8010;
PortLineEdit->setText(QString::number(port));
ServerIP = new QHostAddress;
ShareBtn->setEnabled(false);
FlushBtn->setEnabled(false);
DisableBtn->setEnabled(false);
SearchBtn->setEnabled(false);
DownloadBtn->setEnabled(false);
}
MainWindow::~MainWindow()
{
delete PortLabel;
delete PortLineEdit;
delete EnterBtn;
delete SignBtn;
delete UserNameLabel;
delete UserNameLineEdit;
delete UserPasswdLabel;
delete UserPasswdLineEdit;
delete ServerIPLabel;
delete ServerIPLineEdit;
delete ContentListWidget;
delete ShareTableWidget;
delete DisableBtn;
delete FlushBtn;
delete ResultTableWidget;
delete SearchLineEdit;
delete SearchBtn;
delete DownloadBtn;
delete timer;
delete ProgressBar;
delete SelectDirBtn;
delete DirLineEdit;
delete layout1;
delete layout2;
delete layout3;
delete mainLayout;
delete myWidget;
delete ServerIP;
}
在客户机中,所有要实现的功能都必须建立在客户机与服务器之间已经建立起tcp连接。而要建立连接,一种是无用户名式的请求注册连接,另一种是带用户名和密码的请求登陆连接,我们先介绍注册连接。在注册流程中,我们设定的方案是,用户先输入注册名与密码,确认服务器IP地址与端口号无误后点击注册按钮,然后等待服务器的注册回复消息,并且断开与服务器的tcp连接以便下次重新申请注册。添加的代码如下:
tcp_client.h文件:
class MainWindow : public QMainWindow
{
enum MsgKind{
NAMEMSG = 0,//登陆消息
METAMSG = 1,//上传共享文件元数据消息
DISCONNECT = 2,//断连消息
SHAREMSG = 3,//获取我的共享文件消息
DELETEMSG = 4,//删除共享文件元数据消息
SEARCHMSG = 5,//搜索消息
SIGNMSG = 6,//注册消息
OTHERMSG = 7//普通消息
};
private:
...
QTcpSocket *tcpSocket;
void FormatMsg(QString &msg, MsgKind kind);
public slots:
void slotSignUp();//注册按钮对应的槽函数
void SignConnected();//注册的tcp建立连接后进入该函数
void SignDisconnected();//注册的tcp断开连接后进入该函数
void SignDataReceived();//注册过程中,服务器发送来消息会进入该函数
...
};
tcp_client.cpp文件:
void MainWindow::SignConnected(){
SignBtn->setEnabled(false);
int length = 0;
QString passwd = UserPasswdLineEdit->text();
//分号用于隔开两个字符串
QString msg = UserName + ";" + passwd + ";";
FormatMsg(msg, SIGNMSG);//格式化注册消息
if((length = tcpSocket->write(msg.toLatin1(),msg.length())) != msg.length()){
return;
}
}
void MainWindow::SignDataReceived(){
if (tcpSocket->bytesAvailable() > 8){
char buf[1024];
tcpSocket->read(buf,8);
//解析消息头,消息头有8个字节
QString head = buf;
long msglen = 0;
QString kind;
if(head == "error|||"){//错误消息
return;
}
else{
msglen = head.mid(2,6).toLong();
kind = head.mid(0,2);
tcpSocket->read(buf,msglen);
buf[msglen] = 0;
}
QString msg = buf;//消息体
if(kind == "SS"){ //sign success
ContentListWidget->addItem(msg);
}
else if(kind == "SF"){ //sign fail
ContentListWidget->addItem(msg);
SignBtn->setEnabled(true);
}
tcpSocket->disconnectFromHost();//断开连接
status = false;
}
}
void MainWindow::SignDisconnected(){
}
void MainWindow::slotSignUp(){
//没有建立连接
if(!status){
QString ip = ServerIPLineEdit->text();
if(!ServerIP->setAddress(ip)){
QMessageBox::information(this,"wrong","server ip error");
return ;
}
if(UserNameLineEdit->text() == ""){
QMessageBox::information(this,"wrong","user name error");
return ;
}
if(UserPasswdLineEdit->text() == ""){
QMessageBox::information(this,"wrong","pass word error");
return ;
}
UserName = UserNameLineEdit->text();
QString passwd = UserPasswdLineEdit->text();
tcpSocket = new QTcpSocket(this);
connect(tcpSocket,SIGNAL(connected()),this,SLOT(SignConnected()));
connect(tcpSocket,SIGNAL(disconnected()),this,SLOT(SignDisconnected()));
connect(tcpSocket,SIGNAL(readyRead()),this,SLOT(SignDataReceived()));
//发起tcp连接
tcpSocket->connectToHost(*ServerIP,port);
status = true;
}
//已经建立连接
else{
int length = 0;
QString msg = UserName + ": leave chat room";
FormatMsg(msg, OTHERMSG);//格式化消息
if((length = tcpSocket->write(msg.toLatin1(),msg.length())) != msg.length()){
return;
}
//断开tcp连接
tcpSocket->disconnectFromHost();
status = false;
delete p2pserver;
}
}
登陆代码与注册类似,其代码如下所示:
tcp_client.h文件:
class MainWindow : public QMainWindow
{
...
private:
...
void UpdateShareTableWidget(QString &msg);
void UpdateResultTableWidget(QString &msg);
public slots:
void slotEnter();//点击登陆按钮进入该函数
void slotConnected();//登陆成功后进入该函数
void slotDisconnected();//退出登陆后进入该函数
void DataReceived();//接收到消息后进入该函数
};
tcp_client.cpp文件:
void MainWindow::slotEnter(){
//还没有建立连接
if(!status){
QString ip = ServerIPLineEdit->text();
if(!ServerIP->setAddress(ip)){
QMessageBox::information(this,"wrong","server ip error");
return ;
}
if(UserNameLineEdit->text() == ""){
QMessageBox::information(this,"wrong","user name error");
return ;
}
UserName = UserNameLineEdit->text();
tcpSocket = new QTcpSocket(this);
connect(tcpSocket,SIGNAL(connected()),this,SLOT(slotConnected()));
connect(tcpSocket,SIGNAL(disconnected()),this,SLOT(slotDisconnected()));
connect(tcpSocket,SIGNAL(readyRead()),this,SLOT(DataReceived()));
tcpSocket->connectToHost(*ServerIP,port);
status = true;
}
//已经建立连接
else{
int length = 0;
QString msg = UserName + ": leave chat room";
FormatMsg(msg, OTHERMSG);
if((length = tcpSocket->write(msg.toLatin1(),msg.length())) != msg.length()){
return;
}
//断开连接
tcpSocket->disconnectFromHost();
status = false;
delete p2pserver;
}
}
void MainWindow::DataReceived(){
int flag = 0;//用于标记第一次进入该函数
while (tcpSocket->bytesAvailable() > 8) {
char buf[1024];
tcpSocket->read(buf,8);
//解析头信息
QString head = buf;
long msglen = 0;
QString kind;
if(head == "error|||"){
return;
}
else{
msglen = head.mid(2,6).toLong();
kind = head.mid(0,2);
tcpSocket->read(buf,msglen);
buf[msglen] = 0;
}
QString msg = buf;//消息体
if(kind == "RK"){ //resource messsage
if(flag == 0){
ShareTableWidget->clear();
ShareTableWidget->setRowCount(0);
QStringList tableHead;
tableHead << "文件名" << "大小" << "文件路径" << "IP" << "端口" << "选择";
ShareTableWidget->setHorizontalHeaderLabels(tableHead);
}
flag = 1;
//刷新“我的共享文件信息”
UpdateShareTableWidget(msg);
}
else if(kind == "SR"){ //search messsage
if(flag == 0){
ResultTableWidget->clear();
ResultTableWidget->setRowCount(0);
QStringList tableHead;
tableHead.clear();
tableHead << "文件名" << "大小" << "文件路径" << "拥有者" << "状态" << "IP" << "选择";
ResultTableWidget->setHorizontalHeaderLabels(tableHead);
}
flag = 1;
//刷新“搜索结果”
UpdateResultTableWidget(msg);
}
else if(kind == "EK"){ //error and disconnected messsage
ContentListWidget->addItem(msg);
EnterBtn->setText("登陆");
tcpSocket->disconnectFromHost();
status = false;
ShareBtn->setEnabled(false);
FlushBtn->setEnabled(false);
DisableBtn->setEnabled(false);
SearchBtn->setEnabled(false);
DownloadBtn->setEnabled(false);
SignBtn->setEnabled(true);
}
else if(kind == "OT"){ //normal message
ContentListWidget->addItem(msg);
}
}
}
void MainWindow::UpdateResultTableWidget(QString &msg){
if(msg == ""){
return;
}
tcp_meta tm(msg);//元数据类
int rows = ResultTableWidget->rowCount();
ResultTableWidget->setRowCount(rows + 1);
QTableWidgetItem *item = new QTableWidgetItem(tm.filename);
item->setTextAlignment(Qt::AlignCenter);
ResultTableWidget->setItem(rows, 0, item);
item = new QTableWidgetItem(QString::number(tm.size));
item->setTextAlignment(Qt::AlignCenter);
ResultTableWidget->setItem(rows, 1, item);
item = new QTableWidgetItem(tm.filepath);
item->setTextAlignment(Qt::AlignCenter);
ResultTableWidget->setItem(rows, 2, item);
item = new QTableWidgetItem(tm.owner);
item->setTextAlignment(Qt::AlignCenter);
ResultTableWidget->setItem(rows, 3, item);
if(tm.online == "on")
item = new QTableWidgetItem("在线");
else
item = new QTableWidgetItem("离线");
item->setTextAlignment(Qt::AlignCenter);
ResultTableWidget->setItem(rows, 4, item);
item = new QTableWidgetItem(tm.ip);
item->setTextAlignment(Qt::AlignCenter);
ResultTableWidget->setItem(rows, 5, item);
item = new QTableWidgetItem("check");
item->setFlags(Qt::ItemIsUserCheckable|Qt::ItemIsEnabled|Qt::ItemIsSelectable);
item->setCheckState(Qt::Unchecked);
item->setTextAlignment(Qt::AlignCenter);
ResultTableWidget->setItem(rows, 6, item);
}
void MainWindow::UpdateShareTableWidget(QString &msg){
if(msg == ""){
return;
}
tcp_meta tm(msg);//元数据类
int rows = ShareTableWidget->rowCount();
ShareTableWidget->setRowCount(rows + 1);
QTableWidgetItem *item = new QTableWidgetItem(tm.filename);
item->setTextAlignment(Qt::AlignCenter);
ShareTableWidget->setItem(rows, 0, item);
item = new QTableWidgetItem(QString::number(tm.size));
item->setTextAlignment(Qt::AlignCenter);
ShareTableWidget->setItem(rows, 1, item);
item = new QTableWidgetItem(tm.filepath);
item->setTextAlignment(Qt::AlignCenter);
ShareTableWidget->setItem(rows, 2, item);
item = new QTableWidgetItem(tm.ip);
item->setTextAlignment(Qt::AlignCenter);
ShareTableWidget->setItem(rows, 3, item);
item = new QTableWidgetItem(QString::number(tm.port));
item->setTextAlignment(Qt::AlignCenter);
ShareTableWidget->setItem(rows, 4, item);
item = new QTableWidgetItem("check");
item->setFlags(Qt::ItemIsUserCheckable|Qt::ItemIsEnabled|Qt::ItemIsSelectable);
item->setCheckState(Qt::Unchecked);
item->setTextAlignment(Qt::AlignCenter);
ShareTableWidget->setItem(rows, 5, item);
}
void MainWindow::slotConnected(){
EnterBtn->setText("注销");
ShareBtn->setEnabled(true);
FlushBtn->setEnabled(true);
DisableBtn->setEnabled(true);
SearchBtn->setEnabled(true);
DownloadBtn->setEnabled(true);
SignBtn->setEnabled(false);
int length = 0;
QString passwd = UserPasswdLineEdit->text();
QString msg = UserName + ";" + passwd + ";";
FormatMsg(msg, NAMEMSG);
if((length = tcpSocket->write(msg.toLatin1(),msg.length())) != msg.length()){
return;
}
}
void MainWindow::slotDisconnected(){
EnterBtn->setText("登陆");
ShareBtn->setEnabled(false);
FlushBtn->setEnabled(false);
DisableBtn->setEnabled(false);
SearchBtn->setEnabled(false);
DownloadBtn->setEnabled(false);
SignBtn->setEnabled(true);
}
在上述代码中,有一个函数FormatMsg,这个函数用于格式化客户机发送给服务器的消息,我们会在第5节汇总所有的通信协议,同时在这也给出了代码:
//head is 8 bits
void MainWindow::FormatMsg(QString &msg, MsgKind kind){
qint64 len = msg.length();
QString s = QString::number(len);
if(len > 100000){
msg = "error|||";
return;
}
//长度字符长度为6,不够补零
for(int i = s.length(); i < 6; ++i){
s = "0" + s;
}
switch (kind) {
case NAMEMSG:
msg = "NM" + s + msg;
break;
case METAMSG:
msg = "MM" + s + msg;
break;
case DISCONNECT:
msg = "DC" + s + msg;
break;
case SHAREMSG:
msg = "SM" + s + msg;
break;
case DELETEMSG:
msg = "DM" + s + msg;
break;
case SEARCHMSG:
msg = "SR" + s + msg;
break;
case SIGNMSG:
msg = "GM" + s + msg;
break;
case OTHERMSG:
msg = "OT" + s + msg;
break;
}
}
除此之外,客户机的一些按键需要绑定一些函数,用于向服务器发送请求消息,例如查询自己共享文件信息以及搜索文件信息。
在tcp_client.h文件中添加如下代码:
public slots:
void slotSendMeta();
void slotFlushShare();
void slotDeleteShare();
void slotSearch();
上传共享文件信息功能,添加如下代码:
void MainWindow::slotSendMeta(){
QString filename = QFileDialog::getOpenFileName(this,"选择要共享的文件","/","files (*)");
QFileInfo info(filename);
tcp_meta mt;
int length = 0;
if(filename == ""){
return;
}
mt.filename = info.fileName();
mt.size = info.size();
mt.ip = tcpSocket->localAddress().toString();
mt.owner = UserName;
mt.port = port;
mt.filepath = info.filePath();
QString msg = mt.toString();//组织元数据消息
FormatMsg(msg, METAMSG);
if((length = tcpSocket->write(msg.toLatin1(),msg.length())) != msg.length()){
return;
}
}
删除共享文件功能,添加如下代码:
void MainWindow::slotDeleteShare(){
QString msg = UserName + ";";
int rows = ShareTableWidget->rowCount();
int length = 0;
//将选中项组织成一条消息
for(int i = 0; i < rows; ++i){
if(ShareTableWidget->item(i,5)->checkState() == Qt::Checked){
msg = msg + ShareTableWidget->item(i,0)->text() + ";";
}
}
FormatMsg(msg, DELETEMSG);
if((length = tcpSocket->write(msg.toLatin1(),msg.length())) != msg.length()){
return;
}
}
获取我的共享文件信息功能,添加如下代码:
void MainWindow::slotFlushShare(){
QString msg = UserName;
int length;
FormatMsg(msg, SHAREMSG);
if((length = tcpSocket->write(msg.toLatin1(),msg.length())) != msg.length()){
return;
}
}
搜索文件功能,添加如下代码:
void MainWindow::slotSearch(){
QString filename = SearchLineEdit->text();
if(filename == ""){
return ;
}
QString msg = UserName + ";" + filename + ";";
int length;
FormatMsg(msg, SEARCHMSG);
if((length = tcpSocket->write(msg.toLatin1(),msg.length())) != msg.length()){
return;
}
}
我们将这几个功能都绑定到具体的按钮上,在MainWindow::MainWindow(QWidget *parent)函数中添加如下代码:
connect(EnterBtn,SIGNAL(clicked()),this,SLOT(slotEnter()));
connect(SignBtn,SIGNAL(clicked()),this,SLOT(slotSignUp()));
connect(ShareBtn,SIGNAL(clicked()),this,SLOT(slotSendMeta()));
connect(FlushBtn,SIGNAL(clicked()),this,SLOT(slotFlushShare()));
connect(DisableBtn,SIGNAL(clicked()),this,SLOT(slotDeleteShare()));
connect(SearchBtn,SIGNAL(clicked()),this,SLOT(slotSearch()));
4.2 P2P下载功能设计
我们设计的P2P下载功能很简单,在搜索栏中输入文件全名,点击搜索获取搜索结果并且显示在右下角区域内,将要下载的文件选中。点击选中位置选择一个保存路径,最后点击下载选中项获取文件。
要实现P2P下载功能,最重要的一点是为每个客户机设计一个P2P服务器用于监听其他客户机发来的下载请求,其代码可以仿照服务器的设计,代码如下:
新建p2p_server.h文件,添加代码:
#ifndef P2P_SERVER
#define P2P_SERVER
#include <QTcpServer>
#include <QObject>
#include <QString>
#include <QTcpSocket>
#include "tcp_meta.h"//包含元数据类
class P2P_Server : public QTcpServer
{
Q_OBJECT
public:
P2P_Server(QObject *parent = 0, int port = 0);
~P2P_Server();
private slots:
void checkdata();//核验传输请求
void slotDisconnected();
protected:
void incomingConnection(int socketDescriptor);
private:
QTcpSocket *tcp_client_socket;//建立传输连接
void transfer(QString& filepath);//传输文件函数
};
#endif // P2P_SERVER
新建p2p_server.cpp文件,添加代码:
#include "p2p_server.h"
#include <QFile>
#include <QFileDialog>
P2P_Server::P2P_Server(QObject *parent, int port)
:QTcpServer(parent)
{
tcp_client_socket = NULL;
listen(QHostAddress::Any, port);//监听
}
//建立连接
void P2P_Server::incomingConnection(int socketDescriptor){
tcp_client_socket = new QTcpSocket(this);
connect(tcp_client_socket,SIGNAL(readyRead()),this,SLOT(checkdata()));
connect(tcp_client_socket,SIGNAL(disconnected()),this,SLOT(slotDisconnected()));
tcp_client_socket->setSocketDescriptor(socketDescriptor);
qDebug() << "connected";
}
//传输请求格式:#len#filepath#
#define PAK_ERR "PKE"//包错误
#define FP_ERR "FPE"//文件名错误
#define SUCC "SUC"//请求正确
void P2P_Server::checkdata(){
if(tcp_client_socket->bytesAvailable() >= 11){
char buf[1024];
tcp_client_socket->read(buf,11);
QString msg;
if(buf[0] != '#'){
msg = PAK_ERR;
tcp_client_socket->write(msg.toLatin1(), msg.length());
return;
}
msg = buf;
long len = msg.mid(1, 10).toLong();//文件名长度
tcp_client_socket->read(buf,len + 2);
msg = buf;
QString filepath = msg.mid(1, len);
transfer(filepath);//传输文件
}
}
void P2P_Server::transfer(QString& filepath){
QFile file(filepath);
QFileInfo info(file);
qint64 filesize = info.size();
QString msg;
if(file.open(QIODevice::ReadOnly)){
char buf[257];
msg = SUCC;
tcp_client_socket->write(msg.toLatin1(), msg.length());
for(int i = 0; i < filesize / 256; ++i) {
file.read(buf, 256);//读文件内容
qDebug() << tcp_client_socket->write(buf, 256);
qDebug() << tcp_client_socket->waitForBytesWritten(3000);//等待传输完成
}
//将剩余文件传输
file.read(buf, filesize % 256);
qDebug() << tcp_client_socket->write(buf, filesize % 256);
qDebug() << tcp_client_socket->waitForBytesWritten(3000);
}
else{
msg = FP_ERR;
tcp_client_socket->write(msg.toLatin1(), msg.length());
}
}
void P2P_Server::slotDisconnected(){
}
P2P_Server::~P2P_Server(){
if(tcp_client_socket != NULL)
delete tcp_client_socket;
}
此外,我们还需要设计一个类专门负责处理下载任务,其他函数可以实例化这个类新建下载任务,然后调用这个类的启动传输函数完成P2P下载。我们设计的这个类定义如下:
新建p2p_download.h文件,添加代码:
#ifndef P2P_DOWNLOAD
#define P2P_DOWNLOAD
#include <QtNetwork/QHostAddress>
#include <QtNetwork/QTcpSocket>
#include "tcp_meta.h"
class P2P_Download : public QTcpSocket
{
Q_OBJECT
public:
P2P_Download(QObject *parent, tcp_meta& tm);
~P2P_Download();
qint64 progress;//下载进度
bool status;//状态
bool start_as_client(QString dirname);//启动下载
private:
int port;//p2p下载对方的端口号
QHostAddress *ServerIP;//p2p下载对方的IP地址
QString UserName;//用户名
QString Dirname;//保存路径
QString Dfilepath;//下载文件路径
QString Dfilename;//下载文件名
qint64 filesize;//下载文件大小
private slots:
void ClientConnected();//下载连接建立
void ClientDisconnected();//下载连接断开
void ClientDataReceived();//下载内容到来进入该函数
};
#endif // P2P_DOWNLOAD
新建p2p_download.cpp文件,添加代码:
#include "p2p_download.h"
#include <QFile>
P2P_Download::P2P_Download(QObject *parent, tcp_meta &tm)
:QTcpSocket(parent)
{
status = true;
port = tm.port + 1;
ServerIP = new QHostAddress;
ServerIP->setAddress(tm.ip);
UserName = tm.owner;
Dfilepath = tm.filepath;
Dfilename = tm.filename;
filesize = tm.size;
}
bool P2P_Download::start_as_client(QString dirname){
if(dirname == ""){
return false;
}
Dirname = dirname;
connect(this,SIGNAL(connected()),this,SLOT(ClientConnected()));
connect(this,SIGNAL(disconnected()),this,SLOT(ClientDisconnected()));
connect(this,SIGNAL(readyRead()),this,SLOT(ClientDataReceived()));
connectToHost(*ServerIP,port);
return true;
}
void P2P_Download::ClientConnected(){
QString msg = "#";
QString slen = QString::number(Dfilepath.length());
for(int i = slen.length(); i < 10; ++i){
slen = "0" + slen;
}
msg = msg + slen + "#" + Dfilepath + "#";
write(msg.toLatin1(), msg.length());
}
#define PAK_ERR "PKE"
#define FP_ERR "FPE"
#define SUCC "SUC"
void P2P_Download::ClientDataReceived(){
if(bytesAvailable() >= 3){
char buf[256];
read(buf, 3);
QString msg = buf;
if(msg.mid(0,3) == PAK_ERR){
}
else if(msg.mid(0,3) == FP_ERR){
}
else if(msg.mid(0,3) == SUCC){
QFile dfile(Dirname + Dfilename);
qint64 tsz = 0;
if(dfile.open(QIODevice::ReadWrite)){
for(int i = 0; i < filesize / 256; ++i){
while(bytesAvailable() < 256){ //block
//下载超时
if(!waitForReadyRead(3000)){
qDebug() << "timeout";
disconnectFromHost();
status = false;
return;
}
}
read(buf, 256);
//写入本地电脑
dfile.write(buf, 256);
tsz = tsz + 256;
progress = tsz * 100 / filesize;
qDebug() << progress;
}
while(bytesAvailable() < (filesize % 256)){ //block
if(!waitForReadyRead(3000)){
qDebug() << "timeout";
disconnectFromHost();
status = false;
return;
}
}
read(buf, filesize % 256);
dfile.write(buf, filesize % 256);
progress = 100;
qDebug() << progress;
}
status = true;
disconnectFromHost();
}
}
}
void P2P_Download::ClientDisconnected(){
}
P2P_Download::~P2P_Download(){
this->close();
}
接下来,我们为一些按钮绑定事件,
在tcp_client.h文件中增加代码:
class MainWindow : public QMainWindow
{
...
private:
...
P2P_Server *p2pserver;
QList<P2P_Download*> tasks;
QString ClientDir;
...
public slots:
...
void slotSelectDir();
void slotDownload();
void timeout();
...
};
在tcp_client.cpp文件中增加代码:
void MainWindow::slotSelectDir(){
QString dirname = QFileDialog::getExistingDirectory(this,"选择文件保存位置","/");
DirLineEdit->setText(dirname);
}
void MainWindow::slotDownload(){
QString dirname = DirLineEdit->text();
if(dirname == ""){
return;
}
int rows = ResultTableWidget->rowCount();
bool flag = false;
//为选中项新建下载任务
for(int i = 0; i < rows; ++i){
if(ResultTableWidget->item(i,6)->checkState() == Qt::Checked
&& ResultTableWidget->item(i,4)->text() == "在线"){
tcp_meta tm;
tm.filename = ResultTableWidget->item(i,0)->text();
tm.size = ResultTableWidget->item(i,1)->text().toLong();
tm.filepath = ResultTableWidget->item(i,2)->text();
tm.owner = ResultTableWidget->item(i,3)->text();
tm.ip = ResultTableWidget->item(i,5)->text();
tm.port = port;
//新建下载任务到下载队列
tasks.append(new P2P_Download(this, tm));
//启动下载
tasks.last()->start_as_client(dirname);
//定时器用于更新下载进度条
if(flag == false)
timer->start(100);
flag = true;
}
}
}
void MainWindow::timeout(){
//下载任务数为0
if(tasks.count() == 0){
timer->stop();
return;
}
//更新进度条
ProgressBar->setValue(tasks.at(0)->progress);
if(tasks.at(0)->progress == 100 || tasks.at(0)->status == false){
if(tasks.at(0)->status == false)
ProgressBar->setValue(0);
delete tasks.at(0);
tasks.removeAt(0);
}
}
在MainWindow::MainWindow(QWidget *parent)函数中绑定事件
connect(SelectDirBtn,SIGNAL(clicked()),this,SLOT(slotSelectDir()));
connect(DownloadBtn,SIGNAL(clicked()),this,SLOT(slotDownload()));
connect(timer,SIGNAL(timeout()),this,SLOT(timeout()));
5. meta元数据与通信协议介绍
在设计中,我们将共享文件的相关信息用一个类表示,下表中列出了定义类中的元素:
元素 | 意义 |
---|---|
filename | 共享文件的文件名 |
size | 共享文件的大小 |
owner | 共享文件的拥有者用户名 |
ip | 共享文件所在的ip地址 |
port | 共享文件所在客户机的下载端口号 |
filepath | 共享文件所在客户机的绝对路径 |
online | 共享文件所在客户机的状态(在线或离线) |
我们将这些信息组织为一个类,并提供将信息组织为信息字符串的方法:
新建tcp_meta.h文件,添加代码:
#ifndef TCP_META
#define TCP_META
#include <QString>
struct tcp_meta{
tcp_meta(){}
tcp_meta(QString);//提供信息字符串初始化为元数据类的方法
QString filename;
qint64 size;
QString owner;
QString ip;
qint64 port;
QString filepath;
QString online;
bool status;
QString toString();
};
#endif // META
新建tcp_meta.cpp文件,添加代码:
#include "tcp_meta.h"
#include <QVector>
// #S#FN:filename;SZ:size;OW:owner;IP:ip;PT:port#E#
tcp_meta::tcp_meta(QString mt){
QString head = mt.mid(0,3);
if(head != "#S#"){
status = false;
return;
}
int i = 3, k = i;
QVector<QString> strs;
strs.push_back("FN:");strs.push_back("SZ:");strs.push_back("OW:");
strs.push_back("IP:");strs.push_back("PT:");strs.push_back("FP:");
strs.push_back("OL:");
for(int cnt = 0; cnt < 7; ++cnt){
head = mt.mid(i,3);
if(head != strs.at(cnt)){
status = false;
return;
}
else {
i = i + 3;
k = i;
while(mt.at(i++) != ';');
strs[cnt] = mt.mid(k, i - k - 1);
k = i;
}
}
filename = strs.at(0);
size = strs.at(1).toLong();
owner = strs.at(2);
ip = strs.at(3);
port = strs.at(4).toLong();
filepath = strs.at(5);
online = strs.at(6);
status = true;
}
QString tcp_meta::toString(){
QString res;
res = res + "#S#";
res = res + "FN:" + filename + ";";
res = res + "SZ:" + QString::number(size) + ";";
res = res + "OW:" + owner + ";";
res = res + "IP:" + ip + ";";
res = res + "PT:" + QString::number(port) + ";";
res = res + "FP:" + filepath + ";";
res = res + "OL:" + online + ";";
res = res + "#E#";
return res;
}
信息字符串格式如下:
含义 | 字符串 |
---|---|
信息头 | #S# |
文件名 | FN:filename; |
文件大小 | SZ:文件大小; |
拥有者 | OW:owner; |
IP地址 | IP:ip; |
端口号 | PT:port; |
文件路径 | FP:filepath; |
在线状态 | OL:online; |
信息尾 | #E# |
这个类在客户机和服务器中都会用到。除此之外,客户机向服务器传输消息以及服务器向客户机传输消息都有各自的协议。
客户机向服务器传输消息格式为:
1-2 | 3-8 | 9- |
---|---|---|
信息类型 | 信息长度 | 信息体 |
所有类型如下表:
信息类型 | 信息体格式 | 含义 |
---|---|---|
NM | username;password; | 登陆信息 |
MM | 元数据字符串 | 上传元数据信息 |
DC | - | 断联信息 |
SM | username | 获取共享文件信息 |
DM | username;filename;…;username; | 删除共享文件信息 |
SR | username;filename; | 搜索信息 |
GM | username;password; | 注册信息 |
OT | 消息字符串 | 通知信息 |
服务器向客户机传输消息格式如下:
1-2 | 3-8 | 9- |
---|---|---|
信息类型 | 信息长度 | 信息体 |
所有类型如下表:
信息类型 | 信息体格式 | 含义 |
---|---|---|
RK | meta元数据字符串 | 返回共享文件元数据信息 |
SR | meta元数据字符串 | 返回搜索结果 |
EK | 错误信息字符串 | 断联或错误信息 |
SS | 注册成功字符串 | 注册成功信息 |
SF | 注册失败字符串 | 注册失败信息 |
OT | 消息字符串 | 通知信息 |
这些协议都有各自的函数负责格式化消息,之前已经提到。
至此,所有设计都已经讲到,如有疑问欢迎私信或留言。
注:以上代码要正确运行需要注意头文件的包含以及pro文件中内容的添加。例如要使用数据库功能需要在工程文件中添加QT += sql
语句。最简单是直接使用文章最开始提供的源代码包。