最新资讯

  • 【linux】网络基础(三)TCP服务端网络版本计算器的优化,Json的使用,服务器守护进程化daemon,重谈OSI七层模型

【linux】网络基础(三)TCP服务端网络版本计算器的优化,Json的使用,服务器守护进程化daemon,重谈OSI七层模型

2026-01-30 15:39:27 栏目:最新资讯 0 阅读

小编个人主页详情<—请点击
小编个人gitee代码仓库<—请点击
linux系统编程专栏<—请点击
linux网络编程专栏<—请点击
倘若命中无此运,孤身亦可登昆仑,送给屏幕面前的读者朋友们和小编自己!


目录

    • 前言
    • 一、TCP服务端的优化
      • 服务端
      • 客户端
        • 测试
    • 二、json的使用
    • 三、Protocol.hpp 使用Json平替我们自己的序列化和反序列化模块的代码
      • 测试
    • 四、服务器守护进程化
      • 使用我们自己的Daemon
      • daemon
    • 五、重谈OSI七层模型
    • 六、源代码
      • ClientCal.cc
      • Daemon.hpp
      • Log.hpp
      • makefile
      • Protocol.hpp
      • ServerCal.cc
      • ServerCal.hpp
      • Socket.hpp
      • TcpServer.hpp
    • 总结


前言

【linux】网络基础(二)自定义协议的定制,序列化和反序列化,TCP服务端和客户端的实现——网络版本计算器——书接上文 详情请点击<——,本文会在上文的基础上进行讲解,所以对上文不了解的读者友友请点击前方的蓝字链接进行学习
本文由小编为大家介绍——【linux】网络基础(三)TCP服务端网络版本计算器的优化,Json的使用,服务器守护进程化,重谈OSI七层模型


一、TCP服务端的优化

服务端

  1. 关于TCP服务端和客户端的实现——网络版本计算器 详细讲解以及源代码,详情请点击<——
  2. 如果今天的客户端一次性发来多次计算请求,那么也就代表着我服务端会一次性将多个Request请求序列化转码后的报文读取上来,那么目前我们的服务器的处理逻辑是,读取一次,然后处理一个请求报文,删除处理后的报文,将处理后的序列化的响应结果发送回给客户端,然后服务端继续读取
  3. 那么这种情况下,如果客户端一次性发来多次计算请求,那么我们服务器只会处理一个请求,那么我们如何让我们的服务器在这个过程中处理多次请求呢?很简单,加循环即可
  4. 那么由于是子进程负责的对计算请求调用的回调函数callback进行的处理,所以当读取多个报文进行处理的时候,采用while(true)死循环式的处理流缓冲区inbuffer_stream的数据,直到流缓冲区的多个报文被至不满足一个完整的报文为止,那么循环内每次处理完成,我们就判断一下返回值是否为空,如果为空,那么就代表此时流缓冲区的报文已经不满足一个完整的报文了,所以break退出循环
  5. 那么走到下一步,代表着此时回调函数callback_成功的处理了一个完整的报文,那么我们打印一下处理后序列化转码的结果,然后再打印一下流缓冲区inbuffer_stream的剩余未处理的报文即可,最后处理一次然后调用write向客户端发送一次处理响应后的序列化转码的结果,那么在这个死循环多次的过程就是在一次大的处理过程中对多个报文进行处理的过程
if(fork() == 0)
{
    //child
    listensock_.Close();
    std::string inbuffer_stream;
    while(true)
    {
        char buffer[1280];
        ssize_t n = read(sockfd, buffer, sizeof(buffer));
        if(n > 0)
        {
            buffer[n] = 0;
            inbuffer_stream += buffer;

            lg(Debug, "Debug:request
%s", inbuffer_stream.c_str());

            while(true)
            {
                std::string info = callback_(inbuffer_stream);
                if(info.empty())
                    break;
                    
                lg(Debug, "Debug:response
%s", info.c_str());
                lg(Debug, "Debug:inbuffer_stream
%s", inbuffer_stream.c_str());

                write(sockfd, info.c_str(), info.size());
            }

        }
        else if(n == 0)
            break;
        else
            break;
    }
    close(sockfd);
    exit(0);
}

客户端

  1. 那么作为客户端,我们要制造出可以一次性发送多次数据的场景,那么我们就为了场景需要,那么我们就连续write多次请求报文即可
  2. 那么这里为了便于观察,在客户端小编就不进行read读取数据打印结果了,结果小编已经在服务端进行打印了,所以观察结果在服务端进行观察即可,即让客户端仅仅是生成随机任务,然后一次任务连续发送多次即可
#include 
#include 
#include 
#include 
#include 
#include "Socket.hpp"
#include "Protocol.hpp"

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

// ./clientcal serverip serverport
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));
    std::string opers = "+-*/%~?@=";
    int cnt = 1;

    std::string inbuffer_stream;
    while(cnt <= 5)
    {
        std::cout << "------------第" << cnt << "次测试-------------" << std::endl;
        int x = rand() % 10 + 1;
        usleep(123);
        int y = rand() % 10;
        usleep(654);
        char oper = opers[rand() % opers.size()];
        
        Request req(x, y, oper);
        req.DebugPrint();

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

        // char buffer[128];
        // ssize_t n = read(sockfd.Fd(), buffer, sizeof(buffer));
        // if(n > 0)
        // {
        //     buffer[n] = 0;
        //     inbuffer_stream += buffer;
        //     std::cout << inbuffer_stream << std::endl;

        //     std::string content;
        //     bool r = Decode(inbuffer_stream, &content);
        //     assert(r);

        //     Response resp;
        //     r = resp.Deserialize(content);
        //     assert(r);
            
        //     resp.DebugPrint();
        // }
        // else if(n == 0)
        //     break;
        // else  
        //     break;

        std::cout << "----------------------------------" << std::endl;

        sleep(2);
        cnt++;
    }

    sockfd.Close();

    return 0;
}
测试
  1. 下面我们来运行一下,服务端可以一次性处理read读取上来的多次请求报文,无误

二、json的使用

  1. 其实在上一篇文章中,那么对于序列化和反序列化的工作都是小编带量大家手写的,里面涉及到大量的字符串处理,难道每次写服务器和客户端都需要我们自己手写序列化和反序列化吗,这未免也太挫了,有没有什么简单的,易上手的工具帮助我们完成序列化和反序列化的工作呢?有的,json,protobuf
    (1)json更多的用于可视化的序列化和反序列化,方便调试
    (2)protobuf是基于二进制的序列化和反序列化,更加强调效率
  2. 那么本文就来讲解一下json的使用,既然要使用json,那么首先就要安装json对应的jsoncpp库才能使用,那么使用如下指令安装即可
sudo yum install -y jsoncpp-devel
  1. 那么安装后观察到这么一个界面,并且结尾有complete!后,即为安装成功
  2. 那么头文件是在 /usr/include/jsoncpp 路径下,库文件是在 /lib64/libjsoncpp.so 路径下
// 查看头文件路径的指令
ls /usr/include/jsoncpp -d
// 查看头文件的指令
ll /usr/include/jsoncpp
// 查看库文件的指令
ll /lib64/libjsoncpp.so
  1. 所以我们就使用如上指令查看一下头文件和库文件
  2. 由于jsoncpp是第三方库,所以我们后续如果想要使用json对应的jsoncpp库的时候需要包含头文件所在位置,我们使用 ls /usr/include/jsoncpp 查看json的时候,发现是一个目录,那么我们cd进入这个json目录,然后ls查看这个目录下的头文件,json的头文件有很多,那么我们需要使用的是json.h,由于 /usr/include 是系统路径,g++进行编译的时候会去 /usr/include/ 这个系统路径下进行搜索,所以我们这里只需要包含json.h的相对路径即可 #include
  3. 经过上面的操作,此时g++已经可以找到json的头文件json.h了,但是还找不到库文件,其实库文件也已经被yum安装到了系统路径 /lib64/libjsoncpp.so 同样的g++进行编译的时候也会去 /lib64/ 这个系统路径下进行搜索,但是g++编译器并不知道要链接哪个库文件,所以这里我们需要在编译的时候需要使用-l选项告诉g++编译器,要去链接jsoncpp这个库文件,即编译的时候添加 -ljsoncpp
  4. 所以环境问题搞定,那么json该如何使用呢?首先就要使用Json::value定义一个万能对象root,然后json是一个基于key-value的使用方式,例如初始化的时候,想要设置x字段的值为1,那么就可以使用root[“x”] = 1,注意键值key必须是字符串的形式,那么value直接赋值等于1即可,Json的value可以放整数,字符,字符串等,甚至还可以再放一个万能对象
  5. 那么初始化搞定,接下来我们看一下序列化部分,那么我们就需要使用到Json::FastWriter或者Json::StyledWriter,其中Json::FastWriter不包含换行,Json::StyledWriter包含换行,两个选其一即可
  6. 那么我们先使用Json::FastWriter定义对象w,然后进行序列化,如何序列化呢?调用对象w中的write方法传入万能对象root即可,函数的返回值即为序列化的结果,所以我们打印一下序列化的结果看一下
#include 
#include 
#include 


int main()
{
    //万能对象
    Json::Value root;
    // 初始化 key-value
    root["x"] = 1;
    root["y"] = 1;
    root["op"] = '+';
    root["desc"] = "this is a + oper";

    //序列化
    Json::FastWriter w;
    std::string res = w.write(root);

    std::cout << res;


    return 0;
}

运行结果如下

  1. 那么我们使用Json::StyledWriter定义对象w调用write方法,查看序列化后的结果
#include 
#include 
#include 


int main()
{
    //万能对象
    Json::Value root;
    // 初始化 key-value
    root["x"] = 1;
    root["y"] = 1;
    root["op"] = '+';
    root["desc"] = "this is a + oper";

    //序列化
    // Json::FastWriter w;
    Json::StyledWriter w;
    std::string res = w.write(root);

    std::cout << res;

    return 0;
}

运行结果如下,可见Json::StyledWriter的观感更好一点

  1. 那么接下来就需要反序列化了,那么反序列化之前同样需要先使用Json::Value定义一个万能对象v,然后使用Json::Reader定义一个读对象r,然后调用读对象r中的pause方法,依次传入序列化的字符串,然后再传入万能对象v,经过调用parse之后,各个字段就被反序列化到了万能对象v中了
  2. 那么此时我们想要的各个字段就被放在了万能对象v中,接下来就需要进行解析了,同样的使用key-value的方式获取,后面加上要转换的类型即可,例如要解析的x是一个int整数类型,那么就可以使用v[“x”].asInt()进行获取,同样的char类型的变量其实也是整数家族的,那么同样需要使用v[“op”].asInt(),字符串类型需要使用v[“desc”].asString(),那么这些调用的返回值即为我们想要的对应类型的数据,所以我们定义对应的本地的类型的变量接收即可
  3. 最后我们逐个打印一下各个字段的数据验证是否反序列化,解析成功即可
#include 
#include 
#include 


int main()
{
    //万能对象
    Json::Value root;
    // 初始化 key-value
    root["x"] = 1;
    root["y"] = 1;
    root["op"] = '+';
    root["desc"] = "this is a + oper";

    //序列化
    // Json::FastWriter w;
    Json::StyledWriter w;
    std::string res = w.write(root);

    std::cout << res;

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

    std::cout << "解析结果" << std::endl;
    std::cout << x << std::endl;
    std::cout << y << std::endl;
    std::cout << op << std::endl;
    std::cout << desc << std::endl;


    return 0;
}

运行结果如下,无误

  1. 那么如果在万能对象root中放万能对象part呢?能否成功呢?所以root的键值key同样是字符串类型,value那么就是一个万能对象part了,root[“test”] = part1,如下
#include 
#include 
#include 


int main()
{
    Json::Value part1;
    part1["desc1"] = "i like cpp";
    part1["desc2"] = "i like linux";

    Json::Value root;
    // 初始化
    root["test"] = part1;
    
    // 序列化
    Json::FastWriter w;

    std::string res = w.write(root);
    std::cout << res;

    Json::Value v;
    Json::Reader r;

    // 反序列化
    r.parse(res, v);

    //解析
    Json::Value test = v["test"];

    std::cout << "解析结果" << std::endl;
    std::cout << test << std::endl;


    return 0;
}

运行结果如下,无误,万能对象的键值value可以放万能对象,并且万能对象中对 << 进行了运算符重载,所以我们可以直接使用std::cout << 进行打印万能对象

三、Protocol.hpp 使用Json平替我们自己的序列化和反序列化模块的代码

  1. 那么对于协议的定制分为 序列化 转码 反序列化 解码,那么对于转换和解码我们可以继续使用我们自己手写的,那么关于序列化和反序列化我们完全可以采用Json平替
  2. 并且我们可以再引入条件编译,通过是否定义宏,进而达到自由的使用我们自己的序列化和反序列化模块,或者使用Json的序列化和反序列化模块
// 这里默认不定义宏MySelf那么经过条件编译,采用的就是Json的序列化和反序列化的代码
// #define MySelf 1 

#ifdef MySelf
//我们自己的序列化和反序列化代码
#else
//Json的序列化和反序列化代码
#endif
  1. 所以下面我们就可以再我们的Protocol.hpp中使用Json平替我们自己的序列化和反序列化模块的代码了,替换过程很简单,所以这里小编就不过多解释了,如下
#pragma once

#include 
#include 
#include 


const std::string black_space_sep = " ";
const std::string protocol_sep = "
";

// 123 + 4 -> len
123 + 4

std::string Encode(const std::string& content)
{
    std::string package = std::to_string(content.size());
    package += protocol_sep;
    package += content;
    package += protocol_sep;

    return package;
}

// len
123 + 4
 -> 123 + 4
bool Decode(std::string& package, std::string* content)
{
    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);
    int total_len = len + 1 + len_str.size() + 1;
    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()
    {}

    //123 + 4
    bool Serialize(std::string* out)
    {
#ifdef MySelf        
        std::string s = std::to_string(x);
        s += black_space_sep;
        s += op;
        s += black_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还是Json::StyledWriter都可以
        // 这里看个人喜好自行选择即可
        // Json::FastWriter w;
        Json::StyledWriter w;
        *out = w.write(root);

        return true;
#endif
    }

    //123 + 4 -> 123 + 4
    // 1 + 1
    bool Deserialize(const std::string& in)
    {
#ifdef MySelf        
        size_t left = in.find(black_space_sep);
        if(left == std::string::npos)
            return false;
        std::string part_x = in.substr(0, left);

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

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

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

        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()
    {
        std::cout << "新请求构建完成: " << x << op << y << "=?" << std::endl;
    }

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

class Response
{
public:
    Response(int res, int c)
        : result(res)
        , code(c)
    {}

    Response()
    {}

    //100 0
    bool Serialize(std::string* out)
    {
#ifdef MySelf        
        std::string s = std::to_string(result);
        s += black_space_sep;
        s += std::to_string(code);
        *out = s;

        return true;
#else
        Json::Value root;
        //初始化
        root["result"] = result;
        root["code"] = code;

        Json::StyledWriter w;
        *out = w.write(root);

        return true;
#endif
    }
    //100 0
    bool Deserialize(const std::string& in)
    {
#ifdef MySelf        
        size_t pos = in.find(black_space_sep);
        if(pos == std::string::npos)
            return false;
        std::string part_left = in.substr(0, pos);
        std::string part_right = in.substr(pos + 1);

        result = std::stoi(part_left);
        code = std::stoi(part_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;
    }

public:
    int result;
    int code;
};
  1. 那么接下来为了便于测试,那么对于客户端的代码ClientCal.hpp,的连续write的代码注释掉,即客户端生成一次任务,然后write发送一次任务,read接收一次服务器的响应结果,循环5次
#include 
#include 
#include 
#include 
#include 
#include "Socket.hpp"
#include "Protocol.hpp"

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

// ./clientcal serverip serverport
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));
    std::string opers = "+-*/%~?@=";
    int cnt = 1;

    std::string inbuffer_stream;
    while(cnt <= 5)
    {
        std::cout << "------------第" << cnt << "次测试-------------" << std::endl;
        int x = rand() % 10 + 1;
        usleep(123);
        int y = rand() % 10;
        usleep(654);
        char oper = opers[rand() % opers.size()];
        
        Request req(x, y, oper);
        req.DebugPrint();

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

        char buffer[128];
        ssize_t n = read(sockfd.Fd(), buffer, sizeof(buffer));
        if(n > 0)
        {
            buffer[n] = 0;
            inbuffer_stream += buffer;
            std::cout << inbuffer_stream << std::endl;

            std::string content;
            bool r = Decode(inbuffer_stream, &content);
            assert(r);

            Response resp;
            r = resp.Deserialize(content);
            assert(r);
            
            resp.DebugPrint();
        }
        else if(n == 0)
            break;
        else  
            break;

        std::cout << "----------------------------------" << std::endl;

        sleep(2);
        cnt++;
    }

    sockfd.Close();

    return 0;
}
  1. 那么为了便于测试,对于服务器模块的代码TcpServer.hpp,服务器这边仅仅打印流缓冲区inbuffer_stream的内容,对于结果部分服务器这边不再进行打印,而是由客户端进行打印即可
#pragma once

#include 
#include 
#include 
#include "Socket.hpp"
#include "Log.hpp"

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


class TcpServer
{
public:
    TcpServer(int16_t port, func_t callback)
        : port_(port)
        , callback_(callback)
    {}
    ~TcpServer()
    {
        listensock_.Close();
    }
    
    bool InitServer()
    {
        listensock_.Socket();
        listensock_.Bind(port_);
        listensock_.Listen();
        lg(Info, "init server ... done");

        return true;
    }

    void Start()
    {
        signal(SIGCHLD, SIG_IGN);
        signal(SIGPIPE, SIG_IGN);
        while(true)
        {
            std::string clientip;
            uint16_t clientport;
            int sockfd = listensock_.Accept(&clientip, &clientport);
            if(sockfd < 0)
                continue;
            lg(Info, "get a new link, sockfd: %d, clientip: %s, clientport: %d", sockfd, clientip.c_str(), clientport);
            if(fork() == 0)
            {
                //child
                listensock_.Close();
                std::string inbuffer_stream;
                while(true)
                {
                    char buffer[1280];
                    ssize_t n = read(sockfd, buffer, sizeof(buffer));
                    if(n > 0)
                    {
                        buffer[n] = 0;
                        inbuffer_stream += buffer;

                        lg(Debug, "Debug:request
%s", inbuffer_stream.c_str());

                        while(true)
                        {
                            std::string info = callback_(inbuffer_stream);
                            if(info.empty())
                                break;
                                
                            // lg(Debug, "Debug:response
%s", info.c_str());
                            // lg(Debug, "Debug:inbuffer_stream
%s", inbuffer_stream.c_str());

                            write(sockfd, info.c_str(), info.size());
                        }

                    }
                    else if(n == 0)
                        break;
                    else
                        break;
                }
                close(sockfd);
                exit(0);
            }
            close(sockfd);
        }
    }


private:
    uint16_t port_;
    Sock listensock_;
    func_t callback_;
};

测试

  1. 此时我们的代码中是没有进行宏定义#define MySelf 1的,所以也就意味着默认对于序列化和反序列化的代码会采用Json的代码执行,那么下面我们对于makefile同样需要在编译的时候带上 -ljsoncpp 告诉编译器需要链接jsoncpp库
.PHONY:all
all:servercal clientcal

Lib=-ljsoncpp

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

.PHONY:clean
clean:
	rm -f servercal clientcal
  1. 运行结果如下,无误
  2. 其实那么我们在Protocol.hpp中使用#define MySelf 1定义了MySelf之后,我们采用的序列化和反序列化的方案就从Json变成我们自己手写的了,但是今天小编不想采用这种方式,而是想采用在g++编译器编译的时候定义宏MySelf,那么就需要使用到-D选项定义MySelf了,即-DMySelf=1
.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 -g $(Lib) $(Flag)

.PHONY:clean
clean:
	rm -f servercal clientcal
  1. 重新编译,运行结果如下,果然采用了我们自己的序列化和反序列化模块的代码
  2. 那么此时就便捷多了,如果我们想要采用Json的序列化和反序列化的代码,那么只需要使用#将-DMySelf=1注释掉即可
.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 -g $(Lib) $(Flag)

.PHONY:clean
clean:
	rm -f servercal clientcal
  1. 重新编译,运行结果如下,那么采用的就是Json的序列化和反序列化模块的代码
  2. 所以说,我们再往下考虑一下条件编译的作用,例如网易云音乐的vip功能是如何实现的呢?你充值了vip之后,那么就条件编译给你vip部分的代码,那么你就可以享受vip功能的服务了,如果你不充值vis,那么就条件编译给你普通部分的代码,那么你就只能享受普通的服务

四、服务器守护进程化

使用我们自己的Daemon

  1. 但是这个TCP服务器还不够好,所以我想要这个服务器可以一直运行,不受用户登录和退出的影响,所以我们需要让服务器守护进程化,那么如何让服务器守护进程化呢?小编在之前的文章中已经进行讲解了第四点是讲解的守护进程的原理,第五点TCP服务器与客户端实现——守护进程版是讲解的守护进程的具体实现,详情请点击<——
  2. 所以我们直接将之前文章中守护进程的代码拿过来,包含头文件,即可让我们的服务器守护进程化
// Daemon.hpp
#include 
#include 
#include 
#include 
#include 
#include 
#include 

const std::string nullfile = "/dev/null";

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

    //创建子进程,然后终止父进程,子进程会拷贝父进程
    //资源,例如文件描述符,页表,地址空间,代码等,
    //然后由子进程继续向后执行代码
    if(fork() > 0)
        exit(0);

    //创建session会话,子进程成为服务器,然后守护进程化
    setsid();

    //更改当前子进程代表的服务器进程的工作目录
    if(!cwd.empty())
        chdir(cwd.c_str());

    //打开 /dev/null 文件
    int fd = open(nullfile.c_str(), O_WRONLY);

    //重定向 标准输入,标准输出,标准错误
    if(fd > 0)
    {
        dup2(fd, 0);
        dup2(fd, 1);
        dup2(fd, 2);
        close(fd);
    }
}
  1. 那么我们在ServerCal.cc文件中包含Deamon.hpp,然后服务器初始化之后,服务器启动之前,然后调用Daemon()即可让我们的服务器守护进程化,服务器守护进程化之后,服务器的默认工作路径是/根目录
#include 
#include 
#include 
#include "TcpServer.hpp"
#include "ServerCal.hpp"
#include "Daemon.hpp"

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


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

    ServerCal cal;
    auto newcallback = std::bind(&ServerCal::Calculator, &cal, std::placeholders::_1);
    
    TcpServer* tsvp = new TcpServer(serverport, newcallback);

    tsvp->InitServer();

    // 服务器守护进程化
    Daemon();

    tsvp->Start();

    delete tsvp;


    return 0;
}
  1. 如下,服务器启动无误,此时服务器已经成守护进程化了
// 使用如下指令查看服务器servercal
ps ajx | head -1 && ps ajx | grep servercal | grep -v grep

  1. 那么接下来我们运行客户端,测试是否可以正常和服务端进行通信,如下无误
  2. 那么接下来我们使用kill -9 PID杀掉守护进程化的服务器,如下,无误

daemon

  1. 其实这里已经提供了系统调用接口daemon,那么我们直接让我们的服务器调用daemon即可让我们的服务器守护进程化,那么第一个参数nochdir默认传入0即可,表示默认服务器守护进程化之后处于系统的根目录下运行,第二个参数noclose默认也传入0即可,表示将标准输入,标准输出,标准错误都重定向到/dev/null,即服务器守护进程化后的输入和输出都丢弃
  2. 那么我们在ServerCal.cc中,服务器初始化之后,服务器启动之前,然后调用daemon()即可让我们的服务器守护进程化
#include 
#include 
#include 
#include 
#include "TcpServer.hpp"
#include "ServerCal.hpp"
#include "Daemon.hpp"

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


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

    ServerCal cal;
    auto newcallback = std::bind(&ServerCal::Calculator, &cal, std::placeholders::_1);
    
    TcpServer* tsvp = new TcpServer(serverport, newcallback);

    tsvp->InitServer();

    // 服务器守护进程化
    // Daemon();
    daemon(0, 0);

    tsvp->Start();

    delete tsvp;


    return 0;
}
  1. 运行结果如下,无误
  2. 同样的,我们使用kill -9 PID杀掉守护进程化的服务器即可
  3. 至此我们的TCP服务端网络版本计算器的优化结束

五、重谈OSI七层模型

  1. 所以此时我们再来看OSI七层模型,那么思考操作系统中为什么没有实现应用层,表示层,会话层呢?


  1. 因为不同的应用场景需要使用不同的应用层协议,表示层协议,会话层协议,就拿小编实现的TCP服务端和客户端的实现——网络版本计算器,其实就包含了应用层协议,表示层协议,会话层协议
  2. 应用层协议即我们实现的ServerCal.hpp计算器的实现,那么根据不同的场景需求,会实现不同的功能,例如进行电子邮件,文件传输等功能,所以需要程序员根据不同的场景需求决定应该设计什么样子的应用层协议
  3. 表示层协议即我们实现的自定义协议的定制Protocol.hpp序列化和反序列化,解码,转码,主机字节序列转换成网络字节序列,网络字节序列转换成主机字节序列等,同样根据不同的场景,有可能传输的是音频,图片,视频等,那么如果是视频,那么结构化的数据中就需要包含视频的大小,视频的格式,视频的二进制流等数据,所以也就决定了需要程序员根据不同的传输内容去定制的不同的表示层协议
  4. 会话层协议那么就在TcpSerever.hpp中实现,那么我们今天是服务器接收连接,然后fork子进程,由子进程负责建立与客户端进行通信的会话,负责维护会话,当不需要了之后将会话关闭,那么那么我不仅仅可以由子进程维护,这里我们也可以采用多线程,线程池的形式去进行会话的创建,维护,断开关闭,并且不同的方案会有不同的会话层协议,所以会话层也需要根据不同的场景需求,然后去定制不同的会话层协议
  5. 所以说由于不同的场景需求不同,应用层协议,表示层协议,会话层协议千变万化,操作系统无法实现出那么多的场景的应用层协议,表示层协议,会话层协议,所以操作系统以不变应万变,将传输层,网络层实现在linux操作系统(内核)中,数据链路层在驱动程序中实现即可

六、源代码

ClientCal.cc

#include 
#include 
#include 
#include 
#include 
#include "Socket.hpp"
#include "Protocol.hpp"

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

// ./clientcal serverip serverport
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));
    std::string opers = "+-*/%~?@=";
    int cnt = 1;

    std::string inbuffer_stream;
    while(cnt <= 5)
    {
        std::cout << "------------第" << cnt << "次测试-------------" << std::endl;
        int x = rand() % 10 + 1;
        usleep(123);
        int y = rand() % 10;
        usleep(654);
        char oper = opers[rand() % opers.size()];
        
        Request req(x, y, oper);
        req.DebugPrint();

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

        char buffer[128];
        ssize_t n = read(sockfd.Fd(), buffer, sizeof(buffer));
        if(n > 0)
        {
            buffer[n] = 0;
            inbuffer_stream += buffer;
            std::cout << inbuffer_stream << std::endl;

            std::string content;
            bool r = Decode(inbuffer_stream, &content);
            assert(r);

            Response resp;
            r = resp.Deserialize(content);
            assert(r);
            
            resp.DebugPrint();
        }
        else if(n == 0)
            break;
        else  
            break;

        std::cout << "----------------------------------" << std::endl;

        sleep(2);
        cnt++;
    }

    sockfd.Close();

    return 0;
}

Daemon.hpp

#include 
#include 
#include 
#include 
#include 
#include 
#include 

const std::string nullfile = "/dev/null";

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

    //创建子进程,然后终止父进程,子进程会拷贝父进程
    //资源,例如文件描述符,页表,地址空间,代码等,
    //然后由子进程继续向后执行代码
    if(fork() > 0)
        exit(0);

    //创建session会话,子进程成为服务器,然后守护进程化
    setsid();

    //更改当前子进程代表的服务器进程的工作目录
    if(!cwd.empty())
        chdir(cwd.c_str());

    //打开 /dev/null 文件
    int fd = open(nullfile.c_str(), O_WRONLY);

    //重定向 标准输入,标准输出,标准错误
    if(fd > 0)
    {
        dup2(fd, 0);
        dup2(fd, 1);
        dup2(fd, 2);
        close(fd);
    }
}

Log.hpp

#pragma once

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

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

    ~Log()
    {}

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

    void operator()(int level, const char* format, ...)
    {
        //默认部分 = 日志等级 + 日志时间
        time_t t = time(nullptr);
        struct tm* ctime = localtime(&t);
        char leftbuffer[SIZE];
        snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(), 
        ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday, 
        ctime->tm_hour, ctime->tm_min, ctime->tm_sec);

        va_list s;
        va_start(s, format);
        char rightbuffer[SIZE];
        vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
        va_end(s);

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

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


private:
    int printMethod;
    std::string path;
};

Log lg;

makefile

.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 -g $(Lib) $(Flag)

.PHONY:clean
clean:
	rm -f servercal clientcal

Protocol.hpp

#pragma once

#include 
#include 
#include 


const std::string black_space_sep = " ";
const std::string protocol_sep = "
";

// 123 + 4 -> len
123 + 4

std::string Encode(const std::string& content)
{
    std::string package = std::to_string(content.size());
    package += protocol_sep;
    package += content;
    package += protocol_sep;

    return package;
}

// len
123 + 4
 -> 123 + 4
bool Decode(std::string& package, std::string* content)
{
    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);
    int total_len = len + 1 + len_str.size() + 1;
    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()
    {}

    //123 + 4
    bool Serialize(std::string* out)
    {
#ifdef MySelf        
        std::string s = std::to_string(x);
        s += black_space_sep;
        s += op;
        s += black_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还是Json::StyledWriter都可以
        // 这里看个人喜好自行选择即可
        // Json::FastWriter w;
        Json::StyledWriter w;
        *out = w.write(root);

        return true;
#endif
    }

    //123 + 4 -> 123 + 4
    // 1 + 1
    bool Deserialize(const std::string& in)
    {
#ifdef MySelf        
        size_t left = in.find(black_space_sep);
        if(left == std::string::npos)
            return false;
        std::string part_x = in.substr(0, left);

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

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

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

        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()
    {
        std::cout << "新请求构建完成: " << x << op << y << "=?" << std::endl;
    }

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

class Response
{
public:
    Response(int res, int c)
        : result(res)
        , code(c)
    {}

    Response()
    {}

    //100 0
    bool Serialize(std::string* out)
    {
#ifdef MySelf        
        std::string s = std::to_string(result);
        s += black_space_sep;
        s += std::to_string(code);
        *out = s;

        return true;
#else
        Json::Value root;
        //初始化
        root["result"] = result;
        root["code"] = code;

        Json::StyledWriter w;
        *out = w.write(root);

        return true;
#endif
    }
    //100 0
    bool Deserialize(const std::string& in)
    {
#ifdef MySelf        
        size_t pos = in.find(black_space_sep);
        if(pos == std::string::npos)
            return false;
        std::string part_left = in.substr(0, pos);
        std::string part_right = in.substr(pos + 1);

        result = std::stoi(part_left);
        code = std::stoi(part_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;
    }

public:
    int result;
    int code;
};

ServerCal.cc

#include 
#include 
#include 
#include 
#include "TcpServer.hpp"
#include "ServerCal.hpp"
#include "Daemon.hpp"

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


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

    ServerCal cal;
    auto newcallback = std::bind(&ServerCal::Calculator, &cal, std::placeholders::_1);
    
    TcpServer* tsvp = new TcpServer(serverport, newcallback);

    tsvp->InitServer();

    // 服务器守护进程化
    // Daemon();
    daemon(0, 0);

    tsvp->Start();

    delete tsvp;


    return 0;
}





// int main()
// {
//     // Request req(123, 654, '+');
//     // std::string str;
//     // req.Serialize(&str);
//     // std::cout << str << std::endl;

//     // std::string package = Encode(str);
//     // std::cout << package << std::endl;

//     // std::string content;
//     // Decode(package, &content);

//     // Request requ;
//     // requ.Deserialize(content);
//     // std::cout << requ.x_ << std::endl;
//     // std::cout << requ.op_ << std::endl;
//     // std::cout << requ.y_ << std::endl;


//     // Response res(100, 0);
//     // std::string str;
//     // res.Serialize(&str);
//     // std::cout << str << std::endl;

//     // std::string package = Encode(str);
//     // std::cout << package << std::endl;


//     // std::string content;
//     // Decode(package, &content);
//     // std::cout << content << std::endl;

//     // Response resp;
//     // resp.Deserialize(content);
//     // std::cout << resp.result_ << std::endl;
//     // std::cout << resp.code_ << std::endl;


//     return 0;
// }

ServerCal.hpp

#pragma once

#include "Log.hpp"
#include "Protocol.hpp"

enum
{
    Div_Zero = 1,
    Mod_Zero,
    Other_Oper,
};

class ServerCal
{
public:
    ServerCal()
    {}

    Response CalculatorHelper(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 "";
        //计算
        Response resp = CalculatorHelper(req);

        content = "";
        // 序列化
        resp.Serialize(&content);
        // 转码
        content = Encode(content);

        return content;
    }

    ~ServerCal()
    {}
};

Socket.hpp

#pragma once

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


const int backlog = 10;

enum{
    SocketErr = 1,
    BindErr,
    ListenErr,
};


class Sock
{
public:
    Sock()
    {}

    void Socket()
    {
        sockfd_ = socket(AF_INET, SOCK_STREAM, 0);
        if(sockfd_ < 0)
        {
            lg(Fatal, "socket error, %s : %d", strerror(errno), errno);
            exit(SocketErr);
        }
    }

    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;
        socklen_t len = sizeof(local);

        if(bind(sockfd_, (struct sockaddr*)&local, len) < 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 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(Warning, "accept error, %s : %d", strerror(errno), errno);
            return -1;
        }
        
        char ipstr[128];
        inet_ntop(AF_INET, &(peer.sin_addr), ipstr, sizeof(ipstr));
        *clientip = ipstr;
        *clientport = ntohs(peer.sin_port);

        return newfd;
    }
    
    bool Connect(const std::string& serverip, uint16_t serverport)
    {
        struct sockaddr_in peer;
        memset(&peer, 0, sizeof(peer));
        peer.sin_family = AF_INET;
        peer.sin_port = htons(serverport);
        inet_pton(AF_INET, serverip.c_str(), &(peer.sin_addr));
        socklen_t len = sizeof(peer);

        int n = connect(sockfd_, (struct sockaddr*)&peer, len);
        if(n == -1)
        {
            std::cerr << "connect to " << serverip << ':' << serverport << "error" << std::endl;
            return false;
        }

        return true;
    }

    void Close()
    {
        if(sockfd_ > 0)
        {
            close(sockfd_);
        }
    }

    int Fd()
    {
        return sockfd_;
    }
    
    ~Sock()
    {}
private:
    int sockfd_;
};

TcpServer.hpp

#pragma once

#include 
#include 
#include 
#include "Socket.hpp"
#include "Log.hpp"

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


class TcpServer
{
public:
    TcpServer(int16_t port, func_t callback)
        : port_(port)
        , callback_(callback)
    {}
    ~TcpServer()
    {
        listensock_.Close();
    }
    
    bool InitServer()
    {
        listensock_.Socket();
        listensock_.Bind(port_);
        listensock_.Listen();
        lg(Info, "init server ... done");

        return true;
    }

    void Start()
    {
        signal(SIGCHLD, SIG_IGN);
        signal(SIGPIPE, SIG_IGN);
        while(true)
        {
            std::string clientip;
            uint16_t clientport;
            int sockfd = listensock_.Accept(&clientip, &clientport);
            if(sockfd < 0)
                continue;
            lg(Info, "get a new link, sockfd: %d, clientip: %s, clientport: %d", sockfd, clientip.c_str(), clientport);
            if(fork() == 0)
            {
                //child
                listensock_.Close();
                std::string inbuffer_stream;
                while(true)
                {
                    char buffer[1280];
                    ssize_t n = read(sockfd, buffer, sizeof(buffer));
                    if(n > 0)
                    {
                        buffer[n] = 0;
                        inbuffer_stream += buffer;

                        lg(Debug, "Debug:request
%s", inbuffer_stream.c_str());

                        while(true)
                        {
                            std::string info = callback_(inbuffer_stream);
                            if(info.empty())
                                break;
                                
                            // lg(Debug, "Debug:response
%s", info.c_str());
                            // lg(Debug, "Debug:inbuffer_stream
%s", inbuffer_stream.c_str());

                            write(sockfd, info.c_str(), info.size());
                        }

                    }
                    else if(n == 0)
                        break;
                    else
                        break;
                }
                close(sockfd);
                exit(0);
            }
            close(sockfd);
        }
    }


private:
    uint16_t port_;
    Sock listensock_;
    func_t callback_;
};

总结

以上就是今天的博客内容啦,希望对读者朋友们有帮助
水滴石穿,坚持就是胜利,读者朋友们可以点个关注
点赞收藏加关注,找到小编不迷路!

本文地址:https://www.yitenyun.com/3377.html

搜索文章

Tags

#ios面试 #ios弱网 #断点续传 #ios开发 #objective-c #ios #ios缓存 #服务器 #python #pip #conda #远程工作 香港站群服务器 多IP服务器 香港站群 站群服务器 #kubernetes #笔记 #平面 #容器 #linux #学习方法 #运维 #fastapi #html #css #进程控制 #低代码 #爬虫 #音视频 #cpolar #docker #后端 #数据库 #Trae #IDE #AI 原生集成开发环境 #Trae AI #内网穿透 #网络 #开发语言 #云原生 #iventoy #VmWare #OpenEuler #人工智能 #node.js #Conda # 私有索引 # 包管理 #开源 #RTP over RTSP #RTP over TCP #RTSP服务器 #RTP #TCP发送RTP #MobaXterm #ubuntu #vscode #mobaxterm #深度学习 #计算机视觉 #android #腾讯云 #c# #kylin #物联网 #websocket #tcp/ip #多个客户端访问 #IO多路复用 #回显服务器 #TCP相关API #学习 #算法 #大数据 #web安全 #安全 #数信院生信服务器 #Rstudio #生信入门 #生信云服务器 #nginx #claude #华为 #ModelEngine #架构 #mvp #个人开发 #设计模式 #java #github #git #金融 #大模型 #mcp #金融投资Agent #Agent #我的世界 #n8n #本地部署 #windows #hadoop #hbase #hive #zookeeper #spark #kafka #flink #ssh #qt #C++ #云计算 #我的世界服务器搭建 #minecraft #udp #c++ #c语言 #网络协议 #jar #面试 #http #cpp #项目 #高并发 #NPU #CANN #gemini #gemini国内访问 #gemini api #gemini中转搭建 #Cloudflare #vue.js #前端 #vue #阿里云 #screen 命令 #stm32 #macos #JumpServer #堡垒机 #振镜 #振镜焊接 #ide #AI编程 #单元测试 #集成测试 #pycharm #gpu算力 #编辑器 #DisM++ # GLM-4.6V # 系统维护 #京东云 #性能优化 #SRS #流媒体 #直播 #守护进程 #复用 #screen #umeditor粘贴word #ueditor粘贴word #ueditor复制word #ueditor上传word图片 #YOLOFuse # Base64编码 # 多模态检测 #unity3d #游戏 #服务器框架 #Fantasy #缓存 #mamba #Android #Bluedroid #智能手机 #压力测试 #MCP #科技 #自然语言处理 #神经网络 #libosinfo #todesk #jenkins #unity #游戏引擎 #需求分析 #scala #测试用例 #测试工具 #Dell #PowerEdge620 #内存 #硬盘 #RAID5 #MCP服务器 #flask #spring boot #部署 #GPU服务器 #8U #硬件架构 #fiddler #centos #智能路由器 #5G #搜索引擎 #debian #C2000 #TI #实时控制MCU #AI服务器电源 #运维开发 #mysql #东方仙盟 #银河麒麟 #系统升级 #信创 #国产化 #1024程序员节 #嵌入式硬件 #SAP #ebs #metaerp #oracle ebs #DeepSeek #蓝耘智算 #910B #昇腾 #AIGC #ida #ai #Anaconda配置云虚拟环境 #pytorch #密码学 #可信计算技术 #Nacos #web #微服务 #RAID #RAID技术 #磁盘 #存储 #华为云 #测评 #CCE #Dify-LLM #Flexus #cursor #elasticsearch #ollama #llm #SPA #单页应用 #django #web3.py #信息与通信 #信号处理 #tcpdump #RustDesk #IndexTTS 2.0 #本地化部署 #麒麟OS #ms-swift # 大模型 # 模型训练 #毕业设计 #车辆排放 #oracle #AI #工具集 #PyTorch # Triton # 高并发部署 #transformer #javascript #银河麒麟高级服务器操作系统安装 #银河麒麟高级服务器V11配置 #设置基础软件仓库时出错 #银河麒高级服务器系统的实操教程 #生产级部署银河麒麟服务系统教程 #Linux系统的快速上手教程 #epoll #sqlite #php #java-ee #电气工程 #C# #PLC #单片机 #golang #rdp #sql #Dify #ARM架构 #鲲鹏 #jmeter #功能测试 #软件测试 #自动化测试 #职场和发展 #大模型部署 #mindie #大模型推理 #自动化 #maven #gitlab #课程设计 #SSH反向隧道 # Miniconda # Jupyter远程访问 #EMC存储 #存储维护 #NetApp存储 #yum #windows11 #microsoft #系统修复 #chatgpt #codex #NAS #Termux #Samba #Linux #三维 #3D #三维重建 #万悟 #联通元景 #智能体 #镜像 #idm #微信小程序 #小程序 #微信 #健身房预约系统 #健身房管理系统 #健身管理系统 #react.js #北京百思可瑞教育 #百思可瑞教育 #北京百思教育 #apache #鸭科夫 #逃离鸭科夫 #鸭科夫联机 #鸭科夫异地联机 #开服 #risc-v #deepseek #SSH公钥认证 # PyTorch # 安全加固 #练习 #基础练习 #数组 #循环 #九九乘法表 #计算机实现 #经验分享 #dynadot #域名 #esb接口 #走处理类报异常 #bug菌问答团队 #vllm #YOLO #ffmpeg #数据分析 #数据挖掘 #CUDA #Triton #交互 #arm开发 #企业开发 #ERP #项目实践 #.NET开发 #C#编程 #编程与数学 #语言模型 #昇腾300I DUO #银河麒麟部署 #银河麒麟部署文档 #银河麒麟linux #银河麒麟linux部署教程 #intellij-idea #idea #intellij idea #dify #ui #opencv #Qwen3-14B # 大模型部署 # 私有化AI #vnstat #监控 #redis #文心一言 #AI智能体 #vp9 #攻防演练 #Java web #漏洞 #红队 #leetcode #ssl #AI 推理 #NV #SSH跳板机 # Python3.11 #fpga开发 #LVDS #高速ADC #DDR #API限流 # 频率限制 # 令牌桶算法 #驱动开发 #chrome #处理器 #iBMC #UltraISO #黑群晖 #虚拟机 #无U盘 #纯小白 #支付 #数据结构 #jvm #排序算法 #Gunicorn #WSGI #Flask #并发模型 #容器化 #Python #性能调优 #蓝湖 #Axure原型发布 #gitea #screen命令 #门禁 #梯控 #智能一卡通 #门禁一卡通 #消费一卡通 #智能梯控 #一卡通 #源代码管理 #超时设置 #客户端/服务器 #网络编程 # 目标检测 #管道Pipe #system V #ai编程 #网络安全 #机器人 #llama #muduo库 #uv #uvx #uv pip #npx #Ruff #pytest #YOLO26 #目标检测 #操作系统 #国产化OS #milvus #springboot #知识库 #语音识别 #react native #web server #请求处理流程 #SSH # 批量管理 #个人博客 #ASR #SenseVoice #星图GPU #中间件 #MQTT协议 #交通物流 #C语言 #vivado license #CVE-2025-68143 #CVE-2025-68144 #CVE-2025-68145 #html5 #asp.net #rocketmq #selenium #prometheus #grafana #svn #证书 #scrapy #fabric #postgresql #openHiTLS #TLCP #DTLCP #商用密码算法 #ONLYOFFICE #MCP 服务器 #laravel #tomcat # 双因素认证 # TensorFlow #毕设 #服务器繁忙 #蓝牙 #LE Audio #BAP #serverless #嵌入式编译 #ccache #distcc #puppeteer #adb #连接数据库报错 #链表 #仙盟创梦IDE #硬件工程 #智能家居 #动态规划 #负载均衡 #pyqt #进程 #进程创建与终止 #shell #xlwings #Excel #DNS #dlms #dlms协议 #逻辑设备 #逻辑设置间权限 #mybatis #安全威胁分析 #C #spring cloud #spring #Spring AI #STDIO传输 #SSE传输 #WebMVC #WebFlux #bootstrap #nfs #iscsi #企业微信 #swagger #visual studio code #prompt #大模型学习 #文件管理 #文件服务器 #mariadb #树莓派4b安装系统 #STDIO协议 #Streamable-HTTP #McpTool注解 #服务器能力 #pve #wsl #电脑 #LangGraph #CLI #JavaScript #langgraph.json #paddleocr #分布式 #KMS激活 #计算机网络 #jdk #排序 #ddos #系统架构 #CSDN #数据仓库 #numpy #wordpress #雨云 #LobeChat #vLLM #GPU加速 #ansible #人脸识别sdk #视频编解码 #人脸识别 #海外服务器安装宝塔面板 #翻译 #开源工具 #创业创新 #业界资讯 #openlayers #bmap #tile #server #langchain #大模型开发 #程序员 #CosyVoice3 # 语音合成 #机器学习 #eBPF #Puppet # IndexTTS2 # TTS #简单数论 #埃氏筛法 #TCP #客户端 #嵌入式 #DIY机器人工房 #x86_64 #数字人系统 #说话人验证 #声纹识别 #CAM++ #rtsp #转发 #unix #信令服务器 #Janus #MediaSoup #其他 #SQL注入主机 #json #rust #CVE-2025-61686 #路径遍历高危漏洞 #大模型教程 #AI大模型 #结构体 #制造 #webrtc #ping通服务器 #读不了内网数据库 #推荐算法 #devops #戴尔服务器 #戴尔730 #装系统 #渗透测试 #黑客技术 #计算机 #文件上传漏洞 #mcu #ThingsBoard MCP # GPU租赁 # 自建服务器 #HeyGem # 服务器IP访问 # 端口映射 #CTF #A2A #GenAI #遛狗 #SSE # AI翻译机 # 实时翻译 #bug #sqlserver #聊天小程序 #心理健康服务平台 #心理健康系统 #心理服务平台 #心理健康小程序 #数据安全 #注入漏洞 # 一锤定音 # 大模型微调 #nodejs #具身智能 #https #SSH密钥 # CUDA #dba # ControlMaster #ETL管道 #RAG #向量存储 #数据预处理 #DocumentReader #硬件 #Fun-ASR # 语音识别 # WebUI #harmonyos #HarmonyOS APP #密码 #firefox #safari #LoRA # RTX 3090 # lora-scripts #Docker #le audio #低功耗音频 #通信 #连接 #spring ai #oauth2 #数据可视化 #nmodbus4类库使用教程 #docker-compose #目标跟踪 #rtmp #PowerBI #企业 #windbg分析蓝屏教程 #jupyter #AI电商客服 #Streamlit #Qwen #AI聊天机器人 #Buck #NVIDIA #算力 #交错并联 #DGX #ROS # 局域网访问 # 批量处理 #内存治理 #cosmic #googlecloud #串口服务器 #Modbus #IFix #VibeVoice # 高温监控 #c++20 # 远程连接 #fs7TF #npu #Miniconda # 环境迁移 #大剑师 #nodejs面试题 #matplotlib #AutoDL #安全架构 #Llama-Factory # 树莓派 # ARM架构 #uni-app #H5 #跨域 #发布上线后跨域报错 #请求接口跨域问题解决 #跨域请求代理配置 #request浏览器跨域 #gerrit #opc ua #opc #GB28181 #SIP信令 #SpringBoot #视频监控 #远程软件 #WT-2026-0001 #QVD-2026-4572 #smartermail #游戏机 # GLM-TTS # 数据安全 #银河麒麟操作系统 #openssh #华为交换机 #信创终端 #xshell #host key #UDP的API使用 #TTS私有化 # IndexTTS # 音色克隆 #ESP32 # OTA升级 # 黄山派 #内网 # 网络延迟 #指针 #anaconda #虚拟环境 # Connection refused #系统管理 #服务 #teamviewer #Emby #视频 #代理服务器 #rsync # 数据同步 #ip #bash #Modbus-TCP #blender #设计师 #图像处理 #游戏美术 #技术美术 # ARM服务器 # 大模型推理 #notepad++ #ambari #arm #多线程 #claudeCode #content7 #跳槽 #工作 #挖矿 #Linux病毒 #turn #sql注入 #网安应急响应 #odoo #LLM #chat #微PE # GLM # 服务连通性 #azure #Apple AI #Apple 人工智能 #FoundationModel #Summarize #SwiftUI #大语言模型 #ceph #远程开发 # 高并发 #appche #数据恢复 #视频恢复 #视频修复 #RAID5恢复 #流媒体服务器恢复 #Ubuntu #aws #muduo #TcpServer #accept #高并发服务器 #哈希算法 #服务器开启 TLS v1.2 #IISCrypto 使用教程 #TLS 协议配置 #IIS 安全设置 #服务器运维工具 #ftp #sftp #uniapp #合法域名校验出错 #服务器域名配置不生效 #request域名配置 #已经配置好了但还是报错 #uniapp微信小程序 #框架搭建 #YOLO识别 #YOLO环境搭建Windows #YOLO环境搭建Ubuntu #状态模式 #AI-native #LangFlow # 轻量化镜像 # 边缘计算 #Tokio #华为od #华为机试 #Java #OpenHarmony #版本控制 #Git入门 #开发工具 #代码托管 #SSH跳转 #TTS #openEuler #go # GPU集群 #Gateway #认证服务器集成详解 #计算几何 #斜率 #方向归一化 #叉积 #Ansible # CosyVoice3 # 批量部署 #samba #RSO #机器人操作系统 #glibc #媒体 #远程连接 #能源 #汽车 #cpu #Socket #套接字 #I/O多路复用 #字节序 #weston #x11 #x11显示服务器 #研发管理 #禅道 #禅道云端部署 #WinSCP 下载安装教程 #SFTP #FTP工具 #服务器文件传输 #zabbix #手机h5网页浏览器 #安卓app #苹果ios APP #手机电脑开启摄像头并排查 #深度优先 #DFS #语音生成 #集成学习 #AI写作 #winscp #AI部署 # ms-swift #PN 结 #后端框架 #服务器线程 # SSL通信 # 动态结构体 #RWK35xx #语音流 #实时传输 #node #超算中心 #PBS #lsf #excel #报表制作 #职场 #信息可视化 #用数据讲故事 #lvs # 数字人系统 # 远程部署 #STUN # TURN # NAT穿透 #MCP服务器注解 #异步支持 #方法筛选 #声明式编程 #自动筛选机制 #前端框架 #.net #JNI #CPU #pxe #express #cherry studio #Node.js # child_process #free #vmstat #sar #KMS #slmgr #宝塔面板部署RustDesk #RustDesk远程控制手机 #手机远程控制 #铁路桥梁 #DIC技术 #箱梁试验 #裂纹监测 #四点弯曲 #rustdesk #p2p #可再生能源 #绿色算力 #风电 #麦克风权限 #访问麦克风并录制音频 #麦克风录制音频后在线播放 #用户拒绝访问麦克风权限怎么办 #uniapp 安卓 苹果ios #将音频保存本地或上传服务器 #运维工具 #GLM-4.6V-Flash-WEB # AI视觉 # 本地部署 #网络攻击模型 #Discord机器人 #云部署 #程序那些事 #AI应用编程 # 自动化运维 #r语言 #spine #若依 #TRO #TRO侵权 #TRO和解 #服务器IO模型 #非阻塞轮询模型 #多任务并发模型 #异步信号模型 #多路复用模型 #系统安全 #ipmitool #BMC # 黑屏模式 # TTS服务器 #前端开发 #EN4FE #领域驱动 #自由表达演说平台 #演说 #程序员创富 #移动端h5网页 #调用浏览器摄像头并拍照 #开启摄像头权限 #拍照后查看与上传服务器端 #摄像头黑屏打不开问题 #3d #工业级串口服务器 #串口转以太网 #串口设备联网通讯模块 #串口服务器选型 #okhttp #embedding #IndexTTS2 # 阿里云安骑士 # 木马查杀 #范式 #入侵 #日志排查 #kmeans #聚类 #Karalon #AI Test #YOLOv8 # Docker镜像 #文件IO #输入输出流 #流程图 #论文阅读 #论文笔记 #图论 #国产开源制品管理工具 #Hadess #一文上手 #jetty #蓝桥杯 #小艺 #鸿蒙 #搜索 #代理模式 #Spring AOP #程序人生 #健康医疗 #人大金仓 #Kingbase #SA-PEKS # 关键词猜测攻击 # 盲签名 # 限速机制 #生信 #word #pdf #Smokeping #工程实践 #策略模式 #GPU #租显卡 #训练推理 #时序数据库 #AI应用 #图像识别 #多进程 #python技巧 #高考 #企业级存储 #网络设备 #iot #软件工程 #Anything-LLM #IDC服务器 #私有化部署 #国产操作系统 #麒麟 #V11 #kylinos #大模型应用 #API调用 #PyInstaller打包运行 #服务端部署 #raid #raid阵列 #gpt #API #taro #java大文件上传 #java大文件秒传 #java大文件上传下载 #java文件传输解决方案 #wps #Linux多线程 #bigtop #hdp #hue #kerberos #pencil #pencil.dev #设计 #zotero #WebDAV #同步失败 #轻量化 #低配服务器 #Beidou #北斗 #SSR #simulink #matlab #journalctl #aiohttp #asyncio #异步 #信息安全 #信息收集 #Langchain-Chatchat # 国产化服务器 # 信创 #软件 #本地生活 #电商系统 #商城 #高级IO #poll #PyCharm # 远程调试 # YOLOFuse #Playbook #AI服务器 #欧拉 #VoxCPM-1.5-TTS # 云端GPU # PyCharm宕机 #儿童AI #图像生成 #pjsip # 模型微调 #openresty #lua #智能体来了 #传统行业 #AI赋能 #Syslog #系统日志 #日志分析 #日志监控 #生产服务器问题查询 #日志过滤 #Autodl私有云 #深度服务器配置 # 水冷服务器 # 风冷服务器 #.netcore #SSH保活 # GLM-4.6V-Flash-WEB # AI部署 #everything #材料工程 #数码相机 #智能电视 #VMware创建虚拟机 #远程更新 #缓存更新 #多指令适配 #物料关联计划 #AI生成 # outputs目录 # 自动化 #挖漏洞 #攻击溯源 #编程 #stl #漏洞修复 #IIS Crypto #warp #ComfyUI # 推理服务器 #防毒面罩 #防尘面罩 #n8n解惑 #编程助手 #net core #kestrel #web-server #asp.net-core #elk #rabbitmq #m3u8 #HLS #移动端H5网页 #APP安卓苹果ios #监控画面 直播视频流 #Prometheus #esp32 arduino #决策树 #Zabbix #语音合成 #HistoryServer #Spark #YARN #jobhistory #二值化 #Canny边缘检测 #轮廓检测 #透视变换 #DooTask #ZooKeeper #ZooKeeper面试题 #面试宝典 #深入解析 #Clawdbot #postman #内存接口 # 澜起科技 # 服务器主板 # 显卡驱动备份 #模拟退火算法 #计算机毕业设计 #程序定制 #毕设代做 #课设 #源码 #VMware #Hadoop #KMS 激活 #mongodb #Harbor #wireshark #广播 #组播 #并发服务器 #nacos #银河麒麟aarch64 #MC #MC群组服务器 #uvicorn #uvloop #asgi #event # 服务器迁移 # 回滚方案 #大模型入门 #flutter #select #homelab #Lattepanda #Jellyfin #Plex #Kodi #yolov12 #研究生life #开关电源 #热敏电阻 #PTC热敏电阻 #文件传输 #电脑文件传输 #电脑传输文件 #电脑怎么传输文件到另一台电脑 #电脑传输文件到另一台电脑 #身体实验室 #健康认知重构 #系统思维 #微行动 #NEAT效应 #亚健康自救 #ICT人 #eureka #云服务器 #个人电脑 #PTP_1588 #gPTP #漏洞挖掘 #TensorRT # 推理优化 #SSH别名 #CS2 #debian13 #BoringSSL #企业存储 #RustFS #对象存储 #高可用 #es安装 #远程控制 #云计算运维 #asp.net大文件上传 #asp.net大文件上传下载 #asp.net大文件上传源码 #ASP.NET断点续传 #asp.net上传文件夹 #asp.net上传大文件 #gpu #nvcc #cuda #nvidia #RXT4090显卡 #RTX4090 #深度学习服务器 #硬件选型 #群晖 #音乐 # 鲲鹏 #IntelliJ IDEA #Spring Boot #neo4j #NoSQL #SQL #http头信息 #Coturn #TURN #ci/cd #k8s #log4j #Jetty # 嵌入式服务器 #Windows #模块 #ICE #信创国产化 #达梦数据库 #树莓派 #温湿度监控 #WhatsApp通知 #IoT #MySQL # 服务器IP # 端口7860 #建筑缺陷 #红外 #数据集 #TCP服务器 #开发实战 #SMARC #ARM #全文检索 #银河麒麟服务器系统 #远程桌面 # 代理转发 # 跳板机 #echarts #鸿蒙PC ##租显卡 # 云服务器 #无人机 #junit #新人首发 #web服务器 #可撤销IBE #服务器辅助 #私钥更新 #安全性证明 #双线性Diffie-Hellman # 公钥认证 #Reactor #Kylin-Server #服务器安装 #Android16 #音频性能实战 #音频进阶 #短剧 #短剧小程序 #短剧系统 #微剧 # 智能运维 # 性能瓶颈分析 #空间计算 #原型模式 #hibernate #nosql #代理 #数据访问 #VMWare Tool #I/O模型 #并发 #水平触发、边缘触发 #多路复用 #clickhouse #arm64 #CNAS #CMA #程序文件 #SSH复用 # 远程开发 #Deepoc #具身模型 #开发板 #未来 #磁盘配额 #存储管理 #形考作业 #国家开放大学 #系统运维 #自动化运维 #IO #插件 #开源软件 #DHCP #网络安全大赛 #C++ UA Server #SDK #跨平台开发 #agent #ai大模型 #FHSS #eclipse #servlet #NFC #智能公交 #服务器计费 #FP-增长 #outlook #错误代码2603 #无网络连接 #2603 #mssql #wpf #实时检测 #卷积神经网络 #MOXA #GATT服务器 #蓝牙低功耗 #lucene #DAG #云服务器选购 #Saas #线程 #散列表 #机器视觉 #6D位姿 #UOS #海光K100 #统信 #Proxmox VE #虚拟化 #改行学it #b树 #memory mcp #Cursor #网路编程 #百万并发 #smtp #smtp服务器 #PHP #声源定位 #MUSIC #tensorflow # 远程访问 #memcache #ServBay #飞牛nas #fnos #ansys #ansys问题解决办法 #ranger #MySQL8.0 #分布式数据库 #集中式数据库 #业务需求 #选型误 #智能体对传统行业冲击 #行业转型 #Socket网络编程 #HarmonyOS #雨云服务器 #Minecraft服务器 #教程 #MCSM面板 # 服务器配置 # GPU # 串口服务器 # NPort5630 #Python办公自动化 #Python办公 #copilot #硬盘克隆 #DiskGenius # 键鼠锁定 #mtgsig #美团医药 #美团医药mtgsig #美团医药mtgsig1.2 #opc模拟服务器 #工程设计 #预混 #扩散 #燃烧知识 #层流 #湍流 #量子计算 #反向代理 #政务 #参数估计 #矩估计 #概率论 #adobe #数据迁移 #powerbi #个人助理 #数字员工 #gmssh #宝塔 #1panel #Exchange #sentinel #系统安装 #MinIO #IPv6 #POC #问答 #交付 #scikit-learn #随机森林 #闲置物品交易系统 #静脉曲张 #腿部健康 #运动 #Minecraft #PaperMC #我的世界服务器 #AI Agent #开发者工具 #kong #Kong Audio #Kong Audio3 #KongAudio3 #空音3 #空音 #中国民乐 #计算机外设 #边缘AI # Kontron # SMARC-sAMX8 #scanf #printf #getchar #putchar #cin #cout #ET模式 #非阻塞 #凤希AI伴侣 #remote-ssh #产品经理 #就业 #CMake #Make #C/C++ #OpenAI #故障 #优化 #多模态 #微调 #超参 #LLamafactory #vps #docker安装seata #全链路优化 #实战教程 #database #AI论文写作工具 #学术写作辅助 #论文创作效率提升 #AI写论文实测 # IndexTTS 2.0 #数字化转型 #实体经济 #商业模式 #软件开发 #数智红包 #商业变革 #创业干货 #AB包 #Go并发 #高并发架构 #Goroutine #系统设计 #Tracker 服务器 #响应最快 #torrent 下载 #2026年 #Aria2 可用 #迅雷可用 #BT工具通用 #FASTMCP #sglang #SSH Agent Forwarding # 容器化 #高斯溅射 #UEFI #BIOS #Legacy BIOS #产品运营 #联机教程 #局域网联机 #局域网联机教程 #局域网游戏 #交换机 #三层交换机 #AI智能棋盘 #Rock Pi S #边缘计算 #云开发 #性能 #RAM #c++高并发 #FTP服务器 #uip # 权限修复 #进程等待 #wait #waitpid # HiChatBox # 离线AI #SMTP # 内容安全 # Qwen3Guard #X11转发 #平板 #零售 #智能硬件 #vncdotool #链接VNC服务器 #如何隐藏光标 #H5网页 #网页白屏 #H5页面空白 #资源加载问题 #打包部署后网页打不开 #HBuilderX #gateway #Comate #MinIO服务器启动与配置详解 #算力建设 #tdengine #涛思数据 #服务器解析漏洞