目录
一、什么是共享目录?
共享目录本质上就是一个实现了一个支持多人同时访问的HTTP服务器,在这个服务器中实现了目录列表、文件下载和文件上传的功能。
二、实现流程:
三、线程池创建
●一定数量的线程加上一个任务队列构成了线程池。当链接到来时创建任务,直接将任务放进线程池中由已有线程处理任务。这样就减少了线程创建和销毁的时间,由线程处理任务也减少了资源的消耗。
class HttpTask
{
public:
//设置任务,也就是对于这个任务类进行初始化
void SetHttpTask(int sock, Handler handler);
//任务处理函数
void Run();
};
class ThreadPool
{
//完成线程创建,互斥锁/条件变量初始化
bool ThreadPoolInit();
//线程安全的任务入队
bool PushTask(HttpTask& tt);
//线程安全的任务出队
bool PopTask(HttpTask& tt);
//销毁线程池
bool ThreadPoolStop();
};
四、TCP建立网络通信
TCP通过Socket API建立网络通讯,使用TCP是因为TCP协议有一大特征就是它通过确认应答机制,确认序号等一系列措施保证它是可靠的,我们要在服务器上进行数据文件的传输,那么就要保证其可靠性,不能丢失数据。
socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);//创建
bind(_serv_sock, (sockaddr*)&lst_addr, len);//绑定
listen(_serv_sock, MAX_LISTEN);//监听
五、HTTP请求:
1.获取HTTP请求的数据:通过socket接收http数据
2.解析首行和头部:解析出首行的请求方法、版本号和url。
解析出头部中K:V键值对放到一个map中。
3.对外提供获取请求的接口
class HttpRequest
{
private:
int _cli_sock;
std::string _http_header;
RequestInfo _req_info;
public:
HttpRequest(int sock)
: _cli_sock(sock)
{}
//接收http请求头
bool RecvHttpHeader();
//解析http请求头
bool ParseHttpHeader();
//向外提供解析结果
RequestInfo& GetRequestInfo();
};
六、HTTP响应:
响应文件请求:目录列表展示、文件下载
响应CGI请求:上传文件
1.列表展示:
如果解析出的HTTP请求是一个文件请求,且该文件是一个目录,就进行文件列表展示。
1>浏览目录,获取该目录下所有的文件信息。
#include <dirent.h>
int scandir( const char *dir,struct dirent ***namelist,int (*filter) (const void *b),int (
* compare )( const struct dirent **, const struct dirent ** ) );
//扫描dir目录下(不包括子目录)满足filter过滤模式的文件,fillter为0表示不过滤
//返回的结果是compare函数经过排序的,并保存在namelist中
//第四个参数alphasort按字母排序和versionsort按版本排序
2>组织HTTP响应头部:先组织首行再组织头部。这里要注意一个目录下可能有很多的文件,如果遍历一遍所有的文件计算出content-length响应回去,效率太低,因此这里采用分块传输(Transfer-Chuncked),每次传输body的一部分内容。
//Transfer-Encoding: chunked\r\n\r\n 分块传输
//chunked发送数据的格式
//假设发送hello
//5\r\n :发送数据的大小
//hel\r\n :发送数据
//2\r\n
//lo\r\n
//0\r\n\r\n :发送最后一个分块
3>组织html展示页面。
<html>
<head>
<title>Index of /</VisionHou>
<meta charset='UTF-8'>
</head>
<body>
<h1>hc's Dir/</h1>
<hr />
<ol>
<li>
<strong><a href='/./'>./</a></strong><br />
<small>modified: Tue, 15 Feb 2019 13:48:04 GMT<br />
directory - 4 kbytes<br />
<br />
</small>
</li>
</ol>
<hr />
</body>
</html>
4>发送HTTP头部。
5>发送正文。
2.文件下载:
如果解析出的HTTP请求是一个文件请求,且该文件不是目录,就进行文件下载。
1>获取文件信息
2>组织HTTP响应头部信息
3>发送响应头部:先发首行,再发送头部
4>发送文件数据。
3.文件上传:
如果解析出的HTTP请求是一个CGI请求,就进行CGI响应即文件上传。文件上传成功,就在该目录下创建一个了文件,然后将请求中的body放进文件中。
1>创建管道:由于父子进程双向通信,所以创建两个管道,一个传数据,一个获取结果。
2>创建子进程:fork()。
3>设置子进程环境变量:HTTP请求头以K:V形式存在,使用环境变量传递头信息。
4>程序替换:子进程程序替换后对管道的文件描述符会发生改变,因此程序替换前必须进行文件描述符的复制。
●CGI程序:通过父进程获取的正文数据提取文件数据并将其写入文件。
●头部中的Content-Type中有一个boundary分隔符,用于对正文部分进行解析,将正文数据分割。
POST /upload HTTP/1.1
Host: 192.168.129.129:9999
Connection: keep-alive
Content-Length: 351
Cache-Control: max-age=0
Origin: http://192.168.129.129:9999
Upgrade-Insecure-Requests: 1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryQx2dIaIMSojqLEYy
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.129.129:9999/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
PHYS PATH/home/houcong/Documents/ShareDir/www/upload
content:[------WebKitFormBoundaryQx2dIaIMSojqLEYy
Content-Disposition: form-data; name="FileUpload"; filename="hello.txt"
Content-Type: text/plain
hello world!
------WebKitFormBoundaryQx2dIaIMSojqLEYy
Content-Disposition: form-data; name="FileUpload"; filename=""
Content-Type: application/octet-stream
------WebKitFormBoundaryQx2dIaIMSojqLEYy--
]
处理正文数据,实现文件上传:
//----boundary
//first_boundary: ------boundary
//middle_boundary: \r\n------boundary\r\n
//last_boundary: \r\n------boundary--
1>获取content-type中的boundary
2>从正文起始位置匹配first_boundary,获取上传文件名称,打开文件。
3>循环从剩下正文匹配middle_boundary,将该位置之前数据存储到文件中。
4>遇到last_boundary,将该位置之前数据存储到文件中,文件上传结束。
七、断点续传分块传输
文件在下载或上传时,有可能遇到网络故障而暂停下载/上传,那么下次下载/上传时我们希望从上次暂停的地方继续,而没有必要重新再来,这样可以节省时间提高效率。
1.Range/Content-Range:
1>Range:客户端发请求时用的是Range,制定第一个字节和最后一个字节的位置
//格式 Range:(unit=first byte pos)-[last byte pos]
Range: bytes=0-100 表示第 0-100 字节范围的内容
Range: bytes=-100 表示最后 100 字节的内容
Range: bytes=100- 表示从第 100 字节开始到文件结束部分的内容
Range: bytes=0-0,-1 表示第一个和最后一个字节
Range: bytes=300-500,501-999 多个范围
2>Content-Range:用于服务器的响应头中,在发出Range请求后,服务器会在 Content-Range返回当前接收的范围和总大小。
//格式Content-Range: bytes (unit first byte pos) - [last byte pos]/[all length]
Content-Range: bytes 0-100/1000
注意:使用断点续传的状态码是206,不是200
HTTP/1.1 200 Ok//一般情况
HTTP/1.1 206 Partial Content//使用断点续传
2.Last-Modified/Etag/If-Range:
有一种情况,客户端发起续传请求时,服务器端对应文件已经被改变,直接续传就会出错,通过 Last-Modified和 ETag 标识该文件是唯一的。
1>Last-Modified:
If-Modified-Since :由客户端向服务器发送的HTTP 头信息,记录最后修改时间。
Last-Modified:由服务器向客户端发送的HTTP 头信息,记录最后修改时间。
客户端通过 If-Modified-Since 将先前服务器端发过来的 Last-Modified 最后修改时间戳发送回去,让服务器端判断客户端的页面是否是最新的:
●如果不是最新的,则返回新的内容;
●否则返回 304 告诉客户端页面是最新的,客户端就可以直接从本地加载页面,不用再次下载。
2>Etag :
●一般是一串长的数字串 “589-4d648041f6c76” ,标识文件的唯一性。
●Etag 由服务器端生成,客户端通过 If-Range 来验证资源是否修改。
●如果请求报文中的 Etag没有发生变化,则应答报文的状态码为 206。发生了变化,应答报文的状态码为 200。
3>If-Range:
●判断文件是否发生改变,如果未改变,服务器发送客户端剩余接收的部分,否则发送整个文件。If-Range用 Etag 或者 Last-Modified作为返回值。
●必须和Range配套使用。
//格式:If-Range: Etag | HTTP-Date
If-Range: “627-4d648041f6b80”
If-Range: Fri, 22 Feb 2013 03:45:02 GMT
●下载和上传后的文件可以通过计算MD5值判断文件是否正确:
//两个值计算出来一样表示文件上传成功
//Linux:
md5sum filename
//Windows:
certuitl-hashfile filename MD5