手写一个HTTP服务器

333 阅读7分钟

项目简介

由于想要了解HTTP的原理,但是看完书之后感觉理解不够深刻,所以就自己手写一个HTTP服务器。通过这个项目可以更加深入的了解HTTP协议的原理。
项目采用C/C++编写,操作系统为Windows,IDE为Visual Studio 2019

项目地址

HTTP协议简介

HTTP 是一种能够获取如 HTML 这样的网络资源的 protocol(通讯协议)。它是在 Web 上进行数据交换的基础,是一种 client-server 协议,也就是说,请求通常是由浏览器发起的。一个完整的 Web 文档通常是由不同的子文档拼接而成的,像是文本、布局描述、图片、视频、脚本等等。

image.png

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(); // 生成数据类型
};

给客户端返回需要的信息之后,这次请求和响应就结束了。