本文实验测试部分可参考基于QT的一款P2P共享文件系统
源码包下载地址基于QT的一款P2P共享文件系统下载,想要免费获取可以私信我
Github地址
1. 前言
文章讲述一款简单P2P共享文件软件的制作,设计环境是mysql、windows与QT5.5。要实现的功能包括用户的注册与登录,用户共享文件的上传、删除与下载。同时也具有精确搜索的功能,用户可以通过搜索结果确定要下载的文件。本文会通过服务器的设计、客户端的设计以及服务端客户端通信协议三个方面进行详细的介绍。本人知识有限,文中可能会有一些纰漏,欢迎留言指正,如有疑问也欢迎留言。
2. 系统总体框架
在本文中,我们设计一款P2P共享文件系统,其P2P网络仿照类似Napster的中心化网络,如下图所示:
图中,5个客户机与一个服务器连接构成P2P网络,服务器中存放有所有客户机上传的共享文件的meta信息(包括文件的拥有者、文件名、文件在主机中的绝对路径、IP地址、下载端口号、文件大小、文件拥有者主机状态)。橙色线描述出客户机与服务器和客户机之间的互动,客户机1正在共享一个文件并上传其meta信息;客户机2向服务器发起查询,并提交了要查询文件的meta信息,然后服务器返回了查询结果,客户机2利用查询结果找到了文件的位置,并向客户机3发起下载请求,最后,客户机3接受了下载请求并开始传输文件。
3. 服务器设计
服务端要实现的功能主要是meta数据的管理,因此其界面不需要设计的有多复杂。服务器效果图如下:
界面主要包含四个部分,左上角空白区域用于显示消息,端口左边的输入框用于输入服务器的端口,左下角是一个服务器启动按钮,右边用于刷新当前在线用户,需要添加的代码如下:
建立tcp_server.h,添加如下代码
#ifndef TCP_SERVER_H
#define TCP_SERVER_H
#include <QMainWindow>
#include <QWidget>
#include <QListWidget>
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>
#include <QGridLayout>
#include <QTreeWidgetItem>
#include <QString>
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = 0);
~MainWindow();
private:
QWidget *myWidget;
QListWidget *ContentListWidget;
QLabel *PortLabel;
QLineEdit *PortLineEdit;
QPushButton *CreateBtn;
QTreeWidget *resourceTree;
QGridLayout *mainLayout;
int port;
};
#endif // TCP_SERVER_H
建立tcp_server.cpp文件,添加如下代码:
#include "tcp_server.h"
#include <QMessageBox>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
setWindowTitle("P2P Server");
ContentListWidget = new QListWidget;
myWidget = new QWidget;
PortLabel = new QLabel("端口:");
PortLineEdit = new QLineEdit;
CreateBtn = new QPushButton("打开服务器");
resourceTree = new QTreeWidget();
resourceTree->setHeaderLabel("在线用户");
resourceTree->clear();
mainLayout = new QGridLayout();
mainLayout->addWidget(ContentListWidget,0,0,1,2);
mainLayout->addWidget(PortLabel,1,0);
mainLayout->addWidget(PortLineEdit,1,1);
mainLayout->addWidget(CreateBtn,2,0,1,2);
mainLayout->addWidget(resourceTree,0,2,3,1);
myWidget->setLayout(mainLayout);
setCentralWidget(myWidget);
port = 8010;
PortLineEdit->setText(QString::number(port));
}
MainWindow::~MainWindow()
{
delete PortLabel;
delete PortLineEdit;
delete CreateBtn;
delete ContentListWidget;
resourceTree->clear();
delete resourceTree;
delete mainLayout;
delete myWidget;
}
为了能够实现不同主机之间的通信,我们使用了tcp可靠传输协议。在服务器这边,我们需要实现一个服务器实例,用于监听保存相应的tcp连接以及完成客户端的相应请求。tcp服务器代码如下:
建立tcp_socket_client.h,添加如下代码:
#ifndef TCP_CLIENT_SOCKET
#define TCP_CLIENT_SOCKET
#include <QTcpSocket>
#include <QObject>
#include <QString>
class Tcp_Client_Socket : public QTcpSocket
{
Q_OBJECT
public:
Tcp_Client_Socket(QObject *parent = 0);
~Tcp_Client_Socket();
signals:
void Disconnected(int);
protected slots:
void DataReceived();
void slotDisconnected();
};
#endif // TCP_CLIENT_SOCKET
建立tcp_socket_client.cpp文件,添加如下代码:
#include "tcp_client_socket.h"
#include <QByteArray>
Tcp_Client_Socket::Tcp_Client_Socket(QObject *parent)
:QTcpSocket(parent)
{
connect(this,SIGNAL(readyRead()),this,SLOT(DataReceived()));
connect(this,SIGNAL(disconnected()),this,SLOT(slotDisconnected()));
}
void Tcp_Client_Socket::DataReceived(){
if(bytesAvailable() > 0) {
//tcp连接有数据过来,需要做相应处理
}
}
//发送断开连接信号
void Tcp_Client_Socket::slotDisconnected(){
emit Disconnected(this->socketDescriptor());
}
Tcp_Client_Socket::~Tcp_Client_Socket(){
}
建立server.h,添加如下代码:
#ifndef SERVER
#define SERVER
#include <QTcpServer>
#include <QObject>
#include <QList>
#include <QString>
#include <QPair>
#include "tcp_client_socket.h"
#include "tcp_meta.h"//元数据类
class Server : public QTcpServer
{
Q_OBJECT
public:
Server(QObject *parent = 0, int port = 0);
~Server();
QList<QPair<Tcp_Client_Socket*, QString> > tcp_client_socket_list;//用于保存tcp连接
enum MsgKind{
UpdateName = 0,
UPDATEMETA = 1,
UpdateMsg = 2,
RemoveName = 3,
};
signals:
void UpdateServer(QString, int, Server::MsgKind);
private slots:
QTcpSocket* find_socket(QString);
void slotDisconnected(int);
protected:
void incomingConnection(int socketDescriptor);
};
#endif // SERVER
建立server.cpp文件,添加如下代码:
#include "server.h"
#include <QPair>
Server::Server(QObject *parent, int port)
:QTcpServer(parent)
{
listen(QHostAddress::Any, port);
}
QTcpSocket* Server::find_socket(QString username){
int i = 0;
for(i = 0; i < tcp_client_socket_list.count(); ++i){
if(tcp_client_socket_list.at(i).second == username){
return tcp_client_socket_list.at(i).first;
}
}
return NULL;
}
void Server::incomingConnection(int socketDescriptor){
Tcp_Client_Socket *tcp_client_socket = new Tcp_Client_Socket(this);
connect(tcp_client_socket,SIGNAL(Disconnected(int)),this,SLOT(slotDisconnected(int)));
tcp_client_socket->setSocketDescriptor(socketDescriptor);
QString name = "Unknown";
QPair<Tcp_Client_Socket*, QString> pair(tcp_client_socket, name);
tcp_client_socket_list.append(pair);
}
void Server::slotDisconnected(int descriptor){
int i = 0;
for(i = 0; i < tcp_client_socket_list.count(); ++i){
QTcpSocket *item = tcp_client_socket_list.at(i).first;
if(item->socketDescriptor() == descriptor){
if(tcp_client_socket_list.at(i).second != "Unknown")
emit UpdateServer("", i, RemoveName);
tcp_client_socket_list.removeAt(i);
break;
}
}
}
Server::~Server(){
for(int i = 0; i < tcp_client_socket_list.count(); ++i){
delete tcp_client_socket_list.at(i).first;
}
}
我们为打开服务器按钮连接一个槽函数,用于实例化服务器,并且连接数据库用于增删改查meta元数据。相应文件需要包含QSqlDatabase与QSqlQuery两个头文件。
在tcp_server.h文件中添加如下代码:
#include "server.h"
...
public slots:
void slotCreateServer();//打开服务器
void UpdateServer(QString, int, Server::MsgKind);//更新服务器在线用户的显示列表
private:
...
Server *server;//服务器
...
在tcp_server.cpp中添加如下代码:
#include <QSqlDatabase>
#include <QSqlQuery>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
...
//将按钮点击事件与实例化服务器函数绑定
connect(CreateBtn,SIGNAL(clicked()),this,SLOT(slotCreateServer()));
server = 0;
}
void MainWindow::slotCreateServer(){
//实例化服务器
server = new Server(this, port);
server->db = QSqlDatabase::addDatabase("QMYSQL");
server->db.setHostName("localhost");
server->db.setDatabaseName("ShareFile");
server->db.setUserName("root");
server->db.setPassword("root");
//连接数据库
if (!server->db.open()) {
QMessageBox::critical(0, QObject::tr("无法打开数据库"),
"无法创建数据库连接! ", QMessageBox::Cancel);
return;
}
//将来自server的updateserver信号与UpdateServer函数绑定
connect(server,SIGNAL(UpdateServer(QString,int,Server::MsgKind)),this,SLOT(UpdateServer(QString,int,Server::MsgKind)));
CreateBtn->setEnabled(false);
}
void MainWindow::UpdateServer(QString msg, int length, Server::MsgKind flag){
switch (flag) {
//普通消息,直接显示到content list组件中
case Server::UpdateMsg:
{
ContentListWidget->addItem(msg.left(length));
break;
}
//更新元数据消息
case Server::UPDATEMETA:
{
ContentListWidget->addItem(msg + ": update meta");
break;
}
//更新在线用户,将上线的用户显示到tree组件上
case Server::UpdateName:
{
QTreeWidgetItem *item_name = new QTreeWidgetItem(resourceTree);
item_name->setText(0,msg);
break;
}
//用户离线消息,更新tree组件,删除相应的在线用户
case Server::RemoveName:
{
QPoint p(length,0);
resourceTree->removeItemWidget(resourceTree->itemAt(p),0);
delete resourceTree->itemAt(p);
break;
}
}
}
MainWindow::~MainWindow()
{
...
//删除服务器,关闭数据库连接
if(server != 0){
server->db.close();
delete server;
}
}
到这,服务器的基本框架已经搭建完成,接着需要添加一些功能,例如数据库的增删改查。
先在server.h文件中添加功能函数的声明:
#include <QSqlDatabase>
#include <QSqlQuery>
#include "tcp_meta.h"//元数据类文件声明
class Server : public QTcpServer
{
...
QSqlDatabase db;//建立连接的数据库
...
signals:
void UpdateServer(QString, int, Server::MsgKind);
private slots:
...
void UpdateClients(QString, int);
void UpdateUserName(QString);
void UpdateUserInfo(QString);
void UpdateMeta(QString);
void ReturnMeta(QString);
void DeleteMeta(QString);
void SearchMeta(QString);
...
};
在server.cpp文件中添加广播消息功能函数:
void Server::UpdateClients(QString msg, int length){
//发送信号,使得服务器界面中可以显示更新消息
emit UpdateServer(msg, length, UpdateMsg);
//遍历所有保存的tcp连接,发送消息
for(int i = 0; i < tcp_client_socket_list.count(); ++i){
QTcpSocket *item = tcp_client_socket_list.at(i).first;
format_packet fmsg(msg, OTHERKIND);//将消息打包
if(item->write(fmsg.fmpak.toLatin1(),length + 8) != length){ //发送消息
continue;
}
}
}
format_packet是我们自定义的一个包装类,之后会讲到,接下来添加检测登陆信息的函数:
void Server::UpdateUserName(QString msg){
int i = 0;
int pos = i;
//登陆信息的格式为"username;password;"
//解析消息msg
while(msg.at(i++) != ';');
QString username = msg.mid(pos, i - 1);
pos = i;
while(msg.at(i++) != ';');
QString password = msg.mid(pos, i - pos - 1);
//实例化query
QSqlQuery query(db);
//默认用户登陆时的tcp连接为最新一次建立的tcp连接(为了简单,但可能存在bug)
QTcpSocket *item = tcp_client_socket_list.last().first;
//从login表中根据用户名查找相应的用户的信息
query.exec("select * from login where Username='" + username + "'");
if(!query.next()){
msg = "Server: login error, unknown user name.";
format_packet fmsg(msg, ERRDCKIND);
//通知对应用户登陆失败,没有此用户
item->write(fmsg.fmpak.toLatin1(),fmsg.fmpak.length());
}
else{
//密码错误,通知用户
if(query.value(2).toString() != password){
msg = "Server: login error, password is wrong.";
format_packet fmsg(msg, ERRDCKIND);
item->write(fmsg.fmpak.toLatin1(),fmsg.fmpak.length());
}
//为最新建立的tcp连接设置用户名用于辨认
else{
tcp_client_socket_list.last().second = username;
//发送信号,更新在线用户列表
emit UpdateServer(username, username.length(), UpdateName);
}
}
}
其次,我们添加注册功能函数:
void Server::UpdateUserInfo(QString msg){
int i = 0;
int pos = i;
//解析注册信息字符串,格式与登陆一样
while(msg.at(i++) != ';');
QString username = msg.mid(pos, i - 1);
pos = i;
while(msg.at(i++) != ';');
QString password = msg.mid(pos, i - pos - 1);
QSqlQuery query(db);
QTcpSocket *item = tcp_client_socket_list.last().first;
//在login表中查找用户名
query.exec("select * from login where Username='" + username + "'");
//存在此用户,通知注册失败
if(query.next()){
msg = "Server: sign error, user name exists.";
format_packet fmsg(msg, SIGNFAILKIND);
item->write(fmsg.fmpak.toLatin1(),fmsg.fmpak.length());
}
//更新login表,插入一条数据
//插入失败,通知注册失败
else{
if(!query.exec("insert into login values(0, '" + username +
"', '" + password + "')")){
msg = "Server: sign error";
format_packet fmsg(msg, SIGNFAILKIND);
item->write(fmsg.fmpak.toLatin1(),fmsg.fmpak.length());
}
//插入成功,通知注册成功
else {
msg = "Server: sign success";
format_packet fmsg(msg, SIGNSCKIND);
item->write(fmsg.fmpak.toLatin1(),fmsg.fmpak.length());
}
}
}
添加“增加共享文件”功能函数:
void Server::UpdateMeta(QString mt){
QSqlQuery query(db);
tcp_meta mt_t(mt);//元数据类
//插入数据到resource表中
QString query_str = "insert into Resource values(0, ";
query_str = query_str + "'" + mt_t.filename + "', ";
query_str = query_str + QString::number(mt_t.size) + ", ";
query_str = query_str + "'" + mt_t.filepath + "', ";
query_str = query_str + "'" + mt_t.owner + "', ";
query_str = query_str + "'" + mt_t.ip + "', ";
query_str = query_str + QString::number(mt_t.port) + ")";
//插入一条元数据记录,也就是在服务器中添加了一个共享文件
query.exec(query_str);
}
tcp_meta是我们设计用于表示共享文件元数据的类,之后我们会讲到。添加“删除共享文件”功能函数:
void Server::DeleteMeta(QString msg){
int i = 0;
int pos = i;
QSqlQuery query(db);
//删除文件信息格式为"username;filename;...;filename;"
while(msg.at(i++) != ';');
QString username = msg.mid(pos, i - 1);
for(pos = i; i < msg.length(); ++i){
if(msg.at(i) == ';'){
QString filename = msg.mid(pos, i - pos);
//删除对应记录
query.exec("delete from Resource where FileName='" + filename
+ "' AND Owner='" + username + "'");
pos = i + 1;
}
}
}
用户通过发送“username;filename”可以接收到以filename命名的所有共享文件元数据信息。添加“搜索共享文件”功能函数:
void Server::SearchMeta(QString msg){
int i = 0;
int pos = i;
//解析msg字符串
while(msg.at(i++) != ';');
QString username = msg.mid(pos, i - 1);
pos = i;
while(msg.at(i++) != ';');
QString filename = msg.mid(pos, i - pos - 1);
bool flag = false;//查找成功标志
QSqlQuery query(db);
QTcpSocket *item = find_socket(username);//找到username用户的socket连接
//查找失败退出函数
if(item == NULL){
return;
}
//查找以filename命名的所有元数据
query.exec("select * from Resource where FileName='" + filename + "'");
//依次发送给username用户
while(query.next())
{
tcp_meta mt;//元数据类
mt.filename = query.value(1).toString();
mt.size = query.value(2).toString().toLong();
mt.filepath = query.value(3).toString();
mt.owner = query.value(4).toString();
mt.ip = query.value(5).toString();
mt.port = query.value(6).toString().toLong();
//若不存在mt.owner用户的socket连接,说明对方不在线
if(find_socket(mt.owner) != NULL){
mt.online = "on";
}
else{
mt.online = "off";
}
QString msg = mt.toString();
format_packet fmsg(msg, SRHKIND);
//发送元数据信息
while(item->write(fmsg.fmpak.toLatin1(),msg.length() + 8) != msg.length() + 8);
//设置flag
flag = true;
}
//若查找失败,发送空字符串
if(flag == false){
QString msg = "";
format_packet fmsg(msg, SRHKIND);
while(item->write(fmsg.fmpak.toLatin1(),msg.length() + 8) != msg.length() + 8);
}
}
最后,添加获取自己所有共享文件信息的函数:
void Server::ReturnMeta(QString username){
bool flag = false;
QSqlQuery query(db);
QTcpSocket *item = find_socket(username);
if(item == NULL){
return;
}
//查找所有owner为username的共享文件元数据
query.exec("select * from Resource where Owner='" + username + "'");
//将其依次发送给username用户
while(query.next())
{
tcp_meta mt;
mt.filename = query.value(1).toString();
mt.size = query.value(2).toString().toLong();
mt.filepath = query.value(3).toString();
mt.owner = query.value(4).toString();
mt.ip = query.value(5).toString();
mt.port = query.value(6).toString().toLong();
QString msg = mt.toString();
format_packet fmsg(msg, RESKIND);
while(item->write(fmsg.fmpak.toLatin1(),msg.length() + 8) != msg.length() + 8);
flag = true;
}
//若查找失败或不存在相应文件发送空字符串
if(flag == false){
QString msg = "";
format_packet fmsg(msg, RESKIND);
while(item->write(fmsg.fmpak.toLatin1(),msg.length() + 8) != msg.length() + 8);
}
}
至此,我们服务器这边的功能算是都设计好了。接下来,我们要做的是如何在服务器中调用这些函数。前面已经提到,我们为服务器与客户机之间设计了通信协议,这些协议通俗来说就是一个有规律的字符串。在服务器向客户机发送消息或数据时,我们定义了一个format_packet类用于打包服务器向客户机发送的数据,同样地,在客户机向服务器发送数据时也会有相应的函数打包消息。所以,我们在接收到socket连接发送来的数据时,需要安装协议将其解析,并调用相应的功能函数完成客户机的需求。
我们将服务器对客户机消息的解析过程放在了tcp_client_socket.cpp文件中,因此我们需要添加相应的代码到tcp_client_socket.h文件与tcp_client_socket.cpp文件中:
tcp_client_socket.h:
class Tcp_Client_Socket : public QTcpSocket
{
signals:
...
void UpdateClients(QString, int);
void UpdateUserName(QString);
void UpdateMeta(QString);
void ReturnMeta(QString);
void DeleteMeta(QString);
void SearchMeta(QString);
void UpdateUserInfo(QString);
...
};
tcp_client_socket.cpp:
void Tcp_Client_Socket::DataReceived(){
if(bytesAvailable() > 0) {
char buf[1024];
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);
read(buf,msglen);//读具体消息内容
buf[msglen] = 0;
}
QString msg = buf;//消息字符串
//根据头信息中消息的kind种类,分别发送不同的信号
if(kind == "NM"){ //name messsage
QString logininfo = msg;
emit UpdateUserName(logininfo);
}
else if(kind == "MM"){ //meta message
emit UpdateMeta(msg);
}
else if(kind == "SM"){ //return share file message
QString username = msg;
emit ReturnMeta(username);
}
else if(kind == "DM"){ //delete share file message
emit DeleteMeta(msg);
}
else if(kind == "SR"){ //search share file message
emit SearchMeta(msg);
}
else if(kind == "DC"){ //disconnected message
}
else if(kind == "GM"){ //sign message
emit UpdateUserInfo(msg);
}
else if(kind == "OT"){ //normal message
emit UpdateClients(msg, msg.length());
}
}
}
通过上述代码,很明显我们使用信号与槽的方式调用这些功能函数,所以,我们还需要在tcp建立连接时将这些信号绑定到具体的槽中:
在server.cpp文件中修改incomingConnection函数:
void Server::incomingConnection(int socketDescriptor){
Tcp_Client_Socket *tcp_client_socket = new Tcp_Client_Socket(this);
connect(tcp_client_socket,SIGNAL(UpdateClients(QString,int)),this,SLOT(UpdateClients(QString,int)));
connect(tcp_client_socket,SIGNAL(Disconnected(int)),this,SLOT(slotDisconnected(int)));
connect(tcp_client_socket,SIGNAL(UpdateUserName(QString)),this,SLOT(UpdateUserName(QString)));
connect(tcp_client_socket,SIGNAL(UpdateUserInfo(QString)),this,SLOT(UpdateUserInfo(QString)));
connect(tcp_client_socket,SIGNAL(UpdateMeta(QString)),this,SLOT(UpdateMeta(QString)));
connect(tcp_client_socket,SIGNAL(ReturnMeta(QString)),this,SLOT(ReturnMeta(QString)));
connect(tcp_client_socket,SIGNAL(DeleteMeta(QString)),this,SLOT(DeleteMeta(QString)));
connect(tcp_client_socket,SIGNAL(SearchMeta(QString)),this,SLOT(SearchMeta(QString)));
tcp_client_socket->setSocketDescriptor(socketDescriptor);
QString name = "Unknown";
QPair<Tcp_Client_Socket*, QString> pair(tcp_client_socket, name);
tcp_client_socket_list.append(pair);
}
除此之外,我们也给出format_packet类定义与实现:
新建format_packet.h文件,内容如下:
#ifndef FORMAT_PACKET
#define FORMAT_PACKET
#include <QString>
enum fkind{
RESKIND = 0,//返回自己所有共享文件信息的消息
SRHKIND = 1,//搜索结果消息
ERRDCKIND = 2,//错误连接消息,一般表示登陆失败
SIGNSCKIND = 3,//注册成功消息
SIGNFAILKIND = 4,//注册失败消息
OTHERKIND = 5,//普通消息
};
struct format_packet{
format_packet(QString &str, fkind);
QString fmpak;
};
#endif // FORMAT_PACKET
新建format_packet.cpp文件,内容如下:
#include "format_packet.h"
format_packet::format_packet(QString &str, fkind kind){
long len = str.length();
QString s = QString::number(len);//字符串长度
if(len > 100000){
str = "error|||";//表示错误消息的头字符串
return;
}
//要求长度字符传长度为6,不够补零
for(int i = s.length(); i < 6; ++i){
s = "0" + s;
}
switch (kind) {
case RESKIND:
fmpak = "RK" + s + str;
break;
case SRHKIND:
fmpak = "SR" + s + str;
break;
case ERRDCKIND:
fmpak = "EK" + s + str;
break;
case SIGNSCKIND:
fmpak = "SS" + s + str;
break;
case SIGNFAILKIND:
fmpak = "SF" + s + str;
break;
case OTHERKIND:
fmpak = "OT" + s + str;
break;
}
}
对于通信这部分,我们在此只是简单提及设计,其具体内容之后我们还会提到。现在服务器的设计已经完成,下一步完成客户机的设计。