P2P下载器:即点对点下载器,服务端与客户端。服务端共享文件列表,客户端配对相应服务端,下载所需要的文件。
一、项目介绍
1.项目功能
搜索附近(局域网内)在线用户, 此处不足(只能在局域网内获取,需要网络穿透技术获取别的网络),获取到在线用户列表,可以查看指定用户的共享文件列表,然后对感兴趣的文件进行下载。
项目功能分为服务端和客户端的功能。
1)服务端
1.能够被附近客户端发现的功能(对主机配对请求进行一个响应)
2.提供客户端请求文件列表的功能。
3.提供客户端文件下载的功能。
2)客户端
1.搜索附近的主机(向局域网内广播一个主机配对请求)
2.获取指定主机的共享文件列表。
3.下载指定主机的指定文件。
2.实现技术
1)网络协议
服务端与客户端之间采用HTTP协议实现网络通信和文件传输。
2)httplib库来实现HTTP服务器
3)大文件多进程/多线程 数据分片传输(文件分块技术)
3.项目平台和工具
Centos7.2 vim/gcc/g++/gdb/make
必须在GCC 4.9版本以上开发此项目:
gcc具体更新步骤:
gcc升级:
yum install centos-release-scl-rh centos-release-scl
yum check-update
yum install devtoolset-4-gcc devtoolset-4-gcc-c++
source /opt/rh/devtoolset-4/enable
安装boost库:sudo yum install boost + sudo yum install boost-devel
二、项目框架流程
1.整体框架
主控程序下服务端和客户端分别实现自己的功能。
2.软件流程图
三、项目实现
1.使用httplib库搭建简单的HTTP服务器
HTTP协议回顾:
http协议格式:(超文本传输协议 0.9)
1.URL(统一资源定位符)格式:http://uersname:password@srvip:srvport/path?query_string#ch
2.query_string(查询字符串,提交的数据):urlencode/urldecode(转码/解码) key = val&key = val
3.http/https:https就是基于http进行了一层ssl/tls加密。
4.首行:
请求首行:请求方法(GET/POST/HEAD/PUT/DELETE) URL 协议版本\r\n。
get/post区别:get提交数据再URL中,post提交数据在正文中(url长度有限)
协议版本:0.9(get 短连接)/1.0(post+head 短–>长)/1.1(…单行传输)/1.2(实现双向传输)
响应首行:协议版本 状态码 状态码描述\r\n
状态码:1**/2**/3**/4**/5** 功能
(200(正确传输) / 301(永久重定向) / 302(临时重定向) / 400(请求格式有问题) / 404(资源没找到) /
500(服务器内部问题) / 502(无效的网关) / 504(没有得到及时的响应))
5.头部:以key:val组成的键值对,每个键值对都已 \r\n 作为结尾
Content-Length(正文数据长度,避免粘包问题)
Content-Type(正文数据处理方法) text/html(网页渲染)
Location:重定向
Transfer-Encoding:trunck 发送分块传输数据的每块长度。
Set-Cookie/Cookie:客户端请求连接时,服务端分配的信息(地址信息,会话信息)存放位置。
6.空行: 连续两个\r\n结尾
7.正文:放入文件数据
简单服务器实现代码如下:
#include "httplib.h"
using namespace httplib;void HelloWorld(const Request &req, Response &rsp){rsp.status = 302;rsp.set_header("Location", "http://www.baidu.com");rsp.body = "<html><h1>Hello World</h4></html>";return;
}int main(){Server server;server.Get("/", HelloWorld);server.listen("0.0.0.0", 9000);return 0;
}
运行服务器,在网页上输入本机的ip和指定port,即在网页上显示HELLOWORLD
2.测试获取局域网内所有主机地址
获取本机网络接口信息:getifaddr(&addr)
关于网络编程中常用的函数即结构体:https://blog.csdn.net/nmglwy/article/details/52988272
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <ifaddrs.h>
#include <netinet/in.h>
#include <arpa/inet.h>int main(){struct ifaddrs* addrs;getifaddrs(&addrs);while(addrs != NULL){struct sockaddr_in* ip = (struct sockaddr_in*)addrs->ifa_addr;struct sockaddr_in* mask = (struct sockaddr_in*)addrs->ifa_netmask;if(ip->sin_family != AF_INET){addrs = addrs->ifa_next;continue;}if(ip->sin_addr.s_addr == inet_addr("127.0.0.1")){addrs = addrs->ifa_next;continue;}printf("name:%s\n", addr->ifa_name);printf("ip:%s\n", inet_ntoa(ip->sin_addr));printf("mask:%s\n", inet_ntoa(mask->sin_addr));uint32_t net = ntohl(ip->sin_addr.s_addr & mask->sin_addr.s_addr);uint32_t host = ntohl(~mask->sin_addr.s_addr);int i;for(i =1; i < host; i++){struct in_addr ip;ip.s_addr = htonl(net + i);printf("net:%s\n", ip.s_addr);}addrs = addrs->ifa_next;}return 0;
}
运行后打印:本机网卡名称,IP,子网掩码,和网络号内0–255的IP号
[test@localhost P2PProject]$ ./Getaddr
name:ens33
ip:192.168.78.128
mask:255.255.255.0
ip:192.168.78.0
ip:192.168.78.1
ip:192.168.78.2
ip:192.168.78.3
ip:192.168.78.4
ip:192.168.78.5
ip:192.168.78.6
ip:192.168.78.7
3.服务端功能
通过httplib搭建http服务器
步骤:1.实现主机配对的响应功能。 /hostpair -> 200
2.实现文件列表获取的响应功能。 /list -> 200 filename1\filename2…….
3.实现文件下载的响应功能。 /list/filename -> 200 filedata
把服务端封装成一个类:P2PServer
#include <iostream>
#include <boost/filesystem.hpp> //sudo yum install boost + sudo yum install boost-devel
#include <fstream>
#include "httplib.h"using namespace httplib;
namespace bf = boost::filesystem;
#define SHARE_PATH "Download"class P2PServer{
private:Server _server;
private:static void GetHostPair(const Request &req, Response &rsp); //设置请求状态重定向static void GetFileLsit(const Request &req, Response &rsp); //获取文件列表static void GetFileData(const Request &req, Response &rsp); //获取文件数据
public:P2PServer(){//判断共享目录若不存在,则创建if(!bf::exists(SHARED_PATH)){bf::create_directory((SHARED_PATH)));}bool Start(uint16_t port){_server.Get("/hostpair", GetHostPair);_server.Get("/list", GetFileLsit);_server.Get("/list/(.*)", GetFileData);_server.listen("0.0.0.0", port);}
};
4.客户端功能
实现基于服务器HTTP的分块传输功能实现多进程文件分块下载功能的下载器,通过分块传输提高传输效率
实现步骤:1.获取局域网中的所有的主机IP地址。
2.获取在线主机列表(逐个向主机发送配对请求,判断相应状态)
3.打印在线主机列表,并且提供用户选择想要查看的主机共享文件列表
4.向选择的主机发送文件列表请求,获取到文件列表
5.打印文件列表,并且用户选择想要下载的文件
6.下载文件(向指定主机发送指定的文件下载请求)。
客户端封装为一个类:P2PClient
#include <iostream>
#include <string>
#include <vector>
#include <fstream>
#include <ifaddrs.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <boost/system.hpp>
#include <boost/algorithm/string.hpp>
#include "httplib.h"using namespace httplib;
namespace bf = boost::filesystm;
using std::string;
using std::vector;
using std::cout;
using std::cin;
using std::endl;class P2PClient{
private:uint10_t _srv_port; //服务器端口int host_idx; //vector<string> _online_list; //在线主机列表vector<string> _file_list; //共享文件列表
private:bool GetAllHost(vector<string> &list); //获取所有主机列表bool GetOnlineHost(vector<string> &list); //获取在线主机列表bool ShowOnlineHost(); //显示在线主机bool GetFileList(); //获取共享文件列表bool ShowFileLsit(string& name); //显示共享文件列表bool DownloadFile(string &name); //下载文件 //md5sum + 文件名 计算文件大小int DoFace(){cout << "1.搜索附近主机\n";cout << "2.显示在线主机\n";cout << "3.显示文件列表\n";cout << "0.退出\n";//操作菜单int choose;cout << "please choose: ";fflush(stdout);cin >> choose;return choose;}
public:P2PClient() : _srv_port(port){}bool Start{int choose = DoFace();vector<string> list;string filename;switch(choose){case 1:GetAllHost(list);GetOnlineHost(list);break;case 2:if(!ShowOnlineHost() == flase) break;GetFileList();break;case 3:if(!ShowFileLsit(filename) == false) break;DownloadFile(filename);break;case 0:exit(0);default:break;}return true;}
};
主函数:main.cpp
#include "P2PServer.hpp"
#include "P2PClient.hpp"int main()
{P2PServer server(9000);server.Start();P2PClient client(9000);client.Start();return 0;
}
运行实现:
ddasda
大文件分块传输技术:文件切割,分块传输(http协议)
bool DownloadFile(string &name); //下载文件
四、项目效率改进(多线程并行)
1.附近主机配对速度过慢(多个主机串行化配对,每个主机都要等待配对一段时间(3s))
解决方法:并行主机配对,采用多线程,将所有主机的配对等待时间并行压缩起来。
std::thread
线程运行类的成员函数,需要传入this指针作为第一个参数 std::thread(classname::thr_start, this)
线程对象无法直接赋值,需要使用std::move接口完成,std::thread thr1 = std::move(std::thread())
线程参数传递,若传递的参数形参是引用参数,传递实参时,使用std::ref(variable)修饰。
线程参数传递过多的情况下,参数类型不匹配,需要控制参数个数
改进实现:
static void GetHostPair(const Request &req, Response &rsp);
2.文件大小有上限,不能太大(httplib库源码,是将所有的响应正文一次性放到一个buffer中进行响应)
解决方法:不使用httplib库自主实现http协议。
3.大文件下载比较慢(单线程串行下载)
采用多线程分块传输,对每个大文件进行下载时,每个线程只传输文件的一部分数据
1.获取文件大小 -> 2.根据文件大小进行区域分块 -> 3.创建多线程进行下载文件(每个线程只负责下载自己负责的文件分块数据)
IO操作,分为等待IO就绪和数据拷贝操作两部分,多线程下载文件可将等待过程并行压缩。
改进实现:
bool DownloadFile(string &name); //下载文件
五、项目整体所遇问题:
1.向boost库线程传递bool*参数会出问题,解决方案:传入int*来进行获取。
2.C++标准线程库std::thread出现多参数传递问题, 解决方法:使用boost库线程boost::thread。
3.向创建线程传递回调函数,传递类成员函数,需要使用类名来声明这个函数,并且去地址,第一个参数是this指针
boost::thread(&classname::thr_start, this)
4.线程的参数传递不能使用引用,默认只能传指针,而传引用需要使用std::ret进行修饰。
boost::thread(&classname::thr_start, this, std::ref(obj))
5.C++使用fstream默认打开文件时会截断文件,分块传输会出现问题,解决方法:使用系统调用接口。
6.大文件传输时,受限于硬件资源无法直接传输,
解决方法:
1.多线程并行下载(依然首先与资源)
2.多线程分块串行下载。
7.主机配对速度较慢:每个主机平均需要3~5秒钟,整个局域网主机配对过慢
解决方案:多线程并行进行主机配对,并行压缩等待时间。
六、最终实现与整体代码
P2PServer.hpp
P2PClient.hpp