[计算机网络]---序列化和反序列化

前言

作者:小蜗牛向前冲

名言:我可以接受失败,但我不能接受放弃

  如果觉的博主的文章还不错的话,还请点赞,收藏,关注👀支持博主。如果发现有问题的地方欢迎❀大家在评论区指正 

目录

 一、再谈协议

二、序列化和反序化

1、网络版本计算器的场景搭建

2、服务器构建 

 3、客户端构建

4、序列化和反序列化 

4.1自定义序列化和反序列化

4.2json实现序列化和反序列化

 5、测试


本期学习:重点理解序化和反序列化

 一、再谈协议

        在前面博客谈网络的时候,我们认为协议就是一种约定,在用socket api接口的时候,都是按照“字符串”的方式来进行发送和接受的,但是如果我们要传输一些结构化的数据,又该怎么办?

这就要我们将数据序列化进行传输或者接收。

在网络通信中,传输结构化数据的常见方法有以下几种:

  1. 序列化: 将结构化数据转换为字符串或字节流进行传输。在发送端,可以使用一种序列化格式将数据转换为字符串或字节流;在接收端,再将接收到的字符串或字节流反序列化为相应的数据结构。常见的序列化格式包括 JSON(JavaScript Object Notation)、XML(eXtensible Markup Language)、Protocol Buffers(protobuf)、MessagePack 等。选择序列化格式时,需要考虑数据的大小、可读性、解析效率等因素。

  2. 数据格式协议: 使用一种规定的数据格式协议进行数据传输。这种方法通常需要双方预先约定好数据格式协议,并按照协议的规定进行数据的编码和解码。常见的数据格式协议包括 HTTP、FTP、SMTP 等。在 HTTP 协议中,可以使用 Content-Type 来指定数据的格式,如 application/json 表示 JSON 格式数据,application/xml 表示 XML 格式数据。

  3. 自定义协议: 自定义通信协议,定义数据的传输格式和规则。在自定义协议中,可以根据实际需求灵活地定义数据的结构和编码方式,以及通信过程中的规则和约定。自定义协议通常需要双方进行协商和实现,但可以更好地满足特定场景下的需求。

二、序列化和反序化

为了更好的理解,下面我们将通过自己定制tcp协议,将数据进行序列化和反序列化。为了方便叙述,我们简单的制定网络版本的计算 ,来理解协议中序列化和反序列化

1、网络版本计算器的场景搭建

我们的构想是,客户端通过将计算请求数据序列化,发送到网络中,服务端接收后将数据进行反序列化进行计算处理

处理过程大致如上图:

  • c->s调用函数发送数据的本质就是将一个缓冲区的拷贝到另外一个缓冲区。
  • s->c服务器回显数据的本质也是将s中缓冲区的数据拷贝到c中的缓冲区
  • 所以所tcp是全双工的

2、服务器构建 

这里构建的服务器和上篇在套接字中的是没有本质区别的

#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <functional>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#include "log.hpp"
#include "protocol.hpp"
using namespace std;

namespace server
{
    enum
    {
        USAGE_ERR = 1,
        SOCKET_ERR,
        BIND_ERR,
        LISTEN_ERR
    };

    static const uint16_t gport = 8080;
    static const int gbacklog = 5;
    // const Request &req: 输入型
    // Response &resp: 输出型
    typedef std::function<bool(const Request &req, Response &resp)> func_t;

    // 保证解耦
    void handlerEntery(int sock, func_t func)
    {
        std::string inbuffer;
        while (true)
        {
            // 1. 读取:"content_len"\r\n"x op y"\r\n
            // 1.1 你怎么保证你读到的消息是 【一个】完整的请求
            std::string req_text, req_str;
            // 1.2 我们保证,我们req_text里面一定是一个完整的请求:"content_len"\r\n"x op y"\r\n
            if (!recPackage(sock, inbuffer, &req_text))
                return;
            std::cout << "带报头的请求:\n"
                      << req_text << std::endl;
            if (!deLength(req_text, &req_str))
                return;
            std::cout << "去掉报头的正文:\n"
                      << req_str << std::endl;

            // 2. 对请求Request,反序列化
            // 2.1 得到一个结构化的请求对象
            Request req;
            if (!req.deserialize(req_str))
                return;

            // 3. 计算机处理,req.x, req.op, req.y --- 业务逻辑
            // 3.1 得到一个结构化的响应
            Response resp;
            func(req, resp); // req的处理结果,全部放入到了resp, 回调是不是不回来了?不是!

            // 4.对响应Response,进行序列化
            // 4.1 得到了一个"字符串"
            std::string resp_str;
            resp.serialize(&resp_str);

            std::cout << "计算完成, 序列化响应: " << resp_str << std::endl;

            // 5. 然后我们在发送响应
            // 5.1 构建成为一个完整的报文
            std::string send_string = enLength(resp_str);
            send(sock, send_string.c_str(), send_string.size(), 0); // 其实这里的发送也是有问题的,不过后面再说
        }
    }

    class CalServer
    {
    public:
        CalServer(const uint16_t &port = gport) : _listensock(-1), _port(port)
        {
        }
        void initServer()
        {
            // 1. 创建socket文件套接字对象
            _listensock = socket(AF_INET, SOCK_STREAM, 0);
            if (_listensock < 0)
            {
                logMessage(FATAL, "create socket error");
                exit(SOCKET_ERR);
            }
            logMessage(NORMAL, "create socket success: %d", _listensock);

            // 2. bind绑定自己的网络信息
            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(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
            {
                logMessage(FATAL, "bind socket error");
                exit(BIND_ERR);
            }
            logMessage(NORMAL, "bind socket success");

            // 3. 设置socket 为监听状态
            if (listen(_listensock, gbacklog) < 0) // 第二个参数backlog后面在填这个坑
            {
                logMessage(FATAL, "listen socket error");
                exit(LISTEN_ERR);
            }
            logMessage(NORMAL, "listen socket success");
        }
        void start(func_t func)
        {
            for (;;)
            {
                // 4. server 获取新链接
                // sock, 和client进行通信的fd
                struct sockaddr_in peer;
                socklen_t len = sizeof(peer);
                int sock = accept(_listensock, (struct sockaddr *)&peer, &len);
                if (sock < 0)
                {
                    logMessage(ERROR, "accept error, next");
                    continue;
                }
                logMessage(NORMAL, "accept a new link success, get new sock: %d", sock); // ?

                // version 2 多进程版(2)
                pid_t id = fork();
                if (id == 0) // child
                {
                    close(_listensock);
                    // if(fork()>0) exit(0);
                    //  serviceIO(sock);
                    handlerEntery(sock, func);
                    close(sock);
                    exit(0);
                }
                close(sock);

                // father
                pid_t ret = waitpid(id, nullptr, 0);
                if (ret > 0)
                {
                    logMessage(NORMAL, "wait child success"); // ?
                }
            }
        }
        ~CalServer() {}

    private:
        int _listensock; // 不是用来进行数据通信的,它是用来监听链接到来,获取新链接的!
        uint16_t _port;
    };
}

对于 handlerEntery这个操作我们要分析,在这函数中首先我们要获取到由客户端发送过来的报文,而客户端发送过来的报文肯定是经过序列化的,所以我们要进行反序列化,在通过func回调函数得到一个结构化的响应,在将响应通过处理发送给客户端。

 // 保证解耦
    void handlerEntery(int sock, func_t func)
    {
        std::string inbuffer;
        while (true)
        {
            // 1. 读取:"content_len"\r\n"x op y"\r\n
            // 1.1 你怎么保证你读到的消息是 【一个】完整的请求
            std::string req_text, req_str;
            // 1.2 我们保证,我们req_text里面一定是一个完整的请求:"content_len"\r\n"x op y"\r\n
            if (!recPackage(sock, inbuffer, &req_text))
                return;
            std::cout << "带报头的请求:\n"
                      << req_text << std::endl;
            if (!deLength(req_text, &req_str))
                return;
            std::cout << "去掉报头的正文:\n"
                      << req_str << std::endl;

            // 2. 对请求Request,反序列化
            // 2.1 得到一个结构化的请求对象
            Request req;
            if (!req.deserialize(req_str))
                return;

            // 3. 计算机处理,req.x, req.op, req.y --- 业务逻辑
            // 3.1 得到一个结构化的响应
            Response resp;
            func(req, resp); // req的处理结果,全部放入到了resp, 回调是不是不回来了?不是!

            // 4.对响应Response,进行序列化
            // 4.1 得到了一个"字符串"
            std::string resp_str;
            resp.serialize(&resp_str);

            std::cout << "计算完成, 序列化响应: " << resp_str << std::endl;

            // 5. 然后我们在发送响应
            // 5.1 构建成为一个完整的报文
            std::string send_string = enLength(resp_str);
            send(sock, send_string.c_str(), send_string.size(), 0); // 其实这里的发送也是有问题的,不过后面再说
        }
    }

对于calServer.cc的主程序,就是简单进行cal计算业务 

#include "calServer.hpp"
#include <memory>

using namespace server;
using namespace std;

static void Usage(string proc)
{
    cout << "\nUsage:\n\t" << proc << " local_port\n\n";
}
// req: 里面一定是我们的处理好的一个完整的请求对象
// resp: 根据req,进行业务处理,填充resp,不用管理任何读取和写入,序列化和反序列化等任何细节
bool cal(const Request &req, Response &resp)
{
    // req已经有结构化完成的数据啦,你可以直接使用
    resp._exitcode = OK;
    resp._result = OK;

    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._exitcode = DIV_ZERO;
        else
            resp._result = req._x / req._y;
    }
    break;
    case '%':
    {
        if (req._y == 0)
            resp._exitcode = MOD_ZERO;
        else
            resp._result = req._x % req._y;
    }
    break;
    default:
        resp._exitcode = OP_ERROR;
        break;
    }

    return true;
}

// tcp服务器,启动上和udp server一模一样
// ./tcpserver local_port
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }
    uint16_t port = atoi(argv[1]);

    unique_ptr<CalServer> tsvr(new CalServer(port));
    tsvr->initServer();
    tsvr->start(cal);
    return 0;
}

 3、客户端构建

对于客户端calClinet.hpp完成的主要逻辑是进行连网,输入计算,对服务器进行计算请求,在进行计算信息的构建。 

#pragma once

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

#define NUM 1024
using namespace std;
class CalClient
{
public:
    CalClient(const std::string &serverip, const uint16_t &serverport)
        : _sock(-1), _serverip(serverip), _serverport(serverport)
    {
    }
    void initClient()
    {
        // 1. 创建socket
        _sock = socket(AF_INET, SOCK_STREAM, 0);
        if (_sock < 0)
        {
            std::cerr << "socket create error" << std::endl;
            exit(2);
        }
        // 2. tcp的客户端要不要bind?要的! 要不要显示的bind?不要!这里尤其是client port要让OS自定随机指定!
        // 3. 要不要listen?不要!
        // 4. 要不要accept? 不要!
        // 5. 要什么呢??要发起链接!
    }
    void start()
    {
        struct sockaddr_in server;
        memset(&server, 0, sizeof(server));
        server.sin_family = AF_INET;
        server.sin_port = htons(_serverport);
        server.sin_addr.s_addr = inet_addr(_serverip.c_str());

        if (connect(_sock, (struct sockaddr *)&server, sizeof(server)) != 0)
        {
            std::cerr << "socket connect error" << std::endl;
        }
        else
        {
            std::string line;
            std::string inbuffer;
            while (true)
            {
                std::cout << "mycal>>> ";
                // 输入计算
                getline(cin, line);
                // 进行请求
                Request req = ParseLine(line);
                string content; // 存放计算信息
                req.serialize(&content);
                string send_string = enLength(content); // 添加报头
                send(_sock, send_string.c_str(), send_string.size(), 0);

                string package, text;
                if (!recPackage(_sock, inbuffer, &package))
                    continue;
                if (!deLength(package, &text))
                    continue;

                // 响应
                Response resp;
                resp.deserialize(text);
                std::cout << "exitCode: " << resp._exitcode << std::endl;
                std::cout << "result: " << resp._result << std::endl;
            }
        }
    }
    // 从文本中提取计算格式信息,然后用这些信息构建请求
    Request ParseLine(const std::string &line)
    {
        //"1+1" "123*456" "12/0"
        int status = 0; // 0:操作符之前,1:碰到了操作符 2:操作符之后
        int i = 0;
        int cnt = line.size();
        string left, right;
        char op;
        while (i < cnt)
        {
            switch (status)
            {
            case 0:
            {
                if (!isdigit(line[i])) // isdigit检查字符是否是十进制
                {
                    op = line[i];
                    status = 1;
                }
                else
                    left.push_back(line[i++]);
            }
            break;
            case 1:
            {
                i++;
                status = 2;
            }
            break;
            case 2:
            {
                right.push_back(line[i++]);
            }
            break;
            }
        }
        std::cout << std::stoi(left) << " " << std::stoi(right) << " " << op << std::endl;
        return Request(std::stoi(left), std::stoi(right), op);
    }
    ~CalClient()
    {
        if (_sock >= 0)
            close(_sock);
    }

private:
    int _sock;
    std::string _serverip;
    uint16_t _serverport;
};

calClient.cc 

#include "calClient.hpp"
#include <memory>

using namespace std;

static void Usage(string proc)
{
    cout << "\nUsage:\n\t" << proc << " serverip serverport\n\n";
}
// ./tcpclient serverip serverport
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    string serverip = argv[1];
    uint16_t serverport = atoi(argv[2]);

    unique_ptr<CalClient> tcli(new CalClient(serverip, serverport));
    tcli->initClient();
    tcli->start();
    return 0;
}

4、序列化和反序列化 

4.1自定义序列化和反序列化

前面说了一大堆要对数据进行序列化和反序列化 ,那到底什么是序列化和反序列,其实本质就是将一堆字符串,整和成一个字符串。为了完成我们网络计算器,这里我们写了二个类,一个是 Request对数据进行请求,另外一个是Response,每个类中都要对序化和反序列进行设计

class Request
{
public:
    Request() : _x(0), _y(0), _op(0)
    {
    }
    Request(int x, int y, char op) : _x(x), _y(y), _op(op)
    {
    }
    // 1. 自己写
    // 2. 用现成的
    bool serialize(std::string *out)
    {
    }
    // "x op yyyy";
    bool deserialize(const std::string &in)
    {
    }
public:
    // "x op y"
    int _x;
    int _y;
    char _op;
};

// 响应
class Response
{
public:
    Response() : _exitcode(0), _result(0)
    {
    }
    Response(int exitcode, int result) : _exitcode(exitcode), _result(result)
    {
    }
    bool serialize(std::string *out)
    {

    }
    bool deserialize(const std::string &in)
    {

    }

public:
    int _exitcode; // 0:计算成功,!0表示计算失败,具体是多少,定好标准
    int _result;   // 计算结果
};

其中序列化和反序列化可以自己进行编写,也可以通过库进行完成。

在大部分场景中我们都是用json进行序列结构的,对于自定义序列化,每个实现方式可能不同,所以不重点分析了,下面会有完整代码,大家可以参考实现

4.2json实现序列化和反序列化

在Linux上使用json我们要进行库的安装

sudo yum install -y jsoncpp-devel

序列化 

使用了 JsonCpp 库来创建一个 JSON 对象 root,然后将一些值 _x_y_op 分别存储到 JSON 对象的键 "first""second""oper" 中。接下来,使用 Json::FastWriter 对象 writer 来将 JSON 对象 root 转换为字符串,并将结果写入到 out 指向的位置。

 Json::Value root;
        root["first"] = _x;
        root["second"] = _y;
        root["oper"] = _op;

        Json::FastWriter writer;
        // Json::StyledWriter writer;
        *out = writer.write(root);

反序列化 

 使用了 JsonCpp 库来解析输入的 JSON 字符串 in,并将解析得到的值存储到 Json::Value 类型的对象 root 中。然后,通过访问 root 对象的键 "first""second""oper" 来获取相应的值,并将这些值转换为整数类型,并分别存储到 _x_y_op 变量中。

 Json::Value root;
        Json::Reader reader;
        reader.parse(in, root);

        _x = root["first"].asInt();
        _y = root["second"].asInt();
        _op = root["oper"].asInt();

 注意:asInt() 函数用于将 JSON 值转换为整数类型

protocol.hpp 完整代码实现:添加报文,去报头,请求,响应获取数据包

#pragma once

#include <iostream>
#include <cstring>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <jsoncpp/json/json.h>

#define SEP " "
#define SEP_LEN strlen(SEP)
#define LINE_SEP "/r/n" // /r回车   /n换行   /r/n表示换行的同时将光标移动到行首
#define LINE_SEP_LEN strlen(LINE_SEP)
using namespace std;

// 错误枚举
enum
{
    OK = 0,
    DIV_ZERO,
    MOD_ZERO,
    OP_ERROR
};
// "x op y" -> "content_len"\r\n"x op y"\r\n
// "exitcode result" -> "content_len"\r\n"exitcode result"\r\n

// 添加报头
string enLength(string &text)
{
    string send_string = to_string(text.size());
    send_string += LINE_SEP;
    send_string += text;
    send_string += LINE_SEP;

    return send_string;
}
// 去报头
//  "content_len"\r\n"exitcode result"\r\n
bool deLength(const std::string &package, std::string *text)
{
    auto pos = package.find(LINE_SEP);
    if (pos == string::npos)
        return false;
    string text_len_string = package.substr(0, pos);
    int text_len = stoi(text_len_string);
    *text = package.substr(pos + LINE_SEP_LEN, text_len);
    return true;
}

// 请求
class Request
{
public:
    Request() : _x(0), _y(0), _op(0)
    {
    }
    Request(int x, int y, char op) : _x(x), _y(y), _op(op)
    {
    }
    // 1. 自己写
    // 2. 用现成的
    bool serialize(std::string *out)
    {
#ifdef MYSELF
        *out = "";
        // 结构化 -> "x op y";
        std::string x_string = std::to_string(_x);
        std::string y_string = std::to_string(_y);

        *out = x_string;
        *out += SEP;
        *out += _op;
        *out += SEP;
        *out += y_string;
#else
        Json::Value root;
        root["first"] = _x;
        root["second"] = _y;
        root["oper"] = _op;

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

    // "x op yyyy";
    bool deserialize(const std::string &in)
    {
#ifdef MYSELF
        // "x op y" -> 结构化
        auto left = in.find(SEP);
        auto right = in.rfind(SEP);
        if (left == std::string::npos || right == std::string::npos)
            return false;
        if (left == right)
            return false;
        if (right - (left + SEP_LEN) != 1)
            return false;

        std::string x_string = in.substr(0, left); // [0, 2) [start, end) , start, end - start
        std::string y_string = in.substr(right + SEP_LEN);

        if (x_string.empty())
            return false;
        if (y_string.empty())
            return false;
        _x = std::stoi(x_string);
        _y = std::stoi(y_string);
        _op = in[left + SEP_LEN];
#else
        Json::Value root;
        Json::Reader reader;
        reader.parse(in, root);

        _x = root["first"].asInt();
        _y = root["second"].asInt();
        _op = root["oper"].asInt();
#endif
        return true;
    }

public:
    // "x op y"
    int _x;
    int _y;
    char _op;
};

// 响应
class Response
{
public:
    Response() : _exitcode(0), _result(0)
    {
    }
    Response(int exitcode, int result) : _exitcode(exitcode), _result(result)
    {
    }
    bool serialize(std::string *out)
    {
#ifdef MYSELF
        *out = "";
        std::string ec_string = std::to_string(_exitcode);
        std::string res_string = std::to_string(_result);

        *out = ec_string;
        *out += SEP;
        *out += res_string;
#else
        Json::Value root;
        root["exitcode"] = _exitcode;
        root["result"] = _result;

        Json::FastWriter writer;
        *out = writer.write(root);
#endif
        return true;
    }
    bool deserialize(const std::string &in)
    {
#ifdef MYSELF
        // "exitcode result"
        auto mid = in.find(SEP);
        if (mid == std::string::npos)
            return false;
        std::string ec_string = in.substr(0, mid);
        std::string res_string = in.substr(mid + SEP_LEN);
        if (ec_string.empty() || res_string.empty())
            return false;

        _exitcode = std::stoi(ec_string);
        _result = std::stoi(res_string);
#else
        Json::Value root;
        Json::Reader reader;
        reader.parse(in, root);

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

public:
    int _exitcode; // 0:计算成功,!0表示计算失败,具体是多少,定好标准
    int _result;   // 计算结果
};

// 获取数据包
//  "content_len"\r\n"x op y"\r\n"content_len"\r\n"x op y"\r\n"content_len"\r\n"x op
bool recPackage(int sock, string &inbuffer, string *text)
{
    char buffer[1024];
    while (true)
    {
        // 从网络中接受资源
        ssize_t n = recv(sock, buffer, sizeof(buffer) - 1, 0);
        if (n > 0)
        {
            buffer[n] = 0;
            inbuffer += buffer;

            // 分析
            auto pos = inbuffer.find(LINE_SEP);
            if (pos == string::npos)
                continue;
            string text_len_string = inbuffer.substr(0, pos);
            int text_len = stoi(text_len_string);
            // 报文的总长度
            int total_len = text_len_string.size() + 2 * LINE_SEP_LEN + text_len;
            // text_len_string + "\r\n" + text + "\r\n" <= inbuffer.size();
            std::cout << "处理前#inbuffer: \n"
                      << inbuffer << std::endl;

            if (inbuffer.size() < total_len)
            {
                std::cout << "你输入的消息,没有严格遵守我们的协议,正在等待后续的内容, continue" << std::endl;
                continue;
            }
            // 最少一个完整报文
            *text = inbuffer.substr(0, total_len);
            inbuffer.erase(0, total_len);

            std::cout << "处理后#inbuffer:\n " << inbuffer << std::endl;

            break;
        }
        else
            return false;
    }
    return true;
}

 5、测试

下面我们进行几组简单的测试:

 左边是服务器,又边上是客户端

Linux小命令

killall 是一个在 Unix 和类 Unix 操作系统上用于终止进程的命令。与 kill 命令不同,killall 不是根据进程 ID(PID)来终止进程,而是根据进程的名称来匹配并终止相应的进程。

killall [选项] 进程名

其中,选项可以用来指定不同的操作行为,而进程名则是要终止的进程的名称。

一些常用的选项包括:

  • -e:显示详细的错误信息。
  • -i:交互模式,在终止进程之前询问用户。
  • -q:安静模式,不显示任何输出。
  • -u:指定用户,仅终止指定用户的进程。

注意:killall 命令会终止所有匹配名称的进程,因此需要谨慎使用,以免意外终止系统中重要的进程

 

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

C++动态规划-线性dp算法

莫愁千里路 自有到来风 CSDN 请求进入专栏 X 是否进入《C专栏》? 确定 目录 线性dp简介 斐波那契数列模型 第N个泰波那契数 思路&#xff1a; 代码测试&#xff1a; 三步问题 思路&#xff1a; 代码测试&#xff1a; 最小花费爬楼梯 思路…

Java:如何判断一个链表是否为回文结构?(画图+代码 详解)

一、判断思想 我们设计一个时间复杂度为O(n),额外空间复杂度为O(1)的算法&#xff0c;我们在不创建额外空间的基础上来判断是否为回文结构。 思想&#xff1a; 1、使用快慢指针法&#xff0c;找到链表的中间节点。 2、翻转中间节点的后半部分。 3、分别从头节点和尾节点向中间遍…

LeetCode刷题计划

LeetCode刷题计划 推荐 代码随想录&#xff1a;https://github.com/youngyangyang04/leetcode-master 卡码网 练习ACM模式 https://kamacoder.com/ 01 #include <iostream> using namespace std;int main() {int a ,b;while(cin>>a>>b){cout<<ab<…

从汇编分析C语言可变参数的原理,并实现一个简单的sprintf函数

C语言可变参数 使用printf等函数的时候函数原型是printf(const char* fmt, ...), 这一类参数的个数不限的函数是可变参数 使用 使用一个头文件stdarg.h, 主要使用以下的宏 typedef char * va_list;// 把 n 圆整到 sizeof(int) 的倍数 #define _INTSIZEOF(n) ( (sizeo…

IO流---字节输入输出流,字符输入输出流

IO流概述 IO流&#xff0c;即输入输出流&#xff08;Input Output Stream&#xff09;&#xff0c;是一种抽象概念&#xff0c;用来处理设备之间的数据传输问题&#xff0c;例如文件复制、文件上传、文件下载等。在Java中&#xff0c;对数据的操作通常是通过流的方式进行的&…

幻兽帕鲁云服务器搭建零基础教程,新手小白一看就会

以下教程基于阿里云服务器ECS 来搭建幻兽帕鲁游戏服务器&#xff0c;通过一键部署的方式&#xff0c;最快1分钟即可完成部署。 阿里云一键部署幻兽帕鲁的活动地址&#xff1a;1分钟畅玩&#xff01;一键部署幻兽帕鲁联机服务器 首先&#xff0c;打开阿里云的这个游戏服务器活…

【机器学习笔记】8 决策树

决策树原理 决策树是从训练数据中学习得出一个树状结构的模型。 决策树属于判别模型。 决策树是一种树状结构&#xff0c;通过做出一系列决策&#xff08;选择&#xff09;来对数据进行划分&#xff0c;这类似于针对一系列问题进行选择。决策树的决策过程就是从根节点开始&…

二维数组传参的本质(详解)

目录 一、前言二、分析本质三、总结 一、前言 有时候我们有⼀个⼆维数组的需要传参给⼀个函数的时候&#xff0c;我们是这样写的&#xff1a; #include <stdio.h> void test(int a[3][5], int r, int c) {int i 0;int j 0;for (i 0; i < r; i){for (j 0; j <…

网络安全问题概述

1 计算机网络面临的安全性威胁 两大类威胁&#xff1a;被动攻击和主动攻击。 被动攻击 指攻击者从网络上窃听他人的通信内容。 通常把这类攻击称为截获。 攻击者只是观察和分析某一个协议数据单元 PDU&#xff0c;以便了解所交换的数据的某种性质&#xff0c;但不干扰信息…

【PyQt】11-QTextEdit、QPushButton

文章目录 前言一、文本输入-QTextEdit1.1 代码1.2 运行结果 二、QPushButton2.1.1 按钮上添加文本2.1.2 按键的弹跳效果2.1.3 两个信号可以绑定一个槽。2.1.4 带图标的按键运行结果 2.1.5 按键不可用以及回车默认完整代码2.2 单选按键控件运行结果 2.3 复选框&#xff08;多选框…

1Coze平台介绍

2023年随着OpenAI推出GPT 3.5&#xff0c;AI赛道变得更加火热。GPT&#xff08;Generative Pre-trained Transformer&#xff09;是一种自然语言处理&#xff08;NLP&#xff09;模型&#xff0c;用于生成文本、理解语言和进行各种语言任务。GPT是由OpenAI开发的&#xff0c;它…

Python 异常处理及程序调试

Python 是一门功能强大而又易于学习的编程语言&#xff0c;它提供了丰富的工具和库来帮助开发者编写高效、稳定的程序。然而&#xff0c;在编写复杂的应用程序时&#xff0c;错误和异常是难以避免的。本文将介绍 Python 中的异常处理机制以及程序调试技巧&#xff0c;帮助读者提…

枚举,#define,C中程序内存区域划分

目录 一、枚举 1.1枚举类型的声明 1.2枚举类型的优点 1.3枚举类型的使用 二、#define定义常量 三、C中程序内存区域划分 一、枚举 1.1枚举类型的声明 枚举顾名思义就是⼀⼀列举。 把可能的取值⼀⼀列举。 比如我们现实生活中&#xff1a; ⼀周的星期⼀到星期日是有限…

【AIGC】Stable Diffusion 的提示词入门

一、正向提示词和反向提示词 Stable Diffusion 中的提示词通常用于指导用户对生成的图像进行控制。这些提示词可以分为正向提示词&#xff08;Positive Prompts&#xff09;和反向提示词&#xff08;Negative Prompts&#xff09;两类&#xff0c;它们分别影响图像生成过程中的…

MATLAB计算极限和微积分

一.函数与极限 计算极限&#xff1a;lim(3*x^2/(2x1))&#xff0c;x分别趋于0和1&#xff0c;代码如下&#xff1a; syms x; limit(3*x*x/(2*x1),x,0) limit(3*x*x/(2*x1),x,1) 结果分别为0和1&#xff1a; 1.计算双侧极限 计算极限&#xff1a;lim(3*x^2/(2x1))&#xff0…

输入输出自定义映射矩阵(数据结构树)

输出自定义FC其它算法实现&#xff0c;可以参考下面文章&#xff1a; https://rxxw-control.blog.csdn.net/article/details/125994252https://rxxw-control.blog.csdn.net/article/details/125994252下面我们看下我们的控制要求。在学习本篇博客之前大家可以熟悉下数据结构图…

内网横向渗透-1

目录 内网横向渗透 流量监听工具的使用 ARP欺骗 工具使用 服务密码攻击 hydra medusa ncrack hashcat 内网横向渗透 流量监听工具的使用 ARP欺骗 工具使用 ettercap 工具 可以进行arp欺骗、DNS欺骗&#xff0c;网络钓鱼等等&#xff01; driftnet -i eth0 可以用来…

GiantPandaCV | 视觉类表面缺陷检测项目相关技术总结

本文来源公众号“GiantPandaCV”&#xff0c;仅用于学术分享&#xff0c;侵权删&#xff0c;干货满满。 原文链接&#xff1a;视觉类表面缺陷检测项目相关技术总结 本文由海滨撰写&#xff0c;首发于GaintPandaCV。 零、前言 做这个方向的项目也有一段时间了&#xff0c;作为…

在Postgresql 下安装QGIS

一.打开 Application Stack Builder 二.选择默认端口和安装目标 三.选择【Spatial Extensions】 四.选择安装位置 五.选择安装组件 六.选择数据库和输入对应账号密码 七.安装完成

【Linux】进程的初步认识

进程的初步认识 基本概念描述进程task_struct-PCB的一种task_stuct内容分类 查看进程通过系统调用获取进程标识符 基本概念 要了解进程&#xff0c;首先我们要知道两点 我们可以同时启动多个程序&#xff0c;也就意味着我们可以将多个.exe文件加载到内存操作系统如何去管理这些…