目录
- 项目背景
- 网络协议栈
- 协议分层
- 数据封装与分用
- HTTP协议介绍
- HTTP协议简介
- 认识URL
- URI、URL、URN
- HTTP的五大特点
- HTTP协议格式
- HTTP的请求方法
- HTTP的状态码
- HTTP常见的Header
- CGI机制介绍
- CGI机制概念
- CGI模式实现步骤
- CGI机制的流程
- 日志文件编写
- 套接字相关代码编写
- HTTP服务器的主体逻辑
- HTTP请求结构设计
- HTTP响应结构设计
- EndPoint类编写
- EndPoint结构设计
- 设计线程回调
- 读取HTTP请求
- 处理HTTP请求
- 构建HTTP响应
- 发送HTTP响应
- 差错处理
- 逻辑错误
- 读取错误
- 写入错误
- 引入线程池
- 设计任务类
- 线程池代码实现
- 项目测试
- 错误请求测试
- GET方法上传数据测试
- POST方法上传数据测试
- 项目扩展
项目背景
http协议被广泛使用,从移动端,pc端浏览器,http协议无疑是打开互联网应用窗口的重要协议,http在网络应用层中的地位不可撼动,是能准确区分前后台的重要协议。
本项目目标在于对http协议的理论学习,从零开始完成web服务器开发,坐拥下三层协议,从技术到应用,让网络难点无处遁形。采用C/S模型,编写支持中小型应用的http,并结合mysql,理解常见互联网应用行为,做完该项目,你可以从技术上完全理解从你上网开始,到关闭浏览器的所有操作中的技术细节。
该项目采用的技术涉及到网络编程(TCP/IP协议,socket流式套接字,http协议)、多线程技术、cgi技术、shell脚本、线程池等。
网络协议栈
协议分层
各层的功能如下:
- 应用层:根据特定的通信目的,对数据进行分析处理,以达到某种业务性的目的。
- 传输层:处理传输时遇到的问题,主要是保证数据传输的可靠性。
- 网络层:完成数据的转发,解决数据去哪里的问题。
- 链路层:负责数据真正的发生过程。
数据封装与分用
发送端在发送数据的过程中,自上而下贯穿整个网络协议栈,当数据到达每一层以后都会添加上对应的报头信息,接收端也是,自底向上贯穿整个网络协议栈,当到达每一层以后就会将对应的报头信息提取出来,最终完成的数据的解包以及分用。
本项目需要完成的就是在接收到客户端发来的HTTP请求以后,将HTTP报头的信息提取出来,然后对数据进行分析处理,最终将结果添加上HTTP报头并发给客户端。
HTTP协议介绍
HTTP协议简介
HTTP(Hyper Text Transfer Protocol)协议又叫做超文本传输协议,是一个简单的请求-响应协议,HTTP通常运行在TCP之上。
认识URL
URL(Uniform Resource Lacator)叫做统一资源定位符,也就是我们通常所说的网址,是因特网的万维网服务程序上用于指定信息位置的表示方法。
一个URL大概由以下几部分组成:
http://
:表示的是协议名称,表示请求时需要使用的协议,通常使用的是HTTP协议或安全协议HTTPS。HTTPS是以安全为目标的HTTP通道,在HTTP的基础上通过传输加密和身份认证保证了传输过程的安全性。usr:pass
:表示的是登录认证信息,包括登录用户的用户名和密码。虽然登录认证信息可以在URL中体现出来,但绝大多数URL的这个字段都是被省略的,因为登录信息可以通过其他方案交付给服务器。www.example.jp
:表示的是服务器地址,也叫做域名,比如www.alibaba.com
,www.qq.com
,www.baidu.com
。80
表示的是服务器端口号。HTTP协议和套接字编程一样都是位于应用层的,在进行套接字编程时我们需要给服务器绑定对应的IP和端口,而这里的应用层协议也同样需要有明确的端口号。/dir/index.htm
:表示的是要访问的资源所在的路径。访问服务器的目的是获取服务器上的某种资源,通过前面的域名和端口已经能够找到对应的服务器进程了,此时要做的就是指明该资源所在的路径。uid=1
:表示的是请求时提供的额外的参数,这些参数是以键值对的形式,通过&符号分隔开的。ch1
:表示的是片段标识符,是对资源的部分补充。
URI、URL、URN
- URI(Uniform Resource Indentifier)统一资源标识符:用来唯一标识资源。
- URL(Uniform Resource Locator)统一资源定位符:用来定位唯一的资源。
- URN(Uniform Resource Name)统一资源名称:通过名字来标识资源。
URI、URL、URN三者关系
URL是URI的一种,URL不仅能唯一标识资源,还定义了该如何访问或定位该资源,URN也是URI的一种,URN通过名字来标识资源,因此URL和URN都是URI的子集。
HTTP的五大特点
HTTP的五大特点如下:
- 客户端服务器模式(CS/BS):在一条通信路上一端是服务端,一端是客户端,请求从客户端发出,从服务器进行响应并返回。
- 简单快速:客户端向服务器发送请求时,只需要传输请求资源方法和请求资源路径,并不需要发送额外的请求,而且由于HTTP协议比较简单,导致HTTP协议程序的规模比较小,因此通信速度快。
- 灵活:HTTP协议对数据对象没有要求,可以传输任何类型对象的数据,对于正在传输的数据类型,HTTP协议将通过报头中的Content-Type属性加以标记。
- 无连接:每次连接只会对一个请求进行处理,当服务器对客户端发送过来的请求进行处理并接收到客户端的响应以后,就会立即断开连接。这种处理模式就大大的提高了HTTP协议的效率。
- 无状态: HTTP协议自身不对请求和响应之间的通信状态进行保存,每个请求都是独立的,这是为了让HTTP能更快地处理大量事务,确保协议的可伸缩性而特意设计的。
HTTP协议格式
HTTP请求协议格式
HTTP请求由以下四部分组成:
- 请求行:[请求方法]+[url]+[http版本]
- 请求报头:请求的属性,这些属性都是以key: value的形式按行陈列的。
- 空行:遇到空行表示请求报头结束。
- 请求正文:请求正文允许为空字符串,如果请求正文存在,则在请求报头中会有一个Content-Length属性来标识请求正文的长度。
其中,前面三部分是一般是HTTP协议自带的,是由HTTP协议自行设置的,而请求正文一般是用户的相关信息或数据,如果用户在请求时没有信息要上传给服务器,此时请求正文就为空字符串。
HTTP响应协议格式
HTTP响应由以下四部分组成:
- 状态行:[http版本]+[状态码]+[状态码描述]
- 响应报头:响应的属性,这些属性都是以key: value的形式按行陈列的。
- 空行:遇到空行表示响应报头结束。
- 响应正文:响应正文允许为空字符串,如果响应正文存在,则响应报头中会有一个Content-Length属性来标识响应正文的长度。比如服务器返回了一个html页面,那么这个html页面的内容就是在响应正文当中的。
HTTP的请求方法
HTTP常见的请求方法如下:
方法 | 说明 | 支持的HTTP协议版本 |
---|---|---|
GET | 获取资源 | 1.0、1.1 |
POST | 传输实体主体 | 1.0、1.1 |
PUT | 传输文件 | 1.0、1.1 |
HEAD | 获得报文首部 | 1.0、1.1 |
DELETE | 删除文件 | 1.0、1.1 |
OPTIONS | 询问支持的方法 | 1.1 |
TRACE | 追踪路径 | 1.1 |
CONNECT | 要求用隧道协议连接代理 | 1.1 |
LINK | 建立和资源之间的联系 | 1.0 |
UNLINK | 断开连接关系 | 1.0 |
HTTP的请求方法中最常用的就是GET方法和POST方法,其中GET方法一般用于获取某种资源信息,而POST方法一般用于将数据上传给服务器,但实际GET方法也可以用来上传数据,比如百度搜索框中的数据就是使用GET方法提交的。
GET方法和POST方法都可以带参,其中GET方法通过URL传参,POST方法通过请求正文传参。由于URL的长度是有限制的,因此GET方法携带的参数不能太长,而POST方法通过请求正文传参,一般参数长度没有限制。
HTTP的状态码
HTTP的状态码如下:
类别 | 原因短语 |
---|---|
1XX Informational(信息性状态码) | 接收的请求正在处理 |
2XX Success(成功状态码) | 请求正常处理完毕 |
3XX Redirection(重定向状态码) | 需要进行附加操作以完成请求 |
4XX Client Error(客户端错误状态码) | 服务器无法处理请求 |
5XX Server Error(服务器错误状态码) | 服务器处理请求出错 |
最常见的状态码,比如200(OK),404(Not Found),403(Forbidden请求权限不够),302(Redirect),504(Bad Gateway)。
HTTP常见的Header
HTTP常见的Header如下:
- Content-Type:数据类型(text/html等)。
- Content-Length:正文的长度。
- Host:客户端告知服务器,所请求的资源是在哪个主机的哪个端口上。
- User-Agent:声明用户的操作系统和浏览器的版本信息。
- Referer:当前页面是哪个页面跳转过来的。
- Location:搭配3XX状态码使用,告诉客户端接下来要去哪里访问。
- Cookie:用于在客户端存储少量信息,通常用于实现会话(session)的功能。
CGI机制介绍
CGI机制概念
CGI(Common Gateway Interface,通用网关接口)是一种重要的互联网技术,可以让一个客户端,从网页浏览器向执行在网络服务器上的程序请求数据。CGI描述了服务器和请求处理程序之间传输数据的一种标准。
通常在服务器上获取资源的方法为GET方法,将数据上传至服务器的方法为POST方法,但对于GET方法来说,也可以用来上传资源,只不过是POST方法是通过正文来进行传参的,而GET方法是通过URL来进行传参的。
用户上传数据并不是单纯的将数据上传上去就可以了,还需要对数据进行相应的处理,最后将结果进行返回,但是数据段处理跟HTTP是没有关系的,取决于上层的业务场景。但是HTTP提供了CGI机制,在服务器中布置了若干个CGI程序,当HTTP获取到数据以后,通过程序替换交给对应的CGI程序进行处理,处理完毕以后在通过CGI程序构建HTTP响应返回给客户端。
所以对于CGI程序什么时候进行数据处理,怎么进行数据处理,如何获取CGI程序处理结果,这些细节问题都需要我们自己一步一步的进行实现。
对于CGI模式,只有在用户上传资源的过程中才会被使用,而如果用户只是想获取资源,CGI模式是不会被使用的。
CGI模式实现步骤
创建子进程替换父进程
服务器获取到新连接以后,会创建一个新线程为其服务,执行CGI程序就必须调用exec系列函数进行程序替换,但是由于线程与进程是共享进程地址空间,内存与文件描述符的,如果使用exec系列函数直接进程程序替换的话,就会直接将服务器进程的代码和数据也替换走,也就意味着HTTP就只能运行一次,很显然这是不合理的,所以就需要创建子进程来进行替换。
管道的建立
调用CGI程序的目的是对数据进行处理,那么我们就需要获取到数据,而且最终我们还需要将数据响应回去,那么就需要进行进程间通信,由于是父子进程间的通信,我们就可以选择匿名管道的方式。
管道间的通信属于半双工通信,只支持一端读取数据,一端写入数据,父进程不仅需要将数据发送给子进程,还需要从子进程那儿获取到数据,所以就需要创建两个匿名管道以此来实现双向通信,因此在创建调用fork子进程之前需要先创建两个匿名管道,在创建子进程后还需要父子进程分别关闭两个管道对应的读写端。
完成重定向相关设置
创建用于父子进程间通信的两个匿名管道,父子进程都是各自用两个变量来记录管道对应读写端的文件描述符的,但是对于子进程来说,调用exec系列函数进行程序替换以后,子进程的代码和数据就被替换为了目标CGI程序的代码和数据,而目标CGI程序并不知道对应的文件描述符,就无法知道读写端,也就无法完成进程间通信。
但是我们需要知道的是,进程程序替换只会替换代码和数据,对于进程控制块,页表以及对应的文件描述符并不会进行替换,所以底层的两个匿名管道依然存在,只不过是替换后的CGI程序并不知道这两个匿名管道的文件描述符而已。
所以我们可以做一个约定:从标准输入流读取数据就相当于从匿名管道读取数据,向标准输出流写入数据就相当于向管道文件中写入数据,此时,管道文件就不需要知道对应的文件描述符了,只需要向标准输出流写入数据,标准输入流读取数据即可。
父子进程交付数据
此时父子进程就可以通过匿名管道进行通信了,接下来就要考虑父进程如何将数据交付给CGI程序,CGI程序如何将处理结果交付给父进程。
- 如果请求方法为GET方法,那么就是通过URL进行传参的,此时可以在子进程进行程序替换之前,调用putenv函数将参数导入环境变量,环境变量并不会受程序替换的影响,所以被替换后的CGI程序可以通过getenv函数来获取对应的参数。
- 如果请求方法是POST方法,用户是通过正文进行传参的,此时就可以直接将正文数据写入管道文件中,然后传递给CGI程序即可,但是需要注意的是,我们需要将正文的长度通过putenv函数导入环境变量,为了让CGI程序知道获取多少各参数。
注意:由于正文长度,URL传递的参数和请求方法都比较短,使用管道通信的话就会拖慢效率,所以在此就使用putenv函数将其导入环境变量。
而且被替换后的CGI程序并不知道此时HTTP的请求方法,所以在进行程序替换之前,我们还需要将HTTP的请求方法调用putenv函数导入到环境变量中。在CGI程序启动以后,首先就会通过环境变量获取到HTTP的请求方法,然后再根据方法到管道或者是环境变量中获取数据。
CGI机制的流程
CGI机制的处理流程如下图所示:
对于用户发送过来的HTTP请求:
- 如果请求方法为GET方法带参或者是POST方法,此时需要通过CGI程序获取参数,最终构建响应传递给浏览器,如果请求方法直接为GET方法,就使用非CGI程序直接构建响应传递回浏览器。
- CGI处理就是通过创建子进程进行程序替换的方式来调用CGI程序,通过创建匿名管道、重定向、导入环境变量的方式来与CGI程序进行数据通信,最终根据CGI程序的处理结果构建HTTP响应返回给浏览器。
CGI机制的意义
- CGI机制让服务器获取到的数据交给了CGI程序进行处理,最终由CGI程序构建响应给客户端,对服务器逻辑和业务逻辑进行了解耦,是的服务器和业务分离开来,各司其职。
- CGI机制将从浏览器输入的数据给CGI程序,最终通过CGI程序构建响应在交还给浏览器,也就意味着可以完全忽略中间服务器的处理逻辑,就相当于CGI程序可以直接从标准输入流读取数据,再从标准输出流写入数据。
日志文件编写
本项目的日志文件格式如下:
日志说明:
- 日志级别:分为四个级别,从低到高分为INFO,WARNING,ERROR,FATAL;
- 时间戳:事件产生的时间;
- 日志信息:事件产生的日志信息;
- 错误文件名称:事件在哪一文件上产生;
- 行数:事件在对应文件哪一行上产生。
其中:
- INFO表示正常的日志输出,一切按预期运行;
- WARNING表示警告,该事件不影响服务器运行,但存在风险;
- ERROR表示发生了某种错误,但该事件不影响服务器继续运行;
- FATAL表示发生了致命的错误,该事件将导致服务器停止运行。
接下来就可以针对日志文件编写一个日志函数,其中参数就包括日志级别、日志信息、错误文件名称、错误的行数。
void Log(std::string level, std::string message, std::string file_name, int line)
{std::cout << "[" << level << "]" << "[" << time(nullptr) << "]" << "[" << message << "]" << "[" << file_name << "]" << "[" << line << "]" << std::endl;
}
在这儿我们可以通过C语言的__FILE__
和__LINE__
来获取当前文件的名称和当前文件的行数,需要注意的是,不能将__FILE__
和__LINE__
设置为参数的缺省值,因为这样每次获取到的都是Log函数所在的文件名称和所在的行数。而宏可以在预处理期间将代码插入到目标地点,因此我们可以定义如下宏:
#define LOG(level, message) Log(level, message, __FILE__, __LINE__)
后序在调用LOG函数过程中只需要传入日志级别以及日志信息即可,在预处理期间__FILE__
和__LINE__
就会被插入到目标地点,这时就能获取到日志产生的文件名称和对应的行数了。
日志级别传入问题
后序日志级别传入过程中,我们肯定是以INFO
,WARNING
这种形式进行传入,而不是以"INFO"
,"WARNING"
这种形式进行传入,所以我们可以将这四个日志级别定义为宏,然后通过#
将宏参数level
变成对应的字符串。如下:
#define INFO 1
#define WARNING 2
#define ERROR 3
#define FATAL 4#define LOG(level, message) Log(#level, message, __FILE__, __LINE__)
套接字相关代码编写
我们可以将跟套接字相关的调用接口封装进一个TcpServer类中,方便后序的调用工作,此外,也可以将这个TcpServer类设置为单例模式。
#include "Log.hpp"
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <unistd.h>
#include <pthread.h>#define BACKLOG 5class TcpServer
{public:static TcpServer* GetInstance(int port){static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;//双重判断防止频繁进行加锁解锁if(_svr == nullptr){pthread_mutex_lock(&mtx);if(_svr == nullptr){_svr = new TcpServer(port);_svr->InitServer();}pthread_mutex_unlock(&mtx);}return _svr;}public:// 初始化服务器void InitServer(){Socket();Bind();Listen();LOG(INFO, "tcpserver init success......");}// 创建套接字void Socket(){_listen_sock = socket(AF_INET, SOCK_STREAM, 0);//如果创建套接字失败,打印错误信息if(_listen_sock < 0){LOG(FATAL, "create socket error!");exit(1);}//设置端口复用int opt = 1;setsockopt(_listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));LOG(INFO, "create socket success.....");}// 绑定套接字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; // 云服务器不能直接绑定公网IPif(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, "listen sock success......");}// 获取监听套接字int Sock(){return _listen_sock;}// 析构函数~TcpServer(){if(_listen_sock >= 0){close(_listen_sock); // 关闭监听套接字}}private:int _port; //端口号int _listen_sock; //监听套接字static TcpServer* _svr; // 指向单例对象的static指针private:// 构造函数设置为私有TcpServer(int port):_port(port), _listen_sock(-1){}// 拷贝构造和赋值运算符重载设置为deleteTcpServer(const TcpServer&) = delete;TcpServer* operator= (const TcpServer&) = delete;
};TcpServer* TcpServer::_svr = nullptr; // 初始化单例对象
注意:
- 由于我们使用的是云服务器,云服务器不能直接绑定公网IP,所以我们直接将IP地址设置为
INADDR_ANY
即可。 - 在上述单例模式中,我们是以静态锁的方式进行加锁的,并不需要进行释放,而且为了保证在调用GetInstance函数时不会频繁的进行加锁解锁,我们在此使用双检查加锁解锁的方式。
HTTP服务器的主体逻辑
我们现在就可以将HTTP服务器封装成一个HttpServer类,在构造HttpServer对象时传入一个端口号,然后在调用Loop函数运行服务器,获取单例对象TcpServer中的监听套接字,通过该监听套接字获取新连接,每获取一个监听套接字以后就创建一个新线程为其服务。
#include "Log.hpp"
#include "TcpServer.hpp"#define PORT 8081class HttpServer
{
public:// 构造函数HttpServer(int port = PORT) : _port(port){}void Loop(){TcpServer *tsvr = TcpServer::GetInstance(_port);LOG(INFO, "Loop begin!");while (true){struct sockaddr_in peer;socklen_t len = sizeof(peer);int sock = accept(tsvr->Sock(), (struct sockaddr *)&peer, &len);if (sock < 0){continue;}LOG(INFO, "get a new link!");// 创建新线程处理新连接发起的HTTP请求int *p = new int(sock);pthread_t tid;pthread_create(&tid, nullptr, CallBack::HandlerRequest, (void *)p);pthread_detach(tid);}}~HttpServer(){}private:int _port; // 端口号
};
- 服务器需要将新连接对应的套接字作为参数传递给新线程,为了避免该套接字在新线程读取之前被下一次获取到的套接字覆盖,因此在传递套接字时最好重新new一块空间来存储套接字的值。
- 新线程创建后可以将新线程分离,分离后主线程继续获取新连接,而新线程则处理新连接发来的HTTP请求,代码中的HandlerRequest函数就是新线程处理新连接时需要执行的回调函数。
主函数逻辑
运行服务器需要指定端口号,我们可以采用这个端口号创建一个HttpServer对象,然后再调用Loop()函数运行服务器,此时服务器就会不断获取新连接并创建新线程来处理连接。
#include "HttpServer.hpp"
#include <memory>static void Usage(std::string proc)
{std::cout << "Usage:\n\t" << proc << "port" << std::endl;
}
int main(int argc, char *argv[])
{if(argc != 2){Usage(argv[0]);exit(4);}int port = atoi(argv[1]);std::shared_ptr<HttpServer> http_server(new HttpServer(port));http_server->Loop();return 0;
}
HTTP请求结构设计
HTTP请求类
我们可以封装一个类,这个类包含HTTP请求的内容、HTTP请求的解析结果以及是否需要使用CGI模式的标志位。后续处理请求时就可以定义一个HTTP请求类,读取到的HTTP请求的数据就存储在这个类当中,解析HTTP请求后得到的数据也存储在这个类当中。
class HttpRequest
{
public:// http请求内容std::string _request_line; // 请求行std::vector<std::string> _request_header; // 请求报头std::string blank; // 空行std::string request_body; // 请求正文// http解析内容std::string _method; // 解析方法std::string _uri; // URIstd::string _version; // HTTP版本std::unordered_map<std::string, std::string> _header_kv; // 请求报头中的键值对int _content_length; // 正文长度std::string _path; // 请求资源路径std::string _query_string; // uri中包含的参数// cgi 相关bool _cgi; // 是否使用cgi模式
public:HttpRequest(): _content_length(0) // 默认长度为0,_cgi(false) // 默认不使用cgi模式{}~HttpRequest(){}
};
HTTP响应结构设计
HTTP响应类
跟HTTP请求一样,HTTP响应也可以封装成一个类,这个类当中包括HTTP响应的内容以及构建HTTP响应所需要的数据。后续构建响应时就可以定义一个HTTP响应类,构建响应需要使用的数据就存储在这个类当中,构建后得到的响应内容也存储在这个类当中。
// HTTP响应类
class HttpReponse
{
public:// HTTP响应内容std::string _status_line; // 状态行std::vector<std::string> _reponse_header; // 响应报头std::string _blank; // 空行std::string _reponse_body; // 响应正文(CGI相关)// 所需数据int _status_code; // 状态码int _fd; // 响应文件的fd(非CGI相关)int _size; // 响应文件的大小(非CGI相关)std::string _suffix; // 响应文件的后缀(非CGI相关)public:HttpReponse(): _blank(LINE_END), _status_code(OK), _fd(-1){}~HttpReponse() {}
};
EndPoint类编写
EndPoint结构设计
在这儿我们设计一个EndPoint类,用于进程间通信,对于服务端与客户端进行进程间通信时,客户端是一个EndPoint,服务端又是另一个EndPoint,所以EndPoint应该包含以下成员变量:
- _sock:表示与客户端进行通信的套接字;
- _http_request:表示客户端发来的http请求;
- _http_response:表示将会发给客户端的http响应。
EndPoint也会包含以下四个成员函数:
- RecvHttpReuest:读取客户端发来的http请求;
- HandlerHttpReuest:处理客户端发送过来的http请求;
- BuildHttpRespose:构建将会发送给客户端的http响应;
- SendHttpRespose:向客户端发送http响应。
class EndPoint
{
public:EndPoint(int sock) : _sock(sock){}// 读取客户端发送过来的http请求void RcvHttpRequest(){}// 处理客户端发送过来的http请求void HandlerHttpRequest(){}// 构建发送给客户端的http响应void BuildHttpResponse(){}// 发生http响应给客户端void SendHttpResponse(){}~EndPoint(){};private:int _sock; // 用于进程间通信的套接字;HttpRequest _http_request; // 表示客户端发送来的http请求HttpReponse _http_response; // 表示构建的http响应
};
设计线程回调
服务器每次获取到一个新连接就会创建一个新线程进行处理,这个线程实际上就是需要定义一个EndPoint对象,然后进行读取请求,处理请求,构建响应,发送响应,处理完毕以后关闭套接字即可。
class CallBack
{public:static void* HandlerRequest(void* arg){LOG(INFO, "handler request begin!");int sock = *(int*)arg;EndPoint* ep = new EndPoint(sock);ep->RcvHttpRequest();ep->HandlerHttpRequest();ep->BuildHttpResponse();ep->SendHttpResponse();close(sock);delete ep;LOG(INFO, "handler request end!");return nullptr;}
};
读取HTTP请求
读取HTTP请求的同时就可以对HTTP请求进行解析,分为以下步骤:读取请求行,读取请求报头与空行,解析请求行,解析请求报头,读取请求正文。
class EndPoint
{
public:// 读取客户端发送过来的http请求void RecvHttpRequest(){RecvHttpRequestLine(); // 读取请求行RecvHttpRequestHeader(); // 读取请求报头和空行ParseHttpRequestLine(); // 解析请求行ParseHttpRequestHeader(); // 解析请求报头RecvHttpRequestBody(); // 读取请求报头}
private:int _sock; // 用于进程间通信的套接字;HttpRequest _http_request; // 表示客户端发送来的http请求HttpReponse _http_response; // 表示构建的http响应
};
读取请求行
读取请求行很简单,就是从套接字中读取一行内容存储到HTTP请求类中的request_line中即可。
class EndPoint
{
private:// 读取请求行void RecvHttpRequestLine(){auto &line = _http_request._request_line;if (Utli::ReadLine(_sock, line) > 0){line.resize(line.size() - 1); // 去掉读取上来的\n}}private:int _sock; // 用于进程间通信的套接字;HttpRequest _http_request; // 表示客户端发送来的http请求HttpReponse _http_response; // 表示构建的http响应
};
在不同的平台下,读取HTTP请求时分隔符可能是不一样的,可能是\r,\n,\r\n
所以我们并不能使用C语言或者是C++的gets函数和getline函数进行读取,在这儿我们需要自己定义一个ReadLine函数,方便兼容上面的三种规则。我们可以设计一个工具类,后续对于字符串的处理都放在这个工具类当中。
ReadLine函数的处理逻辑如下:
- 从指定套接字中读取一个一个字符串;
- 如果读取到的字符串既不是
\r
,也不是\n
,就将读取到的字符串添加到用户提供的缓冲区下继续读取下一个字符串; - 如果读取的字符串为
\n
,说明分隔符就为\n
,此时就将\n
读取到用户提供的缓冲区之后就停止读取; - 如果读取到的字符串为
\r
,就需要往后进行窥探,窥探下一个字符串是不是\n
,如果下一个字符串是\n
,就证明此时分隔符为\r\n
,此时就将下一个\n
读取到用户提供的缓冲区之后停止读取;若果窥探失败的话就说明分隔符就是\r
,此时也需要将\n
添加到用户提供的缓冲区之后停止读取。
无论是上面3种的哪一种情况,我们最终都会将\n
添加到用户提供的缓冲区,相当于将这三种行分隔符统一转换成了以\n为行分隔符,所以调用者不需要读取上来的\n,需要后续自行将其去掉。
class Util
{
public:static int ReadLine(int sock, std::string &out){char ch = 'X';while (ch != '\n') // 只要ch不是\n就可以进入循环{ssize_t size = recv(sock, &ch, 1, 0);if (size > 0){if (ch == '\r'){// 窥探下一个字符串是否为\nrecv(sock, &ch, 1, MSG_PEEK);// 下一个字符串为\nif (ch == '\n'){// 就将这个字符串取走recv(sock, &ch, 1, 0);}else // 下一个字符串不是\n{ch = '\n';}}out.push_back(ch);}else if (size == 0) // 对端关闭连接{return 0;}else{return -1;}}return out.size();}
};
注意: recv函数的最后一个参数如果设置为MSG_PEEK
,那么recv函数将返回TCP接收缓冲区头部指定字节个数的数据,但是并不把这些数据从TCP接收缓冲区中取走,这个叫做数据的窥探功能。
请求报头与空行
由于HTTP的请求报头和空行都是按行陈列的,因此可以循环调用ReadLine函数进行读取,并将读取到的每行数据都存储到HTTP请求类的request_header中,直到读取到空行为止。
class EndPoint
{
private:// 读取请求报头与空行void RecvHttpRequestHeader(){std::string line;while (true){line.clear(); // 每一次读取之前先清空Util::ReadLine(_sock, line);if (line == "\n"){_http_request.blank = line;break;}}// 读取一行请求报头line.resize(line.size() - 1); // 去掉最后读取上来的\n_http_request._request_header.push_back(line);}private:int _sock; // 用于进程间通信的套接字;HttpRequest _http_request; // 表示客户端发送来的http请求HttpReponse _http_response; // 表示构建的http响应
};
注意:
- 每次调用ReadLine函数我们需要将缓冲区的内容先进行清空,因为最终我们是需要将读取到的内容push_back到_request_header中的;
- 而且最后一次读取数据的过程中我们也将
\n
读取到了用户提供的缓冲区中,所以此时我们需要将\n
处理掉才可以。
解析请求行
解析请求行就是将请求行中的请求方法,URI,HTTP版本分离开来,存储到HTTP请求中的_method,_uri,_version中去,请求行中的数据都是以空格作为分隔符的,所以此时就可以调用stringstream对象来进行拆分,而且为了后续用户能得到正确的请求方法,还需要通过transform函数统一将请求方法转换为全大写。
class EndPoint
{
private:// 解析请求行void ParseHttpRequestLine(){auto &line = _http_request._request_line;// 对请求行进行拆分std::stringstream ss(line);ss >> _http_request._method >> _http_request._uri >> _http_request._version;// 使用transform将请求方法转换为全大写auto &method = _http_request._method;std::transform(method.begin(), method.end(), method.begin(), toupper);}private:int _sock; // 用于进程间通信的套接字;HttpRequest _http_request; // 表示客户端发送来的http请求HttpReponse _http_response; // 表示构建的http响应
};
解析请求报头
解析请求报头就是将读取到的一行一行的请求报头,以:
的形式拆分为一个个的键值对,然后再存储到HTTP请求中个_header_kv中,后续就可以直接通过属性获得对应的值了。
class EndPoint
{
private:// 解析请求报头void ParseHttpRequestHeader(){std::string key;std::string value;for(auto& iter : _http_request._request_header){if(Util::CutString(iter, key, value, SEP)){_http_request._header_kv.insert({key, value});}}}private:int _sock; // 用于进程间通信的套接字;HttpRequest _http_request; // 表示客户端发送来的http请求HttpReponse _http_response; // 表示构建的http响应
};
我们可以将切割字符串的函数放入进我们的工具类当中,只需要先找到分割符,然后以分隔符为标准进行切割即可。
class Util
{
private:// 切分请求报头static bool CutString(std::string &target, std::string &sub1_out, std::string &sub2_out, std::string sep){size_t pos = target.find(sep, 0);if (pos != std::string::npos){sub1_out = target.substr(0, pos);sub2_out = target.substr(pos + sep.size());return true;}return false;}
};
读取请求正文
读取请求正文之前,我们需要判断是否可以读取,因为只有使用POST方法是我们才会读取请求正文,当请求方法为POST时,我们还需要获取到Content_Length来获取到请求正文的长度,最终正确的读取。
class EndPoint
{
private:// 读取请求正文void RecvHttpRequestBody(){if (IsNeedRecvHttpRequestBody()) // 判断是否需要读取请求正文{int content_length = _http_request._content_length;auto body = _http_request._request_body;// 读取正文char ch = 0;while (content_length){ssize_t size = recv(_sock, &ch, 1, 0);if (size > 0){body.push_back(ch);content_length--;}else{break;}}}}private:int _sock; // 用于进程间通信的套接字;HttpRequest _http_request; // 表示客户端发送来的http请求HttpReponse _http_response; // 表示构建的http响应
};
- 由于后续还需要使用到正文长度,所以我们定义一个content_length将其保存下来;
- 通过Content-Length获取到请求正文的长度后,需要将请求正文长度从字符串类型转换为整型。
处理HTTP请求
定义状态码
在处理请求的过程中可能会因为某些原因而直接停止处理,比如请求方法不正确、请求资源不存在或服务器处理请求时出错等等。为了告知客户端本次HTTP请求的处理情况,服务器需要定义不同的状态码,当处理请求被终止时就可以设置对应的状态码,后续构建HTTP响应的时候就可以根据状态码返回对应的错误页面。
状态码定义如下:
#define OK 200
#define BAD_REQUEST 400
#define NOT_FOUND 404
#define INTERNAL_SERVER_ERROR 500
处理HTTP请求
- 首先我们需要判断请求方法是否正确,如果请求方法不正确,就需要将状态码设置为
BAD_REQUEST
后停止处理。 - 如果请求方法为
GET
方法,就需要判断其是否带参,如果不带参,就说明当前URI
即为客户端请求资源路径;如果带参,就需要以?
作为分隔符,?
左边为客户端请求资源路径,?
右边为则为GET
方法携带的参数,由于此时GET
方法携带了参数,后续就需要使用CGI模式进行处理,所以就需要将_cgi字段设置为true。 - 如果请求方法为
POST
方法,则说明当前URI
即为客户端请求资源路径,POST
方法会通过请求正文上传数据,后续就需要使用CGI模式进行处理,所以就需要将_cgi字段设置为true。 - 接下来需要对客户端的请求资源路径进行处理,首先需要在请求资源路径上添加Web根目录,然后在判断请求资源路径的最后一个字符是否为
/
,如果为/
,就说明客户端请求资源的路径为目录,此时并不会将目录下的所以资源都进行返回,而是默认将目录下的index.html
返回给客户端,所以就需要在请求资源路径上添加上index.html
; - 对请求资源路径进行处理以后,需要通过stat函数来获取请求资源文件的属性信息,如果客户端的请求资源路径是一个目录,则需要在请求资源路径后面添加上
index.html
,然后重新获取请求资源文件的属性信息;如果客户端请求的是一个可执行程序,则说明后续需要使用到CGI模式,需要将_cgi字段设置为true; - 最后就需要根据HTTP请求中的_cgi字段来判断是进行CGI处理还是非CGI处理。
class EndPoint
{
public:// 处理客户端发送过来的http请求void HandlerHttpRequest(){auto &code = _http_response._status_code;if (_http_request._method != "GET" && _http_request._method != "POST") // 非法请求{LOG(FATAL, "method not right!");code = BAD_REQUEST;return;}if (_http_request._method == "GET"){// 判断是否带参size_t pos = _http_request._uri.find('?');if (pos != std::string::npos){// 说明带参, 使用CutString函数进行字符串切割Util::CutString(_http_request._uri, _http_request._path, _http_request._query_string, "?");// 设置_cgi字段为true_http_request._cgi = true;}else // 说明不带参,此时URI就为请求资源路径{_http_request._path = _http_request._uri;}}else if (_http_request._method == "POST"){// 说明此时URI就为客户端请求资源路径_http_request._path = _http_request._uri;// 设置_cgi字段为true_http_request._uri = true;}else{// 什么也不做}// 处理请求资源路径auto &path = _http_request._path;_http_request._path = WEB_ROOT;_http_request._path += path;// 判断请求资源路径是否已/结尾if (_http_request._path[_http_request._path.size() - 1] == '/'){// 需要在目录后面添加上"index.html"_http_request._path += HOME_PAGE;}// 通过stat函数来获取请求资源文件的属性struct stat st;if (stat(_http_request._path.c_str(), &st) == 0){// 说明获取请求资源文件属性成功,该资源存在if (S_ISDIR(st.st_mode)) // 说明该资源是一个目录{// 需要在目录后面添加"/"_http_request._path += "/";// 然后再拼接"index.html"_http_request._path += HOME_PAGE;// 重新获取该请求资源文件的属性信息stat(_http_request._path.c_str(), &st);}else if (st.st_mode & S_IXUSR || st.st_mode & S_IXGRP || st.st_mode & S_IXOTH){// 该资源是一个可执行程序_http_request._cgi = true; // 设置CGI字段为true}_http_response._size = st.st_size; // 设置请求资源文件的大小}else // 请求的资源文件信息不存在{LOG(WARNING, _http_request._path + "NOT FOUND");code = NOT_FOUND;return;}// 获取请求资源文件的后缀size_t pos = _http_request._path.rfind('.');if (pos == std::string::npos){_http_response._suffix = ".html";}else{_http_response._suffix = _http_request._path.substr(pos);}// 是否进行CGI处理if (_http_request._cgi == true){code = ProcessCgi(); // 以CGI方式进行处理}else{code = ProcessNonCgi(); // 简单的静态网页返回}}
private:int _sock; // 用于进程间通信的套接字;HttpRequest _http_request; // 表示客户端发送来的http请求HttpReponse _http_response; // 表示构建的http响应
};
- 本项目所实现的HTTP服务器只支持GET方法和POST方法,除开他们以外的方法都是错误方法,如果需要支持其他方法,只需要添加对应的业务逻辑即可;
- 服务器向外提供的资源都会存放在Web根目录下,比如网页、图片、视频等资源,本项目中Web根目录的名称为wwwroot,Web根目录下所有子目录都会有一个首页文件,当用户请求的资源是一个目录时,默认返回该目录下的首页文件,而我们Web根目录下的首页文件是以
index.html
为结尾的; - 调用stat函数获取请求资源文件的属性,实际上会获取对应的文件inode编号,文件的权限,文件的大小,如果调用stat函数失败的话,则说明该文件不存在,此时就直接在请求文件路径后面添加上
NOT_FOUND
,然后返回; - 当获取到一个文件的属性以后发现该文件是一个目录,那么该文件一定不是以
/
结尾的,因为在上面的逻辑中已经进行了处理,所以此时就需要在该资源文件路径下添加/index.html
,然后重新获取请求资源文件的属性信息; - 只要文件的拥有者,所属组,other其中一个具有可执行权限,就说明该文件是一个可执行程序,此时就需要使用CGI模式进行处理,就需要将_cgi设置为true;
- 后续构建HTTP响应需要用到响应文件的后缀,在这儿我们可以对请求的资源文件从后往前查找
.
,如果没有找到就以.html
结尾,找到了就以找到的字符串结尾; - 由于请求资源文件的大小后续可能会用到,因此在获取到请求资源文件的属性后,可以将请求资源文件的大小保存到HTTP响应类的size中。
CGI处理
CGI在进行程序处理之前需要需要与子进程进行程序替换,在创建子进程之前需要先创建两个匿名管道文件,在这里我们站在父进程的角度对管道文件进行命名,父进程用于读取数据的管道文件叫做input,父进程用于写入数据的管道文件叫做output。
创建子进程以后,父子进程需要关闭各自管道对应的读写端:
- 对于父进程来说,input是用来读取数据的,所以需要保留input[0]而关闭input[1],output是用来写入数据的,所以需要关闭output[0]而保留output[1];
- 对于子进程来说,intput是用来写入数据的,所以需要关闭input[0]保留input[1],output是用来读取数据的,所以需要保留onput[0]关闭output[1];
此时父子进程间的通信信道已经建立好了,我们还需要做的就是让CGI程序从标准输入读取数据,向标准输出写入数据,所以在子进程程序替换之前还需要进行文件描述符的重定向工作。
假设子进程intput[1] 和output[0]对应的文件描述符为3和4,此时对应文件描述符就有如下关系:
此时就需要将子进程标准输入重定向到output,标准输出重定向到input,如下图所示:
同时,子进程在进行程序替换之前,还需要进行各种参数的传递:
- 首先需要将请求方法使用putenv函数导入到环境变量中,供CGI程序判断以哪种方式读取父进程传递过来的参数;
- 如果请求方法为GET方法,则需要将URI携带的参数导入环境变量来传递给CGI程序;
- 如果请求方法是POST方法,则需要将请求正文的长度导入到环境变量,来传递给CGI程序,供CGI程序来判断需要从管道文件中读取多少个参数;
完成上述工作以后,此时就可以进行子进程程序替换了:
- 如果请求方法为POST方法,父进程所做的工作就是将请求正文参数写入管道文件中,以供CGI程序进行读取;
- 然后父进程就需要不断地调用read函数从管道文件中读取数据,将数据存储到HTTP响应的response_body中;
- 管道中的数据读取完毕以后,父进程需要调用waitpid函数等待CGI程序退出,最后关闭对应的文件描述符input[0]以及output[1],防止文件描述符泄漏。
class EndPoint
{
public:// 进行CGI程序处理int ProcessCgi(){int code = OK;auto &bin = _http_request._path; // 需要执行的CGI程序auto &method = _http_request._method; // 请求方法// 需要传递给CGI程序的参数auto &query_string = _http_request._query_string; // GET方法auto &request_body = _http_request._request_body; // POST方法int content_length = _http_request._content_length; // 请求正文长度auto &response_body = _http_response._reponse_body; // CGI程序处理完成以后添加到响应正文参数// 管道文件的创建int input[2];if (pipe(input) < 0){// 管道文件创建失败,返回相应的状态码LOG(ERROR, "pipe input failed!");code = INTERNAL_SERVER_ERROR;return code;}int output[2];if (pipe(output) < 0){LOG(error, "pipe output failed!");code = INTERNAL_SERVER_ERROR;return code;}// 创建子进程pid_t pid = fork();if (pid == 0) // 子进程{// 子进程关闭两个管道对应的读写端close(input[0]);close(output[1]);// 将请求方法通过环境变量传参std::string method_env = "METHOD=";method_env += method;putenv((char *)method_env.c_str());// 判断请求方法if (method == "GET"){// 将query_string通过环境变量进行传参std::string query_string_env = "QUERY_STRING_ENV=";query_string_env += query_string;putenv((char *)query_string_env.c_str());LOG(INFO, "GET Method, Add Query_String env!");}else if (method == "POST"){// 将正文长度通过环境变量进行传参std::string content_length_env = "CONTENT_LENGTH=";content_length_env += std::to_string(content_length);putenv((char *)content_length_env.c_str());LOG(INFO, "POST Method, Add Content_Length env!");}else{// 什么也不做}// 将子进程标准输入以及输出进行重定向dup2(output[0], 0);dup2(input[1], 1);// 将子进程替换为对应的CGI程序execl(bin.c_str(), bin.c_str(), nullptr);exit(1); // 替换失败}else if (pid < 0){LOG(ERROR, "fork error!");code = INTERNAL_SERVER_ERROR;return code;}else{// 父进程// 关闭两个管道文件对应的读写端close(input[1]);close(output[0]);// 将请求正文参数写入管道文件if (method == "POST"){const char *start = _http_request._request_body.c_str();int total = 0;int size = 0;if (total < content_length && (size = write(output[1], start + total, request_body.size() - total) > 0)){total += size;}}// 读取CGI程序的处理结果char ch = 0;while (read(input[0], &ch, 1) > 0){response_body.push_back(ch);}// 等待CGI程序退出int status = 0;pid_t ret = waitpid(pid, &status, 0);if (ret == pid){if (WIFEXITED(status) == 0){// 结果正确LOG(INFO, "CGI program exit normally with correct results");code = OK;}else{LOG(INFO, "CGI program exit normally with incorrect results");code = BAD_REQUEST;}}else{LOG(INFO, "CGI program exit abnormaly");code = INTERNAL_SERVER_ERROR;}}// 关闭对应文件描述符close(input[0]);close(output[1]);return code;}
private:int _sock; // 用于进程间通信的套接字;HttpRequest _http_request; // 表示客户端发送来的http请求HttpReponse _http_response; // 表示构建的http响应
};
- 当管道文件创建失败或者是子进程创建失败时,属于服务器请求时出错,此时就可以将状态码设置为
INTERNAL_SERVER_ERROR
,然后停止处理即可。 - 环境变量是
key=value
形式的,因此在调用putenv
函数导入环境变量前需要先正确构建环境变量,此后被替换的CGI程序在调用getenv
函数时,就可以通过key
获取到对应的value
。 - 子进程传递参数的代码最好放在重定向之前,否则服务器运行后无法看到传递参数对应的日志信息,因为日志是以
cout
的方式打印到标准输出的,而dup2
函数调用后标准输出已经被重定向到了管道,此时打印的日志信息将会被写入管道。 - 父进程循环调用
read
函数从管道中读取CGI程序的处理结果,当CGI程序执行结束时相当于写端进程将写端关闭了(文件描述符的生命周期随进程),此时读端进程将管道当中的数据读完后,就会继续执行后续代码,而不会被阻塞。 - 父进程在等待子进程退出后,可以通过
WIFEXITED
判断子进程是否是正常退出,如果是正常退出再通过WEXITSTATUS
判断处理结果是否正确,然后根据不同情况设置对应的状态码(此时就算子进程异常退出或处理结果不正确也不能立即返回,需要让父进程继续向后执行,关闭两个管道对应的文件描述符,防止文件描述符泄露)。
非CGI处理
非CGI模式处理只需要将客户端请求的资源构建成HTTP响应直接发送给客户端即可,也就是打开资源文件,将资源文件中的内容拷贝到HTTP响应中的reponse_body中,然后在进行HTTP响应的时候发送给客户端即可,但是并不推荐这种做法。
因为reponse_body是处于用户级缓冲区的,而请求资源文件是处于磁盘中中,我们在获取资源的过程中,首先需要将磁盘的文件资源加载到内核缓冲区,然后再由操作系统将其拷贝到用户缓冲区,而发送响应正文时也是先将内容拷贝到内核缓冲区,再由内核缓冲区有操作系统发送给网卡。
对于上述在内核层和用户层之间的来回拷贝动作,本质上是不需要的,我们可以直接将磁盘文件读取到内核,再由内核缓冲区通过操作系统发送给网卡,这就需要用到我们的sendfile函数,函数的功能就是将数据从一个文件描述符拷贝到另一个文件描述符,并且这个拷贝操作是在内核中完成的,因此sendfile比单纯的调用read和write更加高效。
但是需要注意的是,这里还不能直接调用sendfile函数,因为sendfile函数调用后文件内容就发送出去了,而我们应该构建HTTP响应后再进行发送,因此我们这里要做的仅仅是将要发送的目标文件打开即可,将打开文件对应的文件描述符保存到HTTP响应的fd当中。
class EndPoint
{
public:// 非CGI模式进行处理int ProcessNonCgi(){// 打开客户端的请求资源文件,以供后续发送_http_response._fd = open(_http_request._path.c_str(), O_RDONLY);if (_http_response._fd >= 0){// 打开文件成功return OK;}return INTERNAL_SERVER_ERROR;}
private:int _sock; // 用于进程间通信的套接字;HttpRequest _http_request; // 表示客户端发送来的http请求HttpReponse _http_response; // 表示构建的http响应
};
- 如果打开文件失败,则返回
INTERNAL_SERVER_ERROR
状态码表示服务器处理请求时出错,而不能返回NOT_FOUND
,因为之前调用stat获取过客户端请求资源的属性信息,说明该资源文件是一定存在的。
构建HTTP响应
构建HTTP响应首先要构建的就是状态行,状态行由HTTP版本,状态码,状态码描述组成,他们之间以空格为分隔符,状态行构建好以后将对应数据保存在status_line 中即可,但相应报头需要根据HTTP请求是否正常处理完毕来进行构建。
class EndPoint
{
public:// 构建发送给客户端的http响应void BuildHttpResponse(){int code = _http_response._status_code;auto& status_line = _http_response._status_line; // 状态行// 构建状态行status_line += HTTP_VERSION;status_line += " ";status_line += std::to_string(code);status_line += " ";status_line += CodeToDesc(code);status_line += LINE_END;// 构建响应报头std::string path = WEB_ROOT;path += "/";switch(code){case OK:BuildiOkResponse();break;case NOT_FOUND:path += PAGE_404;HandlerError(path);break;case BAD_REQUEST:path += PAGE_404;HandlerError(path);break;case INTERNAL_SERVER_ERROR:HandlerError(path);path += PAGE_404;break;default:break;}}
private:int _sock; // 用于进程间通信的套接字;HttpRequest _http_request; // 表示客户端发送来的http请求HttpReponse _http_response; // 表示构建的http响应
};
- 本项目中将服务器的行分隔符设置为\r\n,在构建完状态行以及每行响应报头之后都需要加上对应的行分隔符,而在HTTP响应类的构造函数中已经将空行初始化为了LINE_END,因此在构建HTTP响应时不用处理空行。
对于状态行中的状态码描述,我们可以编写一个函数,该函数能够根据状态码返回对应的状态码描述。
// 状态行描述
static std::string CodeToDesc(int code)
{std::string desc;switch (code){case OK:desc = "OK";break;case NOT_FOUND:desc = "NOT_FOUND";break;default:break;}return desc;
}
构建响应报头(请求正常处理)
在HTTP请求被正确处理完成以后,就可以进行响应报头的创建,构建响应报头我们至少需要构建Content_Length
和 Content_Type
两个选项。
我们需要通过请求资源文件的后缀来得知返回资源的类型,返回资源的大小需要通过请求被处理的方式来得知,如果是以非CGI的方式进程处理的,那么返回资源的大小早已在获取请求资源属性时被保存到了HTTP响应类中的size当中,如果该请求是以CGI方式进行处理的,那么返回资源的大小应该是HTTP响应类中的response_body的大小。
class EndPoint
{
private:// 构建响应报头void BuildiOkResponse(){std::string content_type = "Content_Type: ";content_type += SuffixToDesc(_http_response._suffix);content_type += LINE_END;_http_response._reponse_header.push_back(content_type);std::string content_length = "Content_Length: ";if (_http_request._cgi){// 以CGI模式进行操作content_length += std::to_string(_http_response._reponse_body.size());}else{// 以非CGI模式进行操作content_length += std::to_string(_http_response._size);}content_length += LINE_END;_http_response._reponse_header.push_back(content_length);}
private:int _sock; // 用于进程间通信的套接字;HttpRequest _http_request; // 表示客户端发送来的http请求HttpReponse _http_response; // 表示构建的http响应
};
对于返回资源的类型,我们可以编写一个函数,该函数能够根据文件后缀返回对应的文件类型。查看Content-Type转化表可以得知后缀与文件类型的对应关系,将这个对应关系存储一个unordered_map容器中,当需要根据后缀得知文件类型时直接在这个unordered_map容器中进行查找,如果找到了则返回对应的文件类型,如果没有找到则默认该文件类型为text/html
。
// 根据后缀获取资源类型
static std::string SuffixToDesc(const std::string &suffix)
{static std::unordered_map<std::string, std::string> suffix_to_desc = {{".html", "text/html"},{".css", "text/css"},{".js", "application/x-javascript"},{".jpg", "application/x-jpg"},{".xml", "text/xml"}};auto iter = suffix_to_desc.find(suffix);if (iter != suffix_to_desc.end()){return iter->second;}return "text/html"; // 所给后缀未找到则默认该资源为html文件
}
构建响应报头(请求出现错误)
对于错误的HTTP请求,服务器会返回错误的页面,因此返回的资源类型就是text/html
,返回的资源大小可以通过获取对应的错误请求资源文件的属性来获得,此外,为了后续发送响应时可以直接调用sendfile
进行发送,这里需要将错误页面对应的文件打开,并将对应的文件描述符保存在HTTP响应类的fd当中。
// 构建HTTP响应吧(请求错误)
void HandlerError(std::string page)
{_http_request._cgi = false; // 返回对应错误界面(使用非CGI模式进行返回)_http_response._fd = open(page.c_str(), O_RDONLY);if(_http_response._fd > 0){// 打开文件成功// 构建响应报头struct stat st;stat(page.c_str(), &st); // 获取错误文件的属性信息std::string content_type = "Content_Type: text/html";content_type += LINE_END;_http_response._reponse_header.push_back(content_type);std::string content_length = "Content_Length: ";content_length += std::to_string(st.st_size);content_length += LINE_END;_http_response._reponse_header.push_back(content_length);_http_response._size = st.st_size;}
}
在这儿需要特别注意的是对于请求处理错误的HTTP响应,需要将HTTP请求中的_cgi字段设置为重新设置为false,因为在后续的将响应文件发送给客户端的过程中,会判断是否以CGI形式进行发送,如果是请求处理出错的HTTP响应本质上就是返回一个错误的页面,是以非CGI的方式进行处理的。
发送HTTP响应
- 首先需要依次发送响应行,响应报头以及空行的内容;
- 对于响应正文的发送需要进行判断是否是通过CGI的方式进行处理,如果是通过CGI的方式进行处理,那么待发送的响应正文就保存在HTTP响应的_response_body中,直接调用send函数进行发送即可;
- 如果是通过非CGI的方式进行处理或者是处理过程中出错的,那么对应待发送资源文件或者错误页面文件的文件描述符就保存在HTTP响应文件中,此时调用sendfile函数直接进行发送即可,发送完成以后关闭对应的文件描述符即可。
// 发生http响应给客户端
void SendHttpResponse()
{// 发送状态行send(_sock, _http_response._status_line.c_str(), _http_response._status_line.size(), 0);// 发送响应报头for(auto& iter : _http_response._reponse_header){send(_sock, iter.c_str(), iter.size(), 0);}// 发送空行send(_sock, _http_response._blank.c_str(), _http_response._blank.size(), 0);// 发送响应正文if(_http_request._cgi){// CGI模式进行处理auto& response_body = _http_response._reponse_body;const char* start = response_body.c_str();size_t size = 0;size_t total = 0;while(total < response_body.size() && (size = send(_sock, start + total, response_body.size() - total, 0)) > 0){total += size;}}else {// 非CGI模式进行处理sendfile(_sock, _http_response._fd, nullptr, _http_response._size);// 关闭对应的文件描述符close(_http_response._fd);}
}
差错处理
逻辑错误
逻辑错误主要是服务器在处理请求的过程中出现的一些错误,比如请求方法不正确、请求资源不存在或服务器处理请求时出错等等。逻辑错误其实我们已经处理过了,当出现这类错误时服务器会将对应的错误页面返回给客户端。
读取错误
逻辑错误是在服务器处理请求过程中出现的错误,而读取错误是在读取请求的过程中出现的错误,他是在服务器处理请求之前的错误,比如调用recv函数读取出错或者是对端连接关闭,此时就意味着服务器都没有成功的读取客户端发来的HTTP请求,更不需要进行后续处理请求,构建响应和发送响应了。
可以在EndPoint类定义一个bool类型的_stop成员,表示是否停止此次处理,默认值设置为false,当读取请求出错时就直接设置stop为true并不再进行后续的读取操作,因此读取HTTP请求的代码需要稍作修改。
class EndPoint
{
public:EndPoint(int sock) : _sock(sock){}// 判断是否需要停止本次处理bool IsStop(){return _stop;}// 读取客户端发送过来的http请求void RecvHttpRequest(){if (!RecvHttpRequestLine() && !RecvHttpRequestHeader()){// 短路求职ParseHttpRequestLine(); // 解析请求行ParseHttpRequestHeader(); // 解析请求报头RecvHttpRequestBody(); // 读取请求报头}}
private:// 读取请求行bool RecvHttpRequestLine(){auto &line = _http_request._request_line;if (Util::ReadLine(_sock, line) > 0){line.resize(line.size() - 1); // 去掉读取上来的\nLOG(INFO, line);}else{_stop = true; // 读取出错,不做任何处理}return _stop;}// 读取请求报头与空行bool RecvHttpRequestHeader(){std::string line;while (true){line.clear(); // 每一次读取之前先清空if (Util::ReadLine(_sock, line) <= 0){// 读取出错_stop = true;break;}if (line == "\n"){// 读到空行_http_request._blank = line;break;}// 读取一行请求报头line.resize(line.size() - 1); // 去掉最后读取上来的\n_http_request._request_header.push_back(line);LOG(INFO, line);}return _stop;}// 读取请求正文bool RecvHttpRequestBody(){if (IsNeedRecvHttpRequestBody()) // 判断是否需要读取请求正文{int content_length = _http_request._content_length;auto body = _http_request._request_body;// 读取正文char ch = 0;while (content_length){ssize_t size = recv(_sock, &ch, 1, 0);if (size > 0){body.push_back(ch);content_length--;}else{_stop = true;break;}}}return _stop;}private:int _sock; // 用于进程间通信的套接字;HttpRequest _http_request; // 表示客户端发送来的http请求HttpReponse _http_response; // 表示构建的http响应bool _stop;
};
- 我们可以将读取请求行,读取请求报头和空行以及读取请求正文的返回值设置为bool类型,只有当请求行读取完成以后再进想请求报头和空行的读取,然后进行解析操作,再读取请求正文,利用逻辑运算符的短路求值策略。
- EndPoint类当中提供了IsStop函数,用于让外部处理线程得知是否应该停止本次处理。
此时服务器创建的新线程在读取请求后,就需要判断是否应该停止本次处理,如果需要则不再进行处理请求、构建响应以及发送响应操作,而直接关闭于客户端建立的套接字即可。
class CallBack
{
public:CallBack(){}static void *HandlerRequest(void *arg){LOG(INFO, "handler request begin");std::cout << "get a new link..." << std::endl;int sock = *(int*)arg;EndPoint *ep = new EndPoint(sock);ep->RecvHttpRequest();if (!ep->IsStop()){LOG(INFO, "Recv No Error, Begin Handler Request");ep->BuildHttpResponse();ep->SendHttpResponse();}else{LOG(WARNING, "Recv Error, Stop Handler Request");}close(sock);delete ep;LOG(INFO, "handler request end");return nullptr;}~CallBack(){}
};
写入错误
在读取请求的过程中会出现读取错误,处理请求的过程中会出现逻辑错误,构建响应结束发送响应的过程就会出现写入错误,当发生写入错误时,此时就不需要在进行后续的发送响应了,直接将_stop字段设置为true,不再进行后续的发送工作。
所以对于构建HTTP响应的代码我们依然需要进行修改:
// 发生http响应给客户端
bool SendHttpResponse()
{// 发送状态行if (send(_sock, _http_response._status_line.c_str(), _http_response._status_line.size(), 0) < 0){// 发送失败, _stop字段设置为true_stop = true;}// 发送响应报头for (auto &iter : _http_response._reponse_header){if (send(_sock, iter.c_str(), iter.size(), 0) < 0){_stop = true;break;}}// 发送空行if (send(_sock, _http_response._blank.c_str(), _http_response._blank.size(), 0) < 0){_stop = true;}// 发送响应正文if (_http_request._cgi){// CGI模式进行处理auto &response_body = _http_response._reponse_body;const char *start = response_body.c_str();size_t size = 0;size_t total = 0;while (total < response_body.size() && (size = send(_sock, start + total, response_body.size() - total, 0)) > 0){total += size;}}else{if (!_stop){// 非CGI模式进行处理if (sendfile(_sock, _http_response._fd, nullptr, _http_response._size) < 0){_stop = true;}}// 关闭对应的文件描述符close(_http_response._fd);}return _stop;
}
当服务器发送响应出错时会收到SIGPIPE信号,而该信号的默认处理动作是终止当前进程,为了防止服务器因为写入出错而被终止,需要在初始化HTTP服务器时调用signal函数忽略SIGPIPE信号。
class HttpServer
{
public:// 初始化服务器void InitServer(){signal(SIGPIPE, SIG_IGN); // 忽略SIGPIPE信号,防止写入时崩溃}
private:int _port; // 端口号
};
引入线程池
- 上面版本的服务器在每次接收到任务时就会创建一个线程,而当任务完成以后就会将创建的线程销毁,就频繁的进行线程的创建以及销毁工作,效率低下;
- 如果同时有大量客户端的连接请求,此时服务器就会创建大量的线程,线程越多,CPU的压力也就越大,因为CPU需要进行大量的线程切换工作;就意味着线程一旦过多,每一个线程被再次调度的周期就变长了,那么就会导致客户端迟迟得不到响应。
这是我们就可以引入线程池了:
- 在服务器中预先创建一批线程和一个任务队列,当服务器每次收到一个请求以后就会将其封装成一个任务去添加到任务队列中去;
- 线程池中的若干线程就会不断的从任务队列中获取任务进行相应的处理工作,如果任务队列中没有任务就进入休眠状态,当有新任务出现时就会唤醒线程进行任务处理。
设计任务类
服务器获取到一个新连接以后,会将其封装成一个任务放进任务队列中。任务类首先需要提供一个通信套接字,与客户端进行通信,其次就需要一个回调函数,当线程池中线程获取到这个任务以后调用回调函数进行处理。
#include "Protocol.hpp"class Task
{public:Task(){}Task(int sock):_sock(sock){}// 处理任务void ProcessOn(){_handler(_sock);}~Task(){}private:int _sock; // 与客户端通信套接字CallBack _handler; // 回调函数
};
- 任务类需要提供一个无参的构造函数,因为后续从任务队列中获取任务时,需要先以无参的方式定义一个任务对象,然后再以输出型参数的方式来获取任务。
任务回调函数的编写
任务类中处理任务的回调函数,其实就是我们之前创建新线程时传入的执行例程CallBack::HandlerRequest
,我们可以将CallBack类的()运算符重载为调用HandlerRequest函数,这时CallBack对象就变成了一个仿函数对象,这个仿函数对象被调用时实际就是在调用HandlerRequest函数。
class CallBack
{
public:CallBack(){}void operator()(int sock){HandlerRequest(sock);}void HandlerRequest(int sock){LOG(INFO, "handler request begin");std::cout << "get a new link..." << std::endl;EndPoint *ep = new EndPoint(sock);ep->RecvHttpRequest();if (!ep->IsStop()){LOG(INFO, "Recv No Error, Begin Handler Request");ep->BuildHttpResponse(); // 构建响应ep->SendHttpResponse(); // 发送响应if(ep->IsStop()){LOG(WARNING, "Send Error, Stop Send Response");}}else{LOG(WARNING, "Recv Error, Stop Handler Request");}close(sock);delete ep;LOG(INFO, "handler request end");}~CallBack(){}
};
其实构建响应的本质过程就是对请求进行处理,处理完毕以后就进行响应的构建,所以在此我们对可以将处理请求的过程直接放在构建响应的步骤中,在此我们使用goto语句,只要处理请求过程中出现错误时直接跳转至构建响应函数即可,进行对应的处理工作。
// 处理客户端发送过来的http请求void BuildHttpResponse(){auto &code = _http_response._status_code;std::string path;struct stat st;size_t pos = 0;if (_http_request._method != "GET" && _http_request._method != "POST") // 非法请求{LOG(FATAL, "method not right!");code = BAD_REQUEST;goto END;}if (_http_request._method == "GET"){// 判断是否带参size_t pos = _http_request._uri.find('?');if (pos != std::string::npos){// 说明带参, 使用CutString函数进行字符串切割Util::CutString(_http_request._uri, _http_request._path, _http_request._query_string, "?");// 设置_cgi字段为true_http_request._cgi = true;}else // 说明不带参,此时URI就为请求资源路径{_http_request._path = _http_request._uri;}}else if (_http_request._method == "POST"){// 说明此时URI就为客户端请求资源路径_http_request._path = _http_request._uri;// 设置_cgi字段为true_http_request._uri = true;}else{// 什么也不做}// 处理请求资源路径path = _http_request._path;_http_request._path = WEB_ROOT;_http_request._path += path;// 判断请求资源路径是否已/结尾if (_http_request._path[_http_request._path.size() - 1] == '/'){// 需要在目录后面添加上"index.html"_http_request._path += HOME_PAGE;}std::cout << "debug: " << _http_request._path.c_str() << std::endl;if (stat(_http_request._path.c_str(), &st) == 0){// 说明获取请求资源文件属性成功,该资源存在if (S_ISDIR(st.st_mode)) // 说明该资源是一个目录{// 需要在目录后面添加"/"_http_request._path += "/";// 然后再拼接"index.html"_http_request._path += HOME_PAGE;// 重新获取该请求资源文件的属性信息stat(_http_request._path.c_str(), &st);}else if (st.st_mode & S_IXUSR || st.st_mode & S_IXGRP || st.st_mode & S_IXOTH){// 该资源是一个可执行程序_http_request._cgi = true; // 设置CGI字段为true}_http_response._size = st.st_size; // 设置请求资源文件的大小}else // 请求的资源文件信息不存在{LOG(WARNING, _http_request._path + "NOT FOUND");code = NOT_FOUND;goto END;}// 获取请求资源文件的后缀pos = _http_request._path.rfind('.');if (pos == std::string::npos){_http_response._suffix = ".html";}else{_http_response._suffix = _http_request._path.substr(pos);}// 是否进行CGI处理if (_http_request._cgi == true){code = ProcessCgi(); // 以CGI方式进行处理}else{code = ProcessNonCgi(); // 简单的静态网页返回}END:BuildHttpResponseHelper();}
线程池代码实现
在此可以将线程池设置为单例模式:
- 将线程池的构造函数设置为私有,将拷贝构造函数和赋值运算符重载函数设置为私有或者是delete;
- 提供一个指向单例对象的static指针,并在类外初始化为空;
- 提供一个全局的访问点来获取一个单例对象,在单例对象第一次被获取时就创建这个单例对象并进行初始化。
线程池的成员变量如下:
- 任务队列:用于存储未被处理的任务;
- 线程数量:用于表示线程池中的线程个数;
- 互斥锁:保证任务队列在多线程环境下的线程安全;
- 条件变量:当任务队列中没有任务时,让该线程在条件变量下进行等待,当有任务出现时,唤醒在该条件变量下等待的线程;
- 指向单例对象的指针:用于指向唯一的单例线程池对象。
线程池的成员函数如下:
- 构造函数:完成互斥锁和条件变量的初始化工作;
- 析构函数:完成互斥锁和条件变量的释放工作;
- InitThreadPool:在初始化阶段完成线程池若干线程的创建;
- PushTask:生产任务时调用,将任务放入任务队列中,并唤醒在条件变量下等待的一个线程进行处理。
- PopTask:消费任务时调用,从任务队列中获取一个任务对象;
- ThreadRoutine:线程池中每个线程的执行例程,完成线程分离以后不断检测是否有新的任务生成,如果有,调用PopTask进行任务处理,如果没有则进行休眠等待被唤醒;
- GetInstance:获取单例对象线程池时调用,如果线程池对象没有被常见则创建线程池对象,如果线程池对象已经被创建则返回线程池对象。
#define NUM 6class ThreadPool
{
public:// 创建一个全局访问点获取单例对象static ThreadPool *GetInstance(){static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 定义一个静态的锁// 双重判断if (_inst == nullptr){pthread_mutex_lock(&mutex); // 加锁if (_inst == nullptr){_inst = new ThreadPool(); // 创建一个线程池对象_inst->InitThreadPool(); // 初始化线程池}pthread_mutex_unlock(&mutex); // 解锁}return _inst;}// 线程执行例程static void *ThreadRoutine(void *arg){pthread_detach(pthread_self()); // 线程分离ThreadPool *tp = (ThreadPool *)arg;while (true){tp->LockQueue(); // 加锁while (tp->IsEmpty()){// 任务队列为空,线程进行等待tp->ThreadWait();}Task task; // 创建任务tp->PopTask(task); // 获取任务tp->UnlockQueue(); // 解锁task.ProcessOn(); // 调用回调函数对任务进行处理}}// 初始化线程池bool InitThreadPool(){// 创建若干线程pthread_t tid;for (int i = 0; i < NUM; i++){if (pthread_create(&tid, nullptr, ThreadRoutine, this) != 0){LOG(FATAL, "create pthread failed");return false;}}LOG(INFO, "create pthread success");return true;}// 将任务Push进任务队列void PushTask(Task &task){LockQueue(); // 加锁_task_queue.push(task); // 插入任务UnlockQueue(); // 解锁ThreadWeakUp(); // 唤醒一个线程对任务进行处理}// 从任务队列中拿任务void PopTask(Task &task){task = _task_queue.front();_task_queue.pop();}// 析构函数~ThreadPool(){pthread_mutex_destroy(&_mtx); // 释放条件变量pthread_cond_destroy(&_cond); // 释放条件变量}private:// 构造函数设置为私有ThreadPool() : _num(NUM){pthread_mutex_init(&_mtx, nullptr); // 初始化互斥锁pthread_cond_init(&_cond, nullptr); // 初始化条件变量}// 拷贝构造和赋值运算符重载设置为deleteThreadPool(const Task &) = delete;ThreadPool *operator=(const Task &) = delete;// 判断任务队列是否为空bool IsEmpty(){return _task_queue.empty();}// 对任务队列进行加锁void LockQueue(){pthread_mutex_lock(&_mtx);}// 对任务队列进行解锁void UnlockQueue(){pthread_mutex_unlock(&_mtx);}// 让线程在条件变量下进行等待void ThreadWait(){pthread_cond_wait(&_cond, &_mtx);}// 唤醒正在等待的线程void ThreadWeakUp(){pthread_cond_signal(&_cond);}private:std::queue<Task> _task_queue; // 任务队列int _num; // 线程数量pthread_mutex_t _mtx; // 互斥锁pthread_cond_t _cond; // 条件变量static ThreadPool *_inst; // 指向单例对象的static指针
};ThreadPool *ThreadPool::_inst = nullptr; // 初始化单例对象为nullptr
- 由于线程的执行例程只有一个
void*
参数,所以需要将ThreadRoutine
设置为静态成员函数,防止this
指针的影响,但是在ThreadRoutine
函数中我们又需要调用线程池的部分成员函数,所以我们我在创建线程过程中需要需要将this
指针作为参数传入传递给线程执行例程; - 对于向任务队列中插入任务和从任务队列中获取任务,都需要互斥锁,但是对于获取任务来说,我们已经在调用线程的执行例程访问任务队列是进行了加锁解锁操作,并不需要在PopTask的内部进行实现;
- 只有当任务队列中有任务线程才会被唤醒,没有任务时就会进入休眠状态,等待被唤醒,因此需要进行判空操作。
引入线程池以后,我们所需要做的就是创建一个任务,将任务插入进任务队列中即可,后续线程池中的线程就会获取到这个任务并且进程任务的处理工作,最终将响应发送回客户端。
#define PORT 8081class HttpServer
{
public:// 构造函数HttpServer(int port = PORT) : _port(port){}// 初始化服务器void InitServer(){signal(SIGPIPE, SIG_IGN); // 忽略SIGPIPE信号,防止写入时崩溃}void Loop(){TcpServer *tsvr = TcpServer::GetInstance(_port);LOG(INFO, "Loop begin!");while (true){struct sockaddr_in peer;socklen_t len = sizeof(peer);int sock = accept(tsvr->Sock(), (struct sockaddr *)&peer, &len);if (sock < 0){continue;}LOG(INFO, "get a new link!");// 构建任务插入进任务队列当中Task task(sock);ThreadPool::GetInstance()->PushTask(task);}}
private:int _port; // 端口号
};
项目测试
服务器结构
至此HTTP服务器后端逻辑已经全部编写完毕,此时我们要做的就是将对外提供的资源文件放在一个名为wwwroot的目录下,然后将生成的HTTP服务器可执行程序与wwwroot放在同级目录下。
本项目对于错误文件页面处理均使用404.html
,内容如下:
<html><head><meta charset="UTF-8"></head><body><h1>对不起,你说查找的资源不存在(404)</h1></body>
</html>
服务器首页编写
服务器的web根目录下的资源文件主要有两种,一种就是用于处理客户端上传上来的数据的CGI程序,另一种就是供客户端请求的各种网页文件了,而网页的制作实际是前端工程师要做的,但现在我们要对服务器进行测试,至少需要编写一个首页,首页文件需要放在web根目录下,取名为index.html。
内容如下:
<!DOCTYPE html>
<html lang="en"><head><title>Logistics — Colorlib Website Template</title><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"><link rel="stylesheet" href="static/css/style.css"><link rel="stylesheet" href="static/css/bootstrap.min.css"><link rel="stylesheet" href="static/css/magnific-popup.css"><link rel="stylesheet" href="static/css/jquery-ui.css"><link rel="stylesheet" href="static/css/owl.carousel.min.css"><link rel="stylesheet" href="static/css/owl.theme.default.min.css"><link rel="stylesheet" href="static/css/bootstrap-datepicker.css"><link rel="stylesheet" href="static/css/flaticon.css"><link rel="stylesheet" href="static/css/aos.css"><link rel="stylesheet" href="static/css/style1.css"></head><body><div class="site-wrap"><div class="site-mobile-menu"><div class="site-mobile-menu-header"><div class="site-mobile-menu-close mt-3"><span class="icon-close2 js-menu-toggle"></span></div></div><div class="site-mobile-menu-body"></div></div><header class="site-navbar py-3" role="banner"><div class="container"><div class="row align-items-center"><div class="col-11 col-xl-2"><h1 class="mb-0"><a href="" class="text-white h2 mb-0">Logistics</a></h1></div><div class="col-12 col-md-10 d-none d-xl-block"><nav class="site-navigation position-relative text-right" role="navigation"><ul class="site-menu js-clone-nav mx-auto d-none d-lg-block"><li class="active"><a href="">Home</a></li><li><a href="about.html">About Us</a></li><li class="has-children"><a href="services.html">Services</a><ul class="dropdown"><li><a href="#">Air Freight</a></li><li><a href="#">Ocean Freight</a></li><li><a href="#">Ground Shipping</a></li><li><a href="#">Warehousing</a></li><li><a href="#">Storage</a></li></ul></li><li><a href="industries.html">Industries</a></li><li><a href="blog.html">Blog</a></li><li><a href="contact.html">Contact</a></li></ul></nav></div><div class="d-inline-block d-xl-none ml-md-0 mr-auto py-3" style="position: relative; top: 3px;"><a href="#" class="site-menu-toggle js-menu-toggle text-white"><span class="icon-menu h3"></span></a></div></div></div></header></div><div class="site-blocks-cover overlay" style="background-image: url(static/image/hero_bg_1.jpg);" data-aos="fade" data-stellar-background-ratio="0.5"><div class="container"><div class="row align-items-center justify-content-center text-center"><div class="col-md-8" data-aos="fade-up" data-aos-delay="400"><h1 class="text-white font-weight-light mb-5 text-uppercase font-weight-bold">Worldwide Freight Services</h1><p><a href="#" class="btn btn-primary py-3 px-5 text-white">Get Started!</a></p></div></div></div></div> <div class="container"><div class="row align-items-center no-gutters align-items-stretch overlap-section"><div class="col-md-4"><div class="feature-1 pricing h-100 text-center"><div class="icon"><span class="icon-dollar"></span></div><h2 class="my-4 heading">Best Prices</h2><p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Perspiciatis ipsum odio minima tempora animi iure.</p></div></div><div class="col-md-4"><div class="free-quote bg-dark h-100"><h2 class="my-4 heading text-center">Get Free Quote</h2><form method="post"><div class="form-group"><label for="fq_name">Name</label><input type="text" class="form-control btn-block" id="fq_name" name="fq_name" placeholder="Enter Name"></div><div class="form-group mb-4"><label for="fq_email">Email</label><input type="text" class="form-control btn-block" id="fq_email" name="fq_email" placeholder="Enter Email"></div><div class="form-group"><input type="submit" class="btn btn-primary text-white py-2 px-4 btn-block" value="Get Quote"> </div></form></div></div><div class="col-md-4"><div class="feature-3 pricing h-100 text-center"><div class="icon"><span class="icon-phone"></span></div><h2 class="my-4 heading">24/7 Support</h2><p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Perspiciatis ipsum odio minima tempora animi iure.</p></div></div></div></div><div class="site-section"><div class="container"><div class="row justify-content-center mb-5"><div class="col-md-7 text-center border-primary"><h2 class="mb-0 text-primary">What We Offer</h2><p class="color-black-opacity-5">Lorem ipsum dolor sit amet.</p></div></div><div class="row align-items-stretch"><div class="col-md-6 col-lg-4 mb-4 mb-lg-0"><div class="unit-4 d-flex"><div class="unit-4-icon mr-4"><span class="text-primary flaticon-travel"></span></div><div><h3>Air Freight</h3><p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Perferendis quis molestiae vitae eligendi at.</p><p class="mb-0"><a href="#">Learn More</a></p></div></div></div><div class="col-md-6 col-lg-4 mb-4 mb-lg-0"><div class="unit-4 d-flex"><div class="unit-4-icon mr-4"><span class="text-primary flaticon-sea-ship-with-containers"></span></div><div><h3>Ocean Freight</h3><p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Perferendis quis molestiae vitae eligendi at.</p><p class="mb-0"><a href="#">Learn More</a></p></div></div></div><div class="col-md-6 col-lg-4 mb-4 mb-lg-0"><div class="unit-4 d-flex"><div class="unit-4-icon mr-4"><span class="text-primary flaticon-frontal-truck"></span></div><div><h3>Ground Shipping</h3><p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Perferendis quis molestiae vitae eligendi at.</p><p class="mb-0"><a href="#">Learn More</a></p></div></div></div></div></div></div><div class="site-section block-13"><!-- <div class="container"></div> --><div class="owl-carousel nonloop-block-13"><div><a href="#" class="unit-1 text-center"><img src="static/picture/img_1.jpg" alt="Image" class="img-fluid"><div class="unit-1-text"><h3 class="unit-1-heading">Storage</h3><p class="px-5">Lorem ipsum dolor sit amet consectetur adipisicing elit. Eos tempore ullam minus voluptate libero.</p></div></a></div><div><a href="#" class="unit-1 text-center"><img src="static/picture/img_2.jpg" alt="Image" class="img-fluid"><div class="unit-1-text"><h3 class="unit-1-heading">Air Transports</h3><p class="px-5">Lorem ipsum dolor sit amet consectetur adipisicing elit. Eos tempore ullam minus voluptate libero.</p></div></a></div><div><a href="#" class="unit-1 text-center"><img src="static/picture/img_3.jpg" alt="Image" class="img-fluid"><div class="unit-1-text"><h3 class="unit-1-heading">Cargo Transports</h3><p class="px-5">Lorem ipsum dolor sit amet consectetur adipisicing elit. Eos tempore ullam minus voluptate libero.</p></div></a></div><div><a href="#" class="unit-1 text-center"><img src="static/picture/img_4.jpg" alt="Image" class="img-fluid"><div class="unit-1-text"><h3 class="unit-1-heading">Cargo Ship</h3><p class="px-5">Lorem ipsum dolor sit amet consectetur adipisicing elit. Eos tempore ullam minus voluptate libero.</p></div></a></div><div><a href="#" class="unit-1 text-center"><img src="static/picture/img_5.jpg" alt="Image" class="img-fluid"><div class="unit-1-text"><h3 class="unit-1-heading">Ware Housing</h3><p class="px-5">Lorem ipsum dolor sit amet consectetur adipisicing elit. Eos tempore ullam minus voluptate libero.</p></div></a></div></div></div><div class="site-section bg-light"><div class="container"><div class="row justify-content-center mb-5"><div class="col-md-7 text-center border-primary"><h2 class="font-weight-light text-primary">More Services</h2><p class="color-black-opacity-5">We Offer The Following Services</p></div></div><div class="row align-items-stretch"><div class="col-md-6 col-lg-4 mb-4 mb-lg-4"><div class="unit-4 d-flex"><div class="unit-4-icon mr-4"><span class="text-primary flaticon-travel"></span></div><div><h3>Air Air Freight</h3><p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Perferendis quis molestiae vitae eligendi at.</p><p><a href="#">Learn More</a></p></div></div></div><div class="col-md-6 col-lg-4 mb-4 mb-lg-4"><div class="unit-4 d-flex"><div class="unit-4-icon mr-4"><span class="text-primary flaticon-sea-ship-with-containers"></span></div><div><h3>Ocean Freight</h3><p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Perferendis quis molestiae vitae eligendi at.</p><p><a href="#">Learn More</a></p></div></div></div><div class="col-md-6 col-lg-4 mb-4 mb-lg-4"><div class="unit-4 d-flex"><div class="unit-4-icon mr-4"><span class="text-primary flaticon-frontal-truck"></span></div><div><h3>Ground Shipping</h3><p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Perferendis quis molestiae vitae eligendi at.</p><p><a href="#">Learn More</a></p></div></div></div><div class="col-md-6 col-lg-4 mb-4 mb-lg-4"><div class="unit-4 d-flex"><div class="unit-4-icon mr-4"><span class="text-primary flaticon-barn"></span></div><div><h3>Warehousing</h3><p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Perferendis quis molestiae vitae eligendi at.</p><p><a href="#">Learn More</a></p></div></div></div><div class="col-md-6 col-lg-4 mb-4 mb-lg-4"><div class="unit-4 d-flex"><div class="unit-4-icon mr-4"><span class="text-primary flaticon-platform"></span></div><div><h3>Storage</h3><p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Perferendis quis molestiae vitae eligendi at.</p><p><a href="#">Learn More</a></p></div></div></div><div class="col-md-6 col-lg-4 mb-4 mb-lg-4"><div class="unit-4 d-flex"><div class="unit-4-icon mr-4"><span class="text-primary flaticon-car"></span></div><div><h3>Delivery Van</h3><p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Perferendis quis molestiae vitae eligendi at.</p><p><a href="#">Learn More</a></p></div></div></div></div></div></div><div class="site-blocks-cover overlay inner-page-cover" style="background-image: url(static/image/hero_bg_2.jpg); background-attachment: fixed;"><div class="container"><div class="row align-items-center justify-content-center text-center"><div class="col-md-7" data-aos="fade-up" data-aos-delay="400"><a href="javascript:;" class="play-single-big mb-4 d-inline-block popup-vimeo"><span class="icon-play"></span></a><h2 class="text-white font-weight-light mb-5 h1">View Our Services By Watching This Short Video</h2></div></div></div></div> <div class="site-section border-bottom"><div class="container"><div class="row justify-content-center mb-5"><div class="col-md-7 text-center border-primary"><h2 class="font-weight-light text-primary">Testimonials</h2></div></div><div class="slide-one-item home-slider owl-carousel"><div><div class="testimonial"><figure class="mb-4"><img src="static/picture/person_3.jpg" alt="Image" class="img-fluid mb-3"><p>John Smith</p></figure><blockquote><p>“Lorem ipsum dolor sit amet consectetur adipisicing elit. Consectetur unde reprehenderit aperiam quaerat fugiat repudiandae explicabo animi minima fuga beatae illum eligendi incidunt consequatur. Amet dolores excepturi earum unde iusto.”</p></blockquote></div></div><div><div class="testimonial"><figure class="mb-4"><img src="static/picture/person_2.jpg" alt="Image" class="img-fluid mb-3"><p>Christine Aguilar</p></figure><blockquote><p>“Lorem ipsum dolor sit amet consectetur adipisicing elit. Consectetur unde reprehenderit aperiam quaerat fugiat repudiandae explicabo animi minima fuga beatae illum eligendi incidunt consequatur. Amet dolores excepturi earum unde iusto.”</p></blockquote></div></div><div><div class="testimonial"><figure class="mb-4"><img src="static/picture/person_4.jpg" alt="Image" class="img-fluid mb-3"><p>Robert Spears</p></figure><blockquote><p>“Lorem ipsum dolor sit amet consectetur adipisicing elit. Consectetur unde reprehenderit aperiam quaerat fugiat repudiandae explicabo animi minima fuga beatae illum eligendi incidunt consequatur. Amet dolores excepturi earum unde iusto.”</p></blockquote></div></div><div><div class="testimonial"><figure class="mb-4"><img src="static/picture/person_5.jpg" alt="Image" class="img-fluid mb-3"><p>Bruce Rogers</p></figure><blockquote><p>“Lorem ipsum dolor sit amet consectetur adipisicing elit. Consectetur unde reprehenderit aperiam quaerat fugiat repudiandae explicabo animi minima fuga beatae illum eligendi incidunt consequatur. Amet dolores excepturi earum unde iusto.”</p></blockquote></div></div></div></div></div><div class="site-section"><div class="container"><div class="row justify-content-center mb-5"><div class="col-md-7 text-center border-primary"><h2 class="font-weight-light text-primary">Our Blog</h2><p class="color-black-opacity-5">See Our Daily News & Updates</p></div></div><div class="row mb-3 align-items-stretch"><div class="col-md-6 col-lg-6 mb-4 mb-lg-4"><div class="h-entry"><img src="static/picture/blog_1.jpg" alt="Image" class="img-fluid"><h2 class="font-size-regular"><a href="#">Warehousing Your Packages</a></h2><div class="meta mb-4">by Theresa Winston <span class="mx-2">•</span> Jan 18, 2019 at 2:00 pm <span class="mx-2">•</span> <a href="#">News</a></div><p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Natus eligendi nobis ea maiores sapiente veritatis reprehenderit suscipit quaerat rerum voluptatibus a eius.</p></div> </div><div class="col-md-6 col-lg-6 mb-4 mb-lg-4"><div class="h-entry"><img src="static/picture/blog_2.jpg" alt="Image" class="img-fluid"><h2 class="font-size-regular"><a href="#">Warehousing Your Packages</a></h2><div class="meta mb-4">by Theresa Winston <span class="mx-2">•</span> Jan 18, 2019 at 2:00 pm <span class="mx-2">•</span> <a href="#">News</a></div><p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Natus eligendi nobis ea maiores sapiente veritatis reprehenderit suscipit quaerat rerum voluptatibus a eius.</p></div></div></div></div></div><div class="site-section border-top"><div class="container"><div class="row text-center"><div class="col-md-12"><h2 class="mb-5 text-black">Try Our Services</h2><p class="mb-0"><a href="javascript:;" class="btn btn-primary py-3 px-5 text-white">Get Started</a></p></div></div></div></div><footer class="site-footer"><div class="container"><div class="row"><div class="col-md-9"><div class="row"><div class="col-md-3"><h2 class="footer-heading mb-4">Quick Links</h2><ul class="list-unstyled"><li><a href="#">About Us</a></li><li><a href="#">Services</a></li><li><a href="#">Testimonials</a></li><li><a href="#">Contact Us</a></li></ul></div><div class="col-md-3"><h2 class="footer-heading mb-4">Products</h2><ul class="list-unstyled"><li><a href="#">About Us</a></li><li><a href="#">Services</a></li><li><a href="#">Testimonials</a></li><li><a href="#">Contact Us</a></li></ul></div><div class="col-md-3"><h2 class="footer-heading mb-4">Features</h2><ul class="list-unstyled"><li><a href="#">About Us</a></li><li><a href="#">Services</a></li><li><a href="#">Testimonials</a></li><li><a href="#">Contact Us</a></li></ul></div><div class="col-md-3"><h2 class="footer-heading mb-4">Follow Us</h2><a href="#" class="pl-0 pr-3"><span class="icon-facebook"></span></a><a href="#" class="pl-3 pr-3"><span class="icon-twitter"></span></a><a href="#" class="pl-3 pr-3"><span class="icon-instagram"></span></a><a href="#" class="pl-3 pr-3"><span class="icon-linkedin"></span></a></div></div></div><div class="col-md-3"><h2 class="footer-heading mb-4">Subscribe Newsletter</h2><form action="#" method="post"><div class="input-group mb-3"><input type="text" class="form-control border-secondary text-white bg-transparent" placeholder="Enter Email" aria-label="Enter Email" aria-describedby="button-addon2"><div class="input-group-append"><button class="btn btn-primary text-white" type="button" id="button-addon2">Send</button></div></div></form></div></div><div class="row pt-5 mt-5 text-center"><div class="col-md-12"><div class="border-top pt-5"><p>Copyright © 2021.Company name All rights reserved.<a target="_blank" href="https://sc.chinaz.com/moban/">网页模板</a></p></div></div></div></div></footer><script src="static/js/jquery-3.3.1.min.js"></script><script src="static/js/jquery-migrate-3.0.1.min.js"></script><script src="static/js/jquery-ui.js"></script><script src="static/js/popper.min.js"></script><script src="static/js/bootstrap.min.js"></script><script src="static/js/owl.carousel.min.js"></script><script src="static/js/jquery.stellar.min.js"></script><script src="static/js/jquery.countdown.min.js"></script><script src="static/js/jquery.magnific-popup.min.js"></script><script src="static/js/bootstrap-datepicker.min.js"></script><script src="static/js/aos.js"></script><script src="static/js/main.js"></script></body>
</html>
首页请求测试
指定端口号运行服务器后可以看到一系列日志信息被打印出来,包括套接字创建成功、绑定成功、监听成功,这时底层用于通信的TCP服务器已经初始化成功了。
此时在浏览器上指定IP和端口访问我们的HTTP服务器,由于我们没有指定要访问服务器web根目录下的那个资源,此时服务器就会默认将web根目录下的index.html文件进行返回,浏览器收到index.html文件后经过刷新渲染就显示出了对应的首页页面。
此时通过ps -aL
命令可以看到线程池中的线程已经被创建好了,其中PID和LWP相同的就是主线程,剩下的就是线程池中处理任务的若干新线程。如下:
错误请求测试
如果我们请求的资源服务器并没有提供,那么服务器就会在获取请求资源属性信息时失败,这时服务器会停止本次请求处理,而直接将web根目录下的404.html文件返回浏览器,浏览器收到后经过刷新渲染就显示出了对应的404页面。
这时在服务器端就能看到一条日志级别为WARNING的日志信息,这条日志信息中说明了客户端请求的哪一个资源是不存在的。
GET方法上传数据测试
编写CGI程序
如果用户在请求服务器的过程中上传了数据,服务器就需要将对应的大户局交给CGI程序进行处理,在测试GET方法上传数据之前,我们需要先编写一个简单的CGI程序。
首先,CGI程序启动后需要先获取父进程传递过来的数据:
- 通过getenv方法获取环境变量中的请求方法;
- 如果请求方法是GET方法,就继续在环境变量中获取父进程传递过来的数据;
- 如果请求方法为POST方法,就需要在环境变量中获取父进程传递过来的数据的长度,然后再0号文件描述符中读取指定长度数据即可。
bool GetQueryString(std::string query_string)
{bool result = false;std::string method = getenv("METHOD"); // 获取请求方法// 判断请求方法if(method == "GET"){// 通过环境变量来获取参数query_string = getenv("QUERY_STRING");result = true;}else if(method == "POST"){int content_length = atoi(getenv("CONTENT_LENGTH"));// 从管道中读取conte_length长度参数char ch = 0;while(content_length){read(0, &ch, 1);query_string += ch;content_length--;}result = true;}else{// 什么也不干result = false;}return false;
}
CGI在获取到父进程传递过来的数据以后,就可以根据对应的业务场景进行数据的处理了,比如用户上传的如果是一个关键字则需要CGI程序做搜索处理。我们这里以演示为目的,认为用户上传的是形如a=10&b=20
的两个参数,需要CGI程序进行加减乘除运算。
我们的CGI程序所要做的就是以&
为分隔符对两个字符串进行分割,然后在以=
为分隔符获取到两个对应的操作数,最后对两个操作数进行加减乘除运算,并将计算结果打印到标准输出即可(标准输出已经被重定向到了管道)。
// 切割字符串
bool CutString(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());return true;}return false;
}
int main()
{std::string query_string;GetQueryString(query_string); // 获取参数// 以"&"对参数进行处理std::string str1;std::string str2;CutString(query_string, "&", str1, str2);// 以"="对参数进行处理std::string name1;std::string value1;CutString(str1, "=", name1, value1);std::string name2;std::string value2;CutString(str2, "=", name2, value2);// 处理数据int x = atoi(value1.c_str());int y = atoi(value2.c_str());//可能向进行某种计算(计算,搜索,登陆等),想进行某种存储(注册)std::cout << "<html>";std::cout << "<head><meta charset=\"utf-8\"></head>";std::cout << "<body>";std::cout << "<h3> " << value1 << " + " << value2 << " = "<< x+y << "</h3>";std::cout << "<h3> " << value1 << " - " << value2 << " = "<< x-y << "</h3>";std::cout << "<h3> " << value1 << " * " << value2 << " = "<< x*y << "</h3>";std::cout << "<h3> " << value1 << " / " << value2 << " = "<< x/y << "</h3>";std::cout << "</body>";std::cout << "</html>";return 0;
}
URL上传数据测试
CGI程序编写编写完毕并生成可执行程序后,将这个可执行程序放到web根目录下,这时在请求服务器时就可以指定请求这个CGI程序,并通过URL上传参数让其进行处理,最终我们就能得到计算结果。
此外,如果请求CGI程序时指定的第二个操作数为0,那么CGI程序在进行除法运算时就会崩溃,这时父进程等待子进程后就会发现子进程是异常退出的,进而设置状态码为NOT_FOUND,最终服务器就会构建对应的错误页面返回给浏览器。
表单数据上传
服务器一般会让用户通过表单来上传参数,HTML中的表单用于搜集用户的输入,我们可以通过设置表单的method属性来指定表单提交的方法,通过设置表单的action属性来指定表单需要提交给服务器上的哪一个CGI程序。
比如现在将服务器的首页改成以下HTML代码,指定将表单中的数据以GET方法提交给web根目录下的test_cgi程序:
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>简易的在线计算器</title>
</head>
<body><form action="/test_cgi" method="get" align="center">操作数1:<br><input type="text" name="x"><br>操作数2:<br><input type="text" name="y"><br><br><input type="submit" value="计算"></form>
</body>
</html>
此时我们直接访问服务器看到的就是一个表单,向表单中输入两个操作数并点击“计算”后,表单中的数据就会以GET方法提交给web根目录下的test_cgi程序,此时CGI程序进行数据计算后同样将结果返回给了浏览器。
POST方法上传数据测试
测试表单通过POST方法上传数据时,只需要将表单中的method属性改为“post”即可,此时点击“计算”提交表单时,浏览器检测到表单的提交方法为POST后,就会将表单中的数据添加到请求正文中,并将请求资源路径替换成表单action指定的路径,然后再次向服务器发起HTTP请求。
由于POST方法是通过请求正文上传的数据,因此表单提交后浏览器上方的URL中只有请求资源路径发生了改变,而并没有在URL后面添加任何参数。同时观察服务器端输出的日志信息,也可以确认浏览器本次的请求方法为POST方法。
以上就是本项目的全部内容。
项目扩展
当前项目主要完成的是GET方法、POST方法以及CGI机制的搭建,如果需要对项目进行扩展,可以分为技术层面和应用层面。
技术层面
- 当前项目编写的是HTTP/1.0版本的服务器,每一次连接只会对一个请求进行处理,当服务器对客户端发送过来的请求处理完毕并且接收到客户端发送过来的响应以后,就会关闭该连接。我们可以将其扩展为HTTP/1.1版本,HTTP/1.1版本支持长连接,一个连接可以对多个请求进行处理,防止连接的频繁建立;
- 我们在后期引入了线程池,针对于中小型的应用都可以满足,但对于大型的应用可以考虑将服务器改写成epoll版本,使服务器的IO操作更加高效;
- 可以给当前服务器新增代理功能,代替客户端去访问某种服务,再将访问结果返回给客户端。
应用层面
- 基于当前HTTP服务器,搭建在线博客;
- 基于当前HTTP服务器,编写在线画图板;
- 基于当前HTTP服务器,编写一个搜索引擎。