Linux项目:自主web服务器


全文约 31372 字,预计阅读时长: 92分钟


项目描述

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

目标

  对http协议(1.0)的理论学习,从零开始完成web服务器开发,坐拥下三层协议,从技术到应用,让网络难点无处遁形。

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

技术特点

  网络编程(TCP/IP协议, socket流式套接字,http协议);多线程技术;cgi技术;线程池。开发环境:centos 7 + vim/gcc/gdb + C/C++。


Web与 http 发展史

万维网的发明:

  1989 年,当时在 CERN 工作的 Tim Berners-Lee 博士写了一份关于建立一个通过网络传输超文本系统的报告。这个系统起初被命名为 Mesh,在随后的 1990 年项目实施期间被更名为万维网(World Wide Web)。它在现有的 TCP 和 IP 协议基础之上建立,由四个部分组成:

一个用来表示超文本文档的文本格式,超文本标记语言(HTML)。
一个用来交换超文本文档的简单协议,超文本传输协议(HTTP)。
一个显示(以及编辑)超文本文档的客户端,即网络浏览器。第一个网络浏览器被称为 WorldWideWeb。
一个服务器用于提供可访问的文档,即 httpd 的前身。

  WWW也可以理解为访问互联网资源的一套生态:浏览器通过http(s)传输html,来访问服务器上的资源,服务器响应请求返回,浏览器渲染刷新。

Http发展史


DNS

  DNS (Domain Name System) 域名系统,是一个层次化、分散化的 Internet 连接资源命名系统。DNS 维护着一个包含域名与对应资源例如IP 地址的列表。

  DNS 最突出的功能是将易于记忆的域名 (例如 mozilla.org) 翻译成为数字化的 IP 地址 (例如 151.106.5.172)。这一从域名到 IP 地址的映射过程被称为DNS 查询 (DNS lookup),与之对应,**DNS 反向查询 (rDNS)**用来找到与 IP 地址对应的域名。浏览器自动帮助完成查询,也会自动填充端口号。


URI URL URN

  • URN不常用:某种资源的命名。
  • URI:全网内确认唯一性的资源。
  • URL:既能确认资源的唯一性,还能确认资源的位置。
    在这里插入图片描述

HTTP 概述

  HTTP 是一种能够获取如 HTML 这样的网络资源的 protocol(通讯协议)。它是在 Web 上进行数据交换的基础,是一种 client-server 协议,也就是说,请求通常是由像浏览器这样的接受方发起的。一个完整的 Web 文档通常是由不同的子文档拼接而成的,像是文本、布局描述、图片、视频、脚本等等。

  一个连接是由传输层来控制的,这从根本上不属于 HTTP 的范围。HTTP 并不需要其底层的传输层协议是面向连接的,只需要它是可靠的,或不丢失消息的(至少返回错误)。在互联网中,有两个最常用的传输层协议:TCP 是可靠的,而 UDP 不是。因此,HTTP 依赖于面向连接的 TCP 进行消息传递,但连接并不是必须的。

在这里插入图片描述

  在客户端(通常指浏览器)与服务器能够交互(客户端发起请求,服务器返回响应)之前,必须在这两者间建立一个 TCP 链接,打开一个 TCP 连接需要多次往返交换消息(因此耗时)。HTTP/1.0 默认为每一对 HTTP 请求/响应都打开一个单独的 TCP 连接(短连接)。当需要连续发起多个请求时,这种模式比多个请求共享同一个 TCP 链接更低效。短连接:请求,响应,关闭连接。

  为了减轻这些缺陷,HTTP/1.1 引入了流水线(被证明难以实现)和持久连接的概念:底层的 TCP 连接可以通过Connection头部来被部分控制。HTTP/2 则发展得更远,通过在一个连接复用消息的方式来让这个连接始终保持为暖连接。

  当 HTTP 流水线启动时,后续请求都可以不用等待第一个请求的成功响应就被发送。然而 HTTP 流水线已被证明很难在现有的网络中实现,因为现有网络中有很多老旧的软件与现代版本的软件共存。因此,HTTP 流水线已被在有多请求下表现得更稳健的 HTTP/2 的帧所取代。

HTTP特点

  • 简单快速,HTTP服务器的程序规模小,因而通信速度很快。
  • 灵活,HTTP允许传输任意类型的数据对象,正在传输的类型由Content-Type加以标记。
  • 无连接,每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。(http/1.0具有的功能,http/1.1兼容)
  • 无状态:是指协议对于交互性场景没有记忆能力。什么是Http无状态?Session、Cookie、Token三者之间的区别

  可是,随着web的发展,因为无状态而导致业务处理变的棘手起来。比如保持用户的登陆状态。http/1.1虽然也是无状态的协议,但是为了保持状态的功能,引入了cookie技术。绝大多数的Web开发,都是构建在HTTP协议之上的Web应用。

HTTP请求与响应

如果用户的URL没有指明要访问的某种资源路径。虽然浏览器会自动默认添加 ’ / ';但依旧没有告知服务器要访问什么资源,此时默认返回对应服务器的首页!还会将Linux下一个特定的路径作为Web根目录。

  • 典型的 HTTP 会话:客户端首先建立连接,其次发送请求。服务端读取请求,分析请求,构建相应并返回。

项目纲要

  • 套接字的封装:提供一个监听套接字,实现http底层与协议解耦。单例、端口复用。
  • Http服务的启动:单例初始化,不停地获取连接、创建线程去解决客户端发来的请求。
  • 添加日志信息:处理流程中一些信息的提示。
    在这里插入图片描述
  • 读取HTTP请求:读取请求行、请求报头、空行、正文。
  • 响应HTTP请求:返回静态网页;有参数的调用cgi程序进行数据处理。
  • CGI技术: CGI机制、数据处理、支持其他语言等
  • 差错处理:读取出错、写入出错。
  • 线程池的引入:设计任务、生产者消费者模型
  • 网页表单数据的获取
  • HTTP访问数据库
  • 项目总结

项目架构

套接字的封装

  • Sock.hpp
#pragma once

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <pthread.h>
#include <cstdlib>
#include <arpa/inet.h>
#include <cstring>
#include <string>

#include"Log.hpp"

using std::cout;
using std::string;
using std::endl;

static const  int backlog = 5;

//单例,封装tcp套接字通信
class TcpServer
{
private:
    int listen_sock;
    int _port;
    static TcpServer *svr;

private:
    TcpServer(int potr) : _port(potr),listen_sock(-1)
    {
    }
    TcpServer(const TcpServer &) = delete;

public:
    static TcpServer *GetInstanc(int port)
    {
        static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
        if (svr == nullptr)
        {
            pthread_mutex_lock(&lock);
            if (svr == nullptr)
            {
                svr = new TcpServer(port);
                svr->Activate();
            }
            pthread_mutex_unlock(&lock);
        }
        return svr;
    }

    void Activate()//激活
    {
        Establish();
        Bind();
        Monitoring();

        LOG(INFO,"Activate success");
    }
    void Establish()//创建
    {
        listen_sock = socket(AF_INET, SOCK_STREAM, 0);
        if (listen_sock < 0)
        {
            exit(1);
        }
        int opt = 1;
        setsockopt(listen_sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));//端口复用,服务器挂掉可以立即重启。
    }
    void Bind()//绑定
    {
        struct sockaddr_in local;
        bzero(&local, sizeof(local));

        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = INADDR_ANY;
        if (bind(listen_sock,(struct sockaddr*)&local,sizeof(local)) < 0)
        {
            exit(2);
        }
    }
    void Monitoring()//监听
    {
        if(listen(listen_sock,backlog)<0)
        {
            exit(3);
        }
    }
    int SockListen()//供外部使用监听套接字 解耦
    {
        return listen_sock;
    }
};

TcpServer *TcpServer::svr = nullptr;  //单例模式

HTTP服务启动

  • HttpSv.hpp
#pragma once 

#include "Sock.hpp"
#include"Protocol.hpp" //线程处理HTTP请求与构建响应

static const size_t PORT = 8081;
class HttpSever
{
private:
    size_t _port;
    TcpServer *tcp_svr;
    bool stop; //后期可以通过信号更改循环条件,而不是直接终止服务进程。
public:
    HttpSever(int potr = PORT) : _port(potr), tcp_svr(nullptr), stop(false)
    {
    }
    ~HttpSever() {}
    void Initialize() //初始化获取单例。sock.hpp不用暴露在外面。
    {
        tcp_svr = TcpServer::GetInstanc(_port);
    }
    void LoopSever()
    {
        int listen_sock = tcp_svr->SockListen();
        while (!stop)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer); //输出型参数,记录请求方的一些信息
            int sock = accept(listen_sock, (sockaddr *)&peer, &len);
            if (sock < 0)
            {
                continue;
            }
            //需要线程去执行任务,传新获取到的sock的地址,在那里关闭sock
            int* psock = new int(sock);
            pthread_t tid;	
            pthread_create(&tid,nullptr,HandlerPetition::Entry,psock);	//临时方案,后期改造成线程池
            pthread_detach(tid);
        }
    }
};

日志信息与工具类

日志信息

方便项目调试。适当的日志信息,可以准确的定位到程序执到哪一步挂了,从而分析并解决BUG。

  • Log.hpp
#pragma once

#include<iostream>
#include<string>
#include<sys/time.h>

#define INFO 1
#define WARNING 2
#define ERROR 3
#define FATAL 4

#define LOG(level, mesage) Log(#level, mesage, __FILE__, __LINE__)

/*【日志级别】:【日志信息】【错误文件名称】【行数】-->可以根据编译器预定义符号获得
 提示信息:info
 警告:     Warning
 错误:     Error
 致命:     FATAL

 宏参数采用# 转换成字符串
 */

void Log(std::string level,std::string mes,std::string filename,size_t line)
{
    //timeval curr;
   // gettimeofday(&curr,nullptr); //时间戳 "[" << curr.tv_sec <<"]" <<
    std::cout << "[ " << level << " ]" << "[ " << mes << " ]" << "[" << filename << "]" << "[" << line << "]" << std::endl;
}

工具类

   在我们的工具类中,有两个函数在编写协议的时候经常会用到,一个是ReadLine()函数,一个是Cutstring函数()。

  ReadLine()函数的作用是将sock中的数据一行一行的读取上来,当浏览器发送http请求给服务器的时候,请求行是为第一行,请求报头中的每种属性也是按行区分开来的,所以我们会用该函数去读取http请求。

  其中,不同浏览器发送过来的http请求中行分隔符是不太一样的,有的是“\n",有的是"\r\n",有的是“\r",因此,我们可以将所有的行分隔符都处理成”\n".方便我们后续的处理。

#pragma once

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

using std::string;

//小工具类:
class Util
{
public:
    //读取处理换行符
    static int ReadLine(int sock, string &out) // \r\n,\r,\n
    {
        char ch = 'x';
        while (ch != '\n') //每次只读一个字节,读到接近换行符
        {
            ssize_t s = recv(sock, &ch, 1, 0);
            if (s > 0)
            {
                if (ch == '\r')//处理为'\n'
                {
                    recv(sock, &ch, 1, MSG_PEEK);//对文件缓冲区进行数据窥探
                    if (ch == '\n')
                    {
                        recv(sock, &ch, 1, 0);
                    }
                    else
                    {
                        ch = '\n';
                    }
                }
                //普通字符或'\n'
                out.push_back(ch);
            }
            else if (s == 0)
            {
                return 0;
            }
            else
            {
                return -1;
            }
        }
    }
    static bool CutString(string& obj,string& out1,string& out2,const string& sep)
    {
        size_t pos = obj.find(sep);
        if(pos != obj.npos)     //左闭右开[)
        {
            out1 = obj.substr(0,pos);
            out2 = obj.substr(pos+sep.size());
            return true;
        }
        return false;
    }

};

请求、响应类

在这里插入图片描述

  • Protocol.hpp
class HandlerPetition   
{
public:
    static void *Entry(void *_sock) //线程的回调
    {
        int sock = *(int *)_sock;
        delete (int *)_sock;
        
        //cout<<" get a  new  link..."<<sock <<endl;
//先看看你的请求是什么样子的 
/* #ifndef DEBUG
#define DEBUG
        cout<<" bb : ================"<<endl;
        char buffer[4096];
        recv(sock,buffer,sizeof(buffer),0);
        cout<<buffer<<endl;
        cout<<" ee: ================"<<endl;
#endif */

        OppositeEnd *handle1 = new OppositeEnd(sock);
        handle1->HttpRecvRequset();
        // handle1->HttpMakeRepsond();
        //handle1->HttpSendRespond();
        delete handle1;
        return nullptr;
    }
};

请求类与响应类。用于存储HTTP报文的各种信息。另外有什么需要记录的报文数据,可以随时添加。

#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <unistd.h>
#include <vector>
#include <cstdlib>
#include "Tool.hpp"
#include <sstream>
#include <unordered_map>
#include <algorithm>
#include <sys/sendfile.h>
#include"Log.hpp"

using std::cerr;
using std::cout;
using std::endl;
using std::unordered_map;
using std::vector;

#define SEP ": "
#define OK 200
#define BAD_REQUEST 400
#define NOT_FOUND 404
#define SERVER_ERROR 500

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

#define WEB_ROOT "webroot"
#define HOMEPAGE "index.html"
#define PAGE_404 "404.html"

class HttpRequst // 分析请求报文,保存请求报文的各种数据
{
public:
    string req_line;         // 状态行
    vector<string> req_head; // kv结构的请求行
    string blank;            // 空行
    string req_body;         // 请求正文 post方法的参数在正文

    // 存放解析完毕之后的结果
    string method;  // 方法
    string version; // http版本
    string url;     // 请求的资源路径?参数

    unordered_map<string, string> kv_head; // 拆分kv结构的请求报头

    int cont_length;     // 正文大小
    string path;         // 资源路径
    string query_string; // 参数

    bool cgi; // 是否请求的是可执行程序
    int size; // 请求资源文件的html的大小

    string suffix; // 资源后缀,根据资源后缀确定发送时content type 的 描述属性

public:
    HttpRequst() : cont_length(0), cgi(false) {}
    ~HttpRequst() {}
};

class HttpRespond // 记录响应报文的各种属性
{
public:
    string respond_line;         // 响应行
    vector<string> respond_head; // 响应报头
    string blank;                // 响应空行
    string respond_body;         // 响应正文

    int status_code; // 状态码
    int fd;          //  默认首页的文件描述符
public:
    HttpRespond() : blank(LINE_END), status_code(OK), fd(-1) {}
    ~HttpRespond() {}
};

与此同时提供一个类,对如下功能的封装:读取分析报文、构建相应、发送响应报文,关闭套接字。

class OppositeEnd
{
private:
    int sock;
    HttpRespond hp_respond;
    HttpRequst hp_request;
	bool stop;  //后期差错处理
public:
    OppositeEnd(int _sock) : sock(_sock)
    {
    }
    ~OppositeEnd()
    {
        close(sock);
    }

    void HttpRecvRequset()  //请求报文的读取加分析
    {
        ReadRequestLine();        // 读取状态行
        ReadRequestHead();        // 读取请求报头
        HttpAnalyseRequestLine(); // 分析状态行
        AnalyseRequestHead();     // 拆分kv报头
        RecvReqBody();            // 读取正文
    }
    void HttpMakeRepsond()
    {
    }
    void HttpSendRespond()
    {
    }

  测试一下: netstat -nltp:查看服务是否启动。

  对于Telnet的认识,可以把Telnet当成一种通信协议,但是对于入侵者而言,Telnet只是一种远程登录的工具。一旦入侵者与远程主机建立了Telnet连接,入侵者便可以使用目标主机上的软、硬件资源,而入侵者的本地机只相当于一个只有键盘和显示器的终端而已。

  为什么需要telnet?telnet就是查看某个端口是否可访问。我们在搞开发的时候,经常要用的端口就是 8080。那么你可以启动服务器,用telnet 去查看这个端口是否可用。

  Telnet协议是TCP/IP协议家族中的一员,是Internet远程登陆服务的标准协议和主要方式。它为用户提供了在本地计算机上完成远程主机工作的能力。在终端使用者的电脑上使用telnet程序,用它连接到服务器。终端使用者可以在telnet程序中输入命令,这些命令会在服务器上运行,就像直接在服务器的控制台上输入一样。可以在本地就能控制服务器。要开始一个telnet会话,必须输入用户名和密码来登录服务器。Telnet是常用的远程控制Web服务器的方法。

[root@VM-12-7-centos output]# telnet 127.0.0.1 8081
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
^]
telnet> 
get /b/a http/1.0

在这里插入图片描述


读取请求

void HttpRecvRequset() // 请求报文的读取加分析
    {
        ReadRequestLine();        // 读取状态行
        ReadRequestHead();        // 读取请求报头
        HttpAnalyseRequestLine(); // 分析状态行
        AnalyseRequestHead();     // 拆分kv报头
        RecvReqBody();            // 读取正文 
    }

读取请求行

	bool ReadRequestLine() // 读取状态行
    {
        if (Util::ReadLine(sock, hp_request.req_line) > 0)
        {
            hp_request.req_line.resize(hp_request.req_line.size() - 1);
            LOG(INFO, hp_request.req_line);
        }
        else
        {
            stop = true;
        }
        cout << "ReadRequestLine() " << stop << endl;
        return stop;
    }

读取请求报头

bool ReadRequestHead() // 读取报头
    {
        string line;
        while (true)
        {
            line.clear();
            if (Util::ReadLine(sock, line) <= 0)
            {
                stop = true;
                break; // 出错
            }

            if (line == "\n")
            {
                hp_request.blank = line;
                break;
            }
            line.resize(line.size() - 1);
            hp_request.req_head.push_back(line);
        }
        cout << "ReadRequestHead() " << stop << endl;
        return stop;
    }

分析请求行

  我们需要将读取到的请求行拆分成三个部分:请求方法,URI,请求版本,以便我们后续根据请求方法,URI和版本构建响应。

  在请求行中,method,uri和version它们之间都隔着空格,所以可以使用streamstring将请求行拆分成method,uri,version。由于http请求的方法没有规定大小写,于是这里将其进行字符串大小写转换,转换成大写。算法头文件中的transform

void HttpAnalyseRequestLine() // 分析请求行 GET /a/b http/1.0
    {
        auto &line = hp_request.req_line;
        std::stringstream ss(line);
        ss >> hp_request.method >> hp_request.url >> hp_request.version; // 默认按照空格分割字符串
        std::transform(hp_request.method.begin(), hp_request.method.end(), hp_request.method.begin(), ::toupper);
        cout << "AnalyseRequestLine()  method.." << hp_request.method << endl;
        /* LOG(INFO,hp_request.method);
        LOG(INFO,hp_request.url);
        LOG(INFO,hp_request.version);  */
    }

分析请求报头

  请求报头中包含了请求中的各种信息,但是它们都是以 ”属性名:属性信息“ 的形式存储在vector中,例如:“Content-Length: 10",为了方便我们找到请求报头中的各种信息,我们需要将请求报头中的每种属性拆分成属性名-属性信息键对值存放在unordered_map。

  每种报头中每种属性里,属性名和属性信息都是用 “:”分隔开的。因此我们需要根据”:“将属性名和属性信息分隔开来。

	void AnalyseRequestHead() // 拆分kv结构的报头
    {
        string key1;
        string val1;
        for (auto &iter : hp_request.req_head)
        {
            if (Util::CutString(iter, key1, val1, SEP))
            {
                hp_request.kv_head.insert({key1, val1});
            }
        }
    }

读取正文

  当解析请求行和请求报头完后,我们就可以知道method和Content-Length,我们就可以判断请求正文中有内容。如果请求正文中有内容,那么就需要读取多少?

  在GET方法中,请求正文是被设置为空,所以GET方法是不需要读取请求正文的。如果是POST方法,它的请求正文有可能为空,也有可能存在。

  如果POST方法中的请求正文存在,Content-Length是不等于0的,在sock读取多少个字节呢?根据Content-Length判断即可。如果POST方法中的请求正文不存在,那么Content-Length是为0,因此是不需要读取请求正文的。阻塞式的一个字节一个字节的读,防止粘包等问题。

   bool RecvReqBody() // 读取正文
    {
        if (IsGetBody())
        {
            int ct_len = hp_request.cont_length;
            cout << " ct_len : " << ct_len << endl;
            char ch = 0;
            while (ct_len)
            {
                ssize_t s = recv(sock, &ch, 1, 0);0
                if (s > 0)
                {
                    hp_request.req_body.push_back(ch);
                    ct_len--;
                }
                else
                {
                    stop = true;
                    break;
                }
            }
            LOG(INFO,"bool RecvReqBody() "+hp_request.req_body);
        }
        return stop;
    }
    bool IsGetBody() // 确认正文是否需要读取,有则读取多少个字节。
    {
        auto &method = hp_request.method;
        if (method == "POST") // 通过Content-Length 找到它的val值。
        {
            auto &kv_head = hp_request.kv_head;
            auto it = kv_head.find("Content-Length");
            if (it != kv_head.end())
            {
                LOG(INFO, "Post Method, Content-Length: "+it->second);
                hp_request.cont_length = atoi(it->second.c_str());
                return true;
            }
        }
        return false;
    }

在这里插入图片描述


构建响应预处理

   浏览器给服务器发出一个http请求目的是让服务器完成某种任务,可能是想访问服务器上的某种资源(文本文件,视频,音频等等),也可能让服务器处理某些数据等等,服务器完成的结果是需要返回给我们的浏览器,文本文件的内容,视频,音频,或数据处理的结果都需要返回给浏览器,但服务器的处理结果是不能直接返回给浏览器,是需要构建一个http响应返回浏览器的,处理结果就放在http的响应正文中。

http响应的构建需要包含:响应行,响应报头,响应空行,响应正文。

构建响应行:版本 响应状态码 响应状态码描述

构建响应报头:构建响应报头至少需要构建 Content-Type 和Content-Length属性。
Content-Type:服务器返回资源是什么类型
Content-Length:服务器返回的资源的大小。每种属性都以空行作为结尾。

构建响应行:将响应报头和响应正文分隔开。 构建响应正文:存放文本文件的内容,视频,音频或数据的处理结果。

   在我们构建响应之前,我们需要根据浏览器发出的http请求来找到我们服务器上的资源,也就是我们的请求路径path。path在解析URI中就已经处理好了,接下来我们直接用就可以了。

在这里插入图片描述

  我们解析看到的path都是以 / 开头,此时就有一个问题,浏览器访问资源的路径是从服务器上的根目录下开始找的吗?答案是不一定,在哪里找资源取决于我们把所有资源放在哪一个目录下。举个例子:我将我的服务器上所有的资源都放在webroot目录下,那么浏览器想要访问的服务器上的资源,就需要到webroot目录下去寻找资源。

在这里插入图片描述

  可是浏览器发送过来的路径都是以根路径最开始的呀,http进程是怎么到webroot目录下去寻找资源呢?答案是http进程在接收到浏览器的访问路径时,首先会对该路径进行修饰的,例如,我的资源目录是webroot,它与http进程是在同一个目录下,所以我的http进程是可以通过相对路径去访问wwwroot目录,因此在编码的时候,我会让我的http进程在路径的前面加上webroot,例如请求路径是/test_cgi,修饰后的路径就变为webroot/test_cgi,这样我们的http进程就会去webroot目录下查找资源。

  接下来我们就需要判断该路径下的资源是否存在,如果不存在,那么将状态码设置为404,也就是找不到的意思,如果该资源存在,那么我们还需要再一步判断,如果访问的资源是一个文本文件, 我们需要记录该文件的大小,然后将该文本文件去构建一个http响应,如果是一个可执行程序,那么我们将标识cgi为真,对其进行cgi处理(下面会讲,不懂的可以先跳动下面看以下cgi处理),如果访问的资源是一个目录呢?是不是就不用进行处理呢?或者有什么办法可以解决这个问题?

  其中一个解决办法就是在每一个目录下都建立一个index.html文件,这个文件代表的是该目录的首页,如果访问到该目录,并且没指明访问该目录的哪一个资源时,htpt进程就会直接将该目录下的index.html中的内容返回给浏览器。举个例子:如果访问的路径是 /,http进程会将/路径修饰成wwwroot/html,那么http进程就会找到wwwroot下的index.html中并将其文本内容返回给浏览器,如果访问的路径是/dir1, http进程会将/dir路径修饰成wwwroot/dir1/index.html,那么http进程就会找到dir1目录下index.html文件并将其文本内容返回给浏览器。

  除此之外,我们还需要构建响应报头中的Content-Type,所以再拿去已经找到的资源的后缀将其放进suffix中,如果没有后缀,则统一设置为".html",然后根据后缀去构建Content-Type。

代码

void HttpMakeRepsond()
    {
        auto &code = hp_respond.status_code;
        auto &method = hp_request.method;
        size_t sufix = 0;
        struct stat st;
        string path1;
        if (hp_request.method != "POST" && hp_request.method != "GET")
        {
            LOG(WARNING, "method is not right");
            code = NOT_FOUND;
            goto END;
        }
        if (method == "GET") // 有参进行调用cgi,进行数据处理
        {
            size_t pos = hp_request.url.find("?");
            if (pos != string::npos) // 切分 url
            {
                // /tcg?a=10&b=20
                Util::CutString(hp_request.url, hp_request.path, hp_request.query_string, "?");
                hp_request.cgi = true;
            }
            else
            {
                hp_request.path = hp_request.url;
            }
        }
        else if (method == "POST") // post已经把正文读上来了
        {
            hp_request.cgi = true;
            hp_request.path = hp_request.url;
        }
        else
        {
            // do nothing
        }
        // /
        path1 = hp_request.path;
        hp_request.path = WEB_ROOT; // 添加前缀
        hp_request.path += path1;
        //  webroot/
        LOG(INFO, "debug: path  " + hp_request.path);
        // webroot/tcg /
        if (hp_request.path[hp_request.path.size() - 1] == '/')
        {
            hp_request.path += HOMEPAGE;
        }
        // webroot/tcg
        if (stat(hp_request.path.c_str(), &st) == 0) // 资源存在
        {
            if (S_ISDIR(st.st_mode)) // 若是目录,则返回该目录下的默认首页
            {
                hp_request.path += "/";
                hp_request.path += HOMEPAGE;
                stat(hp_request.path.c_str(), &st); // 重新获取目录下首页文件的大小
            }
            if (st.st_mode & S_IXUSR || st.st_mode & S_IXGRP || st.st_mode & S_IXOTH) // 具有可执行权限
            {
                hp_request.cgi = true;
            }
            hp_request.size = st.st_size; // 首页的html文件多大
        }
        else // 资源不存在
        {
            LOG(WARNING, hp_request.path += "Not Found");
            code = NOT_FOUND;
            goto END;
        }
        LOG(INFO, hp_request.req_body);
        // 找后缀
        sufix = hp_request.path.rfind(".");
        if (sufix == string::npos)
        {
            hp_request.suffix += ".html";
        }
        else
        {
            hp_request.suffix = hp_request.path.substr(sufix);
        }

        // 说明资源存在,合法请求
        if (hp_request.cgi)
        {
            code = GoCgi();
            cout << "cgi返回的状态码: " << code << endl;
        }
        else
        {
            code = NoCgi();
        }

    END:
        BuildResponseHelper(); // 根据状态码构建对应的响应
        // 上面出错的地方就设置状态码值,返回404页面,还可以添加一些不同状态码系列的描述页面
    }

stat()函数作用:查看一个文件是否存在,并将文件的属性存放在struct stat变量中。返回值:成功返回0,失败返回-1。

 struct stat {
 2     dev_t st_dev; //device 文件的设备编号
 3     ino_t st_ino; //inode 文件的i-node
 4     mode_t st_mode; //protection 文件的类型和存取的权限
 5     nlink_t st_nlink; //number of hard links 连到该文件的硬连接数目, 刚建立的文件值为1.
 6     uid_t st_uid; //user ID of owner 文件所有者的用户识别码 
 7     gid_t st_gid; //group ID of owner 文件所有者的组识别码 
 8     dev_t st_rdev; //device type 若此文件为装置设备文件, 则为其设备编号 
 9     off_t st_size; //total size, in bytes 文件大小, 以字节计算 
10     unsigned long st_blksize; //blocksize for filesystem I/O 文件系统的I/O 缓冲区大小. 
11     u nsigned long st_blocks; //number of blocks allocated 占用文件区块的个数, 每一区块大小为512 个字节. 
12     time_t st_atime; //time of lastaccess 文件最近一次被存取或被执行的时间, 一般只有在用mknod、 utime、read、write 与tructate 时改变.
13     time_t st_mtime; //time of last modification 文件最后一次被修改的时间, 一般只有在用mknod、 utime 和write 时才会改变
14     time_t st_ctime; //time of last change i-node 最近一次被更改的时间, 此参数会在文件所有者、组、 权限被更改时更新 
15 };

  其中st_size属性是查看文件的大小,以字节为单位,st_mode存储了文件的类型和权限。 S_ISDIR (st_mode) 是一个宏定义,作用判断文件是否为目录。st.st_mode&S_IXGRP、st.st_mode&S_IXOTH、st.st_mode&S_IXUSR,分别判断文件所属组,文件的其他人,文件所属人是否具有可执行权限,如果其中有一个为真,那么该文件具有可执行权限。先前所描述的st_mode 则定义了下列数种情况:

1、S_IFMT 0170000 文件类型的位遮罩 
2、S_IFSOCK 0140000 scoket
3、S_IFLNK 0120000 符号连接 
4、S_IFREG 0100000 一般文件 
5、S_IFBLK 0060000 区块装置 
6、S_IFDIR 0040000 目录 
7、S_IFCHR 0020000 字符装置 
8、S_IFIFO 0010000 先进先出 
9、S_ISUID 04000 文件的 (set user-id on execution)10、S_ISGID 02000 文件的 (set group-id on execution)11、S_ISVTX 01000 文件的sticky 位 
12S_IRUSR (S_IREAD) 00400 文件所有者具可读取权限 
13S_IWUSR (S_IWRITE)00200 文件所有者具可写入权限 
14S_IXUSR (S_IEXEC) 00100 文件所有者具可执行权限 
15、S_IRGRP 00040 用户组具可读取权限 
16、S_IWGRP 00020 用户组具可写入权限 
17、S_IXGRP 00010 用户组具可执行权限 
18、S_IROTH 00004 其他用户具可读取权限 
19、S_IWOTH 00002 其他用户具可写入权限 
20、S_IXOTH 00001 其他用户具可执行权限上述的文件类型在 POSIX 中定义了检查这些类型的宏定义 
21S_ISLNK (st_mode) 判断是否为符号连接 
22S_ISREG (st_mode) 是否为一般文件 
23S_ISDIR (st_mode) 是否为目录 
24S_ISCHR (st_mode) 是否为字符装置文件 
25S_ISBLK (s3e) 是否为先进先出 
26S_ISSOCK (st_mode) 是否为socket 若一目录具有sticky 位 (S_ISVTX), 则表示在此目录下的文件只能 被该文件所有者、此目录所有者或root 来删除或改名. 

返回静态网页

  网页本质是一个超文本文件,也就是我们的前端代码,当返回这些代码给浏览器的时候,浏览器就会解析成一个网页。因此如果浏览器访问的资源是是一个文件,那么http进程就直接将该文本文件直接打开,等到发送的响应的时候直接通过sendfile将文件的内容发送给浏览器即可。

  为什么非cgi处理(返回错误页面,或者返回请求网页)不直接将文件内容存放到响应正文response_body中,而是使用sendfile()函数发送给浏览器呢?

  如果使用read()函数将index.html的文件缓冲区中的内容拷贝到用户去中的response_body,再使用write()函数拷贝给sock缓冲区,数据就会经过三个步骤:内核区->用户区->内核区,相比较sendfile()函数,效率会慢一些,因此我们就使用sendfile()函数将响应正文发送给sock().

在这里插入图片描述

int NoCgi() // 简单的网页返回,返回静态网页,只需要打开即可
    {
        hp_respond.fd = open(hp_request.path.c_str(), O_RDONLY); // 先打开,发送后再完毕
        if (hp_respond.fd > 0)
        {
            LOG(INFO, hp_request.path += "OPEN SUCCESS!");
            return OK;
        }
        return NOT_FOUND;
    }

CGI机制

CGI机制的基本概念

  CGI(Common Gateway Interface) 是WWW技术中最重要的技术之一,有着不可替代的重要地位。CGI是外部应用程序(CGI程序)与WEB服务器之间的接口标准,是在CGI程序和Web服务器之间传递信息的过程。所谓的CGI程序就是部署在服务器上的一个一个的可执行程序,这些可执行程序具有处理数据的功能。

  为了大家能够更好的理解cgi机制,我就举一个具体的情况,如果浏览器使用GET方法或者POST方法中访问的资源是一个可执行程序。

  首先http进程就会通过fork创建一个子进程,其次通过execl将可执行程序替换子进程,再次http进程就把参数通过传递给子进程,接着子进程就会解析该参数,例如”a=100&b=200“字符串解析成a=100,b=200。随后子进程将参数处理的结果返回给http进程,最后http进程在通过网络返回给浏览器。这种http进程去调用CGI处理数据的方式就叫做CGI机制。(GET方法和POST传进来的参数需要与调用的CGI程序能解析的参数互相匹配,例如:如果调用的CGI程序只能解析两个参数,那么GET方法和POST方法传进来的就必须是两个参数)


CGI函数的实现

  http进程本身是不会处理参数的,他的功能只是与浏览器互相进行数据的传输,如果要想让服务器具有处理各种参数的功能,那么就需要在服务器上部署各种各样的参数处理的CGI程序,这样就能够使我们的服务器有多种处理数据的功能。

在这里插入图片描述

进程间通信

  http进程给CGI进程之间需要互相通信,http进程需要传递参数给子进程,而CGI进程需要返回结果给http进程。因为http进程和CGI进程是父子进程,所以在互相通信的时候,创建匿名管道是最方便的,但管道的数据传输是单向的,为了能够使http进程和子进程之间都能互相传递数据,因此需要创建两个匿名管道。
在这里插入图片描述

重定向、进程替换

  对于子进程来说,如果子进程被程序替换后,那么它拿到两个管道的文件描述符的数据也会被替换掉,此时子进程就不知道两个管道的文件描述符,就无法去input管道中读取数据,不能再outpipe中的写入数据。

  因为cin是一直往0号文件描述符中的file文件读入数据,cout一直往1号文件描述符中的file文件写入数据,所以在程序替换之前将子进程inpipe[0]的file*重定向到0号文件符,将子进程的outpipe[1]文件描述符就替换到1号文件描述符中去,所以,子进程据可以通过cin去inpipe管道中读取数据,通过cout往outpipe中写入数据。

  Linux中fork子进程后再exec新程序时文件描述符的问题? - Napoleon的回答 - 知乎默认情况下,由exec()的调用程序所打开的所有文件描述符在exec()的执行过程中会保持打开状态,且在新程序中依然有效。这通常很实用,因为调用程序可能会以特定的描述打开文件,而在新程序中这些文件将自动有效,无需再去了解文件名或是把它们重新打开。

  内核为每个文件描述符提供了执行时关闭标志。如果设置了这一标志,那么在成功执行exec()时,会自动关闭该文件描述符,如果调用exec()失败,文件描述符则会保持打开状态。可以通过系统调用fcntl()来访问执行时关闭标志。fcntl()的F_GETFD操作可以获得文件描述符标志的一份拷贝。获取这些标志之后,可以对FD_CLOEXEC位进行修改,再调用fcntl()的F_SETFD操作使其生效。

  进程替换本质上就是去替换一个进程pcb在内存中的对应的代码和数据:操作系统把老程序的PCB中,程序文件(磁盘中的)代码段的起始地址和数据段地址更换成新程序的…(加载新程序到内存——>更新页表信息——>初始化虚拟地址空间),然后这个进程pcb重新开始执行这个新的程序。

  创建管道对应的文件描述符:

在这里插入图片描述
  dup2重定向后,0号文件描述符标识的是inpipe管道,1号文件描述符标识的outpipe管道。所以子进程cin写入数据,就是往inpipe中写入数据,cou输出数据,就是往outpipe输出数据。

在这里插入图片描述

http进程给子进程传参的时候,GET请求和POST请求传参方式是不同的。

  • 如果是GET方法,传递参数给子进程是通过设置环境变量的方式给子进程,因为URI中参数是有大小限制,一般都不会太长,并且程序替换,只替换进程的代码和数据,不会替换环境变量,因此在子进程被execl之前,提前设置一个PATAMETER的环境变量。
  • 如果是POST方法,传递参数给子进程是通过管道的方式给子进程,因为POST中的请求正文的参数是没有限制的。但是子进程怎么知道要在管道中读取多少个字符呢?此时就需要将通过设置一个环境变量Content-Length来标识参数的大小,让子进程知道需要从管道中读取多少个字符。
  • 可是子进程怎么知道要是GET请求还是POST请求,此时就需要在设置一个环境变量METHOD来标识请求方法。 http进程就从ouput[0]中读取到的结果放进response_body中。

CGI代码的实现:

int GoCgi()
    {
        int code = OK;
        auto &method = hp_request.method;          // 方法
        auto &route = hp_request.path;             // 目标程序-替换
        auto &query_str = hp_request.query_string; //  GET 参数
        auto &req_body = hp_request.req_body;      // POST 参数在正文
        int cont_length = hp_request.cont_length;  // 正文大小
        auto &resp_body = hp_respond.respond_body; // 读取cgi的结果

        string method_env;
        string agr_env; // GET压入环境变量让cgi获取参数
        string len_env; // post压入环境变量正文字节数的大小 管道读,读多少

        int intput[2]; // 父进程 写入
        int output[2]; // 父进程  读取
        if (pipe(intput) < 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) // 子进程
        {
            close(intput[1]); // 子进程 intput[0]读取
            close(output[0]); // outpu[1]   写入

            method_env = "METHOD=";
            method_env += method;
            putenv((char *)method_env.c_str());
            if (method == "GET")
            {
                agr_env = "ARG_STRING=";
                agr_env += query_str;
                cout << "debug.. ARG_STRING: " << agr_env.c_str() << endl;
                putenv((char *)agr_env.c_str());
                LOG(INFO, "Get Method, Add Arg_str Env");
            }
            else if (method == "POST")
            {
                len_env = "CONTENT_LENGTH=";
                len_env += std::to_string(cont_length);
                putenv((char *)len_env.c_str());
                LOG(INFO, "POST Method, Add Content-Length Env");
            }
            else
            {
                /* code */
            }
            // 重定向  当调用dup2(int oldfd,int newfd)之后,若newfd原来已经打开了一个文件,
            // 则先关闭这个文件,然后newfd和oldfd指向了相同的文件;若newfd原来没有打开一个文件,则newfd直接指向和oldfd指向相同的文件。
            cout << "debug: before redirect path  " << route << endl;
            dup2(output[1], 1);
            dup2(intput[0], 0);
            execl(route.c_str(), route.c_str(), NULL);
            LOG(ERROR, "EXECL FAILED...");
            exit(1);
        }
        else if (pid < 0)
        {
            LOG(ERROR, "fork error!");
            code = SERVER_ERROR;
            return code;
        }
        else // 父进程
        {
            close(intput[0]); // 读
            close(output[1]); // 写
            if (method == "POST")
            {
                const char *start = req_body.c_str();
                cerr << "f write to  req_body .." << start << endl;
                int total = 0;
                ssize_t size = 0;
                while (total < cont_length && (size = write(intput[1], start + total, req_body.size() - total)) > 0)
                {
                    total += size; // 防止管道在对方没有读走时,数据覆盖
                }
            }
            char ch = 0;
            while (read(output[0], &ch, 1) > 0)
            {
                resp_body.push_back(ch);
            }
            // cerr << "debug: resp_body.. " << resp_body << endl;

            int status = 0;
            pid_t ret = waitpid(pid, &status, 0);
            if (ret == pid)
            {
                if (WIFEXITED(status))
                {
                    if (WIFSIGNALED(status) == 0)
                    {
                        code = OK;
                    }
                    else
                    {
                        code = BAD_REQUEST;
                    }
                }
                else
                {
                    code = SERVER_ERROR;
                }
            }
            close(intput[1]);
            close(output[0]);
        }
        return code;
    }

子CGI程序

子cgi程序

#include "common.hpp"

int main()
{
    string args;
    GetArgsString(args);
    cerr << "args: " << args << endl;
    string argk1;
    string argk2;
    // a=10&b=20
    CutString(args, argk1, argk2, "&");

    string left1, value1;
    string left2, value2;

    CutString(argk1, left1, value1, "=");
    CutString(argk2, left2, value2, "=");

    // 网页显示 获取到的参数
    cout << left1 << ": " << value1 << endl;
    cout << left2 << ": " << value2 << endl;

    // 服务端看一下参数
    cerr << left1 << ": " << value1 << endl;
    cerr << left2 << ": " << value2 << endl;

    int x = atoi(value1.c_str());
    int y = atoi(value2.c_str());

    // 需要自己按html的样式cout,往管道里写入
    cout << "<html>";
    cout << "<head><meta charset=\"utf-8\"></head>";
    cout << "<body>";
    cout << "<h3>" << x << " + " << y << " = " << x + y << "</h3>";
    cout << "<h3>" << x << " - " << y << " = " << x - y << "</h3>";
    cout << "<h3>" << x << " * " << y << " = " << x * y << "</h3>";
    cout << "<h3>" << x << " / " << y << " = " << x / y << "</h3>";
    cout << "</body>";
    cout << "</html>";
    return 0;
}

公共头文件,其他的cgi程序都要用到的功能。

#pragma once
#include <iostream>
#include <string>
#include <cstdlib>
#include <unistd.h>

using std::cerr;
using std::cout;
using std::endl;
using std::string;

/* getenv()用来取得参数name环境变量的内容。
参数name为环境变量的名称,如果该变量存在则会返回指向该内容的指针。环境变量的格式为name=value。 */

bool GetArgsString(string &arg_string)
{
    bool result = false; // 成功获取参数返回真
    string metohd = getenv("METHOD");
    cerr << "cgi method : .. " << metohd << endl;
    if (metohd == "GET")
    {
        arg_string = getenv("ARG_STRING");
        cerr<<"arg_steing ..."<<arg_string<<endl;
        result = true;
    }
    else if (metohd == "POST")
    {
        char ch = 0;
        auto content_length = atoi(getenv("CONTENT_LENGTH"));
        while (content_length)
        {
            read(0, &ch, 1);
            arg_string.push_back(ch);
            content_length--;
        }
        result = true;
    }
    else
    {
        result = false;
    }
    return result;
}

void CutString(string &dest, string &str1, string &str2, const string &sep)
{
    auto pos = dest.find(sep);
    if (pos != string::npos)
    {
        str1 = dest.substr(0, pos);
        str2 = dest.substr(pos + sep.size());
    }
}

CGI总结

在这里插入图片描述

  在我们编写cgi子程序的时候,我们可以忽略浏览器和子CGI之间的通信细节,我们可以认为子cgi中的cgi是读取浏览器中的参数,cout是往浏览器中输出数据,这样就大大减低了理解cgi子程序的 成本。

在这里插入图片描述


构建响应

  当我们处理完cgi函数后和非cgi函数后,我们就已经确定状态码,可以根据状态码统一构建响应的前三行。除了成功的状态码外,其他状态码都会返回一个错误页面。
在这里插入图片描述
  在响应行中,版本,状态码和状态码描述两两之间是以空格分隔开的,最后响应行以\r\n结尾。另外根据响应报文,将状态码转换为状态码描述,如200转换为“OK"。 根据之前解析出来的资源后缀,我们就可以通过后缀判断出返回的资源是一个什么样的类型,接下来就可以构建出报头中的Content-Type,最后在以”\r\n"结尾

状态码描述、后缀网页类型

//状态码转描述
static string CodeDesc(int code)
{
    string desc;
    switch (code)
    {
    case 200:
        desc = "OK";
        break;
    case 404:
        desc = "Not Found";
        break;
    case 400:
        desc = "BAD REQUEST";
        break;
    case 500:
        desc = "Server Error";
    default:
        break;
    }
    return desc;
}

//根据请求后缀确定响应报文Content-Type的属性
static string SuffixDesc(const string &suffix) // 只会被初始化一次
{
    unordered_map<string, string> _sd = {
        {".html", "text/html"},
        {".css", "text/css"},
        {".js", "application/javascript"},
        {".jpg", "application/x-jpg"},
        {".xml", "application/xml"}};
    auto itr = _sd.find(suffix);
    if (itr != _sd.end())
    {
        return itr->second;
    }
    return "text/html";
}

响应报文助手

void BuildResponseHelper() // 响应行的封装 调用不同的响应报头的封装。
    {
        auto &code = hp_respond.status_code;
        cout << "构建响应时的状态码:" << code << endl;
        // 响应行
        auto &line = hp_respond.respond_line;
        line += HTTP_VERSION;
        line += " ";
        line += std::to_string(hp_respond.status_code);
        line += " ";
        line += CodeDesc(hp_respond.status_code);
        line += LINE_END;

        string path = WEB_ROOT;
        path += "/";
        path += PAGE_404;
        switch (code)
        {
        case OK:
            LOG(INFO, "OK page Return!!..");
            MakeOkRsponse();
            break;
        case NOT_FOUND:
            MakeErrorResponse(path); // 出错就返回web根目录下的404页面。
            break;
        case SERVER_ERROR:
            MakeErrorResponse(path);
            break;
        case BAD_REQUEST:
            MakeErrorResponse(path);
            break;
        default:
            break;
            // 1xx 2xx 3xx 4xx 5xx系列的状态码都可以设置一个响应页面
        }
    }

OK响应报文构建

void MakeOkRsponse()
    {
        string phead = "Content-Type: ";
        phead += SuffixDesc(hp_request.suffix);
        phead += LINE_END;
        hp_respond.respond_head.push_back(phead);

        phead = "Content-Length: ";
        if (hp_request.cgi)
        {
            phead += std::to_string(hp_respond.respond_body.size()); // cgi结果读到了body中。
        }
        else
        {
            phead += std::to_string(hp_request.size);
        }
        phead += LINE_END;
        hp_respond.respond_head.push_back(phead);
    }

错误响应报文构建

 void MakeErrorResponse(string path)
    {
        hp_request.cgi = false; // CGI程序出错 退出码404后续发送就不读取body了
        // 返回404
        cout << "404 url-> .. " << path.c_str() << endl;
        hp_respond.fd = open(path.c_str(), O_RDONLY);
        if (hp_respond.fd > 0) //"Content-Type: text/html" "Content-Length: ";
        {
            struct stat st;
            stat(path.c_str(), &st);
            hp_request.size = st.st_size;

            string line = "Content-Type: text/html";
            line += LINE_END;
            hp_respond.respond_head.push_back(line);

            line = "Content-Length: ";
            line += std::to_string(hp_request.size);
            line += LINE_END;
            hp_respond.respond_head.push_back(line);
        }
    }

发送响应报文

  构建完响应后,我们就需要响应中的响应行,响应报头,响应空行,和响应正文依次发送给浏览器。在发送响应正文的时候,如果是cgi处理,cgi的处理结果已经放进响应正文中,所以直接将其发送给sock即可。

  如果是非cgi处理(返回错误页面,返回请求网页)。之前未构建响应正文,但我们已经将需要返回的网页已经打开。所以在发送响应行,响应报头,响应空行后,我们最后再将文件中的内容发送出去。

void HttpSendRespond() // 状态行填充了,响应报头也有了, 空行也有了,正文有了
    {
        send(sock, hp_respond.respond_line.c_str(), hp_respond.respond_line.size(), 0); // 发送响应行
        for (auto it : hp_respond.respond_head)
        {
            send(sock, it.c_str(), it.size(), 0); // 响应报头
        }
        send(sock, hp_respond.blank.c_str(), hp_respond.blank.size(), 0); // 空行
        if (hp_request.cgi)
        {
            LOG(INFO, "HttpSendRespond cgi....");
            auto &response_body = hp_respond.respond_body;
            ssize_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
        {
            /* cout << "....." << hp_respond.fd << endl;
            cout << "...." << hp_request.size << endl; */
            sendfile(sock, hp_respond.fd, nullptr, hp_request.size); // 静态网页
            close(hp_respond.fd);
        }
    }

错误处理

  处理HTTP的时候,有两种类型的错误:

  一、逻辑错误 - 读取完毕 - 要给对方回应。
  二、读取错误 - 读取是否完毕,不一定 - 不给对方回应,退出即可。

   如果是服务器处理的逻辑错误,例如创建子进程失败,http请求路径错误等,那么我们直接返回一个错误页面即可。如果是服务器在读http请求的时候,服务器读到一半请求,浏览器就将连接关掉。无意义的写入,浪费系统资源,写进程会被OS终止掉。HTTP进程收到一个SIGPIPE的信号,因此在初始化服务器的时候需要忽略该SIGPIPE信号。

 static void *Entry(void *_sock) //线程的回调
    {
        int sock = *(int *)_sock;
        delete (int *)_sock; 
        LOG(INFO, "....CallBack()..begin...");
        OppositeEnd *handle1 = new OppositeEnd(sock);
        handle1->HttpRecvRequset();
        if (!handle1->IsStop())
        {
            LOG(INFO, "RecvRequset No Error");
            handle1->HttpMakeRepsond();
            handle1->HttpSendRespond();
        }
        else
        {
            LOG(ERROR, "RecvRequset  Error !");
        }
        delete handle1;
        LOG(INFO, "...CallBack()...end...");
    }

 void Initialize() // 初始化获取单例。sock.hpp不用暴露在外面。
    {
        signal(SIGPIPE, SIG_IGN);
        tcp_svr = TcpServer::GetInstanc(_port);
    }

MakeFile

HTTP所在目录

bin=ht
cc=g++
LD_FLAGS= -std=c++11 -lpthread
curr = $(shell pwd)
src=main.cc

.PHONY:ALL
ALL:$(bin) CGI

$(bin):$(src)
	$(cc) -o $@ $^ $(LD_FLAGS)

CGI:
	cd $(curr)/cgi;\
	make;\
	cd -;

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

.PHONY:output	#新建项目发布的目录
output:
	mkdir -p output
	cp $(bin) output
	cp -rf webroot output
	cp ./cgi/tcg output/webroot

CGI目录下

.PHONY:ALL
ALL:tcg

tcg:test_cgi.cc
	g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f tcg

自动化编译脚本 xxx.sh ;运行:./xxx.sh

#!/bin/bash

make clean
make
make output

线程池改造

  如果来了海量的请求,那么需要海量线程,这样不但服务器效率降低,甚至导致http进程挂掉。解决方法就是:硬件不行加硬件,负载均衡等;软件不行加软件,epoll模式等。线程池是一个在硬件和软件中间做平衡的适中方案。


设计任务

Task.hpp:任务类

把此前单线程的回调函数所在的class HandlerPetition 改造为回调类,增加一个仿函数。首先当线程池中的线程从任务队列中取到一个任务后,其次通过任务类中的接口,接着调用回调类对象的仿函数,与上面写的那一堆进行无缝衔接。

#pragma once
#include"Protocol.hpp"

class TaskQue
{
    private:
    int _sock;
    CallBack _handle; //回调对象
    public:
    TaskQue():_sock(-1)
    {}
    TaskQue(int sock):_sock(sock)
    {
    }
    void Multitasking() //任务处理
    {
        _handle(_sock);
    }
};

CallBack回调类

class CallBack 
{
public:
    CallBack(){}
    void operator()(int sock)
    {
        HandlerRequest(sock);
    }
    void HandlerRequest(int sock)
    {
        LOG(INFO, "....CallBack()..begin...");
        OppositeEnd *handle1 = new OppositeEnd(sock);
        handle1->HttpRecvRequset();
        if (!handle1->IsStop())
        {
            LOG(INFO, "RecvRequset No Error");
            handle1->HttpMakeRepsond();
            handle1->HttpSendRespond();
        }
        else
        {
            LOG(INFO, "RecvRequset  Error !");
        }
        delete handle1;
        LOG(INFO, "...CallBack()...end...");
};

HTTP服务器的循环获取新连接

void LoopSever()
    {
        int listen_sock = tcp_svr->SockListen();
        while (!stop)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer); // 输出型参数,记录请求方的一些信息
            int sock = accept(listen_sock, (sockaddr *)&peer, &len);
            if (sock < 0)
            {
                continue;
            }
            // 需要线程去执行任务,传新获取到的sock的地址,在那里关闭sock
           /*  int *psock = new int(sock);
            pthread_t tid;
            pthread_create(&tid, nullptr, HandlerPetition::Entry, psock);
            pthread_detach(tid); */
            LOG(INFO, "Get a new link");
            TaskQue t(sock);
            ThreadPool::GetSingleIns()->PushTask(t);
        }
    }

线程池

  线程池是一个生产消费者模型,调用方向任务队列中放任务,创建的线程从任务队列中去任务。任务队列就是一个临界资源,消费者与消费者之间是互斥关系,生产者与消费者(同步与互斥的关系)。消费者来消费,等待生产者生产以后,才能消费;生产者,在没有消费者的同时,也在等待。一般会与懒汉模式相结合,形成一个单例模式的线程池。和套接字通信一样,是固定的套路

#include "Task.hpp"
#include <queue>

#define NUM 5
class ThreadPool
{
private:
    int _num;
    //bool _stop; //检测线程池是否在运行
    std::queue<TaskQue> _taskque;
    pthread_mutex_t _lock;
    pthread_cond_t _cond;

    ThreadPool(int num = NUM) : _num(num) 
    {
        pthread_mutex_init(&_lock, nullptr);
        pthread_cond_init(&_cond, nullptr);
    }
    ThreadPool(const ThreadPool &) {}

    static ThreadPool *single_instance;

public:
    static ThreadPool *GetSingleIns()
    {
        static pthread_mutex_t _mtx = PTHREAD_MUTEX_INITIALIZER;
        if (single_instance == nullptr)
        {
            pthread_mutex_lock(&_mtx);
            if (single_instance == nullptr)
            {
                single_instance = new ThreadPool();
                single_instance->InitThreadPool();
                LOG(INFO,"ThreadPool *GetSingleIns()..Success");
            }
            pthread_mutex_unlock(&_mtx);
        }
        return single_instance;
    }
    void Lock()
    {
        pthread_mutex_lock(&_lock);
    }
    void Unlock()
    {
        pthread_mutex_unlock(&_lock);
    }
    void CondWait()
    {
        pthread_cond_wait(&_cond, &_lock); // 不满足某种条件时时释放锁 再将cond条件变量加锁 不占用任何CPU周期
    }
    void CondSignal()
    {
        pthread_cond_signal(&_cond); // 该线程又自动获得该mutex。
    }
    bool IsEmpty()
    {
        return _taskque.size() == 0 ? true : false;
    }
    void PopTask(TaskQue &t)
    {
        t = _taskque.front();
        _taskque.pop();
    }
    void PushTask(const TaskQue& t)//生产消费互斥
    {
        Lock();
        _taskque.push(t);
        Unlock();
        CondSignal();
    }
    static void *HnadlerThread(void *tmp)
    {
        pthread_detach(pthread_self());
        ThreadPool *tpol = (ThreadPool *)tmp;
        while (true)    //始终检测任务队列中是否有任务
        {
            TaskQue t;
            tpol->Lock();
            while (tpol->IsEmpty())
            {
                tpol->CondWait();
            }
            // 不为空就pop,释放锁,处理任务
            tpol->PopTask(t);
            tpol->Unlock();
            LOG(INFO,"线程获取任务,前去处理。。");
            t.Multitasking();
        }
    }
    bool InitThreadPool()
    {
        pthread_t pid;
        for (size_t i = 0; i < _num; i++)
        {
            if (pthread_create(&pid, nullptr, HnadlerThread, this) != 0) // 失败返回错误码
            {
                LOG(FATAL, "create thread pool error!");
                return false;
            }
        }
        LOG(INFO, "create thread pool success!");
        return true;
    }
    ~ThreadPool()
    {
        pthread_mutex_destroy(&_lock);
        pthread_cond_destroy(&_cond);
    }
};

ThreadPool *ThreadPool::single_instance = nullptr;

表单测试

  了解前后端是如何结合到一起的。html里提供网页里都有啥:按钮、输入框等;css决定框的大小、按钮什么样式等等;js:按钮的动态效果等。

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

        <form action="/tcg" method="POST">
            用户名:<input type="text" name="Args1">
            <br>
            <br>
            <br>
            密码:  <input type="text" name="Args2">
            <br><br>
            <input type="submit" value="Upload">
        </form> 

        <p>如果您点击提交,表单数据会提交给tcg,进行后台数据处理</p>
        
    </body>
</html>

在这里插入图片描述


使用Mysql第三方库

1、官网下载mysql第三方库,历史版本6.1.11-Linux_通用,c库。

在这里插入图片描述

2、rz传到cgi目录下

在这里插入图片描述

3、 解压

在这里插入图片描述

4、查看解压好的库。开发只需要lib库文件、和include头文件;为了不影响使用,可以将其他文件删除

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5、创建源文件以后,进行使用测试。

#include <iostream>
#include "mysql.h"

using namespace std;

int main()
{
  cout<<"mysql: .."<<mysql_get_client_info()<<endl;
  return 0;
}

6、动态链接

  使用g++编译g++ -o sql mysqlt1.cc -I include/ -L lib/ -lmysqlclient前;需要将动态库所在的目录,添加到LD_LIBRARY_PATH环境变量。
在这里插入图片描述
在这里插入图片描述
接下来试一试静态库,比较方便后期处理。

静态链接

  使用:g++ -o sql mysqlt1.cc -I include/ -L lib/ -lmysqlclient -lpthread -ldl -static,可能依然会很多警告,可以忽略。

  若是新系统,可能会有下面的报错:
在这里插入图片描述
  解决:在这里插入图片描述
  还有安装c++的标准静态库:yum install -y libstdc++-static


总结与参考

  项目的重点在于HTTP服务器后端的处理逻辑,主要完成的是GET和POST请求方法,以及CGI机制的搭建。但依然有很多可扩展的内容,比如:其他请求方法、http/1.1、长连接、epoll模型、请求转发代理功能等等。

参考:

  • 8
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值