在我们对网络的学习过程中,会接触到网络编程,我们在网络中可以深刻认识到服务器与客户端的交互,当我们输入网址时背后发生的一系列后端操作,为了加深我们对网络部分的学习,我们找到了一个开源项目TinyWebServer来进行我们的学习,将其用C++部分重新实现,来巩固我们的Linux操作,socket网络编程与http协议的学习 Tinyhttp源码分析

项目名称

基于http协议的小型web服务器

实现了一个小型的web服务器,实现用户的基本网页请求,包括动态网页与静态网页的返回

实现环境

Linux centos 3.10、gcc 4.8.5

技术特点

  • 网络编程(http协议,TCP/IP协议,socket流式套接字)
  • 多线程技术(线程池)
  • CGI技术

功能描述

我们所做的是一个通过http协议限制的tcp通信socket套接字编程来满足服务器与客户端交互的一个程序

我们通过加装http协议使其发送的数据都是http封装的报文,有了http协议我们就可以完成对浏览器请求的解析,并依据请求给出回应,而为了让我们的服务器程序能适应多种编程环境,我们引入了一个cgi模块,得以实现cgi程序与web服务器之间的通信

因为有了cgi技术,我们可以在网页上实现一个简易的计算器程序,当我们用户在页面上进行计算请求时,服务端启用这个计算器程序进行计算,并将结果返回给用户,这就实现了用户与服务器之间的交互,而又因为在我们实际场景中服务器是可能被多个线程同时访问的,所以我们又通过一个线程池来对其池化处理,有效地解决了这个问题

涉及知识

http协议基本内容

http报文request部分

http报文response部分

http状态码及其含义

响应状态码有三位数字组成,第一个数字定义了响应的类别,且有五种可能取值

1xx:指示信息–表示请求已接收,继续处理

2xx:成功–表示请求已被成功接收、理解、接受   eg:200 OK //客户端请求成功

3xx:重定向–要完成请求必须进行更进一步的操作

4xx:客户端错误–请求有语法错误或请求无法实现

eg:400 Bad Request //客户端请求有语法错误,不能被服务器所理解 ;401 Unauthorized //请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用 ;403 Forbidden //服务器收到请求,但是拒绝提供服务 ;404 Not Found //请求资源不存在,eg:输入了错误的URL

5xx:服务器端错误–服务器未能实现合法的请求

eg:500 Internal Server Error //服务器发生不可预期的错误 ;503 Server Unavailable //服务器当前不能处理客户端的请求,一段时间后可能恢复正常

http协议的特点

客户 / 服务器模式 (B/S,C/S)
简单快速 ,HTTP 服务器的程序规模小 , 因而通信速度很快。
灵活 ,HTTP 允许传输任意类型的数据对象 , 正在传输的类型由 Content-Type 加以标记。
无连接 , 每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。(http/1.0 具有的功能, http/1.1 兼容 )
无状态

CGI技术

CGI(Common Gateway Interface) 是WWW技术中最重要的技术之一,有着不可替代的重要地位。CGI是外部应用程序(CGI程序)与WEB服务器之间的接口标准,是在CGI程序和Web服务器之间传递信息的过程。其实,要真正理解CGI并不简单,首先我们从现象入手 浏览器除了从服务器下获得资源(网页,图片,文字等),有时候还有能上传一些东西(提交表单,注册用户之类的),看看我们目前的http只能进行获得资源,并不能够进行上传资源,所以目前http并不具有交互式。为了让我 们的网站能够实现交互式,我们需要使用CGI完成,时刻记着,我们目前是要写一个http,所以,CGI的所有交互细节,都需要我们来完成。

我们用一张图来说明我们的http cgi

线程池

线程池技术在我们另一个项目已经详细介绍过了,在此项目中我们先一次性创建多个线程,避免了来时再创建的开销,我们使用线程池来对对其进行处理,构建相应返回,并将线程池设置为单例模式,只有一个线程池对象

具体实现

Log.hpp模块

此模块用以实现日志的打印,设立4种级别日志以及枚举4种错误方式,分别对我们可能出现的套接字创建,绑定,监听以及命令行参数出现的错误进行标识

具体实现:

#pragma once
#include<iostream>
#include<string>
#include<sys/time.h>#define Notice  1
#define Warning 2
#define Error   3
#define Fatal   4 //使用枚举的方式在项目中比数字更加直观
enum ERR
{SocketErr = 1,//套接字错误BindErr,//绑定错误ListenErr,//监听错误ArgErr//命令行参数错误
};
//传入错误级别以及文件
#define LOG(level,message) \Log(#level,message,__FILE__,__LINE__)//#传入宏参(单#转化为字符串),FILE获取文件名(预定义符号),LINE获取行号//日志:[日志级别][message][时间戳][filename][line]
void Log(std::string level, std::string message, std::string filename, size_t line)//
{struct timeval curr;//创建时间结构体用于获取时间戳gettimeofday(&curr, nullptr);//获取时间戳std::cout << "[" << level << "]" << "[" << message << "]" << "[" << curr.tv_sec << "]" << "[" << filename << "]" << "[" << line << "]" << std::endl;
}

Sock.hpp模块

此模块用以实现对网络接口的处理

Sock():套接字的创建

bind():绑定端口与ip

listen():设置监听位accept做准备

accept():链接

Getline():按行读取报文接口,这个接口主要用于下面分隔http协议的request与

#pragma once
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<sys/wait.h>
#include<sys/sendfile.h>
#include<unistd.h>
#include<stdlib.h>
#include<cstring>
#include<vector>
#include<unordered_map>
#include<sstream>
#include"Log.hpp"
#define BACKLOG 5class Sock
{
public:static int Socket(){int sock = socket(AF_INET, SOCK_STREAM, 0);//创建网络相关的资源与文件,(设置ipv4协议,tcp面向字节流,默认0)//原先我们都是直接把错误打印出来,但现在改为日志的形式if (sock < 0){LOG(Fatal, "socket create error");//日志输出exit(SocketErr);}return sock;//返回网络创建的文件描述符}static void Bind(int sock, int port)//绑定端口号与ip{struct sockaddr_in local;//提出addr结构体bzero(&local, sizeof(local));local.sin_family = AF_INET;//设置结构体位ipv4local.sin_port = htons(port);//端口号,字节序转网络字节序local.sin_addr.s_addr = htonl(INADDR_ANY);//绑定0用以接收服务器所有ip收到的地址if (bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0){//进行绑定LOG(Fatal, "socket bind error");exit(BindErr);}}static void Listen(int sock)//设置监听,准备accept{if (listen(sock, BACKLOG) < 0){//监听失败,打印日志LOG(Fatal, "socket listen error");exit(ListenErr);}}static void SetSockOpt(int sock)//设置端口被释放后立即就可以使用{int opt = 1;setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));//设置层级管理权限,套接字,支持层次,设置选项(允许套接口与一个已在使用的地址捆绑),存放选项值的缓冲区,缓冲区长度}static int Accept(int sock)//阻塞等到客户端连接,接受链接{struct sockaddr_in peer;//设置请求链接的结构体socklen_t len = sizeof(peer);//计算大小int s = accept(sock, (struct sockaddr*)&peer, &len);//连接套接字,请求链接的ip与端口号,addr大小//别人连接我的信息都放在了peer里面,可以都写到日志里面if (s < 0){LOG(Warning, "accept error");//再次申明一次,这里就相当于我们去引流,如果没有引到,会接着去引,所以说即使错误了也不要紧,不能直接exit}return s;//不断链接,直到链接到}//作用:整体读取一行内容static void GetLine(int sock, std::string& line){//换行符是没有硬性规定的,有些平台下有可能是\r \n  \r\n 这三种都有可能是换行符标志//最好的方式是按字符读取char c = 'X';while (c != '\n'){ssize_t s = recv(sock, &c, 1, 0);//接收数据赋给s,数据端套接字,数据缓存区,缓存区大小,连接方式默认0if (s > 0){//可以考虑在往后读一个,判断一下是否下一个是\n,如果是那还好,如果不是就读上来一个正常字符,此时你再把这个字符需要放回内核缓冲区是很难做到的//所以这里需要重新的认识一个这个recv函数的扩展MSG_PEEK(数据的窥探功能)就是只看一下,但是并不从内核缓冲区拷贝到用户缓冲区if (c == '\r'){ssize_t ss = recv(sock, &c, 1, MSG_PEEK);if (ss > 0 && c == '\n'){//说明我们的换行符是\r\nrecv(sock, &c, 1, 0);//因为窥探并没有把数据读走}else{// \rc = '\n';}}//1.读取到的内容是常规字符//2.\n//此时将\r \r\n -> 为了\n//因为这个读上来的\n在显示的时候会造成不美观if (c != '\n'){line.push_back(c);}}}}};

HttpServer.hpp模块

该模块负责将服务器启动起来,是web服务器的起点

InitServer():初始化网络服务器,建立网络连接

Start():开启服务,接收客户端请求

GetInstance():加锁饿汉模式保证线程安全

#pragma once#include <pthread.h>
#include "Sock.hpp"
#include "Protocol.hpp"
#include "ThreadPool.hpp"#define PORT 8081//端口号8081class HttpServer{
private:int port;//端口号int lsock;//ipThreadPool *tp;//线程对象static HttpServer *http_svr;//创建一个http服务static pthread_mutex_t lock;//锁//HttpServer()//{}
public:HttpServer(int _p = PORT):port(_p), lsock(-1), tp(nullptr){}static HttpServer *GetInstance(int sk)//设置饿汉单例,需要使用时进行加锁实例化{if (nullptr == http_svr){pthread_mutex_lock(&lock);if (nullptr == http_svr){http_svr = new HttpServer(sk);//实例化}pthread_mutex_unlock(&lock);}return http_svr;}void InitServer()//初始化网络服务{signal(SIGPIPE, SIG_IGN);//屏蔽SIGPIPE信号,该信号会默认结束进程,告诉客户端不进行关闭//建立网络一系列操作lsock = Sock::Socket();Sock::SetSockOpt(lsock);Sock::Bind(lsock, port);Sock::Listen(lsock);tp = new ThreadPool();//将该网络服务分配给一个线程tp->InitThreadPool();}void Start()//开启服务{for (;;){int sock = Sock::Accept(lsock);//链接客户端if (sock < 0){//无连接跳过continue;}LOG(Notice, "get a new link ...");Task *tk = new Task(sock);//分配线程池处理tp->PushTask(tk);//多线程方案//在堆上申请空间,保存sock。//直接传入sock地址,当来了其它连接,会修改sock值//int *sockp = new int(sock);//处理协议后需要通过套接字接送数据和发送数据,所以用套接字做参数//pthread_t tid;//pthread_create(&tid, nullptr, Entry::handlerhttp, (void *)sockp);//线程分离,自动回收,避免等待//pthread_detach(tid);}}~HttpServer()//析构服务器{if (lsock >= 0){//关闭客户端close(lsock);}}
};HttpServer *HttpServer::http_svr = nullptr;//初始化服务器
pthread_mutex_t HttpServer::lock = PTHREAD_MUTEX_INITIALIZER;//锁静态初始化

Util.hpp工具类

该模块主要是封装一些通用API,为其它模块提供服务

StringParse():分割请求行

MakeStringToKV():分割报头

StringToInt():字符串转整型,其实也相当于stii()

#pragma once
#include"Sock.hpp"
#include"Log.hpp"
//打过来的请求都是一行一行的,为了方便分析,我也想要以行的方式读取上来
//需要按行读取的功能class Util
{
public://字符串拆分,也就是1变多static void StringParse(std::string& request_line, std::string& method, std::string& uri, std::string& version)//将我们的一行请求行数据拆分成请求方法,url以及协议版本{//这是最简单的方法来进行字符串解析std::stringstream ss(request_line);ss >> method >> uri >> version;}//容器内的东西是坚决不能修改的,所以最好传参的时候不要传引用static void MakeStringToKV(std::string line, std::string& k, std::string& v)//此方法处理的是请求报头,将请求报头的每一行都以kv的形式分隔存储k:报头;v:内容{std::size_t pos = line.find(": ");//pos的指针应该在冒号的位置if (pos != std::string::npos){//做一下判断,确定找到了//第一个参数是:从哪里开始,第二个参数是长度k = line.substr(0, pos);v = line.substr(pos + 2);}}static ssize_t StringToInt(const std::string& v)//字符串转整型{// return stoi(v);//stringstream 也可以转换成整数std::stringstream ss(v);ssize_t i = 0;ss >> i;return i;}
};

ThreadPool.hpp模块

该模块就是搭建了一个线程池,将我们并发的多个网络请求分配到线程池中的各个线程中去,也是一个经典的生产者消费者模型

#pragma once
#include<iostream>
#include<pthread.h>
#include<queue>
#include"Protocol.hpp"
typedef void(*handler_t)(int);class Task//任务队列
{
private:int sock;//套接字handler_t handler;public:Task(int sk) :sock(sk), handler(Entry::HandlerHttp)//传入套接字与报头{}void Run(){handler(sock);}~Task(){}
};class ThreadPool
{
private:std::queue<Task*> q;//任务队列int num;//线程数pthread_mutex_t lock;//互斥锁pthread_cond_t cond;//条件变量
public:void LockQueue()//加锁{pthread_mutex_lock(&lock);}void UnlockQueue()//解锁{pthread_mutex_unlock(&lock);}bool IsEmpty()//判空{return q.size() == 0;}void ThreadWait()//线程等待{pthread_cond_wait(&cond, &lock);//在cond变量下等,并释放锁}void ThreadWakeup()//唤醒所有等的线程{pthread_cond_signal(&cond);}public:ThreadPool(int n = 8) :num(n)//初始化8个线程{}static void *Routine(void *args){//static是静态成员函数,不能够去访问非静态的成员函数,但是我现在要使用你ThreadPool里面queue,可以通过对象的方式来ThreadPool *this_p = (ThreadPool*)args;//拿到线程池this_p->LockQueue();while (this_p->IsEmpty()){//检测是否有任务this_p->ThreadWait();}Task *tk = this_p->PopTask();//分配线程this_p->UnlockQueue();tk->Run();//让该线程处理请求delete tk;}void InitThreadPool(){pthread_mutex_init(&lock, nullptr);pthread_cond_init(&cond, nullptr);pthread_t tid[num];for (int i = 0; i < num; i++){//创建数量为num的线程pthread_create(tid + i, nullptr, Routine, this);pthread_detach(tid[i]);//县城分离}}void PushTask(Task *tk)//将任务入队{LockQueue();q.push(tk);UnlockQueue();ThreadWakeup();}Task* PopTask(){Task *tk = q.front();q.pop();return tk;}~ThreadPool(){pthread_mutex_destroy(&lock);pthread_cond_destroy(&cond);}
};

Protocol.hpp

这是我们最重要的一个模块,主要负责协议的处理部分,包括获取请求,制作响应,发送响应而等

篇幅较长,我们再将其细分

首先是通用模块,这里解决了响应种类的问题以及提取文件类别的问题

#pragma once
#include"Sock.hpp"
#include"Log.hpp"
#include"Util.hpp"//对于一个服务器来说,应该是从webroot下去拿资源
#define WEBROOT "webroot"#define HOMEPAGE "index.html"#define VERSION "HTTP/1.0"//为了兼容性考虑我们使用http/1.0//不想要被外面人访问到,因为这个函数只和本文件强相关
static std::string CodeToDesc(int code)
{std::string desc;//写成这种形式是代码的维护性将极高,如果来了新的错误,直接加一个case即可switch (code){case 200:desc = "OK";break;case 404:desc = "Not Found";break;case 401:desc = "Unauthorized";break;case 403:desc = "Forbidden";break;case 400:desc = "Bad Request";break;case 500:desc = "Internal Server Error";break;case 503:desc = "Server Unavailable";break;default:break;}return desc;
}static std::string SuffixToDesc(const std::string& suffix)//传入文件结尾,返回相应的文件类别
{if (suffix == ".html" || suffix == ".htm"){return "text/html";}else if (suffix == ".js"){return "application/x-javascript";}else if (suffix == ".css"){return "text/css";}else if (suffix == ".jpg"){return "image/jpeg";}else{return "text/html";}
}

class HttpRequest类

该类主要是为了处理接收的请求文本,我们再来回顾一下http协议的格式内容,存在请求行,请求报头,以及空行后的请求正文

以及我们的url详细解析

该类中详细处理了收到的http协议request部分,包括请求行拆分解析为请求方法,并做相应的处理,url,将url详细拆分,以及版本号

//用来专门分析过来的Request
//请求行、请求报头、空行、正文
class HttpRequest//处理接收的请求文本
{
private:std::string request_line;//请求行std::vector<std::string> request_header;//请求报头std::string blank;//空行std::string request_body;//正文
private:std::string method;//请求方法std::string uri;//urlstd::string version;//协议版本//这两个是通过分析uri来拿到Path和参数std::string path;std::string query_string;std::unordered_map<std::string, std::string> header_kv;//对应报头的kv结构ssize_t content_length; //为了区分开给三个默认为-1表示不存在 0表示没有  >0表示有正文bool cgi;//是否开启cgi模式的标志位ssize_t file_size; //供后续使用的,不然会显得代码很冗余再调用一次statstd::string suffix;//提取文件后缀
public:HttpRequest() :blank("\n"), content_length(-1), path(WEBROOT), cgi(false), suffix("text/html")//默认不开启cgi,为get方法,默认文件为html格式,初始化请求request{}void SetRequest(std::string& line)//设置空行{request_line = line;}//解析请求行//也就是把request_line打散成为三个字符串分别为method uri version //所以此时就需要一个这个功能,可以写在Util这个工具类里面void RequestLineParse(){Util::StringParse(request_line, method, uri, version);//调用之前的字符串分割函数,将请求行分为三部分//LOG(Notice,request_line);//LOG(Notice,method);//LOG(Notice,uri);//LOG(Notice,version);}void InsertHeaderLine(const std::string& line)//插入请求行{request_header.push_back(line);LOG(Notice, "line");}void RequestHeaderParse()//拆分报头{//我们更想用一个Key值直接找到它的Value值,所以可以对一行报头拆分搞个map//说简单点就是:把vector遍历一遍,然后把里面的每个字符串拆分成KVfor (auto& e : request_header){//再把每个字符串拆成map的KV结构std::string k, v;Util::MakeStringToKV(e, k, v);//将e分为kv两部分,分别为类别以及对应的参数LOG(Notice, k);LOG(Notice, v);if (k == "Content-Length"){content_length = Util::StringToInt(v);//设置content_length字段,设置为正文字符数}header_kv.insert({ k, v });//插入报头}}//1.请求方法必须得是post//2.Content-length必须不是0bool IsNeedRecvBody()//处理post方法{//Post PoSt  POST 所以还需要做字母大小写同一//这里就是用了一个C语言的接口strcasecmp()可以忽略大小写问题比较字符串if (strcasecmp(method.c_str(), "POST") == 0 && content_length > 0){//解析请求方法,若为post则开启cgi进行参数传递,从客户端发送给服务器cgi = true;return true;}return false;}ssize_t GetContentLength()//获取正文长度{return content_length;}void SetRequestBody(const std::string& body)//设置请求正文{request_body = body;}bool IsMethodLegal()//判断请求方法是否合法{if (strcasecmp(method.c_str(), "POST") == 0 || strcasecmp(method.c_str(), "GET") == 0){return true;}return false;}bool IsGet()//判断方法是否为get{return strcasecmp(method.c_str(), "GET") == 0 ? true : false;}bool IsPost()//判断是否为post方法{return strcasecmp(method.c_str(), "POST") == 0 ? true : false;}void UriParse()//解析url{std::size_t pos = uri.find('?');//get方法中参数在?后,寻找?分隔符,参数和路径的分隔符是"?",//这个"?"是唯一的,如果查询"?"会被urlencode转化。如果不存在"?"说明没有参数。存在"?"在url中找到"?"前面为路径,后面为参数if (pos == std::string::npos){//说明没有找到'?',直接加上后面字符path += uri;}else{//说明找到了'?'path += uri.substr(0, pos);//分割url,前面为路径query_string = uri.substr(pos + 1);//后面为请求方法cgi = true;//需要cgi处理}}void SetUriEqPath()//设置绝对路径{path += uri;}std::string GetPath()//获取路径{return path;}void IsAddHomePage()//添加文件路径{if (path[path.size() - 1] == '/'){path += HOMEPAGE;}}std::string SetPath(std::string _path){return path = _path;}void SetCgi(){cgi = true;}bool IsCgi(){return cgi;}void SetFileSize(ssize_t s)//设置文件大小{file_size = s;}ssize_t GetFileSize()//获取文件大小{return file_size;}std::string GetQueryString()//获取url中的参数{return query_string;}std::string GetBody()//获取请求正文{return request_body;}std::string GetMethod()//获取方法{return method;}std::string MakeSuffix()//获取文件类别{std::string suffix;size_t pos = path.rfind(".");//找到.后的部分进行判断if (std::string::npos != pos){suffix = path.substr(pos);}return suffix;}~HttpRequest(){}
};

class HttpResponce类

该类相对于请求request就简单多了,只是分拆我们的响应报文

//用来专门分析过来的Response
//状态行、响应报头、空行、响应正文
//可以理解为定义了一套标准
class HttpResponse
{
private:std::string status_line;//响应行std::vector<std::string> response_header;//响应报头std::string blank;std::string response_body;//相应正文public:HttpResponse() :blank("\r\n")//初始化为空行{}//版本号 状态码 状态码描述void SetStatusLine(const std::string& line)//设置响应行{status_line = line;}std::string GetStatusLine()//提取响应行{return status_line;}const std::vector<std::string>& GetRspHeader()//设置响应报头{response_header.push_back("\n");return response_header;}void AddHeader(const std::string& ct)//添加报头{response_header.push_back(ct);}~HttpResponse(){}
};

接下来就是我们的的处理模块,将sock获取的请求与响应分别进行解析,制作响应报头返回,而后在构建响应正文的时候分为,不使用cgi技术处理GET无参,POST无参,普通网页文件直接发送的情况,使用CGI技术处理POST有参的情况,使用进程替换将数据与代码替换到cgi程序中,而后通过管道重定向到标准输入的方式,将POST的参数传递给进程,而GET方法请求方式与正文大小则直接通过环境变量传递,而后将处理结果以标准输出重定向到管道的方式传递给父进程,完成子进程的处理,最后将服务器守护进程化

//通过sock来获取过来的Request和Response,然后在交给对应的两个类里面的具体方法进行拆解分析
class EndPoint
{
private:int sock;HttpRequest  req;HttpResponse rsp;
private:void GetRequestLine()//获取请求行并进行分析{std::string line;Sock::GetLine(sock, line);//读上来的这一行我们要拿去给HttpRequest进行分析req.SetRequest(line);//设置进去了之后,最主要的是能够分析出来他所使用请求方法、URL、版本号req.RequestLineParse();}void SetResponseStatusLine(int code)//设置响应行{std::string status_line;status_line = VERSION;//首先加入版本status_line += " ";status_line += std::to_string(code);//加入上面公共方法的状态码status_line += " ";status_line += CodeToDesc(code);//通过状态码转换为所对应的描述status_line += "\r\n";//对于我们自己写的我们就设置为以\r\n为结束rsp.SetStatusLine(status_line);//设置进我们的响应报文中}void GetRequestHeader()//获取请求报头{//报头就是不断的循环读,一直读取到"\n"停止std::string line;do{//至少读一次为空line = "";Sock::GetLine(sock, line);req.InsertHeaderLine(line);} while (!line.empty());//上面是获得了报头,此时在对报头进行一下分析req.RequestHeaderParse();//设置进响应报文}void SetResponseHeaderLine()//获取响应报头{//图片、网页类型不同表示Content-type不同std::string suffix = req.MakeSuffix();std::string content_type = "Content-Type: ";content_type += SuffixToDesc(suffix);content_type += "\r\n";rsp.AddHeader(content_type);}void GetRequestBody()//获取请求正文{int len = req.GetContentLength();char c;std::string body;while (len != 0){//读上面length模块所显示的字符数ssize_t s = recv(sock, &c, 1, 0);body.push_back(c);len--;}req.SetRequestBody(body);//设置进请求报文中}
public:EndPoint(int _sock) :sock(_sock){}void RecvRequest(){//整体思路://获取完整http请求//分析http请求//读取并分析完了GetRequestLine();//读取报头GetRequestHeader();//好好分析Sock::GetLine你就会发现,在上面读取报头的时候,你已经把空行信息也读完了//读取正文(但是是否有正文呢?所以1.要看请求方法--Post  2.要看Content-length,但是这个参数是要经常判断是否大于0的,所以可以把它直接算出来content-length成员变量)if (req.IsNeedRecvBody()){//说明有正文,那就读GetRequestBody();//这个接口不但把正文都读取上来了,而且还设置进了我所定制的协议里面 }//已经读完了所有的请求}void MakeResponse()//制作响应报文{//开始分析//只想要处理GET和POST方法int code = 200;//当状态码为200时正常std::string path; //在goto 和 end之间最好不要定义变量,因为跳转的时候有可能会导致一些变量的定义丢失if (!req.IsMethodLegal()){//说明不是POST或者GET方法LOG(Warning, "method is not legal");code = 404;goto end;}//说明此时方法是没有问题的,那么就该看是POST还是GET方法了,得到方法就知道是通过正文传参还是uri进行传参了//其中url也分为两种一种是不带?(比如说请求首页),一种是带问好的,也就是有参数的//不管咋分析uri,首先你得是GET方法if (req.IsGet()){req.UriParse();}else{//此时就是POST方法req.SetUriEqPath();}req.IsAddHomePage();//因为直接用ip+port访问服务器应该让他的默认路径为首页路径   // get && 没有参数 ->path 对应着你把资源从服务器上拿下来// 下面两种方式对应着你向服务器要资源,所以才需要参数// get && 有参数  ->path + query_string // post 路径在uri中,参数在body中//所以此时就应该检测对应的path是否在系统内合法 /a/b/c//此时可以使用一个系统接口stat(),成功返回0,失败返回-1path = req.GetPath();LOG(Notice, path);struct stat st;if (stat(path.c_str(), &st) < 0){LOG(Warning, "html is not exist! 404");code = 404;goto end;}else{//说明文件路径合法//结尾没有带'/'但是最后一个是一个目录//但是此时有可能访问的是目录,也有可能是一个普通的html,对于webroot下的任何目录都有一个index.htmlif (S_ISDIR(st.st_mode)){//这里应该在给他拼接index.htmlpath += "/";req.SetPath(path);req.IsAddHomePage();}else{//这里又分为了2种可能,可能请求的是一个可执行程序(二进制)或者html,//如果是可执行程序是由权限属性的if ((st.st_mode & S_IXUSR) || \(st.st_mode & S_IXGRP) || \(st.st_mode & S_IXOTH)){//CGI(就是让服务器帮你把程序跑完,然后给你返回结果的技术)req.SetCgi();}else{//这里说明是一个正常的网页请求      }}if (!req.IsCgi()){req.SetFileSize(st.st_size);//设置对应的文件大小}}end://制作response//设置状态行SetResponseStatusLine(code);SetResponseHeaderLine();//设置响应报头//SetResponseHeader();}//要做的基本工作就是打开文件,再把文件里面的内容通过sock发回去void ExecNonCgi(const std::string& path)//不使用cgi的方法{ssize_t s = req.GetFileSize();int fd = open(path.c_str(), O_RDONLY);if (fd < 0){LOG(Error, "path is not exists bug!!!");return;}//ssize_t sendfile(int out_fd,int in_fd, off_t *offset,size_t count);//在这里面in_fd should be file descriptor opend for reading //out_fd                                            writing //offset 偏移量 count//这也是系统调用接口sendfile(sock, fd, nullptr, s);close(fd);}void ExecCgi()//使用cgi方法{//这里想要使GET的CGI处理和POST的CGI处理方式有所不同//std::string arg;//if(req.IsGet()){//把参数拿上来//arg = req.GetQueryString();//}//else{//arg = req.GetBody();//}//如果是cgi模式就是想要让服务器去创建一个子进程,然后子进程去处理参数,然后把结果在交给父进程,父进程在返回std::string  content_length_env;std::string path = req.GetPath();std::string method = req.GetMethod();//可能回想为什么要这样做呢?难道全部都是用管道直接把数据都发过去不好吗? 事实上这里也就是想要严格的遵守CGI的标准,对于url有参数的就是要环境变量传参//对于想POST方法的,可能body十分的长,所以选择使用管道来传std::string method_env = "METHOD=";method_env += method; //环境变量是全局变量,子进程也是可以获得到的std::string query_string;std::string query_string_env;std::string body;//进程间通信使用管道,但是管道是单向的,所以要出创建两个 1个是父进程给子进程数据  一个是子进程给父进程返回结果int pipe_in[2] = { 0 };int pipe_out[2] = { 0 };//站在被调用进程的角度//pipe[0]是读端 pipe[1]是写端pipe(pipe_in);pipe(pipe_out);putenv((char*)method_env.c_str());pid_t id = fork();if (id == 0){//子进程//但是此时数据是父进程拿着呢,但是要交给子进程去处理,所以这里还涉及到一个进程间通信的问题close(pipe_in[1]); //父进程来写数据进管道,子进程来读close(pipe_out[0]);//父进程来读结果,子进程来写dup2(pipe_in[0], 0);dup2(pipe_out[1], 1);//第一种是通过环境变量传参给子进程   GET ->  query_string if (req.IsGet()){query_string = req.GetQueryString();query_string_env = "QUERY_STRING=";query_string_env += query_string;putenv((char*)query_string_env.c_str());}else if (req.IsPost()){content_length_env = "CONTENT-LENGTH=";content_length_env += std::to_string(req.GetContentLength());putenv((char*)content_length_env.c_str());}execl(path.c_str(), path.c_str(), nullptr); //你要执行的程序的路径和文件名exit(0);}//fatherclose(pipe_in[0]);close(pipe_out[1]);char c = 'X';//第二种通过管道传参给子进程   POST -> body if (req.IsPost()){//post方法body = req.GetBody();int size = body.size();int i = 0;for (; i< size; i++){write(pipe_in[1], &body[i], 1);//使用写端将请求报文的数据挨个写入父进程}}//让子进程去拿着参数去帮你执行完毕以后,对于父进程还需要拿到子进程返回回来的结果ssize_t s = 0;do{s = read(pipe_out[0], &c, 1);//父进程读取管道获取结过if (s > 0){send(sock, &c, 1, 0);}} while (s > 0);waitpid(id, nullptr, 0);//阻塞进程}void SendResponse()//发送响应报文{//先把状态行给发出去std::string line = rsp.GetStatusLine();send(sock, line.c_str(), line.size(), 0);//再把响应报头发出去for (auto& e : rsp.GetRspHeader()){send(sock, e.c_str(), e.size(), 0);}//发送空行,但是这个逻辑不好,其实可以把这个行上面的报头结合到一块,这样上面其实连我们的空行也已经发过去了//所以此时就剩下发送response_body//但是此时应该判断执行模式,有可能你只是想要一个普通的html,还有可能是给我传过参数//1. GET方法uri里面有参数//2.POST方法 有正文//3 分析出来的路径最终访问的是一个可执行程序都要使用cgi技术if (req.IsCgi()){//yes -> cgi LOG(Notice, "use cgi model!");ExecCgi();}else{LOG(Notice, "use no cgi model!");//请求的就是一个普通的htmlstd::string path = req.GetPath();ExecNonCgi(path);//以非cgi的形式运行}}//是基于短链接的,完成一次请求和响应就应该把链接进行关闭~EndPoint(){if (sock >= 0){close(sock);}}
};class Entry
{
public:static void HandlerHttp(int sock)//将其设置为单例{//int sock = *(int*)arg;//就是把堆空间上的arg拷贝到了私有栈里面//delete (int*)arg;//再把堆空间保存的sock进行释放#ifdef DEBUG//条件编译char request[10240];recv(sock, request, sizeof(request), 0);close(sock);
#else EndPoint *ep = new EndPoint(sock);ep->RecvRequest();ep->MakeResponse(); //这里只研究两种方法:是GET和POST 对于其他的方法如果来了,直接构建response返回回去ep->SendResponse();delete ep;
#endif}
};

这就是我们这个小型web服务器的主要模块

我们再来梳理一下实现细节

项目主要包括

HttpServer.hpp:web服务器的起点
ThreadPool.hpp:多线程部分
Sock.hpp:关于网络套接字部分
Protocol.hpp:关于协议处理部分,获得请求,分析请求,制作响应,发送响应。
Util.hpp:与协议无关的处理,比如:字符串转整形
Log.hpp:日志

套接字部分

可以从网络中获得请求。

建立套接字,设置端口复用(setsockopt),绑定套接字(端口号和IP地址),监听套接字(listen),等待连接请求的到来

线程池部分

一次性创建多个线程,避免来一个请求再创建一个线程的开销。使用线程池中的线程来处理请求,构建响应返回,并且将线程池设成单例模式(懒汉模式),只有一个线程池对象。

注意:线程处理的任务时一个个请求。

协议处理

获得请求并细分:

根据HTTP协议请求包括四个部分请求行,请求报头,空行,请求正文。其中请求行包括请求方法,url,协议版本。请求报头是一些属性信息,以KV形式呈现。报头和正文以空行分隔。

收到请求,需要将其拆分成4个部分,请求行,请求报头,空行,请求正文。每个部分还需要细分。由于协议是以行为单位,所以按行读取,由于不同客户端制作的协议行分隔符可能不同(\r,\n,\r\n)。所以按字符读取,我们获取后 统一以\n结尾。

请求行:

读取一行,将请求行细分为请求方法,url,协议版本。以空格分隔。

请求报头:

循环按行读取,读取到空行结束。保存在map中,方便查找。

空行:

在读取报头时可以一起读上来。

请求正文:

GET方法没有正文,不需要读取正文。

POST方法,如果在报头中有一个属性Content_length表示正文字节数。如果大于0,需要读取正文,按字符读取,读取content_length个字符。

分析请求

主要处理GET和POST方法,其它方法直接构建响应返回,状态码为404。

分析请求主要是获得请求资源的路径和参数。请求资源的路径在url中。注意:在web的目录中都会有一个text.html文件。

请求的资源从web根目录开始。GET和POST方式传参方式不同,GET方法参数在url中,POST在正文中

如果是GET方法:

参数和路径的分隔符是"?",这个"?"是唯一的,如果查询"?"会被urlencode转化。如果不存在"?"说明没有参数。存在"?"在url中找到"?"前面为路径,后面为参数。

如果是POST方法:

路径直接是url,有请求正文的话,参数是请求正文。

其中GET有参数,POST有参数需要使用CGI技术来处理参数。

判断请求资源:

请求资源不存在,直接构建响应返回,状态码为404。

如果是一个目录,说明访问下的该目录下的text.html文件。

如果是可执行程序,需要使用cgi技术处理。

如果是一个普通网页文件,说明直接就是访问该文件内容

构建响应返回

以行为单位。根据HTTP协议响应包括四个部分状态行,响应报头,空行,响应正文。其中状态行包括协议版本,状态码,状态码描述。响应报头是一些属性信息,以KV形式呈现。报头和正文以空行分隔。

构建响应行:
                直接构建,需要通过不同状态码,对应不同描述。发送响应行。构建协议版本,状态码(可以响应的为200,不可以响应的404),状态码描述。最后以/r/n结尾。

构建响应报头:
                需要添加一些属性content_type等。发送响应报头。

构建空行:
                发送空行

构建响应正文:发送响应正文。构建响应正文需要区分是否使用CGI技术。
                1.不需要用到CGI技术:

GET无参,POST无参,普通网页文件,直接打开文件,将文件内容发送过去。

2.使用CGI技术:

创建子进程,使用进程替换,替换到CGI程序。替换进程主要替换子进程的数据和代码。通过环境变量和进程间通信技术来传数据。

参数传递:参数POST方式参数用管道发送给替换进程,替换进程不知道管道文件描述符,需要将管道重定向到标准输入。替换进程从标准输入那参数。GET方式,通过环境变量发送给替换进程。请求方式通过环境变量发送给替换进程(通过请求方式来确定从哪里获取参数),正文大小通过环境变量发送给替换进程(POST需要读取正文)。

结果传递:替换进程不清楚管道文件描述符,重定向管道到标准输出,将处理的结果从标准输出传递给父进程。

web服务器将处理的结果发送到网络中。

参考资料:

  • 开源Tinyhttp原码链接: link.

  • CGI获取POST方法和GET方法传参详解链接: link.

  • 管道链接: link.

  • 单例模式链接: link.

  • 线程池链接: link.

  • 守护进程链接: link.

项目--基于http协议的小型web服务器相关推荐

  1. 基于HTTP实现的小型web服务器

    主要流程为:服务器获得请求–>响应并处理请求–>返回结果. 完整的HTTP过渡到TCP实现客户端与服务器的交互过程 1.当客户端执行网络请求的时候,从url中解析出url的主机名,并将主机 ...

  2. mysql 花生壳 2003_基于HTTP协议实现的小型web服务器的方法

    这篇文章主要介绍了基于HTTP协议实现的小型web服务器的方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧 我们先了解一下这个 ...

  3. 自主小型Web服务器实现——TinyHttp

    目录 一.功能 二.技术特点 三.主要框架 四.CGI技术 五.流程 六.具体实现细节 6.1 套接字部分 6.2 线程池部分 6.3 协议处理部分(主要部分) 七.测试结果 一.功能 实现一个自主开 ...

  4. 基于epoll实现简单的web服务器

    1. 简介 epoll 是 Linux 平台下特有的一种 I/O 复用模型实现,于 2002 年在 Linux kernel 2.5.44 中被引入.在 epoll 之前,Unix/Linux 平台下 ...

  5. 自主开发的小型Web服务器

    自主开发的小型Web服务器 1. 技术特点 2. 具体步骤 3. CGI技术 4. Mysql连接 5. Gitee原码链接 6. 参考Blog 1. 技术特点 网络编程(http协议,TCP/IP协 ...

  6. Python基础 3.4 HTTP协议和静态Web服务器

    HTTP协议和静态Web服务器 3.4.1 HTTP协议 1.HTTP协议介绍 全程:超文本传输协议 作用:规定浏览器和web服务器通信的数据格式 HTTP协议是在应用层的协议,基于传输层的TCP传输 ...

  7. arm Linux 低成本方案,参赛作品《低成本基于ARM+Linux平台搭建web服务器的物联网学习板》...

    [报名阶段需要填写的内容] 1. 参赛者姓名(必填项): 王徕泽 2. 单位或学校名称(选填项): 徕泽电子工作室 3. 当前职务或职称(选填项): 室长 4. 参赛作品的名字(必填项): 低成本基于 ...

  8. 〖Web全栈开发③〗—HTTP协议和静态web服务器

    HTTP协议和静态web服务器 (一)三次握手和四次挥手 (二)HTTP协议 2.1 HTTP协议的定义 2.2 HTTP协议的组成 (三)搭建python自带静态web服务器 3.1 静态web服务 ...

  9. http协议跟服务器交互,基于HTTP协议的客户端与服务器之间的数据交互方法专利_专利查询 - 天眼查...

    L-种基于HTTP协议的客户端与服务器之间的数据交互方法,应用于智能$居系统 中,以使用户的客户端与智能家居系统的服务器之间建立稳定的网络连接并进行数据交 互,所述方法包括步骤: 所述客户端与所述服务 ...

最新文章

  1. plsql如何连接oracle11g_64位win7 plsql连不上oracle11gr2 64位—plsql连接配置
  2. 诗歌rails之 Logger
  3. 论文浅尝 | 以知识图谱为基础的开放域对话生成的目标规划
  4. 复数基础——例题_4
  5. python time datetime模块最详尽讲解
  6. 2.ActiveMQ下载和安装(Linux版)
  7. MFC显示位图和显示透明位图
  8. 数据结构之图的基本介绍
  9. 哔哩哔哩mac客户端!亲测!支持big sur系统
  10. Avatar Scaler
  11. asp.net 打印html文件,关于ASP.NET页面打印技术的常用方法总结
  12. 3D游戏导论七 · 模型与动画
  13. 计算机网络——基本认识
  14. 高薪程序员必备—Redis高性能缓存数据库
  15. 基于SAS对美国新冠数据的分析
  16. Vue3.0 mixin的使用 以及混合方式讲解
  17. css 状态条动画_CSS动画的状态
  18. 转载:SAP MDG —— 你最想知道答案的34个问题(基于1909版本)
  19. 安卓强制恢复出厂_手机越来越卡!备份一下恢复出厂设置,还有的救吗?
  20. 用golang判断文件是否存在

热门文章

  1. 惠普6L接无线打印服务器,惠普6L的常见问题及解决办法
  2. Java Excel 提示修复模板问题
  3. pbds库学习笔记(优先队列、平衡树、哈希表)
  4. 如何用python编程机器人培训班_如何Python入门
  5. 一篇文章带你搞定 MongoDB 中的索引(创建/查看/删除)
  6. 推荐几个机器学习和数据挖掘领域相关的中国大牛
  7. 行动大于进球:评价足球运动员的行动价值(Actions Speak Louder than Goals: Valuing Player Actions in Soccer)
  8. 印象笔记不能同步 显示服务器端出现,印象笔记同步失败,服务器端出现问题...
  9. 微信小程序登录功能实现(通过用户名和密码)
  10. 瞻仰了一下Gavin King的风采