仿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;
}

封面图如下:

相关推荐
Lucis__1 小时前
I/O多路复用:基于epoll实现Reactor高性能TCP服务器
linux·服务器·网络·reactor·多路复用
kyle~1 小时前
Linux时间系统3---时间同步控制机制(step、slew、offset、frequency)
linux·运维·服务器
葱卤山猪1 小时前
【自用】解析http post表单数据,将其中的二进制数据保存到csv文件且加载到内存
网络·网络协议·http
Mike117.1 小时前
GBase 8c 序列用在业务流水号上要留几道边界
服务器·数据库
零壹AI实验室1 小时前
DeepSeek本地部署:从零开始,把大模型跑在自己电脑上
服务器·网络·人工智能·电脑
西柚小萌新1 小时前
【计算机常识】--使用 Gitea 在本地/内网搭建 Git 私有服务器
服务器·git·gitea
Agent手记1 小时前
物流对账全流程自动化,落地实操与财务打通方案:基于LLM+智能体驱动的业财一体化实践
运维·人工智能·ai·自动化
铅笔小新z1 小时前
【Linux】进程间通信(IPC)
java·linux·运维
汪汪大队u1 小时前
校园资源共享平台搭建与Shell自动化监控实战
运维·自动化