IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 网络协议 -> 自主web服务器 -> 正文阅读

[网络协议]自主web服务器

自主web服务器

前言

http背景

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

目标

对http协议的理论学习,从零开始完成web服务器开发,实现核心部分,从理论到实践。通过实践来加深对理论部分的理解,看到理论的现象。

描述

采用C/S模型,编写支持中小型应用的http,并结合mysql,理解常见互联网应用行为,从技术上理解从上网开始到关闭浏览器的所有操作中的技术细节。

技术特点

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

项目背景

开发环境

开发环境centos 7+vim/gcc/gdb+、C/C++

www

www是环球信息网的缩写(也称作“Web”,“WWW”,“W3”,"环球网“等,常简称为Web)。Web分为Web客户端和Web服务器程序。

WWW可以让Web客户端(常用浏览器)访问浏览Web服务器上的页面,WWW是一个由许多互相链接的超文本组成的系统。在这个系统中,每个有用的事物称为一样”资源“,并且用一个全局”统一资源标识符“(URI)表示。这些资源通过超文本传输协议(HTTP)传送给用户,而后者通过点击链接来获得资源。

总结来说:WWW是访问互联网资源的一套生态,将所有的内容称为资源,规定了通过url的方式访问资源,并通过设计http协议将资源从远端拉取到本地传送。

这样的一套标准是W3C理事会创建的。

客户端浏览器发展史

因为浏览器离用户最近,互联网公司开发自家浏览器可以内置自家相关生态,便于互联网公司引流,因此发展史波澜起伏。

  • 1990年11月,世界上第一台Web服务器和Web浏览器诞生
  • 1993年1月,NCSA研发html内联显示图片的浏览器,不久windows和mac版的相关浏览器出现
  • NASA httpd 1.0差不多出现
  • 1994年网景公司浏览器出现
  • 1995年微软发布IE1.0和2.0
  • 2000年网景衰弱
  • 2004年,Mozilla基金发布firebox,第二次浏览器大战开始
  • 随后IE发布6,7,8,9,10版本,同步Chrome,Opera,Safari浏览器开始抢占市场
  • 今天浏览器格局形成

服务器端http发展史

  • 1990年,HTTP/0.9诞生
  • 1996年5月,HTTP/1.0诞生(我们项目实现的标准)
  • 1997月HTTP/1.1诞生,目前最主流版本
  • HTTP/2.0正在定制

Tcp/IP分层概述

TCP/IP通信传输流

image-20220309102331057

image-20220309102854610

DNS域名解析

image-20220309103215903

各协议之间的协作

image-20220309103356474

http特点

  • 简单快速,HTTP服务器的程序规模小,因而通信速度很快。
  • 灵活,HTTP允许传输任意类型的数据对象,正在传输的类型由Content-Type加以标记。
  • 无连接,每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。(http/1.0具有的功能,http/1.1兼容)
  • 无状态。(http协议每当有新的请求产生,就会有对应的新响应产生。协议本身并不会保留你之前的一切请求或者响应,这是为了更快的处理大量的事务,确保协议的可伸缩性)

至于HTTP1.1中引入了Cookie,在网络基础部分中讲解。

URI、URL、URN

  • URI,identifier,统一资源标识符,用来唯一地标识一个资源。(宏观)
  • URL,locator,统一资源定位符,它是一种具体的URI,即URL不光光可以用来标识一个资源,而且还指明了如何locate这个资源,能够找到它。(具体)
  • URN,name,统一资源命名,是通过名字来标识资源。

例如:isbn:0-395-36341-1是URI,一个国际标准书号,可以唯一确定哪本书,但是没法直接找到这本书。而两个图书馆的中url都有这个书,通过两个图书馆的url都可以找到这本书。

如果用户的URL没有指明要访问的某种资源(路径),虽然浏览器默认会添加/,但是依旧没有告知服务器要访问什么资源,此时默认返回对应服务的首页。

浏览器URL基本格式

  • HTTP(超文本传输协议)是基于TCP的连接方式进行网络连接
  • HTTP/1.1版本中给出一种持续连接的机制(长链接)
  • 绝大多数的Web开发,都是构建在HTTP协议之上的Web应用

HTTP URL (URL是一种特殊类型的URI,包含了如何获取指定资源)的格式如下:

http://host:post/abs_path
  • http表示要通过HTTP协议来定位网络资源
  • host表示合法的Internet主机域名或者IP地址,本主机IP:127.0.0.1
  • port指定一个端口号,为空则使用缺省端口80
  • abs_path指定请求资源的URI
  • 如果URL中没有给出abs_path,那么当它作为请求URI时,必须以“/”的形式给出,通常这个工作浏览器自动帮我们完成。

/是不是linux服务器的根目录开始呢?

不一定,通常不会设置成根目录,我们通常由http服务器设置为自己的WEB根目录(就是linux下一个特定的路径)

http请求与响应

示意图

image-20220309110311355

http报文细节

image-20220309110619662

image-20220309110903186

http请求方法
序号方法描述
1GET获取资源,获取被URI标识的资源。
2POST传输实体主体,和get一样很常见,向服务器提交资源让服务器处理,比如提交表单、上传文件等,可能导致建立新的资源或者对原有资源的修改。提交的资源放在请求体中。
3HEAD获取报文首部,和GET类似,但是不返回报文主体部分。用于确认URI的有效性以及资源的日期等。
4PUT传输文件,将指定文件放的URI所标示的路径,类似ftp,但是有安全问题,大部分web都不用
5DELETE与PUT相反,删除URI指定的资源,不安全,一般也不会被使用。
6CONNECT使用隧道协议链接代理。HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器。就是把服务器作为跳板,去访问其他网页然后把数据返回回来,连接成功后,就可以正常的get、post了。
7OPTIONS询问支持方法
8TRACE追踪路径
http状态码
类别原因短语
1xxInformational(信息性状态码)接受的请求正在处理
2xxSuccess(成功状态码)请求正常处理完毕
3xxRedirection(重定向)需要进行附加操作以完成请求
4xxClient error(客户端错误)客户端请求出错,服务器无法处理请求
5xxServer Error(服务器错误)服务器处理请求出错

常用的14个状态:

2XX系列含义:正确相关
200-OK表示从客户端发来的请求在服务器端被正常处理了
204-No Content表明请求结果被正确处理了,但是响应信息中没有响应正文
206-Partial Content该状态码表示客户端对服务器进行了范围请求,而且服务器成功的执行了这部分GET请求,响应报文中包含由Content-Range指定的实体内容范围
3XX系列含义:重定向相关
301-Moved Permanently永久性重定向该状态码表示请求的资源已经被分配了新的URI,以后应使用新的URI,也就是说,如果之前将老的URI保存为书签了,后面应该按照响应的Location首部字段重新保存书签
302-Found临时性重定向目标资源被分配了新的URI,希望用户本次使用新的URI进行访问
307-Temporary Redirect临时重定向有空看看三者区别:https://www.cnblogs.com/cswuyg/p/3871976.html

301和302的区别就是之前笔记中餐厅搬家和因为修路临时搬家的例子。

400系列含义:表明客户端发生错误的原因
400-Bad Request客户端错误该状态码表明请求报文中存在语法错误,需修改请求内容重新发送,
403-Forbidden该状态码表明浏览器所请求的资源被服务器拒绝了。服务器没有必要给出详细理由,如果想要说明,可以在响应实体内部进行说明。
404-Not Found服务器上没有对应请求的资源
500系列含义:服务器错误
500-Internal Server Error表明服务器端在执行的时候发生了错误,可能是Web本身存在的bug或者临时故障。
503-Server Unavailable状态码表明服务器目前处于超负载或正在进行停机维护状态,目前无法请求处理。这种情况下,最好写入Retry After首部字段在返回给客户端

项目代码

.h和.hpp

一般来说,.h里面只有声明,没有实现;而.hpp里声明实现都有,后者可以减少.cpp的数量。

所以,该类的调用者只需要include这个hpp文件即可,无需再将cpp文件加到project中进行编译。而实现代码将直接编译到调用者的obj文件中,不再生成单独的obj文件,采用hpp文件将大幅度减少调用project中的cpp文件数与编译次数,也不用再发布烦人的lib与dll文件,所以非常适合用来编写公开的开源库。

传输层及网络层封装

TcpServer.hpp封装

  • 将TcpServer实现成饿汉的单例模式。
  • 关于锁和多线程,可以使用C++的库,但是为了达到复习pthread库接口和回顾底层的效果,这里采用pthread原生库。
  • 关于锁的初始化问题,锁初始化可以使用pthread_mutex_init,如果是全局锁/静态锁可以使用PTHREAD_MUTEX_INITIALIZER初始化并且不用destroy。
#pragma once
#include"Log.hpp"
#include<iostream>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/ip.h>
#include<pthread.h>
#include<unistd.h>
#include<cstring>
#include<string.h>

#define BACKLOG 5


class TcpServer {
  private:
    int _listenSock; 
    int _port;
    static TcpServer* _tcpServerPtr;
    TcpServer(int port) : _listenSock(-1), _port(port) {};
    TcpServer(const TcpServer& t) = delete;
    TcpServer& operator=(const TcpServer& t) = delete;
  public:
    static TcpServer* Getinstance(int port) {
      if (nullptr ==  _tcpServerPtr) {
         static pthread_mutex_t _mtx = PTHREAD_MUTEX_INITIALIZER; /*全局锁/静态锁使用宏初始化可以不用手动销毁*/
         pthread_mutex_lock(&_mtx);
         if (nullptr == _tcpServerPtr) {
            _tcpServerPtr = new TcpServer(port); 
            LOG(INFO, "new TcpServer success");
            _tcpServerPtr->InitTcpServer(); 
         }
         pthread_mutex_unlock(&_mtx);
      } 
      return _tcpServerPtr;
    }
  private:
    void InitTcpServer() {
         Sock();
         Bind();
         Listen();
         LOG(INFO, "InitTcpServer success");
    }
    void Sock() {
        _listenSock = socket(AF_INET, SOCK_STREAM, 0); 
        if (_listenSock < 0) {
            LOG(FATAL, "listenSock create error");
            exit(1);
        }
        int opt = 1;
        setsockopt(_listenSock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
        LOG(INFO, "listenSock success && setsockopt");
    }
    void Bind() {
        sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = INADDR_ANY;
        socklen_t len = sizeof(local); 

        if (bind(_listenSock, (struct sockaddr*)&local, len ) < 0) {
            LOG(FATAL, "bind error");
            exit(2);
        }
        LOG(INFO, "bind success");
    }
    void Listen() {
         int res = listen(_listenSock, BACKLOG); 
         if (res == -1) {
             LOG(FATAL, "listen error");
             exit(3);
         } 
         LOG(INFO, "listen success");
    }
  public:
    int GetListenSock() {
        return _listenSock;
    }
    ~TcpServer() {
      if (_listenSock >= 0) close(_listenSock);
    }
};
TcpServer* TcpServer::_tcpServerPtr = nullptr;

HttpServer.hpp封装

HttpServer的成员变量结构

#pragma once

#include"TcpServer.hpp"
#include"Protocol.hpp"
#include"PthreadPool.h"
#include"Task.hpp"
#include<signal.h>

class HttpServer {
  private:
    int _port;
    bool _stop;
  public:
    HttpServer(int port) :_port(port), _stop(false) {

    }
    void InitHttpServer() {
         signal(SIGPIPE, SIG_IGN); /*捕捉信号,防止读端关闭导致服务器写回时崩溃*/
         LOG(INFO, "InitHttpServer success");
    }
    void Loop() {
       /*获得线程池单例进行处理*/
        sockaddr_in endPoint;
        InitHttpServer();
        LOG(INFO, "Loop begin...");
        while (!_stop) {
            socklen_t len= sizeof(endPoint);
            int sock = accept( TcpServer::Getinstance(_port)-> GetListenSock(), (struct sockaddr*)&endPoint, &len);   
            if (sock < 0) {
                LOG(ERROR, "accept failure");
                continue;
            }
            LOG(INFO, "accept new link success..."); 
            Task t(sock); 
            PthreadPool::GetInstance() -> Push(t);
        }
    }
    ~HttpServer() {

    }
};

Log.hpp

用于将错误信息输出到日志中方便定位。重点以简单为主。

image-20220303200638488

直接函数调用传参麻烦,配合宏来使用函数。注意这里可以cerr进行打印,因为后面cgi机制进行了重定向cout会输入到管道或者可以将输出调试信息放在重定向之前

#pragma once
#include<iostream>
#include<string>
#include<ctime>

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

#define LOG(LEVEL, INFOMATION) (log(#LEVEL, INFOMATION, __FILE__, __LINE__))

void log(std::string level, std::string Information, std::string filename, int lines) {
     std::cerr<<"["<<level<<"]["<<Information<<"]["<<time(nullptr)<<"]["<<filename<<"]["<<lines<<"]"<<std::endl;     
}

处理HTTP协议——Protocol.hpp

Util.hpp工具类Util

Util.hpp:处理整个编码中需要大小写转换,字符判断、字符串转换等方法

实现读取请求行
面临粘包问题及解决方法

根据网络编程学习的知识,tcp是面向字节流的,并不能区分分离出整个报文,应用层在读取接收缓冲区的时候一次读出的字节数也是不固定的,需要使用应用层的协议对每个报文进行分离,解决粘包问题。

读取的基本单位,按照行来读取,读到空行先读取完请求报头,在请求包头中获得请求正文的长度。

Tips:不能用C、C++接口来读取,因为不同接口判断行的方式不同,可能是\r\n或者\n或者\r。也就是说作为设计者要兼容各种行分隔符,设计读取行的接口。

image-20220303103212762

如何处理读行的工作?

统一转化成’\n’结尾。

image-20220303111210215

并且使用recv的缓冲区数据窥探功能。

image-20220303110819839

image-20220303105731239

#pragma once
//工具类
class Util{
  	public:
    	static int ReadLine(int sock, std::string& out) {
            char op = 'x'; 
            while (op != '\n') {
                size_t s = recv(sock, (void*)&op, 1, 0);
                if (s > 0) {
                    if (op == '\r') {
                        //往后窥探数据 
                        recv(sock, (void*)&op, 1 ,MSG_PEEK);
                        if (op == '\n') { /*当前行已经结束了*/
                            recv(sock, (void*)&op, 1, 0);/*读取过来*/
                        }
                        else { /*普通数据,说明以'\r'为结尾*/
                            op = '\n';
                        }
                    }
                    //1.普通字符
                    //2.'\n'
                    out.push_back(op);
                }
                else if (s == 0) {//写端关闭
                    close(sock); 
                    return 0;
                }
                else {
                    return -1;
                }
            }
            return static_cast<int>(out.size());
        }
};
将请求报文切分成kv模型
class Util{
  public:
     static inline bool CutStrToPart(const std::string& target, std::string& out1, std::string& out2, const std::string& seg) {
          auto pos = target.find(seg);
          out1 = target.substr(0, pos);
          out2 = target.substr(pos + seg.size());/*默认到结尾停止*/
          return true;
    }
};
将Methond进行大写转化

为了防止发来的请求不遵守协议,统一对Get和Post进行大写转化。使用Cpp的tranform接口。

class Util{
  public:
    static inline void Tranform(std::string& str) {
        std::transform(str.begin(), str.end(), str.begin(), toupper);
    }
};
状态码和状态描述——Code2Desc方法
static std::string Code2Desc(int code) {
    static inline std::string Code2Desc(const int& code) {
          std::string Desc;
          switch (code) {
              case 404: 
                   Desc = "NOT_FOUND";
                   break;
              case 400:
                   Desc = "BAD_REQUEST";
                   break;
              case 500:
                   Desc = "Internal_Server_Error";
                   break;
              default:
                   Desc = "OK";
                   break;
          }
          return Desc;
    }
}

Http请求——HttpRequest类

image-20220303213108653

class HttpResponse {
    public:
       std::string _status_line;  
       std::vector<std::string> _response_header;
       std::string _blank;/*默认给空行*/
       std::string _response_body;
       /*用来记录一些要发送的信息,方便维护*/
       int _status_code;/*标记返回的状态码,方便构造状态信息和解耦*/
       int _size;/*记录返回文件的大小*/
       int _fd;/*用来保存打开的文件描述符,sendfile减少拷贝*/
    public:
       HttpResponse() :_blank(LINEEND), _status_code(OK) {};
       ~HttpResponse() {}
};

Http响应——HttpResponse类

image-20220303185309370

class HttpResponse {
    public:
       std::string _status_line;  
       std::vector<std::string> _response_header;
       std::string _blank;
       std::string _response_body;
       int _status_code;
       int _fd;
       int _size
    public:   
       HttpResponse(): _blank(LINE_END), _status_code(OK), _fd(-1), _size(0){}
       ~HttpResponse(){}
};

读取及分离请求,构建响应,IO通信的业务逻辑——EndPoint类

读取及分离请求
  1. 使用Util工具类读取请求行
  2. 循环读取请求报头行直到空行读取请求报头

按行读取请求之后对于每一行要进行分离出来,但是分析出来的数据并不是立刻使用,因此需要将分析出来的数据保存在HttpRequest中。

  1. 分析请求行,对于将一个字符串划分成三个串可以采用find+substr的方式,但是在这里小型的不划算,我们这里采用CppIO流中的stringstream字符串序列化来处理。
  2. 分析请求报头,可以发现请求报文中都是k-v模型,所以可以每一行都打散存入map中,常数考虑这里使用哈希表header_kv。切分的过程直接在工具类中编写CutString的方法(find和substr)切割成key和value。
  3. 判断是否读取请求正文(认为GET没有正文部分,POST有正文部分,有则可以通过k-v哈希表查找Content-Length来读取)

image-20220304104803645

      bool RecvHttpRequestLine() {
           std::string& out = _http_request._request_lines;
           if (Util::ReadLine(_sock, out) <= 0) {
              return false;
           }
           return true;
      }
      void ParseHttpRequestLine() {
           std::stringstream ss(_http_request._request_lines);/*默认按空行分割*/
           ss >> _http_request._method >> _http_request._url >> _http_request._version;
           Util::Tranform(_http_request._method);
           LOG(INFO, _http_request._method);
           LOG(INFO, _http_request._url);
           LOG(INFO, _http_request._version);
      }
      int RecvHttpRequestHeader() {
           std::string out;
           while (true) {
               out.clear();
               size_t s = Util::ReadLine(_sock, out);  
               if (s == 1) { /*只有'\n',说明读到了空行读完了*/
                   break; 
               }
               if (s <= 0) {
                   return false;
               }
               out.resize(static_cast<int>(out.size()) - 1);
               _http_request._request_header.push_back(out);
               LOG(INFO ,out);
           }              
           return true;
      }
      void ParseHttpRequestHeader() {
        for(auto& str: _http_request._request_header) {
             std::string key, value;
             Util::CutStrToPart(str, key, value, SEG);
             _http_request._kvMap.insert({key,value});
             LOG(INFO, key);
             LOG(INFO, value);
        }
      }
      bool IsHasHttpRequestBody() {
           auto& str = _http_request._method;
           if (str == "POST") {
               return true; 
           }
           return false;
      }
      void RecvHttpRequestBody() {
        if (IsHasHttpRequestBody()) {
            auto iter = _http_request._kvMap.find("Content-Length");
            if (iter == _http_request._kvMap.end()) {
                LOG(ERROR, "POST NOT FOUND Content-Length");
            }
            else {
                _http_request._Content_Length = atoi((iter -> second.c_str()));
                LOG(INFO, "Content-Length get success");
                int contentLength = _http_request._Content_Length;

                char ch = 0;
                while (contentLength) {
                     size_t s = recv(_sock, &ch, 1, 0);
                     if (s == 0) {
                         LOG(ERROR, "contentLength"); 
                         break;
                     }
                     _http_request._request_body.push_back(ch);
                     contentLength--;
                }
            }
        }  
      }
分析及构建响应

在构建响应之前要先分析请求报文的正确性。

image-20220304164559869

  1. 判断请求方法正确性
  2. 判断GET方法是否通过uri传参,若传参则直接下载,否则是上传。
  3. 分割uri成path和query_string
    1. 这里的路径表明了服务器上的某种资源,是从根目录开始的吗?不一定,需要指明web根目录。通过string的加减就可以变换。特别的,如果访问\,直接返回web目录的index.html。
    2. 这个路径对应的资源如何判断是否存在的?用系统调用stat获得文件信息,如果获取成功文件存在反之不存在,存在则可以获得文件的size和权限等信息。如果不存在就返回404页面,返回码标记成404。
    3. 存在就一定可以读取吗?
      1. 有可能是目录。如果访问的是目录路径,则默认返回该目录下的index.html文件,因此每个目录都要index.html。S_ISREG(stat.st_mode)判断普通文件,S_ISDIR(stat.st_mdoe)判断是目录。
      2. 有可能请求的是一个可执行程序,需要特殊处理。检查是否有S_IXUSRS_IXGRPS_IXOTH权限,有就说明是可执行文件。

分析响应部分:

void BuildHttpResponse() {

        std::string& path = _http_request._path;
        std::string& argv = _http_request._suffix;   

        int& code = _http_response._status_code;

        if (_http_request._method != "POST" && _http_request._method != "GET") {
            code = BAD_REQUEST;
            goto END;
        }
        if (_http_request._method == "GET" ) {
          if (_http_request._url.find(IFQUERY) != std::string::npos) { /*说明是发送*/
              Util::CutStrToPart(_http_request._url, path, argv, IFQUERY);
              path = WEBROOT + path;
              LOG(INFO, path);
              LOG(INFO, argv);
              _http_request._cgi = true;
          }
          else { /*说明是获取*/
            if ( _http_request._url == "/") {
                path = WEBROOT + std::string("/") + std::string(PAGE);
            }
            else  path = WEBROOT + _http_request._url;
            LOG(INFO, path);
            /*判断当前文件是否存在*/
            struct stat st; 
            int cnt = stat(path.c_str(), &st);
            LOG(INFO, path);
            if (cnt < 0) {/*说明资源不存在*/
               code = NOT_FOUND;
               LOG(ERROR, "FILE NOT FOUND");
               goto END;
            } 
            /*说明是目录*/ 
            if (S_ISDIR(st.st_mode)) {
                 path += "/";
                 path += PAGE; 
                 cnt = stat(path.c_str(), &st); 
                 LOG(INFO, path);
                 if (cnt < 0) {
                     code = NOT_FOUND;
                     LOG(ERROR, "Dir not found index.html");
                     goto END;
                 }
                 LOG(INFO, std::string("Dir -> html :") + path);
            }
            /*说明是可执行程序*/
            if ((S_IXUSR&st.st_mode) || (S_IXGRP&st.st_mode) || (S_IXOTH&st.st_mode)) {
                 _http_request._cgi = true;
            }
            
            _http_response._size = st.st_size;
          }
        }
        else if (_http_request._method == "POST") { /*说明是发送*/
            _http_request._cgi = true;
            path = WEBROOT +  _http_request._url;
        }

        if ( _http_request._cgi == false) {
            LOG(INFO, "Call ProcessNonCgi");
            code = ProcessNonCgi();
        }
        else {
            LOG(INFO, "Call ProcessCgi");
            code = ProcessCgi();
        } 
END:
        /*统一通过code进行构建响应报文*/
        /*降低耦合*/
        Code2Response(); 

        return;
      }

构建响应部分:分别对非cgi和cgi部分进行构建不同的响应。这里先对非cgi部分(直接返回静态网页部分)进行处理。

image-20220304203347198

  1. 到了这一段逻辑的时候保证目标网页是存在的
  2. 返回并不是单单返回网页,而是要构建HTTP响应
  3. 传统做法是直接打开文件,然后利用文件描述符进行读写(可以联系文件系统)。但是这样的模式拷贝多次,这里学习sendfile(只在内核层面进行拷贝,少了用户拷贝的两次)。

? image-20220304205616951

private:
int ProcessNonCgi(size) {
    _http_response._fd = open(_http_request.path.c_str(), O_RDONLY);
    if(_http_response._fd >=0) {
         /*请求行*/
        _http_response._status_line = HTTP_VERSION;
        _http_response._status_line += " ";
        _http_response._status_line += std::to_string(_http_response.status_code);
        _http_response._status_line += " ";
        _http_response._status_line += Code2Desc(__http_response.status_code);
        _http_response._status_line += LINE_END;
        _http_response._size = size;
        return OK;
        /*空行在构造的时候给了*/
    }
    return 404;
}

这里是后期整合后的代码

int ProcessNonCgi() {
    _http_response._fd = open(_http_request._path.c_str(), O_RDONLY);
    if (_http_response._fd < 0) {
        LOG(ERROR, "ProcessNonCgi open error");
        return Internal_Server_Error;
    }
    return OK;
}

报头至少要包含的两个属性:

  1. Content-Length告知正文长度
  2. Content-Type告知资源类型(请求资源是什么类型,取决于资源的后缀)
void HandleResponseStatusLine() {
    /*构建响应状态行*/
    _http_response._status_line += "HTTP/1.0";
    _http_response._status_line += " ";
    _http_response._status_line += std::to_string(_http_response._status_code);
    _http_response._status_line += " ";
    _http_response._status_line += Util::Code2Desc(_http_response._status_code);
    _http_response._status_line += LINEEND;
}
void HandleOK() {

    /*响应报头中插入Content-Length属性*/
    std::string ContentLength = "Content-Length: ";
    if (_http_request._cgi == false) { /*返回静态网页*/
        ContentLength += std::to_string(_http_response._size);
    }
    else {
        ContentLength += std::to_string(_http_response._response_body.size());
    }
    ContentLength += LINEEND;
    _http_response._response_header.push_back(ContentLength);

    /*响应报头中插入Content-Type属性,这里的path一定是合法的*/
    std::string ContentType = "Content-Type: ";

    if (_http_request._cgi == false) {

        size_t pos = _http_request._path.rfind(".");  
        if (pos == std::string::npos) {
            LOG(ERROR, "dot is not found");
        }

        std::string suffix = _http_request._path.substr(pos);

        ContentType += Suffix2Desc(suffix);
        ContentType += LINEEND;

    }
    else {
        std::string suffix = ".html";
        ContentType += Suffix2Desc(suffix);
        ContentType += LINEEND;
    }

    _http_response._response_header.push_back(ContentType);
    /*空行初始化给了*/
}
void HandleError(std::string page) {

    /*不管什么方法到这里都是差错处理,要进行静态网页的返回,因此此时不调用cgi(输出正文)输出响应*/
    _http_request._cgi = false;
    std::string ContentLength = "Content-Length: ";
    struct stat st;
    int result = stat(page.c_str(), &st);
    if (result < 0) {
        LOG(ERROR, "Error page not exists");
    } 
    ContentLength += std::to_string(st.st_size);
    ContentLength += LINEEND; 
    _http_response._response_header.push_back(ContentLength);


    std::string ContentType = "Content-Type: text/html";
    ContentType += LINEEND;
    _http_response._response_header.push_back(ContentType); 

    LOG(INFO, std::to_string(_http_request._cgi));
    _http_response._fd = open(page.c_str(), O_RDONLY);
    _http_response._size = st.st_size;

}
IO通信

根据网络基础所学,这里的send实际上是将数据从用户层拷贝到内核级的发送缓冲区,具体什么时候发及发多少由tcp决定。

对于返回静态页面的请求,我们可以先保存文件描述符fd,然后直接使用sendfile进行传送,这样就不需要进行用户层到内核层的拷入和拷出。

对于cgi程序的结果返回,我们将结果通过管道传输到response的响应正文中最后传送过来。

  • sendfile接口说明:[int out_fd(往这里写),int in_fd(从这里读),off_t *offset(0),size_t count(想传输的字节)]。而由于此时没有保存在用户缓冲区中,因此我们要保存起来最后发送的时候先发送状态行,再发送响应报头空行,最后发送响应正文。
void SendHttpResponse() {
           LOG(INFO, "SendHttpResponse");
           send(_sock, _http_response._status_line.c_str(), _http_response._status_line.size(), 0);
           for (auto& iter: _http_response._response_header) {
                send(_sock, iter.c_str(), iter.size(), 0);
                LOG(INFO, iter.c_str());
           }
           send(_sock, _http_response._blank.c_str(), _http_response._blank.size(), 0);

           ssize_t s = 0;
           if (_http_request._cgi == false) {
                s = sendfile(_sock, _http_response._fd, nullptr, _http_response._size);
                LOG(INFO, "sendfile: " + std::to_string(_http_response._size));
           }
           else if (_http_request._cgi == true) {
								auto& response_body = _http_response._response_body;
                LOG(INFO, response_body);
								size_t total = 0;
								char* start = (char*)response_body.c_str();
								while (total < response_body.size() && (s = send(_sock, start + total, response_body.size() - total, 0)) > 0) {
										total += s;
                    LOG(INFO, std::to_string(total));
								} 
                LOG(INFO, "send-s : " + std::to_string(total));
           }

           std::cout<<"errno = " <<errno <<std::endl;
           close(_http_response._fd);
      }
EndPoint类整体代码实现
class EndPoint {
    private:
      int _sock;
      HttpRequest _http_request; 
      HttpResponse _http_response;
    private:
      bool RecvHttpRequestLine() {
           std::string& out = _http_request._request_lines;
           if (Util::ReadLine(_sock, out) <= 0) {
              return false;
           }
           return true;
      }
      void ParseHttpRequestLine() {
           std::stringstream ss(_http_request._request_lines);/*默认按空行分割*/
           ss >> _http_request._method >> _http_request._url >> _http_request._version;
           Util::Tranform(_http_request._method);
           LOG(INFO, _http_request._method);
           LOG(INFO, _http_request._url);
           LOG(INFO, _http_request._version);
      }
      int RecvHttpRequestHeader() {
           std::string out;
           while (true) {
               out.clear();
               size_t s = Util::ReadLine(_sock, out);  
               if (s == 1) { /*只有'\n',说明读到了空行读完了*/
                   break; 
               }
               if (s <= 0) {
                   return false;
               }
               out.resize(static_cast<int>(out.size()) - 1);
               _http_request._request_header.push_back(out);
               LOG(INFO ,out);
           }              
           return true;
      }
      void ParseHttpRequestHeader() {
        for(auto& str: _http_request._request_header) {
             std::string key, value;
             Util::CutStrToPart(str, key, value, SEG);
             _http_request._kvMap.insert({key,value});
             LOG(INFO, key);
             LOG(INFO, value);
        }
      }
      bool IsHasHttpRequestBody() {
           auto& str = _http_request._method;
           if (str == "POST") {
               return true; 
           }
           return false;
      }
      void RecvHttpRequestBody() {
        if (IsHasHttpRequestBody()) {
            auto iter = _http_request._kvMap.find("Content-Length");
            if (iter == _http_request._kvMap.end()) {
                LOG(ERROR, "POST NOT FOUND Content-Length");
            }
            else {
                _http_request._Content_Length = atoi((iter -> second.c_str()));
                LOG(INFO, "Content-Length get success");
                int contentLength = _http_request._Content_Length;

                char ch = 0;
                while (contentLength) {
                     size_t s = recv(_sock, &ch, 1, 0);
                     if (s == 0) {
                         LOG(ERROR, "contentLength"); 
                         break;
                     }
                     _http_request._request_body.push_back(ch);
                     contentLength--;
                }
            }
        }  
      }
      int ProcessNonCgi() {
          _http_response._fd = open(_http_request._path.c_str(), O_RDONLY);
          if (_http_response._fd < 0) {
              LOG(ERROR, "ProcessNonCgi open error");
              return Internal_Server_Error;
          }
          return OK;
      }
      int ProcessCgi() { /*GET带参或者POST方法才会到这里调用cgi程序*/

          int code = OK;

          std::string& bin = _http_request._path; /*这里的一定是合法的*/
          
          LOG(INFO , bin);

          std::string PATH = "METHOD=";
          PATH += _http_request._method;
          putenv((char*)PATH.c_str());/*注意Putenv是浅拷贝*/

          std::string DATA = "INPUT=";

          if (_http_request._method == "GET") {
              DATA += _http_request._suffix;
              LOG(INFO, DATA);
              putenv((char*)DATA.c_str());
          }

          std::string LENGTH =  "CONTENT_LENGTH=";
          if (_http_request._method == "POST") {
              LENGTH += std::to_string(_http_request._request_body.size());
              putenv((char*)LENGTH.c_str());
          }
          else {
              LENGTH += std::to_string(_http_request._suffix.size());
          }

          LOG(INFO, LENGTH);

          int input[2], output[2]; 
          if (pipe(input) < 0) {
             LOG(FATAL, "INPUT PIPE CREATE FAILED");
             code = Internal_Server_Error;
          } 
          if (pipe(output) < 0) {
             LOG(FATAL, "OUTPUT PIEPE CREATE FAILED");
             code = Internal_Server_Error;
          } 
          pid_t pid = fork();
          if (pid == 0) {/*子进程*/
              close(input[0]);
              close(output[1]);

              LOG(INFO , bin);
             
              dup2(input[1], 1);
              dup2(output[0], 0);

              execl(bin.c_str(), bin.c_str(), nullptr);
              LOG(ERROR, "execl error");
              exit(1);
          }
          else if (pid > 0) { /*父进程*/
              close(input[1]);        
              close(output[0]);

              /*通过管道传输正文*/
              if (_http_request._method == "POST") {
                  int cnt = 0;
                  char* str = (char*)_http_request._request_body.c_str();
                  size_t size = _http_request._request_body.size();
                  size_t s;
                  while ((s = write(output[1], str + cnt, size - cnt)) > 0 ) {
                        cnt += s; 
                  }
              } 
              int status = 0;
              pid_t res = waitpid(pid, &status, 0);

              if (res == pid) {
                   if (WIFEXITED(status)) {/*正常退出*/
                     if (WEXITSTATUS(status)) {/*退出码 > 0*/
                        LOG(ERROR, "cgi exitcode is > 0");
                        code = BAD_REQUEST;  
                     }    
                     else {
                        code = OK;
                     }
                   }
                   else if (WIFSIGNALED(status)) {/*异常退出*/
                       LOG(ERROR, "cig is killed");
                       code = Internal_Server_Error;
                   }
              }

              /*读子进程返回的数据*/
              size_t size; 
              char ch = '0';
              while ((size = read(input[0], &ch, 1)) > 0) {
                   _http_response._response_body.push_back(ch); 
              }
              LOG(INFO, _http_response._response_body);
              LOG(INFO, std::to_string(_http_response._response_body.size()));
              /*释放资源*/
              close(input[0]);
              close(output[1]);
          }
          else {
              LOG(FATAL, "fork error.."); 
              return  Internal_Server_Error;
          }
          LOG(INFO, "ProcessCgi() done...");
          return code;
      }
      void HandleResponseStatusLine() {
             /*构建响应状态行*/
             _http_response._status_line += "HTTP/1.0";
             _http_response._status_line += " ";
             _http_response._status_line += std::to_string(_http_response._status_code);
             _http_response._status_line += " ";
             _http_response._status_line += Util::Code2Desc(_http_response._status_code);
             _http_response._status_line += LINEEND;
      }
      void HandleOK() {

             /*响应报头中插入Content-Length属性*/
             std::string ContentLength = "Content-Length: ";
             if (_http_request._cgi == false) { /*返回静态网页*/
                 ContentLength += std::to_string(_http_response._size);
             }
             else {
                 ContentLength += std::to_string(_http_response._response_body.size());
             }
             ContentLength += LINEEND;
             _http_response._response_header.push_back(ContentLength);

             /*响应报头中插入Content-Type属性,这里的path一定是合法的*/
             std::string ContentType = "Content-Type: ";

             if (_http_request._cgi == false) {

                  size_t pos = _http_request._path.rfind(".");  
                  if (pos == std::string::npos) {
                      LOG(ERROR, "dot is not found");
                  }
  
                  std::string suffix = _http_request._path.substr(pos);

                  ContentType += Suffix2Desc(suffix);
                  ContentType += LINEEND;

             }
             else {
                  std::string suffix = ".html";
                  ContentType += Suffix2Desc(suffix);
                  ContentType += LINEEND;
             }

             _http_response._response_header.push_back(ContentType);
             /*空行初始化给了*/
      }
      void HandleError(std::string page) {
              
             /*不管什么方法到这里都是差错处理,要进行静态网页的返回,因此此时不调用cgi(输出正文)输出响应*/
             _http_request._cgi = false;
             std::string ContentLength = "Content-Length: ";
             struct stat st;
             int result = stat(page.c_str(), &st);
             if (result < 0) {
                LOG(ERROR, "Error page not exists");
             } 
             ContentLength += std::to_string(st.st_size);
             ContentLength += LINEEND; 
             _http_response._response_header.push_back(ContentLength);


             std::string ContentType = "Content-Type: text/html";
             ContentType += LINEEND;
             _http_response._response_header.push_back(ContentType); 
             
             LOG(INFO, std::to_string(_http_request._cgi));
             _http_response._fd = open(page.c_str(), O_RDONLY);
             _http_response._size = st.st_size;

      }
      void Code2Response() {
               
           int code = _http_response._status_code;
           LOG(INFO ,std::to_string(code));

           HandleResponseStatusLine();
           switch (code) {
             case OK :
               LOG(INFO, "HandleOK");
               HandleOK();
               break; 
             case NOT_FOUND:
               LOG(INFO, "HandleError NOT_FOUND");
               HandleError(NOT_FOUNT_PAGE);
               break;
             case Internal_Server_Error:
               LOG(INFO, "HandleError ServerERROR");
               HandleError(Internal_Server_Error_PAGE);
               break;
             default:
               break;
           }
      }
    public:
      EndPoint(int sock) :_sock(sock) {

      }
      int RecvHttpRequest() {
           if (RecvHttpRequestLine() && RecvHttpRequestHeader()) {
              ParseHttpRequestLine();
              ParseHttpRequestHeader();
              RecvHttpRequestBody();
              return true;
           }
           else {
              return false;
           }
      }
      void BuildHttpResponse() {

        std::string& path = _http_request._path;
        std::string& argv = _http_request._suffix;   

        int& code = _http_response._status_code;

        if (_http_request._method != "POST" && _http_request._method != "GET") {
            code = BAD_REQUEST;
            goto END;
        }
        if (_http_request._method == "GET" ) {
          if (_http_request._url.find(IFQUERY) != std::string::npos) { /*说明是发送*/
              Util::CutStrToPart(_http_request._url, path, argv, IFQUERY);
              path = WEBROOT + path;
              LOG(INFO, path);
              LOG(INFO, argv);
              _http_request._cgi = true;
          }
          else { /*说明是获取*/
            if ( _http_request._url == "/") {
                path = WEBROOT + std::string("/") + std::string(PAGE);
            }
            else  path = WEBROOT + _http_request._url;
            LOG(INFO, path);
            /*判断当前文件是否存在*/
            struct stat st; 
            int cnt = stat(path.c_str(), &st);
            LOG(INFO, path);
            if (cnt < 0) {/*说明资源不存在*/
               code = NOT_FOUND;
               LOG(ERROR, "FILE NOT FOUND");
               goto END;
            } 
            /*说明是目录*/ 
            if (S_ISDIR(st.st_mode)) {
                 path += "/";
                 path += PAGE; 
                 cnt = stat(path.c_str(), &st); 
                 LOG(INFO, path);
                 if (cnt < 0) {
                     code = NOT_FOUND;
                     LOG(ERROR, "Dir not found index.html");
                     goto END;
                 }
                 LOG(INFO, std::string("Dir -> html :") + path);
            }
            /*说明是可执行程序*/
            if ((S_IXUSR&st.st_mode) || (S_IXGRP&st.st_mode) || (S_IXOTH&st.st_mode)) {
                 _http_request._cgi = true;
            }
            
            _http_response._size = st.st_size;
          }
        }
        else if (_http_request._method == "POST") { /*说明是发送*/
            _http_request._cgi = true;
            path = WEBROOT +  _http_request._url;
        }

        if ( _http_request._cgi == false) {
            LOG(INFO, "Call ProcessNonCgi");
            code = ProcessNonCgi();
        }
        else {
            LOG(INFO, "Call ProcessCgi");
            code = ProcessCgi();
        } 
END:
        /*统一通过code进行构建响应报文*/
        /*降低耦合*/
        Code2Response(); 

        return;
      }

      void SendHttpResponse() {
           LOG(INFO, "SendHttpResponse");
           send(_sock, _http_response._status_line.c_str(), _http_response._status_line.size(), 0);
           for (auto& iter: _http_response._response_header) {
                send(_sock, iter.c_str(), iter.size(), 0);
                LOG(INFO, iter.c_str());
           }
           send(_sock, _http_response._blank.c_str(), _http_response._blank.size(), 0);

           ssize_t s = 0;
           if (_http_request._cgi == false) {
                s = sendfile(_sock, _http_response._fd, nullptr, _http_response._size);
                LOG(INFO, "sendfile: " + std::to_string(_http_response._size));
           }
           else if (_http_request._cgi == true) {
								auto& response_body = _http_response._response_body;
                LOG(INFO, response_body);
								size_t total = 0;
								char* start = (char*)response_body.c_str();
								while (total < response_body.size() && (s = send(_sock, start + total, response_body.size() - total, 0)) > 0) {
										total += s;
                    LOG(INFO, std::to_string(total));
								} 
                LOG(INFO, "send-s : " + std::to_string(total));
           }

           std::cout<<"errno = " <<errno <<std::endl;
           close(_http_response._fd);
      }
      ~EndPoint() {
          close(_sock);
      }
};

HandlerRequest接口——CallBack类

主要作用是通过仿函数来调用回调函数HandlerRequest,从而进行了处理报文部分和线程池部分的解耦。

//#define DEBUG 1
class CallBack{
    public:
      CallBack() = default; 
      void operator()(int _sock) { /*仿函数调回调函数*/
           HandlerRequest(_sock);
      }
      static void* HandlerRequest(int sock) {
          
#ifdef DEBUG
          char buffer[4096];
          size_t s = recv(sock, buffer, sizeof(buffer), 0);
          std::cout<<"---------begin-----------"<<std::endl;
          std::cout<<buffer<<std::endl;
          std::cout<<"---------end-------------"<<std::endl;
#else
          EndPoint* ep = new EndPoint(sock);
          if (ep -> RecvHttpRequest()) {
              ep -> BuildHttpResponse();
              ep -> SendHttpResponse();
          }
          else {
              LOG(INFO, "RecvHttpRequest ERROR");
          }
          delete ep;
#endif
          
          LOG(INFO, "Hander Request End");
          return nullptr;
      }
};

HTTP CGI机制

CGI机制的原理

前面提到客户端花费时间上传数据是为了服务端处理数据,如何处理呢?HTTP需要通过某种方式处理数据,这种方式叫CGI模式。

CGI(Common Gateway Interface)是WWW技术中最重要的技术之一,有着不可替代的重要地位(网络服务器和后端业务程序耦合重要机制,理解PhP/Python/Java如何把网络里的数据拿到自身的程序中)。CGI是外部应用程序(CGI程序)与WEB服务器之间的接口标准,是在CGI程序和Web服务器之间传递信息的过程。

image-20220305155834994

怎么调用目标程序,怎么传递目标数据,怎么拿到结果这就是CGI机制的通信细节。这里就需要我们来实现cgi机制。

需求:httpserver是一个可执行程序,加载到内存中,变成了进程;cgi程序也是一个可执行程序,加载到内存中,也是一个进程。如何用一个进程去执行另一个进程呢?程序替换:exec*

但是目前为止不能直接进行exec,因为当前是一个进程两个线程。因此需要创建子进程,让子进程替换。同时要将client传上的数据交给cgi进程。这就涉及到进程间通信了。

使用CGI进行数据处理的时机

什么时候需要使用CGI来进行数据处理呢?只要用户有数据上传上来,这个数据不是httpServer应该处理的,而应该是CGI处理的。就服务器来说,当面临:

  • POST方法时需要标记使用cgi
  • GET方法传参时需要标记cgi
  • 请求的资源是可执行文件的时候也需要标记cgi

当不存在cgi标记的时候说明就是传静态资源。当存在cgi标记的时候说明就是需要进行数据处理。

调用目标程序

  • PATH中保存着可执行程序,使用exec调用的目标程序。

image-20220305161442624

使用第一个相对来说更方便。

image-20220305165814807

传递目标数据

父进程拿到的数据在哪里?

POST:body;GET:query_string

父进程如何将数据传递给子进程?

一种方式是管道,另一种方式是环境变量。环境变量是具有全局属性的(可以被子进程继承下去),不受exec*的影响。(bash创建子进程后exec后执行自己程序环境变量并没有被替换掉;同时进程地址空间mm_struct中包含命令行参数和环境变量,是进程地址空间的一部分,内核创建的数据结构不变)

CGI如何得知需要从标准输入读多少字节?

数据长度也要通过环境变量传参。

通过管道传输

对于POST方法其数据通常在请求正文中,会比较长,所以通过管道传输同时要保证传输完毕(类似字节流)。

  • 通过进程间通信传递数据

这里约定:管道的通信站在父进程角度。

image-20220305161921755

image-20220305165653358

  • 使用dup2重定向

因此我们约定,让目标被替换之后的进程,在**exec***函数系列执行之前进行重定向,读取管道等价于读取标准输入,写出管道等价于写到标准输出。

image-20220305184738959

dup2替换的规则是拷贝,将input的文件指针拷贝到1中;将output的文件指针拷贝到0中。

image-20220305190143000

int ProcessCgi() { /*GET带参或者POST方法才会到这里调用cgi程序*/

    int code = OK;

    std::string& bin = _http_request._path; /*这里的一定是合法的*/

    LOG(INFO , bin);

    LOG(INFO, LENGTH);

    int input[2], output[2]; 
    if (pipe(input) < 0) {
        LOG(FATAL, "INPUT PIPE CREATE FAILED");
        code = Internal_Server_Error;
    } 
    if (pipe(output) < 0) {
        LOG(FATAL, "OUTPUT PIEPE CREATE FAILED");
        code = Internal_Server_Error;
    } 
    pid_t pid = fork();
    if (pid == 0) {/*子进程*/
        close(input[0]);
        close(output[1]);

        LOG(INFO , bin);

        dup2(input[1], 1);
        dup2(output[0], 0);

        execl(bin.c_str(), bin.c_str(), nullptr);
        LOG(ERROR, "execl error");
        exit(1);
    }
    else {
        LOG(FATAL, "fork error.."); 
        return  Internal_Server_Error;
    }
    LOG(INFO, "ProcessCgi() done...");
    return code;
}
通过环境变量传输

因为GET方法的数据在url中,因此通常比较小,可以使用更加快速的环境变量来传递。

同时将告知程序为GET/POST的信息也通过环境变量来传参。

int ProcessCgi() {
    std::string PATH = "METHOD=";
    PATH += _http_request._method;
    putenv((char*)PATH.c_str());/*注意Putenv是浅拷贝*/

    std::string DATA = "INPUT=";

    if (_http_request._method == "GET") {
        DATA += _http_request._suffix;
        LOG(INFO, DATA);
        putenv((char*)DATA.c_str());
    }

    std::string LENGTH =  "CONTENT_LENGTH=";
    if (_http_request._method == "POST") {
        LENGTH += std::to_string(_http_request._request_body.size());
        putenv((char*)LENGTH.c_str());
    }
    else {
        LENGTH += std::to_string(_http_request._suffix.size());
    }
}

传回结果数据

父进程通过管道读取子进程传回的数据。

else if (pid > 0) { /*父进程*/
    close(input[1]);        
    close(output[0]);

    /*通过管道传输正文*/
    if (_http_request._method == "POST") {
        int cnt = 0;
        char* str = (char*)_http_request._request_body.c_str();
        size_t size = _http_request._request_body.size();
        size_t s;
        while ((s = write(output[1], str + cnt, size - cnt)) > 0 ) {
            cnt += s; 
        }
    } 
    int status = 0;
    pid_t res = waitpid(pid, &status, 0);

    if (res == pid) {
        if (WIFEXITED(status)) {/*正常退出*/
            if (WEXITSTATUS(status)) {/*退出码 > 0*/
                LOG(ERROR, "cgi exitcode is > 0");
                code = BAD_REQUEST;  
            }    
            else {
                code = OK;
            }
        }
        else if (WIFSIGNALED(status)) {/*异常退出*/
            LOG(ERROR, "cig is killed");
            code = Internal_Server_Error;
        }
    }

    /*读子进程返回的数据*/
    size_t size; 
    char ch = '0';
    while ((size = read(input[0], &ch, 1)) > 0) {
        _http_response._response_body.push_back(ch); 
    }
    LOG(INFO, _http_response._response_body);
    LOG(INFO, std::to_string(_http_response._response_body.size()));
    /*释放资源*/
    close(input[0]);
    close(output[1]);
}

总结CGI

浏览器和server进行数据交互的本质,就是进程间通信,这也是socket通信的本质。

浏览器和server无非两种形式的通信:

  1. 浏览器拿下来数据(通常拿下来的是静态网页)
  2. 浏览器上传数据(POST方法或者GET方法带参)

image-20220306162126880

如何看待CGI程序?

子CGI程序的标准输入是浏览器,子CGI程序的标准输出是浏览器。

因此web开发的程序是CGI程序,直接scanf和printf。不用再关心通信细节(由http完成)。

目前主流的web开发语言底层一定有cgi机制,java、php、python直接拿到浏览器输入的请求和输出到浏览器上。

image-20220306162512515

构建差错处理

处理过程中发现构建Response报头和返回静态页面的部分一致,因此可以整理进行复用。

  • http服务器出错的原因

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

  1. 读取错误(读取的时候)-读取不一定完毕-不给对方回应-退出即可
  2. 逻辑错误(分析的时候)-读取完毕-要给对方回应对应的错误页面
  3. 写入出错(响应的时候)-写端不一定完毕,读端关闭(系统会给进程发送SIGPIPE信号)-自定义捕捉进行忽略处理,服务器端继续发送

对每个差错进行返回对应的错误码从而返回不同的页面。

需要注意的细节

  • sockaddr和sockaddr_in
  • 线程池中accpet sock部分采用new
  • getenv函数的坑
  • 对waitpid返回值进行判断

引入线程池

解决问题:

  • 大量链接过来导致服务器内部进程或者线程暴增,进而导致服务器效率严重降低或者挂掉
  • 节省链接请求到来时,创建线程的时间成本
  • 让服务器的效率在一个恒定的稳定区间内(线程个数不增多,CPU调度成本不变)

如何处理海量请求?

(缓解方案)线程池、EPOLL,HTTP改成1.1支持长连接

解决方案是在软件和硬件之间平衡,硬件不够加硬件,软件不行改软件。

image-20220307164713920

回调方法

  • 编写任务回调

将原来的EndPoint类进行设计仿函数,仿函数内部进行回调。而任务中包含了该类对象。此时可以发现,TcpServer和HttpServer解耦(连接是TcpServer的),HttpServer和Protocal解耦(只需要将sock构造成任务并放入线程池),线程池和Protocal只有入口函数的关系,也完成了解耦(通过仿函数调用回调函数进入Protocal入口)

Task.hpp

#pragma once
#include<iostream>
#include"Protocol.hpp"
#include"Log.hpp"

class Task {
    private:
        int _sock;
        CallBack _callBack;
    public:
        Task() = default;
        Task(int sock) :_sock(sock) {

        }
        void TaskHander() {
             LOG(INFO, "CallBack begin");
             _callBack(_sock);
             LOG(INFO, "CallBack end");
        }
        ~Task() {}
};

#include<iostream>
#include<pthread.h>
#include<queue>
#include<string>
#include<unistd.h>
#include<sys/wait.h>
#include"Task.hpp"
#include"Log.hpp"

#define NUM 5

class PthreadPool {
    private:
      std::queue<Task> _blockQueue;
      pthread_cond_t _cond;
      pthread_mutex_t _lock;
      static PthreadPool* _pthreadPool;
    private:
      PthreadPool() = default; 
      PthreadPool(const PthreadPool& t) = delete;
      PthreadPool& operator=(const PthreadPool& t) = delete;
      void Wait() {
           pthread_cond_wait(&_cond, &_lock);
      }
      void WakeUp() {
           pthread_cond_signal(&_cond);
      }
      void Lock() {
           pthread_mutex_lock(&_lock);
      }
      void UnLock() {
           pthread_mutex_unlock(&_lock);
      }
      bool IsEmpty() {
          return _blockQueue.size() == 0;
      }
    public:
      static PthreadPool* GetInstance(){
        static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
        if (_pthreadPool == nullptr) {
            pthread_mutex_lock(&mtx);
            if (_pthreadPool == nullptr) {
                _pthreadPool = new PthreadPool();
                _pthreadPool -> PthreadPoolInit(); 
                LOG(INFO, "PthreadPoolInit done");
            }
            pthread_mutex_unlock(&mtx);
        }
        return _pthreadPool;
      }
      static void* ThreadHandler(void* arg) {
            PthreadPool* _this = (PthreadPool*)(arg); 
            while(true) {
              _this->Lock();
              while (_this -> IsEmpty()) { /*while防止假唤醒*/
                   _this->Wait(); 
                   LOG(INFO , "pthread_t id :" + std::to_string(pthread_self()));
              }
              LOG(INFO, "I am awake ");
              Task t; 
              _this->Pop(t);

              LOG(INFO, "I am get task");
              _this->UnLock();
          
              LOG(INFO, "TaskHander begin..");
              t.TaskHander(); /*处理逻辑的代码放在临界区外面*/
              LOG(INFO, "TaskHander done..");
            }
            return nullptr;
      }
      void PthreadPoolInit() {
           pthread_mutex_init(&_lock, nullptr);
           pthread_cond_init(&_cond, nullptr);
           for (int i = 0; i < NUM; i++) {
               pthread_t pid;
               pthread_create(&pid, nullptr, ThreadHandler, (void*)this);
               pthread_detach(pid);
           }
      }
      void Push(Task& task) {
           Lock();
           _blockQueue.push(task);
           LOG(INFO, "Push one task...");
           UnLock();
           WakeUp();/*唤醒线程*/
      }
      void Pop(Task& task) {
           task = _blockQueue.front();
           _blockQueue.pop();
      }
      ~PthreadPool() {
           pthread_mutex_destroy(&_lock);
           pthread_cond_destroy(&_cond);
           delete _pthreadPool;
           _pthreadPool = nullptr;
      }
};
PthreadPool* PthreadPool:: _pthreadPool = nullptr;

Makefile编写

bin=httpserver
src=main.cc
cc=g++
cgi=cgi_bin,shell_cgi.sh,python_cgi.py
FLAGS=-std=c++11 -lpthread #-DDEBUG1
pwd=$(shell pwd)

.PHONY:all
all:CGI $(bin) 

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

CGI:
		cd $(pwd)/Cgi;\
		make;\
		cd -;

.PHONY:Output
output:
	mkdir -p Output
	mkdir -p Output/Web
	cp -r $(pwd)/Cgi/{$(cgi)} $(pwd)/Web 
	cp -r $(pwd)/Web $(pwd)/Output
	cp -r $(bin) $(pwd)/Output
.PHONY:clean
clean:
	rm -rf *.o $(bin) Output;\
	cd $(pwd)/Cgi;\
	make clean;\
	cd -;

表单测试(前端页面发送整合http

请求)

一般像表单采用post方法提交,若在method中没有指明则是采用get方法提交。

因此比如百度页面的搜索部分其实就是表单<form>,默认以get方法传参,而/s就是要交付的cgi程序,而后面的就是参数,&是参数连接符号,=是参数的value。此时就可以访问将数据传递给对应的进程从而返回html构建的数据结果显示到浏览器上。

https://www.baidu.com/s?wd=nba&rsv_spt=1&rsv_iqid=0x87c278bf0000c600&issp=1&f=8&rsv_bp=1&rsv_idx=2&ie=utf-8&tn=baiduhome_pg&rsv_dl=tb&rsv_enter=1&rsv_sug3=3&rsv_sug1=1&rsv_sug7=100&rsv_sug2=0&rsv_btype=i&prefixsug=nba&rsp=5&inputT=1588&rsv_sug4=1588
  1. GET通过uri传参,form提交的时候,会自动拼接我们的url
  2. POST通过正文传参(通过管道传参)
  3. GET通过url传参,大小一般有限制(通过环境变量传递给子进程),提交的数据基本比较公开

补充内容

做完这个项目,最主要的收获是两点,分别是原理层面和应用层面。额外的还有一些过程中的收获。

Postman测试

测试工具:telnet,Postman。

相较于telnet,前者更容易看到每一步的过程,后者更为直观更方便调试。另外Wfetch软件可以看到\r和\n

同时在这里我们发现之前直接用C/C++写的cgi程序,返回时构建html页面返回是很麻烦的,因为其对字符串的处理不方便(python/php更方便web后端开发)。因此实际上cgi程序上面对获得的数据进行处理的部分更适合用C/C++去处理,而下面的以html方式返回数据结果更适合用web开发的语言比如python和php。但倘若并不需要返回网页(web服务器),如果是游戏服务器,展示给用户的就是游戏数据为了效率采用C/C++。

使用其他语言编写cgi

  • shell脚本
#!/bin/bash

echo "hello world"

ls -a -l ./..

echo "hello world"
  • python脚本
#!/usr/bin/python3
print "hello python"

访问数据库

先在官网下载linux的mysql安装包。

这部分由于数据库并不熟悉,等这学期数据库实训的时候学了数据库到时再回来处理这一部分。

导入数据库

这里涉及到之前学习的动态库的使用,-I-L-l,export LD_LIBRARY_PATH=路径。为了方便起见使用Lib中的静态库进行静态链接(交叉编译的时候要-lpthread,-ldl指明各种第三方库)。

测试代码:

#include<iostream>
#include "./include/mysql.h"
int main() {
    std::cout << "version: " << mysql_get_client_info() <<std::endl;
    return 0;
}

创建数据库、用户、表

使用mysql接口

#include<iostream>
#include<string>
#include"mysql.h>
int main() {
    MYSQL* conn = mysql_init(nullptr);
    /*前面的是用户名,后面的是访问的数据库*/
    if( nullptr == mysql_real_connect(conn, "127.0.0.1", "http_test" , "12345678","http_test",3306, nullptr ,0) ){
        std::cerr << "connect error!" <<std::endl;
    	return 1;
    }
    
    std::string sql = ;
    int ret = mysql_query(conn,sql.c_str());
    std::cerr << "connect success" <<std::endl;
    mysql_close(conn);
}
  网络协议 最新文章
使用Easyswoole 搭建简单的Websoket服务
常见的数据通信方式有哪些?
Openssl 1024bit RSA算法---公私钥获取和处
HTTPS协议的密钥交换流程
《小白WEB安全入门》03. 漏洞篇
HttpRunner4.x 安装与使用
2021-07-04
手写RPC学习笔记
K8S高可用版本部署
mySQL计算IP地址范围
上一篇文章      下一篇文章      查看所有文章
加:2022-03-11 22:34:38  更:2022-03-11 22:35:38 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/26 6:43:34-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码