资源共享中心
是什么
本质上就是一个http服务器,支持多人进行访问。在每一个人访问服务器的时候都可以对其进行文件列表展示,并且可以下载和上传内容,从而达到了一个资源共享的目的。
实现了文件展示,文件下载,文件上传,断点续传
流程
首先在Linux下使用TCP协议搭建一个服务器,这个服务器要支持可以与多个客户端进行连接,那就需要一个线程池。应用层协议选择一个http协议进行数据的传输,当对端发送http请求之后,服务器需要对于http请求进行解析,判断是文件请求还是CGI请求,然后再选择不同的方式进行处理
TCP协议
TCP协议是一个面向字节流的,可靠的,面向连接的
基于这些特点对于这个http服务器的传输层使用的就是TCP协议,而不是UDP协议。这是因为要保证对于在进行文件下载或者上传的时候的可靠性,不能对于数据进行丢失。也就是使用到了TCP的确认应答,校验和,序列号,重发机制以及连接管理来实现可靠性的。
了解TCP:https://blog.csdn.net/qq_40399012/article/details/87819605
线程池
线程池就是一个任务队列加上一个线程或者多个线程。
作用:对于客户端的来到一个http请求之后,就创建出一个任务对象,将这个任务放到线程池中,让线程池中的线程对于这个请求进行处理
好处:防止过度的消耗资源;避免大量线程频繁的创建/销毁
参考代码:https://github.com/YKitty/LinuxDir/tree/master/LinuxCode/pthread/cpAndThreadPool/threadPool
HTTP协议
对于现在大多数公司在做服务器的时候,都是使用的http协议。所以我们的服务器在应用层选用的协议也是http协议。
并且该协议还有以下特点:
-
支持客户/服务器模式
-
简单快速:客户向服务器请求服务时,只需要传输请求方法和路径。由于http协议简单,是的http服务器的程序规模很小,因而通信速度很快
-
无连接:无连接就是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答之后,就断开连接。
-
无状态:无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快。
了解HTTP:https://blog.csdn.net/qq_40399012/article/details/88067906
解析
对于客户端发送过来的http请求进行解析。解析出http请求中的请求方法和url。并且将请求头中的key:value键值对都放到一个map当中。
如果请求方法是一个POST那就是文件上传,请求方法是GET,看url有没有参数,如果有参数的话的就是一个文件上传。没有参数的话就是一个文件请求,对于请求的这个文件,判断是否是目录,是目录就是展示,否则进行文件下载即可。
如何将网页发送过来的路径转化为服务器的物理路径:
//将网络中的路径转化为物理路径放到tmp中
realpath(file.C_str(), tmp);
注意:该函数转化为物理路径之后,会自动把后面的/去掉
文件列表
对于请求的url中的路径是一个目录,将读取该目录下的所有的文件进行展示即可
根据目录下的每一个文件信息组织一个html页面,通过chunked分块传输,在浏览器进行展示
在组织的时候,表明文件大小,文件的类型,最后一次的修改时间
判断是否是目录:采用stat函数,获取该文件的属性然后再进行判断即可
struct stat {
dev_t st_dev; /* ID of device containing file */
ino_t st_ino; /* inode number */
mode_t st_mode; /* file type and mode */
nlink_t st_nlink; /* number of hard links */
uid_t st_uid; /* user ID of owner */
gid_t st_gid; /* group ID of owner */
dev_t st_rdev; /* device ID (if special file) */
off_t st_size; /* total size, in bytes */
blksize_t st_blksize; /* blocksize for filesystem I/O */
blkcnt_t st_blocks; /* number of 512B blocks allocated */
/* Since Linux 2.6, the kernel supports nanosecond
precision for the following timestamp fields.
For the details before Linux 2.6, see NOTES. */
struct timespec st_atim; /* time of last access */
struct timespec st_mtim; /* time of last modification */
struct timespec st_ctim; /* time of last status change */
#define st_atime st_atim.tv_sec /* Backward compatibility */
#define st_mtime st_mtim.tv_sec
#define st_ctime st_ctim.tv_sec
};
通过该结构体中的st_mode字段进行比对
_st.st_mode & S_IFDIR
如何将获得目录下所有的文件:
采用scandir系统调用获取
#include <dirent.h>
int scandir(const char *dirp, struct dirent ***namelist,
int (*filter)(const struct dirent *),
int (*compar)(const struct dirent **, const struct dirent **));
//浏览目录,将这个目录下所有的文件放到dirent结构体中
//并且采用filter进行过滤,看是否将所有的文件放到这个结构体中,如果是NULL的话,就不过滤将全部的文件都放到这个dirent结构体中
//对于compar是比较这个文件看用何种排序放到这个dirent结构体中,有两种比较方法
//一种是alphasort:第一个算法
//另一种是versionsort,版本比较
如何将文件的最后一次修改时间转化为GMT时间:
time(NULL):获得一个时间戳
gmtime()系统调用:根据一个时间戳获得strcut tm结构体
strftime()系统调用:根据struct tm结构体中的内容,将这个时间转化为特定格式的时间
对于http响应头中的Content-Length字段,如何处理:
因为对于一个目录下有可能有着成千上万个文件,这个时候如果实现遍历一遍获的所有文件的大小,在计算出Content-Length将这个发送过去,就会太浪费时间在遍历上。因此就出现了Transfer-Encoding字段
对于这个字段,就是告诉客户端我要进行分块传输,每次传输body的一部分,而不是将全部都进行传输
rsp_header += "Transfer-Encoding: chunked\r\n";
//按照chunked机制进行分块传输
//chunked发送数据的格式
//假设发送hello
//0x05\r\n :发送的数据的大小,十六进制的
//hello\r\n :发送这么多的数据
//最后一个分块
//0\r\n\r\n :发送最后的一个分块
这样的话,就可以不用在头部中要发送Content-Length字段了,可以对于许多个文件进行分块发送
文件下载
首先组织头部信息,发送给客户端,然后在读取文件的数据,将数据全都放在body中发送给客户端就可以了
【注意】:对于文件下载是将文件数据读取到一个缓存区中然后再次将文件发送出去,所以就要每次都进行读取数据大小的判断
文件上传
文件上传就是在服务器的目录下创建一个文件,并且将请求的body中的数据放入其中就好了
对于文件上传涉及到了服务器要对于数据进行处理。如果进行处理就会有可能失败,导致服务器挂掉,所以我们使用CGI(Wed应用程序)来处理而不是让服务器进行处理
如何使用CGI,也即是服务器fork出一个子进程,让这个子进程进行exec(程序替换)来进行处理,服务器只需要接收处理好的数据然后再次发送给客户端就可以了就可以了。
父子进程数据传输:
进程具有独立性。此时我们可以采用匿名管道的方式进行进程间通信。因为父进程需要将数据写到子进程,子进程也要返回数据给父进程,所以我们需要两个管道来进行通信
对于http请求头中的数据,因为是key:value的形式存在的和环境变量一样。所以对于http请求头中的数据就使用环境变量来进行传递就好了。对于body中的数据使用管道
dup2:
子进程exec(程序替换)之后对于管道的文件描述符会发生改变,所以在exec之前必须要进行dup2()系统调用,进行文件描述符的复制。让stdin和stdout分别对应一个管道
父进程将body中的数据发给子进程要注意粘包的问题:
就是有可能将下一次文件上传的数据在这一次发送给了子进程
所以要使用Content-Length字段来防止从接收缓存区读取数据的时候读取到下一个数据。因为有可能是一次发送多个文件,就有可能读取到下一个文件的信息了
int tlen = 0;
while (tlen < content_len)
{
int len = MAX_BUFF > (content_len - tlen) ? (content_len - tlen) : MAX_BUFF;
int rlen = recv(_cli_sock, buf, len, 0);
if (rlen <= 0)
{
return false;
}
if (write(in[1], buf, rlen) < 0)
{
return false;
}
tlen += rlen;
}
CGI:
通过从父进程获取的正文数据中提取出文件数据来创建文件并且将数据写入到文件当中
对于头部当中的Content-Type子,有着一个boundary信息,也就是分隔符。我们通过获取到boundary来对于正文部分的信息进行解析
POST方法是文件上传的常用方法。
Content-Type设置为multipart/from-data(多部分获得数据)
对于文件上传的请求的报文:
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--
对于boundary分为三种:
-
--boundary first boundary
-
\r\nboundary\r\n middle boundary
-
\r\nboundary-- last boundary
boundary的作用:用来对于正文数据进行分割,为了避免和正文内容重复,boundary是http自己生成的。
middle boundary可以有着多个,因为有可能是上传多个文件,但是first boundary和last boundary只可能是有一个
组成:在每部分的boundray中, 首先是boundary分割符,紧挨着就是内容的描述信息,之后使用一个空行分隔, 空行后是正文部分,正文部分的后面又紧跟着下一个boundary
【注意】:在上传的时候,根据匹配boundary来进行创建文件,向文件中写入数据的,但是有可能匹配的时候,会出现
-
没有匹配到boundary
-
全是数据直接写入文件就好了
-
删除写入的数据
-
-
匹配到部分boundary
-
将boundary之前的数据写入到文件当中
-
删除写入的数据
-
-
匹配到first boundary
-
创建/打开文件
-
删除first boundary:防止下次在匹配到first boundary
-
-
匹配到middle boundary
-
要循环匹配,因为有可能会有多个middle boundary
-
将boundary之前的数据写入到文件中,并且删除写入的数据
-
在继续在boundary下面查看内容描述符信息,有没有filename
-
有,创建文件,移除boundary
-
没有,查看内容描述符信息是否完全
-
完全,说明此次不是要上传文件,将boundary删除,防止下次不会再次匹配到这个boundary【注意】:本次只是将boundary删除了并没有删除boundary后面的内容描述信息,下次就有可能将这下信息写入到文件当中,但是此时我们没有打开文件所有不会出现将垃圾信息写入到文件当中
-
不完全,说明从管道中没有读取到完整的信息,再次读取,跳出循环
-
-
-
-
匹配到last boundary
-
说明这是最后一次要将数据写入到文件中了
-
直接写入数据,并且退出函数
-
对于从管道中读取数据进行处理,也要用Content-Length防止粘包,和上面父进程向管道中写入数据是一样的
【注意】:每次删除的时候,必须要使用memset进行一个重置,防止后面移动的数据没有吧前面的数据覆盖完全再次读取buf就会出错
【小技巧】:对于文件上传之后如何查看文件是否正确呢?
采用MD5可以检测文件的一致性问题
对于Linux下使用命令
md5sum filename
对于Windows下
certuitl -hashfile filename MD5
只要这出计算下来的MD5是一样的,就表示文件上传成功
断点续传
实现断点续传就要对于ETag和Last Modify这两个字段才能支持断点续传
If_Range判断这个文件是否符合常规,请求的是上次响应的ETag
通过If-Range判断有没有改变,对于If_Range一般是ETag和Last Modify中的ETag
Range:记录需要这次需要请求的文件的数据部分
If-Range:最后一次修改时间或者是Etag。假设是ETag
Range:bytes=0~100
文件长度:1000
0-100 请求从0到第100字节的数据
101- 请求从101到结尾的数据
-100 请求文件末尾的100个字节
0-100,101-200,201- 三个块要分三次来进行传输数据
响应:状态码必须是206
类型:
分块传输不能使用200OK,而要使用206,并且响应的头信息中需要有Content-Range这个字段
Content-Range:bytes start-end/all
HTTP/1.1 206 Partial Content(部分内容)
Content-Range: bytes 0-100/1000
Contnet-Length: 101
发送多块的话,要每一块都有一个头信息,每一块都分别进行发送
开始的时候,进行断点续传,大小类型是bytes,并且进行分块传输,提高传输效率
思维导图:https://blog.csdn.net/qq_40399012/article/details/87977549
源码:https://github.com/YKitty/LinuxDir/tree/master/Demo/httpServer/src.2