从零开始搭建Linux共享服务器(云享系统)

是男人就下100层,是汉子就来搭个服务器吧ヾ(◍°∇°◍)ノ゙
主要思路
所谓共享服务器,就是支持多人文件共享的HTTP服务器,功能十分简单,但我们关注的是如何尝试构建较高性能服务器、开放出用于扩展功能的接口、并且高效的管理文件数据;所以我们需要经过建立TCP服务端、将到来的客户端连接添加到线程池中、线程获取任务后解析请求报文、根据处理结果调用响应模块、响应模块根据需求调用MySQL(真汉子数据库也要自己封装)里的信息,组织响应报文这些过程,最后达到列表展示|文件上传|下载|断点续传的功能
ps:为了避免文章冗余,我将代码的实现略去只保留函数的原型和说明,有需要的小伙伴可以在最后的Github连接里找到项目源码O(∩_∩)O~

一、建立TCP服务端

构建单例模式
就我们这个例子,服务端存在一个就就够了,并且在一开始就最好能加载所有的资源并开始响应请求,所以我选用了饿汉模式来构建服务端类;

//HttpSever.hpp
class Server{static Server http_serv;Server() {};Server(const Server&)=delete;Server& operator=(const Server&)=delete;
public:static Server* GetHttpServer(){return &http_serv;}...
};
Server Server::http_serv;

建立TCP连接
这里我们是使用网络套接字建立TCP连接,作为服务端,需要先创建一个socket文件,在绑定端口后开始监听,当一个新的客户端连接请求到来并被放入已完成连接队列里时,Accept函数为这个链接创建一个专门用于通信的socket文件并返回操作句柄,服务器则继续监听,接收请求成功后,客户端就可以通过通信socket来和服务器进行数据交换了,至此能够收发数据的TCP服务端至此初步建立;我将这些步骤封装在Socket类里,这里通过图来简单说明下流程:

二、线程池创建与服务端任务添加

为什么要使用线程池技术呢?
我们来举一个真实的栗子—张三火了,CXK的那种,粉丝们都疯狂的想要访问这个服务器来下载它的照片(请强忍不适往下看O(∩_∩)O~)此时服务器所要处理成千上万的连接请求,我们知道,当建立一个连接后,我们往往需要将处理请求的任务交给其他的执行流,以便继续响应其他请求,如果为每一个接入的请求分配一个线程的话,如此巨量的线程创建,调度,销毁(暂且不论线程创建的上限是多少)所花费的系统开销也会成倍增长,以及因此导致的缓存和系统性能下降,更不用说有一天我上了热搜,大量突发性请求产生的线程很可能会使服务器内存达到极限,出现错误;而线程池维护者多个线程与一个等待队列,当线程池里有闲置线程时,等待队列里的任务才会被分配执行;就好比演唱会位子是固定的,所有人都要排队,当有空位置时才会放人进去,这样不管粉丝有多少,都不会让演唱会崩溃;
线程池创建
简单来说,线程池的核心是循环创建的一组线程以及一个等待队列,线程创建好后进行分离,然后因为队列里没有任务,进入休眠;当任务到来时,将任务添加进等待队列,然后再唤醒一个或多个闲置进程来"领取"它;那么任务具体是以什么样的形式被添加进来的呢?,我们要首先创建一个任务类:

//Threadpool.hhp
typedef bool(*Header)(int);
class Task{private:int socket;//传入参数Header TaskHeader;//任务函数的函数指针public:Task(int s=0,Header TH=NULL)//初始化赋值:socket(s),TaskHeader(TH){}void SetTask(int s,Header TH);//创建后赋值;bool Run();//运行;
};

我们规定要运行任的务以它的函数指针和它的参数构成的类,当需要运行时,将参数传入函数指针即可;
多线程的运行是并发的环境,而线程们获取和添加任务,又并非是原子性操作,极易引发线程安全问题,所以还要条件变量,互斥量控制线程的休眠与唤醒,互斥锁保证同一时间访问等待队列时的唯一性;我们来通过画图梳理一遍详细的过程:

服务端任务添加
当线程池构建好后,我们就可以在成功接收到连接请求后,将返回的新socket句柄和运行函数构建一个任务类,添加进线程池的任务队列,线程池就会在有空余时自动运行啦;

//HttpSever.hpp
void Go(){while(1){int client_socket=sock.Accept();if(client_socket==-1)continue;Task tt(client_socket,Header);pool.AddTask(tt);}

三、接收与解析HTTP数据

在线程获取到了任务后,需要对客户端发送到socket文件的请求HTTP数据(报文的首行,头部)进行读取,解析,才能够确定这个服务端想要什么,然后执行对应的响应;
接收HTTP首行-头部
客户端向我们发送的所有数据,都存储在客户端socket上,所以申请一块空间用来存放读到的数据,但要注意的是,在我们读取的时候,并不知道客户端是否已经发送了完整的http请求头在socket上,而一般的读取操作,会将读取的数据从原来的文件里删除,所以我要采用探测读取法,每次读取时并不删除socket里的数据,当检测读取到了头部结尾+空行(\r\n\r\n)时,证明头部和首行读取完毕,此时再用传统的读取方式再读一遍,删除socket里的头部数据;
解析HTTP首行-头部
在获得了Http首行和头部后,我们需要对其进行解析,为此我们需要维护一个类来存储这些信息;

//Tools.hpp
class RequestInfo{public:std::string _method;//请求类型std::string _version;//协议版本std::string _path_info;//请求相对路径std::string _path_phys;//请求绝对路径std::string _query_string;//查询字符串std::vector<std::string> _part_list;//下载请求块;std::unordered_map<std::string,std::string> hd_list;//存储头部键值对;struct stat _st;//获取文件信息;std::string _err_code="200";//错误码...};

首先,我们要把首行分离出来,得到首行里的请求方法,版本协议,以及URL,将URL里的请求路径拆分出来后,判断请求路径指向文件是否存在,存在则并更新文件信息并转化为绝对路径存储;其次,将接下来头部里的一个个键值对用unordered_map存放起来,至此首行与头部便解析完成;

四、组建响应模块

至此,我们已经和客户端建立了联系,明白了客户端想要什么,所以就根据要求调用不同的响应即可,但注意,客户的请求并不是一成不变的,合格的服务器程序要能够快捷的进行功能扩展,所以在这里我使用了继承/多态的方法来使得处理请求的模块化,在扩展时只专注与功能本身,其他方面继承基类方法即可:
首先定义一个用于扩展的抽象类:

//HttpResponse.hpp
class ResponseBasic{//响应接口类public:ResponseBasic(int sock):_cli_sock(sock){}virtual bool ProccessRun(RequestInfo& info)=0;//组织响应;virtual bool RspBody(RequestInfo& info)=0;//组建正文;virtual bool Response(RequestInfo& info)=0;//整个响应流程运行;void CommonHeader(RequestInfo& info);//最大程度上组建通用头部,减少代码冗余,其实是懒得再写...;bool InitResponse(RequestInfo& req_info);//实现好的响应模块初始化对下面的类成员进行初始化protected:bool SendData(const std::string &buf);//实现好的传输数据模块bool SendCData(const std::string &buf);//实现好的分块传输模块protected:mySQL SQL;int _cli_sock;std::string _rsp_header;//响应头std::string _rsp_body;//响应正文std::string _end;//分块传输结尾std::string _etag;//ETag字段用于唯一标识文件是否被修改std::string _lmod;//最后一次修改时间;std::string _date;//系统当前时间;std::string _fsize;//文件大小;std::string _ftype;//文件类型;std::string _cont_len;//正文长度;
};

接下来我们一共实现了五个模块:错误响应,文件列表展示模块,文件下载模块,文件上传模块,分块下载模块(断点续传)模块

错误响应模块

这个模块十分的重要,根据错误响应,客户端和服务端都可以清楚地知道当请求失败时,问题出在哪里,记得我们存储信息的类里面有一个错误码成员变量吗?,当处理请求失败时,立刻更新错误码,然后调用错误响应模块,根据我们在一个无序图里存放的错误信息解释,组织错误响应报文;
存放错误码解释的无序图:

std::unordered_map<std::string,std::string> err_exp={{"200","OK"},{"206","PARTIAL CONTENT"},{"400","Bad Request"},{"403","Forbidden"},{"404","Not Found"},{"405","Method Not Allowed"},{"413","Request Entity Too Large"},{"500","Internal Server Error"},
};
文件上传模块

当解析完成后,判断是一个CGI请求时,我们在外部完成文件上传响应,首先根据请求头部的键值对设置环境变量,创建一个子进程,继承父进程的环境变量,这样就传输了必要的头部信息,再通过匿名管道的方式传输数据,用管道描述符替换父子进程的标准输入和标准输出,这样通过cin,cout就可以进行数据交互啦;上传文件时,协议头里的Content-Type指定了boundary -一个随机生成的字符串,用来分隔文本的开始和结束

  • 境变量里的Content-Type字段里获取boundary,然后处理数据:
----boundary
first_boundary:   ------boundary
middle_boundary:  \r\n------boundary\r\n
last_boundary:    \r\n------boundary--
  • 文起始位置匹配first_boundary,获取上传文件名称后,创建一个同名文件
  • 从剩下正文匹配middle_boundary,将该位置之前数据存储到文件中
  • 当匹配到las_boundary,将该位置之前数据存储到文件中;
  • 通过管道让父进程组织一个成功的相应报文;
  • 父进程根据文件名更新文件相关信息到MySQL库中;
列表展示模块

如果解析出的HTTP请求是一个文件请求,并且该文件是目录时,就调用列表展示模块;

  • 根据用户调用MySQL中的对应表,将表中所有对象的信息进行获取并存储;
SQL.Select("root");//这是自行封装的函数,在下一节讲到;
  • 组织HTTP响应头部:先组织首行再组织头部。这里要注意目录下可能有很多的文件,如果遍历一遍所有的文件计算出Content-length响应回去,效率太低,因此这里采用分块传输(Transfer-Chuncked),每次传输body的一部分内容(Transfer-Encoding: chunked)
  • 组织html展示页面;
  • 发送HTTP头部,正文;
    html页面:
<html>
<head><title>Home/Catalog</title><meta charset='UTF-8'>
</head>
<body><h1>[Path]:/</h1><form action='/upload' method='POST' enctype='multipart/form-data'><input type='file' name='FileUpLoad' /><input type='submit' value='上传' /></form><hr/><ol><li><strong><a href='/hello.txt'>hello.txt</a></strong><br/><small>Modf: Tue,16 Jul 2019 10 : 04 : 26 GMT<br/>text/plain Size:11B<br/><br/></small></li></ol><hr/>
</body>
</html>
文件下载模块

当解析到的HTTP请求是一个文件请求,并且该文件不是目录时,调用文件下载模块

  • 根据文件名从MySQL中获取相关信息;
  • 组织Http相应首行与头部,并发送
  • 组织并发送正文数据
断点续传模块

在文件下载过程中,有可能遇到网络故障而暂停下载,当下次继续下载时,我们希望能够从上次的位置继续下载而不是重头开始,从而提高传输效率;
在这里,我们先要了解以下几个字段:
Range/Content-Range

  • Range是客户端发送续传请求时所用的字段,它定义了第一个字段与最后一个字段,共有五种方式
    Range: bytes=0-100 表示第 0-100 字节范围的内容 Range: bytes=-100 表示最后 100 字节的内容 Range: bytes=100- 表示从第 100 字节开始到文件结束部分的内容 Range: bytes=0-0,-1 表示第一个和最后一个字节 Range: bytes=0-100,101-200 多个范围
  • Content-Range是服务端发送续传响应时所用,表示当前响应范围和总大小
//Content-Range: bytes (unit first byte pos) - [last byte pos]/[all length]Content-Range: bytes 0-100/1000

Last-Modified/Etag–If-Range
有一种情况,客户端发起续传请求时,服务器端对应文件已经被改变,直接续传就会出错,通过 Last-Modified和 ETag 标识该文件是唯一的。
当客户端发起续传请求时,服务器端的对应文件已经发生了改变,此时在续传就会出现问题,所以我们要通过Last-Modified和ETag来表示文件的唯一性;

  • Last-Modified:

    • If-Modified-Since :由客户端向服务器发送的HTTP 头信息,记录最后修改时间。
    • Last-Modified:由服务器向客户端发送的HTTP 头信息,记录最后修改时间。

客户端通过 If-Modified-Since 将先前服务器端发过来的 Last-Modified 最后修改时间戳发送回去,让服务器端判断客户端的页面是否是最新的:如果不是最新的,则返回新的内容;否则返回 304 告诉客户端页面是最新的,客户端就可以直接从本地加载页面,不用再次下载。

  • Etag :

    • 一般是一串长的数字串 ,标识文件的唯一性。
    • Etag 由服务器端生成,客户端通过 If-Range 来验证资源是否修改。
      如果请求报文中的 Etag没有发生变化,则应答报文的状态码为 206。发生了变化,应答报文的状态码为 200。
  • If-Range:
    • 由客户端发出,让服务端判断文件是否发生改变,如果未改变,服务端发送客户端缺失的部分,否则发送整个文件。If-Range用 Etag 或者 Last-Modified作为返回值。
    • 必须与Range成对出现。
If-Range: <day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT
If-Range: <etag>

还可以计算MD5值来验证续传的文件的正确性;

通过MySQL库来管理文件

对,没错,张三又双叒叕火了,依然是CXK那种,不过这时他的粉丝们已经不满足于下载我的照片了,他们有的把自己和张三P在一起,然后上传到服务起来,有的写满的对张三的肺腑之言,也上传到这个服务器,那么问题来了,当我们想提高用户体验时,势必要管理下这些杂乱的文件,最好也能够根据不同的要求,不同的用户将特定的文件筛选,此时MySQL就是一个很好的选择,我们要做的就是用C++封装一个MySQL类,在每次添加或删除文件时都对它进行分类管理,在查找文件时直接使用MySQL语句即可;

  • 如何用C++封装一个MySQL请参考我之后的博客:
至此,一个共享HTTP服务器已经完成了,在以后的时间里,我会持续不断的扩展它的功能,让它变得美观完善,你要不要也来试试呢?
  • 项目源码

从零开始搭建Linux共享服务器(云享系统)相关推荐

  1. 乐视秒杀架构解读:从零开始搭建百万每秒订单系统

    http://dbaplus.cn/news-21-475-1.html 在各种秒杀活动大行其道的今天,订单系统的性能与稳定日益重要.乐视集团作为这一技术的佼佼者,在多次的电商狂欢节中都能抢占商机.拔 ...

  2. 从零开始搭建Linux服务器开发运行环境-详细步骤

    大家好,久违了! 时隔一年,我发表的上一篇博客年后第一更,谈谈收获及展望至今已快一年了,期间一直在忙没有时间写博客,原因就不细说了.由于我的个人云服务器到期了,所以我又买了一台阿里云的一年时长的ECS ...

  3. 从零开始搭建腾讯云上的 SpringBoot 容器化应用

    欢迎大家前往云+社区,获取更多腾讯海量技术实践干货哦~ 作者:李想 由于最近容器技术的火热,各大云计算厂商都已经提供了独立的容器服务,腾讯云也不例外.腾讯云容器服务的官方文档提供了一个基于Node.j ...

  4. 从零开始搭建腾讯云上的SpringBoot容器化应用

    首发于:https://cloud.tencent.com/developer/article/1017063 由于最近容器技术的火热,各大云计算厂商都已经提供了独立的容器服务,腾讯云也不例外.腾讯云 ...

  5. 从零开始搭建智能Ai外呼系统?

    前言:本文作者是咱们"AI产品经理大本营"团员@何静 ,她用非常接地气的文字介绍了智能外呼系统的必备入门信息,对于不是这个细分领域的AI从业者来说,非常值得一看. 1--序言 随着 ...

  6. libnet、libnids、libpcap轻松搭建Linux网络入侵检测系统

    利用三个源码包libnet.libnids.libpcap轻松搭建Linux网络入侵检测系统 如果要搭建基于Linux的网络入侵检测系统,必须要安装libnet.libnids.libpcap这三个源 ...

  7. linux下ganglia监控系统搭建,linux下ganglia监控系统搭建

    Ganglia监控软件主要是用来监控系统性能的软件,如:cpu .mem.硬盘利用率, I/O负载.网络流量情况等,通过曲线很容易见到每个节点的工作状态,对合理调整.分配系统资源,提高系统整体性能起到 ...

  8. 从零开始的Linux 阿里云ECS服务器搭建、FileZilla和宝塔

    云服务器ECS概述 云服务器ECS(Elastic Compute Service)是阿里云提供的性能卓越.稳定可靠.弹性扩展的IaaS级别云计算服务,让您像使用水.电.天然气等公共资源一样便捷.高效 ...

  9. 从零开始搭建solo个人博客系统

    文章目录 1.博客系统的搭建流程 2.服务器选购 2.1阿里云学生主机 2.2普通云主机 3.域名购买与备案(可选) 3.1域名购买 3.2域名服务器备案 3.3域名服务器解析 4.solo安装 4. ...

最新文章

  1. 2017年深度学习十大趋势预测
  2. 【二分答案】【哈希表】【字符串哈希】bzoj2946 [Poi2000]公共串
  3. 「Python基础知识」Python字符串是什么,如何使用
  4. nslookup查询结果详解
  5. [学习笔记]2020年win10最常用dos命令以及win+R即可运行的命令(使用命令打开自己想要的内容,总比找不到更好,赶快收藏)
  6. 花一天时间做一个高质量飞机大战游戏,过万字Unity完整教程!漂亮学妹看了直呼666!
  7. html中form异步验证,form表单验证
  8. virtualBox安装拓展包extension pack失败 --解决办法
  9. 工厂软件支持及测试是什么,软件工厂
  10. wd移动硬盘不能识别_西部数据移动硬盘无法识别恢复
  11. 小根堆创建,插入,删除,排序等操作图解
  12. 一个通用中间组件,简单通用的适配 ViewPager,以及 pager 中的 RecycleView 简化复杂的操作,简单直接。
  13. Unit 2: Linux/Unix Acquisition 2.1 Linux/Unix Acquistion Memory Acquisition
  14. 豪斯医生改名换姓上演《我的“钱”半生》?
  15. 关于DOTA2排名前5战队惯用英雄分析(二)
  16. 【C++】万能头文件 <bits/stdc++.h> 的用法和优缺点
  17. C-------------使用scanf输入字符串的故事;
  18. ISO9000年检记录_2014年
  19. 手写识别转文字怎么弄?用这三个手写识别转文字的软件就够了
  20. 快速了解历史Android操作系统版本(开发学习攻略)

热门文章

  1. c语言星星循环语序,C语言程序设计第五讲(2010级).ppt
  2. 淘宝商品评价api接口,淘宝评论视频API接口,淘宝评论API接口(app、h5端)
  3. 手写的汉字评分matlab,基于深度学习的手写汉字美感评分
  4. 杨守鸿 计算机学院,计算机学院军训师生慰问暨新生见面会顺利举办
  5. 【OpenAirInterface知识-2】OAI端到端部署之核心网部署
  6. 人工智能中的图灵测试
  7. createGiottoVisiumObject从10x原始数据创建 giotto对象
  8. linux中yum provide,Liunx 安装YUM有没有详细的教程。
  9. PHP类实例教程(二十):PHP类接口的实现接口
  10. 任务9、GUI用户登录