基于线程池的HTTP服务器(C/C++)

目录

基于线程池的HTTP服务器

一、项目背景:

二、目标:

三、项目环境和技术特点:

(1)、环境:

(2)、技术特点:

四、从头到尾细节和思路:

(1)、套接字:

(2)、日志

(3)、Task类,线程池、HttpServer类:

(4)、通用工具类:

(5)、协议处理细节:

(6)、CGI机制:

(7)、引入数据库编写CGI程序:

(8)、Makefile的编码和脚本:

(9)、简单编写表单网页测试:

(10)、项目目录结构:

 五、测试结果:

 六、结束语:


基于线程池的HTTP服务器

一、项目背景:

        http协议被广泛使用,从移动端,pc端浏览器,http协议无疑是打开互联网应用窗口的重要协议,http在网络应用层中的地位不可撼动,是能准确区分前后台的重要协议。

二、目标:

        本项目采用C/S模型,编写中小型应用的http,并结合mysql,理解常见互联网行为,做完之后可以从技术上理解从你上网开始,到关闭浏览器的所有操作中的技术细节。

三、项目环境和技术特点:

(1)、环境:

        Centos-7云服务器,VScode(远程连接,也可以使用vim),MySQL5.7。

(2)、技术特点:

        网络编程、多线程技术、cgi技术、shell脚本,线程池。

四、从头到尾细节和思路:

注:代码注释写的比较详细,可能有点啰嗦,如果想要源码可以私信。

(1)、套接字:

        一上来,基于TCP/IP协议栈的层状结构,http在传输层采用的协议是tcp,那话不多说,套接字编程整上;(TcpServer.hpp头文件)

注:这次设计采用单例模式设计,单例模式提供静态成员方法获取对象,使用双检查,避免锁竞争;还有稍微注意一下避免服务器主动关闭,而出现不能马上重启的现象,使用setsockopt进行设计地址复用,原因是因为主动关闭会进入TIME_WAIT状态。

#pragma once

#include <iostream>
#include <string>
#include <cstdlib>

#include <unistd.h>
#include <pthread.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>

#include "Log.hpp"

#define BACK_LOG 10

// 单例,懒汉模式
class TcpServer
{
public:
    static TcpServer* GetInstance(uint16_t port)
    {
        // 静态分配锁
        pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
        // 双检查,避免并发导致锁竞争
        if(_svr == nullptr)
        {
            pthread_mutex_lock(&lock);
            if(_svr == nullptr)
            {
                _svr = new TcpServer(port);
                _svr->_InitServer();
            }
            pthread_mutex_unlock(&lock);
        }
        return _svr;
    }
    int Sock()
    {
        return _listen_sock;
    }
    ~TcpServer()
    {
        if(_listen_sock >= 0)
        {
            close(_listen_sock);
        }
    }
private:
    // 创建套接字句柄
    void _Socket()
    {
        _listen_sock = socket(AF_INET, SOCK_STREAM, 0);
        if(_listen_sock < 0)
        {
            LOG(FATAL, "scoket error");
            exit(2);
        }
        // 设置地址复用,让服务器出现问题能快速重启
        int opt = 1;
        if(setsockopt(_listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0)
        {
            LOG(FATAL, "setsockopt error");
            exit(3);
        }
        LOG(INFO, "socket success");
    }
    // 绑定端口
    void _Bind()
    {
        struct sockaddr_in local;
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = htonl(INADDR_ANY);
        socklen_t len = sizeof(local);
        if(bind(_listen_sock, (struct sockaddr*)&local, len) < 0)
        {
            LOG(FATAL, "bind error");
            exit(4);
        }
        LOG(INFO, "bind success");
    }
    // 把句柄设置进行监听状态
    void _Listen()
    {
        if(listen(_listen_sock, BACK_LOG) < 0)
        {
            LOG(INFO, "listen error");
            exit(5);
        }
        LOG(INFO, "listen success");
    }
    // 初始化服务器
    void _InitServer()
    {
        _Socket();
        _Bind();
        _Listen();
        LOG(INFO, "TcpServer init success");
    }
private:
    // 把构造函数私有
    TcpServer(uint16_t port) : _port(port), _listen_sock(-1)
    {}
    // 把拷贝构造和赋值给禁用
    TcpServer(const TcpServer& ) = delete;
    TcpServer& operator=(const TcpServer& ) = delete;
private:
    uint16_t _port; // 端口
    int _listen_sock; // 监听的fd 
    static TcpServer* _svr; // 全局唯一对象
};
// 唯一对象类外初始化
TcpServer* TcpServer::_svr = nullptr;

(2)、日志

        为了测试方便,简单编写一个日志信息打印,这样测试时很快就能打印相关信息。(Log.hpp)

#pragma once 

#include <iostream>
#include <ctime>
#include <string>

// 定义日志级别
#define INFO 1
#define WARNING 2
#define ERROR 3
#define FATAL 4

// 日志格式 [日志级别][时间戳][日志信息][文件名称][行数]
// 为了方便使用,时间戳函数填充,文件名称和行数使用C语言内置的预定义宏
// 所以调用者只需传入两个参数,这里使用宏替换来实现

// 这里宏替换时,日志等级想让其显示成字符串,
// 所以需要在其前面加个#,实现把一个宏参数变成对应的字符串的语义
#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 << "]";
    std::cout << "[" << time(nullptr) << "]";
    std::cout << "[" << message << "]";
    std::cout << "[" << file_name << "]";
    std::cout << "[" << line << "]" << std::endl;
}

(3)、Task类,线程池、HttpServer类:

       HttpServer类(HttpServer.hpp):http服务器调用tcp套接字进行连接管理,然后http不断接收连接,构建任务(task),塞到线程池里面就行,然后不断循环接收即可。

#pragma once

#include <sstream>
#include <signal.h>
#include "ThreadPool.hpp"
#include "TcpServer.hpp"

#define PORT 8080

class HttpServer
{
public:
    HttpServer(uint16_t port = PORT): _port(port), stop(false)
    {}
    void InitServer()
    {
        // 处理url时需要管道给CGI程序传参,如果发生超时,对端关了,服务器就挂掉了
        signal(SIGPIPE, SIG_IGN);
    }
    void Loop()
    {
        TcpServer* tsvr = TcpServer::GetInstance(_port);
        LOG(INFO, "HttpServer loop begin");
        while(!stop)
        {
            // 处理链接
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int sock = accept(tsvr->Sock(), (struct sockaddr*)&peer, &len);
            if(sock < 0)
                continue;
            std::stringstream ss;
            ss << "get a new link from " << inet_ntoa(peer.sin_addr) << ":" << ntohs(peer.sin_port);
            LOG(INFO, ss.str());
            // 构建任务,放进任务队列
            Task task(sock);
            ThreadPool::GetInstance()->PushTask(task);
        }
    }
private:
    uint16_t _port; 
    bool stop; // 这个信号可以在异常处理时使用,本项目暂不考虑
};

        然后主函数构成HttpServer对象进行循环接收即可。(Main.cpp)

#include "HttpServer.hpp"

// 没有指定端口
void Usage(const char* proc)
{
    std::cout << "Usage: \n\t" << proc << " port" << std::endl;
}

int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(1);
    }
    HttpServer* hs = new HttpServer(atoi(argv[1]));
    // 初始化
    hs->InitServer();
    // 循环
    hs->Loop();
    return 0;
}

        线程池(ThreadPool.hpp):线程池就是一个生产者消费者模型,其中一个任务队列存储任务,若干数量的线程进行处理或者等待,同时有锁和条件变量维护多线程的同步互斥机制就可以了。

注:此项目使用pthread库,所以在编写线程运行函数时要注意一下this指针的问题。

#pragma once

#include <iostream>
#include <queue>
#include <pthread.h>
#include "Task.hpp"
#include "Log.hpp"

#define NUM 10

class ThreadPool
{
public:
    static ThreadPool* GetInstance(size_t num = NUM)
    {
        pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
        if(_tp == nullptr)
        {
            pthread_mutex_lock(&mtx);
            if(_tp == nullptr)
            {
                _tp = new ThreadPool(num);
                _tp->_InitThreadPool();
            }
            pthread_mutex_unlock(&mtx);
        }
        return _tp;
    }
    bool IsStop()
    {
        return _stop;
    }
    // 向调用者提供一个塞任务进来的接口
    void PushTask(const Task& task)
    {
        // 塞任务时加锁
        _LockQueue();
        _task_queue.push(task);
        _UnlockQueue();
        // 唤醒一个线程处理
        _WaskUpThread();
    }
private:
    // 线程处理函数
    static void* _ThreadRoutine(void* args)
    {
        // pthread库再创建线程时就只允许函数只有一个参数,C++从普通成员函数又自带this指针
        // 不通过对象调用所以设置成静态的
        // 但是使用过程又需要this指针,所以参数就传this指针
        ThreadPool* _this = (ThreadPool*) args;
        while(!_this->IsStop())
        {
            _this->_LockQueue();
            // 这里循环判断一下,避免误唤醒导致异常
            while(_this->_IsEmpty())
            {
                _this->_ThreadWait();
            }
            Task task;
            _this->_GetTask(task);
            _this->_UnlockQueue();
            // 任务处理方法在锁外面调用,不暂用锁资源
            task.ProcessOn();
        }
        return nullptr;
    }
    // 获取任务时,调用处一定已经加锁,所以这里不需要加锁
    void _GetTask(Task& task)
    {
        task = _task_queue.front();
        _task_queue.pop();
    }
    void _LockQueue()
    {
        pthread_mutex_lock(&_lock);
    }
    void _UnlockQueue()
    {
        pthread_mutex_unlock(&_lock);
    }
    bool _IsEmpty()
    {
        return _task_queue.empty();
    }
    void _ThreadWait()
    {
        pthread_cond_wait(&_cond, &_lock);
    }
    void _WaskUpThread()
    {
        pthread_cond_signal(&_cond);
    }
    void _InitThreadPool()
    {
        // 创建线程
        int i = 0;
        for(; i < _num; ++i)
        {
            pthread_t tid;
            if(pthread_create(&tid, nullptr, _ThreadRoutine, (void*)this) < 0)
            {
                LOG(FATAL, "init threadpool error");
                exit(6);
            }
            // 创建成功就分离,不关系结果
            pthread_detach(tid);
        }
        LOG(INFO, "init threadpool success");
    }
private:
    ThreadPool(size_t num) : _num(num), _stop(false)
    {
        pthread_mutex_init(&_lock, nullptr);
        pthread_cond_init(&_cond, nullptr);
    }
    ThreadPool(const ThreadPool& ) = delete;
    ThreadPool& operator=(const ThreadPool& ) = delete;
private:
    bool _stop; // 这个信号在处理异常时可以使用,此项目暂不考虑
    size_t _num; // 线程个数
    pthread_mutex_t _lock;
    pthread_cond_t _cond;
    std::queue<Task> _task_queue; // 任务队列
    static ThreadPool* _tp;
};
ThreadPool* ThreadPool::_tp = nullptr;

        Task类:任务类成员变量有处理任务的方法,只需要根据链接句柄调用注册的成员方法就可以了。

#pragma once

#include <iostream>
#include "Protocol.hpp"
class Task
{
public:
    Task()
    {}
    Task(int sock) :_sock(sock)
    {}
    // 处理
    void ProcessOn()
    {
        _handler(_sock);
    }
private:
    int _sock;
    CallBack _handler; // 回调处理方法
};

(4)、通用工具类:

        Until类:提供按行读取,以及拆分字符串的工具函数。

注:Http的行结尾有可能有三种\r \r\n \n,统一处理成\n。

#pragma once 

#include <iostream>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>

class Util
{
public:
    // 返回值说明 
    // 0:说明链接对端关闭了,服务器端就会读到0
    // -1:读取出错了
    // >0:读取到的字符数
    static int ReadLine(int sock, std::string& out)
    {
        char ch = 'a';
        while(ch != '\n')
        {
            ssize_t recv_size = recv(sock, &ch, 1, 0);
            if(recv_size > 0)
            {
                // Http的行结尾有可能有三种\r \r\n \n
                // 所有情况统一处理成\n
                if(ch == '\r')
                {
                    // 先标志位MSG_PEEK窥探后面还有没有\n
                    // 有的话一起读起来
                    // 其他就做处理成\n
                    recv(sock, &ch, 1, MSG_PEEK);
                    if(ch == '\n')
                    {
                        recv(sock, &ch, 1, 0);
                    }
                    else
                    {
                        ch = '\n';
                    }
                }
                out += ch;
            }
            else if(recv_size == 0)
            {
                return 0;
            }
            else 
            {
                return -1;
            }
        }
        return out.size();
    }
    // 分割字符串
    static bool CutString(const std::string& src, std::string& sub1, std::string& sub2, const std::string& seq)
    {
        auto pos = src.find(seq);
        if(pos != std::string::npos)
        {
            sub1 = src.substr(0, pos);
            sub2 = src.substr(pos + seq.size());
            return true;
        }
        else
            return false;
    }
};

(5)、协议处理细节:

注:Protocol.hpp时这个项目最麻烦头文件,很长,要耐心。

        CallBack类:生成的对象要提供给Task对象像函数一样使用,所以重载();然后提供一个处理请求的成员方法就能给Task对象调用了,CallBack只是调用,真正处理的是下面EndPoin类。        

class CallBack
{
public:
    CallBack()
    {}
    // 仿函数:让使用者像函数使用
    void operator()(int sock)
    {
        HandlerRequest(sock);
    }
    void HandlerRequest(int sock)
    {
        LOG(INFO, "Handler begin.........");
#ifdef DEBUG 
        // 编译时,g++ 带上-D DEBUG可以快速进行调试
        // 用来测试一开始的从网页读取是否没问题
        // 编写处理方法时来回切换确认问题所在
        char buf[4096];
        ssize_t s = recv(sock, buf, sizeof(buf)-1, 0);
        if(s > 0)
            buf[s] = '\0';
        LOG(INFO, "Debug begin.........");
        std::cout << buf << std::endl;
        LOG(INFO, "Debug end.........");
#else
        EndPoint* ep = new EndPoint(sock);
        // 读取请求
        ep->RecvHttpRequest();
        // 读取完成,如果没有stop信号,说明合法请求,就可以进行构建和发送
        if(!ep->IsStop())
        {
            LOG(INFO, "Recv success, begin build and send");
            ep->BuildHttpResponse();
            ep->SendHttpResponse();
        }
        delete ep;
#endif
        LOG(INFO, "Handler end..........");
    }
    ~CallBack()
    {}
};

       http协议数据格式:

        接下来就要读取客户端传过来的数据,填入HttpRequest请求类;所以根据http协议格式中一个请求包括请求行,请求报头以及可能有正文,构建HttpRequest请求类。

// Http请求类
struct HttpRequest
{ 
    HttpRequest(): _content_length(0), _cgi(false)
    {}
    ~HttpRequest()
    {}

    std::string _request_line; // 请求行
    std::vector<std::string> _request_header; // 请求报头
    std::string _blank; // 空行
    std::string _request_body; // 请求正文
    std::string _method; // 请求行的方法
    std::string _url; // 请求行的url
    std::string _version; // 请求行的版本号
    std::unordered_map<std::string, std::string> _header_kv; // 请求报头的键值对
    int _content_length; // 请求的正文长度
    std::string _path; // 访问路径
    std::string _query_string; // 参数
    std::string _suffix; // 访问文件后缀
    bool _cgi; // 是否需要CGI程序处理的标志
};

        然后就需要根据请求构建响应,所以根据http协议格式中一个响应包括响应行,响应报头以及可能有正文,构建HttpResponse响应类。

// Http响应类
struct HttpResponse
{
    HttpResponse(): _blank(LINE_END), _status_code(OK), _fd(-1), _body_size(0)
    {}
    ~HttpResponse()
    {}

    std::string _response_line; // 响应行
    std::vector<std::string> _response_header; // 响应报头
    std::string _blank; //空行
    std::string _response_body; // 响应正文
    int _status_code; // 状态码
    int _fd; // 要返回文件的句柄
    int _body_size; // 返回的静态网页大小
};

        有了这两个类,我们就可以开始进行读取和构建,这里还是强调一下,http作为一个非常成熟的协议,细节太庞大了,而我们的项目目标是打通我们上网的细节,然后很多地方只处理关键点,不处理细节。(EndPoint类)

        首先,读取请求思路:

        构建请求:void BuildHttpResponse()

        解析请求注意:a、HttpResponse类专门设计一个_body_size字段专门存储静态网页的大小,而如果需要返回正文用_response_body字段的size来提高大小即可。

b、stat()函数可以获取文件的各种属性,具体man一下就知道了,静态网页大小字段马上可以填写。

         构建响应

         发送响应:void SendHttpResponse();别忘了空行。

#pragma once 

#include <iostream>
#include <sstream>
#include <string>
#include <vector>
#include <unordered_map>
#include <algorithm>
#include <stdlib.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <sys/sendfile.h>
#include "Log.hpp"
#include "Util.hpp"

#define LINE_END "\r\n"
#define HTTP_VERSION "HTTP/1.0"

#define OK 200
#define BAD_REQUEST 400
#define NOT_FOUND 404
#define SERVER_ERROR 500

#define WEB_ROOT "wwwroot"
#define HOME_PAGE "index.html"
#define PAGE_404 "404.html"
#define PAGE_400 "400.html"
#define PAGE_500 "500.html"

// 后缀对应报头Content-Type字段的value
std::string SuffixToDesc(const std::string& suffix)
{
    // 这个表不齐全,只写了几种常见的
    static std::unordered_map<std::string, std::string> suffixtodesc = {
        {".html", "text/html"},
        {".css", "text/css"},
        {".js", "application/x-javascript"},
        {".jpg", "application/x-jpg"}
    };
    auto it = suffixtodesc.find(suffix);
    if(it != suffixtodesc.end())
    {
        return it->second;
    }
    return "text/html";
}

// 状态码对应描述
std::string StatusCodeToDesc(int code)
{
    std::string desc;
    switch(code)
    {
        case OK:
            desc = "OK";
            break;
        case BAD_REQUEST:
            desc = "BAD_REQUEST";
            break;
        case NOT_FOUND:
            desc = "NOT_FOUND";
            break;
        case SERVER_ERROR:
            desc = "SERVER_ERROR";
            break;
        default:
            desc = "ERROR";
            break;
    }
    return desc;

}
// 端点类:进行读取请求,分析请求,构建响应,IO通信
class EndPoint
{
public:
    EndPoint(int sock):_sock(sock), _stop(false)
    {}
    bool IsStop()
    {
        return _stop;
    }
    ~EndPoint()
    {
        if(_sock >= 0)
        {
            close(_sock);
        }
    }
    void RecvHttpRequest()
    {
        // 只有请求行和报头成功读取才有处理下去的必要
        if(!_RecvHttpRequestLine() && !_RecvHttpRequestHeader())
        {
            _ParseHttpRequestLine();
            _ParseHttpRequestHeader();
            _RecvHttpRequestBody();
        }
    }
    void BuildHttpResponse()
    {
        auto& code = _http_response._status_code; 

        auto& method = _http_request._method;
        auto& url = _http_request._url;
        auto& path = _http_request._path;
        auto& query_string = _http_request._query_string;
        auto& suffix = _http_request._suffix;

        size_t suffixpos = 0;
        // 此项目只处理GET和POST方法
        if(method != "GET" && method != "POST")
        {
            code = BAD_REQUEST;
            goto END;
        }
        // 处理url,url分成路径,以及根据方法是否有参数
        if(method == "GET")
        {
            size_t pos = url.find("?"); 
            if(pos != std::string::npos)
            {
                // url有参数,GET方法通过url传参,分割url
                Util::CutString(url, path, query_string, "?");
            }
            else  
            {
                path = url;
            }
        }
        // POST方法不通过url传参,通过正文传参
        else if(method == "POST")
        {
            path = url;
        }
        // 还有完善其他方法,从这里编写
        else  {
            // PUT/HEAD/DELETE.....
        }
        // 加上WEB_ROOT默认路径
        path = WEB_ROOT + path;
        if(path[path.size()-1] == '/')
        {
            // 只有'/'默认返回网站的主页,浏览器也会默认添加
            path += HOME_PAGE;
            // LOG(INFO, path);
        }
        // 判断url访问的文件属性 
        // 目录?可执行程序?....本地有没有这文件?
        struct stat st;  // 文件属性结构体
        if(stat(path.c_str(), &st) == 0) // 返回0说明文件存在
        {
            if(S_ISDIR(st.st_mode)) //判断是否是目录 st.mode是文件的类型和权限等
            {
                path += '/';
                path += HOME_PAGE;
                // 重新获取文件的属性lin
                stat(path.c_str(), &st);
                // LOG(INFO, path);
            }
            else 
            {
                // 查看文件是否具有可执行权限
                if(st.st_mode & S_IXUSR || st.st_mode & S_IXGRP || st.st_mode & S_IXOTH)
                {
                    // 有的话,调用CGI程序处理
                    _http_request._cgi = true;
                }
            }
            _http_response._body_size = st.st_size;//st_size是也就是网页文件的大小
        }
        else 
        {
            // 访问文件不存在,准备返回404网页
            code = NOT_FOUND;
            LOG(WARNING, path + " NOT FOuND");
            goto END;
        }
        // 识别url访问文件的后缀,
        suffixpos = path.rfind('.');
        if(suffixpos == std::string::npos)
            suffix = ".html";
        else 
            suffix = path.substr(suffixpos);
        // 走到这里,路径一定存在,根据是否需要CGI程序处理,各自调用函数处理
        if(_http_request._cgi)
            code = _ProcessCGI();
        else  
        {
            code = _ProcessNonCGI();
            // LOG(INFO, "ProcessNonCGI end");
        }
END:
        // 到这里无论正确与否等等,所有数据已经处理好,构建对应状态码的响应
        _BuildHttpResponseHelper();
        return;
    }
    void SendHttpResponse()
    {
        // 响应行
        send(_sock, _http_response._response_line.c_str(), _http_response._response_line.size(), 0);
        // 响应报头
        LOG(INFO, "send header begin");
        for(auto& e : _http_response._response_header)
        {
            // LOG(INFO, e);
            send(_sock, e.c_str(), e.size(), 0);
        }
        LOG(INFO, "send header end");
        // 响应空行
        send(_sock, _http_response._blank.c_str(), _http_response._blank.size(), 0);
        // 响应正文
        if(_http_request._cgi)
        {
            int total = 0;
            int send_size = 0;
            auto& body_size = _http_response._response_body;
            while(total < body_size.size() && (send_size = send(_sock, body_size.c_str() + total, body_size.size()- total, 0) > 0))
            {
                total += send_size;
            }
        }
        else  
        {
            // 直接用sendfile直接在内核态进行拷贝,效率更高
            sendfile(_sock, _http_response._fd, nullptr, _http_response._body_size);
            close(_http_response._fd);
        }
    }
private:
    // 读取请求行
    bool _RecvHttpRequestLine()
    {
        auto& line = _http_request._request_line;
        int ret = Util::ReadLine(_sock, line);
        if(ret > 0)
        {
            // 把结尾的\n去掉
            line.resize(line.size() - 1);
            LOG(INFO, line);
        }
        // 对端关闭或者读取出错,进入错误处理
        else  
        {
            _stop = true;
        }
        return _stop;
    }
    // 处理请求行
    void _ParseHttpRequestLine()
    {
        auto& line = _http_request._request_line;
        // 使用字符串一次性把请求行分割成三个
        std::stringstream ss (line);
        ss >> _http_request._method >> _http_request._url >> _http_request._version;
        // 方法同个转换成大写
        std::transform(_http_request._method.begin(), _http_request._method.end(), _http_request._method.begin(), toupper);
    }
    // 读取请求报头
    bool _RecvHttpRequestHeader()
    {
        auto& header = _http_request._request_header;
        std::string line;
        // Http协议格式规定报头和正文之间会有一行空行
        while(1)
        {
            line.clear();
            if(Util::ReadLine(_sock, line) > 0)
            {
                if(line == "\n")
                    break;
                else 
                {
                    line.resize(line.size()-1);
                    header.push_back(line);
                    // LOG(INFO, line);
                }
            }
            else  
            {
                _stop = true;
                break;
            }
        }
        return _stop;
    }
    // 处理请求报头
    void _ParseHttpRequestHeader()
    {
        std::string key;
        std::string value;
        // 把请求报头键值对插入哈希表,方便查找
        for(auto& e : _http_request._request_header)
        {
            if(Util::CutString(e, key, value, ": "))
            {
                _http_request._header_kv[key] = value;
            }
        }
    }
    // 判断是否需要读正文
    bool __IsNeedRecvHttpRequestBody()
    {
        // POST方法有正文
        // 报头中有Content-Length字段才读
        if(_http_request._method == "POST")
        {
            auto& header_kv = _http_request._header_kv;
            auto it = header_kv.find("Content-Length");
            if(it != header_kv.end())
            {
                LOG(INFO, it->second);
                _http_request._content_length = std::stoi(it->second);
                return true;
            }
        }
        return false;
    }
    // 读取请求正文
    bool _RecvHttpRequestBody()
    {
        // 先判断有没有正文
        if(__IsNeedRecvHttpRequestBody())
        {
            // 有就按照Cpntent-Length的数值读
            auto& body = _http_request._request_body;
            int len = _http_request._content_length;
            while(len)
            {
                char ch = 'a';
                if(recv(_sock, &ch, 1, 0) > 0)
                {
                    body += ch;
                    len--;
                }
                else 
                {
                    _stop = true;
                    break;
                }
            }
            LOG(INFO, body);
        }
        return _stop;
    }
    // 不需要CGI处理的请求只要把返回静态网页,直接把文件打开,保存fd即可
    int _ProcessNonCGI()
    {
        auto& path = _http_request._path;
        auto& fd = _http_response._fd;
        // LOG(INFO, path);
        fd = open(path.c_str(), O_RDONLY);
        if(fd >= 0)
        {
            LOG(INFO, "open file success");
            return OK;
        }
        else
            return NOT_FOUND;
    }
    // 调用CGI程序,创建子进程程序替换进行,这样就算CGI程序出问题了,也不会影响主程序
    int _ProcessCGI()
    {   
        // LOG(INFO, "CGI begin....");
        int code = OK;
        std::string method_env;
        std::string query_string_env;
        std::string content_length_env;
        auto& method = _http_request._method;
        auto& query_string = _http_request._query_string;
        auto& content_length = _http_request._content_length;
        auto& request_body = _http_request._request_body;

        auto& response_body = _http_response._response_body;
        // 路径就是可执行程序的地址
        auto& bin = _http_request._path;
        // 创建子进程,需要把请求的参数传给子进程,涉及进程间通信,选择匿名管道进行进程间通信
        // 约定:input,output都是对于父进程来说。就是说父进程使用input[0]读,output[1]写,子进程相反
        int input[1];
        int output[1];
        if(pipe(input) < 0)
        {
            LOG(ERROR, "pipe input error");
            code = SERVER_ERROR;
            return code;
        }
        if(pipe(output) < 0)
        {
            LOG(ERROR, "pipe output error");
            code = SERVER_ERROR;
            return code;
        }
        // 创建子进程
        pid_t pid = fork();
        if(pid == 0)
        {
            // 匿名管道半双工,关掉不需要使用的fd
            close(input[0]);
            close(output[1]);
            // 程序替换会替换掉数据和代码,但是PCB和文件描述符表这些是不会的,环境变量存在栈区之上也不会被覆盖
            // 但是替换后子进程拿不到原来的管道的fd是多少,所以重定向为0 1冲所周知的fd
            // 在重定向放到最后替换之前做,不然很容易引起异常现象
            method_env = "METHOD=";
            method_env += method;
            putenv((char*)method_env.c_str());
            if(method == "GET")
            {
                LOG(INFO, query_string);
                query_string_env = "QUERY_STRING=";
                query_string_env += query_string;
                putenv((char*)query_string_env.c_str());
            }
            else if(method == "POST")
            {
                LOG(INFO, std::to_string(content_length));
                content_length_env = "CONTENT_LENGTH=";
                content_length_env += std::to_string(content_length);
                putenv((char*)content_length_env.c_str());
            }
            else {
                // 其他方法的处理
            }
            // 导完环境变量,重定向
            dup2(input[1], 1); // 往原来的标准输入写
            dup2(output[0], 0); // 从原来的标准输入读
            // 程序替换
            execl(bin.c_str(), bin.c_str(), nullptr);
        }
        else if(pid < 0)
        {
            LOG(ERROR, "fork error");
            code = SERVER_ERROR;
            return code;
        }
        else  
        {
            // 父进程
            close(input[1]);
            close(output[0]);
            // POST参数需要写入管道
            if(method == "POST")
            {
                LOG(INFO, request_body);
                int total = 0; // 实时已经写入总共的字节数
                int write_size = 0; // 单次写入的字节数
                // 避免无法一次性将数据写入管道的情况
                while(total < content_length && (write_size = write(output[1], request_body.c_str() + total, request_body.size() - total) > 0))
                {
                    total += write_size;
                }
            }
            // 接收结果,CGI执行完,程序就退出了
            // 所以父进程一定会读到0
            char ch = 'a';
            while(read(input[0], &ch, 1))
            {
                response_body += ch;
            }
            LOG(INFO, response_body);
            // 回收子进程资源
            int status = 0;
            pid_t ret = waitpid(pid, &status, 0);
            if(ret == pid)
            {
                // 先判断程序是否退出
                if(WIFEXITED(status))
                {
                    // 正常的情况下,获取退出码,判断是否执行结果是否正确
                    if(WEXITSTATUS(status) == 0)
                        code = OK;
                    else 
                        code = BAD_REQUEST;
                }
                else  
                    code = SERVER_ERROR;
            }
            // 关闭管道
            close(input[0]);
            close(output[1]);
        }
        return code;
    }

    void _BuildHttpResponseHelper()
    {
        auto& code = _http_response._status_code;
        auto& path = _http_request._path;
        // 构建状态行
        auto& response_line = _http_response._response_line;
        response_line = HTTP_VERSION;
        response_line += " ";
        response_line += std::to_string(code);
        response_line += " ";
        response_line += StatusCodeToDesc(code);
        response_line += LINE_END;
        // 处理路径加上Web根目录
        path = WEB_ROOT + path;
        path += '/';
        switch(code)
        {
        case OK:
            // OK的情况直接返回结果和对应网页就行
            __BulidOkResponse();
            break;
            // 其他异常情况返回对应网页
        case NOT_FOUND:
            path += PAGE_404;
            __HandlerError(path);
            break;
        case BAD_REQUEST:
            path += PAGE_400;
            __HandlerError(path);
            break;
        case SERVER_ERROR:
            path += PAGE_500;
            __HandlerError(path);
            break;
        default:
            break;
        }
    }
    void __BulidOkResponse()
    {
        std::string line = "Content-Type: ";
        line += SuffixToDesc(_http_request._suffix);
        line += LINE_END;
        _http_response._response_header.push_back(line);
        // LOG(INFO, line);
        // 正文长度字段,分为不经过CGI处理的静态网页大小和经过CGI处理的大小
        line = "Content-Length: ";
        if(_http_request._cgi)
            line += std::to_string(_http_response._response_body.size()); // CGI处理的结果存在_http_response._response_body里面
        else  
            line += std::to_string(_http_response._body_size); // _http_response._body_size专门用来存储网页大小
        line += LINE_END;
        _http_response._response_header.push_back(line);

        //LOG(INFO, line);
    }
    void __HandlerError(const std::string& path)
    {
        // 出现错误返回对应的网页
        _http_request._cgi = false;
        // 打开文件,让后面发送的接口使用sendfile函数直接往句柄里_sock里面写
        _http_response._fd = open(path.c_str(), O_RDONLY);
        if(_http_response._fd >= 0)
        {
            // 获取对应静态网页的属性
            struct stat st;
            stat(path.c_str(), &st);
            // 王爷大小
            _http_response._body_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_response._body_size);
            line += LINE_END;
            _http_response._response_header.push_back(line);
        }
    }
private:
    int _sock; // 读写的句柄
    HttpRequest _http_request; // 请求
    HttpResponse _http_response; // 响应
    bool _stop; // 停止标志
};

(6)、CGI机制:

        CGI(Common Gateway Interface):公共网关接口 是WWW技术中最重要的技术之一,有着不可替代的重要地位。CGI是外部应用程序(CGI程序)与WEB服务器之间的接口标准,是在CGI程序和Web服务器之间传递信息的过程。

注意:a、调用哪个CGI程序由前端网页使用表单等控件实现地址填充。

b、http服务器只是给CGI程序提供一个平台,调用CGI程序处理,根据返回结果构建响应返回。

c、总体上讲,CGI程序的的标准输入是浏览器,标准输出也是浏览器,中间所有的通信细节(数据怎么来,怎么给发浏览器都不管)都交给http服务器。

d、CGI能用几乎所有的后端语言编写。

(7)、引入数据库编写CGI程序:

        使用C/C++连接数据,需要使用Mysql库提供的接口,所以先要在官网下载Mysql对应版本的库文件。动态链接或者静态链接都可以,这个项目用的静态链接。

         链接前,先在本地创建好给这个项目使用的账号,数据库,分配好权限;再使用mysql.h的各个文件链接。

CGI程序文件夹下的通用工具类:

这段解码和编码的代码转载自:C++写的UrlEncode和UrlDecode - claireyuancy - 博客园

#pragma once

#include <iostream>
#include <string>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <assert.h>
class Util
{
public:
    // 分割字符串
    static bool CutString(const std::string& src, std::string& sub1, std::string& sub2, const std::string& seq)
    {
        auto pos = src.find(seq);
        if(pos != std::string::npos)
        {
            sub1 = src.substr(0, pos);
            sub2 = src.substr(pos + seq.size());
            return true;
        }
        else
            return false;
    }
    // 获取CGI参数
    static bool GetQueryString(std::string& query_string)
    {
        std::string method = getenv("METHOD");
        // std::cerr << "METHOD=" << method << std::endl;
        bool result = false;
        if(method == "GET")
        {
            query_string = getenv("QUERY_STRING");
            result = true;
        }
        else if(method == "POST")
        {
            query_string.clear();
            int content_length = std::stoi(getenv("CONTENT_LENGTH")); 
            while(content_length > 0)
            {
                char ch = 'a';
                read(0, &ch, 1);
                query_string += ch;
                content_length--;
            }
            result = true;
        }
        else  
            return false;
        // std::cerr << "QUERY_STRING=" << query_string << std::endl;
        return result;
    }
    // 编码
    static unsigned char ToHex(unsigned char x)   
    {   
        return  x > 9 ? x + 55 : x + 48;   
    }
    static std::string UrlEncode(const std::string& str)  
    {  
        std::string strTemp = "";  
        size_t length = str.length();  
        for (size_t i = 0; i < length; i++)  
        {  
            if (isalnum((unsigned char)str[i]) ||   
                (str[i] == '-') ||  
                (str[i] == '_') ||   
                (str[i] == '.') ||   
                (str[i] == '~'))  
                strTemp += str[i];  
            else if (str[i] == ' ')  
                strTemp += "+";  
            else  
            {  
                strTemp += '%';  
                strTemp += ToHex((unsigned char)str[i] >> 4);  
                strTemp += ToHex((unsigned char)str[i] % 16);  
            }  
        }  
        return strTemp;  
    }
    // 解码
    static unsigned char FromHex(unsigned char x)   
    {   
        unsigned char y;  
        if (x >= 'A' && x <= 'Z') y = x - 'A' + 10;  
        else if (x >= 'a' && x <= 'z') y = x - 'a' + 10;  
        else if (x >= '0' && x <= '9') y = x - '0';  
        else assert(0);  
        return y;  
    } 
    static std::string UrlDecode(const std::string& str)  
    {  
        std::string strTemp = "";  
        size_t length = str.length();  
        for (size_t i = 0; i < length; i++)  
        {  
            if (str[i] == '+') strTemp += ' ';  
            else if (str[i] == '%')  
            {  
                assert(i + 2 < length);  
                unsigned char high = FromHex((unsigned char)str[++i]);  
                unsigned char low = FromHex((unsigned char)str[++i]);  
                strTemp += high*16 + low;  
            }  
            else strTemp += str[i];  
        }  
        return strTemp;  
    }
};

mysql_cgi.cpp:注意浏览器把中文参数传过来时是经过编码的,所以需要解码,不解码就会乱码,所以在插入数据库前要把数据解码一下。

#include "Util.hpp"
#include "./include/mysql.h"

const std::string HOST = "127.0.0.1";
const std::string USER = "http";
const std::string PASSWORD = "http123.";
const std::string DB = "HTTP";


// 模拟一下用户使用表单提交注册,到服务器把数据插入到数据库的过程
int main()
{
    // 测试能不能mysql.h能不能链接上而已
    // std::cout << "mysql client version: " << mysql_get_client_info() << std::endl;
    // 获取参数
    std::string query_string; 
    Util::GetQueryString(query_string);
    // 用户名和密码分开
    std::string namekv;
    std::string passwordkv;
    Util::CutString(query_string, namekv, passwordkv, "&");
    // 分成name=... 和password=...
    // 取出用户输入的用户名和密码
    std::string tmp;
    std::string sql_name;
    std::string sql_name_tmp;
    std::string sql_password;
    std::string sql_password_tmp;
    Util::CutString(namekv, tmp, sql_name_tmp, "=");
    Util::CutString(passwordkv, tmp, sql_password_tmp, "=");
    // 确保中文不会乱码,需要进行解码
    sql_name = Util::UrlDecode(sql_name_tmp);
    sql_password = Util::UrlDecode(sql_password_tmp);
    
    // 获取成功,开始链接数据库,插入数据
    //初始化数据库,获得句柄
    MYSQL* my = mysql_init(nullptr);
    if(my != nullptr)
    {
        // 链接数据库
        if(mysql_real_connect(my, HOST.c_str(), USER.c_str(), PASSWORD.c_str(), DB.c_str(), 3306, nullptr, 0) != nullptr)
        {
            // 链接成功,设置编码格式
            mysql_set_character_set(my, "utf8");
            // 开始插入,这边是先在本地创建数据库和user表,所以可以直接插入
            std::string insert_sql = "insert into user (name, password) values (\'";
            insert_sql += sql_name.c_str();
            insert_sql += "\', \'";
            insert_sql += sql_password.c_str();
            insert_sql += "\');";
            if(mysql_query(my, insert_sql.c_str()) == 0)
            {
                std::cout << "<html>";
	            std::cout << "<head><meta charset=\"utf-8\"></head>";
                std::cout << "<body><h1>注册成功!</h1></body>";
            }
        }
        // 关闭连接
        mysql_close(my);
    }
    return 0;
}

(8)、Makefile的编码和脚本:

         Makefile的编写:首先是服务器目录下的Makefile

bin=HttpServer
src=Main.cpp
cc=g++
LD_FLAGS=-std=c++11 -l pthread
curr=$(shell pwd)

.PHONY:all
all:$(bin) CGI

$(bin):$(src)
	$(cc) -o $@ $^ $(LD_FLAGS) 
CGI:
	cd $(curr)/cgi;\
	make;\
	cd -

.PHONY:clean
clean:
	rm -rf $(bin);\
	rm -rf output;\
	cd $(curr)/cgi;\
	make clean;\
	cd -

.PHONY:output
output:
	mkdir -p output;\
	cp -rf $(bin) $(curr)/output;\
	cp -rf wwwroot $(curr)/output;\
	cp -rf $(curr)/cgi/test_cgi $(curr)/output/wwwroot
	cp -rf $(curr)/cgi/mysql_cgi $(curr)/output/wwwroot

CGI程序文件夹下的Makefile:

curr=$(shell pwd)
.PHONY:all 
all:test_cgi mysql_cgi

test_cgi:test_cgi.cpp 
	g++ -o $@ $^ -std=c++11 
mysql_cgi:mysql_cgi.cpp
	g++ -o $@ $^ -std=c++11 -I $(curr)/include -L $(curr)/lib -l mysqlclient -l pthread -l dl -static
.PHONY:clean
clean:
	rm -rf test_cgi mysql_cgi

 为了构建工程方便把make命令都写进build.sh:

#!/bin/bash

make clean;\
make;\
make output

(9)、简单编写表单网页测试:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
    </head>
    <body>

        <form action="/mysql_cgi" method="POST">
            用户名:<input type="text" name="用户名">
            <br>
            <br>
            <br>
            密  码:<input type="text" name="密码">
            <br><br>
            <input type="submit" value="提交注册">
        </form> 

        <p>如果您点击提交,表单数据会提交给后台CGI程序处理</p>

    </body>
</html>

(10)、项目目录结构:

 五、测试结果:

网页访问提交:

 

 再到日志和数据库查看:

 

 六、结束语:

        其实现在有很多开源的http服务器,以后绝对不可能会让你从0开始写,但是这样折腾下来,确实会对上网的技术细节有另外一个层次的理解。祝大家都能学到想学到的,而且都能学懂

  • 8
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
C/C++中可以通过pthread库来实现线程池。下面是一个简单的C++线程池实现的代码示例: ```cpp #include <iostream> #include <pthread.h> #include <queue> using namespace std; class ThreadPool { public: ThreadPool(int size) { this->size = size; threads = new pthread_t[size]; for (int i = 0; i < size; i++) { pthread_create(&threads[i], NULL, worker, this); } } void addTask(void (*task)(void*), void* arg) { pthread_mutex_lock(&mutex); tasks.push(make_pair(task, arg)); pthread_mutex_unlock(&mutex); } void waitAll() { for (int i = 0; i < size; i++) { pthread_join(threads[i], NULL); } } private: static void* worker(void* arg) { ThreadPool* pool = static_cast<ThreadPool*>(arg); while (true) { pthread_mutex_lock(&pool->mutex); if (pool->tasks.empty()) { pthread_mutex_unlock(&pool->mutex); break; } auto task = pool->tasks.front(); pool->tasks.pop(); pthread_mutex_unlock(&pool->mutex); task.first(task.second); } return NULL; } int size; pthread_t* threads; queue<pair<void (*)(void*), void*>> tasks; pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; }; void task(void* arg) { int* num = static_cast<int*>(arg); cout << "Task #" << *num << " is running." << endl; delete num; } int main() { ThreadPool pool(5); for (int i = 0; i < 10; i++) { int* num = new int(i); pool.addTask(task, num); } pool.waitAll(); return 0; } ``` 上面的代码使用了pthread库来创建线程池,并通过addTask方法将任务添加到线程池的任务队列中。执行结果如下: ``` Task #0 is running. Task #1 is running. Task #2 is running. Task #3 is running. Task #4 is running. Task #5 is running. Task #6 is running. Task #7 is running. Task #8 is running. Task #9 is running. ``` 这里的任务只是简单地输出了一条信息,实际上可以根据需要编写自己的任务逻辑,利用线程池提高程序的执行效率和稳定性。
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值