【项目】HTTP服务器

article/2025/9/18 18:32:17

⭐️ 本博客介绍的是一个自主实现HTTP服务的一个项目,这要介绍的是项目实现的整个过程,用到的技术、遇到的问题以及都是如何解决的。想完成该项目,需要我们对HTTP有了解,这里可以查看我的往期博客——HTTP协议。这里还会用到流式套接字,也可以翻阅我的往期博客进行查看——流式套接字。下面就开始正式介绍~
⭐️ 项目代码:https://gitee.com/byte-binxin/http-project

目录

  • 项目介绍
  • 开发环境
  • 主要技术
  • 项目框架图
  • 项目演示
  • 项目实现
    • 项目文件部署
    • 打印日志
    • 组件模块
      • TcpServer类
      • ThreadPool类
    • 任务类
    • Util类
    • HttpServer类
    • 协议模块
      • 协议请求类
      • 协议响应类
      • EndPoint类
        • 框架
        • 设计回调
        • 读取请求
          • 读取请求行
          • 读取请求报头
          • 分析请求报头
          • 分析请求正文
          • 读取请求正文
        • 构建响应
          • 主体
          • 非cgi处理
          • cgi机制处理
          • 根据状态码构建响应
            • 构建正确响应
            • 构建错误响应
        • 发送响应
    • 部署cgi程序
      • 简单的计算程序
      • 用户数据存储程序
  • Postman测试
    • GET方法不带参数
    • GET方法带参数
    • POST方法
  • 项目迭代
  • 项目总结


项目介绍

该项目采用B/S模型,从零开始编写支持中小型应用的http,并结合mysql。整个项目服务器大体分为客户端建立连接,读取分析请求、处理请求、构建响应、构建响应几个部分。该服务器能够根据用户的请求返回简单的静态网页和动态网页,应对处理常见的错误请求。此外为了能够处理客户端发起的请求,在HTTP服务器提供的平台上搭建了CGI机制,CGI机制可以处理HTTP 的一些数据请求,并作出相应的处理。为了能够让项目更加完善,我在该服务器之上增加了一个登录和注册模块,结合mysql存储用户数据,并且部署了一个简单的计算器服务。

开发环境

  • Centos7.6、C/C++、vim、g++、Makefile、Postman

主要技术

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

项目框架图

在这里插入图片描述

项目演示

服务器启动,绑定一个8081的端口号运行,如下:

在这里插入图片描述

服务器启动后,使用浏览器进行访问,获取到一个登录页面:
在这里插入图片描述

请求的日志信息:

在这里插入图片描述

登录后成功后就会返回一个计算器页面,同时服务器后台也会进行核对:

在这里插入图片描述

后台打印的日志信息:

在这里插入图片描述

当然这个项目的核心在服务器处理HTTP协议细节分析和处理上,上面演示的都是服务器正常处理的情况,一些错误请求都能够正确处理,后面我们再详谈。

项目实现

项目文件部署

在这里插入图片描述

  • main.cc:用来编译整个项目,启动服务器
  • TcpServer.hpp:存放单例TcpServer类,使用SockAPI和单例模式编写一个TcpServer,成为一个独立的组件,插入HttpServer中进行使用
  • ThreadPool.hpp:存放单例ThreadPool类,使用POSXI线程库的线程、互斥量和条件变量编写一个单例模式的线程池,也作为一个独立的组件,插入HttpServer中使用
  • Task.hpp:存放任务类,用来将每一个连接封装成任务,里面有对应的回调机制,可以执行任务
  • HttpServer.hpp:存放HttpServer类,该类调用TcpServer和ThreadPool组件,每次获取一个连接都将其封装成为一个任务,并且放入线程池中进行处理
  • Protocol.hpp:存放一些HTTP协议处理的类,协议请求类,协议响应类,协议处理类,还包括一个回调类,可以调用前面三个类中的成员方法,供任务类使用
  • Util.hpp:存放工具类,该类提供了一些字符串处理的方法,方便我们使用
  • Log.hpp:存放打印日志函数,可以帮我打印日志
  • cgi目录:该目录下可以存放cgi程序,供服务器调用
  • wwwroot目录:这是服务器的web根目录,里面存放了一些网页资源和cgi程序,服务器启动自动从该目录下进行路径搜索资源

打印日志

为了方便后期编码调试和项目演示,这里设计了一个日志打印函数,日志打印的格式如下:

在这里插入图片描述

日志的四个级别:

  • INFO:正常信息
  • WARNING:警告信息
  • ERROR:错误信息
  • FATAL:致命信息

我们可以将它们定义为四个宏:

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

时间戳可以通过time库函数进行获取,错误文件名称和错误行分别通过可通过分别通过__FILE____LINE__两个宏进行获取,于是我们就可以写出一个日志函数:

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

上面的这一个函数用到了四个参数,每次调用传四个参数会显得比较麻烦,且后面两个参数是比较固定的,所以为了方便,这里采用一个宏来封装该函数,如下:

// 替换,方便调用日志打印函数  # 数字转宏字符串
#define LOG(level, message) Log(#level, message, __FILE__, __LINE__)

这样,以后调用日志函数就只需要调用该宏即可,__FILE____LINE__两个宏变量都会在所替换的文件中进行替换。

看下面的一个打印效果:
在这里插入图片描述

组件模块

TcpServer类

该类的编写主要用到的是SocketAPI,主要过程为:

  • 创建套接字
  • 绑定端口号
  • 将套接字设置为监听状态

成员变量:

  • port:端口号
  • listen_sock:监听套接字
  • svr:单例TcpServer
  • cg:内嵌垃圾回收类

成员方法:

  • InitServer:服务器初始化
  • GetInstance:单例获取方法

代码实现:

#define BACKLOG 5class TcpServer
{
public:void InitServer(){Socket();Bind();Listen();LOG(INFO, "TcpServer Init Success");}static TcpServer* GetInstance(int port){static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;// 静态的锁,不需要destroy if (_svr == nullptr){pthread_mutex_lock(&lock);if (_svr == nullptr){_svr = new TcpServer(port);_svr->InitServer();}pthread_mutex_unlock(&lock);}return _svr;}class CGarbo{public:~CGarbo(){if (TcpServer::_svr == nullptr){delete TcpServer::_svr;}}};int GetListenSock(){return _listen_sock;}~TcpServer(){if (_listen_sock >= 0) close(_listen_sock);}
private:// 构造私有TcpServer(int port):_port(port),_listen_sock(-1){}// 禁止拷贝TcpServer(const TcpServer&) = delete;TcpServer& operator=(const TcpServer&) = delete;void Socket(){_listen_sock = socket(AF_INET, SOCK_STREAM, 0);if (_listen_sock < 0){LOG(FATAL, "create socket error!");exit(1);}LOG(INFO, "create socket success");// 将套接字设置为可以地址复用int opt = 1;setsockopt(_listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));}void Bind(){struct 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;if (bind(_listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0){LOG(FATAL, "bind error!");exit(2);}LOG(INFO, "bind success");}void Listen(){if (listen(_listen_sock, BACKLOG) < 0){LOG(FATAL, "listen error!");exit(3);}LOG(INFO, "lieten success");}
private:int _port;int _listen_sock;static TcpServer* _svr;// 单例static CGarbo _cg;// 内嵌垃圾回收
};TcpServer* TcpServer::_svr = nullptr;
TcpServer::CGarbo _cg;

说明几点:

  • 这里获取单例的方法中用到的互斥量使用PTHREAD_MUTEX_INITIALIZER字段进行初始化,这样的好处就是改互斥量出来作用域可以自动销毁,更加方便
  • 更详细的这部分内容可以查看往期博客,有更详细介绍

ThreadPool类

该项目频繁获取连接,需要派出一个线程去处理相应的任务,如果每次来一个连接就去创建一个线程,断开连接就销毁线程的话,这样对操作系统开销比较大,同时也会带来一定的负担。如果使用线程池的话,来一个任务就立即处理,不需要去创建线程,这样就节省了创建线程时间,同时也可以防止服务器线程过多导致操作系统过载的问题。

该类用到了POSIX线程库的一套接口,成员变量有:

  • q:任务队列
  • num:线程池线程个数
  • lock:互斥量
  • cond:条件变量
  • tp:单例线程池
  • cg:内嵌垃圾回收类(析构时回收单例资源)

成员方法:

  • InitThreadPool:初始化线程池
  • GetInstance:获取单例线程池
  • Routine:线程执行方法
  • Put:放任务
  • Get:取任务

代码实现:

#define NUM 5class ThreadPool
{
private:ThreadPool(int max_pthread = NUM):_stop(false),_max_thread(max_pthread){}ThreadPool(const ThreadPool&) = delete;ThreadPool& operator=(const ThreadPool&) = delete;
public:static ThreadPool* GetInstance(int num = NUM){static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;// 静态的锁,不需要destroy if (_tp == nullptr){pthread_mutex_lock(&lock);if (_tp == nullptr){_tp = new ThreadPool(num);if (!_tp->InitThreadPool())exit(-1);}pthread_mutex_unlock(&lock);}return _tp;}class CGarbo{public:~CGarbo(){if (ThreadPool::_tp == nullptr){delete ThreadPool::_tp;}}};static void* Runtine(void* arg){pthread_detach(pthread_self());ThreadPool* this_p = (ThreadPool*)arg;while (1){this_p->LockQueue();// 防止伪唤醒使用whilewhile (this_p->IsEmpty()){this_p->ThreadWait();}Task* t;this_p->Get(t);this_p->UnlockQueue();// 解锁后处理任务t->ProcessOn();delete t;// 任务统一在堆上创建}}bool InitThreadPool(){pthread_mutex_init(&_mutex, nullptr);pthread_cond_init(&_cond, nullptr);pthread_t t[_max_thread];for(int i = 0; i < _max_thread; ++i){if (pthread_create(t + i, nullptr, Runtine, this) != 0){LOG(FATAL, "ThreadPool Init Fail!");return false;}}LOG(INFO, "ThreadPool Init Success");return true;}void Put(Task* data){LockQueue();_q.push(data);UnlockQueue();WakeUpThread();}void Get(Task*& data){data = _q.front();_q.pop();}~ThreadPool(){pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_cond);}
public:void LockQueue(){pthread_mutex_lock(&_mutex);}void UnlockQueue(){pthread_mutex_unlock(&_mutex);}void ThreadWait(){pthread_cond_wait(&_cond, &_mutex);}void WakeUpThread(){pthread_cond_signal(&_cond);//pthread_cond_broadcast(&_cond);}bool IsEmpty(){return _q.empty();} 
private:std::queue<Task*>  _q;bool _stop;int             _max_thread;pthread_mutex_t _mutex;pthread_cond_t  _cond;static ThreadPool* _tp;static CGarbo cg;// 内嵌垃圾回收
};

任务类

每一个获取到的连接都可以封装称为一个任务类,然后放进线程池中,让线程池中的线程取出然后执行对应的方法。

成员变量:

  • sock:获取到的连接的套接字
  • handlerRequest:处理任务的回调机制(该类在Protocol.hpp中进行了编写)

成员方法:

  • handlerRequest:处理任务

代码实现:

class Task
{
public:Task(int sock):_sock(sock){}// 处理任务void ProcessOn(){_handlerRequest(_sock);}
private:int _sock;CallBack _handlerRequest;// 设置回调,处理请求与构建响应
};

Util类

工具类主要提供了一些分析协议时会用到的字符串处理的方法,这里写了两个:

  • ReadLine

我们都知道,HTTP协议用行的方式来陈列协议内容,其中不同的浏览器下行分隔符的表示方式是不一样的,一般有下面三种:

  1. \r\n
  2. \r
  3. \n

所以为了方便分析协议,我们可以在读取协议的每一行时候都将其行分隔符进行统一处理,统一转为\n的形式。所以这里设计了一个ReadLine的方法进行处理。

思路如下:

  1. 该函数从sock中读取一行协议内容,然后将行分割符进行处理,然后返回,所以这里使用两个参数:sock、out转换之后的一行)
int ReadLine(int sock, std::string& out);
  1. 逐个读取字符,如果不是\r\n,就直接将该字符加入out中。如果此时是\r,那么改行分隔符可能是\r或·\r\n,所以接下来读取的字符可能是\n或下一行的其它字符,所以此时需要根据下一个字符判断是哪一种情况,如果此时直接使用recv读取下一个字符,会将缓冲区的字符拷贝到上层,这样对下一次读取一行很不利。能不能放回去能?这是一个很麻烦的事情,所以有没有一种方法能够让只查看下一个字符而不拷走的方法呢?答案是有的,我们可以调整recv的选项字段,选择MSG_PEEK选项,只读不拷走下一个字符,所以这里我们选择使用MSG_PEEK选项进行窥探

在这里插入图片描述

​ 如果下一个字符为\n,代表该协议的行分隔符是\r\n类型的,所以我们将该字符读走,否则我们直接把要添加的字符改成\n

if (ch == '\r'){// 使用MSG选项进行窥探,不取走接受缓冲区的数据 recv(sock, &ch, 1, MSG_PEEK);if (ch == '\n'){// 情况1// 窥探成功,将该数据从接受缓冲区取走recv(sock, &ch, 1, 0);}else{// 情况2ch = '\n';}
}
  1. 最后处理完上面两种情况之后,接下来检验ch这个字符,如果是\n,就将该字符添加至out,并停止读取,返回out的大小

整个函数代码如下:

static int ReadLine(int sock, std::string& out)
{char ch = '*';while (ch != '\n'){ssize_t sz = recv(sock, &ch, 1, 0);//std::cout << "debug: " << sz << " " << ch << " " << __LINE__ << std::endl;if (sz > 0){// 三种情况都转为\n// 1. \r\n// 2. \n// 3. \rif (ch == '\r'){// 使用MSG选项进行窥探,不取走接受缓冲区的数据 recv(sock, &ch, 1, MSG_PEEK);if (ch == '\n'){// 情况1// 窥探成功,将该数据从接受缓冲区取走recv(sock, &ch, 1, 0);}else{// 情况2ch = '\n';}}// 正常或者转换后out += ch;}else if (sz == 0){return 0; }else{return -1;}}return out.size();
}
  • CurString

    我们都知道,HTTP报头中的信息是以key:value的方式行陈列出来的,所以我们需要将其进行解析,分割成两个字符串。所以这里实现了一个简单的字符串分割方法:

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

HttpServer类

http服务器类在启动时,会将TcpServer和ThreadPool加载进HttpServer,http服务主要负责获取连接,并将器封装成任务,然后放进线程池中进行任务处理。

成员变量:

  • port:绑定端口号

  • stop:停止运行标志位

成员方法:

  • InitServer:初始化服务器
  • Loop:运行服务器

代码实现:

#define PORT 8081class HttpServer
{
public:HttpServer(int port = PORT, int num = NUM):_port(port),_num(num){}void InitServer(){signal(SIGPIPE, SIG_IGN);//忽略该信号,否则服务器写入时,对端关闭会发送该信号导致服务器关闭}void Loop(){LOG(INFO, "Loop Begin");// 两个组件TcpServer* tsvr = TcpServer::GetInstance(_port);ThreadPool* tp  = ThreadPool::GetInstance(_num);int lsock = tsvr->GetListenSock();struct sockadd_in* peer;socklen_t len = sizeof(peer);while (!_stop){int sock = accept(lsock, (struct sockaddr*)&peer, &len);if (sock < 0){continue;}LOG(INFO, "Get a new link");// 构建任务Task* t = new Task(sock);tp->Put(t);}LOG(INFO, "Loop End");}
private:int _port;int _num;static bool _stop;
};

注意: 服务器初始化的时候,将SIGPIPE设置为SIG_IGN,忽略该信号。考虑到服务器给客户端发送响应时,也就是往sock写入时,客户端关闭连接,此时操作系统会向服务器发送SIGPIPE信号终止服务器,导致服务器崩溃,这样显然是不行的,所以我们选择在服务器初始化的时候就忽略该信号。

协议模块

协议请求类

下面是HTTP请求协议内容:

在这里插入图片描述

整个项目主要分析GET和POST请求,需要知道的是,GET方法可以获取服务器资源,也可以上传数据,且是通过uri中’?'后面的参数进行提交。POST是直接通过正文上传数据,且报头中有Content-Length字段标识正文的长度大小。

协议请求类主要用来存放描述请求类的一些成员,必须要有的四个(HTTP请求协议的四个内容):

  • request_line:请求行
  • request_header:请求报头:使用一个vector存放请求报头的属性行,分行存储
  • blank:空行
  • request_body:请求报头(根据请求方法进行获取,POST方法就进行读取)

下面三个成员变量是存放报头解析处理的三个属性:

  • method:请求方法
  • uri:请求资源
  • version:HTTP版本

下面四个成员变量是用来存储请求报头的几个属性:

  • http_header_kv:哈希表存储报头中的key:value
  • content-length:如果请求方法是POST,就用该成员000存储正文长度
  • uri_path:uri带参数就要从uri中分离出path填充该变量
  • uri_query:POST方法不用填充该变量,GET方法需要分带参和不带参两种情况填充该参数

最后一个成员变量标识是否启用cgi机制:

  • cgi:只有上传了数据,都需要使用cgi机制进行处理,或者说是请求资源是一个可执行程序,也需要启动cgi

代码实现:

class HttpRequest
{
public:std::string _request_line;std::vector<std::string> _request_header;std::string _blank;std::string _request_body;// 解析后std::string _method;// 请求方法std::string _uri;// 资源标识std::string _version;// HTTP版本// 存储报头中的k: vstd::unordered_map<std::string, std::string> _http_header_kv;int _content_length = 0;// 正文长度std::string _uri_path;std::string _uri_query;// ?后面的参数bool _cgi = false;// 是否需要使用cgi模式  GET方法带参数  POST方法
};

协议响应类

下面是HTTP响应协议的内容:
在这里插入图片描述

根据响应协议内容定制的四个成员变量:

  • status_line:状态行
  • response_header:响应报头
  • blank:空行
  • response_body:响应正文

状态行可以要填充三个内容:版本、状态码、状态码描述,我们只需要设置状态码即可,第一填充的是HTTP/1.0,状态码根据执行情况进行填充,状态码描述根据对应状态码返回html文件。所以这里需要四个成员变量(具体设计原因下个板块介绍):

  • status_code:状态码
  • fd:要返回资源的文件描述符
  • body_size:返回资源的大小
  • suffix:文件后缀

代码实现:

class HttpResponse
{
public:std::string _status_line;std::vector<std::string> _response_header;std::string _blank = LINE_END;// #define LINE_END "\r\n"std::string _response_body;// _status_line: version status_code status_desc 状态描述int _status_code = OK;int _fd = -1;// 最终要打开的文件int _body_size = 0;// 文件的大小std::string _suffix = ".html";// 文件后缀
};

EndPoint类

框架

端点类主要是做这几件事:读取请求、分析请求、处理请求、构建响应、返回响应。

四个成员变量:

  • sock:与对端进行通信的套接字
  • stop:停止标志位
  • http_request:http请求
  • http_reponse:http响应

主要的成员方法:

  • RecvHttpRequest:读取HTTP协议请求
  • BuildHttpResponse:构建HTTP协议响应
  • SendHttpResponse:发送HTTP协议响应

处理每一个节点时,都只需要依次调用上面的三个方法,注意这里有一个停止标志位,是为了应对读取请求过程中发送错误,这时可以将该标志位设置为true,后面根据该标志位判断是否需要继续构建响应和发送响应。

代码实现:

// 读取请求,分析请求,构建响应
// IO通信
class EndPoint
{
public:EndPoint(int sock):_sock(sock){}void RecvHttpRequest();void BuildHttpResponse();void SendHttpResponse();bool IsStop(){return _stop;}~EndPoint(){if (_sock >= 0) close(_sock);}
private:int _sock;bool _stop = false;HttpRequest _http_request;HttpResponse _http_response;
};

设计回调

设计一个回调,龚任务类使用,该回调主要是处理HTTP请求和响应,顺序调用EndPoint类中三个方法,调用完读取协议方法后,需要对stop标志位进行判断,正常读取就继续构建响应,错误就不构建响应。

代码实现:

class CallBack
{
public:void operator()(int sock){HandlerRequest(sock);}void HandlerRequest(int sock){LOG(INFO, "Handler Request Begin");EndPoint* ep = new EndPoint(sock);ep->RecvHttpRequest();if (!ep->IsStop()){LOG(INFO, "Recv No Error, Begin Build And Send");ep->BuildHttpResponse();ep->SendHttpResponse();}else{LOG(WARNING, "Recv Error, Stop Build And Send");}delete ep;LOG(INFO, "Handler Request End");close(sock);}
};

读取请求

读取协议的主要几个步骤:

  1. 读取请求行
  2. 读取请求报头
  3. 分析请求行
  4. 分析请求报头
  5. 读取请求正文

逻辑: 1或2读取失败,后续三个步骤都可以不执行,如果成功就继续执行后三个步骤

代码实现:

void RecvHttpRequest()
{if (RecvHttpRequestLine() && RecvHttpRequestHeader()){ParseHttpRequestLine();ParseHttpRequestHeader();// 读取正文RecvHttpRequestBody();}
}
读取请求行

请求行只有一行,可以调用一次工具类中的ReadLine接口进行读取,而且还能够处理行分割,统一转为\n。如果读取是发生错误,就将停止标志位设置为true,并且返回false

代码实现:

bool RecvHttpRequestLine()
{std::string& line = _http_request._request_line;ReadLine(_sock, line);if (line.size() > 0) line.pop_back();//去掉结尾的'\n'符号else_stop = true;return !_stop;
}
读取请求报头

请求报头中有很多行,报头和报文是以空行进行分割的,所以我们只需要一行一行读取,直到读取到空行就可以停止读取。读取到的每一行全部放入大vector容器中,进行存放,这里也同样需要进行错误处理,读取失败就将停止标志位设置为true,并且返回false

代码实现:

bool RecvHttpRequestHeader()
{while (1){std::string line;if (Util::ReadLine(_sock, line) <= 0){_stop = true;break;}if (line == "\n"){// 空行_http_request._blank = line; break;}// 去掉换行line.pop_back();_http_request._request_header.push_back(line);}return !_stop;
}
分析请求报头

请求行分为请求方法、请求uri和HTTP版本三个部分,三者都在status_line存放,且都是空格分割,这里我们可以使用streamstring这个对象对字符串进行转换,提取出三个部分,如下:

std::stringstream ss(_http_request._request_line);
ss >> _http_request._method >> _http_request._uri >> _http_request._version;

为了方便处理,这里对请求方法字符串大小统一处理,都转为大写,这里选择使用transform这个函数进行转换

在这里插入图片描述

代码实现:

void ParseHttpRequestLine()
{// 解析 Get / HTTP/1.1std::stringstream ss(_http_request._request_line);ss >> _http_request._method >> _http_request._uri >> _http_request._version;auto& method = _http_request._method; // 将请求方法统一转化为大写std::transform(method.begin(), method.end(), method.begin(), toupper); 
}
分析请求正文

分析请求正文就是将vector中提取出字符串,进行’key: value分析,存入一个哈希表中。需要知道的是,解析每一个字符串时,分隔符是: ,所以这里调用工具类中的字符串切割方法进行分割字符串

代码实现:

void ParseHttpRequestHeader()
{std::string key;std::string value;for (auto& s : _http_request._request_header){// key: value if (Util::CutString(s, key, value, SEP))_http_request._http_header_kv.insert({key, value});}
}
读取请求正文

需要知道的是,只有请求方法是POST时,才需要读取请求正文,所以我们需要先判断是否需要读取请求报头,如果需要就提取出Content-Length字段,并对请求类中该成员进行填充,判断代码如下:

bool IsNeedRecvHttpRequestBody()
{auto& method = _http_request._method;if (method == "POST"){auto& header_kv = _http_request._http_header_kv;auto iter = header_kv.find("Content-Length");if (iter != header_kv.end()){_http_request._content_length = stoi(iter->second);return true;}}return false;
}

正式读取请求正文时,也需要进行读取错误处理判断,如果读取出错,也需要将停止标志位设置为true

代码实现:

void RecvHttpRequestBody()
{if (IsNeedRecvHttpRequestBody()){char ch;int content_length = _http_request._content_length;auto& http_request_body = _http_request._request_body;while (content_length){ssize_t sz = recv(_sock, &ch, 1, 0);if (sz > 0){http_request_body += ch;content_length--;}else if (sz == 0){// 客户端发送数据大小与Content-Length字段描述的不符_stop = true;break;}else{// error_http_response._status_code = SERVER_ERROR;break;}}}LOG(INFO, _http_request._request_body);
} 

构建响应

主体

在这里插入图片描述

该项目定义了一下几个状态码:

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

主要还是常见的几个错误码,如果后期改善项目,还可以继续增加

构建响应的几个步骤:

  1. 判断请求方法,不是GET且不是POST方法都不做处理,将错误码设置为BAD_REQUSET,然后跳转到最后,构建对应的错误请求响应
  2. 如果是GET请求方法,且带有参数('?'后面),那么需要将uri进行字符串分割,分割为uri_path和uri_query两个部分,且此时有数据上传,需要启动cgi机制;如果不带参数,path就是uri,不用启动cgi机制
  3. 如果是POST请求方法,这里是需要启动cgi机制
  4. 接下来就是对资源路径进行分析,请求路径可能是一个文件、可能是一个目录,也可能一个可执行程序,所以这里我们需要一一进行判断,在此之前,我们还需要给这个路径添加上服务器的根目录——webroot,这个目录是我们应该设置的
#define WEB_ROOT "webroot"
path = WEB_ROOT + path;	

​ 如果发现该路径是一个未指明具体资源的一个目录,不是一个明确的文件(例如:wwwroot/a/),就需要在路径后面添加该目录下的默认资源页面进行返 回,如下:

#define HOMT_PAGE "index.html"
if (_path[_path.size()-1] == '/'){_path += HOME_PAGE;
}

​ 为了判断文件的属性,这里用到一个系统调用stat,它可以根据路径找到文件并提炼出文件属性,可以曲儿文件是否存在,函数原 型如下:

int stat(const char *pathname, struct stat *buf);

​ 返回值为0代表成功,找不到文件返回-1,如果文件存在,就判断是目录还是可执行程序,还是普通文件,如果是目录,就在目录路 径添加默认文件,如果是普通文件,路径不作处理,只需要提取出文件大小填充响应类的body_size字段,如果是可执行程序,启 动cgi机制即可。可执行程序可以根据S_IXUSRS_IXGRPS_IXOTH三个标志位进行判断,只有满足一个,就代表是可执行程序, 代码如下:

struct stat st;
if (stat(_path.c_str(), &st) == 0){// 资源存在,判断是否是一个目录if (S_ISDIR(st.st_mode)){// 是一个目录,添加默认首页_path += "/";_path += HOME_PAGE;// 对st进行更新,获取文件大小stat(_path.c_str(), &st);}_http_response._body_size = st.st_size; // 判断请求是否是一个可执行程序// 拥有者具有可执行,所属组具有,其他人具有都能够证明是一个可执行程序if((st.st_mode & S_IXUSR)||(st.st_mode & S_IXGRP)||(st.st_mode & S_IXOTH)){_http_request._cgi = true;}
}
else{// 资源不存在  LOG(WARNING, _path+" Not Found");_http_response._status_code = NOT_FOUND;goto END;
}
  1. 接下来就是分析资源的后缀,如果不是可执行程序,都可以提取出后缀,以便后序根据根据判断文本类型,代码如下:
// 提取后缀,默认为 ".html"
pos = _path.rfind('.');
if (pos != std::string::npos){_http_response._suffix = _path.substr(pos);
}
  1. 根据cgi机制启动标准位判断是否需要启动,如果需要,就执行cgi的方法,否则执行非cgi处理的方法
  2. 细节处理完之后,最后根据状态码等内容统一构建一个响应

代码实现:

void BuildHttpResponse()
{auto& method = _http_request._method;auto& _path = _http_request._uri_path;auto& query = _http_request._uri_query;size_t pos = 0;if (method != "GET" && method != "POST"){// 非法请求LOG(WARNING, "method is not right");_http_response._status_code = BAD_REQUEST;goto END;}if (method == "GET"){// 是否带参 uri=path?if (_http_request._uri.find("?") != std::string::npos){// 找到?,说明带有参数Util::CutString(_http_request._uri, _path, query, "?"); //std::cout << _path << ":" << query << std::endl;// webroot/test_cgi_http_request._cgi = true;}else{_path = _http_request._uri;}}else if (method == "POST"){// 使用cgi模式_http_request._cgi = true;_path = _http_request._uri;}//std::cout << __LINE__ << ": " + _path <<std::endl;// path拼接前缀 webroot_path = WEB_ROOT + _path;// 未知名具体文件资源时,默认返回当前目录下的首页 if (_path[_path.size()-1] == '/'){_path += HOME_PAGE;}//std::cout << "debug: path:" + _path + " query:" + query << std::endl;struct stat st;if (stat(_path.c_str(), &st) == 0){// 资源存在,判断是否是一个目录if (S_ISDIR(st.st_mode)){// 是一个目录,添加默认首页_path += "/";_path += HOME_PAGE;// 对st进行更新,获取文件大小stat(_path.c_str(), &st);}_http_response._body_size = st.st_size; // 判断请求是否是一个可执行程序// 拥有者具有可执行,所属组具有,其他人具有都能够证明是一个可执行程序if((st.st_mode & S_IXUSR)||(st.st_mode & S_IXGRP)||(st.st_mode & S_IXOTH)){_http_request._cgi = true;}}else{// 资源不存在  LOG(WARNING, _path+" Not Found");_http_response._status_code = NOT_FOUND;goto END;}// 提取后缀,默认为 ".html"pos = _path.rfind('.');if (pos != std::string::npos){_http_response._suffix = _path.substr(pos);}if (_http_request._cgi){// cgi模式 ProcessCgi   做数据处理_http_response._status_code = ProcessCgi();}else{// 非cgi模式,NoProcessCgi  不做数据处理,直接返回静态网页// 构建http响应,返回网页_http_response._status_code = ProcessNonCgi();}END:// 统一处理,构建响应BuildHttpResponseHelper();return;
}
非cgi处理

该处理只需要进行打开资源文件的操作即可,如果文件打开失败,把状态码设置为NOT_FOUND,否则设置为OK

代码实现:

int ProcessNonCgi()
{// 打开文件_http_response._fd = open(_http_request._uri_path.c_str(), O_RDONLY);if (_http_response._fd >= 0){return OK;}return NOT_FOUND;
}					
cgi机制处理

什么是CGI?

CGI(Common Gateway Interface) 是WWW技术中最重要的技术之一,是Web 服务器运行时外部程序的规范,按CGI 编写的程序可以扩展服务器功能。CGI 应用程序能与浏览器进行交互,还可通过数据API与数据库服务器等外部数据源进行通信,从数据库服务器中获取数据。格式化为HTML文档后,发送给浏览器,也可以将从浏览器获得的数据放到数据库中。几乎所有服务器都支持CGI,可用任何语言编写CGI。使用命令行参数或环境变量表示服务器的详细请求,服务器与浏览器通信采用标准输入输出方式,是标准CGi的做法。

没有CGI的服务器只能够给浏览器发送一些网页资源,如果浏览器上传资源的话,该服务器就无法处理,HTTP为我们提供了CGI机制,可以供我们在平台上根据需求搭建CGI程序。

能调用cgi机制,一定是启动了cgi机制,也就是使用POST方法、GET方法带参数或访问可执行程序:

  • GET方法带参数
  • POST方法
  • 访问资源是一个可执行程序

cgi程序由我们自己进行编写,可以使用任何语言,我们只需要调用该程序处理请求即可。如何用一个程序抵用另一个程序,这对大家来说应该是不陌生的——程序替换,我们可以通过创建子进程,如何让子进程进行程序替换,去执行对应的cgi程序,为了让cgi程序能够将请求处理结果返回个父进程,这里需要让父子进程进行通信。进程间通信的方式有很多种,我们这里选择使用匿名管道,因为管道通信时单向的,因为需要双向通信,所以这里采用创建两个匿名管道的方法进行双向通信,创建两个管道:out[2],in[2],父进程使用out[1]作为写端,in[0]作为读端,子进程使用out[0]作为读端,in[1]作为写端,如下:

在这里插入图片描述

父进程想cgi传参可以有两种:往管道里写和导环境变量。如果是GET请求,因为参数是比较短的,所以这里我们可以采取导环境变量的方式;如果是POST请求,因为POST的参数在正文中,正文相比GET命令行参数肯定会大很多,所以这里采用往管道里写的方式传参,这里还需要导入Content-Length的大小,导进环境变量,让cgi能够得知。同时我们需要让cgI知道请求方法是什么,所以这个也同样需要通过导环境变量的方式让cgi能够读取到,所以总结如下:

  • GET:需导环境变量METHODQUERY_STRING
  • POST:正文从管道写,需导环境变量METHODCONTENT_LENGTH

这里还有一个问题:cgi如何得知管道的读端和写端是多少?

程序替换后,进程的代码和数据会进行替换,但进程的数据结构是不变的。子进程的文件描述符表和替换前是一样的,这些是都不变的,所以这里我们可以在程序替换前,将子进程的管道读端和写端进行重定向,把子进程的读端重定向到标准输入,写端重定向到标准输出中,这样程序替换后,cgi只需要用标准输入和标准输出进行读写管道即可,整个cgi布局如下图:

在这里插入图片描述

可以看出的是,cgi程序本质上是使用标准输入和标准输出与浏览器进行交互,完全可以回来中间一整套通信细节。同时需要注意父进程等待子进程,要根据子进程退出情况设置相应的退出码。

代码实现:

int ProcessCgi()
{auto& method = _http_request._method;auto& path = _http_request._uri_path;auto& query = _http_request._uri_query;// GET url提交参数auto& body = _http_request._request_body;// POST 正文请求int content_length = _http_request._content_length;auto& code = _http_response._status_code;// 对于父进程 input是读入,output是写入 int input[2];int output[2];// 创建两个管道if (pipe(input) < 0){LOG(ERROR, "create output pipe error!");return SERVER_ERROR;}if (pipe(output) < 0){LOG(ERROR, "create output pipe error!");return SERVER_ERROR;}pid_t pid = fork();if (pid == 0){// 子进程close(input[0]);// 关闭读,用来写  close(output[1]);// 关闭写,用来读 // 导入环境变量,便于程序替换后方便识别是何种请求方式std::string method_env = "METHOD=" + method;putenv((char*)method_env.c_str());if (method == "GET"){std::string query_env = "QUERY_STRING=" + query;putenv((char*)query_env.c_str());}else if (method == "POST"){// 将正文长度导入环境变量std::string body_size_env = "CONTENT_LENGTH=" + std::to_string(content_length);putenv((char*)body_size_env.c_str());}// 对标准输入和标准输出进行重定向dup2(input[1], 1);dup2(output[0], 0);// 程序替换execlexecl(path.c_str(), path.c_str(), nullptr);// 失败就退出exit(5);}else if (pid < 0){// 创建失败LOG(ERROR, "fork error!");return SERVER_ERROR;}else{// 父进程close(input[1]);// 关闭写,用来读 close(output[0]);// 关闭读,用来写 if (method == "POST"){int size = 0;int total = 0;// 将参数写进管道while ((size = write(output[1], body.c_str()+total, body.size()-total)>0)){total += size; }}// 读取管道char ch;while (read(input[0], &ch, 1) > 0){_http_response._response_body += ch;}int status;pid_t ret = waitpid(pid, &status, 0);//LOG(INFO, "code="+std::to_string(code));if (ret > 0){if (WIFEXITED(status)){// 正常退出if (WEXITSTATUS(status) == 0){// 退出码正常code = OK;}else{// 退出码不正常 // 5:程序替换失败// 6: 请求方法不对code = WEXITSTATUS(status) == 5 ? SERVER_ERROR : BAD_REQUEST;LOG(INFO, "code="+std::to_string(code));}}else{// 异常退出code = SERVER_ERROR;LOG(INFO, "code="+std::to_string(code));}}else{// 等待失败code = SERVER_ERROR;LOG(INFO, "code="+std::to_string(code));}//LOG(INFO, "code="+std::to_string(code));close(input[0]);close(output[1]);}return code;
}
根据状态码构建响应

不管状态码如何,响应行基本是一样的,所以可以先对响应行进行构建,如何根据不同的状态码去构建不同的响应报头和响应正文,如下:

void BuildHttpResponseHelper()
{auto& code = _http_response._status_code;// 版本 状态码 状态描述_http_response._status_line += HTTP_VERSION;_http_response._status_line += " " + std::to_string(code);_http_response._status_line += " " + Code2Desc(code);_http_response._status_line += LINE_END;// 行分割符switch(code){case OK:BuildOkResponseHeader();break;case BAD_REQUEST:HandlerError(PAGE_400);break;case FORBIDDEN:HandlerError(PAGE_403);break;case NOT_FOUND:HandlerError(PAGE_404);break;case SERVER_ERROR:HandlerError(PAGE_500);break;case BAD_GATEWAY:HandlerError(PAGE_504);break;default:break;}
}
构建正确响应

正确响应的响应报头必须填写的两个字段是Content-LengthContent-TypeContent-Length这个字段需对于启用cgi和不启用cgi两种机制是不同的。如果是cgi机制,正文被读取到了response_body中了,所以该字段就是response_body的大小,如果不是cgi机制,该字段就是请求资源文件的大小,这个在获取文件属性的时候已经进行了填充,所以读取content_length成员变量即可

这里补充一个根据文件后缀获取文本类型的函数:

static std::string Suffix2Desc(const std::string& suffix)
{static std::unordered_map<std::string, std::string> suffix2desc = {{".html", "text/html"},{".css", "text/css"},{".xml", "text/xml"},{".js", "application/x-javascript"},{".jpg", "image/jpeg"}};if (suffix2desc.find(suffix) != suffix2desc.end()){// 找到了return suffix2desc[suffix];}return "text/html";
}

如果需要补充,可以查询该工具——Content-Type

代码实现如下:

void BuildOkResponseHeader()
{std::string content_type_string = "Content-Type: ";content_type_string += Suffix2Desc(_http_response._suffix) + LINE_END;_http_response._response_header.push_back(content_type_string);std::string content_length_string = "Content-Length: ";if (_http_request._cgi){// POST GET带参,根据响应正文的大小获取大小content_length_string += std::to_string(_http_response._response_body.size()) + LINE_END;}else{//非cgi,获取页面, 根据body_size获取页面大小content_length_string += std::to_string(_http_response._body_size) + LINE_END;}_http_response._response_header.push_back(content_length_string); 
}
构建错误响应

错误响应的正文统一返回对应错误码的html格式的文件,如下(宏变量定义):

#define PAGE_400 "400.html"
#define PAGE_403 "403.html"
#define PAGE_404 "404.html"
#define PAGE_500 "500.html"
#define PAGE_504 "504.html"

代码实现:

void HandlerError(std::string page)
{// 错误处理统一通过页面返回_http_request._cgi = false;// 打开文件// 错误页面统一放在wwwroot目录下page = WEB_ROOT + std::string("/") + page;LOG(INFO, "HandlerError:"+page);_http_response._fd = open(page.c_str(), O_RDONLY);if (_http_response._fd > 0){struct stat st;stat(page.c_str(), &st);_http_response._body_size = st.st_size;std::string content_type_string = "Content-Type: ";content_type_string += Suffix2Desc(".html") + LINE_END;_http_response._response_header.push_back(content_type_string);std::string content_length_string = "Content-Length: ";content_length_string += std::to_string(_http_response._body_size) + LINE_END;_http_response._response_header.push_back(content_length_string);}
}

发送响应

主要有以下几个步骤:

  1. 发送响应行
send(_sock, _http_response._status_line.c_str(), _http_response._status_line.size(), 0);
  1. 发送报头
for (auto& iter : _http_response._response_header){send(_sock, iter.c_str(), iter.size(), 0);
}
  1. 发送空行
send(_sock, _http_response._blank.c_str(), _http_response._blank.size(), 0);
  1. 发送正文

​ 如果启动了cgi机制,正文内容就放在了response_body中,如果不是cgi机制,就需要传送资源文件给客户端,如果是用writeread将文件先读出来,再写入客户端套接字中,需要经过用户层,sendfile这个接口可以在内核层完成一个文件到一个文件的拷贝,不经过用户层,效率比前者高,如下:

在这里插入图片描述

sendfile

功能: 把一个文件描述符的内容拷贝给另一个文件描述符,在内核层完成该操作,不经过用户层,比read和write的效率高

#include <sys/sendfile.h> 
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

参数:

  • out_fd: 要被写入的文件描述符
  • in_fd: 要被读取的文件描述符
  • offset: 偏移量,可以记录读取文件的位置
  • count: 要拷贝内容的大小

返回值: 成功返回已经写入的字节数,失败返回-1

代码实现:

void 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);}// 发送空行send(_sock, _http_response._blank.c_str(), _http_response._blank.size(), 0);// 发送正文 使用sendfile,将一个文件的内容拷贝给另一个文件,不经过用户层,在内核区进行拷贝if (_http_request._cgi){auto& body = _http_response._response_body;int size = 0;int total = 0;while ((size = send(_sock, body.c_str()+total, body.size()-total, 0))>0){total += size;}} sendfile(_sock, _http_response._fd, nullptr, _http_response._body_size);// 关闭文件close(_http_response._fd);}
}

部署cgi程序

简单的计算程序

该cgi程序处理两个参数的请求,并且算出加减乘除的结果,以html文本进行返回。

代码实现:

bool GetQueryString(std::string& query)
{std::string method = getenv("METHOD");std::cerr << "method: " + method << std::endl;if (method == "GET"){query = getenv("QUERY_STRING");//  cerr << "query_string: " + query << endl;return true;}else if (method == "POST"){int content_length = atoi(getenv("CONTENT_LENGTH"));//  std::cerr << "debug: content_length:" << content_length << std::endl;char ch;while (content_length){read(0, &ch, 1);query += ch;content_length--;}std::cerr << "query_string: " + query << std::endl;return true;}else{return false;}
}void CutString(const std::string& in, const std::string sep, std::string& out1, std::string& out2)
{size_t pos = in.find(sep);if (pos != std::string::npos){out1 = in.substr(0, pos);out2 = in.substr(pos+sep.size());}
}int main()
{std::string query;if (!GetQueryString(query)){exit(7);// 请求方法出错}std::string s1;std::string s2;CutString(query, "&", s1, s2);std::string name1;std::string value1;CutString(s1, "=", name1, value1);std::string name2;std::string value2;CutString(s2, "=", name2, value2);if (value1.size() == 0 || value2.size() == 0){exit(7);}// 写入管道//std::cout << name1 + ":" + value1 << std::endl;//std::cout << name2 + ":" + value2 << std::endl;int num1 = stoi(value1), num2 = stoi(value2);if (num2 == 0){std::cout << "<head><meta charset=\"UTF-8\"></head>" << std::endl;std::cout << "<body><h1>除零错误</h1></body>" << std::endl;}else{std::cout << "<head><meta charset=\"UTF-8\"></head>" << std::endl;std::cout << "<body><h1>计数结果如下:</h1><br/>" << std::endl;std::cout << "<h2>" << num1 << "+" << num2 << "=" << (double)num1+num2 <<  "</h2><br/>" << std::endl;std::cout << "<h2>" << num1 << "-" << num2 << "=" << (double)num1-num2 <<  "</h2><br/>" << std::endl;std::cout << "<h2>" << num1 << "*" << num2 << "=" << (double)num1*num2 <<  "</h2><br/>" << std::endl;std::cout << "<h2>" << num1 << "/" << num2 << "=" << (double)num1/num2 <<  "</h2><br/><body/>" << std::endl;}std::cerr << name1 + ":" + value1 << std::endl;std::cerr << name2 + ":" + value2 << std::endl;return 0;
}

测试: 使用浏览器提交表单的方式进行测试

在这里插入图片描述

在这里插入图片描述

用户数据存储程序

我们设计一个mysql_cgi程序,该程序连接MySQL,可以将用户注册的信息进行存储和查询,所以这里可以设计一个前端注册界面和登录界面,如下:
在这里插入图片描述

使用C语言连接数据库,把用户数据放入数据库,方便登录进行查询,下面是插入数据的代码:

bool implement(std::string& sql)
{MYSQL* mfp = mysql_init(nullptr);// 设置编码格式mysql_set_character_set(mfp, "utf8");if (mysql_real_connect(mfp, "127.0.0.1", "http_server", "llyscysygr", "http_server", 3306, nullptr, 0) == nullptr){std::cerr << "connect mysql error!" << std::endl;return false;}std::cerr << "connect mysql success!" << std::endl;int ret = mysql_query(mfp, sql.c_str());mysql_close(mfp);return ret == 0;
}bool Insert(std::string& name, std::string& value)
{std::string sql;sql = "insert into user values('" + name + "','" + value + "');";return implement(sql);
}

用户登录时和注册是要进行数据库查询代码:

int Select(std::string& name, std::string& pwd)
{std::string sql;sql = "select * from user where name='" + name + "';";std::cerr << sql << std::endl;MYSQL* mfp = mysql_init(nullptr);// 设置编码格式mysql_set_character_set(mfp, "utf8");if (mysql_real_connect(mfp, "127.0.0.1", "http_server", "llyscysygr", "http_server", 3306, nullptr, 0) == nullptr){std::cerr << "connect mysql error!" << std::endl;return -1;}if (mysql_query(mfp, sql.c_str())){return -1;}// 获取查询结果MYSQL_RES* sqlres = mysql_store_result(mfp);if (sqlres == nullptr) return 1;my_ulonglong num = mysql_num_rows(sqlres);//uint32_t col = mysql_num_fields(sqlres);//std::cerr << col << std::endl;std::cerr << num << std::endl;if (num == 0){// 账户不存在return 1;}MYSQL_ROW line = mysql_fetch_row(sqlres);std::cerr << line[0] <<":" << line[1] << std::endl;//std::cerr << pwd << std::endl;if (line[1] != pwd){//密码不正确return 2;}mysql_close(mfp);mysql_free_result(sqlres);return 0;
}

需要对前端表单提交过来的数据进行解码,也就是Decode,所以这里有一点对编码后的字符串进行解码的代码如下:

std::string UrlDecode(const std::string& szToDecode)
{std::string result;int hex = 0;for (size_t i = 0; i < szToDecode.length(); ++i){switch (szToDecode[i]){case '+':result += ' ';break;case '%':if (isxdigit(szToDecode[i + 1]) && isxdigit(szToDecode[i + 2])){std::string hexStr = szToDecode.substr(i + 1, 2);hex = strtol(hexStr.c_str(), 0, 16);//字母和数字[0-9a-zA-Z]、一些特殊符号[$-_.+!*'(),] 、以及某些保留字[$&+,/:;=?@]//可以不经过编码直接用于URLif (!((hex >= 48 && hex <= 57) || //0-9(hex >=97 && hex <= 122) ||   //a-z(hex >=65 && hex <= 90) ||    //A-Z//一些特殊符号及保留字[$-_.+!*'(),]  [$&+,/:;=?@]hex == 0x21 || hex == 0x24 || hex == 0x26 || hex == 0x27 || hex == 0x28 || hex == 0x29|| hex == 0x2a || hex == 0x2b|| hex == 0x2c || hex == 0x2d || hex == 0x2e || hex == 0x2f|| hex == 0x3A || hex == 0x3B|| hex == 0x3D || hex == 0x3f || hex == 0x40 || hex == 0x5f)){result += char(hex);i += 2;}else result += '%';}else {result += '%';}break;default:result += szToDecode[i];break;}}return result;
}

以上都是该程序核心部分代码,其它的代码都是一些逻辑,可以在我的gitee进行查看。

简单的逻辑:

  1. 用户注册号账号可以进行登录,如果注册的账号已经存在需要返回错误页面,正确返回登录页面
  2. 用户登录成功返回html+css+js编写的简单的计算器页面,密码错误返回密码错误的页面

效果演示:

用户注册成功:

在这里插入图片描述

用户登录:
在这里插入图片描述

后台数据库:
在这里插入图片描述

Postman测试

GET方法不带参数

在这里插入图片描述

GET方法带参数

在这里插入图片描述

POST方法

在这里插入图片描述

项目迭代

服务器底层采用多反应堆模式reactor,异步线程用来监听客户端请求,并将其交给同步工作线程,同步线程处理客户端请求。reactor底层采用select/poll/epoll(三选一)进行IO多路转接。项目中分有两种reactor:MainReactor(用来监听客户端连接请求)和SubReactor(有多个,用来处理客户端请求),MainReactor单独负责监测监听套接字,如果有连接到来,就触发对应的读回调,并把accept获取到的fd添加到一个SubReactor中进行检测,如果检测到事件触发,SubReactor就会掉用该fd对应的读回调或者写回调(事先注册好的)
具体如下图:
在这里插入图片描述
测试: 使用webbench对服务器并发量进行测试,

  1. 并发连接数1000,访问时间5s
    在这里插入图片描述
  2. 并发连接数5000,访问时间5s在这里插入图片描述

项目代码:https://gitee.com/byte-binxin/http-project-reactor

项目总结

遇到的问题和解决的方法

  1. 读取协议报头的行分隔符需要做统一处理,在判断行分隔符是\r还是\r\n时,不能够直接调用recv继续读取下一个字符,否则会将接受缓冲区的字符拿走,这时候需要使用MSG_PEEK选项进行窥探下一个字符,使用该选项不会将接受缓冲区的字符拿走,十分地友好
  2. 调用cgi程序之前,需要根据不同的方法导入不同的环境变量,且还要让cgi程序知道两个管道的读端和写端的文件描述符刚开始想通过导入环境变量的方式让cgi调用getenv知道,但是发现直接到环境变量不太友好,对端不但要通过环境变量的方式获取参数,还要通过环境变量获取文件描述符,这样有点麻烦。最后想出用dup2系统调用对两个文件描述符进行重定向,重定向到标准输入和标准输出,这样cgi程序可以直接通过标准输入和标准输出进行读写管道
  3. 发送响应正文时,如果要返回网页资源,开始想通过read先进行读文件,读到自己定义的一个缓冲区中,然后调用write将缓冲区中的内容写入sock中。这种方法感觉十分地麻烦,每次都要开一个缓冲区,开销大,效率低。后来发现sendfile这个接口可以在内核完成一个文件描述符到另一个文件描述符的拷贝,效率很高
  4. 服务器在写入时,客户端关闭连接会导致服务器崩溃。这是因为操作系统给服务器进程发送了SIGPIPE信号,导致服务器崩溃,这个bug开始并没有考虑到,后面意识到了将该信号设置为SIG_IGN,忽略该信号,解决了问题

在这里插入图片描述


http://chatgpt.dhexx.cn/article/VU6qahhd.shtml

相关文章

centos搭建http服务器

在虚拟机centos上搭建一个http服务器&#xff0c;端口号改为8080&#xff0c;并创建一个hello.html网页&#xff0c;网页的内容是自己的姓名拼音首字母&#xff0c;通过主机win10的浏览器访问这个页面&#xff0c; 步骤: 1.配置主机和虚拟机网络连通 2.在cent上通过yum指令安装…

http-server服务

实验简介&#xff1a; 两台在同一局域网的主机。机器A&#xff08;系统win10&#xff0c;IP地址为192.168.22.36&#xff09;&#xff0c;机器B。机器A中安装虚拟机win7系统&#xff0c;IP地址为192.168.41.130。 1. 安装node.js win7 无法使用node14版本及以上&#xff0c;以及…

web/http服务器实现

文章目录 项目开发流程程序编码项目实例 项目开发流程 需求分析&#xff1a;实现基本的HTTP服务器&#xff0c;支持浏览器的访问&#xff08;支持标准http协议&#xff09; 接收浏览器发送HTTP请求&#xff1b;解析请求数据&#xff0c;请求方式&#xff08;GET&#xff09;&a…

http服务-搭建简易的http服务器

##http服务-搭建简易的http服务器 仅仅实现了Post和Get,也就简单的使用了 200 OK,100-continue和发送http格式的文本而已&#xff0c;其实我们只要按照HTTP协议收发数据即可。 先来一张做http上传功能时的意外收获&#xff0c;也是待会的测试图片 浏览器访问我们所建立的链接 …

【C++】搭建HTTP服务器

目录 项目介绍 网络协议栈介绍 协议分层 数据的封装与分用 HTTP相关知识介绍 HTTP的特点 URL格式 URI、URL、URN HTTP的协议格式 HTTP的请求方法 HTTP的状态码 HTTP常见的Header CGI机制介绍 CGI机制的概念 CGI机制的实现步骤 CGI机制的意义 日志编写 套接字…

HTTP服务器开发教程

<![CDATA[ 概述 最近因为项目需要&#xff0c;必须自己实现一个HTTP服务器的部分功能。 需求大概是这样的&#xff0c;winform里使用webrowser控件显示一个本地的html页面&#xff0c;同时winform启动http服务&#xff0c;本地html页面里通过javascript使用XMLHttpReq…

Windows搭建HTTP服务器

Windows搭建HTTP服务器 1、开启Windows功能 打开控制面板-程序和功能-启用或关闭Windows功能&#xff0c;勾选Internet information services下拉框的全部选项。 2、搭建HTTP服务器 搜索IIS&#xff0c;打开Internet information services&#xff08;IIS&#xff09;管理器…

一条命令搭建HTTP服务器

文章目录 1.前言2.本地http服务器搭建2.1.Python的安装和设置2.2.Python服务器设置和测试 3.cpolar的安装和注册3.1 Cpolar云端设置3.2 Cpolar本地设置 4.公网访问测试5.结语 转载自远程内网穿透的文章&#xff1a;【Python】快速简单搭建HTTP服务器并公网访问「cpolar内网穿透…

HTTP服务器(一)

问&#xff1a;HTTP协议的职责是什么&#xff1f; 答&#xff1a;生成针对目标web服务器的HTTP请求报文&#xff1b; 对Web服务器请求的内容的处理。&#xff08;听起来挺像废话的…就是客户端向服务器发出的第一步 和 服务器接收最后一步&#xff0c;一种固定格式请求和对应…

HTTP服务

URL&#xff1a;Uniform Resource Locator&#xff0c;统一资源定位符&#xff0c;对可以从互联网上得到的资源的位置和访问 方法的一种简洁的表示&#xff0c;是互联网上标准资源的地址。 网址格式&#xff1a;<协议>://<主机或主机名>[:port]/<目录资源,路径&…

【HTTP协议与Web服务器】

HTTP协议与Web服务器 浏览器与服务器通信过程HTTP的请求报头HTTP请求报头结构HTTP的请求方法 HTTP应答报头HTTP应答报头结构应答状态 web服务器的c语言实现 浏览器与服务器通信过程 浏览器与Web服务器再应用层通信使用的是HTTP协议&#xff0c;而HTTP协议在传输层使用的是TCP协…

http文件服务器

收费工具&#xff0c;二百大洋。学生党勿扰。 程序下载链接:https://download.csdn.net/download/wocanimei007/87755375 无任何后门&#xff0c;请放心使用。 毫不夸张的说&#xff0c;应该目前市场上最好用的文件互传工具。 1 概述 在unbuntu和win上进行文件互传的时候&am…

HTTP/HTTPS服务器

HTTP/HTTPS 一、消息传播过程 1、发送消息的时候做的事情。 比如说客户端发送给的消息是&#xff1a;Hello World 首先是在客户端这里&#xff0c;数据会经过一系列处理从应用层传输到数据链路层。也就对应着调用send&#xff0c;将数据拷贝到内核&#xff0c;内核再协议栈&a…

HTTPS服务器

目录 HTTPS简介 https概念 SSL/TLS 加/解密方式 对称加密 非对称加密 消息摘要 数字签名 数字证书 http与https的区别 作业需求 ​编辑 HTTPS简介 https概念 HTTPS &#xff08;全称&#xff1a;Hyper Text Transfer Protocol over SecureSocket Layer&#xff0…

HTTP服务器搭建

文章目录 实验环境说明服务器配置下载安装HTTP服务修改配置文件 客户端测试Linux客户端测试Windows客户端测试 实验环境说明 主机系统系统版本IP地址服务端LinuxRHEL 7.0192.168.43.128/24客户端LinuxRHEL 7.4192.168.43.15/24客户端Windows 11Windows 11192.168.43.1/24 基本环…

WEB/HTTP服务器搭建

HTTP 对于软件都有服务和客户&#xff0c;有服务端和客户端 服务 就是在操作系统运行一个或者多个程序&#xff0c;并为客户端提供相应所需的服务 协议 就是计算机网络中进行数据交换而建立的规则、标准或约定的集合。只有遵守这个约定&#xff0c;计算机之间才能相互通信…

ESP32基础应用之HTTP 服务器

文章目录 1 HTTP服务器简介2 ApiPost测试工具3 HTTP服务器实验3.1 ApiPost之GET测试3.2 ApiPost之POST测试3.3 ApiPost值PUT测试 参考资料&#xff1a; esp32 http服务器编程指南 1 HTTP服务器简介 HTTP服务器一般指Web服务器&#xff0c;是指驻留于因特网上某种类型计算机的…

http 服务器搭建

目录 http 服务器搭建有很多条数据时用response.write测试服务器是否开启 http 服务器搭建 通过http模块提供的方法可以创建服务器 1 引入http模块 &#xff08;nodejs内置模块&#xff0c; 可以直接引入&#xff09; const http require(http);创建服务器http.createServer…

HTTP服务器(一)HTTP服务器入门介绍

一、简介 1.1、做项目的时候&#xff0c;要请求REST服务器。而FEST服务器还是用http基本原理(即阉割版)来请求。 1.2、请求端口EndPoint/请求方法Method/请求内容格式ContentType/请求的数类型PostData(一般为JSon格式)。 1.3、我们熟悉的HTTP、FTP、Telnet等协议都是建立在…

【八】http服务器开发--实现一个http服务器

文章目录 一、整体概述二、接收http请求三、响应http请求四、完整代码即效果4.1 完整代码&#xff1a;4.2 实现过程 附&#xff08;stat函数&#xff09; 一、整体概述 本节主要实现一个http服务器的示例&#xff1b;该http服务器的主要功能是&#xff0c;在浏览器端访问服务器…