项目简介
由于想要了解HTTP的原理,但是看完书之后感觉理解不够深刻,所以就自己手写一个HTTP服务器。通过这个项目可以更加深入的了解HTTP协议的原理。
项目采用C/C++
编写,操作系统为Windows
,IDE为Visual Studio 2019
。
项目地址
HTTP协议简介
HTTP 是一种能够获取如 HTML 这样的网络资源的 protocol
(通讯协议)。它是在 Web 上进行数据交换的基础,是一种 client-server 协议,也就是说,请求通常是由浏览器发起的。一个完整的 Web 文档通常是由不同的子文档拼接而成的,像是文本、布局描述、图片、视频、脚本等等。
HTTP 消息由采用 ASCII 编码的多行文本构成。
请求信息可以分为请求行、请求头、请求体。
响应信息可以分为响应行、响应头、响应体。
请求行和请求头的每一行之间都是以\r\n结尾,请求体和请求头之间还有一组\r\n
GET / HTTP/1.1\r\n # 请求行
Host: wwww.juejin.cn\r\n # 请求头
Accept-Language: fr\r\n # 请求头
Accept:text/javascript, application/javascript\r\n # 请求头
Accept-Encoding: gzip, deflate, br\r\n # 请求头
Accept-Language: zh-CN,zh;q=0.9\r\n # 请求头
Connection: keep-alive\r\n # 请求头
\r\n
# 这里是请求体
响应行和响应头的每一行之间都是以\r\n结尾,响应体和响应头之间还有一组\r\n
HTTP/1.1 200 OK\r\n # 响应行
Content-Type: text/html; charset=utf-8\r\n # 响应头
Connection: keep-alive\r\n # 响应头
Content-Language: zh-CN\r\n # 响应头
\r\n
<!DOCTYPE html> # \r\n之后的内容就是响应体
<html lang="en">
<head>
<title>A simple webpage</title>
</head>
<body
<p>Hello, world!</p>
</body>
</html>
项目开始
网络通信初始化,设置端口可复用,设置监听的地址和端口号
#pragma comment(lib, "WS2_32.lib")
SOCKET startup(unsigned short* port) {
// 1. 网络通信初始化
WSADATA data;
// 如果成功,则WSAStartup函数将返回0
int result = WSAStartup(
MAKEWORD(1, 1), // 调用者可以使用的Windows套接字规范的最高版本
&data // 指向WSADATA数据结构的指针,该数据结构将接收Windows套接字实现的详细信息。
);
if (result) { // 返回0表示成功,不为0表示失败
error_die("WSAStartup");
}
// 2.创建套接字
SOCKET server_socket = socket(
PF_INET, // 套接字类型-IPV4
SOCK_STREAM, // 数据流-TCP
IPPROTO_TCP // 协议
);
if (server_socket == -1) {
// 打印错误提示并结束程序
error_die("套接字");
}
// 3.设置端口可复用
int opt = 1;
result = setsockopt(
server_socket, // 套接字。
SOL_SOCKET, // 被设置的选项的级别,如果想要在套接字级别上设置选项,就必须把level设置为 SOL_SOCKET
SO_REUSEADDR, // 打开或关闭地址复用功能
(const char*)&opt, // 传入1表示打开地址复用功能
sizeof(opt) // 参数4的大小
);
if (result == -1) {
error_die("setsockopt");
}
// 4.配置服务器端的网络地址
SOCKADDR_IN server_addr;
memset(&server_addr, 0, sizeof(server_addr)); // 初始化为0
server_addr.sin_family = PF_INET;// 套接字类型
server_addr.sin_port = htons(*port); // 端口-小端转大端
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // ip地址-127.0.0.1 小端转大端
// 绑定套接字
result = ::bind(server_socket, (SOCKADDR*)&server_addr, sizeof(server_addr));
if (result < 0) {
error_die("bind");
}
// 动态分配一个端口
if (*port == 0) {
int nameLen = sizeof(server_addr);// 地址缓冲区长度
// 获取一个套接口的本地名字
result = getsockname(
server_socket, // 标识一个已捆绑套接口的描述符号
(SOCKADDR*)&server_addr, // 接收套接口的地址
&nameLen // 地址缓冲区长度-字节
);
if (result < 0) {
error_die("getsockname");
}
*port = server_addr.sin_port;
}
// 创建监听队列
if (listen(server_socket, 5) < 0) {
error_die("listen");
}
return server_socket;
}
接下写一个listenSocket函数用来初始化网络通信并监听客户端发送的消息,接收到用户的连接请求之后就创建一个新的线程处理用户请求。
// 监听套接字-传入一个需要监听的端口
void listenSocket(unsigned short port) {
DWORD threadId; // 线程ID
HANDLE hThread; // 线程句柄
/*初始化socket*/
SOCKET serv_socket = startup(&port); // 调用网络通信初始化函数
printf(u8"http服务器初始化,正在监听%d端口... \n", port);
struct sockaddr_in client_addr; // 客户端信息
int client_addr_len = sizeof(client_addr);
// 循环等待套接字
while (true)
{
// 阻塞式等待用户通过浏览器发送请求
SOCKET client_socket = accept(serv_socket, (struct sockaddr*)&client_addr, &client_addr_len);
if (client_socket == -1) {
error_die("accept");
}
// 创建一个新进程处理客户端请求
hThread = (HANDLE)_beginthreadex(NULL, 0, accept_request, (void*)client_socket, 0, (unsigned*)&threadId);
}
// 退出服务端套接字
closesocket(serv_socket);
}
上面的函数接收到客户端的请求之后,会创建一个新的线程进程处理客户端请求,这里创建一个线程处理函数
/*
* 处理用户请求的线程
*/
unsigned WINAPI accept_request(void* arg) {
Http* app = getHttp();
SOCKET client = (SOCKET)arg; // 客户端套接字
// 解析客户端请求
Request request(client);
// 生成服务端响应
Response response(request, app->getStatic());
cout << "methods:" << request.getMethods() << " " << "path:" << request.path << endl;
// 判断是否是 GET 或者 POST 请求
if (request.getMethods() != "GET" && request.getMethods() != "POST" && request.getMethods() != "OPTIONS") {
// 向浏览器返回一个错误页面
unimplement(client);
closesocket(client);
return 0;
}
// 处理请求和响应
HandleFUNC allFunc = app->all("*");
if (allFunc) {
allFunc(request, response);
}
allFunc = app->all(request.path);
if (allFunc) {
allFunc(request, response);
}
// 如果是OPTIONS请求
if (request.getMethods() == "OPTIONS") {
handle_options(client, response);
} else if (request.getMethods() == "GET") {
allFunc = app->get(request.path);
// 如有有对应的请求处理函数
if (allFunc) {
allFunc(request, response);
}
else {
// 静态资源
response.send();
}
} else if (request.getMethods() == "POST") {
allFunc = app->post(request.path);
if (allFunc) {
allFunc(request, response);
}
}
//关闭客户端socket
closesocket(client);
return 0;
}
创建解析HTTP请求的类
class Request {
private:
SOCKET client;// 客户端套接字
unordered_map<string, string> head; // 请求头信息
unordered_map<string, string> query; // 请求参数
private:
string method; // 请求方法
string url; // 资源路径
public:
string path; // 请求路径
private:
unordered_map<string, string> body_form; // 保存form表单提交的请求体
JSON body_json; // 保存json提交的请求体
XML body_xml; // 保存xml的请求体
/* form-data */
vector<FormDataField> fields; // 参数对象
vector<FormDataFile> files; // 文件对象
public:
Request();
// 传入套接字,获取请求头信息
Request(SOCKET clientSock);
// 设置请求信息
void setRequest(SOCKET clientSock);
// 设置请求头
void setHead(SOCKET clientSock);
// 设置请求体
void setBody(SOCKET clientSock);
// 解析form表单的提交
void parseForm(string& body);
// 解析json格式的提交
void parseJSON(string& body);
// 解析XML格式的提交
void parseXML(string& body);
// 解析FormData格式的提交
void parseFormData(vector<char>& body);
// 获取请求信息
string getRequest();
string getMethods();
string getUrl();
// 浏览器是否支持gzip
bool isgzip();
// 读取剩下的资源,释放内存-discard
void release();
// 获取socket
SOCKET getSocket();
// 释放资源
~Request();
};
Request类的构造函数,setRequest这个函数用来从socket中不断地读取字符直到碰到\r\n
为止,代表读取完整个请求行
// 构造函数-解析请求信息
Request::Request(SOCKET clientSock) {
this->setRequest(clientSock);
}
// 传入套接字,读取请求信息
void Request::setRequest(SOCKET clientSock) {
this->client = clientSock; // 保存socket- 备用
char buff[1024] = { 0 }; // 1K
// 读取一行数据
int numchars = get_line(clientSock, buff, sizeof(buff) - 1);
int j = 0; // buff下标
int i = 0; // method下标
char method[255] = { 0 }; // 请求方法 GET or POST
trimStart(buff, sizeof(buff), &j); // 跳过空格
// 获取请求方法
while (!isspace(buff[j]) && i < sizeof(method) - 1) {
method[i++] = buff[j++];
}
method[i] = '\0';
this->method = method;
// 解析资源文件路径
char url[255] = { 0 }; // 存放完整资源路径
trimStart(buff, sizeof(buff), &j); // 跳过空格
i = 0; // 重置
while (
j < sizeof(buff) &&
i < sizeof(url) - 1 &&
!isspace(buff[j])
)
{
url[i++] = buff[j++];
}
url[i] = '\0';
char* outer_ptr = NULL;
// 用?分割请求参数 /api?name=zs&age=1
char* tmp = strtok_s(url, "?", &outer_ptr);
this->url = tmp; // 请求资源路径
this->path = tmp; // 请求路径
// 获取请求参数
tmp = strtok_s(NULL, "?", &outer_ptr);
if (tmp) {
parseQuery(this->query, tmp);
}
// 读取剩下的数据,设置请求头
this->setHead(clientSock);
}
setHead函数从socket中不断地读取字符,每碰到一次\r\n
代表读取了一行的请求头信息。直到读取到\r\n\r\n
(两个\r\n\r\n),代表请求头已经读取完毕,剩下的是请求体信息。
// 设置请求头
void Request::setHead(SOCKET clientSock)
{
// Host: developer.mozilla.org
char buff[1024] = { 0 };
char* outer_ptr = NULL;
int numchars = 1;
while (numchars > 0 && strcmp(buff, "\n"))
{
numchars = get_line(clientSock, buff, sizeof(buff) - 1);
if (numchars > 1 && strcmp(buff, "\n")) { // 如果不是最后一个字符 '\n'
char* tmp = strtok_s(buff, ":", &outer_ptr); // 用 : 分割 Content-tye: text/plain
string key(tmp);
delete_space(key); // 去除两端空格
tmp = strtok_s(NULL, ":", &outer_ptr);
string value(tmp);
delete_space(value); // 去除两端空格
if (!key.empty() && !value.empty()) {
this->head.emplace(key, value);
}
}
}
if (this->method == "POST") {
//这是里post请求
setBody(clientSock);
}
}
setBody函数用来读取请求体信息,一般来说GET请求没有请求体,所以只需要判断是否是POST请求,如果是则读取请求体信息。但是服务端也不知道客户端发送的请求体是多少个字节,所以请求头有一个参数Content-Length
,这个参数代表请求体中发送的数据的字节数。
// 设置请求体
void Request::setBody(SOCKET clientSock)
{
string length = this->head["Content-Length"]; // 请求体长度
string body; // 临时保存请求体-文本文件
vector<char> body_binary;
int len = atoi(length.c_str()); // string-转-int
if (len <= 0) return;
string contentType = this->head["Content-Type"]; // 判断form-data
if (contentType.find("multipart/form-data") != -1) {
get_body(clientSock, len, body_binary);
}
else {
get_body(clientSock, len, body);
}
// 处理4种不同的post请求提交方式
if (contentType.find("application/x-www-form-urlencoded") != -1) {
parseForm(body);
}
else if (contentType.find("application/json") != -1) {
parseJSON(body);
}
else if (contentType.find("text/xml") != -1) {
parseXML(body);
}
else if (contentType.find("multipart/form-data") != -1) {
parseFormData(body_binary);
}
}
读取完请求体中的信息之后,需要根据不同的POST提交分别去解析请求体中的数据,其中FormData
格式的提交是字符和二进制文件混合在一起的,需要特别注意。
// // 解析form表单的提交
void Request::parseForm(string &body)
{
char* str = (char*)body.c_str();
parseQuery(this->body_form, str); // 解析form表单,并保存
}
// 解析json格式的提交
void Request::parseJSON(string& body)
{
this->body_json = JSON::parse(body);
for (JSON::iterator it = this->body_json.begin(); it != this->body_json.end(); ++it) {
std::cout << it.key() << " : " << it.value() << "\n";
}
}
// 解析XML格式的提交
void Request::parseXML(string& body)
{
this->body_xml.Parse(body.c_str());
}
// 解析FormData格式的提交
void Request::parseFormData(vector<char>& body)
{
string contentType = this->head["Content-Type"];
// ; 分割 multipart/form-data; boundary=----WebKitFormBoundary5On24LeqJdcOBcBC
size_t index = contentType.find("=");
if (index == -1) return;
string boundary = contentType.substr(index + 1);
string start = "--" + boundary; // 每一个数据段开始位置
string end = start + "--"; // 数据段结束位置
string buff; // 文本缓存
vector<char> part_buff;// 二进制缓存
int startindex = 0; // 读取到的位置
unordered_map<string, string> partHead; // 存放part头
while (true)
{
buff.clear();
startindex = get_line(body, buff, startindex);
// 读取到开始区域
if (buff == start) {
partHead.clear();
buff.clear();
startindex = parsePartHead(body, startindex, buff, partHead);
}
else if (buff == end) {// 读取到了结束标志
buff.clear();
break;
}
if (buff.size() == 0) { // 读取内容区域 { buff.size() == 0 表示读到了\r\n }
buff.clear();
// 判断是文本还是 文件
if (partHead.count("filename") > 0) { // 如果存在filename字段,说明是一个文件
/* 第一种读取方式 */
startindex = parsePartBody_File(body, part_buff, startindex, start, end);
cout << "上传文件大小:" << part_buff.size() << endl;
/* 第二种读取方式 */
// startindex = get_PartBody_File(body, part_buff, startindex);
FormDataFile file;
if (partHead.count("Content-Disposition")) {
file.contentDisposition = partHead["Content-Disposition"];
}
if (partHead.count("name")) {
file.contentDisposition = partHead["name"];
}
if (partHead.count("filename")) {
file.contentDisposition = partHead["filename"];
}
if (partHead.count("Content-Type")) {
file.contentDisposition = partHead["Content-Type"];
}
file.value.swap(part_buff);
this->files.push_back(file); // 保存文件对象
}
else {
startindex = get_line(body, buff, startindex);
FormDataField field;
if (partHead.count("Content-Disposition")) {
field.contentDisposition = partHead["Content-Disposition"];
}
if (partHead.count("name")) {
field.contentDisposition = partHead["name"];
}
if (partHead.count("filename")) {
field.contentDisposition = partHead["filename"];
}
if (partHead.count("Content-Type")) {
field.contentDisposition = partHead["Content-Type"];
}
field.value.swap(buff);
this->fields.push_back(field); // 保存参数对象
}
}
}
}
Response类用来给客户端返回信息
class Response
{
private:
// 响应行
string protocol = "HTTP/1.1 "; // 协议和版本
string status_code = "200 OK\r\n"; // 状态码和状态描述
unordered_map<string, string> head; // 响应头
private:
bool gzip;
string url;// 请求路径
string folder; // 文件夹
string path; // 文件路径
SOCKET client;// 套接字
string mime; // content-type
int res_is_empty; // 资源是否存在 -1 不存在
public:
Response();
Response(Request& request, string folder);
void send(); // 给客户端返回资源
void send(string); // 给客户端返回字符串
void header(string key, string value); // 设置响应头
unordered_map<string, string> getHeader(); // 返回响应头
void setGZIP(bool); // 设置gzip
private:
void sendHeaders(); // 发送响应头
void snedFile(string filePath); // 发送资源文件
void sendFileGZIP(); // 发送GZIP资源文件
void sendText(string);// 发送文本
void generateHead(); // 生成默认响应头
void generatePath(string url); // 生成请求文件路径
void generateMIME(); // 生成数据类型
};
给客户端返回需要的信息之后,这次请求和响应就结束了。