目录
基于线程池的HTTP服务器
一、项目背景:
http协议被广泛使用,从移动端,pc端浏览器,http协议无疑是打开互联网应用窗口的重要协议,http在网络应用层中的地位不可撼动,是能准确区分前后台的重要协议。
二、目标:
本项目采用C/S模型,编写中小型应用的http,并结合mysql,理解常见互联网行为,做完之后可以从技术上理解从你上网开始,到关闭浏览器的所有操作中的技术细节。
三、项目环境和技术特点:
(1)、环境:
Centos-7云服务器,VScode(远程连接,也可以使用vim),MySQL5.7。
(2)、技术特点:
网络编程、多线程技术、cgi技术、shell脚本,线程池。
四、从头到尾细节和思路:
注:代码注释写的比较详细,可能有点啰嗦,如果想要源码可以私信。
(1)、套接字:
一上来,基于TCP/IP协议栈的层状结构,http在传输层采用的协议是tcp,那话不多说,套接字编程整上;(TcpServer.hpp头文件)
注:这次设计采用单例模式设计,单例模式提供静态成员方法获取对象,使用双检查,避免锁竞争;还有稍微注意一下避免服务器主动关闭,而出现不能马上重启的现象,使用setsockopt进行设计地址复用,原因是因为主动关闭会进入TIME_WAIT状态。
#pragma once
#include <iostream>
#include <string>
#include <cstdlib>
#include <unistd.h>
#include <pthread.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include "Log.hpp"
#define BACK_LOG 10
// 单例,懒汉模式
class TcpServer
{
public:
static TcpServer* GetInstance(uint16_t port)
{
// 静态分配锁
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
// 双检查,避免并发导致锁竞争
if(_svr == nullptr)
{
pthread_mutex_lock(&lock);
if(_svr == nullptr)
{
_svr = new TcpServer(port);
_svr->_InitServer();
}
pthread_mutex_unlock(&lock);
}
return _svr;
}
int Sock()
{
return _listen_sock;
}
~TcpServer()
{
if(_listen_sock >= 0)
{
close(_listen_sock);
}
}
private:
// 创建套接字句柄
void _Socket()
{
_listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if(_listen_sock < 0)
{
LOG(FATAL, "scoket error");
exit(2);
}
// 设置地址复用,让服务器出现问题能快速重启
int opt = 1;
if(setsockopt(_listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0)
{
LOG(FATAL, "setsockopt error");
exit(3);
}
LOG(INFO, "socket success");
}
// 绑定端口
void _Bind()
{
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = htonl(INADDR_ANY);
socklen_t len = sizeof(local);
if(bind(_listen_sock, (struct sockaddr*)&local, len) < 0)
{
LOG(FATAL, "bind error");
exit(4);
}
LOG(INFO, "bind success");
}
// 把句柄设置进行监听状态
void _Listen()
{
if(listen(_listen_sock, BACK_LOG) < 0)
{
LOG(INFO, "listen error");
exit(5);
}
LOG(INFO, "listen success");
}
// 初始化服务器
void _InitServer()
{
_Socket();
_Bind();
_Listen();
LOG(INFO, "TcpServer init success");
}
private:
// 把构造函数私有
TcpServer(uint16_t port) : _port(port), _listen_sock(-1)
{}
// 把拷贝构造和赋值给禁用
TcpServer(const TcpServer& ) = delete;
TcpServer& operator=(const TcpServer& ) = delete;
private:
uint16_t _port; // 端口
int _listen_sock; // 监听的fd
static TcpServer* _svr; // 全局唯一对象
};
// 唯一对象类外初始化
TcpServer* TcpServer::_svr = nullptr;
(2)、日志
为了测试方便,简单编写一个日志信息打印,这样测试时很快就能打印相关信息。(Log.hpp)
#pragma once
#include <iostream>
#include <ctime>
#include <string>
// 定义日志级别
#define INFO 1
#define WARNING 2
#define ERROR 3
#define FATAL 4
// 日志格式 [日志级别][时间戳][日志信息][文件名称][行数]
// 为了方便使用,时间戳函数填充,文件名称和行数使用C语言内置的预定义宏
// 所以调用者只需传入两个参数,这里使用宏替换来实现
// 这里宏替换时,日志等级想让其显示成字符串,
// 所以需要在其前面加个#,实现把一个宏参数变成对应的字符串的语义
#define LOG(level, message) Log(#level, message, __FILE__, __LINE__)
void Log(std::string level, std::string message, std::string file_name, int line)
{
std::cout << "[" << level << "]";
std::cout << "[" << time(nullptr) << "]";
std::cout << "[" << message << "]";
std::cout << "[" << file_name << "]";
std::cout << "[" << line << "]" << std::endl;
}
(3)、Task类,线程池、HttpServer类:
HttpServer类(HttpServer.hpp):http服务器调用tcp套接字进行连接管理,然后http不断接收连接,构建任务(task),塞到线程池里面就行,然后不断循环接收即可。
#pragma once
#include <sstream>
#include <signal.h>
#include "ThreadPool.hpp"
#include "TcpServer.hpp"
#define PORT 8080
class HttpServer
{
public:
HttpServer(uint16_t port = PORT): _port(port), stop(false)
{}
void InitServer()
{
// 处理url时需要管道给CGI程序传参,如果发生超时,对端关了,服务器就挂掉了
signal(SIGPIPE, SIG_IGN);
}
void Loop()
{
TcpServer* tsvr = TcpServer::GetInstance(_port);
LOG(INFO, "HttpServer loop begin");
while(!stop)
{
// 处理链接
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sock = accept(tsvr->Sock(), (struct sockaddr*)&peer, &len);
if(sock < 0)
continue;
std::stringstream ss;
ss << "get a new link from " << inet_ntoa(peer.sin_addr) << ":" << ntohs(peer.sin_port);
LOG(INFO, ss.str());
// 构建任务,放进任务队列
Task task(sock);
ThreadPool::GetInstance()->PushTask(task);
}
}
private:
uint16_t _port;
bool stop; // 这个信号可以在异常处理时使用,本项目暂不考虑
};
然后主函数构成HttpServer对象进行循环接收即可。(Main.cpp)
#include "HttpServer.hpp"
// 没有指定端口
void Usage(const char* proc)
{
std::cout << "Usage: \n\t" << proc << " port" << std::endl;
}
int main(int argc, char* argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(1);
}
HttpServer* hs = new HttpServer(atoi(argv[1]));
// 初始化
hs->InitServer();
// 循环
hs->Loop();
return 0;
}
线程池(ThreadPool.hpp):线程池就是一个生产者消费者模型,其中一个任务队列存储任务,若干数量的线程进行处理或者等待,同时有锁和条件变量维护多线程的同步互斥机制就可以了。
注:此项目使用pthread库,所以在编写线程运行函数时要注意一下this指针的问题。
#pragma once
#include <iostream>
#include <queue>
#include <pthread.h>
#include "Task.hpp"
#include "Log.hpp"
#define NUM 10
class ThreadPool
{
public:
static ThreadPool* GetInstance(size_t num = NUM)
{
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
if(_tp == nullptr)
{
pthread_mutex_lock(&mtx);
if(_tp == nullptr)
{
_tp = new ThreadPool(num);
_tp->_InitThreadPool();
}
pthread_mutex_unlock(&mtx);
}
return _tp;
}
bool IsStop()
{
return _stop;
}
// 向调用者提供一个塞任务进来的接口
void PushTask(const Task& task)
{
// 塞任务时加锁
_LockQueue();
_task_queue.push(task);
_UnlockQueue();
// 唤醒一个线程处理
_WaskUpThread();
}
private:
// 线程处理函数
static void* _ThreadRoutine(void* args)
{
// pthread库再创建线程时就只允许函数只有一个参数,C++从普通成员函数又自带this指针
// 不通过对象调用所以设置成静态的
// 但是使用过程又需要this指针,所以参数就传this指针
ThreadPool* _this = (ThreadPool*) args;
while(!_this->IsStop())
{
_this->_LockQueue();
// 这里循环判断一下,避免误唤醒导致异常
while(_this->_IsEmpty())
{
_this->_ThreadWait();
}
Task task;
_this->_GetTask(task);
_this->_UnlockQueue();
// 任务处理方法在锁外面调用,不暂用锁资源
task.ProcessOn();
}
return nullptr;
}
// 获取任务时,调用处一定已经加锁,所以这里不需要加锁
void _GetTask(Task& task)
{
task = _task_queue.front();
_task_queue.pop();
}
void _LockQueue()
{
pthread_mutex_lock(&_lock);
}
void _UnlockQueue()
{
pthread_mutex_unlock(&_lock);
}
bool _IsEmpty()
{
return _task_queue.empty();
}
void _ThreadWait()
{
pthread_cond_wait(&_cond, &_lock);
}
void _WaskUpThread()
{
pthread_cond_signal(&_cond);
}
void _InitThreadPool()
{
// 创建线程
int i = 0;
for(; i < _num; ++i)
{
pthread_t tid;
if(pthread_create(&tid, nullptr, _ThreadRoutine, (void*)this) < 0)
{
LOG(FATAL, "init threadpool error");
exit(6);
}
// 创建成功就分离,不关系结果
pthread_detach(tid);
}
LOG(INFO, "init threadpool success");
}
private:
ThreadPool(size_t num) : _num(num), _stop(false)
{
pthread_mutex_init(&_lock, nullptr);
pthread_cond_init(&_cond, nullptr);
}
ThreadPool(const ThreadPool& ) = delete;
ThreadPool& operator=(const ThreadPool& ) = delete;
private:
bool _stop; // 这个信号在处理异常时可以使用,此项目暂不考虑
size_t _num; // 线程个数
pthread_mutex_t _lock;
pthread_cond_t _cond;
std::queue<Task> _task_queue; // 任务队列
static ThreadPool* _tp;
};
ThreadPool* ThreadPool::_tp = nullptr;
Task类:任务类成员变量有处理任务的方法,只需要根据链接句柄调用注册的成员方法就可以了。
#pragma once
#include <iostream>
#include "Protocol.hpp"
class Task
{
public:
Task()
{}
Task(int sock) :_sock(sock)
{}
// 处理
void ProcessOn()
{
_handler(_sock);
}
private:
int _sock;
CallBack _handler; // 回调处理方法
};
(4)、通用工具类:
Until类:提供按行读取,以及拆分字符串的工具函数。
注:Http的行结尾有可能有三种\r \r\n \n,统一处理成\n。
#pragma once
#include <iostream>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
class Util
{
public:
// 返回值说明
// 0:说明链接对端关闭了,服务器端就会读到0
// -1:读取出错了
// >0:读取到的字符数
static int ReadLine(int sock, std::string& out)
{
char ch = 'a';
while(ch != '\n')
{
ssize_t recv_size = recv(sock, &ch, 1, 0);
if(recv_size > 0)
{
// Http的行结尾有可能有三种\r \r\n \n
// 所有情况统一处理成\n
if(ch == '\r')
{
// 先标志位MSG_PEEK窥探后面还有没有\n
// 有的话一起读起来
// 其他就做处理成\n
recv(sock, &ch, 1, MSG_PEEK);
if(ch == '\n')
{
recv(sock, &ch, 1, 0);
}
else
{
ch = '\n';
}
}
out += ch;
}
else if(recv_size == 0)
{
return 0;
}
else
{
return -1;
}
}
return out.size();
}
// 分割字符串
static bool CutString(const std::string& src, std::string& sub1, std::string& sub2, const std::string& seq)
{
auto pos = src.find(seq);
if(pos != std::string::npos)
{
sub1 = src.substr(0, pos);
sub2 = src.substr(pos + seq.size());
return true;
}
else
return false;
}
};
(5)、协议处理细节:
注:Protocol.hpp时这个项目最麻烦头文件,很长,要耐心。
CallBack类:生成的对象要提供给Task对象像函数一样使用,所以重载();然后提供一个处理请求的成员方法就能给Task对象调用了,CallBack只是调用,真正处理的是下面EndPoin类。
class CallBack
{
public:
CallBack()
{}
// 仿函数:让使用者像函数使用
void operator()(int sock)
{
HandlerRequest(sock);
}
void HandlerRequest(int sock)
{
LOG(INFO, "Handler begin.........");
#ifdef DEBUG
// 编译时,g++ 带上-D DEBUG可以快速进行调试
// 用来测试一开始的从网页读取是否没问题
// 编写处理方法时来回切换确认问题所在
char buf[4096];
ssize_t s = recv(sock, buf, sizeof(buf)-1, 0);
if(s > 0)
buf[s] = '\0';
LOG(INFO, "Debug begin.........");
std::cout << buf << std::endl;
LOG(INFO, "Debug end.........");
#else
EndPoint* ep = new EndPoint(sock);
// 读取请求
ep->RecvHttpRequest();
// 读取完成,如果没有stop信号,说明合法请求,就可以进行构建和发送
if(!ep->IsStop())
{
LOG(INFO, "Recv success, begin build and send");
ep->BuildHttpResponse();
ep->SendHttpResponse();
}
delete ep;
#endif
LOG(INFO, "Handler end..........");
}
~CallBack()
{}
};
http协议数据格式:
接下来就要读取客户端传过来的数据,填入HttpRequest请求类;所以根据http协议格式中一个请求包括请求行,请求报头以及可能有正文,构建HttpRequest请求类。
// Http请求类
struct HttpRequest
{
HttpRequest(): _content_length(0), _cgi(false)
{}
~HttpRequest()
{}
std::string _request_line; // 请求行
std::vector<std::string> _request_header; // 请求报头
std::string _blank; // 空行
std::string _request_body; // 请求正文
std::string _method; // 请求行的方法
std::string _url; // 请求行的url
std::string _version; // 请求行的版本号
std::unordered_map<std::string, std::string> _header_kv; // 请求报头的键值对
int _content_length; // 请求的正文长度
std::string _path; // 访问路径
std::string _query_string; // 参数
std::string _suffix; // 访问文件后缀
bool _cgi; // 是否需要CGI程序处理的标志
};
然后就需要根据请求构建响应,所以根据http协议格式中一个响应包括响应行,响应报头以及可能有正文,构建HttpResponse响应类。
// Http响应类
struct HttpResponse
{
HttpResponse(): _blank(LINE_END), _status_code(OK), _fd(-1), _body_size(0)
{}
~HttpResponse()
{}
std::string _response_line; // 响应行
std::vector<std::string> _response_header; // 响应报头
std::string _blank; //空行
std::string _response_body; // 响应正文
int _status_code; // 状态码
int _fd; // 要返回文件的句柄
int _body_size; // 返回的静态网页大小
};
有了这两个类,我们就可以开始进行读取和构建,这里还是强调一下,http作为一个非常成熟的协议,细节太庞大了,而我们的项目目标是打通我们上网的细节,然后很多地方只处理关键点,不处理细节。(EndPoint类)
首先,读取请求思路:
构建请求:void BuildHttpResponse()
解析请求注意:a、HttpResponse类专门设计一个_body_size字段专门存储静态网页的大小,而如果需要返回正文用_response_body字段的size来提高大小即可。
b、stat()函数可以获取文件的各种属性,具体man一下就知道了,静态网页大小字段马上可以填写。
构建响应:
发送响应:void SendHttpResponse();别忘了空行。
#pragma once
#include <iostream>
#include <sstream>
#include <string>
#include <vector>
#include <unordered_map>
#include <algorithm>
#include <stdlib.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <sys/sendfile.h>
#include "Log.hpp"
#include "Util.hpp"
#define LINE_END "\r\n"
#define HTTP_VERSION "HTTP/1.0"
#define OK 200
#define BAD_REQUEST 400
#define NOT_FOUND 404
#define SERVER_ERROR 500
#define WEB_ROOT "wwwroot"
#define HOME_PAGE "index.html"
#define PAGE_404 "404.html"
#define PAGE_400 "400.html"
#define PAGE_500 "500.html"
// 后缀对应报头Content-Type字段的value
std::string SuffixToDesc(const std::string& suffix)
{
// 这个表不齐全,只写了几种常见的
static std::unordered_map<std::string, std::string> suffixtodesc = {
{".html", "text/html"},
{".css", "text/css"},
{".js", "application/x-javascript"},
{".jpg", "application/x-jpg"}
};
auto it = suffixtodesc.find(suffix);
if(it != suffixtodesc.end())
{
return it->second;
}
return "text/html";
}
// 状态码对应描述
std::string StatusCodeToDesc(int code)
{
std::string desc;
switch(code)
{
case OK:
desc = "OK";
break;
case BAD_REQUEST:
desc = "BAD_REQUEST";
break;
case NOT_FOUND:
desc = "NOT_FOUND";
break;
case SERVER_ERROR:
desc = "SERVER_ERROR";
break;
default:
desc = "ERROR";
break;
}
return desc;
}
// 端点类:进行读取请求,分析请求,构建响应,IO通信
class EndPoint
{
public:
EndPoint(int sock):_sock(sock), _stop(false)
{}
bool IsStop()
{
return _stop;
}
~EndPoint()
{
if(_sock >= 0)
{
close(_sock);
}
}
void RecvHttpRequest()
{
// 只有请求行和报头成功读取才有处理下去的必要
if(!_RecvHttpRequestLine() && !_RecvHttpRequestHeader())
{
_ParseHttpRequestLine();
_ParseHttpRequestHeader();
_RecvHttpRequestBody();
}
}
void BuildHttpResponse()
{
auto& code = _http_response._status_code;
auto& method = _http_request._method;
auto& url = _http_request._url;
auto& path = _http_request._path;
auto& query_string = _http_request._query_string;
auto& suffix = _http_request._suffix;
size_t suffixpos = 0;
// 此项目只处理GET和POST方法
if(method != "GET" && method != "POST")
{
code = BAD_REQUEST;
goto END;
}
// 处理url,url分成路径,以及根据方法是否有参数
if(method == "GET")
{
size_t pos = url.find("?");
if(pos != std::string::npos)
{
// url有参数,GET方法通过url传参,分割url
Util::CutString(url, path, query_string, "?");
}
else
{
path = url;
}
}
// POST方法不通过url传参,通过正文传参
else if(method == "POST")
{
path = url;
}
// 还有完善其他方法,从这里编写
else {
// PUT/HEAD/DELETE.....
}
// 加上WEB_ROOT默认路径
path = WEB_ROOT + path;
if(path[path.size()-1] == '/')
{
// 只有'/'默认返回网站的主页,浏览器也会默认添加
path += HOME_PAGE;
// LOG(INFO, path);
}
// 判断url访问的文件属性
// 目录?可执行程序?....本地有没有这文件?
struct stat st; // 文件属性结构体
if(stat(path.c_str(), &st) == 0) // 返回0说明文件存在
{
if(S_ISDIR(st.st_mode)) //判断是否是目录 st.mode是文件的类型和权限等
{
path += '/';
path += HOME_PAGE;
// 重新获取文件的属性lin
stat(path.c_str(), &st);
// LOG(INFO, path);
}
else
{
// 查看文件是否具有可执行权限
if(st.st_mode & S_IXUSR || st.st_mode & S_IXGRP || st.st_mode & S_IXOTH)
{
// 有的话,调用CGI程序处理
_http_request._cgi = true;
}
}
_http_response._body_size = st.st_size;//st_size是也就是网页文件的大小
}
else
{
// 访问文件不存在,准备返回404网页
code = NOT_FOUND;
LOG(WARNING, path + " NOT FOuND");
goto END;
}
// 识别url访问文件的后缀,
suffixpos = path.rfind('.');
if(suffixpos == std::string::npos)
suffix = ".html";
else
suffix = path.substr(suffixpos);
// 走到这里,路径一定存在,根据是否需要CGI程序处理,各自调用函数处理
if(_http_request._cgi)
code = _ProcessCGI();
else
{
code = _ProcessNonCGI();
// LOG(INFO, "ProcessNonCGI end");
}
END:
// 到这里无论正确与否等等,所有数据已经处理好,构建对应状态码的响应
_BuildHttpResponseHelper();
return;
}
void SendHttpResponse()
{
// 响应行
send(_sock, _http_response._response_line.c_str(), _http_response._response_line.size(), 0);
// 响应报头
LOG(INFO, "send header begin");
for(auto& e : _http_response._response_header)
{
// LOG(INFO, e);
send(_sock, e.c_str(), e.size(), 0);
}
LOG(INFO, "send header end");
// 响应空行
send(_sock, _http_response._blank.c_str(), _http_response._blank.size(), 0);
// 响应正文
if(_http_request._cgi)
{
int total = 0;
int send_size = 0;
auto& body_size = _http_response._response_body;
while(total < body_size.size() && (send_size = send(_sock, body_size.c_str() + total, body_size.size()- total, 0) > 0))
{
total += send_size;
}
}
else
{
// 直接用sendfile直接在内核态进行拷贝,效率更高
sendfile(_sock, _http_response._fd, nullptr, _http_response._body_size);
close(_http_response._fd);
}
}
private:
// 读取请求行
bool _RecvHttpRequestLine()
{
auto& line = _http_request._request_line;
int ret = Util::ReadLine(_sock, line);
if(ret > 0)
{
// 把结尾的\n去掉
line.resize(line.size() - 1);
LOG(INFO, line);
}
// 对端关闭或者读取出错,进入错误处理
else
{
_stop = true;
}
return _stop;
}
// 处理请求行
void _ParseHttpRequestLine()
{
auto& line = _http_request._request_line;
// 使用字符串一次性把请求行分割成三个
std::stringstream ss (line);
ss >> _http_request._method >> _http_request._url >> _http_request._version;
// 方法同个转换成大写
std::transform(_http_request._method.begin(), _http_request._method.end(), _http_request._method.begin(), toupper);
}
// 读取请求报头
bool _RecvHttpRequestHeader()
{
auto& header = _http_request._request_header;
std::string line;
// Http协议格式规定报头和正文之间会有一行空行
while(1)
{
line.clear();
if(Util::ReadLine(_sock, line) > 0)
{
if(line == "\n")
break;
else
{
line.resize(line.size()-1);
header.push_back(line);
// LOG(INFO, line);
}
}
else
{
_stop = true;
break;
}
}
return _stop;
}
// 处理请求报头
void _ParseHttpRequestHeader()
{
std::string key;
std::string value;
// 把请求报头键值对插入哈希表,方便查找
for(auto& e : _http_request._request_header)
{
if(Util::CutString(e, key, value, ": "))
{
_http_request._header_kv[key] = value;
}
}
}
// 判断是否需要读正文
bool __IsNeedRecvHttpRequestBody()
{
// POST方法有正文
// 报头中有Content-Length字段才读
if(_http_request._method == "POST")
{
auto& header_kv = _http_request._header_kv;
auto it = header_kv.find("Content-Length");
if(it != header_kv.end())
{
LOG(INFO, it->second);
_http_request._content_length = std::stoi(it->second);
return true;
}
}
return false;
}
// 读取请求正文
bool _RecvHttpRequestBody()
{
// 先判断有没有正文
if(__IsNeedRecvHttpRequestBody())
{
// 有就按照Cpntent-Length的数值读
auto& body = _http_request._request_body;
int len = _http_request._content_length;
while(len)
{
char ch = 'a';
if(recv(_sock, &ch, 1, 0) > 0)
{
body += ch;
len--;
}
else
{
_stop = true;
break;
}
}
LOG(INFO, body);
}
return _stop;
}
// 不需要CGI处理的请求只要把返回静态网页,直接把文件打开,保存fd即可
int _ProcessNonCGI()
{
auto& path = _http_request._path;
auto& fd = _http_response._fd;
// LOG(INFO, path);
fd = open(path.c_str(), O_RDONLY);
if(fd >= 0)
{
LOG(INFO, "open file success");
return OK;
}
else
return NOT_FOUND;
}
// 调用CGI程序,创建子进程程序替换进行,这样就算CGI程序出问题了,也不会影响主程序
int _ProcessCGI()
{
// LOG(INFO, "CGI begin....");
int code = OK;
std::string method_env;
std::string query_string_env;
std::string content_length_env;
auto& method = _http_request._method;
auto& query_string = _http_request._query_string;
auto& content_length = _http_request._content_length;
auto& request_body = _http_request._request_body;
auto& response_body = _http_response._response_body;
// 路径就是可执行程序的地址
auto& bin = _http_request._path;
// 创建子进程,需要把请求的参数传给子进程,涉及进程间通信,选择匿名管道进行进程间通信
// 约定:input,output都是对于父进程来说。就是说父进程使用input[0]读,output[1]写,子进程相反
int input[1];
int output[1];
if(pipe(input) < 0)
{
LOG(ERROR, "pipe input error");
code = SERVER_ERROR;
return code;
}
if(pipe(output) < 0)
{
LOG(ERROR, "pipe output error");
code = SERVER_ERROR;
return code;
}
// 创建子进程
pid_t pid = fork();
if(pid == 0)
{
// 匿名管道半双工,关掉不需要使用的fd
close(input[0]);
close(output[1]);
// 程序替换会替换掉数据和代码,但是PCB和文件描述符表这些是不会的,环境变量存在栈区之上也不会被覆盖
// 但是替换后子进程拿不到原来的管道的fd是多少,所以重定向为0 1冲所周知的fd
// 在重定向放到最后替换之前做,不然很容易引起异常现象
method_env = "METHOD=";
method_env += method;
putenv((char*)method_env.c_str());
if(method == "GET")
{
LOG(INFO, query_string);
query_string_env = "QUERY_STRING=";
query_string_env += query_string;
putenv((char*)query_string_env.c_str());
}
else if(method == "POST")
{
LOG(INFO, std::to_string(content_length));
content_length_env = "CONTENT_LENGTH=";
content_length_env += std::to_string(content_length);
putenv((char*)content_length_env.c_str());
}
else {
// 其他方法的处理
}
// 导完环境变量,重定向
dup2(input[1], 1); // 往原来的标准输入写
dup2(output[0], 0); // 从原来的标准输入读
// 程序替换
execl(bin.c_str(), bin.c_str(), nullptr);
}
else if(pid < 0)
{
LOG(ERROR, "fork error");
code = SERVER_ERROR;
return code;
}
else
{
// 父进程
close(input[1]);
close(output[0]);
// POST参数需要写入管道
if(method == "POST")
{
LOG(INFO, request_body);
int total = 0; // 实时已经写入总共的字节数
int write_size = 0; // 单次写入的字节数
// 避免无法一次性将数据写入管道的情况
while(total < content_length && (write_size = write(output[1], request_body.c_str() + total, request_body.size() - total) > 0))
{
total += write_size;
}
}
// 接收结果,CGI执行完,程序就退出了
// 所以父进程一定会读到0
char ch = 'a';
while(read(input[0], &ch, 1))
{
response_body += ch;
}
LOG(INFO, response_body);
// 回收子进程资源
int status = 0;
pid_t ret = waitpid(pid, &status, 0);
if(ret == pid)
{
// 先判断程序是否退出
if(WIFEXITED(status))
{
// 正常的情况下,获取退出码,判断是否执行结果是否正确
if(WEXITSTATUS(status) == 0)
code = OK;
else
code = BAD_REQUEST;
}
else
code = SERVER_ERROR;
}
// 关闭管道
close(input[0]);
close(output[1]);
}
return code;
}
void _BuildHttpResponseHelper()
{
auto& code = _http_response._status_code;
auto& path = _http_request._path;
// 构建状态行
auto& response_line = _http_response._response_line;
response_line = HTTP_VERSION;
response_line += " ";
response_line += std::to_string(code);
response_line += " ";
response_line += StatusCodeToDesc(code);
response_line += LINE_END;
// 处理路径加上Web根目录
path = WEB_ROOT + path;
path += '/';
switch(code)
{
case OK:
// OK的情况直接返回结果和对应网页就行
__BulidOkResponse();
break;
// 其他异常情况返回对应网页
case NOT_FOUND:
path += PAGE_404;
__HandlerError(path);
break;
case BAD_REQUEST:
path += PAGE_400;
__HandlerError(path);
break;
case SERVER_ERROR:
path += PAGE_500;
__HandlerError(path);
break;
default:
break;
}
}
void __BulidOkResponse()
{
std::string line = "Content-Type: ";
line += SuffixToDesc(_http_request._suffix);
line += LINE_END;
_http_response._response_header.push_back(line);
// LOG(INFO, line);
// 正文长度字段,分为不经过CGI处理的静态网页大小和经过CGI处理的大小
line = "Content-Length: ";
if(_http_request._cgi)
line += std::to_string(_http_response._response_body.size()); // CGI处理的结果存在_http_response._response_body里面
else
line += std::to_string(_http_response._body_size); // _http_response._body_size专门用来存储网页大小
line += LINE_END;
_http_response._response_header.push_back(line);
//LOG(INFO, line);
}
void __HandlerError(const std::string& path)
{
// 出现错误返回对应的网页
_http_request._cgi = false;
// 打开文件,让后面发送的接口使用sendfile函数直接往句柄里_sock里面写
_http_response._fd = open(path.c_str(), O_RDONLY);
if(_http_response._fd >= 0)
{
// 获取对应静态网页的属性
struct stat st;
stat(path.c_str(), &st);
// 王爷大小
_http_response._body_size = st.st_size;
// 正文字段
std::string line = "Content-Type: text/html";
line += LINE_END;
_http_response._response_header.push_back(line);
line = "Content-Length: ";
line += std::to_string(_http_response._body_size);
line += LINE_END;
_http_response._response_header.push_back(line);
}
}
private:
int _sock; // 读写的句柄
HttpRequest _http_request; // 请求
HttpResponse _http_response; // 响应
bool _stop; // 停止标志
};
(6)、CGI机制:
CGI(Common Gateway Interface):公共网关接口 是WWW技术中最重要的技术之一,有着不可替代的重要地位。CGI是外部应用程序(CGI程序)与WEB服务器之间的接口标准,是在CGI程序和Web服务器之间传递信息的过程。
注意:a、调用哪个CGI程序由前端网页使用表单等控件实现地址填充。
b、http服务器只是给CGI程序提供一个平台,调用CGI程序处理,根据返回结果构建响应返回。
c、总体上讲,CGI程序的的标准输入是浏览器,标准输出也是浏览器,中间所有的通信细节(数据怎么来,怎么给发浏览器都不管)都交给http服务器。
d、CGI能用几乎所有的后端语言编写。
(7)、引入数据库编写CGI程序:
使用C/C++连接数据,需要使用Mysql库提供的接口,所以先要在官网下载Mysql对应版本的库文件。动态链接或者静态链接都可以,这个项目用的静态链接。
链接前,先在本地创建好给这个项目使用的账号,数据库,分配好权限;再使用mysql.h的各个文件链接。
CGI程序文件夹下的通用工具类:
这段解码和编码的代码转载自:C++写的UrlEncode和UrlDecode - claireyuancy - 博客园
#pragma once
#include <iostream>
#include <string>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <assert.h>
class Util
{
public:
// 分割字符串
static bool CutString(const std::string& src, std::string& sub1, std::string& sub2, const std::string& seq)
{
auto pos = src.find(seq);
if(pos != std::string::npos)
{
sub1 = src.substr(0, pos);
sub2 = src.substr(pos + seq.size());
return true;
}
else
return false;
}
// 获取CGI参数
static bool GetQueryString(std::string& query_string)
{
std::string method = getenv("METHOD");
// std::cerr << "METHOD=" << method << std::endl;
bool result = false;
if(method == "GET")
{
query_string = getenv("QUERY_STRING");
result = true;
}
else if(method == "POST")
{
query_string.clear();
int content_length = std::stoi(getenv("CONTENT_LENGTH"));
while(content_length > 0)
{
char ch = 'a';
read(0, &ch, 1);
query_string += ch;
content_length--;
}
result = true;
}
else
return false;
// std::cerr << "QUERY_STRING=" << query_string << std::endl;
return result;
}
// 编码
static unsigned char ToHex(unsigned char x)
{
return x > 9 ? x + 55 : x + 48;
}
static std::string UrlEncode(const std::string& str)
{
std::string strTemp = "";
size_t length = str.length();
for (size_t i = 0; i < length; i++)
{
if (isalnum((unsigned char)str[i]) ||
(str[i] == '-') ||
(str[i] == '_') ||
(str[i] == '.') ||
(str[i] == '~'))
strTemp += str[i];
else if (str[i] == ' ')
strTemp += "+";
else
{
strTemp += '%';
strTemp += ToHex((unsigned char)str[i] >> 4);
strTemp += ToHex((unsigned char)str[i] % 16);
}
}
return strTemp;
}
// 解码
static unsigned char FromHex(unsigned char x)
{
unsigned char y;
if (x >= 'A' && x <= 'Z') y = x - 'A' + 10;
else if (x >= 'a' && x <= 'z') y = x - 'a' + 10;
else if (x >= '0' && x <= '9') y = x - '0';
else assert(0);
return y;
}
static std::string UrlDecode(const std::string& str)
{
std::string strTemp = "";
size_t length = str.length();
for (size_t i = 0; i < length; i++)
{
if (str[i] == '+') strTemp += ' ';
else if (str[i] == '%')
{
assert(i + 2 < length);
unsigned char high = FromHex((unsigned char)str[++i]);
unsigned char low = FromHex((unsigned char)str[++i]);
strTemp += high*16 + low;
}
else strTemp += str[i];
}
return strTemp;
}
};
mysql_cgi.cpp:注意浏览器把中文参数传过来时是经过编码的,所以需要解码,不解码就会乱码,所以在插入数据库前要把数据解码一下。
#include "Util.hpp"
#include "./include/mysql.h"
const std::string HOST = "127.0.0.1";
const std::string USER = "http";
const std::string PASSWORD = "http123.";
const std::string DB = "HTTP";
// 模拟一下用户使用表单提交注册,到服务器把数据插入到数据库的过程
int main()
{
// 测试能不能mysql.h能不能链接上而已
// std::cout << "mysql client version: " << mysql_get_client_info() << std::endl;
// 获取参数
std::string query_string;
Util::GetQueryString(query_string);
// 用户名和密码分开
std::string namekv;
std::string passwordkv;
Util::CutString(query_string, namekv, passwordkv, "&");
// 分成name=... 和password=...
// 取出用户输入的用户名和密码
std::string tmp;
std::string sql_name;
std::string sql_name_tmp;
std::string sql_password;
std::string sql_password_tmp;
Util::CutString(namekv, tmp, sql_name_tmp, "=");
Util::CutString(passwordkv, tmp, sql_password_tmp, "=");
// 确保中文不会乱码,需要进行解码
sql_name = Util::UrlDecode(sql_name_tmp);
sql_password = Util::UrlDecode(sql_password_tmp);
// 获取成功,开始链接数据库,插入数据
//初始化数据库,获得句柄
MYSQL* my = mysql_init(nullptr);
if(my != nullptr)
{
// 链接数据库
if(mysql_real_connect(my, HOST.c_str(), USER.c_str(), PASSWORD.c_str(), DB.c_str(), 3306, nullptr, 0) != nullptr)
{
// 链接成功,设置编码格式
mysql_set_character_set(my, "utf8");
// 开始插入,这边是先在本地创建数据库和user表,所以可以直接插入
std::string insert_sql = "insert into user (name, password) values (\'";
insert_sql += sql_name.c_str();
insert_sql += "\', \'";
insert_sql += sql_password.c_str();
insert_sql += "\');";
if(mysql_query(my, insert_sql.c_str()) == 0)
{
std::cout << "<html>";
std::cout << "<head><meta charset=\"utf-8\"></head>";
std::cout << "<body><h1>注册成功!</h1></body>";
}
}
// 关闭连接
mysql_close(my);
}
return 0;
}
(8)、Makefile的编码和脚本:
Makefile的编写:首先是服务器目录下的Makefile
bin=HttpServer
src=Main.cpp
cc=g++
LD_FLAGS=-std=c++11 -l pthread
curr=$(shell pwd)
.PHONY:all
all:$(bin) CGI
$(bin):$(src)
$(cc) -o $@ $^ $(LD_FLAGS)
CGI:
cd $(curr)/cgi;\
make;\
cd -
.PHONY:clean
clean:
rm -rf $(bin);\
rm -rf output;\
cd $(curr)/cgi;\
make clean;\
cd -
.PHONY:output
output:
mkdir -p output;\
cp -rf $(bin) $(curr)/output;\
cp -rf wwwroot $(curr)/output;\
cp -rf $(curr)/cgi/test_cgi $(curr)/output/wwwroot
cp -rf $(curr)/cgi/mysql_cgi $(curr)/output/wwwroot
CGI程序文件夹下的Makefile:
curr=$(shell pwd)
.PHONY:all
all:test_cgi mysql_cgi
test_cgi:test_cgi.cpp
g++ -o $@ $^ -std=c++11
mysql_cgi:mysql_cgi.cpp
g++ -o $@ $^ -std=c++11 -I $(curr)/include -L $(curr)/lib -l mysqlclient -l pthread -l dl -static
.PHONY:clean
clean:
rm -rf test_cgi mysql_cgi
为了构建工程方便把make命令都写进build.sh:
#!/bin/bash
make clean;\
make;\
make output
(9)、简单编写表单网页测试:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<form action="/mysql_cgi" method="POST">
用户名:<input type="text" name="用户名">
<br>
<br>
<br>
密 码:<input type="text" name="密码">
<br><br>
<input type="submit" value="提交注册">
</form>
<p>如果您点击提交,表单数据会提交给后台CGI程序处理</p>
</body>
</html>
(10)、项目目录结构:
五、测试结果:
网页访问提交:
再到日志和数据库查看:
六、结束语:
其实现在有很多开源的http服务器,以后绝对不可能会让你从0开始写,但是这样折腾下来,确实会对上网的技术细节有另外一个层次的理解。祝大家都能学到想学到的,而且都能学懂。