迷你网盘

前言:

迷你网盘是一个小项目,本质上是一个http服务器,在这个http服务器汇总实现文件上传,下载,展示功能。在下载功能中使用http协议拓展出断点续传输、分块传输功能。最终实现出一个迷你网盘。首先要搭建好一个TCP服务器,然后在服务器内创建一个线程池来处理服务器接收到的请求,当收到请求时,首先就是要把请求的内容用一个类保存起来,这个类中有一个成员变量是stat结构体,用来存放请求文件的文件信息。让后分析这个请求的内容,辨别这是一个什么类型的请求。请求一共有三类:文件列表请求,文件下载请求和文件上传请求。

文件上传请求:客户端请求上传文件,客户端的请求是GET方式并且查询字符串不为空,或者请求方式是POST就代表这是一个文件上传请求,此时利用CGI技术启动一个进程来处理这个请求,这样即使上传失败也不会影响服务器的运行。将首行和请求头通过环境变量的方式传递给子进程,而正文数据则通过管道传递给子进程。

文件列表请求:列出该目录下的所有文件,利用stat结构体中的st_mode判断这个请求的文件是一个目录,就运用dirent结构体和scandir函数把该目录下的所有文件信息都放到一个dirent数组中,然后根据服务器设置的方法需要排好序,把dirent数组里面存放的是该目录下的每一个文件的信息,再将这些文件信息打包成一个个小包发送给客户端。

文件下载请求:客户端请求下载该文件。当请求的文件是一个文件时,则说明该请求是一个文件下载请求,首先判断是不是一个断点续传,如果是的话就执行断点续传的流程,如果不是则执行普通文件下载的流程。

 

目录

前言:

整体框架:

1.HTTP服务端设计

2.线程池设计

2.1线程池的作用:

3.HTTP请求处理

3.1接收HTTP头部

3.2解析HTTP头部

首行:请求方法  URL   协议版本

头部

文件信息

4.HTTP响应处理

4.1文件上传的CGI请求处理

父进程做的事

子进程做的事

文件上传程序

4.2文件列表请求

4.3文件下载请求

断点续传

普通文件下载


整体框架:

  1. 建立tcp服务端程序;
  2. 是为新的客户端建立线程池任务并添加到线程池中,因此我们还要创建一个线程池;
  3. 线程池中的线程获取任务;
  4. 接收http请求数据;
  5. 解析http请求数据;
  6. 判断请求类型;

1.HTTP服务端设计

  1. 创建socket
  2. 绑定地址信息
  3. 开始监听
  4. 获取已连接成功的客户端socket
  5. 服务端程序的业务处理--->创建线程任务,将任务添加到线程池中

HTTP服务端最重要的是搭建TCP服务器,至于我为什么要使用TCP协议而不是其他的协议的理由想必不需要多说了吧,TCP是现在使用最广泛的协议,我使用TCP不仅能够搭建一个效率可观,可靠的服务器,还能达到学习的目的。

搭建TCP服务器的时候有几个需要注意的地方:

  • 创建套接字的时候,要地址复用,服务器挂掉之后需要立即重启,此时就需要地址复用,调用setsockopt可以达到这个功能,一般的调用方式:
int opt = 1;
setsockopt(_server_sock, SOL_SOCKET, SO_REUSEADDR, (char*)&opt, sizeof(opt));
  • 绑定ip地址的时候,可以选择绑定这个局域网内所有的ip地址,因为有的服务器不止一张网卡,就有不同的ip地址
lst_addr.sin_addr.s_addr = htonl(INADDR_ANY);  
  • 让后开始监听,此时需要注意函数的第二个参数,全链接队列的个数,实际全连接个数=此参数+1
listen(_server_sock, 5);//此时全连接队列的个数就是6
  • 还有一点,在服务器启动的时候,要忽略SIG_PIPE信号,至于为何如此,是因为项目中使用了CGI技术
signal(SIGPIPE, SIG_IGN);

 此时启动HTTP服务器,就开始accept新连接,收到新连接时就将该连接放到线程池内,让线程池中的某个线程去处理这个请求。所以此处要设计一个线程池。

在服务器启动之后,头两件事情就是初始化HTTP,并且创建一个线程池。

2.线程池设计

  1. 创建线程
  2. 创建线程安全的任务队列--->任务入队,处理完毕后任务出队
  3. 线程池终止

2.1线程池的作用:

  1. 线程的创建和销毁都是有开销的,线程池的可以减少这些开销
  2. 线程池控制线程池的并发数可以有效的避免大量的线程池争夺CPU资源而造成堵塞
  3. 线程池可以提供定时、定期、单线程、并发数控制等功能

线程池设计中还应设计一个类,这个类叫做任务类,这个类就是到达的任务。成员变量只有两个,一个是accept到的套接字,另一个就是任务处理函数。

要实现线程安全的线程池,就需要使用互斥锁和条件变量,线程池里面的数量有上限,自己设置;线程创建出来之后要分离,创建线程池的时候同时要初始化好互斥锁和条件变量;析构函数要删除互斥锁和条件变量。

线程创建之后要去检测是否有任务,没有任务就去沉睡,当有任务来临时,就唤醒一个线程去执行任务。,因此线程池内还应该维护一条任务队列,用于存放到达的任务。

任务入队要上锁,

出队不需要上锁,因为出队是在线程接口中调用,但是线程接口在出队之前就会加锁。

3.HTTP请求处理

有任务(连接)来了,创建一个任务类,把这个任务任到任务队列中,添加到任务对列中后,记得要唤醒一个线程!!!

线程醒来之后去处理这个任务,处理任务是有一个专门的函数来处理,处理流程如下图所示:

3.1接收HTTP头部

设计出一个类(RequestInfo)来专门放接收到的HTTP数据,

http请求头格式:

  • 首行
  • 头部
  • 空行
  • 正文

要读取到头部,就要把首行和头部全部读取出来,此时我的做法是:把数据从缓存区中全部预先读取出来,放到事先定义好的数组tmp里面:

//预先读取,不从缓存区中把数据拿出来                                       
int ret = recv(_cli_sock, tmp, MAX_HTTPHDR, MSG_PEEK);     

再通过下面的函数,找到空行的位置:

char* ptr = strstr(tmp, "\r\n\r\n");  

 ptr-tmp就是头部+首行的长度:

通过下面的函数把头部+首行的内容放到RequestInfo的一个成员变量里面:

_http_header.assign(tmp, ptr - tmp);

接收HTTP数据之后,就要通过这个类来分析这是一个什么请求,根据不同的请求来做出不同的响应。

再通过下面的函数把正文放到tmp数组里面,此时会覆盖掉之前tmp里面的内容。

recv(_cli_sock, tmp, hdr_len + 4, 0); 

3.2解析HTTP头部

上面接收的HTTP头部都放在了一个RequestInfo类的一个成员变量里面,解析起来还不简单嘛?

首行+头部信息都是以行为单位陈列的,故只需在出现“\r\n”的地方将其分割开就好了,第一个是首行,后面的全部是HTTP头部

首行:请求方法  URL   协议版本

首行是以空格分割的。

请求方法只有三种:GET 、POST 、 HEAD

URL:虚拟资源路径?查询字符串。所以此处要分两步解析,看有没有“?”

协议版本只有三种:HTTP/0.9 、HTTP/1.0 、 HTTP/1.1 

URL中的虚拟路径的根目录是相对的,要将它转换成绝对的路径,需要调用函数:

realpath()

头部

头部的信息以 key: value  的形式存在,可以设计一个unordered_map来存放。

文件信息

调用stat函数将文件信息存到一个结构体中,第一个参数是文件路径,第二个参数是结构体。ls -l会调用stat函数

 int stat(const char *file_name, struct stat *buf);

struct stat {
    dev_t         st_dev;                 //文件的设备编号
    ino_t         st_ino;                   //节点
    mode_t        st_mode;           //文件的类型和存取的权限
    nlink_t       st_nlink;               //连到该文件的硬连接数目,刚建立的文件值为1
    uid_t         st_uid;                  //用户ID
    gid_t         st_gid;                  //组ID
    dev_t         st_rdev;               //(设备类型)若此文件为设备文件,则为其设备编号
    off_t         st_size;                 //文件字节数(文件大小)
    unsigned long st_blksize;     //块大小(文件系统的I/O 缓冲区大小)
    unsigned long st_blocks;     //块数
    time_t        st_atime;            //最后一次访问时间
    time_t        st_mtime;           //最后一次修改时间
    time_t        st_ctime;            //最后一次改变时间(指属性)
};

请求类型有三种:首先判断是不是CGI,再来判断是不是文件列表。

  1. 文件列表请求 ,请求的是一个目录
  2. 文件下载请求, 请求的不是一个目录
  3. 文件上传请求(CGI),①请求方法是GET并且查询字符串不为空、②POST

因此响应也要有三种。

4.HTTP响应处理

 

4.1文件上传的CGI请求处理

如果请求是一个文件上传的请求,服务器的处理方法是创建一个子进程去处理。

在fork之前就要创建两个匿名管道,一个用来向子进程传递数据,另一个用来从子进程获取处理结果。在fork之前是因为子进程要共享父进程的数据。

父进程做的事

  1. 通过in管道传递正文数据给子进程                                                                                                                         
  2. 通过out管道读取子进程的处理结果直到返回0              
  3. 将数据处理结果组织http数据,响应给客户端  

子进程做的事

利用setenv函数给子进程设置环境变量把

  • 请求方法
  • 协议版本
  • 资源路径
  • 查询字符串

这四个数据传给子进程。这四者也就是首行解析出来的内容。再用同样的方式把请求头传给子进程。

再将那两个管道重定向,使子进程的标准输入变成父进程向子进程传递数据的管道,标准输出变成子进程向父进程传递数据的管道。如果子进程想打印数据的话,可以打印在标准错误上面。

最后就是进行程序替换了,使子进程去执行文件上传的程序代码。

文件上传程序

POST /upload HTTP/1.1
Host: 192.168.122.135:8080
Connection: keep-alive
Content-Length: 202
Cache-Control: max-age=0
Origin: http://192.168.122.135:8080
Upgrade-Insecure-Requests: 1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryBJP7FhV4yjQ3wgVo
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.109 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Referer: http://192.168.122.135:8080/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9

------WebKitFormBoundaryBJP7FhV4yjQ3wgVo
Content-Disposition: form-data; name="fileupload"; filename="hello.txt"
Content-Type: text/plain

hello world!!
------WebKitFormBoundaryBJP7FhV4yjQ3wgVo
Content-Disposition: form-data; name="anniu";
Content-Type: text/plain

上传文件
------WebKitFormBoundaryBJP7FhV4yjQ3wgVo--

上述是服务器收到的请求。

文件上传程序我命名为upload。首行和请求报头的内容都在环境变量中放着。需要上传的文件数据从标准输入中获得(重定向)

利用getenv函数从环境变量中获取“Content-Length”和“Content-Type”字段,从而知道你所需要上传的文件大小和文件类型。

文件的boundary在Content-Type中,寻找“boundary=”字段,然后构造好下列三个boundary:

  • _f_boundary = "--" + boundary;
  • _m_boundary = "\r\n" + _f_boundry + "\r\n";                                   
  • _l_boundary = "\r\n" + _f_boundry + "--"; 

此时就可以从在管道里读取的正文中匹配boundary,如果有boundary的话,首先匹配到的是_f_boundary,

如果上传的不止一个文件,还会有_m_boundary,最后是_l_boundary。

检测到_f_boundary之后,就去后面匹配文件名,匹配到了就根据这个文件名创建文件,匹配不到则说明出错了。

如果检测到_m_boundary,同_f_boundary的操作一样。

直到遇到_l_boundary,则说明文件上传完毕。

4.2文件列表请求

判断是不是文件列表很简单,只需要一行代码就可搞定:

if(info._st.st_mode & S_IFDIR)
{
    ....
}

 st_mode是stat结构体中的成员,表示文件的类型和存取的权限,如果上述if判断为真,就表示该文件为目录文件,就可说明该请求是文件列表请求。

文件列表请求的响应就把该目录下的文件信息展示出来。而且一访问这个服务器就是文件列表展示功能,所以这里还要写好网页的html,包括网页标题,html页面中的元信息,form表单等等。

这里再介绍一个结构体:dirent。为了获取某文件夹目录内容,所使用的结构体。起着一个索引的作用

#include <dirent.h>
 
struct dirent
{
    long d_ino;               /* inode number 索引节点号 */
    off_t d_off;              /* offset to this dirent 在目录文件中的偏移 */
    unsigned short d_reclen;  /* length of this d_name 文件名长 */
    unsigned char d_type;     /* the type of d_name 文件类型 */
    char d_name [NAME_MAX+1]; /* file name (null-terminated) 文件名,最长256字符 */
}

 相关函数:

opendir(),readdir(),closedir();

通过readdir函数读取到的文件名存储在结构体dirent的d_name成员中,而函数int stat(const char *file_name, struct stat *buf);的作用就是获取文件名为d_name的文件的详细信息,存储在stat结构体中。stat函数在上面就有说明了。

还有一个关于文件操作的函数:scandir()

头文件:#include  <dirent.h>

int  scandir(const char *dir, struct dirent **namelist, nt (*select)  (const  struct  dirent *), nt (*compar)  (const struct dirent **, const struct dirent**));

函数说明:scandir()会扫描参数dir指定的目录文件,经由参数select指定的函数来挑选目录结构至参数namelist数组中,最后再调用参数compar指定的函数来排序namelist数组中的目录数据。每次从目录文件中读取一个目录结构后便将此结构传给参数select所指的函数, select函数若不想要将此目录结构复制到namelist数组就返回0,若select为空指针则代表选择所有的目录结构。scandir()会调用qsort()来排序数据,参数compar则为qsort()的参数,若是要排列目录名称字母则可使用alphasort(). 结构dirent定义请参考readdir()。
返回值  :成功则返回复制到namelist数组中的数据结构数目,有错误发生则返回-1。

实际调用:
 

struct dirent** p_dirent = NULL; 

int num = scandir(info._path_phys.c_str(), &p_dirent, NULL, alphasort);

函数扫描info._path_phys.c_str()目录,将该目录下所有的目录结构复制到p_dirent数组中,调用alphasort函数来排序p_dirent数组中的目录数据(依字母顺序来排序)。

p_dirent数组里面存放的是该目录下的每一个文件的信息,再将这些文件信息打包成一个个小包发送给客户端。把所有文件的信息都发过去之后,

4.3文件下载请求

发送文件:while循环调用read函数,返回值大于0就继续,

判断了不是CGI请求,也不是文件列表请求之后,那么就只有文件下载请求了。

文件下载请求里面还有点花样呢,首先判断是不是断点续传,如果是的话就走断点续传的路子,如果不是就走普通文件下载的路子。

首先判断是不是断点续传,判断的方式是在接收到的HTTP头部中查找“If-Range”,如果没有这个字段肯定就不是断点续传了;如果找到了这个字段,还有进行后续的判断:查看最后一次修改的时间和是否被修改标志,如果在中途被修改了就要重新传输,如果If-Range字段值若是跟ETag值或者更新的日期时间匹配一致,那么就作为范围请求处理,也就是断点续传。

随后就是查找“Range”,没有range字段也不是断点续传。

断点续传

range.erase(range.begin(), range.begin() + 6);去掉“bytes=”

size_t pos = range.find("-");寻找“-”

Range: bytes=-500   这种情况代表只需传输最后500字节,pos = 0;

Range: bytes=500-   这种情况代表是500以后到文件最后,pos = range.size()-1;

Range: byte=200-500   这种情况代表传输从200-500的字节

找到你说需要响应的是哪些字节的数据,就可以着手构建响应报头了,

  • 此时响应报头的这状态码不再是200 OK,而是206(断点续传)      协议版本 206 PARTIAL CONTENT
  • 并且响应的头信息中需要有 Content-Range: bytes start-end/len,这个len就是你要传的字节数。

把响应头构建好并且发送过去之后,就要发送客户端要下载的文件数据了

首先以只读的方式打开这个文件,然后要调用一个函数lseek

off_t lseek(int fd, off_t offset, int whence);

lseek()函数会重新定位被打开文件的位移量,根据参数offset以及whence的组合来决定:offset为正则向文件末尾移动(向前移),为负数则向文件头部(向后移)。

SEEK_SET: 
  从文件头部开始偏移offset个字节。 
SEEK_CUR: 
  从文件当前读写的指针位置开始,增加offset个字节的偏移量。 
SEEK_END: 
  文件偏移量设置为文件的大小加上偏移量字节。

发送数据的时候要注意一些细节:

假如:已经发送的数据量+你刚刚读取的数据量>需要发送的数据量

那么你就只需发送:需要发送的数据量-已经发送的数据量。其余情况就是你读了多少就发多少。

文件数据发送完毕之后要记得关闭文件描述符。

普通文件下载

普通文件下载要比断点续传简单,首先构建响应报头,发送完响应报头之后就以只读的方式打开一个文件,发送数据,发送完了关闭文件。

项目缺点:

  • 分块传输这里,使用多线程传输可以速度更快。
  • 客户端下载文件、上传文件优化:以多线程+http的分块传输实现。
  • 压力测试:大概四五十客户端的时候下载文件速度达到二三百kb左右,硬件性能上不行,可以使用分布式。
  • 传输大文件:我采用的是循环式的读取文件内容,每次读取都放到一个缓冲的buff当中,直到把文件读取完。这里的传输大文件是指文件太大,把服务器的磁盘写满,我们可以在每次服务器起来的时候,检测一下磁盘的剩余空间,每次上传文件之后都修改这个数据,可以设置一个阈值,当磁盘剩余空间已经少于这个阈值时,返回302状态码,提醒客户端请求其他的服务器。

 

 

 

 

 

 

 

 

 

 

 

 

phpdisk Mini版本目前主要应用于论坛,是一款基于论坛的二次开发插件版本。PHPDISK网盘系统[简称:PHPDISK],(),是一套采用PHP和MySQL构建的网络硬盘(文件存储管理)系统,可替代传统的FTP文件管理。友好的界面,操作的便捷深受用户的欢迎。 她是一套可用于网络上文件办公、共享、传递、查看的多用户文件存储系统。广泛应用于互联网、公司、网吧、学校等地管理及使用文件,多方式的共享权限,全方位的后台管理,满足从个人到企业各方面应用的需求。 主要功能有: 文件的上传、下载等基本的功能。 此版本功能比较简单,后续会不断完善及增强功能。 目前提供的是免费版本,任何用户及团体都可下载使用,但需要保留底部的版权链接。如需要购买授权,请看网站说明或联系官方人员。 程序安装: 解压、上传到论坛的相应目录,然后到论坛的插件上去安装即可。 phpdisk网盘(Mini) v2.2 是一套针对论坛应用的网盘系统,基于论坛的二次开发,可以实现论坛附件分离、文件管理的一个专业的文件管理解决方案。 目前版本支持 Discuz! X1.5,X2.0,X2.5,X3.0 20130807 添加修正 【新增】浏览自己的文件可以分页 【修正】X3.0顶部出现出错提示 【修正】自己网盘的个性设置无效问题 功能说明: 此版本比上一版本提供了更多人性化的设置,以及站长运营方式。 完全调用现有的论坛上注册用户信息,对于现有的论坛用户没有任何冲突。 【增加】我的网盘,上传文件管理功能 ( v2.2版) 【新增】以论坛插件形式的方式安装,比普通插件功能更多,基本可以实现一个中小型的附件管理系统;如果服务器配置跟得上,实现大型的附件管理系统也是可以的。 【新增】网盘后台广告位设置 【新增】下载需要打开第二下载页面,双倍的广告位展示 【新增】在发布帖子时可以上传文件到网盘Mini上,随时可以使用旧的文件链接 【新增】可以支持分布服务器上传、多节点分流下载(付费版本) 【新增】论坛后台,网盘系统管理、配置
本课程详细讲解了以下内容:    1.jsp环境搭建及入门、虚拟路径和虚拟主机、JSP执行流程    2.使用Eclipse快速开发JSP、编码问题、JSP页面元素以及request对象、使用request对象实现注册示例    3.请求方式的编码问题、response、请求转发和重定向、cookie、session执行机制、session共享问题     4.session与cookie问题及application、cookie补充说明及四种范围对象作用域     5.JDBC原理及使用Statement访问数据库、使用JDBC切换数据库以及PreparedStatement的使用、Statement与PreparedStatement的区别     6.JDBC调用存储过程和存储函数、JDBC处理大文本CLOB及二进制BLOB类型数据     7.JSP访问数据库、JavaBean(封装数据和封装业务逻辑)     8.MVC模式与Servlet执行流程、Servlet25与Servlet30的使用、ServletAPI详解与源码分析     9.MVC案例、三层架构详解、乱码问题以及三层代码流程解析、完善Service和Dao、完善View、优化用户体验、优化三层(加入接口和DBUtil)    1 0.Web调试及bug修复、分页SQL(Oracle、MySQL、SQLSERVER)     11.分页业务逻辑层和数据访问层Service、Dao、分页表示层Jsp、Servlet     12.文件上传及注意问题、控制文件上传类型和大小、下载、各浏览器下载乱码问题     13.EL表达式语法、点操作符和中括号操作符、EL运算、隐式对象、JSTL基础及set、out、remove     14.过滤器、过滤器通配符、过滤器链、监听器     15.session绑定解绑、钝化活化     16.以及Ajax的各种应用     17. Idea环境下的Java Web开发
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值