网络编程-序列化和反序列化/应用层协议/

news2024/11/19 13:31:21

预备知识

理解为什么要应用层协议? 

  在学过套接字编程后,我们对协议的理解更深了一步,协议也就是一种约定,也可以通俗理解为一种口头约定,对于通信双方来说是必须要遵守的。TCP和UDP协议它们是传输层控制协议,也就是在传输层的,今天我们学习的是应用层的协议,它跟序列化和反序列化有什么关系呢?先看场景

  TCP是全双工的,因此它有两个缓冲区,可以同时读和写。在通信的时候,我们使用了read和write将数据从用户拷贝到内核的缓冲区中(sendto和recvfrom也是如此),因此这样看来,read和write更像是一种拷贝函数 ,作为发送方,我们将数据通过read函数,拷贝到内核的发送缓冲区后,那么这个数据还需要我们管吗?

  我们有没有想过,这个数据在发送缓冲区中,这个缓冲区也有一定的大小,那么这个数据它什么时候发给对方,要发送多少,或者发送错误了怎么办,这些事情我们关心吗?

  很明显,我们不关心,因为这是别人已经设计好的协议决定的,我们作为程序猿,只关注应用层,对于传输层,就交给传输控制协议就好了。

  理解这部分后,我们再看接收方,接收方同样也不关心传输层怎么样,它只关心应用层,但是我们使用比如write这样的函数从接收缓冲区中读取,我们可能直接拷贝了出来了一大坨东西,有可能这一坨包含了两个请求,也可能包含了两个半的请求,甚至也可能只有半个请求,那么对于这种数据该如何处理,我们肯定得有个标准才行。因此,我们应用层的约定,也就是协议来了!

所以为什么需要序列化和反序列化呢?

  协议,我们双方只需要规定好数据的格式,比如长度,大小,分隔符这些,我们在发送的时候先对数据进行处理,然后接收方读到数据后,就可以按照我们原本的约定来对数据进行提取,这样就可以得到想要的数据。

  所以,我们发送方对数据的处理其实就是加报文的过程,接收方提取数据就是解包的过程。

  但是,网络之间发送的信息其实都比较复杂,并不只是简单的一些整形字符串,它可能是一个结构体,比如我们待会要实现的网络计算器,就算这个计算器再简单,它也得包含左操作数和右操作数,以及一个操作符。所以,我们把它定义成一个结构体,然后直接发送给对象行吗?技术上是可行的,但是非常不推荐这样做。因为结构体的大小在不同的操作系统和不同的编译器上可能是不一样的,比如内存对齐。

  因此,我们可以将操作数们和操作符转化成一串字符串,然后在字符串前加上报文,然后发送给对方;对方再对拿到的数据进行反序列化,就可以提取到想要的数据。

网络计算器的简单实现

  顺便一提,今后用到的协议几乎都是tcp协议。

Log.hpp

这里顺便复习一下之前封装的文件打印的类

#pragma once

#include<iostream>
#include<time.h>
#include<stdarg.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<stdlib.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 Warning:
                return "Warning";
            case Error:
                return "Error";
            case Fatal:
                return "Fatal";
            default:
                return "None";
        }
    }

    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)
        {
            std::cout << "打开文件失败" << std::endl;
            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()
    {
    }

    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[SIZE * 2];
        snprintf(logtxt,sizeof(logtxt),"%s %s\n",leftbuffer,rightbuffer);

        printLog(level,logtxt);
    }
private:
    int printMethod;//打印的方式(比如向键盘还是文件)
    std::string path;
};

Log lg;

 Socket.hpp

  之前使用套接字编程,但是今后我们更多的是解除应用层,所以在这里再对套接字进行一次封装,今后就可以直接拿这个使用,方便还能使代码简洁。

#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,"socker 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;

        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 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;
        }
        // 开始准备输出ip和端口号
        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::cout << "connect to " << ip << ":" << port << "error" << std::endl;
            return false;
        }
        return true;
    }

    void Close()
    {
        close(sockfd_);
    }

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

 Protocol.hpp

 拓展使用第三方库序列化和反序列化

  实现这个计算器,其实主要是为了对序列化和反序列化进行更深刻的理解。我们费老半天的劲写出来的序列化其实存在各种问题,今后我们序列化其实可以用别人现成的,比如json和prutobuf ,其中prutobuf它是二进制的,主打效率,而json十分简单,这里就使用json。

  要在Linux上时候,首先要先安装这个第三方库,执行以下命令进行安装

sudo yum install -y jsoncpp-devel

安装好后,执行以下命令来查看该库需要包含的头文件

ls /usr/include/jsoncpp/json

这里头文件看似比较多,但是我们最多只用json.h

除了看头文件,我们还可以用命令查看库在哪里

ls /lib64/libjsoncpp.so -l

 

 代码

#pragma once

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


const std::string blank_space_sep = " "; // 空格分隔符,主要作用于操作数
const std::string protocol_sep = "\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;
}

bool Decode(std::string &package, std::string *content) // 解包
{
    std::size_t pos = package.find(protocol_sep);
    if (pos == std::string::npos)
        return false; // 说明该包不完整
    std::string len_str = package.substr(0, pos);
    std::size_t len = std::stoi(len_str);
    std::size_t total_len = len_str.size() + len + protocol_sep.size() * 2; // 总长度
    if (package.size() < total_len)
        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 // 复习条件编译,灵活编写代码
        // 序列化  即构建报文的有效载荷
        // 将其信息转化成字符串
        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 
    }

    bool Deserialize(const std::string &in)
    {
#ifdef MySelf
        std::size_t left = in.find(blank_space_sep);
        if (left == std::string::npos)
            return false;
        std::string part_x = in.substr(0, left); // 注意substr是左闭右开的

        std::size_t right = in.rfind(blank_space_sep); // 找右要用rfind,从尾开始找
        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()
    {
        std::cout << "New Request: " << 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() // 同理
    {
    }

public:
    bool Serialize(std::string *out)
    {
#ifdef MySelf
        std::string s = std::to_string(result);
        s += blank_space_sep;
        s += "code: ";
        s += std::to_string(code);
        *out = s;

        return true;
#else
        Json::Value root;
        root["result"] = result;
        root["code"] = code;
        // Json::StyledWriter w;  // 响应方换一种写法
        Json::FastWriter w;
        *out = w.write(root);
        return true;
#endif
    }

    bool Deserialize(const std::string &in)
    {
#ifdef MySelf
        std::size_t pos = in.find(blank_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 << "Response success, result: " << result << " ,code :" << code << std::endl;
    }

public:
    int result;
    int code; // 这个用来判断结果是否可信,设置出错码可以找到原因
};

两个类都提供了无参的构造函数。

TcpServer.hpp

#pragma once

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

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()
    {
        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,"accept a new link, sockfd: %d, clientip: %s, clientport: %d", sockfd, clientip.c_str(), clientport);

            if(fork() == 0)  // 子进程负责计算
            {
                listensock_.Close();  // 子进程用不到,所以可以关闭
                std::string inbuuffer_stream;
                while(true)   // 需要特别理解这部分代码,如果读上来的不是一个完整的请求,那么会保留曾经之前读取的部分,直到读上一个完整的请求
                {
                    char buffer[1024];
                    ssize_t n = read(sockfd,buffer,sizeof(buffer));
                    if(n > 0)
                    {
                        buffer[n] = 0;
                        inbuuffer_stream += buffer;
                        lg(Debug, "debug:\n%s", inbuuffer_stream.c_str());

                        while(true)
                        {
                            std::string info = callback_(inbuuffer_stream);
                            if (info.empty())
                                break;
                            lg(Debug, "debug,response:\n%s",info.c_str());
                            lg(Debug,"debug:\n%s",inbuuffer_stream.c_str());
                            write(sockfd,info.c_str(),info.size());
                        }
                    }
                    else if(n == 0)
                        break;
                    else 
                        break;
                }

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

    ~TcpServer()
    {}
private:
    uint16_t port_;
    Sock listensock_;
    func_t callback_;  // 回调函数
};

 这里我们没有用多线程了,因为加入线程池太麻烦了,简单实现就用多进程版本。

 

ServerCal.hpp

#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 = Mod_Zero;
            else
                resp.result = req.x % req.y;
            break;
        case '/':
            if (req.y == 0)
                resp.code = Div_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()
    {
    }
};

ClientCal.cc

#include <iostream>
#include <string>
#include <ctime>
#include <cstdio>
#include <unistd.h>
#include "Protocol.hpp"
#include "Socket.hpp"

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

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)
    {
        std::cout << "Connet error" << std::endl;
        return 1;
    }

    srand(time(nullptr) ^ getpid());  // 随机数种子,加强随机性
    int cnt = 1;
    const std::string opers = "+-*/%&^";

    std::string inbuffer_stream;
    while(cnt <= 10)
    {
        std::cout << "###############The " << cnt << " times test" << "#########" << std::endl;
        int x = rand() % 200 + 1;
        usleep(1000);
        int y = rand() % 150;
        usleep(1200);
        char oper = opers[rand()%opers.size()];
        Request req(x,y,oper);  // 构建请求
        req.DebugPrint();

        std::string package;
        req.Serialize(&package);

        package = Encode(package);
        
        ssize_t m =  write(sockfd.Fd(),package.c_str(),package.size());
        if(m < 0)
        {
            std::cout << "写入失败" << std::endl;
            return 1;
        }
        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);
            if(!r)
            {
                std::cout << "读取出错(1)!!!" << std::endl;
                exit(0); // 这里为了方便调试,客户端读不完整直接退出
                //这里其实直接扔一个断言(assert)会比较好
            }

            Response resp;
            r = resp.Deserialize(content);
            if(!r)
            {
                std::cout << "读取出错!!!" << std::endl;
                exit(0);  // 同理
            }

            resp.DebugPrint();
        }
        else 
        {
            std::cout << "读取结果失败" << std::endl;
        }
        printf("\n");
        sleep(1);

        cnt++;
    }

    sockfd.Close();   
    return 0;
}

 在这里可以结合代码考虑一下,如果读取的报文不是完整的,该怎么办?

ServerCal.cc

#include "TcpServer.hpp"
#include "ServerCal.hpp"
#include <unistd.h>

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

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }
    uint16_t port = std::stoi(argv[1]);
    ServerCal cal;
    TcpServer *tsvp = new TcpServer(port, std::bind(&ServerCal::Calculator, &cal, std::placeholders::_1)); // 绑定函数
    tsvp->InitServer();
    daemon(0, 0); // 系统库里的守护进程函数
    tsvp->Start();
    return 0;
}

 这里顺便用了一下系统库里的守护进程函数。

daemon()

 其中第一个参数就是决定这个进程运行后要放在哪个目录,选择0就默认是系统的根目录。

第二个参数就是我们之前也做过的,要不要将标准输入,标准输出,标准错误重定向到/dev/null。选择0就是默认重定向到/dev/null。

这里可以再温习一下之前我们自己实现的Daemon

#pragma once

#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <signal.h>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

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

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

    //2.将自己变成独立的会话
    if(fork() > 0)
        exit(0);  // 如果是父进程,那么就会直接退出,子进程继续向后执行,并且已经成为孤儿进程了
    setsid();     // 这个函数就是让这个进程成为一个新的会话

    // 3.更改当前调用进程的工作目录
    if(!cwd.empty())
        chdir(cwd.c_str());  // 因为大多数下,服务器是安装在系统上的,所以工作目录也就是在根目录下
    
    // 4.将标准输入,标准输出,标准错误重定向至/dev/null,这个文件也就是垃圾处理文件
    int fd = open(nullfile.c_str(),O_RDWR);
    if(fd > 0)
    {
        dup2(fd,0);
        dup2(fd,1);
        dup2(fd,2);
        close(fd);   // 最后关掉这个文件描述符
    }
}

执行结果 

ps ajx | grep servercal

 用这个来看我们启动的服务。

makefile 

对于之前的条件编译,我们没必要再代码中进行硬编码,我们可以在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 $(Lib) $(Flag)

.PHONY:clean
clean:
	rm -f clientcal servercal

 其中Flag和Lib可以看作是makefile里面的宏,在makefile中,加了-D就可以定义一个宏,#-D就是不定义,因此我们可以在makefile里面控制是否用json的库函数。

再说说OSI七层模型 

 

  之前说OSI七层协议的时候,因为我们作为初学者,很多东西只靠听并不了解,到现在已经积累了一定使用加深理解后,再看OSI就简单一些了。

  比如传输层,就是我们之前用到的Sock接口相关的,传输层上面就是会话层,会话层就是负责建立连接和断开连接的,所以我们之前学到的TCP这些就是会话层的。比如我们在这次的例子中,我们通过创建子进程处理请求,其实就相当于新建立了一个会话。让子进程管理会话,只是在这里看不出来管理而已,比如如果客户端连接成功,但是一直不发送请求,我们可以设计多少时间后没有请求就关闭连接等管理行为。

  再看表示层的描述,“固有的数据格式和网络标准数据格式”,说的很抽象,但这不就是今天我们自定义的协议吗?比如固有的数据格式,就是我们规定了有几个操作符,几个操作数,中间还有空格等等。其实就是我们设计的序列化和反序列化,以及添加和删除报头的功能。

  最后就是应用层,在今天看来,就是我们写的SverCal.hpp这样的,它对数据进行处理计算,也就是网络计算。

  虽然传输层往下还有,但是我们最多也就接触到传输层了,由此可见,OSI模型设计的还是很好的。

总结

  今天我们自定义的协议只能处理整形,而现实生活中还有浮点数的计算,或者超大数字的运算,那么此时一份协议肯定就不够用了,这里也可以设计三份协议,然后在报头中添加该计算要使用哪种协议。 

  虽然今后我们在工作中,很少会自己自定义协议,设计序列化和反序列化,大多数都用现成的,但是我们必须要做过一次,心里有底才行。 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1434912.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

XAI:探索AI决策透明化的前沿与展望

文章目录 &#x1f4d1;前言一、XAI的重要性二、为什么需要可解释人工智能三、XAI的研究与应用四、XAI的挑战与展望 &#x1f4d1;前言 随着人工智能技术的快速发展&#xff0c;它已经深入到了我们生活的方方面面&#xff0c;从智能手机、自动驾驶汽车到医疗诊断和金融投资&…

[UI5 常用控件] 07.SplitApp,SplitContainer

文章目录 前言1. SplitApp1.1 组件结构1.2 Demo1.3 mode属性 2. SplitContainer 前言 本章节记录常用控件SplitApp&#xff0c;SplitContainer。主要功能是在左侧显示Master页面&#xff0c;右侧显示Detail页面。 Master页面和Detail页面可以由多个Page组成&#xff0c;并支持…

2024数学建模美赛F题Reducing Illegal Wildlife Trade原创论文讲解(含完整python代码)

大家好呀&#xff0c;从发布赛题一直到现在&#xff0c;总算完成了数学建模美赛本次F题目非法野生动物贸易完整的成品论文。 本论文可以保证原创&#xff0c;保证高质量。绝不是随便引用一大堆模型和代码复制粘贴进来完全没有应用糊弄人的垃圾半成品论文。 F题论文共42页&…

ios设备解锁 --Apeaksoft iOS Unlocker

Apeaksoft iOS Unlocker是一款针对iOS系统的密码解锁工具。其主要功能包括解锁多种锁屏类型&#xff0c;包括数字密码、Touch ID、Face ID和自定义密码。此外&#xff0c;它还可以帮助用户删除iPhone密码以进入锁屏设备&#xff0c;忘记的Apple ID并将iPhone激活为新的&#xf…

2023年12月 Python(四级)真题解析#中国电子学会#全国青少年软件编程等级考试

Python等级考试(1~6级)全部真题・点这里 一、单选题(共25题,共50分) 第1题 下列有关分治算法思想的描述不正确的是?( ) A:将问题分解成的子问题具有相同的模式。 B:将问题分解出的各个子问题相互之间有公共子问题。 C:当问题足够小时, 可以直接求解。 D:可以将…

鸿蒙(HarmonyOS)项目方舟框架(ArkUI)之QRCode组件

鸿蒙&#xff08;HarmonyOS&#xff09;项目方舟框架&#xff08;ArkUI&#xff09;之QRCode组件 一、操作环境 操作系统: Windows 10 专业版、IDE:DevEco Studio 3.1、SDK:HarmonyOS 3.1 二、QRCode组件 用于显示单个二维码的组件。 子组件 无。 接口 QRCode(value: st…

MongoDB的操作和理解

什么是MongoDB? MongoDB&#xff1a;基于分布式文件存储的数据库由C语言编写。旨在为WEB应用提供可扩展的高性能数据存储解决方案。MongoDB是一个介于关系数据库和非关系数据库(nosql)之间的产品&#xff0c;是非关系数据库当中功能最丰富&#xff0c;最像关系数据库的。 Mo…

【LeetCode】每日一题 2024_2_4 Nim 游戏(找规律,博弈论)

文章目录 LeetCode&#xff1f;启动&#xff01;&#xff01;&#xff01;题目&#xff1a;Nim 游戏题目描述代码与解题思路 LeetCode&#xff1f;启动&#xff01;&#xff01;&#xff01; 题目&#xff1a;Nim 游戏 题目链接&#xff1a;292. Nim 游戏 题目描述 代码与解题…

ubuntu系统下c++ cmakelist vscode debug(带传参的debug)的详细示例

c和cmake的debug&#xff0c;网上很多都需要配置launch.json&#xff0c;cpp.json啥的&#xff0c;记不住也太复杂了&#xff0c;我这里使用cmake插件带有的设置&#xff0c;各位可以看一看啊✌(不知不觉&#xff0c;竟然了解了vscode中配置文件的生效逻辑&#x1f923;) 克隆…

go test单元测试详解

目录 介绍&测试范围 测试函数 执行机制 常用执行模式 子测试 帮助函数Helper() 测试覆盖率 介绍&测试范围 go test测试是go自带的测试工具&#xff0c;主要包括单元测试和性能测试两大类。 包括了工程目录下所有以_test.go为后缀名的源代码文件&#xff0c;这…

C++ 语法文件

程序运行时产生的数据都属于临时数据&#xff0c;程序结束就会被释放。 通过文件可以可以将数据持久化 c中对文件操作需要包含头文件fstream 文件的类型分为两种 1.文本文件 文件以文本的ASCII码形式存储在计算机中 2.二进制文件 稳重以文本的二进制形式存储在计算机中 用…

基于idea解决springweb项目的Java文件无法执行问题

前言 上一篇文章的话介绍了spring以及创建spring项目&#xff0c;但是因为有宝子私聊我说创建的项目那个JAVA文件显示灰色还有一个红点&#xff0c;问我怎么解决下面我来简答的写一下怎么修改配置让他正常的运行 配置 原因好像是因为基于maven的JAVA项目构架&#xff0c;对应…

Android Studio中打开文件管理器

文章目录 一、前言二、操作步骤 一、前言 在Android Studio中有时候需要查看手机的文件目录或者复制文件&#xff0c;但是有时候文件管理器找不到在哪&#xff0c;这里记录该操作流程 二、操作步骤 第一步: 第二步: 第三步:

CentOS7搭建k8s-v1.28.6集群详情

文章目录 1.灌装集群节点操作系统1.1 设置hosts1.2 设置nameserver1.3 关闭防火墙1.4 关闭Selinux1.5 关闭Swap分区1.6 时间同步1.7 调整内核参数1.8 系统内核升级 2.安装Docker2.1 卸载旧Docker2.2 配置Docker软件源2.3 安装Docker 3.部署Kubernets集群3.1 设置 K8s 软件源3.2…

51单片机 跑马灯

#include <reg52.h>//毫秒级延时函数 void delay(int z) {int x,y;for(x z; x > 0; x--)for(y 114; y > 0 ; y--); }sbit LED1 P1^0x0; sbit LED2 P1^0x1; sbit LED3 P1^0x2; sbit LED4 P1^0x3; sbit LED5 P1^0x4; sbit LED6 P1^0x5; sbit LED7 P1^0x6; s…

属性“xxxx”在类型“ArrayConstructor”上不存在。是否需要更改目标库? 请尝试将 “lib” 编译器选项更改为“es2015”或更高版本。

使用vscode编写vue&#xff0c;在使用elementUI时&#xff0c;发现代码中的form报错如下&#xff1a; 属性“form”在类型“ArrayConstructor”上不存在。是否需要更改目标库? 请尝试将 “lib” 编译器选项更改为“es2015”或更高版本。 解决方法&#xff1a; 打开jsconfig.…

如何配置SSH实现无公网ip远程连接访问Deepin操作系统

&#x1f4d1;前言 本文主要是配置SSH实现无公网ip远程连接访问Deepin操作系统的文章&#xff0c;如果有什么需要改进的地方还请大佬指出⛺️** &#x1f3ac;作者简介&#xff1a;大家好&#xff0c;我是青衿&#x1f947; ☁️博客首页&#xff1a;CSDN主页放风讲故事 &…

【防止重复提交】Redis + AOP + 注解的方式实现分布式锁

文章目录 工作原理需求实现1&#xff09;自定义防重复提交注解2&#xff09;定义防重复提交AOP切面3&#xff09;RedisLock 工具类4&#xff09;过滤器 请求工具类5&#xff09;测试Controller6&#xff09;测试结果 工作原理 分布式环境下&#xff0c;可能会遇到用户对某个接…

有趣的CSS - 按钮文字上下滑动

目录 整体效果核心代码html 代码css 部分代码 完整代码如下html 页面css 样式页面渲染效果 整体效果 这个按钮效果主要使用 :hover 伪选择器以及 transition 过渡属性来实现两个子元素上下过渡的效果。 此效果可以在主入口按钮、详情或者更多等按钮处使用&#xff0c;增加一些鼠…

Express框架介绍—node.js

Express—Node.js 官网传送门(opens new window) 基于 Node.js 平台&#xff0c;快速、开放、极简的 Web 开发框架 Express 是用于快速创建服务器的第三方模块。 Express 初体验 基本使用 安装 Express&#xff1a; npm install express创建服务器&#xff0c;监听客户端请…