自主实现HTTP

"让我们,跳吧在无比宏达的星系!" 


一、背景

        超文本传输协议(Hyper Text Transfer Protocol,HTTP)是一个简单的请求-响应协议,它通常运行在TCP之上。它指定了客户端可能发送给服务器什么样的消息以及得到什么样的响应。请求和响应消息的头以ASCII形式给出;而消息内容则具有一个类似MIME的格式。这个简单模型是早期Web成功的有功之臣,因为它使开发和部署非常地直截了当。

        万维网WWW(World Wide Web)发源于欧洲日内瓦量子物理实验室CERN,正是WWW技术的出现使得因特网得以超乎想象的速度迅猛发展。这项基于TCP/IP的技术在短短的十年时间内迅速成为已经发展了几十年的Internet上的规模最大的信息系统,它的成功归结于它的简单、实用。在WWW的背后有一系列的协议和标准支持它完成如此宏大的工作,这就是Web协议族,其中就包括HTTP超文本传输协议。                                                                                          来自这里

http被广泛使用,从移动端,pc端的浏览器,http成为打开互联网应用窗口的重要协议。

        


二、项目简介

 (1)项目描述

项目采用C/S(BS)模型,实现支持较为小型应用的http服务。做完项目后可以明显的对打开浏览器上网、关闭浏览器中,很多技术上细节的实现这样的概况有一定的了解。

技术特点:
● 网络编程(TCP/IP协议 soket套接字 http协议)

● 多线程技术

● cgi技术

● 线程池

(2) Http协议

什么是DNS(域名解析)?


三、自主实现HTTP(日志信息与底层连接)

(1)日志信息

打印日志信息,是我们在观察程序走向,查找bug的利器。好的日志信息打印,能帮我们快速定位错误。

我们设计的打印格式为:

[错误类型,内容信息,当前文件,当前行数];

当前文件与当前行数我们都可以使用编译器的内置宏:

__FILE__ 包含当前程序文件名的字符串

__LINE__ 表示当前行号的整数

__DATE__ 包含当前日期的字符串

__STDC__ 如果编译器遵循ANSI C标准,它就是个非零值

__TIME__ 包含当前时间的字符串

#pragma once
#include <iostream>
#include <string>
#define INFO 1
#define WARNING 2
#define ERROR 3
#define FATAL 4
#define LOG(level,message) Log(#level,message,__FILE__,__LINE__)

void Log(std::string level,std::string message,std::string file_name,int line)
{
    std::cout << "( " << "[" << level << "]" << "[" << message <<"]" << "[" << file_name << "]" << "[" <<line << "]" << " )" << std::endl;
}

Log函数那么多的参数 都需要手动传入? 为避免这个麻烦,用户使用的是LOG宏函数, LOG的参数level在预处理后变成数字! 为了保证其成为子串 在前面+ "#"即可。

(2)Socket套接字

网络模式之所以选择分层,其目的就是为了让每层实现解耦,降低开发学习成本。

       #include <sys/types.h>          /* See NOTES */
       #include <sys/socket.h>

       int socket(int domain, int type, int protocol);

①单例模式

对TcpServer在这里我更倾向于设计成单例模式,即程序运行期间,只允许出现一份这个类。

当然难题就到了 如何拿到 指向这个类的 唯一static对象 ,以及这个对象的初始化; 考虑到程序会牵涉到多线程,不得不引进锁! 当然这是系统POSIX库里。

       #include <pthread.h>

       int pthread_mutex_lock(pthread_mutex_t *mutex);
       int pthread_mutex_trylock(pthread_mutex_t *mutex);
       int pthread_mutex_unlock(pthread_mutex_t *mutex);

②连接接口

TcpServer;

int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);
                //套接字       协议号        传入传出参数                     opt大小

setsockopt 取自

    void Socket()
    {
        listen_sock =  socket(AF_INET,SOCK_STREAM,0);
        if(listen_sock < 0){
            LOG(FATAL,"Socket set Error!");
            exit(-1);
        }

        int opt = 1;
        //这里用来 让端口号 可复用
        setsockopt(listen_sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
        LOG(INFO,"Create Socket Success!");
    }

    void Bind()
    {
        struct sockaddr_in local;
        memset(&local,'0',sizeof(local));
    
        //填充local 进行 bind
        local.sin_family = AF_INET;
        //host -> net 字节序
        local.sin_port = htons(port);
        local.sin_addr.s_addr = INADDR_ANY;

        if(bind(listen_sock,(struct sockaddr*)&local,sizeof(local)) < 0){
            LOG(FATAL,"Bind Error!");
            exit(-2);
        }

        LOG(INFO,"Bind Success!");
    }

    void Listen()
    {
        //std::cout << "listen_sock: " << listen_sock << std::endl;
        if(listen(listen_sock,2) < 0){
            LOG(FATAL,"Listen sock fail!");
            exit(-3);
        }
        LOG(INFO,"Listen sock Success!");
    }

HttpServer;

这是我们暴露在外的接口;

#pragma once
#include "TcpServer.hpp"
#include "Log.hpp"

#include <iostream>

#define DEFAULT_PORT 8801

class HttpServer
{
private:
    int port;
    bool stop;

public:
    HttpServer(int _port = DEFAULT_PORT):port(_port),stop(false){}
    ~HttpServer(){}

    void Loop()
    {
        TcpServer* svr = TcpServer::GetInstance(port);
        LOG(INFO,"Loop Begin!");

        while(!stop)
        {
            struct sockaddr_in peer;
            socklen_t  len = sizeof(peer);

            int sock = accept(svr->Sock(),(struct sockaddr*)&peer,&len);
            if(sock < 0){
                continue;
            }
            LOG(INFO,"get a new link");
        }
    }
};

我们来看看演示吧;


四、报头管理

(1)工具类

①ReadLine与CutString

MSG_PEEK
This flag causes the receive operation to return data from the beginning of the receive queue without removing that data  from  the  queue.Thus, a subsequent receive call will return the same data.
 

译:

此标志使接收操作从接收队列的开头返回数据,而不从队列中删除该数据。因此,后续的接收调用将返回相同的数据

class Ulit
{
public:    //ReadLine的返回值 是告诉外层 sock里的数据是否读取完成
    static int ReadLine(int sock,std::string& out)
    {   
        //从套接字sock里读取 out进行串的输出
        //读到的字符 给ch 并添加到out上
        char ch;
        while(ch !='\n')
        {
            ssize_t size = recv(sock,&ch,1,0);
            if(size > 0){
                //有些报头的结尾为 \r\n
                if(ch == '\r'){
                    //MSG_PEEK时代表只是查看数据,而不取走数据
                    recv(sock,&ch,1,MSG_PEEK);
                    if(ch == '\n'){
                        recv(sock,&ch,1,0);
                    }
                    else{
                        ch = '\n';
                    }
                }
                out += ch;
            }
            else if(size == 0){
                return 0;
            }
            else{
                return -1;
            }
        }
        return out.size();
    }

    static bool CutString(const std::string& target,std::string& sub1_out,\
    std::string& sub2_out,const std::string& sep)
    {   
        size_t pos = target.find(sep);
        if(pos != std::string::npos)
        {
            //[0,pos)
            sub1_out = target.substr(0,pos);
            sub2_out = target.substr(pos+sep.size());
            return true;
        }
        return false;
    }
};

(2)报头类

①POST与GET

POST与GET都是常见的 请求方法;

POST 是从正文获取参数 并且需要有Content-length字段

GET 没有正文 参数获取是在请求行的 path路径中 以 “?”间隔

②状态码

  • 2XX(成功):成功——操作被成功接收并处理。
  • 3XX(重定向):重定向——需要进一步的操作以完成请求
  • 4XX(请求错误):客户端错误——请求包含语法错误或无法完成请求
  • 5XX(服务器错误):服务器错误——服务器在处理请求的过程中发生了错误

取自这里

我们常访问网站,有时候会出现一个404

浏览器访问的本质! 就是发起访问对端部署服务的主机里的文件!我们可以可看到访问/a/b/c目录时,是找不到的。因此 页面返回了404的访问错误。 

class HttpRequest
{
public:
    std::string request_line;
    std::vector<std::string> request_header;
    std::string blank;
    std::string request_body;

    //请求行
    std::string method;
    std::string uri;  //path(文件路径)?args(参数)
    std::string version;

    //请求字段
    std::unordered_map<std::string,std::string> header_kv;

    //POST
    int content_length = 0;
    std::string path;
    std::string suffix;
    std::string query_string;

    bool cgi = false;
    int size;
};


class HttpResponse
{
public:
    std::string response_line;
    std::vector<std::string> response_header;
    std::string blank = LINE_END;
    std::string response_body;

    int fd = -1;
    int status_code = OK;
};


五、HTTP获取请求

(1)获取、分析报头

EndPoint是 处理http报头的地方。 

①RecvHttpRequestLine\RecvHttpRequestHeader

分别从sock 按照ReadLine的方式 行读取。 填充到http_request中。

    bool RecvHttpRequestLine()
    {
        auto& line = http_request.request_line;
        //这里用到我们之前写的工具
        //ReadLine是按 行读的!!!
        if(Ulit::ReadLine(sock,line) > 0)
        {
            //去掉'\n'
            line.resize(line.size()-1);
            LOG(INFO,http_request.request_line);
        }
        else
        {
            //当Ulit返回的 < 0的值 说明出错了
            //应该终止服务器
            stop = true;
        }
        return stop;
    }

    bool RecvHttpRequestHeader()
    {
        std::string line;
        //因为请求字段 可能存在多行
        while(true)
        {
            line.clear();
            if(Ulit::ReadLine(sock,line) <= 0)
            {
                stop = true;
                break;
            }

            //什么时候说明 字段读完?
            //读到空行的时候
            if(line == "\n")
            {
                http_request.blank = LINE_END;
                break;
            }
            //去掉"\n"
            line.resize(line.size()-1);
            //填充 字段
            http_request.request_header.push_back(line);
            //打印日志
            LOG(INFO,line);
        }
        return stop;
    }

②ParseHttpRequestLine\ParseHttpRequestHeader

stringstream:

transform;

    void ParseHttpRequestLine()
    {
        //从sock读取的第一行 已经填入进line中了
        //解析line
        auto& line = http_request.request_line;
        std::stringstream ss(line);
        //line: 方法 uri 版本
        ss >> http_request.method >> http_request.uri >> http_request.version;
        //有些的method 为全大写或者小写 这里统一 为大写
        auto& method = http_request.method;
        std::transform(method.begin(),method.end(),method.begin(),::toupper);
    }

    void ParseHttpRequestHeader()
    {
        //填入unordered_map映射
        std::string key;
        std::string value;
    
        for(auto& iter:http_request.request_header)
        {
            // key: value  SEP --> ": "
            if(Ulit::CutString(iter,key,value,SEP)){
                http_request.header_kv.insert({key,value});
            }
        }
    }

③RecvHttpRequestBody\IsNeedRecvHttpRequestBody

    // 处理正文
    bool IsNeedRecvHttpRequestBody()
    {
        auto &method = http_request.method;
        if (method == "POST")
        {
            auto &header_kv = http_request.header_kv;
            auto iter = header_kv.find("Content-Length");
            if (iter != header_kv.end())
            {
                LOG(INFO, "Post Method, Content-Length: " + iter->second);
                http_request.content_length = atoi(iter->second.c_str());
            }
            return true;
        }
        return false;
    }

    bool RecvHttpRequestBody()
    {
        if (IsNeedRecvHttpRequestBody())
        {
            int content_length = http_request.content_length;
            auto &body = http_request.request_body;

            char ch = 0;
            while (content_length)
            {
                ssize_t s = recv(sock, &ch, 1, 0);
                if (s > 0)
                {
                    body.push_back(ch);
                    content_length--;
                }
                else
                {
                    stop = true;
                    break;
                }
                LOG(INFO,body);
            }
        }
        return stop;
    }

④测试

我们来看看效果吧~

六、构建响应

linux系统文件类型有七种,

即:普通文件、目录文件、字符设备、块设备、管道文件、链接文件和套接字(socket)文件。

通过 man 2 stat 可获取到 其中 st_mode 可以用来判别服务的类型。原文如下:

 通过创建struct stat可以获得 文件的一系列信息。 

如何理解首页?

我们每一次操作,点击一个页面框,都是一次跳转! 都是 文件的切换!

首页,也就是我们网页资源的根路径,访问服务器上的任何文件,都是根路径开始。

(1)处理方法与参数

①填充method与qurey_string

 // 这是非法请求方法
        if (http_request.method != "POST" && http_request.method != "GET")
        {
            std::cout << "method: " << http_request.method << std::endl;
            LOG(WARNING, "method is not valid!");
            code = BAD_REQUEST;
            goto END;
        }

        // POST ||  GET 的参数 与 路径
        if (http_request.method == "GET")
        {
            size_t pos = http_request.uri.find('?');
            if (pos != std::string::npos)
            {
                // 文件 与 参数分离
                Ulit::CutString(http_request.uri, http_request.path, http_request.query_string, "?");
                // 携带参数! 这里也就 意味着需要调用cgi程序
                http_request.cgi = true;
            }
            else
            {
                // 没带参数
                http_request.path = http_request.uri;
            }
        }
        else if (http_request.method == "POST")
        {
            // 因为POST的参数在 正文里
            // 借此 正文的读取 我们就放在cgi里
            http_request.cgi = true;
            http_request.path = http_request.uri;
        }
        else
        {
            // Do nothing 语法规范
        }

②TackleRequestPath

处理访问文件路径\处理文件类型\获取访问资源大小;

        bool TackleRequestPath()
    {
        std::string _path;
        struct stat st;
        _path = http_request.path;
        http_request.path = WEB_ROOT;
        http_request.path += _path;

        //1.路径是目录
        if(http_request.path[http_request.path.size()-1] == '/'){
            //返回首页目录
            http_request.path += HOME_PAGE;
        }

        //2.路径资源是否存在
        if(stat(http_request.path.c_str(),&st) == 0)
        {
            //该资源存在
            if(S_ISDIR(st.st_mode))
            {
                //是路径吗? 是路径的话 仍然需要返回 首页
                http_request.path += "/";
                http_request.path += HOME_PAGE;
                //更新path的st
                stat(http_request.path.c_str(),&st);
            }

            //这是文件 判断访问的是否是 可执行程序? ---> cgi
            if((st.st_mode & S_IXUSR)|| (st.st_mode & S_IXGRP) || (st.st_mode & S_IXOTH))
            {
                http_request.cgi = true;
            }
            // 要访问的资源 大小
            http_request.size = st.st_size;
        }
        else
        {
            //不存在
            LOG(WARNING,http_request.path + "Not Found");
            //设置 响应码
            http_response.status_code = BAD_REQUEST;
            return false;
        }

        size_t found = http_request.path.rfind(".");
        if(found == std::string::npos)
        {
            //默认 访问的文件类型
            http_request.suffix = ".html";
        }
        else
        {
            http_request.suffix = http_request.path.substr(found);
        }
        return true;
    }

(2)构建响应并返回

①文件类型描述 与 错误码描述

Content-Type,内容类型,一般是指网页中存在的Content-Type,用于定义网络文件的类型和网页的编码,决定文件接收方将以什么形式、什么编码读取这个文件,这就是经常看到一些Asp网页点击的结果却是下载到的一个文件或一张图片的原因。

在下面代码的容器里 只设计了一些类型。 


static std::string Code_Desc(int code)
{
    std::string desc;
    switch (code)
    {
    case 200:
        desc = "OK";
        break;
    case 404:
        desc = "Not Found";
        break;
    default:
        break;
    }
    return desc;
}

static std::string Suffix_Desc(const std::string& suffix)
{
    static std::unordered_map<std::string, std::string> suffix_desc = {
        {".html", "text/html"},
        {".css", "text/css"},
        {".js", "application/javascript"},
        {".jpg", "application/x-jpg"},
        {".xml", "application/xml"},
    };

    auto iter = suffix_desc.find(suffix);
    if(iter != suffix_desc.end())
    {
        return iter->second;
    }

    return "test/html";
}

    void BuildOKRespnse()
    {
        std::string line = "Content-Type: ";
        line += Suffix_Desc(http_request.suffix);
        line += LINE_END;
        http_response.response_header.push_back(line);

        std::string line = "Content-Length: ";
        if(http_request.cgi){
            //POST
            line += std::to_string(http_response.response_body.size());
        }
        else{
            line += std::to_string(http_request.size); //GET
        }
        line += LINE_END;
        http_response.response_header.push_back(line);
    }

    void HandlerError(std::string page)
    {
        //返回用户的对于page 的页面 即打开文件
        http_response.fd = open(page.c_str(),O_RDONLY);
        if( http_response.fd > 0)
        {
            //构建响应字段
            struct stat st;
            stat(page.c_str(),&st);

            http_request.size = st.st_size;
            //出错误 默认就返回的是静态网页
            std::string line = "Content-Type: text/html";
            line+=LINE_END;
            http_response.response_header.push_back(line);            

            line = "Content-Length: ";
            line += std::to_string(http_request.size);
            line +=LINE_END;
            http_response.response_header.push_back(line);            
        }
    }

    void BuildHttpResponseHelper()
    {
        auto &code = http_response.status_code;

        // 构建状态行
        auto &status_line = http_response.response_line;
        status_line += HTTP_VERSION;
        status_line += " ";
        status_line += std::to_string(code);
        status_line += " ";
        status_line += Code_Desc(code);
        status_line += LINE_END;

        // 构建响应正文,可能包括响应报头
        std::string path = WEB_ROOT;
        path += "/";
        switch (code)
        {
        case OK:
            BuildOKRespnse();
            break;
        case NOT_FOUND:
            //读取错误的 要返回什么页面!
            path += PAGE_404;
            HandlerError(path);
            break;
        case BAD_REQUEST:
            path += PAGE_404;
            HandlerError(path);
            break;
        case SERVER_ERROR:
            path += PAGE_500;
            HandlerError(path);
            break;
        default:
            break;
        }
    }


七、发送报文

(1)发送报头

       #include <sys/sendfile.h>

       ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
       

sendfile() copies data between one file descriptor and another.  Because this copying is done within the kernel,

sendfile() is more efficient than the combination of read(2) and write(2), which would require transferring data to and from user space.
 

译:

sendfile()在一个文件描述符和另一个文件之间复制数据。因为这种复制是在内核内完成的,

sendfile()比读(2)和写(2)的组合更有效,这需要在用户空间之间传输数据。

sendfile函数是在两个文件描述符中直接传递数据(完全在内核中操作),从而避免了用户和内核之间的数据拷贝,所以效率很高,也被称之为零拷贝。

网页发起的资源,都是一个一个目录下部署在服务器上的文件。

send的底层,是需要从用户态切换到内核态,从磁盘拷贝数据进内存的缓冲区,进行发送。

sendfile则不用这么繁琐,直接是从磁盘读取 以内核的身份 向sock 进行发送。

    // 发送响应
    void SendHttpResponse()
    {
        // 响应行
        send(sock, http_response.response_line.c_str(), http_response.response_line.size(), 0);
        // 响应字段
        for (auto iter : http_response.response_header)
        {
            send(sock, iter.c_str(), iter.size(), 0);
        }
        // 空行
        send(sock, http_response.blank.c_str(), http_response.blank.size(), 0);

        // 正文
        if (http_request.cgi)
        {
            auto &response_body = http_response.response_body;
            size_t size = 0;  // 每次发送的大小
            size_t total = 0; // 累计发送的大小
            const char *start = response_body.c_str();
            while (total < response_body.size() && (size = send(sock, start + total, response_body.size() - total, 0) > 0))
            {
                total += size;
            }
        }
        else
        {
            sendfile(sock,http_response.fd,nullptr,http_request.size);
            close(http_response.fd);
        }
    }

测试返回一个静态网页:

我们现在wwwroot创建一个首页 和 404访问错误页面;

    //返回一个静态网页! 只需要打开文件即可。 send处会对fd进行 关闭
    int ProcessNonCgi()
    {
        http_response.fd = open(http_request.path.c_str(),O_RDONLY);
        if(http_response.fd >= 0)
        {
            LOG(INFO,http_request.path + "Open Success!");
            return OK;
        }

        return NOT_FOUND;
    }

 本地环回:

浏览器;


八、CGI技术

前面那么多个位置出现了CGI,而且这个作者也不讲,不知道他在干嘛~

显然肯定是一个特别重要的技术! 那么CGI到底是是什么呢?

CGI(Common Gateway Interface)公共网关接口,是外部扩展应用程序与 Web 服务器交互的一个标准接口。

它可以使外部程序处理www上客户端送来的表单数据并对此作出反应, 这种反应可以是文件、 图片、 声音、 视频等,可以在浏览器窗体上出现的任何数据 [5]  。服务器端与客户端进行交互的常见方式多,CGI 技术就是其中之一。根据CGI标准,编写外部扩展应用程序,可以对客户端浏览器输入的数据进行处理,完成客户端与服务器的交互操作。

看一个现象;

 为什么要有CGI??

        cgi的出现绝非大佬们一拍脑门,搞出来的。要理解cgi 我们不得不窥测http服务器运行过程。

浏览器与http通信的底层是socket!

http服务与底层cgi通信的底层是进程间通信(这里我使用的是管道)! 

我们如果屏蔽掉http这一层。 本质上浏览器交互的实际上是http调用底层的cgi!

CGI程序的标准输入成了浏览器!

CGI程序的标准输出也是浏览器!

(1)CGI预备知识

        介绍了cgi的来龙去脉以及它在Web中不可替代的作用。我们接下来试试手吧。

①进程替换 

        HTTP服务器是一个进程,CGI也是一个进程。如何让一个进程去调用执行另外一个进程呢?  进程替换

        但是进程替换会把当前进程的代码和数据全部替换掉! 也就意味着发生替换后HTTPServer也就挂掉了,显然这不能容忍! 因此我们不能直接进行替换,而是fork子进程帮我们父进程完成替换

        进程替换函数有多个,但并不是本篇的重点; 如有需要可以参考

        

②进程间通信

管道通信(Communication Pipeline)即发送进程以字符流形式将大量数据送入管道,接收进程可从管道接收数据,二者利用管道进行通信。无论是SQL Server用户,还是PB用户,作为C/S结构开发环境,他们在网络通信的实现上,都有一种共同的方法——命名管道(pipe)。由于当前操作系统的不惟一性,各个系统都有其独自的通信协议,导致了不同系统间通信的困难。尽管TCP/IP协议已发展成为Internet的标准,但仍不能保证C/S应用程序的顺利进行。命名管道作为一种通信方法,有其独特的优越性,这主要表现在它不完全依赖于某一种协议,而是适用于任何协议——只要能够实现通信。                                                 取自这里

 选用管道的其中之一在于,其自带同步与互斥机制,能够在多线程的情况下使通信设计简单化。即:管道只允许一方进行读或写,其余都会进入挂起状态。

(2)CGI编写

①管道

       #include <unistd.h>

       int pipe(int pipefd[2]);

pipe() creates a pipe, a unidirectional data channel that can be used for interprocess communication. The array pipefd is used to return two file descriptors referring to the ends of the pipe. pipefd[0] refers to the read end of the pipe. pipefd[1] refers to the write end of the pipe.

译:

pipe()创建一个管道,一个可用于进程间通信的单向数据通道。数组pipefd用于返回引用管道末端的两个文件描述符。pipefd[0]表示管道的读取端。pipefd[1]表示管道的写入端


为什么要导入环境变量? 

主程序(子进程)的代码和数据,会被替换调用的CGI的代码数据覆盖掉。为此,难以拿到父进程(http)读取好 报头信息。 但是 环境变量属于内核代码,子进程会继承父进程的,不会因程序替换而被覆盖掉,与此同样道理的,如下面的文件数据结构,也不会受程序替换的影响,……

dup2重定向的理解;

      int dup2(int oldfd, int newfd);

dup2() makes newfd be the copy of oldfd, closing newfd first if necessary, but note the following

译:

dup2()使newfd成为oldfd的副本,如果需要,首先关闭newfd;

②进程替换

       #include <unistd.h>

       extern char **environ;

       int execl(const char *path, const char *arg, ...);

③数据传输 

父进程POST传参;

构建响应正文;

(3)CGI程序 

①comm.h

        cgi程序和访问wwwroot资源是一样的。 都是http服务器上 一个目录,这也成了一种解耦。

我们先来写一个简单的cgi程序 实现一个简易的计算器; 

我们正好可以 移植工具类Ulit里的 函数进行切分。 

bool GetQueryString(std::string& query_string)
{
    bool result = false;
    std::string method = getenv("METHOD");
    if(method == "GET")
    {
        query_string = getenv("QUERY_STRING");
        result = true;
    }
    else if(method == "POST")
    {
        //这里只需要拿参数的大小 --- > ContentLength
        int content_length = atoi(getenv("CONTENT_LENGHTH"));
        char c = 0;
        //读取正文
        while(content_length){
            //这里已经dup2 
            read(0,&c,1);
            query_string+=c;
            content_length--;
        }
        result = true;
    }
    else{
        result = false;
    }
    return result;
}


bool CutString(const std::string &target, std::string &sub1_out,
    std::string &sub2_out, const std::string &sep)
{
    size_t pos = target.find(sep);
    if (pos != std::string::npos)
    {
        //[0,pos)
        sub1_out = target.substr(0, pos);
        sub2_out = target.substr(pos + sep.size());
        return true;
    }
    return false;
}

②测试

int main()
{
  std::string query_string;
  GetQueryString(query_string);

  // a=200&b=100
  std::string str1;
  std::string str2;
  CutString(query_string, str1, str2, "&");

  // 变量名、值
  std::string name1;
  std::string value1;
  CutString(str1, name1, value1, "=");

  std::string name2;
  std::string value2;
  CutString(str2, name2, value2, "=");

  // 但是这里是 不显示的!  因为已经被 重定向 了 cout-->1
  // std::cout << name1 << ":" << value1<<std::endl;
  // std::cout << name2 << ":" << value2<<std::endl;
  std::cerr << name1 << ":" << value1 << std::endl;
  std::cerr << name2 << ":" << value2 << std::endl;

  int x = std::stoi(value1);
  int y = std::stoi(value2);

  // 可能向进行某种计算(计算,搜索,登陆等),想进行某种存储(注册)
  std::cout << "<html>";
  std::cout << "<head><meta charset=\"utf-8\"></head>";
  std::cout << "<body>";
  std::cout << "<h3> " << value1 << " + " << value2 << " = " << x + y << "</h3>";
  std::cout << "<h3> " << value1 << " - " << value2 << " = " << x - y << "</h3>";
  std::cout << "<h3> " << value1 << " * " << value2 << " = " << x * y << "</h3>";
  std::cout << "<h3> " << value1 << " / " << value2 << " = " << x / y << "</h3>";
  std::cout << "</body>";
  std::cout << "</html>";
  return 0;
}

 cgi程序拷贝到目录下;

(4)处理系统信号

        该http服务的底层是TCP,如果我们正在发送报头,突然对方不读了!我们发送的socket是否没有意义了? 是的! 对方进行了关闭,写入出现了问题,系统就会发送sigpipe

        #include <signal.h>

       typedef void (*sighandler_t)(int);

       sighandler_t signal(int signum, sighandler_t handler);
       仅仅需要捕获信号,不做处理。很多大型http_server都会选择这样处理


九、线程池引入

        cgi结束,本项目基本上也就到了尾声了!唉,终于完了。该http服务器是一种中小型应用,

引入线程池化技术,更高效地获取连接,处理任务。

        如果不理解线程池的读者,可以看看这个——这里

 (1)整体框架设计

(2)获取任务 

       #include <pthread.h>

       int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                          void *(*start_routine) (void *), void *arg);

thread: pthread_t的 线程变量

pthread_attr_t: 一般为nullptr 默认特性

start_routine; 函数指针 线程完成任务的函数

arg: 传给线程函数的参数

#pragma once
#include <iostream>
#include <pthread.h>
#include <queue>
#include "Task.hpp"
#include "Log.hpp"
#define NUM 8
class ThreadPool
{
private:
    int num;
    std::queue<Task> task_queue;
    pthread_mutex_t lock;
    pthread_cond_t cond;
    static ThreadPool *_Inst;
    bool stop = false;

    ThreadPool(int _num = NUM) : num(num) 
    {
        pthread_mutex_init(&lock,nullptr);
        pthread_cond_init(&cond,nullptr);
    }

    ThreadPool(const ThreadPool &) = delete;

public:
    ~ThreadPool()
    {
        pthread_mutex_destroy(&lock);
        pthread_cond_destroy(&cond);
    }

    static ThreadPool *GetInstance()
    {
        static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
        if (_Inst == nullptr)
        {
            pthread_mutex_lock(&mutex);
            if (_Inst == nullptr)
            {
                _Inst = new ThreadPool();
                _Inst->InitThreadPool();
            }
            pthread_mutex_unlock(&mutex);
        }
        return _Inst;
    }

    static void* ThreadRoutine(void *args)
    {
        //回指 线程池
        ThreadPool* tp = (ThreadPool*)args;

        while(true)
        {
            Task t;
            tp->Lock();
            while(tp->TaskQueueEmpty()){
                tp->ThreadWait();
            }
            tp->PopTask(t);
            tp->UnLock();
            // + 任务自带的解决方法
        }   
    }

    bool InitThreadPool()
    {
        // 创建线程池
        for (int i = 0; i < num; ++i)
        {
            pthread_t tid;
            if(pthread_create(&tid, nullptr,ThreadRoutine,this) != 0){
                LOG(FATAL,"Create ThreadPool fail!");
                return false;
            }
        }
        LOG(INFO,"Create ThreadPool Success!");
        return true;
    }

    void PushTask(Task in)
    {
        Lock();
        task_queue.push(in);
        UnLock();
        ThreadWakeUp();
    }

    //外面有一层大锁
    void PopTask(Task& out)
    {
        out = task_queue.front();
        task_queue.pop();
    }

    bool TaskQueueEmpty() 
    {
        return task_queue.empty();
    }

    void Lock() 
    {
        pthread_mutex_lock(&lock);
    }
 
    void UnLock()
    {
        pthread_mutex_unlock(&lock);
    }

    void ThreadWakeUp()
    {
        pthread_cond_signal(&cond);
    }

    void ThreadWait()
    {
        pthread_cond_wait(&cond,&lock);
    }
};
ThreadPool *ThreadPool::_Inst = nullptr;

(3)Task设计

Task;

 CallBack重载()运算符,调用服务启动;

HttpServer;

测试; 


十、表单与连接数据库

(1)什么是表单?

        http进行到这里差不多基本上是弄完了,这里补充的就是和数据库的联系。

表单在网页(WEB)中主要负责数据采集功能。一个表单有三个基本组成部分: 表单标签:这里面包含了处理表单数据所用CGI程序的URL以及数据提交到服务器的方法。包含了密码框、拉选择框和文件上传框等。

表单按钮:包括提交按钮、复位按钮和一般按钮;用于将数据传送到服务器上的CGI脚本或者取消输入,还可以用表单按钮来控制其他定义了处理脚本的处理工作。      取自这里噢

比如我们要登录一个网站,我们曾经注册了账号的。服务端是如何获得我们信息的呢?

不过在开始之前,我们改一改Makefile;

bash脚本;

 

我们就可以拿到一份发行版本的软件。 

保证mysql服务有效
在官网上下载合适自己平台的mysql connect库,以备后用

这就是我之前下载的mysql操作库;

如果不想将库拷贝到当前目录,也可以像下面一样建立软链接。 

(2)表单commit

我们写一个简单的提交

// 解锁库存
int main()
{
    std::string query_string;
    // from http
    if (GetQueryString(query_string))
    {
        std::cerr << query_string << std::endl;
        // name=tom&passwd=33444
        std::string name;
        std::string passwd;
        CutString(query_string, name, passwd, "&");

        // name
        std::string _name;
        std::string sql_name;
        CutString(name, _name, sql_name, "=");
        // passwd
        std::string _passwd;
        std::string sql_passwd;
        CutString(passwd, _passwd, sql_passwd, "=");

        std::string sql = "insert into user(name, passwd) values (\'";
        sql += sql_name;
        sql += "\',\'";
        sql += sql_passwd;
        sql += "\')";

        //插入数据库
        if(InsertSQL(sql))
        {
            std::cout << "<html>";
            std::cout << "<head><meta charset=\"utf-8\"></head>";
            std::cout << "<body><h1>注册成功!</h1></body>";
        }
    }
    return 0;
}

int mysql_query(MYSQL *mysql, const char *q);
第二个参数为要执行的sql语句

bool InsertSQL(std::string sql)
{
    MYSQL *conn = mysql_init(nullptr);
    mysql_set_character_set(conn, "utf8");
    if (nullptr == mysql_real_connect(conn, "127.0.0.1", USER.c_str(),passwd.c_str(), db.c_str(), 8801, nullptr, 0))
    {
        std::cerr << "connect error!" << std::endl;
        return 1;
    }
    std::cerr << "connetc success!" << std::endl;

    std::cerr << "query : " << sql << std::endl;
    int ret = mysql_query(conn, sql.c_str());
    std::cerr << "result: " << ret << std::endl;

    mysql_close(conn);
    return true;
}

创建成功! 


结语: 

现在我们就大概走通了一遍http服务的整个流程。当然该项目还是有很多没有解决的地方,

如何支持转发跳转(303),我们的协议文件中大量定义了宏,是否可以进行把配置文件化?

从技术上;

如:只支持Http1.0 、如何实现Http1.1的长链接、连接管理等等; 或者进行更高性能的改装,如加入epoll、select高级IO,提高管理socket的能力

从应用上;

可以在该项目部署其他有趣的项目,比如说个人博客,网页版的计算器、画图板等等


本篇到此结束,

感谢你的阅读

祝你好运~ 

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值