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

一、初步了解和实现。

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

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

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

#include <iostream>
#include <cstdlib>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <vector>
#include <thread>
#include <fstream>
#include <sstream>

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\n", 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<char>(fileStream)), std::istreambuf_iterator<char>());
        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\n\r\n") + 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\nContent-Type: text/plain\r\nContent-Length: 0\r\n\r\n";
            } else {
                response = "HTTP/1.1 500 Internal Server Error\r\nContent-Type: text/plain\r\nContent-Length: 0\r\n\r\n";
            }
        } else {
            response = "HTTP/1.1 400 Bad Request: Invalid filename\r\nContent-Type: text/plain\r\nContent-Length: 0\r\n\r\n";
        }
    } else {
        response = "HTTP/1.1 400 Bad Request: Invalid POST request format\r\nContent-Type: text/plain\r\nContent-Length: 0\r\n\r\n";
    }

    return response;
}
// 新建函数 processRequest 来处理请求
std::string processRequest(const std::string& request, const std::string& directory, const std::vector<std::string>& 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\nContent-Type: text/plain\r\nContent-Length: 13\r\n\r\nHello, World!";
        }
        // 处理 /user-agent 请求
        else if (path == "/user-agent") {
            report = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: " 
                     + std::to_string(userAgent.length()) + "\r\n\r\n" + userAgent;
        }
        // 处理 /echo/ 请求
        else if (path.find("/echo/") == 0) {
            std::string responseContent = captureAfterKey(request);
            report = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: " 
                     + std::to_string(responseContent.length()) + "\r\n\r\n" + responseContent;
        }else if (method == "POST") {
        // 确保路径以 "/files/" 开始
        if (path.find("/files/") == 0) {
            report = handlePostRequest(request, directory);
        } else {
            report = "HTTP/1.1 404 Not Found\r\nContent-Type: text/plain\r\nContent-Length: 0\r\n\r\n";
        }
    }
        // 处理其他请求,需要directory参数
        else {
            if (directory.empty()) {
                report = "HTTP/1.1 404 Not Found\r\nContent-Type: text/plain\r\nContent-Length: 0\r\n\r\n";
                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\nContent-Type: application/octet-stream\r\nContent-Length: " 
                             + std::to_string(responseContent.length()) + "\r\n\r\n" + responseContent;
                } else {
                    report = "HTTP/1.1 404 Not Found\r\n\r\n";
                }
            } else {
                report = "HTTP/1.1 404 Not Found\r\n\r\n";
            }
        }
    } else {
        report = "HTTP/1.1 400 Bad Request\r\n\r\n";
    }
    
    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<std::string> 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\n";
        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!\n";
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.\n";
}


// 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\n";
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\n";
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\n";
return 1;
}
//
int connection_backlog = 5;
if (listen(server_fd, connection_backlog) != 0) {
std::cerr << "listen failed\n";
return 1;
}
//
struct sockaddr_in client_addr;
int client_addr_len = sizeof(client_addr);

std::cout << "Waiting for a client to connect...\n";

    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\n";
            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\nContent-Type: text/plain\r\nContent-Length: 0\r\n\r\n";你需要说明回复类型和长度(比如所html文件和该文件大小),但是也可以"HTTP/1.1 200 ok\r\n\r\n";后面直接接上发送文件的函数,实现发送给前端html文件(下方截图是该网站提供本地测试集,而我们也可以通过调用Crul工具,对我们所构建的服务器进行测试)

curl 命令_curl命令-CSDN博客

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

 

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

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

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

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

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

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

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

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>File Management</title>
    <style>
        /* CSS样式定义 */
    </style>
</head>
<body>
    <div class="container">
        <h1>File Management System</h1>
        
        <!-- 文件上传表单 -->
        <form action="/files/" method="post" enctype="multipart/form-data" class="upload-form">
            <!-- 文件选择框 -->
            <input type="file" name="file" id="file" required>
            <!-- 提交按钮 -->
            <input type="submit" value="Upload File">
        </form>

        <!-- 文件列表 -->
        <div class="file-list">
            <h2>File List</h2>
            <!-- 文件列表展示区域 -->
            <div id="fileList"></div>
        </div>
    </div>

    <script>
        // 获取文件列表并展示在页面上的函数
        function getFileList() {
            // 使用fetch API从服务器获取文件列表数据
            fetch('/list-files')
                .then(response => response.json())
                .then(data => {
                    let fileListElement = document.getElementById('fileList');
                    fileListElement.innerHTML = '';
                    let fileListHTML = '<ul>';
                    data.forEach(file => {
                        // 根据文件信息构建列表项的HTML
                        fileListHTML += `<li><a href="/downloaded/${file.name}">${file.name}</a> - Size: ${file.size} - Uploaded: ${file.uploaded}</li>`;
                    });
                    fileListHTML += '</ul>';
                    fileListElement.innerHTML = fileListHTML;
                })
                .catch(error => console.error('Error fetching file list:', error));
        }

        // 当页面加载时调用getFileList()函数,获取文件列表并展示
        window.onload = getFileList;

    </script>
</body>
</html>

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

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

<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
  <link rel="stylesheet" type="text/css" href="./404.css"/>

</head>
<body>
  <script src="404.js"></script>
  <div id="container">
    <div class="content">
      <h2>404</h2>
      <h4>Opps! Page not found</h4>
      <p>
        ㄟ(▔^▔ㄟ)    (╯▔^▔)╯   ㄟ(▔皿▔ㄟ)    (╯▔皿▔)╯

      </p>
      <a href="index" target="_self">Go to Index Page</a>
        //这里的a href="index",点击后就是发送了http://localhost:4221/index给服务器端
      </a>
    </div>
  </div>
</body>
</html>

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

#ifndef EPOLL_SERVER_HPP
#define EPOLL_SERVER_HPP

#include "ThreadPool.hpp" // 包含线程池的头文件
#include <sys/epoll.h> // 包含使用Epoll所需的头文件
#include <string> // 包含使用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 <vector>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <functional>
#include <future>

class ThreadPool {
public:
    ThreadPool(size_t); // 构造函数,接受线程数量作为参数
    template<class F, class... Args>
    auto enqueue(F&& f, Args&&... args) 
        -> std::future<typename std::result_of<F(Args...)>::type>; // 提交任务到线程池并返回结果的方法
    ~ThreadPool(); // 析构函数,销毁线程池

private:
    std::vector<std::thread> workers; // 存储工作线程的容器
    std::queue<std::function<void()>> 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<class F, class... Args>
auto ThreadPool::enqueue(F&& f, Args&&... args) 
    -> std::future<typename std::result_of<F(Args...)>::type> {
    using return_type = typename std::result_of<F(Args...)>::type;

    auto task = std::make_shared<std::packaged_task<return_type()>>(
            std::bind(std::forward<F>(f), std::forward<Args>(args)...)
    );
    std::future<return_type> res = task->get_future();

    {
        std::unique_lock<std::mutex> lock(queue_mutex);
        tasks.emplace([task](){ (*task)(); });
    }
    condition.notify_one(); // 通知一个工作线程有新任务可执行

    return res;
}

void ThreadPool::work() {
    while(!stop_flag) {
        std::function<void()> task;

        {
            std::unique_lock<std::mutex> 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<void()> 类型的 task 变量,用于存储待执行的任务。
然后通过一个 std::unique_lockstd::mutex 对任务队列进行加锁,以确保线程安全地访问任务队列。
接着调用 condition.wait() 来等待条件的满足,即等待任务队列非空或者 stop_flag 被设置为 true。
如果 stop_flag 被设置为 true 并且任务队列为空,则直接返回,结束工作函数的执行。
否则,将队首的任务取出并移动到 task 中,并从任务队列中移除该任务。
最后执行取出的任务。
*/

#endif // THREAD_POOL_HPP
#include <iostream>
#include <cstdlib>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <vector>
#include <thread>
#include <fstream>
#include <sstream>
#include "ThreadPool.hpp"
#include "EpollServer.hpp"
#include <fcntl.h>
#include <sys/sendfile.h>
#include <sys/stat.h>
#include <ctime>
#include <dirent.h>
#include <filesystem>
// #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\n", 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\n\r\n") + 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\nContent-Type: text/plain\r\nContent-Length: 0\r\n\r\n";
                //std::cout << "Send!!!\n" << std::endl;
                char report[520] = "HTTP/1.1 200 ok\r\n\r\n";
                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\nContent-Type: text/plain\r\nContent-Length: 0\r\n\r\n";
                std::cerr << "Failed to save file" << std::endl;
            }
        }
        else
        {
            response = "HTTP/1.1 400 Bad Request: Invalid filename\r\nContent-Type: text/plain\r\nContent-Length: 0\r\n\r\n";
            std::cerr << "No filename found in request" << std::endl;
        }
    }
    else
    {
        response = "HTTP/1.1 400 Bad Request: Invalid POST request format\r\nContent-Type: text/plain\r\nContent-Length: 0\r\n\r\n";
        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<std::string> 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 = "[\n";
    for (size_t i = 0; i < files.size(); ++i)
    {
        std::string fileInfo = getFileInfo((targetDirectory + "/" + files[i]));
        fileListContent += "  " + fileInfo;
        if (i < files.size() - 1)
        {
            fileListContent += ",\n"; // 除了最后一个元素外,每个元素后都添加逗号和换行符
        }
    }
    fileListContent += "\n]"; // 添加结束括号

    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\nContent-Type: text/html\r\nContent-Length: " + std::to_string(len) + "\r\n\r\n";
    // 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\nContent-Length: 0\r\n\r\n";
        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\nContent-Type: text/css\r\nContent-Length: " + std::to_string(len_css) + "\r\n\r\n";
    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\nContent-Length: 0\r\n\r\n";
        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\nContent-Type: text/javascript\r\nContent-Length: " + std::to_string(len_js) + "\r\n\r\n";
    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\nContent-Length: 0\r\n\r\n";
        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\nContent-Type: image/png\r\nContent-Length: " + std::to_string(len_js) + "\r\n\r\n";
    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\nContent-Type: " + contentType +
                         "\r\nContent-Length: " + std::to_string(fileStat.st_size) +
                         "\r\n\r\n";

    // 发送 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<std::string>& 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\nContent-Type: text/plain\r\nContent-Length: 13\r\n\r\nHello, World!";
            // send(client_fd, report.c_str(), report.length(), 0);
            char report[520] = "HTTP/1.1 200 ok\r\n\r\n";
            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\nContent-Type: text/plain\r\nContent-Length: " + std::to_string(userAgent.length()) + "\r\n\r\n" + 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\nContent-Type: text/plain\r\nContent-Length: " + std::to_string(responseContent.length()) + "\r\n\r\n" + responseContent;
            send(client_fd, report.c_str(), report.length(), 0);
        }
        else if (path == "/list-files")
        {
            report = "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\n" + 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\nContent-Type: text/plain\r\nContent-Length: 0\r\n\r\n";
                // 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<std::string> 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\n";
        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!\n";
    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\n";
        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\n";
        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\n";
        return 1;
    }
    //
    int connection_backlog = 5;
    if (listen(server_fd, connection_backlog) != 0)
    {
        std::cerr << "listen failed\n";
        return 1;
    }

    // 输出等待客户端连接的消息
    std::cout << "Waiting for a client to connect...\n";

    // 创建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...\n";

        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\n";
                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++中我使用了<sys/sendfile.h>这个库来调用sendfile函数来实现零拷贝的文件传输(原本是打算写一个File.hpp实现的,发现有这个库就拿来主义了)。

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

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

 

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

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

  • 59
    点赞
  • 45
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是使用epoll线程池实现高并发服务器的C++11代码示例: ```cpp #include <iostream> #include <thread> #include <vector> #include <queue> #include <mutex> #include <condition_variable> #include <sys/epoll.h> #include <unistd.h> #define MAX_EVENTS 100 #define THREAD_POOL_SIZE 10 std::mutex mtx; std::condition_variable cv; std::queue<int> taskQueue; void workerThread() { while (true) { std::unique_lock<std::mutex> lock(mtx); cv.wait(lock, [] { return !taskQueue.empty(); }); int fd = taskQueue.front(); taskQueue.pop(); // 处理任务,这里可以根据具体需求进行处理 lock.unlock(); // 继续监听其他事件 } } int main() { // 创建epoll句柄 int epoll_fd = epoll_create(1); if (epoll_fd == -1) { std::cerr << "Failed to create epoll" << std::endl; return 1; } // 创建线程池 std::vector<std::thread> threadPool; for (int i = 0; i < THREAD_POOL_SIZE; ++i) { threadPool.emplace_back(workerThread); } // 添加监听事件到epoll句柄 struct epoll_event event; event.events = EPOLLIN; event.data.fd = /* 监听的文件描述符 */; if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, /* 监听的文件描述符 */, &event) == -1) { std::cerr << "Failed to add event to epoll" << std::endl; return 1; } // 开始监听事件 struct epoll_event events[MAX_EVENTS]; while (true) { int num_events = epoll_wait(epoll_fd, events, MAX_EVENTS, -1); if (num_events == -1) { std::cerr << "Failed to wait for events" << std::endl; return 1; } for (int i = 0; i < num_events; ++i) { if (events[i].events & EPOLLIN) { // 处理读事件,将任务添加到任务队列 std::lock_guard<std::mutex> lock(mtx); taskQueue.push(events[i].data.fd); cv.notify_one(); } } } // 清理资源 close(epoll_fd); for (auto& thread : threadPool) { thread.join(); } return 0; } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值