简单的http服务器实现C++

目录

1.网页前端部分

2.服务器后端部分

封装网络地址类

封装锁类

日志类

socket套接字封装类

TcpServer类

Http服务器类

HttpRequest类的编写

HttpResponse类的编写

HttpServer类的编写

ServerMain.cc服务器源文件

核心特点

总结


1.网页前端部分

这一部分的目的是为了测试我们的http服务器是否能跑通而做的简单页面。

其结构如下:

wwwroot文件夹里面有我们的图片,404页面,内容页面,首页,登入,注册,由于我们的目的是了解http服务器,所以前端的页面部分我就不做过多讲解。因为这些页面的实现我也是从网络上摘取下来的,没有讲解的意义。重点还是在服务器。

2.服务器后端部分

我们还是先把我们的老朋友给请出来:

封装网络地址类

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

// 封装网络地址类
class InetAddr
{
private:
    void ToHost(const struct sockaddr_in &addr)
    {
        _port = ntohs(addr.sin_port);
        //_ip = inet_ntoa(addr.sin_addr);
        char ip_buf[32];
        ::inet_ntop(AF_INET, &addr.sin_addr, ip_buf, sizeof(ip_buf));
        _ip = ip_buf;
    }

public:
    InetAddr(const struct sockaddr_in &addr)
        : _addr(addr)
    {
        ToHost(addr); // 将addr进行转换
    }

    std::string AddrStr()
    {
        return _ip + ":" + std::to_string(_port);
    }
    InetAddr()
    {
    }

    bool operator==(const InetAddr &addr)
    {
        return (this->_ip == addr._ip && this->_port == addr._port);
    }

    std::string Ip()
    {
        return _ip;
    }
    uint16_t Port()
    {
        return _port;
    }
    struct sockaddr_in Addr()
    {
        return _addr;
    }
    ~InetAddr()
    {
    }

private:
    std::string _ip;
    uint16_t _port;
    struct sockaddr_in _addr;
};

封装锁类

cpp 复制代码
#pragma once

#include <pthread.h>

class LockGuard
{
public:
    LockGuard(pthread_mutex_t *mutex) : _mutex(mutex)
    {
        pthread_mutex_lock(_mutex);
    }
    ~LockGuard()
    {
        pthread_mutex_unlock(_mutex);
    }

private:
    pthread_mutex_t *_mutex;
};

日志类

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <ctime>
#include <stdarg.h>
#include <fstream>
#include <string.h>
#include <pthread.h>


namespace log_ns
{
    enum
    {
        DEBUG = 1,
        INFO,
        WARNING,
        ERROR,
        FATAL
    };

    std::string LevelToString(int level)
    {
        switch (level)
        {
        case DEBUG:
            return "DEBUG";
        case INFO:
            return "INFO";
        case WARNING:
            return "WARNING";
        case ERROR:
            return "ERROR";
        case FATAL:
            return "FATAL";
        default:
            return "UNKNOW";
        }
    }

    std::string GetCurrTime()
    {
        time_t now = time(nullptr);
        struct tm *curr_time = localtime(&now);
        char buffer[128];
        snprintf(buffer, sizeof(buffer), "%d-%02d-%02d %02d:%02d:%02d",
                curr_time->tm_year + 1900,
                curr_time->tm_mon + 1,
                curr_time->tm_mday,
                curr_time->tm_hour,
                curr_time->tm_min,
                curr_time->tm_sec);
        return buffer;
    }

    class logmessage
    {
    public:
        std::string _level;
        pid_t _id;
        std::string _filename;
        int _filenumber;
        std::string _curr_time;
        std::string _message_info;
    };

    #define SCREEN_TYPE 1
    #define FILE_TYPE 2

    const std::string glogfile = "./log.txt";
    pthread_mutex_t glock = PTHREAD_MUTEX_INITIALIZER;

    class Log
    {
    public:
        Log(const std::string &logfile = glogfile) : _logfile(logfile), _type(SCREEN_TYPE)
        {
        }
        void Enable(int type)
        {
            _type = type;
        }

        void FlushLogToScreen(const logmessage &lg)
        {
            printf("[%s][%d][%s][%d][%s] %s",
                lg._level.c_str(),
                lg._id,
                lg._filename.c_str(),
                lg._filenumber,
                lg._curr_time.c_str(),
                lg._message_info.c_str());
        }

        void FlushLogToFile(const logmessage &lg)
        {
            std::ofstream out(_logfile, std::ios::app);
            if (!out.is_open())
                return;
            char logtxt[2048];
            snprintf(logtxt, sizeof(logtxt), "[%s][%d][%s][%d][%s] %s",
                    lg._level.c_str(),
                    lg._id,
                    lg._filename.c_str(),
                    lg._filenumber,
                    lg._curr_time.c_str(),
                    lg._message_info.c_str());
            out.write(logtxt, strlen(logtxt));
            out.close();
        }

        void FlushLog(const logmessage &lg)
        {
            pthread_mutex_lock(&glock);
            switch (_type)
            {
            case SCREEN_TYPE:
                FlushLogToScreen(lg);
                break;
            case FILE_TYPE:
                FlushLogToFile(lg);
                break;
            }
            pthread_mutex_unlock(&glock);
        }

        void logMessage(std::string filename, int filenumber, int level, const char *format, ...)
        {
            logmessage lg;

            lg._level = LevelToString(level);
            lg._id = getpid();
            lg._filename = filename;
            lg._filenumber = filenumber;
            lg._curr_time = GetCurrTime();

            va_list ap;
            va_start(ap, format);
            char log_info[1024];
            vsnprintf(log_info, sizeof(log_info), format, ap);
            va_end(ap);
            lg._message_info = log_info;

            // 打印出日志
            FlushLog(lg);
        }
        ~Log()
        {
        }

    private:
        int _type;
        std::string _logfile;
    };

    Log lg;

    #define LOG(level, Format, ...) do {lg.logMessage(__FILE__, __LINE__, level, Format, ##__VA_ARGS__); }while (0)
    #define EnableScreen() do {lg.Enable(SCREEN_TYPE);}while(0)
    #define EnableFile() do {lg.Enable(FILE_TYPE);}while(0)
}

socket套接字封装类

cpp 复制代码
#pragma once

#include <iostream>
#include <cstring>
#include <functional>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <pthread.h>
#include <memory>

#include "Log.hpp"
#include "InetAddr.hpp"

namespace socket_ns
{
    using namespace log_ns;
    class Socket;
    using SockSPtr = std::shared_ptr<Socket>;

    enum
    {
        SOCKET_ERROR = 1,
        BIND_ERROR,
        LISTEN_ERROR
    };
    const static int gbacklog = 8;
    // 模板方法模式
    class Socket
    {
    public:
        virtual void CreateSocketOrDie() = 0;
        virtual void CreateBindOrDie(uint16_t port) = 0;
        virtual void CreateListenOrDie(int backlog = gbacklog) = 0;
        virtual SockSPtr Accepter(InetAddr *cliaddr) = 0;
        virtual bool Conntecor(const std::string &peerip, uint16_t peerport) = 0;
        virtual int Sockfd() = 0;
        virtual void Close() = 0;

        virtual ssize_t Recv(std::string *out) = 0;
        virtual ssize_t Send(const std::string &in) = 0;

    public:
        void BuildListenSocket(uint16_t port)
        {
            CreateSocketOrDie();
            CreateBindOrDie(port);
            CreateListenOrDie();
        }

        bool BuildClientSocket(const std::string &peerip, uint16_t peerport)
        {
            CreateSocketOrDie();
            return Conntecor(peerip, peerport);
        }
    };

    class TcpSocket : public Socket
    {
    public:
        TcpSocket()
        {
        }
        TcpSocket(int sockfd) : _sockfd(sockfd)
        {
        }
        ~TcpSocket()
        {
        }
        void CreateSocketOrDie() override
        {
            // 1.创建socket
            _sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
            if (_sockfd < 0)
            {
                LOG(FATAL, "sockfd create error\n");
                exit(SOCKET_ERROR);
            }
            LOG(INFO, "listensockfd create success, fd: %d\n", _sockfd);
        }
        void CreateBindOrDie(uint16_t port) override
        {
            struct sockaddr_in local;
            memset(&local, 0, sizeof(local));
            local.sin_family = AF_INET;
            local.sin_port = htons(port);
            local.sin_addr.s_addr = INADDR_ANY;

            // 2.bind _listensockfd 和 Socket addr
            if (::bind(_sockfd, (struct sockaddr *)&local, sizeof(local)) < 0)
            {
                LOG(FATAL, "bind error\n");
                exit(BIND_ERROR);
            }
            LOG(INFO, "bind success, sockfd: %d\n", _sockfd);
        }
        void CreateListenOrDie(int backlog) override
        {
            // 3.因为tcp是面向连接的,tcp需要未来不断地能够做到获取连接
            if (::listen(_sockfd, gbacklog) < 0)
            {
                LOG(FATAL, "listen error\n");
                exit(LISTEN_ERROR);
            }
            LOG(INFO, "listen success\n");
        }
        SockSPtr Accepter(InetAddr *cliaddr) override
        {
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            // 4. 获取连接
            int sockfd = ::accept(_sockfd, (struct sockaddr *)&client, &len);
            if (sockfd < 0)
            {
                LOG(WARNING, "accept error\n");
                return nullptr;
            }
            *cliaddr = InetAddr(client);
            LOG(INFO, "get a new link, client info: %s, sockfd is : %d\n", cliaddr->AddrStr().c_str(), sockfd);
            return std::make_shared<TcpSocket>(sockfd); // C++14
        }
        bool Conntecor(const std::string &peerip, uint16_t peerport) override
        {
            struct sockaddr_in server;
            memset(&server, 0, sizeof(server));
            server.sin_family = AF_INET;
            server.sin_port = htons(peerport);
            ::inet_pton(AF_INET, peerip.c_str(), &server.sin_addr);

            int n = ::connect(_sockfd, (struct sockaddr *)&server, sizeof(server));
            if (n < 0)
            {
                return false;
            }
            return true;
        }
        int Sockfd()
        {
            return _sockfd;
        }
        void Close()
        {
            if (_sockfd > 0)
            {
                ::close(_sockfd);
            }
        }
        ssize_t Recv(std::string *out) override
        {
            char inbuffer[4096];
            ssize_t n = ::recv(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0);
            if (n > 0)
            {
                inbuffer[n] = 0;
                *out += inbuffer;
            }
            return n;
        }
        ssize_t Send(const std::string &in) override
        {
            return ::send(_sockfd, in.c_str(), in.size(), 0);
        }

    private:
        int _sockfd; // 可以是listensock, 也可以是普通socketfd
    };

}

这个我在网络版本计算器这篇文章中已经讲得很清楚了,这里我就不缀叙了。

TcpServer类

cpp 复制代码
#pragma once
#include <functional>
#include "Socket.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"

using namespace socket_ns;

static const int gport = 8888;

using service_io_t = std::function<std::string(std::string &requeststr)>;

class TcpServer
{
public:
    TcpServer(service_io_t service, uint16_t port = gport)
        : _port(port), _listensock(std::make_shared<TcpSocket>()), _isrunning(false), _service(service)
    {
        _listensock->BuildListenSocket(_port);
    }

    class ThreadData
    {
    public:
        SockSPtr _sockfd;
        TcpServer *_self;
        InetAddr _addr;

    public:
        ThreadData(SockSPtr sockfd, TcpServer *self, const InetAddr &addr)
            : _sockfd(sockfd), _self(self), _addr(addr)
        {
        }
    };

    void Loop()
    {
        _isrunning = true;
        while (_isrunning)
        {
            InetAddr client;
            SockSPtr newsock = _listensock->Accepter(&client);
            if (newsock == nullptr)
                continue;
            LOG(INFO, "get a new link, client info : %s, sockfd is : %d\n", client.AddrStr().c_str(), newsock->Sockfd());

            // version 2 --- 多线程版本 --- 不能关闭fd了,也不需要了
            pthread_t tid;
            ThreadData *td = new ThreadData(newsock, this, client);
            pthread_create(&tid, nullptr, Execute, td); // 新线程进行分离
        }
        _isrunning = false;
    }
    static void *Execute(void *args)
    {
        pthread_detach(pthread_self());
        ThreadData *td = static_cast<ThreadData *>(args);

        std::string requeststr;
        ssize_t n = td->_sockfd->Recv(&requeststr);
        if (n > 0)
        {
            std::string responsestr = td->_self->_service(requeststr);
            td->_sockfd->Send(responsestr);
        }
        td->_sockfd->Close();
        delete td;
        return nullptr;
    }

    ~TcpServer()
    {
    }

private:
    uint16_t _port;
    SockSPtr _listensock;
    bool _isrunning;
    service_io_t _service;
};

这个我也是在网络版本计算器这篇文章中已经讲得很清楚了,我们这里都是复用,这就是范式编程的好处。以及我们之前的各种封装本质上都是一劳永逸的做法。

接下来的就是我们本次的重点了。

Http服务器类

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <vector>
#include <sstream>
#include <functional>
#include <fstream>
#include <unordered_map>

const static std::string base_sep = "\r\n";
const static std::string line_sep = ": ";
const static std::string prefixpath = "wwwroot"; // web根目录
const static std::string homepage = "index.html";
const static std::string httpversion = "HTTP/1.0";
const static std::string spacesep = " ";
const static std::string suffixsep = ".";
const static std::string html_404 = "404.html";
const static std::string arg_sep = "?";

class HttpRequest
{
private:
    //\r\n
    //\r\ndata
    std::string GetLine(std::string &reqstr)
    {
        auto pos = reqstr.find(base_sep);
        if (pos == std::string::npos)
            return std::string();
        std::string line = reqstr.substr(0, pos);
        reqstr.erase(0, line.size() + base_sep.size());
        return line.empty() ? base_sep : line;
    }
    void ParseReqLine()
    {
        std::stringstream ss(_req_line); // cin>>
        ss >> _method >> _url >> _version;

        if (strcasecmp(_method.c_str(), "GET") == 0)
        {
            auto pos = _url.find(arg_sep);
            if (pos != std::string::npos)
            {
                _body_text = _url.substr(pos + arg_sep.size());
                _url.resize(pos);
            }
        }

        _path += _url;

        if (_path[_path.size() - 1] == '/')
        {
            _path += homepage; // 如果用户没有指定资源路径名,那么我们就自动将首页信息返回
        }

        // wwwroot/index.html
        // wwwroot/image/1.png
        auto pos = _path.rfind(suffixsep);
        if (pos != std::string::npos)
        {
            _suffix = _path.substr(pos);
        }
        else
        {
            _suffix = " .default";
        }
    }
    void ParseReqHeader()
    {
        for (auto &header : _req_headers)
        {
            auto pos = header.find(line_sep);
            if (pos == std::string::npos)
                continue;
            std::string k = header.substr(0, pos);
            std::string v = header.substr(pos + line_sep.size());
            if (k.empty() || v.empty())
                continue;
            _headers_kv.insert(std::make_pair(k, v));
        }
    }

public:
    HttpRequest() : _blank_line(base_sep), _path(prefixpath)
    {
    }
    void Deserialize(std::string &reqstr)
    {
        // 基本的反序列化
        _req_line = GetLine(reqstr);
        std::string header;
        do
        {
            header = GetLine(reqstr);
            if (header.empty())
                break;
            else if (header == base_sep)
                break;
            _req_headers.push_back(header);
        } while (true);

        if (!reqstr.empty())
        {
            _body_text = reqstr;
        }

        // 在进一步反序列化
        ParseReqLine();
        ParseReqHeader();
    }

    std::string Url()
    {
        LOG(DEBUG, "Client Want url %s\n", _url.c_str());
        return _url;
    }
    std::string Path()
    {
        LOG(DEBUG, "Client Want path %s\n", _path.c_str());
        return _path;
    }
    std::string Suffix()
    {
        return _suffix;
    }
    std::string Method()
    {
        LOG(DEBUG, "Client request method is %s\n", _method.c_str());
        return _method;
    }
    std::string GetRequestBody()
    {
        LOG(DEBUG, "Client request method is %s, args: %s, request path: %s\n", _method.c_str(), _body_text.c_str(), _path.c_str());
        return _body_text;
    }
    void Print()
    {
        std::cout << "------------------" << std::endl;
        std::cout << "###" << _req_line << std::endl;
        for (auto &header : _req_headers)
        {
            std::cout << "@@@" << header << std::endl;
        }
        std::cout << "***" << _blank_line;
        std::cout << ">>>" << _body_text << std::endl;

        std::cout << "Method: " << _method << std::endl;
        std::cout << "Url: " << _url << std::endl;
        std::cout << "Version: " << _version << std::endl;

        for (auto &headers_kv : _headers_kv)
        {
            std::cout << ")))" << headers_kv.first << "->" << headers_kv.second << std::endl;
        }
    }
    ~HttpRequest()
    {
    }

private:
    // 基本的httprequest的格式
    std::string _req_line;
    std::vector<std::string> _req_headers;
    std::string _blank_line;
    std::string _body_text;

    // 更具体的属性字段,需要进一步反序列化
    std::string _method;
    std::string _url;
    std::string _path;
    std::string _suffix; // 资源后缀
    std::string _version;
    std::unordered_map<std::string, std::string> _headers_kv;
};

class HttpResponse
{
public:
    HttpResponse() : _version(httpversion), _blank_line(base_sep)
    {
    }
    void AddCode(int code, const std::string &desc)
    {
        _status_code = code;
        _desc = desc;
    }
    void AddHeader(const std::string &k, const std::string &v)
    {
        _headers_kv[k] = v;
    }
    void AddBodyText(const std::string &body_text)
    {
        _resp_body_text = body_text;
    }
    std::string Serialize()
    {
        // 1.构建状态行
        _status_line = _version + spacesep + std::to_string(_status_code) + spacesep + _desc + base_sep;

        // 2.构建应答报头
        for (auto &header : _headers_kv)
        {
            std::string header_line = header.first + line_sep + header.second + base_sep;
            _resp_headers.push_back(header_line);
        }

        // 3.空行和正文

        // 4.正式序列化
        std::string responsestr = _status_line;
        for (auto &line : _resp_headers)
        {
            responsestr += line;
        }
        responsestr += _blank_line;
        responsestr += _resp_body_text;

        return responsestr;
    }
    ~HttpResponse()
    {
    }

private:
    // httpresponse 属性
    std::string _version;
    int _status_code;
    std::string _desc;
    std::unordered_map<std::string, std::string> _headers_kv;

    // 基本的httpresponse的格式
    std::string _status_line;
    std::vector<std::string> _resp_headers;
    std::string _blank_line;
    std::string _resp_body_text;
};

using func_t = std::function<HttpResponse(HttpRequest &)>;

class HttpServer
{
private:
    std::string GetFileContent(const std::string &path)
    {
        std::ifstream in(path, std::ios::binary);
        if (!in.is_open())
            return std::string();
        in.seekg(0, in.end);
        int filesize = in.tellg(); // 告知我你的rw偏移量是多少
        in.seekg(0, in.beg);

        std::string content;
        content.resize(filesize);
        in.read((char *)content.c_str(), filesize);
        in.close();

        return content;
    }

public:
    HttpServer()
    {
        _mime_type.insert(std::make_pair(".html", "text/html"));
        _mime_type.insert(std::make_pair(".jpg", "image/jpeg"));
        _mime_type.insert(std::make_pair(".png", "image/png"));
        _mime_type.insert(std::make_pair(".default", "text/html"));

        _code_to_desc.insert(std::make_pair(100, "Continue"));
        _code_to_desc.insert(std::make_pair(200, "OK"));
        _code_to_desc.insert(std::make_pair(201, "Created"));
        _code_to_desc.insert(std::make_pair(301, "Moved Permanently"));
        _code_to_desc.insert(std::make_pair(302, "Found"));
        _code_to_desc.insert(std::make_pair(404, "Not Found"));
    }
    std::string HandlerHttpRequest(std::string &reqstr) // req 曾经被客户端序列化过
    {
#ifdef TEST
        std::cout << "------------------------------" << std::endl;
        std::cout << reqstr;

        std::string responsestr = "HTTP/1.1 200 OK\r\n";
        responsestr += "Content-Type: text/html\r\n";
        responsestr += "\r\n";
        responsestr += "<html><h1>hello Linux, hello World!</h1></html>";

        return responsestr;
#else
        std::cout << "------------------------------" << std::endl;
        std::cout << reqstr;
        std::cout << "------------------------------" << std::endl;

        HttpRequest req;
        HttpResponse resp;
        req.Deserialize(reqstr);
        // req.Method();

        // 最基本的上层处理
        if (req.Path() == "wwwroot/redir")
        {
            // 处理重定向
            std::string redir_path = "https://www.baidu.com";
            // resp.AddCode(302, _code_to_desc[302]);
            resp.AddCode(301, _code_to_desc[301]);
            resp.AddHeader("Location", redir_path);
        }
        else if (!req.GetRequestBody().empty())
        {
            if (IsServiceExists(req.Path()))
            {
                resp = _service_list[req.Path()](req);
            }
        }
        else
        {
            // 最基本的上层处理,处理静态资源
            std::string content = GetFileContent(req.Path());
            if (content.empty())
            {
                content = GetFileContent("wwwroot/404.html");
                resp.AddCode(404, _code_to_desc[404]);
                resp.AddHeader("Content-Length", std::to_string(content.size()));
                resp.AddHeader("Content-Type", _mime_type[".html"]);
                resp.AddBodyText(content);
            }
            else
            {
                resp.AddCode(200, _code_to_desc[200]);
                resp.AddHeader("Content-Length", std::to_string(content.size()));
                resp.AddHeader("Content-Type", _mime_type[req.Suffix()]);
                resp.AddHeader("Set-Cookie", "username=zhangsan");
                // resp.AddHeader("Set-Cookie", "passwd=12345");
                resp.AddBodyText(content);
            }
        }

        return resp.Serialize();
#endif
    }
    void InsertService(const std::string &servicename, func_t f)
    {
        std::string s = prefixpath + servicename;
        _service_list[s] = f;
    }
    bool IsServiceExists(const std::string &servicename)
    {
        auto iter = _service_list.find(servicename);
        if (iter == _service_list.end())
            return false;
        else
            return true;
    }
    ~HttpServer() {}

private:
    std::unordered_map<std::string, std::string> _mime_type;
    std::unordered_map<int, std::string> _code_to_desc;
    std::unordered_map<std::string, func_t> _service_list;
};

我们可以看到这块内容是相当多的,但是不用害怕,我一点一点来详细讲解。

首先是我们要引入的头文件

我逐个说明这些语句 / 头文件的作用,一句话讲清楚:

  1. #pragma once:预处理指令,用于防止当前头文件被重复包含(避免重复定义的编译错误)。
  2. #include <iostream>:提供标准输入 / 输出流功能,比如用cout输出内容、cin读取输入。
  3. #include <string>:提供std::string类,用于处理字符串(支持拼接、截取、比较等操作)。
  4. #include <vector>:提供std::vector容器,是动态可变长度的数组,支持自动扩容、随机访问元素。
  5. #include <sstream>:提供字符串流(如std::stringstream),用于字符串与其他数据类型的转换(比如把整数转成字符串)。
  6. #include <functional>:提供函数对象、绑定器等工具,比如std::function(包装函数 /lambda)、std::bind(绑定函数参数)。
  7. #include <fstream>:提供文件流功能,用于读写本地文件(比如ifstream读文件、ofstream写文件)。
  8. #include <unordered_map>:提供std::unordered_map容器,是无序键值对哈希表,平均 O (1) 时间复杂度完成查找、插入。

这些是 HTTP 服务器相关的常量定义,结合 Web 场景一句话说明每个的作用:

  1. base_sep = "\r\n":HTTP 协议的基础换行分隔符(比如 HTTP 请求 / 响应头的行与行之间用\r\n分隔)。
  2. line_sep = ": ":HTTP 请求 / 响应头中 "字段名 - 字段值" 的分隔符(比如Content-Type: text/html里的: )。
  3. prefixpath = "wwwroot":网站静态资源的根目录路径(存放 HTML、CSS 等文件的文件夹)。
  4. homepage = "index.html":网站的默认首页文件名(用户访问根路径时自动加载的文件)。
  5. httpversion = "HTTP/1.0":当前服务器所遵循的 HTTP 协议版本。
  6. spacesep = " ":HTTP 请求行的部分分隔符(比如GET /index.html HTTP/1.0中各部分用空格分隔)。
  7. suffixsep = ".":文件名与扩展名的分隔符(比如区分index.html中的 "index" 和 "html")。
  8. html_404 = "404.html":资源不存在时返回的 "404 错误页面" 文件名。
  9. arg_sep = "?":URL 中 "路径" 与 "查询参数" 的分隔符(比如/index.html?name=test中?)。

HttpRequest类的编写

我们先来研究研究http请求该如何来编写。

我们先来看看私有成员变量:

第一部分:基本 HTTP 请求格式相关

  1. std::string _req_line;:存储 HTTP 请求的请求行 (比如GET /index.html HTTP/1.0)。
  2. std::vector<std::string> _req_headers;:存储未解析的原始请求头列表(每行是一条完整的头信息)。
  3. std::string _blank_line;:存储请求头与请求体之间的空行 (对应 HTTP 协议的\r\n分隔)。
  4. std::string _body_text;:存储 HTTP 请求的请求体内容(比如 POST 提交的参数、GET 的查询参数)。

第二部分:解析后的具体属性字段

  1. std::string _method;:存储解析后的 HTTP请求方法(比如 GET、POST)。
  2. std::string _url;:存储解析后的请求URL 路径 (比如/index.html)。
  3. std::string _path;:存储资源在服务器本地的实际路径 (比如wwwroot/index.html)。
  4. std::string _suffix;:存储资源的后缀名 (比如.html,用于识别文件类型)。
  5. std::string _version;:存储解析后的 HTTP协议版本 (比如HTTP/1.0)。
  6. std::unordered_map<std::string, std::string> _headers_kv;:存储解析后的请求头键值对 (比如Host对应localhost)。

这是一个用于拆分 HTTP 请求字符串的工具函数,属于 HTTP 服务器中请求解析模块的一部分。以下是其核心逻辑与作用解析:

1. 函数功能

从传入的 HTTP 请求字符串(reqstr)中,按 HTTP 协议的换行分隔符(base_sep,即\r\n)提取一行内容,并从原字符串中移除已提取的部分,实现请求内容的逐行解析。

2. 关键逻辑解析

  • 查找分隔符 :通过reqstr.find(base_sep)定位\r\n在请求字符串中的位置,作为当前行的结束标识;
  • 异常处理 :若未找到\r\npos == string::npos),返回空字符串,表示无法提取完整行;
  • 提取当前行 :用substr(0, pos)截取从字符串开头到\r\n的内容,作为当前行;
  • 清理已处理内容 :调用reqstr.erase删除已提取的行及后续的\r\n,避免重复解析;
  • 结果处理 :若提取的行是空内容,返回base_sep\r\n),否则返回提取的行。

3. 实际用途

因此该函数是 HTTP 请求解析的基础工具,通常用于:

  • 拆分 HTTP 请求的请求行 (如GET /index.html HTTP/1.0);
  • 拆分 HTTP 请求的请求头列表 (如Host: localhost等行);
  • 实现请求内容的 "逐行读取",为后续解析请求方法、URL、请求头键值对等逻辑提供基础。

这是 HTTP 请求解析模块中的ParseReqLine函数,核心功能是解析 HTTP 请求行,提取关键信息并构建服务器本地资源路径,是连接请求字符串与服务器资源处理的核心逻辑。其具体逻辑与作用如下:

1. 拆分请求行

  • 通过std::stringstream将请求行(如GET /index.html?name=test HTTP/1.0)按空格拆分为:
    • _method:HTTP 请求方法(如GET);
    • _url:请求的 URL 路径(如/index.html?name=test);
    • _version:HTTP 协议版本(如HTTP/1.0)。

2. 处理 GET 请求的查询参数

  • 通过strcasecmp判断请求方法为GET时,查找 URL 中 "?"(arg_sep)的位置:
    • 若存在 "?",将其后方内容存入_body_text(作为 GET 请求的参数);
    • 截断 URL,仅保留 "?" 前的路径部分。

3. 构建服务器本地资源路径

  • 将 URL 拼接至服务器根路径(_path初始为wwwroot);
  • 若路径以 "/" 结尾(如用户访问/),自动拼接默认首页(homepage,如index.html),确保能定位到具体资源文件(最终路径如wwwroot/index.html)。

4. 提取资源后缀

  • 通过rfind查找路径中 "."(suffixsep)的位置,提取其后内容作为资源后缀(_suffix,如.html),用于后续识别文件的 MIME 类型;
  • 若未找到后缀,默认设为.default

该函数的处理结果直接决定了服务器后续资源定位 (通过_path)、资源类型识别 (通过_suffix)、请求参数处理 (通过_body_text)的逻辑,是 HTTP 服务器请求解析流程中的关键步骤。

这是 HTTP 请求解析模块中的ParseReqHeader函数,核心功能是将原始请求头列表解析为键值对哈希表,便于后续快速查询请求头信息。其具体逻辑与作用如下:

1. 核心功能

将未解析的原始请求头(如Host: localhostUser-Agent: curl/7.68.0)转换为键-值结构,存入_headers_kv哈希表,实现请求头信息的快速检索。

2. 解析逻辑步骤

  1. 遍历原始请求头列表 :循环处理_req_headers中的每一条请求头;
  2. 定位分隔符 :查找请求头中 ":"(line_sep,HTTP 请求头的键值分隔符)的位置;
  3. 异常过滤:若未找到分隔符,跳过当前请求头;
  4. 拆分键值对 :通过分隔符将请求头拆分为键k(如Host)和值v(如localhost);
  5. 过滤无效数据:若键或值为空,跳过当前请求头;
  6. 存入哈希表 :将有效的键值对插入_headers_kv,完成解析。

3. 实际作用

解析后的_headers_kv哈希表,可支持后续服务器逻辑快速获取请求头信息 (如获取Content-Length判断请求体大小、获取Host确定目标域名),检索时间复杂度为 O (1),是 HTTP 请求处理流程中 "结构化解析" 的关键步骤之一。

这是HttpRequest类的构造函数, 通过初始化列表提前设置_blank_line(协议分隔符\r\n)和_path(资源根路径),确保HttpRequest对象创建时就具备 HTTP 解析所需的基础配置,为后续的请求行、请求头解析流程提供必要的初始值。

这是HttpRequest类的反序列化入口函数,核心功能是将无结构的原始 HTTP 请求字符串,转换为类内的结构化成员变量,是连接客户端原始请求数据与服务器后续处理逻辑的核心环节。其流程与作用如下:

1. 流程:分层完成反序列化

(1)基本反序列化:提取请求的基础结构

  • 提取请求行 :通过GetLine从原始请求字符串reqstr中提取第一行,赋值给_req_line(对应 HTTP 请求的 "请求行" 部分);
  • 提取请求头 :通过do-while循环持续调用GetLine提取内容,将非空、非分隔符的行存入_req_headers(原始请求头列表);当遇到空行或base_sep\r\n)时终止循环(对应 HTTP 协议中 "请求头与请求体的分隔规则");
  • 提取请求体 :若reqstr剩余内容非空,将其赋值给_body_text(对应 HTTP 请求的 "请求体" 部分)。

(2)进一步反序列化:解析为结构化字段

  • 调用ParseReqLine:解析_req_line,提取请求方法、URL、协议版本等信息,并构建服务器本地的资源路径;
  • 调用ParseReqHeader:解析_req_headers,将原始请求头转换为键值对哈希表,便于后续快速查询。

2. 作用

该函数实现了 "原始请求字符串→基础结构→结构化字段" 的分层解析逻辑,将无结构的 HTTP 请求数据,转化为程序可直接使用的结构化信息(如请求方法、资源路径、请求头键值对),为后续服务器的资源定位、响应构建等流程提供了可直接调用的数据基础。

这些是HttpRequest类的数据访问函数,核心作用是对外提供解析后的 HTTP 请求信息(部分含调试日志):

  1. Url():打印客户端请求 URL 的调试日志,返回解析后的请求 URL。
  2. Path():打印客户端请求资源本地路径的调试日志,返回服务器本地的资源路径。
  3. Suffix():直接返回解析后的资源后缀名。
  4. Method():打印客户端请求方法的调试日志,返回解析后的 HTTP 请求方法。
  5. GetRequestBody():打印包含请求方法、参数、资源路径的调试日志,返回请求体(或 GET 请求的查询参数)内容。

这是HttpRequest类的调试辅助函数,核心作用是将解析后的 HTTP 请求信息(含原始结构与结构化字段)格式化输出到控制台,便于开发阶段验证请求解析的正确性。其打印内容分为以下几类:

  1. 原始请求结构

    • 打印分隔线与原始请求行(标记为 "###");
    • 遍历打印未解析的原始请求头列表(标记为 "@@@");
    • 打印请求头与请求体的分隔空行(标记为 "***")及请求体内容(标记为 ">>>")。
  2. 解析后的结构化字段

    • 打印请求方法、URL、HTTP 协议版本;
    • 遍历打印请求头的键值对(标记为 ")))",格式为 "键 -> 值")。

该函数的设计目的是让我们快速直观地核对 "原始请求" 与 "解析结果" 是否一致,是 HTTP 服务器开发中常用的调试工具。

HttpResponse类的编写

接下来我们来研究http应答该怎么写:

这些是HttpResponse类的私有成员变量,分为 "响应核心属性" 和 "响应格式载体" 两类,支撑 HTTP 响应的构建与序列化:

一、HTTP 响应核心属性(配置响应的关键信息)

  1. std::string _version;:存储响应遵循的 HTTP 协议版本(如HTTP/1.0),是响应状态行的组成部分。
  2. int _status_code;:存储响应的状态码(如 200、404),标识请求的处理结果。
  3. std::string _desc;:存储状态码对应的描述(如 "OK""Not Found"),与状态码配合构成状态行的提示信息。
  4. std::unordered_map<std::string, std::string> _headers_kv;:存储响应头的键值对(如Content-Type: text/html),是构建响应头的基础数据。

二、HTTP 响应格式载体(组装响应字符串的结构)

  1. std::string _status_line;:存储组装完成的响应状态行(如HTTP/1.0 200 OK\r\n),是响应的首行内容。
  2. std::vector<std::string> _resp_headers;:存储格式化后的响应头列表(每条为完整的头信息行),用于后续拼接响应字符串。
  3. std::string _blank_line;:存储响应头与响应体之间的空行分隔符(\r\n),符合 HTTP 协议的格式规范。
  4. std::string _resp_body_text;:存储响应的正文内容(如 HTML 页面、数据),是客户端接收的实际内容。

这是HttpResponse类的构造函数,该构造函数的作用是在HttpResponse对象创建时,提前配置好响应的协议版本与格式分隔符,为后续构建状态行、响应头、响应体的流程奠定基础。

这是HttpResponse类的响应状态配置函数,作用是设置 HTTP 响应的状态信息:

  • 接收两个参数:code(HTTP 状态码,如 200、404)和desc(状态码对应的描述,如 "OK""Not Found");
  • 将参数分别赋值给类的私有成员_status_code_desc,为后续构建响应状态行提供核心数据。

该函数是配置 HTTP 响应结果的基础接口,状态码与描述的组合直接决定了客户端对请求结果的判定(如成功、资源不存在等)。

这是HttpResponse类的响应头配置函数,核心作用是向响应中添加 HTTP 响应头的键值对:

  • 接收两个参数:k(响应头字段名,如Content-Type)和v(对应字段值,如text/html);
  • 将键值对存入_headers_kv哈希表,为后续序列化时构建符合 HTTP 规范的响应头提供数据基础。

响应头是 HTTP 响应的关键组成部分,其配置直接影响客户端对响应内容的解析逻辑(如资源类型、内容长度等),该函数是构建合法 HTTP 响应的基础接口之一。

这是HttpResponse类的响应体配置函数,核心作用是设置 HTTP 响应的正文内容:

  • 接收参数body_text(响应体的实际内容,如 HTML 页面、数据文本等);
  • 将其赋值给类的私有成员_resp_body_text,为后续序列化时拼接完整的 HTTP 响应提供正文数据。

响应体是 HTTP 响应中客户端实际接收并展示的核心内容,该函数是构建包含有效内容的 HTTP 响应的关键接口之一。

这是HttpResponse类的HTTP 响应序列化函数,核心作用是遵循 HTTP 协议格式,将类内的结构化响应信息(状态、响应头、响应体)组装为可直接发送给客户端的完整响应字符串。其流程与对应 HTTP 规范如下:

1. 构建状态行(符合 HTTP 状态行格式)

按 "HTTP版本 状态码 描述\r\n" 的规范,拼接_version(协议版本)、spacesep(空格)、状态码(转字符串)、状态描述,最后追加base_sep\r\n),生成响应的首行内容(如HTTP/1.0 200 OK\r\n)。

2. 构建响应头列表(符合 HTTP 响应头格式)

遍历_headers_kv中的键值对,按 "键: 值\r\n" 的规范,拼接每个字段的完整行,存入_resp_headers向量(如Content-Type: text/html\r\n),为后续拼接做准备。

3. 组装完整响应字符串

  • 先将状态行存入结果字符串responsestr
  • 追加所有格式化后的响应头行;
  • 追加_blank_line\r\n,对应 HTTP 协议中 "响应头与响应体的空行分隔规则");
  • 最后追加响应体_resp_body_text,形成符合 HTTP 规范的完整响应内容。

该函数是 HTTP 服务器向客户端返回结果的关键环节,其输出的字符串是客户端能直接解析的标准 HTTP 响应格式。

HttpServer类的编写

接下来就是将两者结合起来了

这是 C++ 中的类型别名定义,核心作用是统一 "HTTP 请求处理器" 的函数接口类型:

  1. 类型定义内容usingstd::function定义别名func_t,其函数签名为:接收HttpRequest&(HTTP 请求对象的引用)作为参数,返回HttpResponse(HTTP 响应对象)。

  2. 实际用途 在 HTTP 服务器开发中,func_t通常作为请求处理函数的统一接口类型------ 用于规范 "处理请求、生成响应" 的函数格式,方便管理不同的请求处理器(如路由对应的业务处理函数),实现请求分发逻辑与具体业务处理的解耦,提升代码的灵活性与可维护性。

这些是 HTTP 服务器核心类的私有成员变量,分别承担 "资源类型映射""状态码配置""请求路由管理" 的功能,是服务器处理请求、构建响应的基础数据容器:

1. std::unordered_map<std::string, std::string> _mime_type

  • 作用 :存储 "资源后缀名→MIME 类型" 的映射关系(如.html对应text/html.jpg对应image/jpeg)。
  • 意义 :HTTP 响应需通过Content-Type头告知客户端资源类型,该映射可根据资源后缀快速匹配对应的 MIME 类型,确保响应头配置符合 HTTP 规范。

2. std::unordered_map<int, std::string> _code_to_desc

  • 作用 :存储 "HTTP 状态码→状态描述" 的映射关系(如200对应OK404对应Not Found)。
  • 意义:构建响应状态行时,可通过状态码快速获取对应的描述文本,避免硬编码,提升状态信息配置的灵活性与一致性。

3. std::unordered_map<std::string, func_t> _service_list

  • 作用 :存储 "请求路径→请求处理函数" 的路由映射(如路径/index.html对应处理该请求的func_t类型函数)。
  • 意义:实现 HTTP 服务器的路由分发逻辑 ------ 当收到请求时,可根据请求路径匹配对应的处理函数,完成 "请求→业务逻辑→响应" 的流程衔接。

这是一个文件内容读取函数,核心作用是按二进制模式读取指定路径的文件内容,并以字符串形式返回,通常用于 HTTP 服务器加载静态资源(如 HTML 页面、图片等)。其流程与设计特点如下:

1. 执行流程

  • 打开文件 :以二进制模式(std::ios::binary)打开指定路径的文件,避免文本模式下的换行符自动转换(确保非文本文件如图片的内容完整性);
  • 失败处理 :若文件打开失败(!in.is_open()),直接返回空字符串;
  • 获取文件大小 :通过seekg移动到文件末尾,用tellg获取当前位置(即文件字节数),再将文件指针移回开头;
  • 读取内容 :调整字符串content的大小为文件字节数,通过read函数将文件内容读取到字符串中;
  • 收尾:关闭文件,返回存储了文件内容的字符串。

2. 设计特点

  • 采用二进制模式读取,兼容文本文件与非文本文件(如图片、二进制数据);
  • 借助std::string存储文件内容(C++ 字符串支持任意字节数据),简化数据传递流程;
  • 包含基础的错误处理(文件打开失败时返回空),为上层逻辑提供错误标识。

构造函数我们用来初始化一些参数:

  1. _mime_type[".html"] = "text/html";:配置 html 文件的 MIME 类型;
  2. _mime_type[".jpg"] = "image/jpeg";:配置 jpg 图片的 MIME 类型;
  3. _mime_type[".png"] = "image/png";:配置 png 图片的 MIME 类型;
  4. _mime_type[".js"] = "application/x-javascript";:配置 js 文件的 MIME 类型;
  5. _mime_type[".css"] = "text/css";:配置 css 文件的 MIME 类型;
  6. _mime_type[".default"] = "text/html";:配置未知后缀文件的默认 MIME 类型;
  7. _code_to_desc[200] = "OK";:配置 200 状态码的描述文本;
  8. _code_to_desc[404] = "Not Found";:配置 404 状态码的描述文本;
  9. _code_to_desc[400] = "Bad Request";:配置 400 状态码的描述文本;
  10. _code_to_desc[500] = "Internal Server Error";:配置 500 状态码的描述文本;

我们接下来分三步来看这个HandlerHttpRequest函数

这是HandlerHttpRequest函数中TEST 模式下的请求处理逻辑 ,作用是在编译时定义了TEST宏的情况下,跳过复杂业务逻辑,直接返回固定 HTTP 响应,用于快速测试服务器通信:

核心逻辑

  1. 条件编译触发#ifdef TEST判定编译时存在TEST宏,执行该代码块;
  2. 调试输出 :打印分隔线与客户端请求字符串reqstr,便于测试时查看请求内容;
  3. 构建固定响应 :按 HTTP 协议格式拼接:
    • 状态行(HTTP/1.1 200 OK\r\n);
    • 响应头(Content-Type: text/html\r\n,指定响应体为 HTML 类型);
    • 空行(\r\n,分隔响应头与响应体);
    • 响应体(简单 HTML 页面,内容为hello Linux, hello World!);
  4. 返回响应:直接返回该固定响应字符串。

用途

此逻辑是 HTTP 服务器的测试辅助分支,用于快速验证服务器与客户端的基本通信链路是否正常,避免实际业务逻辑(如请求解析、资源读取)对测试的干扰。

这是HandlerHttpRequest函数在 ** 非 TEST 模式(正式运行模式)** 下的请求处理逻辑,是 HTTP 服务器的核心业务流程,负责解析请求、分场景处理并生成合规响应:

1. 基础流程

  • 调试输出:打印分隔线与客户端请求字符串,用于正式运行时查看请求内容;
  • 请求解析 :创建HttpRequestHttpResponse对象,调用Deserialize将请求字符串解析为结构化的req对象,为后续处理提供清晰的请求信息。

2. 分场景业务处理

(1)重定向场景

当请求路径为wwwroot/redir时:

  • 配置 301 永久重定向,设置状态码(301)与Location响应头(指向百度地址),引导客户端跳转到指定 URL。

(2)动态服务场景

当请求体非空且路径已注册服务时:

  • 调用IsServiceExists校验路径对应的服务是否存在;
  • 若存在,执行_service_list中注册的业务处理函数,由该函数生成响应(实现业务逻辑与路由的解耦)。

(3)静态资源场景

处理静态文件请求:

  • 调用GetFileContent读取请求路径对应的文件内容;
    • 若内容为空(文件不存在):读取 404 页面,设置 404 状态码、内容长度、MIME 类型(text/html),将 404 页面作为响应体;
    • 若内容非空:设置 200 状态码,补充内容长度、对应资源的 MIME 类型、Cookie(如username=zhangsan)等响应头,将文件内容作为响应体。

3. 响应返回

将构建完成的HttpResponse对象序列化为符合 HTTP 协议的字符串,返回给客户端。

此逻辑是 HTTP 服务器的正式业务处理分支,覆盖了重定向、动态业务接口、静态资源这三类常见 HTTP 请求场景,同时包含异常处理(资源不存在返回 404),保证了响应的合规性与服务的可用性。

这部分代码包含条件编译闭合指令与两个服务管理函数,是 HTTP 服务器动态业务路由的核心辅助逻辑:

1. #endif

这是条件编译的闭合指令,用于结束之前的#ifdef TEST代码块,明确区分 TEST 模式与正式运行模式的代码范围,保证编译时仅执行对应模式的逻辑。

2. InsertService(const std::string &servicename, func_t f)

  • 功能:注册 HTTP 请求对应的业务处理服务
  • 流程 :将传入的服务名拼接前缀路径prefixpath作为路由键,将处理函数ffunc_t类型)存入_service_list路由映射表
  • 作用:实现业务逻辑与路由的动态绑定,支持灵活扩展业务接口,解耦路由配置与具体业务代码

3. IsServiceExists(const std::string &servicename)

  • 功能:校验指定业务服务是否已注册
  • 流程 :在_service_list中查找传入的服务名,未找到则返回false,找到则返回true
  • 作用:在请求处理阶段提前校验服务存在性,避免调用不存在的业务函数,提升代码健壮性

ServerMain.cc服务器源文件

cpp 复制代码
#include "TcpServer.hpp"
#include "Http.hpp"

HttpResponse Login(HttpRequest &req)
{
    HttpResponse resp;
    std::cout << "外部已经拿到参数了: " << std::endl;
    req.GetRequestBody();
    std::cout << "######################" << std::endl;
    resp.AddCode(200, "OK");
    resp.AddBodyText("<html><h1>result done!</h1></html>");
    return resp;
}
// ./tcpserver 8888
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << " local-port" << std::endl;
        exit(0);
    }
    uint16_t port = std::stoi(argv[1]);

    // 我们的软件代码,我们手动的划分了三层

    HttpServer hserver;
    hserver.InsertService("/login", Login);
    std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(std::bind(&HttpServer::HandlerHttpRequest, &hserver, std::placeholders::_1), port);
    tsvr->Loop();

    return 0;
}

这段代码是简易 HTTP 服务器的主程序入口,核心作用是整合 TCP 底层网络通信与 HTTP 应用层处理逻辑,实现带登录业务接口的 HTTP 服务启动,具体可分为三部分:

  1. 基础依赖引入 :导入封装好的 TCP 服务器头文件(TcpServer.hpp)和 HTTP 核心组件头文件(Http.hpp),分别为网络通信、HTTP 请求 / 响应处理、路由管理提供底层支撑。
  2. 登录业务处理函数定义 :自定义Login函数作为 HTTP 业务处理接口,接收 HTTP 请求对象后,获取请求体(用于读取登录参数),并构建包含 200 成功状态码、简单 HTML 结果的 HTTP 响应返回。
  3. 主程序逻辑(main 函数)
    • 启动参数校验:要求运行程序时传入端口号(如./tcpserver 8888),否则提示正确用法并退出;
    • HTTP 服务初始化:创建HttpServer对象,将/login请求路径与Login业务函数绑定,完成路由注册;
    • TCP-HTTP 逻辑绑定:通过智能指针创建TcpServer对象,将HttpServer的请求处理核心函数绑定为 TCP 服务器的回调,打通 "TCP 接收客户端数据→HTTP 解析 / 处理请求" 的链路;
    • 启动服务循环:调用 TCP 服务器的Loop方法,让服务器持续监听指定端口,接收并处理客户端的 HTTP 请求。

核心特点

整体逻辑通过 "路由注册 + 回调绑定" 实现解耦:TCP 层负责纯网络通信,HTTP 层负责请求解析与业务分发,最终实现对/login路径的 HTTP 请求进行专属业务处理,并返回合规响应。

总结

  1. 核心目标是启动一个可处理/login请求的 HTTP 服务器,整合 TCP 网络层与 HTTP 应用层能力;
  2. 关键设计是动态注册业务接口(/login绑定Login函数),解耦路由配置与业务逻辑;
  3. 运行逻辑是监听指定端口,持续处理客户端 HTTP 请求,对/login路径返回登录业务响应。
相关推荐
dangdang___go3 小时前
文件操作2+程序的编译和链接(1)
java·服务器·前端
std860213 小时前
Linux 6.18发布:年度最后版本或成新长期支持版本
linux·运维·服务器
HalvmånEver3 小时前
Linux:进程替换(进程控制四)
linux·运维·服务器·学习·进程
一叶之秋14123 小时前
从零开始学Linux进程控制:fork、wait、exec 详解
linux·运维·服务器
thinkMoreAndDoMore4 小时前
linux系统服务管理框架
linux·运维·服务器
waves浪游4 小时前
进程控制(中)
linux·运维·服务器·开发语言·c++
满天星83035774 小时前
【Linux】信号(上)
linux·运维·服务器·开发语言·c++
科普瑞传感仪器4 小时前
航空航天领域青睐:复杂曲面机器人抛光为何必须采用六维力控?
运维·人工智能·机器人·自动化·无人机
CIb0la4 小时前
在 ARM CPU 上运行 x86 应用的开源项目:FEX
linux·运维·生活