一、初步了解和实现。

        在五月初的时候接触到了一个编程网站,比较入门级,但是通过每个阶段的小目标设计来实现项目的最终功能,也能了解到一些相关的知识,所以试着做了一下里面HTTP服务器的c++建设,虽然之前也有做,但是其实那个也有点久之前了,这一次想着通过简单复习,顺便把一些和Linux高性能服务器的相关知识也一并用上。

Catalog | CodeCrafters(每个月都有免费的简单项目练手,而且可以简单发布在你的github上)

GitHub – asexe/http-: codecrafters这是这个项目我在github上的实现

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
std::string captureAfterKey(const std::string& input) {
std::size_t echoPos = input.find("/echo/");
if (echoPos == std::string::npos) {
// 如果没有找到 /echo/,返回空字符串
return "";
}
// 从 /echo/ 后面开始查找空格
std::size_t spacePos = input.find(' ', echoPos + 6); // /echo/ 长度为6
if (spacePos == std::string::npos) {
// 如果没有找到空格,取从 /echo/ 后面到字符串末尾的部分
return input.substr(echoPos + 6);
} else {
// 如果找到了空格,取空格前的部分
return input.substr(echoPos + 6, spacePos - echoPos - 6);
}
}
std::string extractUserAgent(const std::string& request) {
std::size_t userAgentPos = request.find("User-Agent: ");
if (userAgentPos == std::string::npos) {
// 如果没有找到 User-Agent 头,返回空字符串
return "";
}
// 找到 User-Agent 头,现在找到该行的结束位置
std::size_t endOfLinePos = request.find("\r
", userAgentPos);
if (endOfLinePos == std::string::npos) {
// 如果没有找到行结束,返回空字符串
return "";
}
// 提取 User-Agent 头的值
return request.substr(userAgentPos + sizeof("User-Agent: ") - 1, endOfLinePos - userAgentPos - sizeof("User-Agent: ") + 1);
}
// 新增函数,用于读取文件内容并返回
std::string readFileContent(const std::string& filePath) {
std::ifstream fileStream(filePath, std::ios::binary | std::ios::ate);
if (fileStream) {
std::streamsize size = fileStream.tellg();
fileStream.seekg(0, std::ios::beg);
std::string content;
content.resize(size);
if (size > 0) {
fileStream.read(&content[0], size);
}
return content;
} else {
return "";
}
}
// 新增函数,用于读取文件内容并返回
std::string readFileContent(const std::string& directory, const std::string& filename) {
std::ifstream fileStream((directory + "/" + filename).c_str(), std::ios::binary | std::ios::ate);
if (fileStream) {
std::streamsize size = fileStream.tellg();
fileStream.seekg(0, std::ios::beg);
std::string content((std::istreambuf_iterator(fileStream)), std::istreambuf_iterator());
return content;
} else {
return "";
}
}
std::string handlePostRequest(const std::string& request, const std::string& directory) {
std::string response;
std::string filename;
std::string fileContent;
// 查找 POST 请求正文的开始
size_t postHeaderEnd = request.find("\r
\r
") + 4;
if (postHeaderEnd != std::string::npos) {
// 获取 POST 请求正文内容
fileContent = request.substr(postHeaderEnd);
// 提取文件名,假设它紧跟在 "POST /files/" 之后
size_t filenameStart = request.find("POST /files/") + 11;
size_t filenameEnd = request.find(" ", filenameStart); // 假设文件名之后有一个空格
if (filenameEnd != std::string::npos) {
filename = request.substr(filenameStart, filenameEnd - filenameStart);
// 构造完整的文件路径
std::string filePath = directory + "/" + filename;
// 保存文件
std::ofstream outFile(filePath, std::ios::binary);
if (outFile) {
outFile << fileContent;
response = "HTTP/1.1 201 Created\r
Content-Type: text/plain\r
Content-Length: 0\r
\r
";
} else {
response = "HTTP/1.1 500 Internal Server Error\r
Content-Type: text/plain\r
Content-Length: 0\r
\r
";
}
} else {
response = "HTTP/1.1 400 Bad Request: Invalid filename\r
Content-Type: text/plain\r
Content-Length: 0\r
\r
";
}
} else {
response = "HTTP/1.1 400 Bad Request: Invalid POST request format\r
Content-Type: text/plain\r
Content-Length: 0\r
\r
";
}
return response;
}
// 新建函数 processRequest 来处理请求
std::string processRequest(const std::string& request, const std::string& directory, const std::vector& keyword) {
std::string report;
size_t start_pos = request.find(" ");
size_t end_pos = request.find(" ", start_pos + 1);
if (start_pos != std::string::npos && end_pos != std::string::npos) {
std::string method = request.substr(0, start_pos);
std::string path = request.substr(start_pos + 1, end_pos - start_pos - 1);
std::cout << "Received path: " << path << std::endl;
// 提取 User-Agent 头的值
std::string userAgent = extractUserAgent(request);
if (path == "/") {
report = "HTTP/1.1 200 OK\r
Content-Type: text/plain\r
Content-Length: 13\r
\r
Hello, World!";
}
// 处理 /user-agent 请求
else if (path == "/user-agent") {
report = "HTTP/1.1 200 OK\r
Content-Type: text/plain\r
Content-Length: " 
+ std::to_string(userAgent.length()) + "\r
\r
" + userAgent;
}
// 处理 /echo/ 请求
else if (path.find("/echo/") == 0) {
std::string responseContent = captureAfterKey(request);
report = "HTTP/1.1 200 OK\r
Content-Type: text/plain\r
Content-Length: " 
+ std::to_string(responseContent.length()) + "\r
\r
" + responseContent;
}else if (method == "POST") {
// 确保路径以 "/files/" 开始
if (path.find("/files/") == 0) {
report = handlePostRequest(request, directory);
} else {
report = "HTTP/1.1 404 Not Found\r
Content-Type: text/plain\r
Content-Length: 0\r
\r
";
}
}
// 处理其他请求,需要directory参数
else {
if (directory.empty()) {
report = "HTTP/1.1 404 Not Found\r
Content-Type: text/plain\r
Content-Length: 0\r
\r
";
return report;
}
if (path.find("/files/") == 0) {
// 提取文件名
std::string filename = path.substr(7); // 去掉 "/files/" 前缀
std::string responseContent = readFileContent(directory, filename);
// 如果文件内容不为空,设置正确的响应类型
if (!responseContent.empty()) {
report = "HTTP/1.1 200 OK\r
Content-Type: application/octet-stream\r
Content-Length: " 
+ std::to_string(responseContent.length()) + "\r
\r
" + responseContent;
} else {
report = "HTTP/1.1 404 Not Found\r
\r
";
}
} else {
report = "HTTP/1.1 404 Not Found\r
\r
";
}
}
} else {
report = "HTTP/1.1 400 Bad Request\r
\r
";
}
return report;
}
void handle_client(int client_fd, struct sockaddr_in client_addr, const std::string& directory) {
char buffer[1024];
std::string report;
std::vector keyword = {"/files/", "/echo/", "/index.html", "/user-agent"};
int bytes_received = recv(client_fd, buffer, sizeof(buffer), 0);
if (bytes_received < 0) {
std::cerr << "Error receiving data from client
";
close(client_fd);
return;
}
std::string request(buffer, bytes_received);
report = processRequest(request, directory, keyword);
// Send the response to the client
send(client_fd, report.c_str(), report.length(), 0);
close(client_fd);
}
int main(int argc, char **argv) {
// You can use print statements as follows for debugging, they'll be visible when running tests.
std::cout << "Logs from your program will appear here!
";
std::string directory;
for (int i = 1; i < argc; ++i) {
if (std::string(argv[i]) == "--directory" && i + 1 < argc) {
directory = argv[++i];
break;
}
}
if (directory.empty()) {
std::cerr << "Error: No directory specified with --directory flag.
";
}
// Uncomment this block to pass the first stage
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd < 0) {
std::cerr << "Failed to create server socket
";
return 1;
}
//
// // Since the tester restarts your program quite often, setting REUSE_PORT
// // ensures that we don't run into 'Address already in use' errors
int reuse = 1;
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEPORT, &reuse, sizeof(reuse)) < 0) {
std::cerr << "setsockopt failed
";
return 1;
}
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(4221);
//
if (bind(server_fd, (struct sockaddr *) &server_addr, sizeof(server_addr)) != 0) {
std::cerr << "Failed to bind to port 4221
";
return 1;
}
//
int connection_backlog = 5;
if (listen(server_fd, connection_backlog) != 0) {
std::cerr << "listen failed
";
return 1;
}
//
struct sockaddr_in client_addr;
int client_addr_len = sizeof(client_addr);
std::cout << "Waiting for a client to connect...
";
while (true) {
struct sockaddr_in client_addr;
int client_addr_len = sizeof(client_addr);
int client_fd = accept(server_fd, (struct sockaddr *) &client_addr, (socklen_t *) &client_addr_len);
if (client_fd < 0) {
std::cerr << "Error accepting connection
";
continue; // Skip to the next iteration if accept fails
}
// Create a new thread to handle the client
std::thread client_thread(handle_client, client_fd, client_addr, directory);
client_thread.detach(); // Detach the thread to let it run independently
}
// Close the server socket when done (not reached in this example)
close(server_fd);
return 0;
}

        由于这个网站的HTTP项目是有任务要求的,所以这是重新在它的部分源码上实现的功能,可能有一些小bug,同时通过这个网站我也有了一些其他思路和了解(笔者之前的web服务器是简单的功能实现,因为没有跟视频或者书籍所做的,也是不明白一个较简单完整的HTTP服务应该实现哪些内容。)上面这段代码一个比较突出的点是使用了directory这个变量,它是可以在编译时锚定指定文件夹,来实现对外部文件夹设置为上传存储的文件夹。

 二、关于HTTP协议

        在实现前端与后端通信的过程中,不可避免的就是HTTP协议,而现在的HTTP协议功能也更加强大和完善,而我的代码实现更多的是http1.1的协议,因此我的这些项目实现更多的是对自己掌握知识的实现,并不能说是一个很好的项目,而涉及HTTP协议就不能离开CRLF – MDN Web 文档术语表:Web 相关术语的定义 | MDN (mozilla.org)(通过filter查询相关内容,比如http标头,确实挺详细的)确实不好看,但是手册这东西就是得备着以防万一。也可以看csdn的相关博客,比如下方。

http协议【计算机网络】HTTP 协议详解_http协议解析-CSDN博客

http报文HTTP 报文详解_http报文-CSDN博客

        在前后端的交互中,后端会在握手成功后等待前端送来的HTTP报文,并且通过报文内容发送相应的回复报文给后端的服务器中,这就需要把传递过来的http报文进行处理,比如说删除前面多余字符,只留关键语句,这里使用命令台

curl -i -X GET http://localhost:4221/index.html

,发来的报文就可以删去http://localhost:4221/这些字符串,匹配后面的字符串,对后面的字符串进行相应的处理。其实就是我向正在监听4421端口的服务器,发送了一个请求,而我只需要知道内容就是我需要index.html这个文件,所以可以通过服务器提取内容,并对内容进行相应处理,通过前端的浏览器打开开发者功能,也能看到前端所接收的服务器报文。发回的报文也不能是随意的,是要有格式的,但是我们可以直接简单点,比如成功响应回复”HTTP/1.1 200 OK\r
Content-Type: text/plain\r
Content-Length: 0\r
\r
“;你需要说明回复类型和长度(比如所html文件和该文件大小),但是也可以”HTTP/1.1 200 ok\r
\r
“;后面直接接上发送文件的函数,实现发送给前端html文件(下方截图是该网站提供本地测试集,而我们也可以通过调用Crul工具,对我们所构建的服务器进行测试)

curl 命令_curl命令-CSDN博客

在Linux上实现HTTP服务器,进行前端对后端的文件上传和下载(实现线程池,epoll等)插图

 这是使用简单Curl进行测试。(可以看到的加上长度和类别给前端响应,前段会收到相关的信息)

在Linux上实现HTTP服务器,进行前端对后端的文件上传和下载(实现线程池,epoll等)插图(1)

 在Linux上实现HTTP服务器,进行前端对后端的文件上传和下载(实现线程池,epoll等)插图(2)

前端接收html的http报文(服务器的200回复是可以修改的,就是你可以回复是200 ooook)

三、在源代码上的升级迭代

        看到这,想必你也明白大概思路,就是在大家约定俗称的条例下,服务器和前端发送相互信息,而服务器要对http报文掐头,只看内容,然后进行相应反馈发给前端,前端将内容展现给用户,而socket这些比较细节的部分,我想通过上述简单代码,也能有个大概了解(可以自己加上部分输出),自己尝试修改和破坏代码,我相信很快就能理解。

        而以下,是我对这个简单代码的部分扩展,首先,由于没有完全按照书本或者视频的内容,里面的许多内容都是想到哪,做到哪,这也导致我在代码实现的过程中十分头痛,就比如对http报文响应的函数设计中,一开始是打算设计就是对关键字进行分类判断最后进行统一回复的,而到后来设计发送文件相关的时就需要重新设计。而且部分功能也有重叠,也就是修修改改,自己项目做出来后就感觉自己其实是在屎山代码是添屎,尽量大家在写项目时跟着书本或者视频,也不要想到哪写道哪,不然有的是你痛苦的。

asexe/http (github.com)这是代码,在后续的实现中,我所添加的主要为线程池和epoll复用,对此我们需要加上互斥量以实现三部分:管理线程(负责线程数的扩容与缩减)、任务队列(负责向任务队列填装任务)、工作线程(负责从任务队列取任务并处理)。 其中任务队列、工作线程采用生产者-消费者模型。采用互斥量+条件变量实现线程同步。而文件实现部分(下载文件),我们需要加上互斥锁,避免同时对文件的访问。

        而在前端中,我们所设计的网页,也可以通过设计实现发送文件和转移到其他网页。

        服务器实现部分虽然加上了部分注释,但可能还是比较难以理解。


File Management
/* CSS样式定义 */

File Management System

File List

这里的前端发送和接收文件都使用了json。JSON 基本使用_json怎么用-CSDN博客

注意,下图的html文件虽然绑定了JavaScript和CSS文件,但是前端的处理方法是在html读取的这两个链接后,在发送请求JavaScript和CSS文件的报文,所以在服务器后端中的代码中,也必须实现相应的功能。


Document

404

Opps! Page not found

ㄟ(▔^▔ㄟ) (╯▔^▔)╯ ㄟ(▔皿▔ㄟ) (╯▔皿▔)╯

Go to Index Page //这里的a href="index",点击后就是发送了http://localhost:4221/index给服务器端

以下是服务器部分代码实现(后面都是又臭又长的代码部分,希望大家自己链接下载吧,别看了,但是hpp的部分注释,源代码没有,也比较少,可以看看)。

#ifndef EPOLL_SERVER_HPP
#define EPOLL_SERVER_HPP
#include "ThreadPool.hpp" // 包含线程池的头文件
#include  // 包含使用Epoll所需的头文件
#include  // 包含使用std::string所需的头文件
class EpollServer {
private:
int server_fd; // 服务器的文件描述符
int epoll_fd; // Epoll的文件描述符
std::string directory; // 服务器文件目录
ThreadPool pool; // 线程池对象
void eventLoop(); // 内部事件循环函数
public:
EpollServer(int server_fd, const std::string& directory, size_t thread_count); // 构造函数
~EpollServer(); // 析构函数
void start(); // 启动服务器函数
};
#endif // EPOLL_SERVER_HPP

 这下面的hpp设计的类和成员,就是实现管理线程(负责线程数的扩容与缩减)、任务队列(负责向任务队列填装任务)、工作线程(负责从任务队列取任务并处理)。 其中任务队列、工作线程采用生产者-消费者模型。采用互斥量+条件变量实现线程同步。

#ifndef THREAD_POOL_HPP
#define THREAD_POOL_HPP
#include 
#include 
#include 
#include 
#include 
#include 
#include 
class ThreadPool {
public:
ThreadPool(size_t); // 构造函数,接受线程数量作为参数
template
auto enqueue(F&& f, Args&&... args) 
-> std::future<typename std::result_of::type>; // 提交任务到线程池并返回结果的方法
~ThreadPool(); // 析构函数,销毁线程池
private:
std::vector workers; // 存储工作线程的容器
std::queue<std::function> tasks; // 存储待执行的任务的队列
std::mutex queue_mutex; // 保护任务队列的互斥锁
std::condition_variable condition; // 用于线程间的条件变量通信
bool stop_flag; // 停止标识
void work(); // 工作线程执行的函数
ThreadPool(const ThreadPool&) = delete; // 禁止拷贝构造
ThreadPool& operator=(const ThreadPool&) = delete; // 禁止赋值操作
};
ThreadPool::ThreadPool(size_t threads)
: stop_flag(false) {
for(size_t i = 0; i < threads; ++i) {
workers.emplace_back([this]() {
work();
});
}
}
ThreadPool::~ThreadPool() {
// 在析构函数中等待所有线程结束
for(std::thread &worker: workers) {
worker.join();
}
}
template
auto ThreadPool::enqueue(F&& f, Args&&... args) 
-> std::future<typename std::result_of::type> {
using return_type = typename std::result_of::type;
auto task = std::make_shared<std::packaged_task>(
std::bind(std::forward(f), std::forward(args)...)
);
std::future res = task->get_future();
{
std::unique_lock lock(queue_mutex);
tasks.emplace([task](){ (*task)(); });
}
condition.notify_one(); // 通知一个工作线程有新任务可执行
return res;
}
void ThreadPool::work() {
while(!stop_flag) {
std::function task;
{
std::unique_lock lock(queue_mutex);
condition.wait(lock, [this]{ return stop_flag || !tasks.empty(); });
if(stop_flag && tasks.empty())
return;
task = std::move(tasks.front());
tasks.pop();
}
task();
}
}
/*
首先定义一个 std::function 类型的 task 变量,用于存储待执行的任务。
然后通过一个 std::unique_lockstd::mutex 对任务队列进行加锁,以确保线程安全地访问任务队列。
接着调用 condition.wait() 来等待条件的满足,即等待任务队列非空或者 stop_flag 被设置为 true。
如果 stop_flag 被设置为 true 并且任务队列为空,则直接返回,结束工作函数的执行。
否则,将队首的任务取出并移动到 task 中,并从任务队列中移除该任务。
最后执行取出的任务。
*/
#endif // THREAD_POOL_HPP
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "ThreadPool.hpp"
#include "EpollServer.hpp"
#include 
#include 
#include 
#include 
#include 
#include 
// #include "File.hpp"
const int MAX_EVENTS = 10;
int server_fd;
// 获取硬件支持的线程数,如果没有,就使用默认值4
size_t thread_count = std::thread::hardware_concurrency() ? std::thread::hardware_concurrency() : 4;
std::string captureAfterKey(const std::string &input)
{
std::size_t echoPos = input.find("/echo/");
if (echoPos == std::string::npos)
{
// 如果没有找到 /echo/,返回空字符串
return "";
}
// 从 /echo/ 后面开始查找空格
std::size_t spacePos = input.find(' ', echoPos + 6); // /echo/ 长度为6
if (spacePos == std::string::npos)
{
// 如果没有找到空格,取从 /echo/ 后面到字符串末尾的部分
return input.substr(echoPos + 6);
}
else
{
// 如果找到了空格,取空格前的部分
return input.substr(echoPos + 6, spacePos - echoPos - 6);
}
}
std::string extractUserAgent(const std::string &request)
{
std::size_t userAgentPos = request.find("User-Agent: ");
if (userAgentPos == std::string::npos)
{
// 如果没有找到 User-Agent 头,返回空字符串
return "";
}
// 找到 User-Agent 头,现在找到该行的结束位置
std::size_t endOfLinePos = request.find("\r
", userAgentPos);
if (endOfLinePos == std::string::npos)
{
// 如果没有找到行结束,返回空字符串
return "";
}
// 提取 User-Agent 头的值
return request.substr(userAgentPos + sizeof("User-Agent: ") - 1, endOfLinePos - userAgentPos - sizeof("User-Agent: ") + 1);
}
std::string extractFileName(const std::string &postContent)
{
// 查找 "filename=\"" 字符串
size_t filenamePos = postContent.find("filename=\"");
if (filenamePos != std::string::npos)
{
// 找到文件名开始的位置
size_t filenameStart = filenamePos + 10; // "filename=\"" 的长度为 10
// 找到文件名结束的位置
size_t filenameEnd = postContent.find("\"", filenameStart);
if (filenameEnd != std::string::npos)
{
// 提取文件名
std::string filename = postContent.substr(filenameStart, filenameEnd - filenameStart);
return filename;
}
}
return "";
}
//从 HTTP 请求中提取文件名和文件内容,并将文件保存到指定的目录中。
std::string handlePostRequest(const std::string &request, const std::string &directory, int client_fd)
{
std::string response;
std::string filename;
std::string fileContent;
// 查找 POST 请求正文的开始
size_t postHeaderEnd = request.find("\r
\r
") + 4;
if (postHeaderEnd != std::string::npos)
{
// 获取 POST 请求正文内容
fileContent = request.substr(postHeaderEnd);
// 提取文件名,通过检查 "Content-Disposition" 头部字段
size_t contentDispositionPos = request.find("Content-Disposition: form-data;");
if (contentDispositionPos != std::string::npos)
{
// 找到文件名开始的位置
size_t filenameStart = request.find("filename=\"", contentDispositionPos);
if (filenameStart != std::string::npos)
{
// 提取文件名
filename = extractFileName(request.substr(filenameStart));
// std::cout << "Filename: " << filename << std::endl;
}
}
if (!filename.empty())
{
// 构造完整的文件路径
std::string filePath;
if (directory.empty())
{
// 如果 directory 为空,则下载文件到 downloaded 文件夹
filePath = "downloaded/" + filename;
}
else
{
// 否则,上传文件到  文件夹
filePath = directory + "downloaded/" + filename;
}
/*if (std::filesystem::exists(filePath)) {
std::cout << "File exists: " << filePath << std::endl;
} else {
std::cout << "File does not exist: " << filePath << std::endl;
}*/
// 保存文件
std::ofstream outFile(filePath, std::ios::binary);
if (outFile)
{
outFile << fileContent;
outFile.close(); // 确保文件已关闭
// 返回成功响应
//response = "HTTP/1.1 200 OK\r
Content-Type: text/plain\r
Content-Length: 0\r
\r
";
//std::cout << "Send!!!
" << std::endl;
char report[520] = "HTTP/1.1 200 ok\r
\r
";
int s = send(client_fd, report, strlen(report), 0);
// 打开并发送HTML文件内容
int fd = open("index.html", O_RDONLY);
sendfile(client_fd, fd, NULL, 2500); // 使用零拷贝发送文件内容
close(fd);
}
else
{
response = "HTTP/1.1 500 Internal Server Error\r
Content-Type: text/plain\r
Content-Length: 0\r
\r
";
std::cerr << "Failed to save file" << std::endl;
}
}
else
{
response = "HTTP/1.1 400 Bad Request: Invalid filename\r
Content-Type: text/plain\r
Content-Length: 0\r
\r
";
std::cerr << "No filename found in request" << std::endl;
}
}
else
{
response = "HTTP/1.1 400 Bad Request: Invalid POST request format\r
Content-Type: text/plain\r
Content-Length: 0\r
\r
";
std::cerr << "Invalid POST request format" << std::endl;
}
return response;
}
std::string fileName(const std::string &filePath)
{
size_t found = filePath.find_last_of("/\");
if (found != std::string::npos)
{
return filePath.substr(found + 1);
}
return filePath;
}
// 新增函数,用于获取文件的详细信息
std::string getFileInfo(const std::string &filePath)
{
struct stat fileStat;
if (stat(filePath.c_str(), &fileStat) == 0)
{
time_t modTime = fileStat.st_mtime; // 获取文件修改时间
struct tm *timeInfo = localtime(&modTime);
char timeBuffer[100];
strftime(timeBuffer, sizeof(timeBuffer), "%Y-%m-%d %H:%M:%S", timeInfo);
std::string size = std::to_string(fileStat.st_size) + " bytes";
return "{\"name\": \"" + fileName(filePath) + "\", \"size\": \"" + size + "\", \"uploaded\": \"" + std::string(timeBuffer) + "\"}";
}
else
{
return "{}";
}
}
//读取文件列表
std::string listFiles(const std::string &directory)
{
std::vector files;
std::string fileListContent;
std::string targetDirectory = directory.empty() ? "downloaded" : directory;
DIR *dir = opendir(targetDirectory.c_str());
if (dir == NULL)
{
return "[]"; // 如果目录打开失败,返回空数组
}
struct dirent *ent;
while ((ent = readdir(dir)) != NULL)
{
std::string file = ent->d_name;
if (file != "." && file != "..")
{
files.push_back(file);
}
}
closedir(dir);
// 如果没有文件,直接返回空数组
if (files.empty())
{
return "[]";
}
// 开始构建 JSON 数组字符串
fileListContent = "[
";
for (size_t i = 0; i < files.size(); ++i)
{
std::string fileInfo = getFileInfo((targetDirectory + "/" + files[i]));
fileListContent += "  " + fileInfo;
if (i < files.size() - 1)
{
fileListContent += ",
"; // 除了最后一个元素外,每个元素后都添加逗号和换行符
}
}
fileListContent += "
]"; // 添加结束括号
return fileListContent;
}
//404访问操作反馈
void NF(int client_fd)
{
std::string reportt;
struct stat fileStat;
int fd = open("404/404.html", O_RDONLY);
if (fd == -1)
{
// handle error
return;
}
fstat(fd, &fileStat);
off_t len = fileStat.st_size;
reportt = "HTTP/1.1 404 Not Found\r
Content-Type: text/html\r
Content-Length: " + std::to_string(len) + "\r
\r
";
// send HTTP header
send(client_fd, reportt.c_str(), reportt.length(), 0);
// send file content
sendfile(client_fd, fd, NULL, len);
close(fd);
// printf("I am here");
}
// 发送 CSS 文件的函数
void sendCSS(int client_fd, const std::string &css_path)
{
std::ifstream css_file(css_path);
if (!css_file.is_open())
{
// 文件打开失败,发送 404 Not Found 错误
std::string response = "HTTP/1.1 404 Not Found\r
Content-Length: 0\r
\r
";
send(client_fd, response.c_str(), response.length(), 0);
return;
}
// 获取 CSS 文件的大小
struct stat file_stat;
stat(css_path.c_str(), &file_stat);
off_t len_css = file_stat.st_size;
// 构造 HTTP 头部
std::string header = "HTTP/1.1 200 OK\r
Content-Type: text/css\r
Content-Length: " + std::to_string(len_css) + "\r
\r
";
send(client_fd, header.c_str(), header.length(), 0);
// 发送 CSS 文件内容
char buffer[1024];
while (!css_file.eof())
{
css_file.read(buffer, sizeof(buffer));
send(client_fd, buffer, css_file.gcount(), 0);
}
css_file.close();
}
// 发送 JavaScript 文件的函数
void sendJS(int client_fd, const std::string &js_path)
{
std::ifstream js_file(js_path);
if (!js_file.is_open())
{
// 文件打开失败,发送 404 Not Found 错误
std::string response = "HTTP/1.1 404 Not Found\r
Content-Length: 0\r
\r
";
send(client_fd, response.c_str(), response.length(), 0);
return;
}
// 获取 JavaScript 文件的大小
struct stat file_stat;
stat(js_path.c_str(), &file_stat);
off_t len_js = file_stat.st_size;
// 构造 HTTP 头部
std::string header = "HTTP/1.1 200 OK\r
Content-Type: text/javascript\r
Content-Length: " + std::to_string(len_js) + "\r
\r
";
send(client_fd, header.c_str(), header.length(), 0);
// 发送 JavaScript 文件内容
char buffer[1024];
while (!js_file.eof())
{
js_file.read(buffer, sizeof(buffer));
send(client_fd, buffer, js_file.gcount(), 0);
}
js_file.close();
}
void sendimg(int client_fd, const std::string &js_path)
{
std::ifstream js_file(js_path);
if (!js_file.is_open())
{
// 文件打开失败,发送 404 Not Found 错误
std::string response = "HTTP/1.1 404 Not Found\r
Content-Length: 0\r
\r
";
send(client_fd, response.c_str(), response.length(), 0);
return;
}
// 获取 img 文件的大小
struct stat file_stat;
stat(js_path.c_str(), &file_stat);
off_t len_js = file_stat.st_size;
// 构造 HTTP 头部
std::string header = "HTTP/1.1 200 OK\r
Content-Type: image/png\r
Content-Length: " + std::to_string(len_js) + "\r
\r
";
send(client_fd, header.c_str(), header.length(), 0);
// 发送 img 文件内容
char buffer[1024];
while (!js_file.eof())
{
js_file.read(buffer, sizeof(buffer));
send(client_fd, buffer, js_file.gcount(), 0);
}
js_file.close();
}
void sendfd(int client_fd, const std::string& fd_name, const std::string& directory) {
std::string filePath;
if (directory.empty()) {
// 如果 directory 为空,则文件从 downloaded 文件夹上传
filePath = "downloaded/" + fd_name;
} else {
// 否则,文件从指定目录的 downloaded 文件夹上传
filePath = directory + "/downloaded/" + fd_name;
}
// 确保文件存在
if (!std::filesystem::exists(filePath)) {
std::cerr << "File does not exist: " << filePath << std::endl;
NF(client_fd); // 发送 404 Not Found 页面
return;
}
// 打开文件
int fd = open(filePath.c_str(), O_RDONLY);
if (fd < 0) {
std::cerr << "Failed to open file: " << filePath << std::endl;
return;
}
// 获取文件状态
struct stat fileStat;
if (fstat(fd, &fileStat) != 0) {
std::cerr << "Failed to get file status for: " << filePath << std::endl;
close(fd);
return;
}
// 构建 HTTP 响应头
std::string contentType = "application/octet-stream"; // 默认内容类型
// 可以根据文件类型设置不同的内容类型
// if (fd_name.find(".html") != std::string::npos) {
//     contentType = "text/html";
// }
std::string header = "HTTP/1.1 200 OK\r
Content-Type: " + contentType +
"\r
Content-Length: " + std::to_string(fileStat.st_size) +
"\r
\r
";
// 发送 HTTP 头部
send(client_fd, header.c_str(), header.length(), 0);
// 发送文件内容
off_t offset = 0;
if (sendfile(client_fd, fd, &offset, fileStat.st_size) < 0) {
std::cerr << "Failed to send file: " << filePath << std::endl;
}
// 关闭文件描述符
close(fd);
}
// 新建函数 processRequest 来处理请求
std::string processRequest(const std::string &request, const std::string &directory /*, const std::vector& keyword*/, int client_fd)
{
std::string report;
size_t start_pos = request.find(" ");
size_t end_pos = request.find(" ", start_pos + 1);
if (start_pos != std::string::npos && end_pos != std::string::npos)
{
std::string method = request.substr(0, start_pos);
std::string path = request.substr(start_pos + 1, end_pos - start_pos - 1);
std::cout << "Received path: " << path << std::endl;
// 提取 User-Agent 头的值
std::string userAgent = extractUserAgent(request);
if (path == "/" || path == "/index")
{
// report = "HTTP/1.1 200 OK\r
Content-Type: text/plain\r
Content-Length: 13\r
\r
Hello, World!";
// send(client_fd, report.c_str(), report.length(), 0);
char report[520] = "HTTP/1.1 200 ok\r
\r
";
int s = send(client_fd, report, strlen(report), 0);
// 打开并发送HTML文件内容
int fd = open("index.html", O_RDONLY);
sendfile(client_fd, fd, NULL, 2500); // 使用零拷贝发送文件内容
close(fd);
}
// 处理 /user-agent 请求
else if (path == "/user-agent")
{
report = "HTTP/1.1 200 OK\r
Content-Type: text/plain\r
Content-Length: " + std::to_string(userAgent.length()) + "\r
\r
" + userAgent;
send(client_fd, report.c_str(), report.length(), 0);
}
else if (path == "/404.css")
{
sendCSS(client_fd, "404/404.css");
}
else if (path == "/404.js")
{
sendJS(client_fd, "404/404.js");
}
else if (path == "/img/404.png")
{
sendimg(client_fd, "404/img/404.png");
}
else if (path.substr(0, 12) == "/downloaded/")
{
std::cout << "sosososos" << std::endl;
std::string fd_name = path.substr(12); // 去掉 "/downloaded/"
sendfd(client_fd, fd_name, directory);
}
// 处理 /echo/ 请求
else if (path.find("/echo/") == 0)
{
std::string responseContent = captureAfterKey(request);
report = "HTTP/1.1 200 OK\r
Content-Type: text/plain\r
Content-Length: " + std::to_string(responseContent.length()) + "\r
\r
" + responseContent;
send(client_fd, report.c_str(), report.length(), 0);
}
else if (path == "/list-files")
{
report = "HTTP/1.1 200 OK\r
Content-Type: application/json\r
\r
" + listFiles(directory);
std::cout << listFiles(directory) << std::endl;
std::cout << report << std::endl;
send(client_fd, report.c_str(), report.length(), 0);
}
else if (method == "POST")
{
// 确保路径以 "/files/" 开始
if (path.find("/files/") == 0)
{
report = handlePostRequest(request, directory, client_fd);
send(client_fd, report.c_str(), report.length(), 0);
}
else
{
// report = "HTTP/1.1 404 Not Found\r
Content-Type: text/plain\r
Content-Length: 0\r
\r
";
// send(client_fd, report.c_str(), report.length(), 0);
NF(client_fd);
}
}
else{
NF(client_fd);
}
}
}
void handle_client(int client_fd, struct sockaddr_in client_addr, const std::string &directory)
{
char buffer[1024];
std::string report;
/*std::vector keyword = {"/files/", "/echo/", "/index.html", "/user-agent"};*/
int bytes_received = recv(client_fd, buffer, sizeof(buffer), 0);
if (bytes_received < 0)
{
std::cerr << "Error receiving data from client
";
close(client_fd);
return;
}
std::string request(buffer, bytes_received);
report = processRequest(request, directory /*, keyword*/, client_fd);
// Send the response to the client
// send(client_fd, report.c_str(), report.length(), 0);
close(client_fd);
}
EpollServer::EpollServer(int server_fd, const std::string &directory, size_t thread_count)
: server_fd(server_fd), directory(directory), pool(thread_count)
{
// 创建epoll实例
epoll_fd = epoll_create1(0);
if (epoll_fd < 0)
{
std::cerr << "Error: epoll_create1 failed" << std::endl;
exit(EXIT_FAILURE);
}
// 设置服务器套接字为非阻塞
fcntl(server_fd, F_SETFL, O_NONBLOCK);
// 初始化epoll事件
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = server_fd;
// 将服务器套接字添加到epoll监听
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &ev) < 0)
{
std::cerr << "Error: epoll_ctl failed" << std::endl;
exit(EXIT_FAILURE);
}
}
EpollServer::~EpollServer()
{
pool.stop();
close(epoll_fd);
close(server_fd);
}
void EpollServer::eventLoop()
{
struct epoll_event events[MAX_EVENTS];
while (true)
{
int numEvents = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
for (int i = 0; i < numEvents; ++i)
{
if (events[i].data.fd == server_fd)
{
struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);
int client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_addr_len);
if (client_fd < 0)
{
std::cerr << "Error: accept failed" << std::endl;
continue;
}
// 使用线程池来处理新的连接
pool.enqueue([this, client_fd, client_addr]()
{ handle_client(client_fd, client_addr, directory); });
}
}
}
}
void EpollServer::start()
{
eventLoop();
}
int main(int argc, char **argv)
{
// You can use print statements as follows for debugging, they'll be visible when running tests.
std::cout << "Logs from your program will appear here!
";
std::string directory;
for (int i = 1; i < argc; ++i)
{
if (std::string(argv[i]) == "--directory" && i + 1 < argc)
{
directory = argv[++i];
break;
}
}
if (directory.empty())
{
std::cout << "Default directory set to: " << directory << std::endl;
}
// Uncomment this block to pass the first stage
server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd < 0)
{
std::cerr << "Failed to create server socket
";
return 1;
}
//
// // Since the tester restarts your program quite often, setting REUSE_PORT
// // ensures that we don't run into 'Address already in use' errors
int reuse = 1;
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEPORT, &reuse, sizeof(reuse)) < 0)
{
std::cerr << "setsockopt failed
";
return 1;
}
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(4221);
//
if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) != 0)
{
std::cerr << "Failed to bind to port 4221
";
return 1;
}
//
int connection_backlog = 5;
if (listen(server_fd, connection_backlog) != 0)
{
std::cerr << "listen failed
";
return 1;
}
// 输出等待客户端连接的消息
std::cout << "Waiting for a client to connect...
";
// 创建EpollServer实例并启动
EpollServer server(server_fd, directory, thread_count);
server.start(); // 启动事件循环
// 关闭服务器套接字(服务器正常运行时不会执行到这一步)
close(server_fd);
return 0;
/*
size_t thread_count = std::thread::hardware_concurrency() ? std::thread::hardware_concurrency() : 4;
struct sockaddr_in client_addr;
int client_addr_len = sizeof(client_addr);
std::cout << "Waiting for a client to connect...
";
while (true) {
struct sockaddr_in client_addr;
int client_addr_len = sizeof(client_addr);
client_fd = accept(server_fd, (struct sockaddr *) &client_addr, (socklen_t *) &client_addr_len);
if (client_fd < 0) {
std::cerr << "Error accepting connection
";
continue; // Skip to the next iteration if accept fails
}
// Create a new thread to handle the client
pool.enqueue(handle_client, client_fd, client_addr, directory);//线程池(2)
//std::thread client_thread(handle_client, client_fd, client_addr, directory);//新建线程(1)
//client_thread.detach(); // Detach the thread to let it run independently
//EpollServer server(server_fd, directory, thread_count);//epoll线程(3)
// 启动事件循环
//server.start();//(3)
}
// Close the server socket when done (not reached in this example)
close(server_fd);
return 0;
*/
}

        在eventLoop函数中,创建了一个 epoll_event 结构体数组 events 用于存储事件信息。进入一个无限循环,调用 epoll_wait 函数等待事件的发生,该函数会阻塞直到有事件发生。当有事件发生时,通过遍历 events 数组来处理每一个事件。如果事件对应的文件描述符为 server_fd,表示有新的客户端连接请求到达,代码会接受连接并创建一个新的套接字 client_fd,然后将其交给线程池中的一个线程来处理。线程池中的线程会调用 handle_client 函数来处理客户端连接,同时传入客户端的套接字描述符 client_fd 和客户端地址信息 client_addr。

          在上述代码中,如果编译时没有指定directory,就会以本地的服务器的根目录为默认地址,其实应该分离函数实现的cpp(在线程池内的新线程的创建会包含局部变量服务器和客户端的fd,讲道理很好提取函数形成新的cpp),但是出现一些问题(说是定义重复的问题,不知道该怎么处理),而且上述代码肯定也有一些错误是我没有发现,下面是代码的实现效果。(访问不存在的地址)

        代码中并没有实现不存在downloaded文件夹程序自动创建的功能,(后续说不定闲了就实现了)所以在运行代码的过程中,需要用户在对应的文件夹下创建名为downloaded的文件夹作为上传以及下载的目标文件夹,需要修改可以在实现函数的区域内修改相应的文件夹名,以及不存在则创建新文件夹的功能。(名为handlePostRequest的函数中,并且如果细分,需要重新定义上传文件夹的地址(都使用的是相对地址),其实操作起来不会太难)。而在c++中我使用了这个库来调用sendfile函数来实现零拷贝的文件传输(原本是打算写一个File.hpp实现的,发现有这个库就拿来主义了)。

下图为输入不存在的地址时,服务器会发回404的html文件,前端在接收到此HTML文件后,发现链接着js文件和css文件,就会再次发送请求报文给服务器端,此时的服务器段还需要根据所收到的请求,发出相应的请求和文件。

在Linux上实现HTTP服务器,进行前端对后端的文件上传和下载(实现线程池,epoll等)插图(3)

(下图是文件上传,下载,列表刷新的实现,在测试过程中发现前端发送html文件到服务器端有部分错误(服务器能够成功下载,并且在本地文件夹内可以发现该完整文件,但是前端会发出不带任何错误代码的回馈,而是发出禁止警告),可能是前端读取到文件中了不同版本的http协议而发出错误警告)

 在Linux上实现HTTP服务器,进行前端对后端的文件上传和下载(实现线程池,epoll等)插图(4)

下图为发送HTML文件所产生的错误警告,但结果服务器能够正确下载文件。

在Linux上实现HTTP服务器,进行前端对后端的文件上传和下载(实现线程池,epoll等)插图(5)

        后续的函数代码分离和代码优化,应该也会在未来慢慢实现,也大概到这里先鸽一会了。不要问为什么index.html为什么看起来那么寒酸,而404网页却不是这样,因为404的网页是我先写的,到后面服务器修修改改的时候就完全不想什么美观了,就只想测试功能就行了,大概就这样吧。我相信这个充满个人风格的代码和九曲大肠般的螺旋回转设计思路,一眼就看出来是萌新写的了,还请各位多多见谅。

在Linux上实现HTTP服务器,进行前端对后端的文件上传和下载(实现线程池,epoll等)插图(6)

本站无任何商业行为
个人在线分享-虚灵IT资料分享 » 在Linux上实现HTTP服务器,进行前端对后端的文件上传和下载(实现线程池,epoll等)
E-->