自己动手完成一款简易P2P共享文件软件的制作(一)
文章目录
- 1. 前言
- 2. 系统总体框架
- 3. 服务器设计
本文实验测试部分可参考基于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_OBJECTpublic: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;"//解析消息msgwhile(msg.at(i++) != ';');QString username = msg.mid(pos, i - 1);pos = i;while(msg.at(i++) != ';');QString password = msg.mid(pos, i - pos - 1);//实例化queryQSqlQuery 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);//设置flagflag = 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 messsageQString logininfo = msg;emit UpdateUserName(logininfo);}else if(kind == "MM"){ //meta messageemit UpdateMeta(msg);}else if(kind == "SM"){ //return share file messageQString username = msg;emit ReturnMeta(username);}else if(kind == "DM"){ //delete share file messageemit DeleteMeta(msg);}else if(kind == "SR"){ //search share file messageemit SearchMeta(msg);}else if(kind == "DC"){ //disconnected message}else if(kind == "GM"){ //sign messageemit UpdateUserInfo(msg);}else if(kind == "OT"){ //normal messageemit 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;}
}
对于通信这部分,我们在此只是简单提及设计,其具体内容之后我们还会提到。现在服务器的设计已经完成,下一步完成客户机的设计。
自己动手完成一款简易P2P共享文件软件的制作(二)
自己动手完成一款简易P2P共享文件软件的制作(一)相关推荐
- 自己动手完成一款简易P2P共享文件软件的制作(二)
文章目录 4. 客户机设计 4.1 GUI界面与功能设计 4.2 P2P下载功能设计 5. meta元数据与通信协议介绍 本文实验测试部分可参考基于QT的一款P2P共享文件系统 源码包下载地址基于QT ...
- 一款简易低成本智能割草机的制作——硬件篇
概述 近年来,随着大家环保意识和生活水平的提高,不少人开始在庭院里种植了小花小草.但是由于没有定期管理,很多庭院变得杂草丛生,不仅不美观,还引来一群蚊子昆虫在这安居乐业.尤其天热的时候,室外温度三四十 ...
- 基于墨刀实现的原型系统:一款简易的读书软件
一.产品说明 读书对于任何年龄段的人来说都是必不可少的,一款好的读书软件可以让人多读书.爱读书,基于以上想法设计了一款简易的读书app,旨在让更多人多读书.爱读书. 二.产品架构 主要分为三个功能模块 ...
- 简易数字时钟软件详细制作过程
这是我自己用VS2010制作的简易数字时钟小软件,在制作过程中收获知识不少,希望和初学MFC编程的朋友分享一下. 一.其功能有一下三点: 1.打开软件后,其程序自动获取当前电脑系统的日期.时间和周次, ...
- 手把手教你使用Python打造一款简易搜索引擎
/1 前言/ 相信大家在知识共享的这个年代一定在网上下载了很多的文件保存以供日后有时间学习吧,毕竟硬盘空间也比较有限,下面我们就来说说我们要做的这个项目,就是搜索盘搜里的资源然后进行下载. /2 项目 ...
- list mybatis 接收 类型_基于mybatis拦截器实现的一款简易影子表自动切换插件
近期因工作需要,小编基于mybatis拦截器开发了一款简易影子表自动切换插件,可以根据配置实现动态修改表名,即将对原source table表的操作自动切换到对target table表的操作.该插件 ...
- 边下边看 七款P2P下载软件全能大比拼
本文转自 http://soft.zol.com.cn/119/1191087_all.html 评测环境和参评软件介绍 在各种高清资源和大容量的视频资源日渐将硬盘撑满的时候,网友感觉到资源和空间的矛 ...
- CW——一款简易且有趣的文本编辑器
CW--一款简易且有趣的文本编辑器 CW总文件夹 src文件夹 CW.py beaqss.py friend.py settingDLG.py cfg文件夹, 配置文件 exa文件夹, 示例文件夹 i ...
- Weex 300行代码开发一款简易的跑步App
通过Weex 300行代码开发一款简易的跑步App 2017-03-28 Weex正如它的目标, 一套构建高性能.可扩展的原生应用的跨平台开发方案 Weex 给大家带来的无疑是客户端开发效率的提升,我 ...
最新文章
- 明白了最基本的压缩原理
- spring mvc使用的一些注意事项
- CV之YOLOv3:深度学习之计算机视觉神经网络Yolov3-5clessses训练自己的数据集全程记录
- mysql修改字段非必输_mysql有些字段是非必填的,传空要查所有数据该怎么处理?...
- JS JAVASCRIPT 判断两个日期相隔多少天
- OpenCV图像或视频显示在VC对话框中的方法
- ajax跨域请求wcf,ajax wcf 指定某个域名 进行跨域访问
- java 类型通配符_java中泛型之类型通配符(?)
- xml 增 删 改 查
- JavaScript学习指南 修订版pdf
- 阿里联手数据港合建数据中心 服务金额至少40亿
- 巴法云 mixly 扩展库
- Wps ppt中无法打开超链接外部文件的解决办法。
- 分析Padavan的代码一
- 招银网络科技杭州 java_招银网络科技_杭州Java后端_视频一、二面,HR面
- 更新至OSX 10.10后MBA外接网卡无法使用的解决
- vue使用百度地图3.0,使用JavaScriptAPI版,聚合点,个性化地图切换卫星地图
- STM32f767之通用定时器
- 软件需求规格说明书和系统需求规格说明书的区别
- matlab图像的增强
热门文章
- 魔兽世界人最多的服务器部落,《魔兽世界》怀旧服联盟和部落哪边人多 阵营人数对比...
- java整合openoffice实现word、execl、ppt转换为pdf
- 01.在谷歌浏览器安装Vue开发者工具
- Python 封闭科赫曲线绘制
- 常山股份搬家可实现销售65亿元,利税14亿元
- 常用的计算机有哪些台式的还有哪些,电脑有哪些常用快捷键?70个电脑常用的快捷键大全...
- PC端各浏览器布局兼容问题【Chrome、Firefox、IE】——长期更新
- 关闭Hyper-V,解决ENSP中AR启动失败错误代码40
- Web 实时推送技术的总结
- Keil5新建GD32裸机程序工程模板