目录
[如何确保从接收到的字节流中提取到一条完整的HTTP请求报文 ?](#如何确保从接收到的字节流中提取到一条完整的HTTP请求报文 ?)
[八、 主函数逻辑设计](#八、 主函数逻辑设计)
一、TCP服务端代码逻辑思路
我们在上一个文章中的服务端测试代码中讨论过,该测试代码是有bug的:由于HTTP协议是基于TCP来传输数据的,这就代表请求和应答的传输是面向字节流的。我们的服务端中每次向缓存中读取的数据是有限的,如果一次HTTP的请求过大,我们无法保证字节流中一定有一条完整的HTTP请求,此时应用层不对现有的字节流做处理,继续返回传输层接收来自客户端的HTTP请求信息。直到字节流中包含至少一条完整的HTTP请求,我们再从字节流中将一条完整的请求提取出来。
现在我们来重新设计一下服务端,逻辑如下:服务器的主要工作是为通信创造条件---即创建监听套接字、绑定IP和端口号、监听来自客户端的请求、接收来自客户端的请求并获取IO套接字、通过IO套接字接收请求信息、处理请求信息、将处理结果返回给客户端。
我们前面说过,服务器在面临不同的请求时,所做的处理也各不相同,但无论是处理何种请求,服务器都必须要做前四步操作。因此,我们可以对服务器代码进行解耦。将涉及到接收、处理、返回信息的代码从服务端分离出去,由应用层提供相应的业务处理函数传入服务器中。由于涉及到通信,因此IO套接字、标识客户端的网络结构体是必须要作为函数参数,供函数中的代码片段使用。
同时,由于未来服务器可能面临大批量的服务请求,我们采取多线程模式实现并发服务器,来减少操作系统创建独立进程的开销。
二、TCP服务端代码设计
由于我们在之前的文章中已经多次实现了TCP服务端代码并做了详细讲解,此处不再过多赘述。如果有遗忘,请移步至该文章:【Linux】网络编程套接字Socket:TCP网络编程_linux tcp服务端-CSDN博客
#pragma once
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <string>
#include <iostream>
#include <arpa/inet.h>
#include <cstring>
#include <signal.h>
#include <sys/wait.h>
#include <functional>
#include <pthread.h>
#include <memory>
extern const int BUFFER_SIZE;
static const int MAX_LINK_NUM = 6;
using io_service_t = std::function<void(const int io_sockfd, const struct sockaddr_in& from_client)>;
class TcpServer
{
private:
int _listen_sockfd; // 监听套接字
uint16_t _port; // 端口号
io_service_t _handle_method; // 通信服务函数
bool is_running;
public:
// 应用层提供端口号和业务函数,构造服务端
TcpServer(uint16_t port, io_service_t service_method)
:_listen_sockfd(-1), _port(port), _handle_method(service_method), is_running(false)
{
// 初始化服务器操作
// 1、创建监听套接字
CreateListenSockfd();
// 2、绑定IP + PORT
BindLocalIPAndPort();
// 3、将套接字设置为监听状态
SetListenSockfd();
}
~TcpServer()
{}
// 1、创建监听套接字
void CreateListenSockfd()
{
if((_listen_sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0){
std::cerr << "CreateListenSockfd false!" << std::endl;
exit(errno);
}
}
// 2、绑定IP + PORT
void BindLocalIPAndPort()
{
struct sockaddr_in local_addr;
memset(&local_addr, 0, sizeof(local_addr));
local_addr.sin_addr.s_addr = INADDR_ANY;
local_addr.sin_port = htons(_port);
local_addr.sin_family = AF_INET;
if(bind(_listen_sockfd, (struct sockaddr*)&local_addr, sizeof(local_addr)) < 0){
std::cerr << "BindLocalIPAndPort false!" << std::endl;
exit(errno);
}
}
// 3、将套接字设置为监听状态
void SetListenSockfd()
{
if(listen(_listen_sockfd, MAX_LINK_NUM) < 0){
std::cerr << "SetListenSockfd false!" << std::endl;
exit(errno);
}
}
// 4、接收来自客户端的连接请求,获取io套接字和客户端网络结构体
void AcceptLinkAndGetIOSockfdWithClientAddr(int* iosockfd, struct sockaddr_in* fromclient)
{
struct sockaddr_in from_client;
socklen_t len = sizeof(from_client);
memset(&from_client, 0, len);
int io_sockfd = accept(_listen_sockfd, (struct sockaddr*)&from_client, &len);
if(io_sockfd < 0){
std::cerr << "AcceptLinkAndGetIOSockfdWithClientAddr false!" << std::endl;
exit(errno);
}
*iosockfd = io_sockfd;
*fromclient = from_client;
}
// 进行业务处理,线程执行函数
static void* ThreadServiceRoute(void* ThreadSelf)
{
pthread_detach(pthread_self()); // 线程分离
ThreadNeedData* thread_self = static_cast<ThreadNeedData*>(ThreadSelf);
thread_self->_self->_handle_method(thread_self->_io_sockfd, thread_self->_client_netaddr); // 进行业务处理
close(thread_self->_io_sockfd); // 业务处理完毕后,关闭无用文件描述符
delete thread_self; // 释放线程数据资源
return nullptr;
}
// 启动服务器
void Loop()
{
std::cout << "Tcp Server Start..." << std::endl;
is_running = true;
int io_sockfd = 0;
struct sockaddr_in client;
memset(&client, 0, sizeof(client));
while(true)
{
// 1、接收客户端连接请求
AcceptLinkAndGetIOSockfdWithClientAddr(&io_sockfd, &client);
std::cout << "Tcp Server Accept Success..." << std::endl;
// 2、创建多线程处理业务
ThreadNeedData* thread_data = new ThreadNeedData(this, io_sockfd, client);
pthread_t tid = 0;
if(pthread_create(&tid, nullptr, ThreadServiceRoute, thread_data) < 0)
{
std::cerr << "pthread_create false!" << std::endl;
exit(errno);
}
// 主线程继续接受请求,创建线程以处理业务
}
is_running = false;
}
// 内部类,线程所需要的数据,作为指针参数传递给pthread_create函数构造线程
class ThreadNeedData
{
public:
int _io_sockfd;
struct sockaddr_in _client_netaddr;
TcpServer* _self; // 目的:使用TcpServer中的方法
public:
ThreadNeedData(TcpServer* self, const int io_sockfd, const struct sockaddr_in& client_netaddr)
:_self(self), _io_sockfd(io_sockfd), _client_netaddr(client_netaddr)
{}
};
};
三、URL编码与解码
体力活,照搬即可,不是本节重点,不用细致纠结。
// 作用:将输入的字符串按照 URL 编码规则进行编码,并返回编码后的字符串
std::string URLencode(const std::string &input)
{
// std::ostringstream 是 C++ 标准库中的一个类,继承自 std::ostream,用于进行字符串的流式输出。
// 它的功能类似于 std::cout,但输出内容会被写入到一个字符串中。
std::ostringstream escaped;
// escaped.fill('0'); 设置流输出时填充字符为 '0',主要用于后续的十六进制输出,确保不足两位时用 0 填充。
escaped.fill('0');
// escaped << std::hex; 设置流的输出基数为十六进制,这样后续的整数输出将以十六进制格式进行。
escaped << std::hex;
for (std::string::const_iterator i = input.begin(); i != input.end(); ++i)
{
char c = *i;
// 保留未保留字符:字母和数字
if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~')
{
escaped << c;
}
else
{
// 将特殊字符转换为 %XX 形式
escaped << std::uppercase;
escaped << '%' << std::setw(2) << (int)(unsigned char)c;
escaped << std::nouppercase;
// 如果字符 c 不是字母、数字或者那些不需要编码的字符,那么它需要被编码。
// std::uppercase 用于确保接下来的十六进制字母输出为大写形式。
// escaped << '%' 输出一个百分号 %,这是 URL 编码中表示特殊字符的格式。
// (int)(unsigned char)c 将字符 c 转换为其对应的 ASCII 整数值(因为 char 可能是有符号的,所以先转换为 unsigned char 确保得到正确的无符号值)。
// std::setw(2) 设置输出宽度为 2,这样十六进制数不足两位时会用之前设置的填充字符 '0' 来补齐。
// escaped << std::nouppercase; 恢复十六进制字母输出为小写(默认情况下输出小写字母)。
}
}
return escaped.str(); // escaped.str() 从 std::ostringstream 对象中提取最终生成的字符串,并将其返回
}
// 将百分号编码的字符串转换回普通字符串
std::string URLdecode(const std::string &input)
{
std::string decoded;
std::string::size_type i = 0;
while (i < input.size())
{
if (input[i] == '%')
{
// 将 %XX 转换为对应的字符
if (i + 2 < input.size())
{
std::string hex = input.substr(i + 1, 2);
char c = static_cast<char>(std::stoi(hex, nullptr, 16));
decoded += c;
i += 3; // 跳过 %XX
}
else
{
// 无效的百分号编码
decoded += input[i++];
}
}
else if (input[i] == '+')
{
// 在某些情况下,'+' 被解释为空格
decoded += ' ';
++i;
}
else
{
decoded += input[i++];
}
}
return decoded;
}
四、符号定义
下述符号为HTTP请求和应答格式中的常用符号,为了方便提前定义出来。
const int BUFFER_SIZE = 1024;
const std::string header_final_and_message_begin = "\r\n\r\n";
const std::string header_end_sign = "\r\n";
const std::string key_value_sep_sign = ": ";
const std::string content_attribute = "Content-Length";
const std::string space_block = " ";
const std::string root_path = "wwwroot";
const std::string home_page_path = "wwwroot/home_page.html";
五、HTTP请求设计
在本节,我们将使用浏览器作为客户端访问我们的服务端。而浏览器所发送的HTTP请求是已经进行序列化的字节流。因此,我们想要得到完整的HTTP请求以及请求中的各种详细信息,我们就需要对接收到的请求进行正确的序列化操作。
同时,我们提供用于获得请求中具体信息的各种函数方法接口,以便于根据请求信息构建响应。
例如:当HTTP请求想访问具体的路径文件时,我们就要在响应正文中插入该文件的内容。此时,我们就需要拿到HTTP请求中的文件路径。
class HTTP_Request
{
private:
std::string request_line; // 请求行
std::vector<std::string> request_headers; // 请求报头
std::string black_line; // 空行
std::string content; // 请求正文
std::string visit_path; // 访问文件路径
// 细分
std::string request_method; // 请求方法
std::string request_url; // url
std::string http_version; // HTTP版本
std::unordered_map<std::string, std::string> header_key_val; // 属性和内容的键值对
std::string file_suffix_kind; // 文件后缀类型
std::string parameter; // url中的参数
public:
HTTP_Request()
: visit_path(root_path)
{
}
// 获取一行请求信息
std::string GetLine(std::string &str)
{
size_t end_pos = str.find(header_end_sign);
// 找不到分隔符说明该请求报文除了正文部分外已经全被提取完毕,返回空
if (end_pos == std::string::npos)
{
return std::string();
}
// 查找过程中,可能遇见单独的换行符"\r\n",此时end_pos == 0, ret为空
std::string ret = str.substr(0, end_pos);
str.erase(0, ret.size() + header_end_sign.size());
return ret.empty() ? header_end_sign : ret;
}
// 提取正文
std::string GetContent(std::string &str)
{
std::string content = str.substr();
str.erase();
return content;
}
void DeSerialize(std::string &request_str)
{
// 1、提取请求行
request_line = GetLine(request_str);
// 2、提取请求报头
std::string header_line;
do
{
header_line = GetLine(request_str);
// 提取到换行符或者字节流中的请求行和报头都已经提取完毕
if (header_line == header_end_sign || header_line.empty())
{
break;
}
request_headers.emplace_back(header_line);
} while (true);
// 3、提取空行
black_line = header_end_sign;
// 4、提取正文
content = GetContent(request_str);
//----------------继续细分---------------------
// 请求行中包含: 方法、url、http版本
// 请求报头中包含: 属性、内容
ParseRequestLine();
ParseRequestHeaders();
}
// 继续细分提取请求行
void ParseRequestLine()
{
// 创建 std::stringstream 对象:
// 通过将 _req_line 作为参数传递给 std::stringstream 的构造函数,创建了一个字符串流对象 ss。
// 这意味着 ss 将会处理 _req_line 字符串,就像它是一个输入流那么进行处理。
std::stringstream ss(request_line);
// 自然的,在标准输入中,空格作为输入结束的标志
ss >> request_method >> request_url >> http_version;
// 对于url,字符'?'之前是文件路径,之后是查询参数。所以我们要将文件路径和参数提取出来
// 情况:如果只有根目录,那么我们将访问文件路径设置为"首页"
// 如果是其他文件路径,在web根目录之后添加文件路径即可
auto pos_val = request_url.find("?"); // 查看url中是否有参数
std::string want_path;
if(pos_val!= std::string::npos){
parameter = request_url.substr(pos_val + 1);
want_path = request_url.substr(0, pos_val);
}
else{
want_path = request_url; // 没有参数的话直接在web根目录后添加访问路径即可
}
visit_path += want_path;
if (visit_path[visit_path.size() - 1] == '/') // 如果是根目录,就将路径设置为首页路径
{
visit_path = home_page_path;
}
// 得到访问文件的后缀名
auto pos_suffix = want_path.rfind(".");
if (pos_suffix != std::string::npos)
{
file_suffix_kind = want_path.substr(pos_suffix);
}
else
{
file_suffix_kind = ".default";
}
}
// 继续细分提取请求报头
void ParseRequestHeaders()
{
for (auto &header : request_headers)
{
size_t pos = header.find(key_value_sep_sign);
if (pos == std::string::npos)
{
continue;
}
std::string key = header.substr(0, pos);
std::string val = header.substr(pos + key_value_sep_sign.size());
if (key.empty() || val.empty())
{
continue;
}
header_key_val.insert(std::make_pair(key, val));
}
}
std::string GetFilePath()
{
return visit_path;
}
std::string GetFileSuffix()
{
return file_suffix_kind;
}
std::string RequestMethod()
{
return request_method;
}
};
六、HTTP响应设计
class HTTP_Response
{
private:
std::string response_line; // 响应行
std::vector<std::string> response_headers; // 响应报头组
std::string black_line; // 空行
std::string content; // 响应正文
// 细分
std::string status_code; // 状态码
std::string status_code_desc; // 状态码描述
std::string http_version; // HTTP版本
std::unordered_map<std::string, std::string> header_key_val; // 属性和内容的键值对
public:
HTTP_Response()
:http_version("HTTP/1.1")
{}
std::string SetReponseLine()
{
return http_version + space_block + status_code + space_block + status_code_desc + header_end_sign;
}
// 设置状态码
void SetCode(const std::string &code)
{
status_code = code;
}
// 设置状态码描述
void SetCodeDesc(const std::string &desc)
{
status_code_desc = desc;
}
// 设置正文内容
void SetContent(const std::string &file_content)
{
content = file_content;
}
// 设置请求报头的key和value
void SetHeader(const std::string &key, const std::string &val)
{
header_key_val.insert(std::make_pair(key, val));
}
// 构建响应报头
void SetResponseHeaders()
{
std::string header;
for (auto &kv : header_key_val)
{
header = kv.first + ": " + kv.second + "\r\n";
response_headers.emplace_back(header);
}
}
// 序列化
void Serialize(std::string *out)
{
response_line = SetReponseLine();
*out += response_line;
SetResponseHeaders();
for (auto &header : response_headers)
{
*out += header; // 添加报文
}
*out += header_end_sign; // 添加换行符
*out += content; // 添加响应正文
}
};
七、IO服务设计
我们之前讲过,要将IO服务与服务器实现代码解耦。所以针对HTTP协议格式的通信服务,我们将使用该类中的NetIOService函数作为TCP服务端的IO服务功能函数。
功能概述:1、接收来自浏览器的HTTP请求字节流;2、从接收到的字节流中提取一条完整的HTTP请求;3、对HTTP请求进行反序列化处理,得到具体的HTTP请求结构体;4、处理HTTP请求,并返回相应的HTTP应答对象;5、对HTTP应答进行序列化处理为字节流,发送给客户端(浏览器);
其中,我们将着重讲解:从接收到的字节流中提取一条完整的HTTP请求。
如何确保从接收到的字节流中提取到一条完整的HTTP请求报文 ?
由于HTTP协议是基于TCP来传输数据的,这就代表请求和应答的传输是面向字节流的。我们的服务端中每次向缓存中读取的数据是有限的,如果一次HTTP的请求过大,我们无法保证字节流中一定有一条完整的HTTP请求,此时应用层不对现有的字节流做处理,继续返回传输层接收来自客户端的HTTP请求信息。直到字节流中包含至少一条完整的HTTP请求,我们再从字节流中将一条完整的请求提取出来。
那么我们如何保证一定能正确检测到字节流中是否存在至少一条完整的HTTP请求报文?
通过观察HTTP请求的格式我们就会发现:请求属性行和请求正文之间有两个连续的"\r\n",如果该次HTTP请求中不包含请求正文,那么"\r\n\r\n"就是该次请求报文的结束标志。那么问题来了,为什么我们不以"\r\n"作为判断报文结束的标志符呢?原因很简单,无论是请求行还是请求报头,他们之间的分隔符都是"\r\n",因此"\r\n"并不具有标识报文末尾位置的特殊性。
可见,在没有响应正文时,我们只需要判断字节流中是否有两个连续的"\r\n",即可判断字节流中是否至少有一条完整的请求。如果当前字节流中没有"\r\n\r\n",说明字节流中绝对不包含一条完整的请求,我们对当前字节流不做处理,直接回到传输层继续接收信息。
可是问题来了,我们并不能确保每次接收到的都是不包含请求正文的HTTP请求,而一条HTTP请求中的请求正文文末并没有任何特殊的结束标识符,那我们如何进行判断呢?
在学习过HTTP请求报头的属性后我们了解到,对于包含请求正文的HTTP请求,在它们的请求报头中有这么一个属性:Content_Length: XXX 。前者代表属性名,后者标识正文的长度。有了这个报头,那我们的工作就简单多了。我们只需要在当前字节流中依次按行提取请求报头,在每行请求报头中寻找是否包含该属性字段。如果包含,则说明当前请求中包含请求正文,我们将长度提取出来,并计算该条HTTP请求的理论总长度,并与当前字节流的长度比较即可。如果当前接收到的字节流的长度小于该条请求的理论总长度,我们就不提取出该条请求。让程序返回传输层继续进行信息接收。如果当前字节流的长度大于等于该次请求的理论长度,我们就继续进行对该次请求的提取操作。
// 处理请求得到响应的函数类型
using HandleRequestToReponse_t = std::function<HTTP_Response(HTTP_Request&)>;
class HTTP_Server
{
private:
HandleRequestToReponse_t HandleRequestToReponse; // 处理请求得到响应的功能函数
public:
HTTP_Server(HandleRequestToReponse_t func)
:HandleRequestToReponse(func)
{}
void NetIOService(const int io_sockfd, const struct sockaddr_in &from_client)
{
while (true)
{
// 1、服务端接收来自客户端的HTTP请求
std::string req_stream;
RecvInfo(io_sockfd, &req_stream);
// 2、判断字节流中是否有至少一条的完整HTTP请求,有则提取,无则继续接收信息
std::string http_message = GetOneHttpMessageFromStream(req_stream);
// 如果接收到的字符串为空,则继续去recv接收信息
if (http_message.empty()){
continue;
}
else{
std::cout << http_message << std::endl;
}
// 对http请求进行反序列化
HTTP_Request request;
request.DeSerialize(http_message);
HTTP_Response response;
// 处理响应,返回应答
response = HandleRequestToReponse(request);
std::string response_str;
// 应答序列化
response.Serialize(&response_str);
std::cout << response_str << std::endl;
// 返回应答
SendInfo(io_sockfd, response_str);
close(io_sockfd);
}
}
// 接收信息
void RecvInfo(int io_sockfd, std::string *out)
{
char buffer[BUFFER_SIZE];
ssize_t r_num = recv(io_sockfd, buffer, BUFFER_SIZE - 1, 0);
if (r_num > 0)
{
buffer[r_num] = '\0';
*out += buffer;
}
}
// 发送信息
void SendInfo(int io_sockfd, const std::string &in)
{
ssize_t w_num = send(io_sockfd, in.c_str(), in.size(), 0);
if (w_num < 0)
{
std::cerr << "Server SendInfo false!" << std::endl;
exit(errno);
}
}
// 检查每个请求行中是否包含Content_Length属性
// 如果包含,则返回正文长度
// 不包含则返回0,代表无正文
int CheckContentLengthExistsInLine(const std::string &line)
{
size_t sep_pos = line.find(key_value_sep_sign);
if (strncmp(line.c_str(), content_attribute.c_str(), content_attribute.size()) == 0)
{
size_t end_pos = line.find(header_end_sign);
return std::stoi(line.substr(sep_pos + key_value_sep_sign.size(), end_pos));
}
return 0;
}
// 返回值:正文长度 (参数:1、报头的结尾位置 2、字节流)
size_t CheckRequestEveryLine(size_t end_pos, const std::string &req_stream)
{
size_t line_satrt_pos = 0;
size_t line_end_pos = 0;
size_t content_len = 0;
std::string line_string;
while (line_end_pos != end_pos) // 如果一直检查到了报头属性的结尾位置还没找到,则说明请求中无正文
{
// 找到请求报头行的结尾位置
line_end_pos = req_stream.find(header_end_sign, line_satrt_pos);
// 提取一个请求报头行
line_string = req_stream.substr(line_satrt_pos, line_end_pos);
if ((content_len = CheckContentLengthExistsInLine(line_string)) > 0)
{
return content_len; // 存在正文长度属性则直接返回正文长度
}
// 未找到Content_Length属性,将查找的起始位置移动至下一个请求报头行的起始位置
line_satrt_pos = line_end_pos + header_end_sign.size();
}
return 0;
}
std::string GetOneHttpMessageFromStream(std::string &req_stream)
{
if(req_stream.empty()) return std::string();
// 1、首先找字节流中是否有两个连续的分割换行符------即"\r\n\r\n"
size_t header_end_pos = 0;
if ((header_end_pos = req_stream.find(header_final_and_message_begin)) == std::string::npos)
{
// 没有代表当前请求不完整,直接返回空串
return std::string();
}
// 当前请求有完整报头,接下来判断是否有报文。须在包头属性中查找是否有Content-Length报头属性
// 按行查找报头属性,并判断该次请求中是否有请求正文。如果有,得到正文长度,下一步去判断当前字节流中是否包含该完整正文
size_t content_len = CheckRequestEveryLine(header_end_pos, req_stream);
// 计算出HTTP请求字符串的理论长度
int total_len = header_end_pos + header_final_and_message_begin.size() + content_len;
// 如果该次请求中包含请求正文
if (content_len > 0)
{
// 判断当前字节流的长度是否等于该次请求的理论长度
if (total_len < req_stream.size())
{
// 如果当前字节流的长度小于请求的理论长度,直接返回传输层继续接收信息
return std::string();
}
}
// 执行流走到这儿,表明字节流中至少包含了一条完整的HTTP请求
// 提取出完整的HTTP请求,并从字节流中分割出来
std::string ret = req_stream.substr(0, total_len);
req_stream.erase(0, total_len);
return ret; // 返回提取出的HTTP请求字符串
}
};
八、 主函数逻辑设计
1、我们在构建基于HTTP的IO服务时,需要为HTTP_Server类提供一个处理请求构造应答的功能函数。
2、我们在构建服务端时,需要为其提供进行网络IO通信的功能函数。
3、web根目录及页面设置,关于具体代码大家可以在网上查找源码使用。需要注意的是,当我们想在浏览器中显示一个页面时,服务端发送的信息实际上就是一个html类型的文件内容。而该文件内容会被浏览器经过解析后生成相应的页面。
4、在函数中,我们可以根据HTTP请求的具体信息来设置HTTP响应。例如,如果服务端并无想要访问的文件信息,我们可以将在响应中设置404状态码、并返回404页面的html文件的内容给浏览器,代表当前文件不存在。或者我们可以根据HTTP中访问文件的后缀类型,在响应中根据文件名对照表来设置Content_Type属性。
#include "http_protocol.hpp"
#include "Tcp_Server.hpp"
// 获取指定路径下的文件内容
std::string GetFileContent(const std::string &path)
{
// 打开文件,以二进制模式读取
std::ifstream in(path, std::ios::binary);
// 检查文件是否成功打开
if (!in.is_open())
return std::string(); // 如果文件未打开,返回空字符串
// 将文件指针移动到文件末尾
in.seekg(0, in.end);
// 获取文件大小(即文件末尾的偏移量)
int filesize = in.tellg(); // 返回当前读取指针的位置(文件大小)
// 将文件指针移动回文件开头
in.seekg(0, in.beg);
// 创建一个字符串,并调整其大小以容纳文件内容
std::string content;
content.resize(filesize); // 调整字符串大小为文件大小
// 从文件中读取内容到字符串中
in.read((char *)content.c_str(), filesize);
// 关闭文件
in.close();
// 返回读取到的文件内容
return content;
}
// 将接收到的请求处理,并生成响应的应答返回
HTTP_Response HandleRequestToReponse(HTTP_Request &req)
{
// 1、获取HTTP请求中想要访问的文件路径
std::string want_file_path = req.GetFilePath();
// 2、获取该路径文件的文件内容
std::string content = GetFileContent(want_file_path);
HTTP_Response resp;
if (content.empty()) // 如果文件内容为空,说明想要访问的文件不存在
{
// 构建404 Not Found 应答
resp.SetCode("404");
resp.SetCodeDesc("Not Found!");
content = GetFileContent("wwwroot/404NotFound.html");
}
else
{
resp.SetCode("200");
resp.SetCodeDesc("OK!");
}
// 构建应答报头
resp.SetHeader("Content_Length: ", std::to_string(content.size()));
// 构建响应正文
resp.SetContent(content);
return resp;
}
int main(int argc, char *argv[])
{
if(argc < 2){
std::cerr << "未输入端口号..." <<std::endl;
return -1;
}
// 构建服务
HTTP_Server http_server(HandleRequestToReponse);
// 构建服务端
TcpServer tcp_server(std::stoi(argv[1]), std::bind(&HTTP_Server::NetIOService, &http_server, std::placeholders::_1, std::placeholders::_2));
// 启动服务端
tcp_server.Loop();
return 0;
}
九、关于GET方法的理解
我们来到百度首页随便搜索一个单词,并观察其URL。
可以明显观察到,我们在百度首页查询hello单词所生成的HTTP请求中的方法是GET方法。观察该URL,我们知道"?"后代表查询参数。 但"/s"表示的是文件路径吗?
在深入讨论之前,我们先来简单了解一下form表单。
**<form>
表单是 HTML 中用于创建交互式网页表单的元素,用户可以通过表单输入数据并提交给服务器。**表单通常包含各种输入字段,如文本框、单选按钮、复选框、下拉菜单等,以及提交按钮。
基本结构
一个简单的表单通常包含以下几个部分:
<form action="/submit-url" method="post">
<!-- 输入字段 -->
<label for="name">姓名:</label>
<input type="text" id="name" name="name">
<label for="email">邮箱:</label>
<input type="email" id="email" name="email">
<!-- 提交按钮 -->
<button type="submit">提交</button>
</form>
关键属性
action
: 指定表单数据提交的目标URL。method
: 指定数据提交的HTTP方法,常用值为get
和post
。
示例
<form action="/submit" method="post">
<label for="username">用户名:</label>
<input type="text" id="username" name="username">
<label for="password">密码:</label>
<input type="password" id="password" name="password">
<button type="submit">登录</button>
</form>
当我们使用form表单提交数据时,我们可以通过action设置URL,method指定该次请求所使用的方法(默认方法为POST)。但实际上,我们所设置的URL并不必须是指向文件的文件路径,也可以是一个提前约定好的具有特殊含义的字符串。例如,在百度查询的URL中,/s就代表"Search"服务,即查找服务。"?"后所跟着的就是查询参数。
因此,在未来,我们接收到不同的URL时,我们可以对我们的代码进行改造。例如:当HTTP请求的方法是GET时,我们可以提取出请求中的URL和参数,并与我们事先约定好的服务进行匹配。对于不同的URL我们可以提前设置不同的服务,这就意味着我们可以在HTTP_Server类中设置一个服务方法列表,使用哈希表作为储存。使用URL进行任务的匹配操作。
主函数代码:
HTTP_Response HandleTaskSearch(HTTP_Request &req)
{
HTTP_Response resp;
// strcasecmp函数判断字符串是否相等忽略大小写
if(strcasecmp(req.GetRequestMethod().c_str(), "GET") == 0) // 如果是get方法
{
// 获取参数
std::string para = req.GetParameter();
// 对参数做处理。。。。
std::cout << para << std::endl;
// 设置应答。。。
resp.SetCode("200");
resp.SetCodeDesc("OK!");
resp.SetContent("<html><h1>Search Success!</h1></html>");
}
else{
// .......
}
return resp;
}
int main(int argc, char *argv[])
{
if(argc < 2){
std::cerr << "未输入端口号..." <<std::endl;
return -1;
}
// 构建服务
HTTP_Server http_server(HandleRequestToReponse);
// 添加服务和服务函数
http_server.AddHandleTaskKeyVal("/s", HandleTaskSearch);
// 构建服务端
TcpServer tcp_server(std::stoi(argv[1]), std::bind(&HTTP_Server::NetIOService, &http_server, std::placeholders::_1, std::placeholders::_2));
// 启动服务端
tcp_server.Loop();
return 0;
}
HTTP_Server中改造的代码部分:
// 处理请求得到响应的函数类型
using HandleRequestToReponse_t = std::function<HTTP_Response(HTTP_Request&)>;
class HTTP_Server
{
private:
HandleRequestToReponse_t Default_HandleRequestToReponse; // 默认服务
std::unordered_map<std::string, HandleRequestToReponse_t> HandleRequestToReponse_List; // 处理请求得到响应的功能函数
public:
HTTP_Server(HandleRequestToReponse_t func)
:Default_HandleRequestToReponse(func)
{}
void AddHandleTaskKeyVal(std::string key, HandleRequestToReponse_t val)
{
key = "wwwroot" + key;
HandleRequestToReponse_List[key] = val;
}
// 判断服务列表中是否有对应的服务
bool IsServiceExistInList(std::string key)
{
for(auto& kv : HandleRequestToReponse_List)
{
if(kv.first == key){
return true;
}
}
return false;
}
void NetIOService(const int io_sockfd, const struct sockaddr_in &from_client)
{
while (true)
{
// 1、服务端接收来自客户端的HTTP请求
std::string req_stream;
RecvInfo(io_sockfd, &req_stream);
// 2、判断字节流中是否有至少一条的完整HTTP请求,有则提取,无则继续接收信息
std::string http_message = GetOneHttpMessageFromStream(req_stream);
// 如果接收到的字符串为空,则继续去recv接收信息
if (http_message.empty()){
continue;
}
else{
std::cout << http_message << std::endl;
}
// 对http请求进行反序列化
HTTP_Request request;
request.DeSerialize(http_message);
HTTP_Response response;
// ##############################################
// 处理响应,返回应答
std::string url_path = request.GetFilePath();
// 如果服务已经注册过,直接执行对应的服务函数
if(IsServiceExistInList(url_path)){
response = HandleRequestToReponse_List[url_path](request);
}
else{
// 否则执行默认服务函数
response = Default_HandleRequestToReponse(request);
}
// ###############################################
std::string response_str;
// 应答序列化
response.Serialize(&response_str);
std::cout << response_str << std::endl;
// 返回应答
SendInfo(io_sockfd, response_str);
close(io_sockfd);
}
}
}
使用postman构建GET方法的HTTP请求:
查看请求:
查看应答:
所以,在以后的服务设计中,我们可以通过**"事先约定特殊字符串的服务含义,并对每个服务构建特定的功能函数来构建应答内容"。**如我们在百度搜索hello字符,搜索服务对应的特殊字符串就是"/s",它所返回的就是搜索结果。另外对于参数的处理,我们可以使用进程间通信将参数传递给其他进程处理,可以构建子进程处理、可以在子进程中使用ecec系列函数替换为其他语言的程序进行处理等等等。
关键在于,我们要理解URL所携带的内容是多样的,我们针对不同的URL可以构建不同的处理方案。这才是我们需要理解的。
十、完整代码
http_protocol.hpp:
#pragma once
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <string>
#include <iostream>
#include <arpa/inet.h>
#include <cstring>
#include <signal.h>
#include <sys/wait.h>
#include <functional>
#include <pthread.h>
#include <memory>
#include <unordered_map>
#include <vector>
#include <sstream>
#include <utility>
#include <iomanip>
#include <fstream>
#include <functional>
const int BUFFER_SIZE = 1024;
const std::string header_final_and_message_begin = "\r\n\r\n";
const std::string header_end_sign = "\r\n";
const std::string key_value_sep_sign = ": ";
const std::string content_attribute = "Content-Length";
const std::string space_block = " ";
const std::string root_path = "wwwroot";
const std::string home_page_path = "wwwroot/home_page.html";
// 作用:将输入的字符串按照 URL 编码规则进行编码,并返回编码后的字符串
std::string URLencode(const std::string &input)
{
// std::ostringstream 是 C++ 标准库中的一个类,继承自 std::ostream,用于进行字符串的流式输出。
// 它的功能类似于 std::cout,但输出内容会被写入到一个字符串中。
std::ostringstream escaped;
// escaped.fill('0'); 设置流输出时填充字符为 '0',主要用于后续的十六进制输出,确保不足两位时用 0 填充。
escaped.fill('0');
// escaped << std::hex; 设置流的输出基数为十六进制,这样后续的整数输出将以十六进制格式进行。
escaped << std::hex;
for (std::string::const_iterator i = input.begin(); i != input.end(); ++i)
{
char c = *i;
// 保留未保留字符:字母和数字
if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~')
{
escaped << c;
}
else
{
// 将特殊字符转换为 %XX 形式
escaped << std::uppercase;
escaped << '%' << std::setw(2) << (int)(unsigned char)c;
escaped << std::nouppercase;
// 如果字符 c 不是字母、数字或者那些不需要编码的字符,那么它需要被编码。
// std::uppercase 用于确保接下来的十六进制字母输出为大写形式。
// escaped << '%' 输出一个百分号 %,这是 URL 编码中表示特殊字符的格式。
// (int)(unsigned char)c 将字符 c 转换为其对应的 ASCII 整数值(因为 char 可能是有符号的,所以先转换为 unsigned char 确保得到正确的无符号值)。
// std::setw(2) 设置输出宽度为 2,这样十六进制数不足两位时会用之前设置的填充字符 '0' 来补齐。
// escaped << std::nouppercase; 恢复十六进制字母输出为小写(默认情况下输出小写字母)。
}
}
return escaped.str(); // escaped.str() 从 std::ostringstream 对象中提取最终生成的字符串,并将其返回
}
// 将百分号编码的字符串转换回普通字符串
std::string URLdecode(const std::string &input)
{
std::string decoded;
std::string::size_type i = 0;
while (i < input.size())
{
if (input[i] == '%')
{
// 将 %XX 转换为对应的字符
if (i + 2 < input.size())
{
std::string hex = input.substr(i + 1, 2);
char c = static_cast<char>(std::stoi(hex, nullptr, 16));
decoded += c;
i += 3; // 跳过 %XX
}
else
{
// 无效的百分号编码
decoded += input[i++];
}
}
else if (input[i] == '+')
{
// 在某些情况下,'+' 被解释为空格
decoded += ' ';
++i;
}
else
{
decoded += input[i++];
}
}
return decoded;
}
class HTTP_Request
{
private:
std::string request_line; // 请求行
std::vector<std::string> request_headers; // 请求报头
std::string black_line; // 空行
std::string content; // 请求正文
std::string visit_path; // 访问文件路径
// 细分
std::string request_method; // 请求方法
std::string request_url; // url
std::string http_version; // HTTP版本
std::unordered_map<std::string, std::string> header_key_val; // 属性和内容的键值对
std::string file_suffix_kind; // 文件后缀类型
std::string parameter; // url中的参数
public:
HTTP_Request()
: visit_path(root_path)
{
}
// 获取一行请求信息
std::string GetLine(std::string &str)
{
size_t end_pos = str.find(header_end_sign);
// 找不到分隔符说明该请求报文除了正文部分外已经全被提取完毕,返回空
if (end_pos == std::string::npos)
{
return std::string();
}
// 查找过程中,可能遇见单独的换行符"\r\n",此时end_pos == 0, ret为空
std::string ret = str.substr(0, end_pos);
str.erase(0, ret.size() + header_end_sign.size());
return ret.empty() ? header_end_sign : ret;
}
// 提取正文
std::string GetContent(std::string &str)
{
std::string content = str.substr();
str.erase();
return content;
}
void DeSerialize(std::string &request_str)
{
// 1、提取请求行
request_line = GetLine(request_str);
// 2、提取请求报头
std::string header_line;
do
{
header_line = GetLine(request_str);
// 提取到换行符或者字节流中的请求行和报头都已经提取完毕
if (header_line == header_end_sign || header_line.empty())
{
break;
}
request_headers.emplace_back(header_line);
} while (true);
// 3、提取空行
black_line = header_end_sign;
// 4、提取正文
content = GetContent(request_str);
//----------------继续细分---------------------
// 请求行中包含: 方法、url、http版本
// 请求报头中包含: 属性、内容
ParseRequestLine();
ParseRequestHeaders();
}
// 继续细分提取请求行
void ParseRequestLine()
{
// 创建 std::stringstream 对象:
// 通过将 _req_line 作为参数传递给 std::stringstream 的构造函数,创建了一个字符串流对象 ss。
// 这意味着 ss 将会处理 _req_line 字符串,就像它是一个输入流那么进行处理。
std::stringstream ss(request_line);
// 自然的,在标准输入中,空格作为输入结束的标志
ss >> request_method >> request_url >> http_version;
// 对于url,字符'?'之前是文件路径,之后是查询参数。所以我们要将文件路径和参数提取出来
// 情况:如果只有根目录,那么我们将访问文件路径设置为"首页"
// 如果是其他文件路径,在web根目录之后添加文件路径即可
auto pos_val = request_url.find("?"); // 查看url中是否有参数
std::string want_path;
if(pos_val!= std::string::npos){
parameter = request_url.substr(pos_val + 1);
want_path = request_url.substr(0, pos_val);
}
else{
want_path = request_url; // 没有参数的话直接在web根目录后添加访问路径即可
}
visit_path += want_path;
if (visit_path[visit_path.size() - 1] == '/') // 如果是根目录,就将路径设置为首页路径
{
visit_path = home_page_path;
}
// 得到访问文件的后缀名
auto pos_suffix = want_path.rfind(".");
if (pos_suffix != std::string::npos)
{
file_suffix_kind = want_path.substr(pos_suffix);
}
else
{
file_suffix_kind = ".default";
}
}
// 继续细分提取请求报头
void ParseRequestHeaders()
{
for (auto &header : request_headers)
{
size_t pos = header.find(key_value_sep_sign);
if (pos == std::string::npos)
{
continue;
}
std::string key = header.substr(0, pos);
std::string val = header.substr(pos + key_value_sep_sign.size());
if (key.empty() || val.empty())
{
continue;
}
header_key_val.insert(std::make_pair(key, val));
}
}
std::string GetFilePath()
{
return visit_path;
}
std::string GetFileSuffix()
{
return file_suffix_kind;
}
std::string GetRequestMethod()
{
return request_method;
}
std::string GetParameter()
{
return parameter;
}
};
class HTTP_Response
{
private:
std::string response_line; // 响应行
std::vector<std::string> response_headers; // 响应报头组
std::string black_line; // 空行
std::string content; // 响应正文
// 细分
std::string status_code; // 状态码
std::string status_code_desc; // 状态码描述
std::string http_version; // HTTP版本
std::unordered_map<std::string, std::string> header_key_val; // 属性和内容的键值对
public:
HTTP_Response()
:http_version("HTTP/1.1")
{}
std::string SetReponseLine()
{
return http_version + space_block + status_code + space_block + status_code_desc + header_end_sign;
}
// 设置状态码
void SetCode(const std::string &code)
{
status_code = code;
}
// 设置状态码描述
void SetCodeDesc(const std::string &desc)
{
status_code_desc = desc;
}
// 设置正文内容
void SetContent(const std::string &file_content)
{
content = file_content;
}
// 设置请求报头的key和value
void SetHeader(const std::string &key, const std::string &val)
{
header_key_val.insert(std::make_pair(key, val));
}
// 构建响应报头
void SetResponseHeaders()
{
std::string header;
for (auto &kv : header_key_val)
{
header = kv.first + ": " + kv.second + "\r\n";
response_headers.emplace_back(header);
}
}
// 序列化
void Serialize(std::string *out)
{
response_line = SetReponseLine();
*out += response_line;
SetResponseHeaders();
for (auto &header : response_headers)
{
*out += header; // 添加报文
}
*out += header_end_sign; // 添加换行符
*out += content; // 添加响应正文
}
};
// 处理请求得到响应的函数类型
using HandleRequestToReponse_t = std::function<HTTP_Response(HTTP_Request&)>;
class HTTP_Server
{
private:
HandleRequestToReponse_t Default_HandleRequestToReponse; // 默认服务
std::unordered_map<std::string, HandleRequestToReponse_t> HandleRequestToReponse_List; // 处理请求得到响应的功能函数
public:
HTTP_Server(HandleRequestToReponse_t func)
:Default_HandleRequestToReponse(func)
{}
void AddHandleTaskKeyVal(std::string key, HandleRequestToReponse_t val)
{
key = "wwwroot" + key;
HandleRequestToReponse_List[key] = val;
}
// 判断服务列表中是否有对应的服务
bool IsServiceExistInList(std::string key)
{
for(auto& kv : HandleRequestToReponse_List)
{
if(kv.first == key){
return true;
}
}
return false;
}
void NetIOService(const int io_sockfd, const struct sockaddr_in &from_client)
{
while (true)
{
// 1、服务端接收来自客户端的HTTP请求
std::string req_stream;
RecvInfo(io_sockfd, &req_stream);
// 2、判断字节流中是否有至少一条的完整HTTP请求,有则提取,无则继续接收信息
std::string http_message = GetOneHttpMessageFromStream(req_stream);
// 如果接收到的字符串为空,则继续去recv接收信息
if (http_message.empty()){
continue;
}
else{
std::cout << http_message << std::endl;
}
// 对http请求进行反序列化
HTTP_Request request;
request.DeSerialize(http_message);
HTTP_Response response;
// 处理响应,返回应答
std::string url_path = request.GetFilePath();
// 如果服务已经注册过,直接执行对应的服务函数
if(IsServiceExistInList(url_path)){
response = HandleRequestToReponse_List[url_path](request);
}
else{
// 否则执行默认服务函数
response = Default_HandleRequestToReponse(request);
}
std::string response_str;
// 应答序列化
response.Serialize(&response_str);
std::cout << response_str << std::endl;
// 返回应答
SendInfo(io_sockfd, response_str);
close(io_sockfd);
}
}
// 接收信息
void RecvInfo(int io_sockfd, std::string *out)
{
char buffer[BUFFER_SIZE];
ssize_t r_num = recv(io_sockfd, buffer, BUFFER_SIZE - 1, 0);
if (r_num > 0)
{
buffer[r_num] = '\0';
*out += buffer;
}
}
// 发送信息
void SendInfo(int io_sockfd, const std::string &in)
{
ssize_t w_num = send(io_sockfd, in.c_str(), in.size(), 0);
if (w_num < 0)
{
std::cerr << "Server SendInfo false!" << std::endl;
exit(errno);
}
}
// 检查每个请求行中是否包含Content_Length属性
// 如果包含,则返回正文长度
// 不包含则返回0,代表无正文
int CheckContentLengthExistsInLine(const std::string &line)
{
size_t sep_pos = line.find(key_value_sep_sign);
if (strncmp(line.c_str(), content_attribute.c_str(), content_attribute.size()) == 0)
{
size_t end_pos = line.find(header_end_sign);
return std::stoi(line.substr(sep_pos + key_value_sep_sign.size(), end_pos));
}
return 0;
}
// 返回值:正文长度 (参数:1、报头的结尾位置 2、字节流)
size_t CheckRequestEveryLine(size_t end_pos, const std::string &req_stream)
{
size_t line_satrt_pos = 0;
size_t line_end_pos = 0;
size_t content_len = 0;
std::string line_string;
while (line_end_pos != end_pos) // 如果一直检查到了报头属性的结尾位置还没找到,则说明请求中无正文
{
// 找到请求报头行的结尾位置
line_end_pos = req_stream.find(header_end_sign, line_satrt_pos);
// 提取一个请求报头行
line_string = req_stream.substr(line_satrt_pos, line_end_pos);
if ((content_len = CheckContentLengthExistsInLine(line_string)) > 0)
{
return content_len; // 存在正文长度属性则直接返回正文长度
}
// 未找到Content_Length属性,将查找的起始位置移动至下一个请求报头行的起始位置
line_satrt_pos = line_end_pos + header_end_sign.size();
}
return 0;
}
std::string GetOneHttpMessageFromStream(std::string &req_stream)
{
if(req_stream.empty()) return std::string();
// 1、首先找字节流中是否有两个连续的分割换行符------即"\r\n\r\n"
size_t header_end_pos = 0;
if ((header_end_pos = req_stream.find(header_final_and_message_begin)) == std::string::npos)
{
// 没有代表当前请求不完整,直接返回空串
return std::string();
}
// 当前请求有完整报头,接下来判断是否有报文。须在包头属性中查找是否有Content-Length报头属性
// 按行查找报头属性,并判断该次请求中是否有请求正文。如果有,得到正文长度,下一步去判断当前字节流中是否包含该完整正文
size_t content_len = CheckRequestEveryLine(header_end_pos, req_stream);
// 计算出HTTP请求字符串的理论长度
int total_len = header_end_pos + header_final_and_message_begin.size() + content_len;
// 如果该次请求中包含请求正文
if (content_len > 0)
{
// 判断当前字节流的长度是否等于该次请求的理论长度
if (total_len < req_stream.size())
{
// 如果当前字节流的长度小于请求的理论长度,直接返回传输层继续接收信息
return std::string();
}
}
// 执行流走到这儿,表明字节流中至少包含了一条完整的HTTP请求
// 提取出完整的HTTP请求,并从字节流中分割出来
std::string ret = req_stream.substr(0, total_len);
req_stream.erase(0, total_len);
return ret; // 返回提取出的HTTP请求字符串
}
};
TCP_Server.hpp:
#pragma once
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <string>
#include <iostream>
#include <arpa/inet.h>
#include <cstring>
#include <signal.h>
#include <sys/wait.h>
#include <functional>
#include <pthread.h>
#include <memory>
extern const int BUFFER_SIZE;
static const int MAX_LINK_NUM = 6;
using io_service_t = std::function<void(const int io_sockfd, const struct sockaddr_in& from_client)>;
class TcpServer
{
private:
int _listen_sockfd; // 监听套接字
uint16_t _port; // 端口号
io_service_t _handle_method; // 通信服务函数
bool is_running;
public:
// 应用层提供端口号和业务函数,构造服务端
TcpServer(uint16_t port, io_service_t service_method)
:_listen_sockfd(-1), _port(port), _handle_method(service_method), is_running(false)
{
// 初始化服务器操作
// 1、创建监听套接字
CreateListenSockfd();
// 2、绑定IP + PORT
BindLocalIPAndPort();
// 3、将套接字设置为监听状态
SetListenSockfd();
}
~TcpServer()
{}
// 1、创建监听套接字
void CreateListenSockfd()
{
if((_listen_sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0){
std::cerr << "CreateListenSockfd false!" << std::endl;
exit(errno);
}
}
// 2、绑定IP + PORT
void BindLocalIPAndPort()
{
struct sockaddr_in local_addr;
memset(&local_addr, 0, sizeof(local_addr));
local_addr.sin_addr.s_addr = INADDR_ANY;
local_addr.sin_port = htons(_port);
local_addr.sin_family = AF_INET;
if(bind(_listen_sockfd, (struct sockaddr*)&local_addr, sizeof(local_addr)) < 0){
std::cerr << "BindLocalIPAndPort false!" << std::endl;
exit(errno);
}
}
// 3、将套接字设置为监听状态
void SetListenSockfd()
{
if(listen(_listen_sockfd, MAX_LINK_NUM) < 0){
std::cerr << "SetListenSockfd false!" << std::endl;
exit(errno);
}
}
// 4、接收来自客户端的连接请求,获取io套接字和客户端网络结构体
void AcceptLinkAndGetIOSockfdWithClientAddr(int* iosockfd, struct sockaddr_in* fromclient)
{
struct sockaddr_in from_client;
socklen_t len = sizeof(from_client);
memset(&from_client, 0, len);
int io_sockfd = accept(_listen_sockfd, (struct sockaddr*)&from_client, &len);
if(io_sockfd < 0){
std::cerr << "AcceptLinkAndGetIOSockfdWithClientAddr false!" << std::endl;
exit(errno);
}
*iosockfd = io_sockfd;
*fromclient = from_client;
}
// 进行业务处理,线程执行函数
static void* ThreadServiceRoute(void* ThreadSelf)
{
pthread_detach(pthread_self()); // 线程分离
ThreadNeedData* thread_self = static_cast<ThreadNeedData*>(ThreadSelf);
thread_self->_self->_handle_method(thread_self->_io_sockfd, thread_self->_client_netaddr); // 进行业务处理
close(thread_self->_io_sockfd); // 业务处理完毕后,关闭无用文件描述符
delete thread_self; // 释放线程数据资源
return nullptr;
}
// 启动服务器
void Loop()
{
std::cout << "Tcp Server Start..." << std::endl;
is_running = true;
int io_sockfd = 0;
struct sockaddr_in client;
memset(&client, 0, sizeof(client));
while(true)
{
// 1、接收客户端连接请求
AcceptLinkAndGetIOSockfdWithClientAddr(&io_sockfd, &client);
std::cout << "Tcp Server Accept Success..." << std::endl;
// 2、创建多线程处理业务
ThreadNeedData* thread_data = new ThreadNeedData(this, io_sockfd, client);
pthread_t tid = 0;
if(pthread_create(&tid, nullptr, ThreadServiceRoute, thread_data) < 0)
{
std::cerr << "pthread_create false!" << std::endl;
exit(errno);
}
// 主线程继续接受请求,创建线程以处理业务
}
is_running = false;
}
// 内部类,线程所需要的数据,作为指针参数传递给pthread_create函数构造线程
class ThreadNeedData
{
public:
int _io_sockfd;
struct sockaddr_in _client_netaddr;
TcpServer* _self; // 目的:使用TcpServer中的方法
public:
ThreadNeedData(TcpServer* self, const int io_sockfd, const struct sockaddr_in& client_netaddr)
:_self(self), _io_sockfd(io_sockfd), _client_netaddr(client_netaddr)
{}
};
};
Server_Main.hpp:
#include "http_protocol.hpp"
#include "Tcp_Server.hpp"
// 获取指定路径下的文件内容
std::string GetFileContent(const std::string &path)
{
// 打开文件,以二进制模式读取
std::ifstream in(path, std::ios::binary);
// 检查文件是否成功打开
if (!in.is_open())
return std::string(); // 如果文件未打开,返回空字符串
// 将文件指针移动到文件末尾
in.seekg(0, in.end);
// 获取文件大小(即文件末尾的偏移量)
int filesize = in.tellg(); // 返回当前读取指针的位置(文件大小)
// 将文件指针移动回文件开头
in.seekg(0, in.beg);
// 创建一个字符串,并调整其大小以容纳文件内容
std::string content;
content.resize(filesize); // 调整字符串大小为文件大小
// 从文件中读取内容到字符串中
in.read((char *)content.c_str(), filesize);
// 关闭文件
in.close();
// 返回读取到的文件内容
return content;
}
// 将接收到的请求处理,并生成响应的应答返回
HTTP_Response HandleRequestToReponse(HTTP_Request &req)
{
// 1、获取HTTP请求中想要访问的文件路径
std::string want_file_path = req.GetFilePath();
// 判断是否是提前约定好的服务
// 2、获取该路径文件的文件内容
std::string content = GetFileContent(want_file_path);
HTTP_Response resp;
if (content.empty()) // 如果文件内容为空,说明想要访问的文件不存在
{
// 构建404 Not Found 应答
resp.SetCode("404");
resp.SetCodeDesc("Not Found!");
content = GetFileContent("wwwroot/404NotFound.html");
}
else
{
resp.SetCode("200");
resp.SetCodeDesc("OK!");
}
// 构建应答报头
resp.SetHeader("Content_Length: ", std::to_string(content.size()));
// 构建响应正文
resp.SetContent(content);
return resp;
}
HTTP_Response HandleTaskSearch(HTTP_Request &req)
{
HTTP_Response resp;
// strcasecmp函数判断字符串是否相等忽略大小写
if(strcasecmp(req.GetRequestMethod().c_str(), "GET") == 0) // 如果是get方法
{
// 获取参数
std::string para = req.GetParameter();
// 对参数做处理。。。。
std::cout << para << std::endl;
// 设置应答。。。
resp.SetCode("200");
resp.SetCodeDesc("OK!");
resp.SetContent("<html><h1>Search Success!</h1></html>");
}
else{
// .......
}
return resp;
}
int main(int argc, char *argv[])
{
if(argc < 2){
std::cerr << "未输入端口号..." <<std::endl;
return -1;
}
// 构建服务
HTTP_Server http_server(HandleRequestToReponse);
// 添加服务和服务函数
http_server.AddHandleTaskKeyVal("/s", HandleTaskSearch);
// 构建服务端
TcpServer tcp_server(std::stoi(argv[1]), std::bind(&HTTP_Server::NetIOService, &http_server, std::placeholders::_1, std::placeholders::_2));
// 启动服务端
tcp_server.Loop();
return 0;
}