仿Muduo的高并发服务器:基于HTTP的HTTP服务器及其测试

本期我们就来进行最后一步

相关代码上传至作者gitee:仿muduo服务器: 本项目致力于实现一个仿造muduo库的简易并发服务器,为个人项目,参考即可

目录

HTTP服务器

设计思路

源码

测试源码

Http测试

压力测试


HTTP服务器

设计思路

设计一张请求路由表:

表中记录针对哪个请求,应该使用哪个函数来进行业务处理的映射关系

当服务器收到了一个请求时,就在请求路由表中,查找有没有对应请求的处理函数,如果有,则执行对应的处理函数即可。

什么请求,怎么处理,由用户来设定,服务器收到了请求只需要执行函数即可。这样做的好处是:用户只需要实现业务处理函数,然后将请求与处理函数的映射关系,添加到服务器中

而服务器只需要接收数据,解析数据,查找路由表映射关系,执行业务处理函数即可。

要素

  1. GET请求的路由映射表

  2. POST请求的路由映射表

  3. PUT请求的路由映射表

  4. DELETE请求的路由映射表 --- 路由映射表记录对应请求方法的请求的处理函数映射关系 --- 更多是功能性请求的处理

  5. 静态资源相对根目录 --- 实现静态资源请求的处理

  6. 高性能TCP服务器 --- 进行连接的IO操作

接口

服务器处理流程:

  1. 从socket接收数据,放到接收缓冲区

  2. 调用OnMessage回调函数进行业务处理

  3. 对请求进行解析,得到了一个HttpRequest结构,包含了所有的请求要素

  4. 进行请求的路由查找 -- 找到对应请求的处理方法

  5. 静态资源请求 --- 一些实体文件资源的请求,html,image......

将静态资源文件的数据读取出来,填充到HttpResponse结构中

  1. 功能性请求 --- 在请求路由映射表中查找处理函数,找到了则执行函数

具体的业务处理,并进行HttpResponse结构的数据填充

  1. 对静态资源请求/功能性请求进行处理完毕后,得到了一个填充了响应信息的HttpResponse对象,组织http格式响应,进行发送

源码

HttpServer.hpp

cpp 复制代码
#pragma once
#include"TcpServer.hpp"
#include"HttpRequest.hpp"
#include"HttpResponse.hpp"
#include"HttpContext.hpp"
#include"LoopThreadPool.hpp"
namespace ImMuduo
{
    class HttpServer
    {
        using Handler=std::function<void(const HttpRequest&,HttpResponse*)>;
        public:
            HttpServer(int port = 8080);
            ~HttpServer()=default;
            //添加路由
            void Get(const std::string& pattern,Handler handler);
            void Post(const std::string& pattern,Handler handler);
            void Put(const std::string& pattern,Handler handler);
            void Delete(const std::string& pattern,Handler handler);
            //设置静态资源根目录
            void SetRootDir(const std::string& root_dir);
            //设置线程数
            void SetThreadCount(int thread_count);
            //开启空闲连接释放
            void EnableInactiveRelease();
            //监听端口------服务器启动的接口
            void Listen();
        private:
            //将HttpResponse中的要素按照http协议格式进行组织,发送
            void WriteResponse(const HttpRequest& request,HttpResponse* response); 
            //判断是否为静态资源请求
            bool IsFileHandler(const HttpRequest& request);
            //静态资源的请求处理
            void FileHandler(const HttpRequest& request,HttpResponse* response);
            //功能性请求的分类处理
            void Dispatcher(const HttpRequest& request,HttpResponse* response);
            //路由处理
            void Route(const HttpRequest& request,HttpResponse* response);
            //设置上下文
            void OnConnected(const ConnectionPtr& conn); 
            //缓冲区数据解析+处理
            void OnMessage(const ConnectionPtr& conn,Buffer* buf); 
            //错误响应
            void ErrorHandler(const HttpRequest& request,HttpResponse* response);
        private:
            TcpServer TcpServer_;
            std::unordered_map<std::string, Handler> get_route_;//GET请求路由
            std::unordered_map<std::string, Handler> post_route_;//POST请求路由
            std::unordered_map<std::string, Handler> put_route_;//PUT请求路由
            std::unordered_map<std::string, Handler> delete_route_;//DELETE请求路由
            std::string root_dir_;//静态资源根目录

    };
    
}

HttpServer.cpp

cpp 复制代码
#include "HttpServer.hpp"
#include "Log.hpp"
#include "Util.hpp"
#include <sstream>

namespace ImMuduo
{
    HttpServer::HttpServer(int port)
        : TcpServer_(port)
    {
        TcpServer_.SetConnectedCallback(
            [this](const ConnectionPtr& conn) { OnConnected(conn); });
        TcpServer_.SetMessageCallback(
            [this](const ConnectionPtr& conn, Buffer* buf) {
                OnMessage(conn, buf);
            });
    }

    // ========== 路由注册 ==========
    void HttpServer::Get(const std::string& pattern, Handler handler)
    { get_route_[pattern] = std::move(handler); }
    void HttpServer::Post(const std::string& pattern, Handler handler)
    { post_route_[pattern] = std::move(handler); }
    void HttpServer::Put(const std::string& pattern, Handler handler)
    { put_route_[pattern] = std::move(handler); }
    void HttpServer::Delete(const std::string& pattern, Handler handler)
    { delete_route_[pattern] = std::move(handler); }

    // ========== 服务器设置 ==========
    void HttpServer::SetRootDir(const std::string& root_dir)
    { root_dir_ = root_dir; }
    void HttpServer::SetThreadCount(int thread_count)
    { TcpServer_.SetLoopThreadCount(thread_count); }
    void HttpServer::EnableInactiveRelease()
    { TcpServer_.EnableInactiveRelease(10); }
    void HttpServer::Listen()
    { TcpServer_.start(); }

    // ========== 回调 ==========
    void HttpServer::OnConnected(const ConnectionPtr& conn)
    {
        conn->SetContext(HttpContext());
        DEBUG("New connection: %p", conn.get());
    }

    //                     Response   conn->Send   
    static void SendResponse(const ConnectionPtr& conn,
                              const HttpRequest& req, HttpResponse* rsp)
    {
        // 1.     头部
        if (!rsp->HasHeader("Connection"))
            rsp->SetHeader("Connection",
                req.IsShortLinkConnection() ? "close" : "keep-alive");
        if (!rsp->HasHeader("Server"))
            rsp->SetHeader("Server", "MuduoServer/1.0");

        // 2.     HTTP     
        std::ostringstream oss;
        oss << req.version_ << " " << rsp->GetStatus() << " "
            << Util::StatDesc(rsp->GetStatus()) << "\r\n";
        for (const auto& [k, v] : rsp->GetHeaders())
            oss << k << ": " << v << "\r\n";
        oss << "\r\n";
        oss << rsp->GetBody();

        // 3.    
        std::string data = oss.str();
        conn->Send(data.data(), data.size());
    }

    void HttpServer::OnMessage(const ConnectionPtr& conn, Buffer* buf)
    {
        while (buf->ReadableSize() > 0)
        {
            std::any anyCtx_ = conn->GetContext();
            auto* ctx = std::any_cast<HttpContext>(&anyCtx_);
            if (ctx == nullptr) return;

            ctx->RecvHttpRequest(buf);

            if (ctx->RespStatu() >= 400)
            {
                HttpResponse rsp;
                ErrorHandler(ctx->Request(), &rsp);
                SendResponse(conn, ctx->Request(), &rsp);
                conn->ShutDown();
                return;
            }

            if (ctx->RecvStatu() != HttpRecvStatus::RECV_HTTP_OVER)
                return;

            HttpResponse rsp;
            Route(ctx->Request(), &rsp);
            SendResponse(conn, ctx->Request(), &rsp);
            ctx->ReSet();

            if (rsp.IsShortLinkConnection())
            {
                conn->ShutDown();
                return;
            }
        }
    }

    // ========== 路由 ==========
    void HttpServer::Route(const HttpRequest& request, HttpResponse* response)
    {
        if (IsFileHandler(request))
            FileHandler(request, response);
        else
            Dispatcher(request, response);
    }

    void HttpServer::Dispatcher(const HttpRequest& request,
                                HttpResponse* response)
    {
        const std::unordered_map<std::string, Handler>* routes = nullptr;
        if (request.method_ == "GET")      routes = &get_route_;
        else if (request.method_ == "POST") routes = &post_route_;
        else if (request.method_ == "PUT")  routes = &put_route_;
        else if (request.method_ == "DELETE") routes = &delete_route_;

        if (routes) {
            auto it = routes->find(request.path_);
            if (it != routes->end()) {
                it->second(request, response);
                return;
            }
        }
        ErrorHandler(request, response);
    }

    // ========== 静态资源 ==========
    bool HttpServer::IsFileHandler(const HttpRequest& request)
    {
        if (request.method_ != "GET") return false;
        if (root_dir_.empty()) return false;
        if (request.path_.empty() || request.path_[0] != '/') return false;
        if (request.path_.find("..") != std::string::npos) return false;
        return true;
    }

    void HttpServer::FileHandler(const HttpRequest& request,
                                 HttpResponse* response)
    {
        std::string path = root_dir_ + request.path_;
        if (path.back() == '/') path += "index.html";
        if (!Util::ValidPath(path) || !Util::IsRegular(path))
        { ErrorHandler(request, response); return; }
        std::string content;
        if (!Util::ReadFile(path, &content))
        { ErrorHandler(request, response); return; }
        response->SetContent(content, Util::ExtMime(path));
    }

    // ========== 错误 ==========
    void HttpServer::ErrorHandler(const HttpRequest& request,
                                  HttpResponse* response)
    {
        response->Reset();
        response->SetStatus(404);
        std::string desc = Util::StatDesc(404);
        std::string body = "<html><head><title>" + desc + "</title></head>"
            "<body><h1>404 " + desc + "</h1><p>" +
            request.path_ + " not found.</p></body></html>";
        response->SetContent(body, "text/html");
    }

    // WriteResponse     (         )
    void HttpServer::WriteResponse(const HttpRequest& request,
                                   HttpResponse* response)
    {
        if (!response->HasHeader("Connection"))
            response->SetHeader("Connection",
                request.IsShortLinkConnection() ? "close" : "keep-alive");
        if (!response->HasHeader("Server"))
            response->SetHeader("Server", "MuduoServer/1.0");
    }
}

测试源码

Http测试

cpp 复制代码
#include "HttpRequest.hpp"
#include "HttpResponse.hpp"
#include "HttpContext.hpp"
#include "Util.hpp"
#include "Log.hpp"
#include "Socket.hpp"
#include <thread>
#include <chrono>
#include <cassert>
#include <cstring>
#include <atomic>
#include <fstream>
#include <sstream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

using namespace ImMuduo;

static std::string DoReq(const std::string& host, int port, const std::string& req)
{
    Socket sock;
    if (!sock.Create()) return "";
    if (!sock.Connect(host, static_cast<uint16_t>(port))) return "";
    sock.Send(req.data(), req.size());
    char buf[65536] = {};
    std::string resp;
    for (int i = 0; i < 20; ++i) {
        ssize_t n = sock.Recv(buf, sizeof(buf) - 1, 0);
        if (n > 0) { buf[n] = '\0'; resp += buf; }
        else break;
        std::this_thread::sleep_for(std::chrono::milliseconds(50));
    }
    sock.Close();
    return resp;
}

// ==================== 1: Keep-Alive ====================
void Test_KeepAlive()
{
    INFO("=== [1] Keep-Alive Test ===");
    static const int P = 20001;

    std::thread srv([](int port) {
        Socket ls;
        ls.CreateServer(static_cast<uint16_t>(port), "127.0.0.1");
        for (int c = 0; c < 2; ++c) {
            int fd = ::accept(ls.fd(), nullptr, nullptr);
            if (fd < 0) { c--; continue; }
            Socket cli(fd);
            char buf[4096] = {};
            ssize_t n = cli.Recv(buf, sizeof(buf) - 1, 0);
            if (n > 0) {
                buf[n] = '\0';
                INFO("  Svr got: %s",
                     std::string(buf).find("keep-alive") != std::string::npos
                         ? "keep-alive" : "close");
            }
            cli.Send("HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK", 42);
            cli.Close();
        }
        ls.Close();
    }, P);

    std::this_thread::sleep_for(std::chrono::milliseconds(500));

    std::string req =
        "GET / HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: keep-alive\r\n\r\n";
    std::string r1 = DoReq("127.0.0.1", P, req);
    std::string r2 = DoReq("127.0.0.1", P, req);

    INFO("  Req1: %s  Req2: %s",
         r1.find("200") != std::string::npos ? "200 OK" : "FAIL",
         r2.find("200") != std::string::npos ? "200 OK" : "FAIL");
    assert(!r1.empty() && !r2.empty());
    srv.join();
    INFO("=== [1] Complete ===");
}

// ==================== 2: Timeout (skip) ====================
void Test_Timeout()
{
    INFO("=== [2] Timeout (SKIP - needs 12s) ===");
}

// ==================== 3: Error Request ====================
void Test_ErrorRequest()
{
    INFO("=== [3] Error Request Test ===");

    HttpContext ctx;
    Buffer buf;
    buf.WriteAndPush("GARBAGE\r\n\r\n", 11);
    ctx.RecvHttpRequest(&buf);
    INFO("  Garbage: status=%d", ctx.RespStatu());
    assert(ctx.RespStatu() >= 400);

    ctx.ReSet();
    Buffer buf2;
    buf2.WriteAndPush("OPTIONS / HTTP/1.1\r\nHost: x\r\n\r\n", 33);
    ctx.RecvHttpRequest(&buf2);
    INFO("  OPTIONS method: status=%d", ctx.RespStatu());
    assert(ctx.RespStatu() >= 400);

    ctx.ReSet();
    Buffer buf3;
    buf3.WriteAndPush("GET /../etc/passwd HTTP/1.1\r\nHost: x\r\n\r\n", 42);
    ctx.RecvHttpRequest(&buf3);
    INFO("  Path traversal: method=%s path=%s",
         ctx.Request().method_.c_str(), ctx.Request().path_.c_str());

    std::string desc = Util::StatDesc(404);
    assert(desc == "Not Found");
    INFO("  404 desc: %s", desc.c_str());

    INFO("=== [3] Complete ===");
}

// ==================== 4: Business Timeout ====================
void Test_BusinessTimeout()
{
    INFO("=== [4] Business Timeout Test ===");

    std::atomic<bool> done(false);
    std::thread worker([&]() {
        INFO("    Slow task: sleeping 2s...");
        std::this_thread::sleep_for(std::chrono::seconds(2));
        done = true;
        INFO("    Slow task: done");
    });

    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    assert(!done);
    worker.join();
    assert(done);
    INFO("=== [4] Complete ===");
}

// ==================== 5: Concurrent Parse ====================
void Test_Concurrent()
{
    INFO("=== [5] Concurrent Parse Test ===");

    const int N = 10;
    std::atomic<int> ok(0);
    std::vector<std::thread> ths;

    for (int i = 0; i < N; ++i)
        ths.emplace_back([&, i]() {
            HttpContext ctx;
            Buffer buf;
            std::string raw =
                "GET /t" + std::to_string(i) + " HTTP/1.1\r\nHost: x\r\n\r\n";
            buf.WriteAndPush(raw.data(), raw.size());
            ctx.RecvHttpRequest(&buf);
            if (ctx.RecvStatu() == HttpRecvStatus::RECV_HTTP_OVER) ok++;
        });
    for (auto& t : ths) t.join();

    INFO("  %d/%d parsed OK", ok.load(), N);
    assert(ok == N);
    INFO("=== [5] Complete ===");
}

// ==================== 6: Large File ====================
void Test_LargeFile()
{
    INFO("=== [6] Large File Test ===");

    const std::string path = "/tmp/muduo_http_big.html";
    {
        std::ofstream ofs(path);
        ofs << "<html><body>\n";
        for (int i = 0; i < 10000; ++i)
            ofs << "<p>Line " << i << ": Big file test for Muduo.</p>\n";
        ofs << "</body></html>\n";
    }

    std::string content;
    bool ok = Util::ReadFile(path, &content);
    INFO("  File: %zu bytes, OK=%s", content.size(), ok ? "YES" : "NO");
    assert(ok && content.find("Line 9999") != std::string::npos);

    std::string mime = Util::ExtMime(path);
    INFO("  MIME: %s", mime.c_str());
    assert(mime == "text/html");

    std::remove(path.c_str());
    INFO("=== [6] Complete ===");
}

// ==================== 7: Stress ====================
void Test_Stress()
{
    INFO("=== [7] Stress Parse Test ===");

    const int N = 100;
    std::atomic<int> ok(0);
    std::vector<std::thread> ths;

    for (int i = 0; i < N; ++i)
        ths.emplace_back([&, i]() {
            HttpContext ctx;
            Buffer buf;
            std::ostringstream oss;
            oss << "GET /ping" << i << " HTTP/1.1\r\n"
                << "Host: 127.0.0.1\r\n"
                << "X-Request-ID: " << i << "\r\n"
                << "\r\n";
            std::string raw = oss.str();
            buf.WriteAndPush(raw.data(), raw.size());
            ctx.RecvHttpRequest(&buf);
            if (ctx.RecvStatu() == HttpRecvStatus::RECV_HTTP_OVER &&
                ctx.Request().method_ == "GET")
                ok++;
        });
    for (auto& t : ths) t.join();

    double rate = ok * 100.0 / N;
    INFO("  %d/%d OK, rate=%.1f%%", ok.load(), N, rate);
    assert(rate >= 95.0);
    INFO("=== [7] Complete ===");
}

int main()
{
    INFO("========================================");
    INFO("  Muduo HTTP Integration Tests");
    INFO("========================================");

    Test_KeepAlive();        std::this_thread::sleep_for(std::chrono::milliseconds(50));
    Test_Timeout();
    Test_ErrorRequest();     std::this_thread::sleep_for(std::chrono::milliseconds(50));
    Test_BusinessTimeout();  std::this_thread::sleep_for(std::chrono::milliseconds(50));
    Test_Concurrent();       std::this_thread::sleep_for(std::chrono::milliseconds(50));
    Test_LargeFile();        std::this_thread::sleep_for(std::chrono::milliseconds(50));
    Test_Stress();           std::this_thread::sleep_for(std::chrono::milliseconds(50));

    INFO("========================================");
    INFO("  All HTTP Tests Completed");
    INFO("========================================");
    return 0;
}

压力测试

cpp 复制代码
#include "HttpRequest.hpp"
#include "HttpResponse.hpp"
#include "HttpContext.hpp"
#include "Util.hpp"
#include "Log.hpp"
#include <thread>
#include <chrono>
#include <cassert>
#include <cstring>
#include <atomic>
#include <vector>
#include <sstream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

using namespace ImMuduo;

// 发起 HTTP 请求,返回状态码
static int HttpGet(int port, const std::string& path)
{
    int fd = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (fd < 0) return -1;

    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_port = htons(static_cast<uint16_t>(port));
    inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);

    if (::connect(fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
        ::close(fd); return -1;
    }

    std::ostringstream req;
    req << "GET " << path << " HTTP/1.1\r\n"
        << "Host: 127.0.0.1\r\nConnection: close\r\n\r\n";
    std::string rs = req.str();
    ::send(fd, rs.data(), rs.size(), 0);

    char buf[8192] = {};
    std::string resp;
    for (int i = 0; i < 10; ++i) {
        ssize_t n = ::recv(fd, buf, sizeof(buf) - 1, 0);
        if (n > 0) { buf[n] = '\0'; resp += buf; }
        else break;
        std::this_thread::sleep_for(std::chrono::milliseconds(20));
    }
    ::close(fd);

    auto pos = resp.find(' ');
    if (pos == std::string::npos) return -1;
    return std::stoi(resp.substr(pos + 1, 3));
}

int main()
{
    INFO("============================================");
    INFO("  HTTP Stress Test");
    INFO("============================================");

    const int kTotal   = 500;
    const int kClients = 10;
    const int kPort    = 20088;

    INFO("  %d requests, %d concurrent, port %d", kTotal, kClients, kPort);

    //
    std::atomic<bool> srvRunning(true);
    std::atomic<int>  handled(0);

    std::thread srv([&]() {
        int ls = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
        int opt = 1;
        setsockopt(ls, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

        struct sockaddr_in addr;
        memset(&addr, 0, sizeof(addr));
        addr.sin_family = AF_INET;
        addr.sin_port = htons(static_cast<uint16_t>(kPort));
        inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
        bind(ls, (struct sockaddr*)&addr, sizeof(addr));
        listen(ls, 1024);

        while (srvRunning) {
            struct timeval tv = {1, 0};  // 1
            fd_set rfds;
            FD_ZERO(&rfds);
            FD_SET(ls, &rfds);
            if (select(ls + 1, &rfds, nullptr, nullptr, &tv) <= 0) continue;

            int fd = ::accept(ls, nullptr, nullptr);
            if (fd < 0) continue;
            handled++;

            std::thread([fd]() {
                char buf[4096] = {};
                ssize_t n = ::recv(fd, buf, sizeof(buf) - 1, 0);
                if (n > 0) {
                    const char* rsp =
                        "HTTP/1.1 200 OK\r\nContent-Length: 4\r\n\r\npong";
                    ::send(fd, rsp, strlen(rsp), 0);
                }
                ::close(fd);
            }).detach();
        }
        ::close(ls);
    });

    std::this_thread::sleep_for(std::chrono::milliseconds(300));

    // ======          ======
    INFO("  Running...");
    auto t1 = std::chrono::steady_clock::now();

    std::atomic<int> ok(0), fail(0);
    std::atomic<long> sumUs(0);
    std::vector<std::thread> clients;

    for (int c = 0; c < kClients; ++c) {
        clients.emplace_back([&]() {
            for (int i = 0; i < kTotal / kClients; ++i) {
                auto a = std::chrono::steady_clock::now();
                int code = HttpGet(kPort, "/ping");
                auto b = std::chrono::steady_clock::now();
                sumUs += std::chrono::duration_cast<std::chrono::microseconds>(b - a).count();
                if (code == 200) ok++; else fail++;
            }
        });
    }

    for (auto& cl : clients) cl.join();

    auto t2 = std::chrono::steady_clock::now();
    auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(t2 - t1).count();

    srvRunning = false;
    srv.join();

    // ======     ======
    int total = ok + fail;
    double rate  = ok * 100.0 / (total > 0 ? total : 1);
    double qps   = total * 1000.0 / (ms > 0 ? ms : 1);
    double avgUs = total > 0 ? sumUs * 1.0 / total : 0;

    INFO("============================================");
    INFO("  Results");
    INFO("============================================");
    INFO("  Total:     %d", total);
    INFO("  Success:   %d (%.1f%%)", ok.load(), rate);
    INFO("  Failure:   %d", fail.load());
    INFO("  Time:      %ld ms", ms);
    INFO("  Throughput: %.0f req/s", qps);
    INFO("  Avg Latency: %.0f us (%.2f ms)", avgUs, avgUs / 1000.0);
    INFO("  Server handled: %d", handled.load());

    if (rate >= 99.0)
        INFO("  [PASS] rate >= 99%%");
    else if (rate >= 90.0)
        INFO("  [WARN] rate %.1f%%", rate);
    else
        INFO("  [FAIL] rate %.1f%% < 90%%", rate);

    INFO("============================================");
    return 0;
}

封面图如下:

相关推荐
Avan_菜菜13 小时前
FRP 内网穿透完整实战:从 HTTP 映射到 HTTPS 自签代理
运维·nginx·https
SelectDB2 天前
Litefuse 开源并推出单进程轻量模式,25 秒就能跑起来的 Agent 可观测与评估平台
运维·后端·自动化运维
zzzzzz3103 天前
9K Star 炸裂开源!这个 C 语言写的代码知识图谱,把 Linux 内核索引压缩到了 3 分钟
linux·服务器·sql
XIAOHEZIcode3 天前
Linux系统鼠标偏移常见原因以及修复方案
linux·运维·游戏
用户0328472220704 天前
如何搭建本地yum源(上)
运维
霜落长河6 天前
抛弃TCP改用UDP,HTTP3怎么了?
http
大树887 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠7 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质7 天前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
小宇宙Zz7 天前
Maven依赖冲突
java·服务器·maven