P2P下载器(Linux下C++项目实战)

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

 

Published by

风君子

独自遨游何稽首 揭天掀地慰生平