Web版简易加法计算

本文介绍如何使用HTTP协议、CGI接口构建一个Web服务,该服务接收GET或POST请求,根据请求参数计算两个数字的和。内容包括HTTP方法、状态码、CGI协议的工作原理以及代码实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

##HTTP
HTTP是一个客户端和服务器端请求和应答的标准(TCP)。客户端是终端用户,服务器端是网站。通过使用Web浏览器、网络爬虫或者其它的工具,客户端发起一个到服务器上指定端口(默认端口为80)的HTTP请求。(我们称这个客户端)叫用户代理。应答的服务器上存储着(一些)资源,比如HTML文件和图像。(我们称)这个应答服务器为源服务器。

通常,由HTTP客户端发起一个请求,建立一个到服务器指定端口(默认是80端口)的TCP连接。HTTP服务器则在那个端口监听客户端发送过来的请求。一旦收到请求,服务器(向客户端)发回一个状态行,比如"HTTP/1.1 200 OK",和(响应的)消息,消息的消息体可能是请求的文件、错误消息、或者其它一些信息。HTTP使用TCP而不是UDP的原因在于(打开)一个网页必须传送很多数据,而TCP协议提供传输控制,按顺序组织数据,和错误纠正。

######HTTP服务器
通过浏览器,发送一个标准的HTTP请求,就能够得到一个标准的HTTP响应
项目中实现的是:通用的HTTP服务器框架来实现自己的业务

要实现的核心功能(需求分析(粗略))
  1. 能够接受标准的HTTP请求
  2. a)GET方法
  3. b)POST方法
  4. 能够根据请求作出响应一个标准的HTTP响应
  5. a)能够根据url返回一个服务器上的静态文件(html,css,JavaScript,图片等)
  6. b)根据请求中的参数(GET url,POST body)动态生成一个页面(//这个页面是基于CGI的方法,CGI可以理解成一个标准)
模块划分
  1. 初始化模块 //实现一个TCP服务器
  2. 响应请求模块 //使用多线程的方式处理并发的请求
    a)读取请求并解析 //操作和解析字符串(反序列化)
    b)根据请求内容进行计算
    i)处理静态文件 //直接将静态文件的内容返回
    ii)处理动态页面 //使用CGI的方式实现动态计算生成页面
    c)把响应的结果写回给客户端 //操作和拼接字符串

请求报文的构成:
这里写图片描述

响应报文的构成
这里写图片描述

#####告知服务器意图的HTTP方法
GET方法(获取资源)
用来请求访问已被URI识别的资源。指定的资源经服务器端解析后返回响应内容。
使用GET方法的请求·响应的例子
这里写图片描述
POST方法(传输实体主体)
POST的主要目的并不是获取响应的主体内容。
使用POST方法的请求·响应的例子
这里写图片描述

######返回结果的HTTP状态码
状态码类别

状态码类别原因短语
1XXInformation(信息性状态码)接收的请求正在处理
2XXSuccess(成功状态码)请求正常处理完毕
3XXRedirection(重定向状态码)需要进行附加操作以完成请求
4XXClient Error(客户端错误状态码)服务器无法处理请求
5XXServer Error(服务器错误状态码)服务器处理请求出错

//数字中的第一位指定了响应类别,后两位无分类

常用状态码:
200 OK 请求被正常处理
204 No Content 请求已成功处理,但响应报文中不含实体的主体部分
206 Partial Content 范围性请求且服务器成功执行
301 Moved Permanently 永久性重定向
302 Found 临时性重定向
303 See Other 使用GET方法定向获取请求资源
304 Not Modified 请求未满足
307 Temporary Redirect 临时重定向
400 Bad Request 请求报文中存在语法错误
401 Unauthorized 发送的请求需要有通过HTTP认证(BASIC认证、DIGEST认证)的认证信息
403 Forbidden 对请求资源的访问被服务器拒绝了
404 Not Found 服务器上无法找到请求的资源
500 Internal Server Error 服务器端在执行请求时发生了错误
503 Service Unavailable 服务器暂时处于超负载或正在进行停机维护,现在无法处理请求

#####CGI协议
CGI是外部应用程序(CGI程序)与WEB服务器之间的接口标准,是在CGI程序和Web服务器之间传递信息的过程。CGI规范允许Web服务器执行外部程序,并将它们的输出发送给Web浏览器,CGI将Web的一组简单的静态超媒体文档变成一个完整的新的交互式媒体。

1.收到的请求是一个(GET && 带有query_string )|| (POST) 触发CGI逻辑
2.创建一个子进程,子进程通过exec执行一个本地的可执行程序,通过这个可执行程序完成动态页面的计算
3.这里涉及到一个重要的问题:

  1. 子进程需要知道http请求具体长什么样子;
    需要知道进行替换哪个可执行文件
    需要知道HTTP请求的方法是什么 (环境变量METHOD )
    需要知道query_string是什么 (环境变量 QUERT_STRING )
    需要知道body是什么 (匿名管道)

  2. 子进程也要把计算完毕的结果反馈给父进程
    http响应的body ( 匿名管道 )
    部分header ( 匿名管道 ) (子进程把标准输入和标准输出重定向到管道上)

例如:1+2
收到的http请求可能是GET请求,query_string包含了1+2,也可能是一个POST请求,body中包含了1+2

子进程程序替换之后,CGI程序响应数据会这么写:
假设CGI程序返回的html页面是一个
这里写图片描述
以下内容是CGI程序所写回管道的内容(一部分header和一部分body)
这里写图片描述
如果要想仅仅使用管道完成进程间通信的话,要额外的约定协议。
此处的协议指的是,父进程按照啥样的格式组织数据并写入管道,以及子进程按照啥样的格式来解析。
如果以下图这种方式来组织数据的话,则在CGI程序中解析数据会更麻烦些,而如果我们使用环境变量配合管道两个双管齐下的话,子进程直接读取环境变量的内容就能获取方法和content-length,而直接从标准输入中读取数据就能够得到http请求中的body。
这里写图片描述

我在这个HTTP中实现一个简单的CGI程序:
根据请求中的输入数据,计算两个数字的和
两个要相加的数,通过query_string(GET)或者body(POST)来传递
例如:a=100&b=200

代码

http_server.cc

#include "http_server.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <sstream>

#include "util.hpp"


typedef struct sockaddr sockaddr;
typedef struct sockaddr_in sockaddr_in;

namespace http_server{


int HttpServer::Start(const std::string& ip,short port){
    int listen_sock = socket(AF_INET,SOCK_STREAM,0);
    if(listen_sock <0){
        perror("socket");
        return -1;
    }
    //要给socket加上一个选项,能够重用我们的连接
    int opt = 1;
    setsockopt(listen_sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));

    sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = inet_addr(ip.c_str());
    addr.sin_port = htons(port);
    int ret = bind(listen_sock,(sockaddr*)&addr, sizeof(addr));
    if(ret < 0){
        perror("bind");
        return -1;
    }
    ret = listen( listen_sock, 5);
    if(ret < 0){
        perror("listen");
        return -1;
    }
   // printf("ServerStart OK!\n");
   LOG(INFO)<< "SeverStart OK!\n";
   while(1){
       //基于多线程来实现一个TCP服务器
       sockaddr_in peer;
       socklen_t len = sizeof(peer);
       int new_sock = accept(listen_sock,(sockaddr*)&peer,&len);
       if (new_sock <0){
           perror("accept");
           continue;
       }
       //如果成功,创建新线程,使用新线程完成这次请求的计算
       Context* context = new Context();
       context->new_sock = new_sock;
       context->server = this;
       pthread_t tid;
       pthread_create(&tid,NULL,ThreadEntry,reinterpret_cast<void*>(context));
       pthread_detach(tid);
   }
   close(listen_sock);
   return 0;
}

//线程入口函数
void* HttpServer::ThreadEntry(void* arg){
    //准备工作
    Context* context = reinterpret_cast<Context*>(arg);
    HttpServer* server = context->server;
    //1.从文件描述符中读取数据,转换成Request对象
    int ret = 0;
    ret = server->ReadOneRequest(context);
    if(ret <0){
        LOG(ERROR) << "ReadOneRequest error!" << "\n";
        //用这个函数构造一个404的http Response 对象
        server->Process404(context);
        goto END;
    }
    //TODO TEST 通过以下函数将一个解析出来的请求打印出来
    server->PrintRequest(context->req);

    //2.把Resquest对象计算生成Response对象
    ret = server->HandlerRequest(context);
    if(ret <0){
        LOG(ERROR) << "HandlerRequest error!" << "\n";
       //用这个函数构造一个404的http Response 对象
        server->Process404(context);
        goto END;
    }
END:
    //TODO 处理失败的情况
    //3.把Response对象进行序列化,写回到客户端
    server->WriteOneResponse(context);
    //收尾工作
    close(context->new_sock);
    delete context;
    return NULL;                                                                                                                                        
}
//构造一个状态码为404 的Response对象
int HttpServer::Process404(Context* context){
    Response* resp = &context->resp;
    resp->code = 404;
    resp->desc = "Not Found";
    //用""可以续行
    resp->body = "<head><meta http-equiv=\"content-type\" content=\"text/html;charset=utf-8\"><h1>404!您的页面被喵星人吃掉了</h1>";
    std::stringstream ss;
    ss << resp->body.size();
    std::string size;
    ss >> size;
    resp->header["Content-Length"] = resp->body.size();
    return 0;
}

//从socket读取字符串,构造生成Request对象
int HttpServer::ReadOneRequest(Context* context){
    Request* req = &context->req;
    //1.从socket中读取一行数据作为Request的首行  按行读取的分隔符应该就是\n  \r  \r\n
    std::string first_line;
    FileUtil::ReadLine(context->new_sock,&first_line);
    //2.解析首行,获取到请求的method和url
    int ret = ParseFirstLine(first_line,&req->method,&req->url);
    if(ret < 0){
        LOG(ERROR) <<"ParseFirstLine error! first_line=" << first_line << "\n";
        return -1;
    }
    //3.解析url,获取到url_path,和query_string
    ret = ParseUrl(req->url,&req->url_path,&req->query_string);
    if(ret <0){
        LOG(ERROR) <<"ParseYrl error! url=" << req->url << "\n";
        return -1;
    }
    //4.循环的按行读取数据,每次读取到一行数据,就进行一个hander的解析
    //读到空行,说明header解析完毕
    std::string header_line;
    while(1){
        FileUtil::ReadLine(context->new_sock,&header_line);
        //如果 header_line 是空行,就退出循环
        //由于ReadLine返回的hander——line不包含n等分隔符
        //因此读到空行的时候,header_line就是空字符串
        if(header_line == ""){
          break;
        }
        ret = ParseHeader(header_line,&req->header);
        if(ret <0){
            LOG(ERROR) << "ParseHeader error! header_line=" << header_line << "\n";
            return -1;
        }
    }
    //5.如果是POST请求,并且header中包含了Content-Length字段
    Header::iterator it = req->header.find("Content-Length");
    if(req->method == "POST" && it == req->header.end()){
        LOG(ERROR) <<"POST Request has no Content-Length!\n";
        return -1;
    }
    //  如果是GRT请求,就不用读body
    if(req->method == "GET"){
        return 0;
    }
    
    //  如果是POST请求,但是没有Content-Length字段,认为这次请求失败
    //  继续读取socket,获取到body的内容
    int content_length = atoi(it->second.c_str());
    ret = FileUtil::ReadN(context->new_sock,content_length,&req->body);
    if(ret <0){
        LOG(ERROR) << "ReadN error! content_length=" << content_length << "\n";
        return -1;
    }
    return 0;
}

//解析首行,就是按照空格进行分割,分割成三个部分
//这三个部分分别是 方法,url,版本号
int HttpServer::ParseFirstLine(const std::string& first_line,std::string* method,std::string* url){
    std::vector<std::string> tokens;
    StringUtil::Split(first_line," ", &tokens);
    if(tokens.size() != 3){
        //首行格式不对
        LOG(ERROR) << "ParseFirstLine error! split error! first_line=" << first_line << "\n";
        return -1;
    }
    //如果版本号中不包含HTTP关键字,也认为出错
    if(tokens[2].find("HTTP") == std::string::npos){
        //首行的格式不对,版本信息中不包含HTTP关键字
        LOG(ERROR) << "ParseFirstLine error! version error! first_line=" << first_line << "\n";
        return -1;
    }
    *method = tokens[0];
    *url = tokens[1];
    return 0;
}

//https://www.baidu.com/s?wd=%E9%B2%9C%E8%8A%B1&rsv_spt=1&rsv_iqid=0x837a570
//解析一个标准的url,比较复杂,核心思路是以?作为分隔符,从?左边来查找url_path,从?右边来查找query_string
//我们此处只实现一个简化版本,只考虑不包含域名和协议的情况,以及#的情况
//例如:
// /s?wd=%E9%B2%9C%E8%8A%B1&rsv_spt=1&rsv_iqid=0x837a570
// 我们就单纯以?为分割,?左边的就是path,?右边的就是query_string
// /path/index.html  这种情况下没有?
int HttpServer::ParseUrl(const std::string& url, std::string* url_path, std::string* query_string){
    size_t pos = url.find("?");
    if(pos == std::string::npos){
        //没找到
        *url_path =url;
        *query_string = "";
        return 0;
    }
    //找到了
    *url_path = url.substr(0,pos);
    *query_string = url.substr(pos+1);
    return 0;
}

//此函数用于解析一行header
//此处的实现使用string::find来实现
//如果使用split的话,可能有如下问题:
//HOST:http://www.baidu,com
int HttpServer::ParseHeader(const std::string& header_line,Header* header){
    size_t pos = header_line.find(":");
    if(pos == std::string::npos){
        //找不到:说明header的格式有问题
        LOG(ERROR) << "ParseHeader error! has no : header-line=" << header_line << "\n";
        return -1;
    }
    //这个header的格式还是不正确,没有value
    if(pos + 2 >= header_line.size())
    {
        LOG(ERROR) << "ParseHeader error! has no value! header_line=" << header_line << "\n";
        return -1;
    }
    (*header)[header_line.substr(0,pos)] = header_line.substr(pos + 2);
    return 0;
} 

//该函数实现序列化,把Response对象转换成一个string
//写回到socket中
//此函数完全按照HTTP协议的要求来构造响应数据
//我们实现这个函数的细节可能有很大差异,但是只要能遵守HTTP协议那么就都是ok的
int HttpServer::WriteOneResponse(Context* context){
    //iostream和stringstream之间的关系类似于printf和sprintf之间的关系
    //1.进行序列化
    const Request& resp = context->resp;
    std::stringstream ss;
    ss << "HTTP/1.1" << resp.code <<" "<<resp.desc<<"\n";
    if(resp.cgi_resp == ""){
        //当前认为是在处理静态页面
        for(auto item : resp.header){
            ss << item.first << ":"<<item.second <<"\n";                                    
        }
        ss <<"\n";
        ss << resp.body;
    }else{
        //当前是在处理CGI生成的页面
        //cgi_resp同时包含了响应数据的header空行和body
        ss << resp.cgi_resp;
    }
    //2.将序列化的结果写入到socket中
    const std::string& str = ss.str();
    write(context->new_sock,str.c_str(),str.size());
    return 0;
}

//通过输入的Request对象计算生成Response对象
//1.支持静态文件
//a)GET 并且没有query_string作为参数
//2.动态生成页面(使用CGI的方式来动态生成)
//a)GET并且存在query_string作为参数
//b)POST请求
int HttpServer::HandlerResquest(Context* context){
    const Request& req = context->req;
    Response* resp = &context->resp;
    resp->code = 200;
    resp->desc = "OK";
    //判断当前的处理方式是按照静态文件处理还是动态生成
    if(req.method == "GET" && req.query_string == ""){
        return context->server->ProcessStaticFile(context);
                                                    
    }else if((req.method == "GET" && req.query_string != "")
            || req.method == "POST"){
        return context->server->PoressCGI(context);
                                                    
    }else{
        LOG(ERROR) << "Unsupport Method! method" << req.method << "\n";
        return -1;
                                                               
    }
    return -1;

}

//1.通过Request中的url_path字段,计算出文件在磁盘上的路径是什么
//例如url_path /index.html
//想要得到的磁盘上的文件就是 ./wwwroot/index.html
//注意,./wwwroot是我们此处约定的根目录
//2.打开文件,将文件中的所有内容读取出来放到body中
int HttpServer::ProcessStaticFile(Context* context)
{
    const Request& req = context->req;
    Response* resp = &context->resp;
    //1.获取到静态文件的完整路径
    std::string file_path;
    GetFilePath(req.url_path,&file_path);
    //2.打开并读取完整的文件
    int ret = FileUtil::ReadAll(file_path,&resp->body);
    if(ret < 0){
        LOG(ERROR) << "ReadAll error! file_path=" << file_path << "\n";
        return -1;
    }
    return 0;
}
//通过url_path找到对应的文件路径
//例如 请求url可能是 HTTP://192.168.80.132:9090/
//这种情况下 url_path是 /
//此时等价于请求/index.html
//例如 请求url可能是 HTTP://192.168.80.132:9090/image
//这种情况下 url_path是 / 
//如果url_path指向的是一个目录,就尝试在这个目录下访问一个叫做index.html的文件
void HttpServer::GetFilePath(const std::string& url_path,std::string* file_path)
{
    *file_path = "./wwwroot" +url_path;
    //判定一个路径是普通文件还是目录文件
    //1.linux的stat函数
    //2.通过boost filesystem模块进行判断
    if(FileUtil::IsDir(*file_path)/*如果当前文件是一个目录,就可以进行一个文件名拼接,拼接上index.html后缀*/)
    {
        //1./image/
        //2./image
        if(file_path->back() != '/'){
           file_path->push_back('/');
           }
        (*file_path) += "index.html";
    }
    return;
}

int HttpServer::ProcessCGI(Context* context){
    const Request& req = context->req;
    Response* resp = &context->resp;
    //1.创建一对匿名管道(父子进程要双向通信,因为匿名管道是半双工的)
    int fd1[2],fd2[2];
    pipe(fd1);
    pipe(fd2);
    int father_write = fd1[1];//父进程用来写的
    int child_read = fd1[0];
    int father_read = fd2[0];//父进程用来读的
    int child_write = fd2[1];
    //2.设置环境变量
    //  a)METHOD 请求方法
    std::string env = "REQUEST_METHOD" + req.method;
    putenv(const_cast<char*>(env.c_str()));
    if(req.method == "GET"){
        //  b)GET方法,QUERY_STRING 请求的参数
        env = "QUERY_STRING" + req.query_string;
        putenv(const_cast<char*>(env.c_str()));
    }else if(req.method == "POST"){
        //  c)POST方法,就设置CONTENT_LENGTH
        auto pos = req.header.find("Content-Length");
        env = "CONTENT_LENGTH=" + pos->second;
        putenv(const_cast<char*>(env.c_str()));
    }
    pid_t ret = fork();
    if(ret < 0){
        perror("fork");
        goto END;
    }
    if(ret > 0){
    //3.fock,父进程流程
    close(child_read);
    close(child_write);
    //  a)如果是POST请求,父进程就要把body写入到管道中
    if(req.method == "POST"){
    write(father_write, req.body.c_str(), req.body.size());
    }
    //  b)阻塞式的读取管道,尝试吧子进程的结果读取出来,并且放到Response对象中
    FileUtil::ReadAll(father_read, &resp->cgi_resp);
    //  c)对子进程进行进程等待(为了避免僵尸进程)
    wait(NULL);
    }else{
    //4.fock,子进程流程
    close(father_write);
    close(father_read);
    //  a)把标准输入和标准输出进行重定向
    dup2(child_read,0);
    dup2(child_write,1);
    //  b)先获取到要替换的可执行文件是那个(通过url_path来获取)
    std::string file_path;
    GetFilePath(req.url_path, &file_path);
    //  c)进行进程的程序替换
    execl(file_path.c_str(),file_path.c_str(),NULL);
    //  d)由我们的CGI可执行程序完成动态页面的计算,并且写回数据到管道中
    //这部分逻辑,我们需要放到另外的单独的文件中实现,并且根据该文件编译生成我们的CGI可执行程序
    }
END:
    //TODO   统一处理收尾工作
    close(father_read);
    close(father_write); 
    close(child_read); 
    close(child_write); 
    return 0;
}

////////////////////////////////////////////////////
//分割线,以下为测试用函数
///////////////////////////////////////////////////
void HttpServer::PrintRequest(const Request& req)
{
    LOG(DEBUG) << "Request:" << "\n" << req.method << " " << req.url << "\n" << req.url_path << " " << req.query_string << "\n";
    //for(Header::const_iterator it = req.header.begin();it != req.header.end();++it)
    //这种麻烦
    //C++ 11 range based for 基于区间的循环
    for(auto it : req.header)
    {
        //LOG(DEBUG) << it ->first << ":" << it->second << "\n";
        LOG(DEBUG) << it.first << ":" << it.second << "\n";
    }
    LOG(DEBUG) << "\n";
    LOG(DEBUG) << req.body << "\n";
}             


}// end http_server

http_server.h

#pragma once
#include <string>
#include <unordered_map>
                                                                                                                                                          
namespace http_server{
 
typedef std::unordered_map<std::string,std::string> Header;

  
//请求结构
struct Request{
    std::string method;//请求方法 GET/POST
    std::string url;
  //例如url为一个形如 http://www.baidu.com/index.html?kwd="cpp"
    std::string url_path;  //路径 如 index.html
    std::string query_string;  // kwd="cpp"
    //std::string version;   //暂时先不考虑版本号
    Header header; //一组字符串键值对
    std::string body; //http的请求body
  };                                 
//响应结构
struct Response {
    int code;     //状态码
    std::string desc;  //状态码描述
    //std::string version;  //暂不考虑版本号
    //下面这两个变量专门给处理静态页面时使用的。当前请求如果是请求静态页面,这两个字段会被填充,并且cgi_resp字段为空
    Header header;  //响应报文中的header数据
    std::string body;   //响应报文中的body数据
    //下面这个变量专门给CGI来使用的,并且如果当前请求是CGI的话,cgi_resp就会被CGI程序进行填充,并且header和body这两个字段为空
    std::string cgi_resp;//CGI程序返回给父进程的内容,包含了部分header和body
                         //引入这个变量,是为了避免解析CGI程序返回的内容,因为这部分内容可以直接写到socket中
    };
                                                                      
//当前请求的上下文。包含了这次请求的所有需要的中间数据
//方便进行扩展。整个处理请求的过程中,每一个环节都能够拿到所有和这次请求相关的数据

 struct Context {
     Request req;
     Response resp;
     int new_sock;
     HttpServer* server;
    };
                                                                                 
    //实现核心流程的类
class HttpServer{
public:
    //以下的几个函数返回0表示成功,返回小于0的值表示执行失败
    int Start(const std::string& ip,short port);   //服务器启动

    //根据HTTP请求字符串,进行反序列化。从socket 读取一个字符串,输出是一个Request对象
    int ReadOneRequest(Context* context); 
    //根据Response对象,拼接成一个字符串,写回到客户端
    int WriteOneRequest(Context* context);
    //根据Request对象,构造Response对象
    int HeadlerRequest(Context* context);                                                                                                                
    int Process404(Context* context);
    int ProcessStaticFile(Context* context);
    int ProcessCGI(Context* context);
private:
    static void* ThreadEntry(void* arg);
    int ParseFirstLine(const std::string& first_line,std::string* method,std::string* url);
    int ParseUrl(const std::string& url, std::string* url_path, std::string* query_string);
    int ParseHeader(const std::string& header_line,Header* header);
    void GetFilePath(const std::string& url_path,std::string* file_path);
    //下面为测试函数
    void PrintRequest(const Request& req);
};
 

}// end http_server

http_server_main.cc

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

using namespace http_server;

int main(int argc,char* argv[]){
    if(argc != 3)
    {
        std::cout << "Usage ./server [ip][port]" << std::endl;
        return 1;
    }
    HttpServer server;
    server.Start(argv[1],atoi(argv[2]));
}

http_server_test.cc

#include "http_server.h"
#include <iostream>
using namespace http_server;

int main(){
    HttpServer server;
    int ret = server.Start("0",9090);
    std::cout << "ret :"<< ret << std::endl;
}

until.hpp

//////////////////////////////////////////////////////////
//此文件放置一些工具类和函数
//为了让这些工具用的方便,直接把声明和实现都放在.hpp中
/////////////////////////////////////////////////////////
#pragma once
#include <iostream>
#include <fstream>
#include <unordered_map>
#include <boost/algorithm/string.hpp>
#include <boost/filesystem.hpp>
#include <vector>
#include <sys/time.h>
#include <sys/socket.h>

class TimeUtil{
public:
    //获取到当前的秒级时间戳
    static int64_t TimeStamp(){     
        struct timeval tv;
        gettimeofday(&tv,NULL);
        return tv.tv_sec;
    }
    //获取当前的微秒级时间戳
    static int64_t TimeStampUS(){
        struct timeval tv;
        gettimeofday(&tv,NULL);
        //123456
        return 1000*1000*tv.tv_sec +tv.tv_usec;
    }
};

enum LogLevel{
    DEBUG,
    INFO,     //一般的信息
    WARNING,   //警告
    ERROR,     //一般错误
    CRITIAL,    //致命错误
};


inline std::ostream& Log(LogLevel level,const char* file,int line){
    std::string prefix ="I";
    if(level == WARNING){
        prefix ="W";
    }else if(level == ERROR){
        prefix ="E";
    }else if(level == CRITIAL){
        prefix = "C";
    }else if(level == DEBUG){
        prefix = "D";
    }
    std::cout <<"[" << prefix << TimeUtil::TimeStamp() << "" << file << ":" << line << "]";
    return std::cout;
}

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


class FileUtil{
public:
    //从文件描述符中读取一行
    //一行的界定标识是\n  \r  \r\n
    //返回的line中是不包含界定标识的
    //例如:
    //aaa\nbbb\nccc
    //调用ReadLine返回的line对象的内容为aaa,不包含\n
    static int ReadLine(int fd,std::string* line){
        line->clear();
        while(true){
            char c = '\0';
            ssize_t read_size = recv(fd, &c, 1, 0);//recv用法和read相似
            if(read_size <= 0){
                return -1;
            }
            if (c == '\n'){
                break;
            }
            //如果当前字符是\r\,把这个情况处理成\n
            if(c == '\r'){
                //虽然从缓冲区读了一个字符,但是缓冲区并没有把它删掉
                recv(fd, &c,1,MSG_PEEK);
                if(c == '\n'){
                    //发现\r后面一个字符刚好就是\n,为了不影响下次循环就需要把这样的字符从缓冲区中干掉
                    recv(fd, &c, 1, 0);
                    break;
                }else{     //如果不是\n则强制设置成\n
                    c = '\n';
                }
            }
            //这个条件涵盖了\r和\r\n的情况
            if (c == '\n'){
                break;            
            }
            line->push_back(c);
        }
        return 0;
    }

    static int ReadN(int fd,size_t len,std::string* output){
        output->clear();
        char c = '\0';
        for(size_t i= 0;i < len;++i)
        {
            recv(fd,&c,1,0);
            output->push_back(c);
        }
        return 0;
    }


    //从文件中读取全部内容到 std::string中
    static int ReadAll(const std::string& file_path,std::string* output)
    {
        std::ifstream file(file_path.c_str());
        if(!file.is_open()){
            LOG(ERROR) << "Open file error! file_path=" << file_path << "\n";
            return -1;
        }
        file.seekg(0,file.end);//seekg调整文件指针的位置,此处将文件指针调整到文件末尾
        int length = file.tellg(); //查询当前文件指针的位置,返回值就是文件指针位置相对于文件起始位置的偏移量
        //为了从头读取文件,需要把文件指针设置到开头位置
        file.seekg(0,file.beg);
        //读取完整的文件内容
        output->resize(length);//开辟内存,开辟保证能存下length的长度
        file.read(const_cast<char*>(output->c_str()),length);
        //万一忘记写close,问题不大,因为ifstream对象会在析构的时候自动的进行关闭文件描述符
        file.close();
        return 0;
    }

    static int ReadAll(int fd,std::string* output){
        //TODO 此函数完成从文件描述符中读取所有数据的操作
        while(true){
            char buf[1024] = {0};
            ssize_t read_size = read(fd, buf, sizeof(buf) - 1);
            if(read_size < 0){
                perror("read");
                return -1;
            }
            if(read_size == 0){
                //文件读完了
                return 0;
            }
            buf[read_size] = '\0';
            (*output) += buf;
        }
        return 0;
    }

    static bool IsDir(const std::string& file_path)
    {
        return boost::filesystem::is_directory(file_path);
    }
};

class StringUtil{
public:
    //把一个字符串,按照split_char进行切分,分成的N个子串,放到output数组中
    //token_compress_on 的含义是:
    //例如分隔符是空格,字符串就是"a  b"(中间有两个空格)
    //对于打开压缩情况,返回的子串就是有两个,"a","b"
    //token_compress_off
    //对于关闭压缩的情况,返回的子串就是有三个,"a"," ","b"
    static int Split(const std::string& input,const std::string& split_char,std::vector<std::string>* output){
        boost::split(*output,input,boost::is_any_of(split_char),boost::token_compress_on);
        return 0;
    }
    
    typedef std::unordered_map<std::string, std::string> UrlParam;
    static int ParseUrlParam(const std::string& input, UrlParam* output){
        //1.按照取地址符号切分成若干个kv
        std::vector<std::string> params;
        Split(input, "&", &params);
        //2.再针对每一个kv,按照 = 切分,放到输出结果中
        //range based for
        for (auto item : params){
            std::vector<std::string> kv;
            Split(item, "=", &kv);
            if(kv.size() != 2){
                //说明该参数非法
                LOG(WARNING) << "kv format error! item=" << item << "\n";
                continue;
            }
            (*output)[kv[0]] = kv[1];
        }
        return 0;
    }
};

cgi_main.cc

#include <iostream>
#include <string>
#include <sstream>
#include "until.hpp"

void HttpResponse(const std::string& body){
    std::cout << "Content-Length:" << body.size() << "\n";
    std::cout << "\n";    //这个\n是http协议中的空行
    std::cout << body;
    return;
}
//这个代码生成的就是我们的CGI程序,通过这个CGI程序完成具体的业务
//本CGI程序仅仅完成简单的两个数的加法运算
int main(int argc,char* argv[], char* env[]){
    size_t i =0;
    for(;env[i] != NULL;++i){
        std::cerr << "env:" << env[i] << "\n";
    }
    //1.先获取到method
    const char* method = getenv("REQUEST_METHOD");
    if(method == NULL){
        HttpResponse("No env REQUEST_METHOD!");
        return 1;
    }
    StringUtil::UrlParam params;
    if(std::string(method) == "GET"){
    //2.如果是GET请求,从QUERY_STRING读取请求参数 
    //4.解析query_string或者body中的数据 
        const char* query_string = getenv("QUERY_STRING");
        StringUtil::ParseUrlParam(query_string,&params);
    }else if(std::string(mothod) == "POST"){
    //3.如果是POST请求,从CONTENT_LENGTH读取body的长度
    //  根据body的长度,从标准输入中读取请求的body
    //4.解析query_string或者body中的数据
    const char* content_length = getenv("CONTENT_LENGTH");
    char buf[1024 *10] = {0};
    read (0.buf,sizeof(buf) - 1);
    StringUtil::ParserlParam(buf,&params);
    //5.根据业务需要进行计算,此处的计算是a+b的值
    int a = std::stoi(params["a"]);
    int b = std::stoi(params["b"]);
    int result = a + b;
    //6.根据计算结果,构造响应的数据,写回到标准输出中
    std::stringstream ss;
    ss << "<hl> result = " << result << "</hl>";
    HttpResponse(ss.str());
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值