Linux网络编程—应用层自定义协议与序列化

第一章:应用层

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

再谈"协议"

协议是一种"约定"。socket api的接口,在读写数据时,都是按"字符串"的方式来发送接收的。如果我们要传输一些"结构化的数据"怎么办呢?

其实,协议就是双方约定好的结构化的数据。

网络版计算器

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

约定方案一:

  • 客户端发送一个形如"1+2"的字符串;
  • 这个字符串中有两个操作数,都是整形;
  • 两个数字之间会有一个字符是运算符,运算符只能是 +;
  • 数字和运算符之间没有空格;

约定方案二:

  • 定义结构体来表示我们需要交互的信息;
  • 发送数据时将这个结构体按照一个规则转换成字符串,接收到数据的时候再按照相同的规则把字符串转化回结构体;
  • 这个过程叫做"序列化"和"反序列化"

序列化 和 反序列化

无论我们采用方案一,还是方案二,还是其他的方案,只要保证,一端发送构造的数据,在另一端能够正确的进行解析,就是ok的。这种约定,就是应用层协议

但是,为了让我们深刻理解协议,我们打算自定义实现一下协议的过程。

  • 我们采用方案2,我们也要体现协议定制的细节
  • 我们要引入序列化和反序列化,只不过我们课堂直接采用现成的方案 -- jsoncpp库
  • 我们要对socket进行字节流的读取处理

第二章:重新理解read、write、recv、send和tcp为什么支持全双工

所以:

  • 在任何一台主机上,TCP连接既有发送缓冲区,又有接受缓冲区,所以,在内核中,可以在发消息的同时,也可以收消息,即全双工
  • 这就是为什么一个tcp sockfd读写都是它的原因
  • 实际数据什么时候发,发多少,出错了怎么办,由TCP控制,所以TCP叫做传输控制协议

第三章:开始实现

Makefile

cpp 复制代码
.PHONY:all
all:servercal clientcal

Flag=-DMySelf=1
Lib=-ljsoncpp

servercal:ServerCal.cc
	g++ -o $@ $^ -std=c++11 $(Lib) #$(Flag)
clientcal:ClientCal.cc
	g++ -o $@ $^ -std=c++11 $(Lib) #$(Flag)

.PHONY:clean
clean:
	rm -f servercal clientcal

Socket.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <unistd.h>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Log.hpp"

enum {
    SocketErr = 2,
    BindErr,
    ListenErr
};

const int backlog = 10;

class Sock {
public:
    Sock() {}
    ~Sock() {}

public:
    void Socket() {
        sockfd_ = socket(AF_INET, SOCK_STREAM, 0);
        if (sockfd_ < 0) {
            lg(Fatal, "socket error, %s:%d", strerror(errno), errno);
            exit(SocketErr);
        }
        int opt = 1;
        setsockopt(sockfd_, SOL_SOCKET, SO_REUSEADDR|SO_REUSEPORT, &opt, sizeof(opt));
    }

    void Bind(uint16_t port) {
        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;

        if (bind(sockfd_, (struct sockaddr*)&local, sizeof(local)) < 0) {
            lg(Fatal, "bind error, %s:%d", strerror(errno), errno);
            exit(BindErr);           
        }
    }

    void Listen() {
        if (listen(sockfd_, backlog) < 0) {
            lg(Fatal, "listen error, %s:%d", strerror(errno), errno);
            exit(ListenErr);             
        }
    }

    int Accepet(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(Warning, "accept error, %s:%d", strerror(errno), errno);
            return -1;             
        }
        char ipstr[64];
        inet_ntop(AF_INET, &peer.sin_addr, ipstr, sizeof(ipstr));
        *clientip = ipstr;
        *clientport = ntohs(peer.sin_port);
        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 << " error" << std::endl;
            return false;
        }
        return true;
    }

    void Close() { close(sockfd_); }
    int Fd() { return sockfd_; }

private:
    int sockfd_;
};

Protocol.hpp

cpp 复制代码
#pragma once

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

// #define MySelf 1

const std::string blank_space_sep = " "; 
const std::string protocol_sep = "\n";

//"x op y" => "len"\n"x op y"\n
std::string Encode(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 Decode(std::string& package, std::string* content) {
    //取第一个\n之前的len,并转为整形
    size_t pos = package.find(protocol_sep);
    if (pos == std::string::npos) return false;
    std::string len_str = package.substr(0, pos);//报头字符串
    size_t len = std::stoi(len_str);//报头表示有效载荷的长度
    //检查整个package的长度。 package = len_str + content_str + 2 (2个\n)
    size_t total_len = len_str.size() + len + 2;
    if (package.size() < total_len) return false;
    
    *content = package.substr(pos + 1, len);
    //从流中获取完一条完整报文后就要移除它,erase 移除报文 
    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:
    //struct => string,  "x op y"
    bool Serialize(std::string* out) {
#ifdef MySelf
        //构建报文的有效载荷
        std::string s = std::to_string(x);
        s += blank_space_sep;
        s += op;
        s += blank_space_sep;
        s += std::to_string(y);
        *out = s;
        return true;
#else
        Json::Value root;
        root["x"] = x;
        root["y"] = y;
        root["op"] = op;
        // Json::FastWriter w;
        Json::StyledWriter w;
        *out = w.write(root);
        return true;
#endif
    }

    // string => struct 
    bool Deserialize(const std::string& in) { 
#ifdef MySelf
        // 索引: 0 1 2 3 4
        // 字符: 3   +   4
        size_t left = in.find(blank_space_sep);//left是1
        if (left == std::string::npos) return false;
        std::string part_x = in.substr(0, left);

        size_t right = in.rfind(blank_space_sep);//right是3
        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 DebugPrint() {
        printf("请求构建完成:%d %c %d = ?\n", x, op, y);
    }
public:
    int x;
    int y;
    char op;
};


class Response {
public:
    Response(int res, int c)
        :result(res), code(c) {}
    Response() {}
public:
    //struct => string  "result code"
    bool Serialize(std::string* out) {
#ifdef MySelf        
        //构建有效载荷
        std::string s = std::to_string(result);
        s += blank_space_sep;
        s += std::to_string(code);
        *out = s;
        return true;
#else
        Json::Value root;
        root["result"] = result;
        root["code"] = code;
        // Json::FastWriter w;
        Json::StyledWriter w;
        *out = w.write(root);
        return true;
#endif  
    }

    //string => struct
    bool Deserialize(const std::string& in) {
#ifdef MySelf        
        size_t pos = in.find(blank_space_sep);//left是1
        if (pos == std::string::npos) return false;
        std::string left = in.substr(0, pos);
        std::string right = in.substr(pos + 1);
        result = std::stoi(left);
        code = std::stoi(right);       
        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 DebugPrint() {
        std::cout << "结果响应完成, result:" << result << ", code:" << code << std::endl << std::endl; 
    }
public:
    int result;
    int code;//0可信;!0是几,表明对应的错误原因
};

ServerCal.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include "Protocol.hpp"

enum {
    Div_Zero = 1,
    Mod_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 = Div_Zero;
                else resp.result = req.x / req.y;
            }
                break;
            case '%': 
            {
                if (req.y == 0) resp.code = Mod_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 = Decode(package, &content);
        if (!r) return "";
        Request req;
        r = req.Deserialize(content);
        if (!r) return "";

        content = "";
        Response resp = CalculatorHelper(req);

        resp.Serialize(&content);
        content = Encode(content);
        return content;
    }

    ~ServerCal() {}
};

TcpServer.hpp

cpp 复制代码
#pragma once

#include <signal.h>
#include <functional>
#include <string>
#include "Log.hpp"
#include "Socket.hpp"

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

class TcpServer {
public:
    TcpServer(uint16_t port, func_t callback) 
        :port_(port), callback_(callback) {}
    bool InitServer() {
        listensockfd_.Socket();
        listensockfd_.Bind(port_);
        listensockfd_.Listen();
        lg(Info, "Init server done.");
        return true;
    }
    void Start() {
        signal(SIGCHLD, SIG_IGN);
        while (true) {
            std::string clientip;
            uint16_t clientport;
            int sockfd = listensockfd_.Accepet(&clientip, &clientport);
            if (sockfd < 0) continue;
            lg(Info, "Accepet a new link, sockfd:%d, clientip:%s, clinetport:%d", sockfd, clientip.c_str(), clientport);
            //提供服务(多进程版)
            if (fork() == 0) {
                listensockfd_.Close();
                std::string inbuff_stream;
                //数据计算
                while (true) {
                    char buffer[128];
                    ssize_t n  = read(sockfd, buffer, sizeof(buffer));
                    if (n > 0) {
                        buffer[n] = 0;
                        inbuff_stream += buffer;
                        lg(Debug, "debug: %s", inbuff_stream.c_str());//回显读到的信息
                        
                        while (true) { 
                            std::string info = callback_(inbuff_stream);
                            if (info.empty()) break;
                            //inbuff_stream有多个请求
                            lg(Debug, "debug, response:\n%s", info.c_str());
                            // lg(Debug, "%s", inbuff_stream.c_str());
                            write(sockfd, info.c_str(), info.size());   
                        }

                        // //inbuff_stream只有一个请求
                        // std::string info = callback_(inbuff_stream);
                        // if (info.empty()) continue;
                        // write(sockfd, info.c_str(), info.size());
                    }
                    else if (n == 0) break;
                    else break;
                }
                exit(0);
            }
            close(sockfd);
        }
    }
    ~TcpServer() {}
private:
    uint16_t port_;
    Sock listensockfd_;
    func_t callback_;
};

ServerCal.cc

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

static void Usage(const std::string& proc) {
    std::cout << "\n\tUsage: " << proc << " port\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;
    //std::bind的作用: 将成员函数绑定到特定对象,创建一个可调用对象
    //bind作用,类的成员函数第一个参数是this指针,bind把第一个参数固定写死成this指针,后续调用可省略
    TcpServer *tsvp = new TcpServer(port, std::bind(&ServerCal::Calculator, &cal, std::placeholders::_1));
    tsvp->InitServer();
    daemon(0, 0);
    tsvp->Start();
}

ClientCal.cc

cpp 复制代码
#include <iostream>
#include <string>
#include <ctime>
#include <cassert>
#include <unistd.h>
#include "Socket.hpp"
#include "Protocol.hpp"

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

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) return 1;

    srand(time(nullptr) ^ getpid());
    int cnt = 1;
    const std::string opers = "+-*/%=-=&^";
    std::string inbuff_stream;//报文
    while (cnt <= 10) {
        std::cout << "第" << cnt << "次测试" << std::endl;
        int x = rand() % 100 + 1;
        usleep(1234);
        int y = rand() % 100;
        usleep(1234);
        char op = opers[rand() % opers.size()];
        Request req(x, y, op);//初始化请求
        req.DebugPrint();

        std::string package;
        req.Serialize(&package);
        package = Encode(package);
        std::cout << "最新发出的请求:\n" << package;
        write(sockfd.Fd(), package.c_str(), package.size());
        // write(sockfd.Fd(), package.c_str(), package.size());
        // write(sockfd.Fd(), package.c_str(), package.size());
        // write(sockfd.Fd(), package.c_str(), package.size());

        char buffer[128];
        int n = read(sockfd.Fd(), buffer, sizeof(buffer));
        if (n > 0) {
            buffer[n] = 0;
            inbuff_stream += buffer;//报文
            std::cout << inbuff_stream << std::endl;
            
            std::string content;//有效载荷
            bool r = Decode(inbuff_stream, &content);
            assert(r);

            Response resp;
            r = resp.Deserialize(content);
            assert(r);

            resp.DebugPrint();
            cnt++;
        }
        // usleep(500000);
        sleep(1);
    }

    sockfd.Close();
    return 0;
}

包装器的作用

bind的作用

第四章:关于流式数据的处理

  • 你如何保证你每次读取就能读完请求缓冲区的所有内容?
  • 你怎么保证读取完毕或者读取没有完毕的时候,读到的就是一个完整的请求呢?
  • 处理TCP缓冲区中的数据,一定要保证正确处理请求
cpp 复制代码
const std::string blank_space_sep = " "; 
const std::string protocol_sep = "\n";

//"x op y" => "len"\n"x op y"\n  \n不属于报文的一部分,约定
std::string Encode(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"  无法保证package就是一个独立的完整的报文
bool Decode(std::string& package, std::string* content) {
    //取第一个\n之前的len,并转为整形
    size_t pos = package.find(protocol_sep);
    if (pos == std::string::npos) return false;
    std::string len_str = package.substr(0, pos);//报头字符串
    size_t len = std::stoi(len_str);//报头表示有效载荷的长度
    //检查整个package的长度。 package = len_str + content_str + 2 (2个\n)
    size_t total_len = len_str.size() + len + 2;
    if (package.size() < total_len) return false;
    
    *content = package.substr(pos + 1, len);
    //从流中获取完一条完整报文后就要移除它,erase 移除报文 
    package.erase(0, total_len);
    return true;
}

所以,完整的处理过程应该是:

TCP缓冲区与协议

附录

Jsoncpp

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

特性

  1. 简单易用:Jsoncpp 提供了直观的 API,使得处理 JSON 数据变得简单。
  2. 高性能:Jsoncpp 的性能经过优化,能够高效地处理大量 JSON 数据。
  3. 全面支持:支持 JSON 标准中的所有数据类型,包括对象、数组、字符串、数字、布尔值和 null。
  4. 错误处理:在解析 JSON 数据时,Jsoncpp 提供了详细的错误信息和位置,方便开发者调试。

当使用Jsoncpp库进行JSON的序列化和反序列化时,确实存在不同的做法和工具类可供选择。以下是对Jsoncpp中序列化和反序列化操作的详细介绍:

安装

bash 复制代码
ubuntu:sudo apt-get install libjsoncpp-dev
Centos: sudo yum install jsoncpp-devel

序列化

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

  1. 使用 Json::Value 的 toStyledString 方法:

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

示例:

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

int main() {
    Json::Value part1;
    part1["haha"] = "haha";
    part1["hehe"] = "hehe";

    Json::Value root;
    root["x"] = 100;
    root["y"] = 200;
    root["op"] = '+';
    root["desc"] = "this is a + oper";
    root["test"] = part1;

    Json::FastWriter w;
    // Json::StyledWriter w;
    std::string res = w.write(root);
    std::cout << res << std::endl;

    return 0;
}
bash 复制代码
//Json::FastWriter w;
[sxy@VM-12-13-centos testjson]$ ./a.out 
{"desc":"this is a + oper","op":43,"test":{"haha":"haha","hehe":"hehe"},"x":100,"y":200}


//Json::StyledWriter w;
[sxy@VM-12-13-centos testjson]$ ./a.out 
{
   "desc" : "this is a + oper",
   "op" : 43,
   "test" : {
      "haha" : "haha",
      "hehe" : "hehe"
   },
   "x" : 100,
   "y" : 200
}

反序列化

反序列化指的是将序列化后的数据重新转换为原来的数据结构或对象。Jsoncpp提供了以下方法进行反序列化:

  1. 使用 Json::Reader:

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

示例:

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

int main() {
    Json::Value v;
    Json::Reader r;
    r.parse(res, v);

    int x = v["x"].asInt();
    int y = v["y"].asInt();
    int op = v["op"].asInt();
    std::string desc = v["desc"].asString();

    std::cout << x << std::endl;
    std::cout << y << std::endl;
    std::cout << op << std::endl;
    std::cout << desc << std::endl;
    return 0;
}
bash 复制代码
[sxy@VM-12-13-centos testjson]$ ./a.out 
100
200
43
this is a + oper
  1. 使用 Json::CharReader 的派生类(不推荐了,上面的足够了):
  • 在某些情况下,你可能需要更精细地控制解析过程,可以直接使用 Json::CharReader 的派生类。
  • 但通常情况下,使用 Json::parseFromStream 或 Json::Reader 的 parse 方法就足够了。

总结

  • toStyledString、StreamWriter 和 FastWriter 提供了不同的序列化选项,你可以根据具体需求选择使用。
  • Json::Reader 和 parseFromStream 函数是Jsoncpp中主要的反序列化工具,它们提供了强大的错误处理机制。
  • 在进行序列化和反序列化时,请确保处理所有可能的错误情况,并验证输入和输出的有效性。

Json::Value

Json::Value 是 Jsoncpp 库中的一个重要类,用于表示和操作 JSON 数据结构。以下是一些常用的 Json::Value 操作列表:

1. 构造函数

  • Json::Value():默认构造函数,创建一个空的 Json::Value 对象。
  • Json::Value(ValueType type, bool allocated = false):根据给定的ValueType(如 nullValue,intValue,stringValue 等)创建一个Json::Value 对象。

2. 访问元素

  • Json::Value& operator[](const char* key):通过键(字符串)访问对象中的元素。如果键不存在,则创建一个新的元素。
  • Json::Value& operator[](const std::string& key):同上,但使用std::string 类型的键。
  • Json::Value& operator[](ArrayIndex index):通过索引访问数组中的元素。如果索引超出范围,则创建一个新的元素。
  • Json::Value& at(const char* key):通过键访问对象中的元素,如果键不存在则抛出异常。
  • Json::Value& at(const std::string& key):同上,但使用 std::string 类型的键。

3. 类型检查

  • bool isNull():检查值是否为 null。
  • bool isBool():检查值是否为布尔类型。
  • bool isInt():检查值是否为整数类型。
  • bool isInt64():检查值是否为 64 位整数类型。
  • bool isUInt():检查值是否为无符号整数类型。
  • bool isUInt64():检查值是否为 64 位无符号整数类型。
  • bool isIntegral():检查值是否为整数或可转换为整数的浮点数。
  • bool isDouble():检查值是否为双精度浮点数。
  • bool isNumeric():检查值是否为数字(整数或浮点数)。
  • bool isString():检查值是否为字符串。
  • bool isArray():检查值是否为数组。
  • bool isObject():检查值是否为对象(即键值对的集合)。

4. 赋值和类型转换

  • Json::Value& operator=(bool value):将布尔值赋给 Json::Value 对象。
  • Json::Value& operator=(int value):将整数赋给 Json::Value 对象。
  • Json::Value& operator=(unsigned int value):将无符号整数赋给Json::Value 对象。
  • Json::Value& operator=(Int64 value):将 64 位整数赋给 Json::Value 对象。
  • Json::Value& operator=(UInt64 value):将 64 位无符号整数赋给 Json::Value对象。
  • Json::Value& operator=(double value):将双精度浮点数赋给 Json::Value 对象
  • Json::Value& operator=(const char* value):将 C 字符串赋给 Json::Value对象。
  • Json::Value& operator=(const std::string& value):将 std::string 赋给 Json::Value 对象。
  • bool asBool():将值转换为布尔类型(如果可能)。
  • int asInt():将值转换为整数类型(如果可能)。
  • Int64 asInt64():将值转换为 64 位整数类型(如果可能)。
  • unsigned int asUInt():将值转换为无符号整数类型(如果可能)。
  • UInt64 asUInt64():将值转换为 64 位无符号整数类型(如果可能)。
  • double asDouble():将值转换为双精度浮点数类型(如果可能)。
  • std::string asString():将值转换为字符串类型(如果可能)。

5. 数组和对象操作

  • size_t size():返回数组或对象中的元素数量。
  • bool empty():检查数组或对象是否为空。
  • void resize(ArrayIndex newSize):调整数组的大小。
  • void clear():删除数组或对象中的所有元素。
  • void append(const Json::Value& value):在数组末尾添加一个新元素。
  • Json::Value& operator[](const char* key, const Json::Value& defaultValue = Json::nullValue):在对象中插入或访问一个元素,如果键不存在则使用默认值。
  • Json::Value& operator[](const std::string& key, const Json::Value&defaultValue = Json::nullValue):同上,但使用 std::string 类型的键。
相关推荐
极客BIM工作室3 小时前
LSTM门控机制:本质是神经元构成的小型网络
网络·机器学习·lstm
小武~3 小时前
#嵌入式Linux电源管理实战:深入解析CPU调频governor原理与优化
linux
HalvmånEver5 小时前
Linux:基础开发工具(五)
linux·运维·服务器
qq_278984135 小时前
ubuntu vlan网络设置
linux·服务器·网络
q***58195 小时前
基础篇:Linux安装redis教程(详细)
linux·运维·redis
小二·5 小时前
Linux 高频面试题(50道 + 详细答案)
linux·运维·服务器
FLPGYH6 小时前
BMC 深度解析:服务器带外管理的核心技术架构与实践
linux·服务器·c++·驱动开发
史丹利复合田6 小时前
在openEuler上安装Atune并成功运行
linux