应用层协议与序列化

一、应用层

我们程序员写的一个个解决我们实际问题,满足我们日常需求的网络程序,都是在应用层。

二、协议

协议是一种"约定".socketapi的接口,在读写数据时,都是按"字符串"的方式来发送接收的。

注意:协议就是双方约定好的结构化的数据。

三、TCP对相关报文的处理过程

四、网络版计算器的实现

例如,我们需要实现一个服务器版的加法器。我们需要客户端把要计算的两个加数发过去,然后由服务器进行计算,最后再把结果返回给客户端。

(1)约定方案一:

●客户端发送一个形如"1+2"的字符串;

这个字符串中有两个操作数,都是整形;

●两个数字之间会有一个字符是运算符,运算符只能是+;

●数字和运算符之间没有空格;

(2)约定方案二:

●定义结构体来表示我们需要交互的信息;

●发送数据时将这个结构体按照一个规则转换成字符串,接收到数据的时候再按照相同的规则把字

符串转化回结构体;

●这个过程叫做"序列化"和"反序列化"

五、序列化和反序列化

无论我们采用方案一,还是方案二,还是其他的方案,只要保证,一端发送时构造的数据,在另一

端能够正确的进行解析就行,这种双方约定,就是应用层协议。

我们希望在应用层的报文是下面这个样子的:

六、read、write、recv、send和tcp都支持全双工的原因

在任何一台主机上,TCP连接既有发送缓冲区,又有接受缓冲区,所以,在内核中,可以在发消

息的同时,也可以收消息,即全双工;这就是为什么一个TCPsockfd读写都是它的原因;实际数

据什么时候发,发多少,出错了怎么办,由TCP控制,所以TCP叫做传输控制协议。

七、网络版计算器(含JSON序列化和反序列化)的实现:

7.1服务端

协议的制定(Protocal.hpp文件):

cpp 复制代码
#pragma once

#include <iostream>
#include <stdbool.h>
#include <string>
#include <jsoncpp/json/json.h>

// 定义空格分隔符
const std::string blank_space_sep = " ";
// 定义协议分隔符
const std::string protocol_sep = "\n";

// 封装报文函数
// "x op y" -> "len"\n"x op y"\n
std::string Package(std::string &content)
{
    std::string package = std::to_string(content.size());
    package += protocol_sep;
    package += content;
    package += protocol_sep;

    return package;
}

// 报头报文分离
// "len"\n"x op y"\n -> "x op y"
bool Depacksge(std::string &package, std::string *content)
{
    size_t pos = package.find(protocol_sep);
    if (pos == std::string::npos)
    {
        return false;
    }

    // 截取全面的len
    std::string len_str = package.substr(0, pos);
    size_t len = std::stoi(len_str);

    // 判断整个字符串的长度是否正确,都则则不进行转换
    //  len(package) = lend_str + content_str +2
    size_t total_len = len_str.size() + len + 2;
    if (total_len > package.size())
    {
        return false;
    }

    // 提取报文信息
    *content = package.substr(pos + 1, len);

    // 移除报文
    package.erase(0, total_len);

    return true;
}

class Request
{
public:
    Request(int data1, int data2, char oper)
        : x(data1), y(data2), op(oper)
    {
    }
    Request()
    {
    }

public:
    // 序列化
    bool Serialize(std::string *out)
    {
#ifdef MySelf
        // 构建报文的有效载荷
        // 把结构化的东西转成字符串
        // struct->x op y
        std::string str = std::to_string(x);
        str += blank_space_sep;
        str += op;
        str += blank_space_sep;
        str += std::to_string(y);

        *out = str;

        return true;
#else
        Json::Value root;
        root["x"] = x;
        root["y"] = y;
        root["op"] = op;

        Json::FastWriter w;
        *out = w.write(root);
        return true;
#endif
    }

    // 反序列化
    bool Deserialize(const std::string &in) //"x op y"
    {
#ifdef MySelf
        // 解析字符串
        //"x op y"
        size_t left = in.find(blank_space_sep);
        if (left == std::string::npos)
        {
            return false;
        }
        std::string part_x = in.substr(0, left);

        size_t right = in.rfind(blank_space_sep);
        if (right == std::string::npos)
        {
            return false;
        }
        std::string part_y = in.substr(right + 1);

        if (left + 2 != right)
        {
            return false;
        }

        op = in[left + 1];
        x = std::stoi(part_x);
        y = std::stoi(part_y);

        return true;
#else
        Json::Value root;
        Json::Reader r;
        r.parse(in, root);

        x = root["x"].asInt();
        y = root["y"].asInt();
        op = root["op"].asInt();
        return true;
#endif
    }

    // 调试信息
    void debug()
    {
        std::cout << "新请求构建完成:" << std::endl;
        std::cout << "x = " << x << std::endl;
        std::cout << "y = " << y << std::endl;
        std::cout << "op = " << op << std::endl;
    }

public:
    int x;
    int y;
    char op;
};

class Response
{
public:
    Response(int data, int code)
        : _result(data), _code(code)
    {
    }

    Response()
    {
    }

public:
    // 序列化
    bool Serialize(std::string *out)
    {
#ifdef MySelf
        // 构建有效载荷
        std::string str = std::to_string(_result);
        str += blank_space_sep;
        str += std::to_string(_code);

        *out = str;

        return true;
#else
        Json::Value root;
        root["result"] = _result;
        root["code"] = _code;

        Json::FastWriter w;
        *out = w.write(root);
        return true;

#endif
    }

    // 反序列化
    bool Deserialize(const std::string &in) //"result code"
    {
#ifdef MySelf
        // 解析字符串
        //"result code"
        size_t pos = in.find(blank_space_sep);
        if (pos == std::string::npos)
        {
            return false;
        }
        std::string result = in.substr(0, pos);
        std::string code = in.substr(pos + 1);

        _result = std::stoi(result);
        _code = std::stoi(code);

        return true;
#else
        Json::Value root;
        Json::Reader r;
        r.parse(in, root);

        _result = root["result"].asInt();
        _code = root["code"].asInt();
        return true;
#endif
    }

    // 调试信息
    void debug()
    {
        std::cout << "结果响应完成:" << std::endl;
        std::cout << "result = " << _result << std::endl;
        std::cout << "code = " << _code << std::endl;
    }

public:
    int _result;
    int _code; // 错误码,0表示可信,非零表示不可信,表示对应的错误原因
};

// 协议的测试代码
void test()
{
    // Request序列化测试
    Request req(10, 10, '+');
    std::string str;
    req.Serialize(&str);

    str = Package(str);
    std::cout << str << std::endl;

    // Request反序列化测试
    std::string content;
    bool r = Depacksge(str, &content);
    std::cout << content << std::endl;
    Request temp1;
    temp1.Deserialize(content);

    std::cout << temp1.x << " " << temp1.y << " " << temp1.op << std::endl;

    // Response序列化测试
    Response resp(1000, 3);
    std::string result;
    resp.Serialize(&result);
    std::cout << result << std::endl;

    result = Package(result);
    std::cout << result << std::endl;

    // Response反序列化测试
    std::string content1;
    r = Depacksge(result, &content1);
    std::cout << content1 << std::endl;

    Response temp2;
    temp2.Deserialize(content1);
    std::cout << temp2._result << std::endl;
    std::cout << temp2._code << std::endl;
}

日志(Log.hpp文件):

cpp 复制代码
#pragma once

#include <iostream>
#include <stdarg.h>
#include <time.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
#include <fcntl.h>
#include<string.h>

#define SIZE 1024

// 设置日志等级
#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4

#define Screen 1
#define Onefile 2
#define Classfile 3

#define logFile "log.txt"

class log
{
public:
    log()
    {
        PrintMethod = Screen;
        path = "./log/";
    }
    void Enable(int method)
    {
        PrintMethod = method;
    }

    std::string levelToString(int level)
    {
        switch (level)
        {
        case Info:
            return "Info";
        case Debug:
            return "Debug";
        case Warning:
            return "Warning";
        case Error:
            return "Error";
        default:
            return "None";
        }
    }

    void logmessage(int level, const char *format, ...) // 后面的省略号表示可变参数
    {

        char leftbuffer[SIZE];

        time_t t = time(nullptr);
        struct tm *ctime = localtime(&t);

        snprintf(leftbuffer, sizeof(leftbuffer), "[%s],[%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
                 ctime->tm_year + 1900, ctime->tm_mon, ctime->tm_mday,
                 ctime->tm_hour, ctime->tm_min, ctime->tm_sec);

        char rightbuffer[SIZE];

        va_list s;
        va_start(s, format);
        vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
        va_end(s);

        char logtxt[SIZE * 3];
        snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftbuffer, rightbuffer);

        // printf("%d-%d-%d %d:%d:%d\n",ctime->tm_year + 1900, ctime->tm_mon, ctime->tm_mday, ctime->tm_hour,ctime->tm_min,ctime->tm_sec);

        //printf("%s", logtxt);
        PrintLog(level,logtxt);
        // 格式:默认部分+自定义部分(可变参数部分)
    }

    void PrintLog(int level, const std::string& logtxt)
    {
        switch(PrintMethod)
        {
            case Screen:
                std::cout << logtxt << std::endl;
            break;
            case Onefile:
                printOneFile(logFile, logtxt);
            break;
            case Classfile:
                printClassFile(level, logtxt);
            break;
            default:
            break;
        }
    }
    void printOneFile(const std::string &logname, const std::string &logtxt)
    { 
        std::string _logname = path + logname;
        int fd = open(_logname.c_str(),O_WRONLY|O_CREAT|O_APPEND,0666);
        if(fd < 0)
        {
            return  ;
        }
        write(fd,logtxt.c_str(),logtxt.size());
        close(fd);

    }
    void printClassFile(int level, const std::string &logtxt)
    {
        std::string filename = logFile;
        filename += ".";
        filename += levelToString(level);
        printOneFile(filename, logtxt);
    }
    ~log()
    {
    }
private:
    int PrintMethod;
    std::string path;
};


log lg;

守护进程化(Daemon.hpp文件):

cpp 复制代码
#pragma once
#include <iostream>
#include <cstdlib>
#include <signal.h>
#include <unistd.h>
#include <fcntl.h>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>

// Linux系统提供的垃圾文件
const std::string nullfile = "/dev/null";

void Daemon(const std::string &cwd = " ")
{
    // 忽略其他异常信号
    signal(SIGCLD, SIG_IGN);
    signal(SIGPIPE, SIG_IGN);
    signal(SIGSTOP, SIG_IGN);

    // 进来是父进程,出去就是子进程了(让自己不要成为组长)
    if (fork() > 0)
    {
        exit(0);
    }

    // 让自己成为新的会话
    setsid();

    // 更改守护进程的目录
    if (!cwd.empty())
    {
        chdir(cwd.c_str());
    }

    // 关标准输入标准输出和标准错误全部重定向至/dev/null中
    int fd = open(nullfile.c_str(), O_RDWR);
    if (fd > 0)
    {
        dup2(fd, 1);
        dup2(fd, 0);
        dup2(fd, 3);
        close(fd);
    }
}

套接字封装文件(Socket.hpp文件):

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstring>
#include "Log.hpp"

const int backlog = 10;

enum
{
    SocketError = 2,
    BindError,
    ListenError
};

class Sock
{
public:
    Sock()
    {
    }

    ~Sock()
    {
    }

    // 创建套接字
    void Socket()
    {
        _sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (_sockfd < 0)
        {
            lg.logmessage(Fatal, "socket fail, %s, %d", errno, strerror(errno));
            exit(SocketError);
        }
    }

    // 创建一个绑定接口
    void Bind(const uint16_t port)
    {
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_addr.s_addr = INADDR_ANY;
        local.sin_port = htons(port);

        if (bind(_sockfd, (struct sockaddr *)&local, sizeof(local) < 0))
        {
            lg.logmessage(Fatal, "bind fail, %s, %d", errno, strerror(errno));
            exit(BindError);
        }
    }

    void Listen()
    {
        if (listen(_sockfd, backlog) < 0)
        {
            lg.logmessage(Fatal, "listen fail, %s, %d", errno, strerror(errno));
            exit(ListenError);
        }
    }

    int Accept(std::string *clientip, uint16_t *clientport)
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int newfd = accept(_sockfd, (struct sockaddr *)&peer, &len);
        if (newfd < 0)
        {
            lg.logmessage(Warning, "accept fail, %s, %d", errno, strerror(errno));
            return -1;
        }

        char ipstr[64];
        inet_ntop(AF_INET, &peer.sin_addr, ipstr, sizeof(ipstr));
        // 获取远端主机的信息
        *clientport = ntohs(peer.sin_port);
        *clientip = ipstr;

        return newfd;
    }

    bool Connect(const std::string &ip, const uint16_t &port)
    {
        struct sockaddr_in peer;
        memset(&peer, 0, sizeof(peer));
        peer.sin_family = AF_INET;
        peer.sin_port = htons(port);
        inet_pton(AF_INET, ip.c_str(), &peer.sin_addr);

        int n = connect(_sockfd, (struct sockaddr *)&peer, sizeof(peer));
        if (n == -1)
        {
            std::cerr << "connect to" << ip << ":" << port << std::endl;
            return false;
        }
        return true;
    }

    void Close()
    {
        close(_sockfd);
    }

    int Fd()
    {
        return _sockfd;
    }

private:
    int _sockfd;
};

服务端头文件(TcpServer.hpp文件):

cpp 复制代码
#pragma once
#include "Log.hpp"
#include "Socket.hpp"
#include <signal.h>
#include <string>
#include "ServerCal.hpp"
#include <functional>

using func_t = std::function<std::string(std::string &package)>;

class TcpServer
{
public:
    TcpServer(uint16_t port, func_t callback)
        : _port(port), _callback(callback)
    {
    }

    bool InitServer()
    {
        // 创建套接字
        _listen_sockfd.Socket();
        _listen_sockfd.Bind(_port);
        _listen_sockfd.Listen();
        lg.logmessage(Info, "init server success...done");

        return true;
    }

    void Start()
    {
        while (true)
        {
            signal(SIGCHLD, SIG_IGN);
            signal(SIGPIPE, SIG_IGN);
            std::string clientip;
            uint16_t clientport;
            int sockfd = _listen_sockfd.Accept(&clientip, &clientport);
            if (sockfd < 0)
            {
                continue;
            }
            lg.logmessage(Info, "accept a new link,sockfd:%d,clientip:%s,clientport:%d", sockfd, clientip.c_str(), clientport);

            // 开始服务(多进程)
            pid_t pid = fork();
            if (pid == 0)
            {
                _listen_sockfd.Close();

                // 数据处理
                while (true)
                {
                    std::string inbuffer_stream;
                    char buffer[1024];
                    ssize_t n = read(sockfd, buffer, sizeof(buffer));
                    if (n > 0)
                    {
                        buffer[n] = '\0';
                        inbuffer_stream += buffer;

                        // 添加调试日志信息
                        lg.logmessage(Debug, "debuug:%s", inbuffer_stream.c_str());

                        while (true)
                        {
                            std::string info = _callback(inbuffer_stream);
                            if (info.empty())
                            {
                                break;
                            }

                            write(sockfd, info.c_str(), info.size());
                        }
                    }
                    else if (n == 0)
                    {
                        break;
                    }
                    else
                    {
                        break;
                    }
                }

                exit(0);
            }
            close(sockfd);
        }
    }

    ~TcpServer()
    {
    }

private:
    uint16_t _port;
    Sock _listen_sockfd;
    func_t _callback;
};

计算处理(ServerCal.hpp文件):

cpp 复制代码
#pragma once
#include <iostream>
#include <stdbool.h>
#include "Protocol.hpp"

enum
{
    Dic_ZERO = 1,
    Mode_ZERO,
    Other_oper
};

class ServerCal
{
public:
    ServerCal()
    {
    }

    Response CalculatorHelper(const Request &req)
    {
        Response resp(0, 0);
        switch (req.op)
        {
        case '+':
            resp._result = req.x + req.y;
            break;
        case '-':
            resp._result = req.x - req.y;
            break;
        case '*':
            resp._result = req.x * req.y;
            break;
        case '/':
        {
            if (req.y == 0)
            {
                resp._code = Dic_ZERO;
            }
            else
            {
                resp._result = req.x / req.y;
            }
        }
        break;
        case '%':
        {
            if (req.y == 0)
            {
                resp._code = Mode_ZERO;
            }
            else
            {
                resp._result = req.x % req.y;
            }
        }
        break;
        default:
            resp._code = Other_oper;
            break;
        }

        return resp;
    }

    std::string Calculator(std::string &package)
    {
        std::string content;
        bool r = Depacksge(package, &content);
        if (r == false)
        {
            return "";
        }

        // 对对象进行反序列化
        Request req;
        r = req.Deserialize(content);
        if (r == false)
        {
            return "";
        }

        // 进行计算
        Response resp = CalculatorHelper(req);

        // 计算完后序列化返回
        std::string message;
        resp.Serialize(&message);
        message = Package(message);

        return message;
    }

    ~ServerCal()
    {
    }
};

服务端主函数代码(ServerCalculator.cc文件):

cpp 复制代码
#include "TcpServer.hpp"
#include "ServerCal.hpp"
#include "Daemon.hpp"
#include <unistd.h>

static void Usage(const std::string &proc)
{
    std::cout << "\n\rUsage:" << proc << "port\n\n"
              << std::endl;
}

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }
    uint16_t port = std::stoi(argv[1]);

    ServerCal cal;
    TcpServer *tcpsvr = new TcpServer(port, std::bind(&ServerCal::Calculator, &cal, std::placeholders::_1));
    tcpsvr->InitServer();

    // 自定义的守护进程化
    //Daemon();

    //系统的守护进程化
    daemon(0, 0);

    tcpsvr->Start();

    return 0;
}

7.2客户端

ClientCalculator.cc文件:

cpp 复制代码
#include<assert.h>
#include <iostream>
#include <stdbool.h>
#include <time.h>
#include <unistd.h>
#include "Log.hpp"
#include "Socket.hpp"
#include "Protocol.hpp"

void Usage(const std::string &proc)
{
    std::cout << "\nUsage:" << proc << "serverip serverport\n"
              << std::endl;
}

// 运行格式:./calclient ip port
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(0);
    }

    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    Sock sockfd;
    sockfd.Socket();
    bool r = sockfd.Connect(serverip, serverport);
    if (r == false)
    {
        return 1;
    }

    srand((unsigned int)time(nullptr) ^ getpid());
    int count = 10;
    const std::string opers = "+-*/%";
    std::string inbuffer_stream;
    while (count--)
    {
        int x = rand() % 100 + 1;
        usleep(128);
        int y = rand() % 100;
        usleep(129);
        char oper = opers[rand() % opers.size()];

        Request req(x, y, oper);
        // 查看调试信息
        req.debug();

        // 序列化
        std::string package;
        req.Serialize(&package);
        package = Package(package);
        std::cout << "这是最新发送出去的请求:\n" << package << std::endl;
        
        write(sockfd.Fd(), package.c_str(), package.size());

        char buffer[1024];
        ssize_t n = read(sockfd.Fd(), buffer, sizeof(buffer));//这里也无法保证读取的是完整的报文
        if(n>0)
        {
            buffer[n]='\0';
            inbuffer_stream += buffer;// "len"\n"result code"\n
            std::string content;
            bool r = Depacksge(inbuffer_stream, &content);// "reault code"
            assert(r == true);

            //反序列化
            Response resp;
            r = resp.Deserialize(content);
            assert(r == true);

            resp.debug();
        }
        sleep(1);
    }

    sockfd.Close();
    return 0;
}

7.3运行结果

八、Jsoncpp

8.1Jsoncpp的概念

Jsoncpp是一个用于处理JSON数据的C++库。它提供了将JSON数据序列化为字符串以及从字符串反序列化为C++数据结构的功能。Jsoncpp是开源的,广泛用于各种需要处理JSON数据的C++项目中。

8.2Jsoncpp的特性

1.简单易用:Jsoncpp提供了直观的API,使得处口JSON数据变得简单。

2.高性能:Jsoncpp的性能经过优化,能够高效地处理大量JSON数据。

3.全面支持:支持JSON标准中的所有数据类型,包括对象、数组、字符串、数字、布尔值和null。

4.错误处理:在解析JSON数据时,Jsoncpp提供了详细的错误信息和位置,方便开发者调试。当使用Jsoncpp库进行JSON的序列化和反序列化时,确实存在不同的做法和工具类可供选择。

8.3Jsoncpp的下载

九、序列化

序列化指的是将数据结构或对象转换为一种格式,以便在网络上传输或存储到文件中。Jsoncpp提供了多种方式进行序列化使用Json

9.1使用Json::FastWriter

优点:比StyledWriter更快,因为它不添加额外的空格和换行符。

使用方式如下:

cpp 复制代码
#include <iostream>
#include <jsoncpp/json/json.h>

//{a:120, b:"123"}

int main()
{
    // 定义对象
    Json::Value root;
    root["x"] = 100;
    root["y"] = 200;
    root["op"] = "+";
    root["dect"] = "this is a + oper";

    // 序列化
    Json::FastWriter w;
    std::string str = w.write(root);
    std::cout << str << std::endl;

    return 0;
}

运行结果:

9.2使用Json::Value的toStyledString方法:

优点:将Json::Value对象直接转换为格式化的Json字符串

使用方法如下:

cpp 复制代码
#include <iostream>
#include <jsoncpp/json/json.h>

//{a:120, b:"123"}

int main()
{
    // 定义对象
    Json::Value root;
    root["x"] = 100;
    root["y"] = 200;
    root["op"] = "+";
    root["dect"] = "this is a + oper";

    // 序列化
    Json::StyledWriter w;
    std::string str = w.write(root);
    std::cout << str << std::endl;

    return 0;
}

运行结果:

9.3使用Json.:StreamWriter

优点:提供了更多的定制选项,如缩进、换行符等使用

示例如下:

cpp 复制代码
#include <iostream>
#include <sstream>
#include <jsoncpp/json/json.h>

int main()
{
    Json::Value root;
    root["x"] = 100;
    root["y"] = 200;
    root["op"] = "+";
    root["dect"] = "this is a + oper";

    // 序列化(用 StreamWriterBuilder)
    Json::StreamWriterBuilder builder;
    builder["indentation"] = ""; // 不缩进,紧凑输出
    std::string str = Json::writeString(builder, root);

    std::cout << str << std::endl;

    return 0;
}

运行结果:

十、反序列化

反序列化指的是将序列化后的数据重新转换为原来的数据结构或对象。

Jsoncpp提供了以下方法进行反序列化:使用Json:Reader

优点:提供详细的错误信息和位置,方便调试。

使用如下:

cpp 复制代码
#include <iostream>
#include <jsoncpp/json/json.h>

//{a:120, b:"123"}

int main()
{
    // 定义对象
    Json::Value root;
    root["x"] = 100;
    root["y"] = 200;
    root["op"] = "+";
    root["dect"] = "this is a + oper";

    // 序列化
    Json::FastWriter w;
    std::string str = w.write(root);
    std::cout << str << std::endl;

    // 反序列化
    Json::Value v;
    Json::Reader r;
    r.parse(str, v);
    int x = v["x"].asInt();
    int y = v["y"].asInt();
    std::string op = v["op"].asString();
    std::string desc = v["dect"].asString();

    std::cout << x << std::endl;
    std::cout << y << std::endl;
    std::cout << op << std::endl;
    std::cout << desc << std::endl;

    return 0;
}

运行结果:

相关推荐
苏宸啊1 小时前
linux进程控制(一)
linux
许长安1 小时前
protobuf 使用详解
c++·经验分享·笔记·中间件
开开心心_Every1 小时前
轻量级PDF阅读器,仅几M大小打开秒开
linux·运维·服务器·安全·macos·pdf·phpstorm
重生之我是Java开发战士1 小时前
【笔试强训】Week3:重排字符串,分组,DNA序列
算法
Soley1 小时前
用 Boost.Log 封装一个更顺手的 C++17 日志库:GoodLog
c++
We་ct1 小时前
LeetCode 97. 交错字符串:动态规划详解
前端·算法·leetcode·typescript·动态规划
云达闲人2 小时前
搭建DevOps企业级仿真实验环境:006Proxmox 基础环境验证
运维·devops·proxmox ve·sre·仿真实验环境·快照与克隆·运维实操教程
热心网友俣先生2 小时前
2026年第二十三届五一数学建模竞赛B题四问参考答案+多算法对比
算法·数学建模
HAPPY酷2 小时前
从Public到Private:UE5 C++类创建路径差异全解析
java·c++·ue5