协议,序列化,反序列化,Json

news2024/11/23 1:22:12

文章目录

  • 协议
  • 序列化和反序列化
  • 网络计算器
    • protocol.hpp
    • Server.hpp
    • Server.cc
    • Client.hpp
    • Client.cc
    • log.txt
    • 通过结果再次理解通信过程
  • Json
    • 效果

协议

协议究竟是什么呢?首先得知道主机之间的网络通信交互的是什么数据,像平时使用聊天APP聊天可以清楚,用户看到的不仅仅是聊天的文字,还能够看到用户的头像昵称等其他属性。也就可以证明网络通信不仅仅是交互字符串那么简单。事实上网络通信还可能会通过一个结构化的数据去交互,例如聊天软件里,一台主机向另一台发送消息,这个消息里面就包含了头像等其他的数据。

一台主机发送数据会把所有的数据整合成一个结构化数据统一发送,而收到数据的主机再将这个结构化数据分解成原始的每个独立的数据。而为了确保主机之间收到数据后能够成功的分解,整合和分解两个过程必须是按照统一的约定来执行,而这个约定就是协议

序列化和反序列化

上述的将所有需要发送的数据整合到一起的过程就称为序列化过程而分解的过程就称为反序列化过程

网络的通信就可以理解为:

image-20230806212144823

本篇文章就利用编写一个最简单的网络计算器来感受这个通信的过程

网络计算器

protocol.hpp

这个头文件用来编写协议及序列化反序列化的过程。

  1. 首先因为是一个计算器所以肯定需要两个数和一个计算符号,但是作为服务端不能要求客户怎么样去输入这个计算的格式,可能客户会输入 1+1 也可能会输入 1 + 1 。因此作为服务端要将客户的输入识别成自己的规定。这里就规定数 计算符号 数。所以可以先规定好分隔符,利用宏定义方便修改。
  2. 因为TCP是面向字节流的,所以要明确通信的数据的边界,不能读多也不能读少。因为对于TCP而言,它是全双工的,所以就会出现接收方来不及读,导致整个缓冲区里有大量的数据,因此就要规定好边界保证读到的是一个完整的数据
  3. 对于计算器而言,首先是要获得到需要计算的数据,然后处理得到计算结果。因此可以定义两个结构体,一个结构体负责发送请求也就是发送计算的数据,另一个结构体负责响应请求也就是处理计算结果。
  4. 在发送请求的结构体里保存了两个数和计算符号,但是由于需要网络通信,所以要在结构体里定义好序列化过程的方法。同时服务端拿到数据后要想处理数据就必须要先反序列化,所以结构体里也定义好反序列化过程的方法
  5. 在响应请求的结构体里,同样的服务端需要将计算好的数据发回给客户端也需要定义好序列化过程的方法,而客户端要获取数据也需要反序列化过程的方法
  6. 因为要确保读到的数据是完整的一个数据,因此可以定义一个函数,将序列化好的数据加上一个报头,也就是这个数据的长度,用来标识这个数据的长度,读的时候就可以根据长度的依据去读取。
  7. 当然因为这个报头并不是需要真正要传输的数据,所以需要再定义一个函数用来去掉这个报头
  8. 当两端读取到数据时就需要判断读到的是否是一个完整的数据。如果还没读到完整的数据就继续读。可以定义一个函数用来读取数据并且判断是否读到完整数据,这个依据就是读到的数据的长度是否和原数据的长度相等,这个原数据长度为报头加上正文加上分隔符的长度
#pragma once

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

using namespace std;

// 定义好分隔符
#define SEP " "                       // 一条数据里每个元素的分隔符
#define SEP_LEN strlen(SEP)           // 分隔符的大小
#define LINE_SEP "\r\n"               // 数据与数据的分隔符
#define LINE_SEP_LEN strlen(LINE_SEP) // 分隔符大小

// 为通信的数据加上数据的长度和分割
// 确保每条数据都能精确的读取到,不读多也不读少
// "text.size()"\r\n"text"\r\n
string enlength(const string &text)
{
    string res = to_string(text.size());
    res += LINE_SEP;
    res += text;
    res += LINE_SEP;

    return res;
}

// 将全部的一条数据去掉前面的数据长度
// 提取出原始数据
bool delength(const string &package, string *text)
{
    // 找到第一个元素分隔符,弃掉前面的长度
    auto pos = package.find(LINE_SEP);
    if (pos == string::npos)
        return false;

    // 确认正文的长度
    int len = stoi(package.substr(0, pos));

    // 从第一个分割符往后开始到记录的字符串长度就是原始的数据
    *text = package.substr(pos + LINE_SEP_LEN, len);

    return true;
}

// 请求
class Request
{
public:
    int _x;
    int _y;
    char _op;

    Request()
        : _x(0), _y(0), _op('0')
    {
    }

    Request(int x, int y, char op)
        : _x(x), _y(y), _op(op)
    {
    }

    // 序列化过程
    // 因为通信的数据一开始分为了好几个独立的元素
    // 所以将这些独立的元素合并成一个数据
    bool serialize(string *out)
    {
        *out = "";
        *out += to_string(_x);
        *out += SEP;
        *out += _op;
        *out += SEP;
        *out += to_string(_y);
        return true;
    }

    // 反序列化过程
    // 将合并的一整个数据分解回原始的几个独立数据
    bool unserialize(const string &in)
    {
        auto left = in.find(SEP);
        auto right = in.rfind(SEP);

        if (left == string::npos || right == string::npos || left == right)
            return false;
        // 因为对于计算器而言,计算符号只有1位
        if (right - left - SEP_LEN != 1)
            return false;

        _x = stoi(in.substr(0, left));
        _y = stoi(in.substr(right + SEP_LEN));
        _op = in[left + SEP_LEN];
        return true;
    }
};

// 响应请求
class Response
{
public:
    int _exitcode; // 返回码
    int _result;   // 返回结果

    Response()
        : _exitcode(0), _result(0)
    {
    }

    Response(int exitcode, int result)
        : _exitcode(exitcode), _result(result)
    {
    }

    bool serialize(string *out)
    {
        *out = "";
        *out += to_string(_exitcode);
        *out += SEP;
        *out += to_string(_result);
        return true;
    }

    bool unserialize(const string &in)
    {
        auto pos = in.find(SEP);

        if (pos == string::npos)
            return false;

        _exitcode = stoi(in.substr(0, pos));
        _result = stoi(in.substr(pos + SEP_LEN));
        return true;
    }
};

// 读取数据并且判断是否是个完整的数据的方法
bool recvPackage(int sock, string &buff, string *text)
{
    char buffer[1024];
    while (1)
    {
        ssize_t n = recv(sock, buffer, sizeof(buffer), 0);
        if (n > 0)
        {
            // 找到报头和正文之间的分隔符
            buffer[n] = 0;
            buff += buffer;
            auto pos = buff.find(LINE_SEP);
            if (pos == string::npos)
                continue;

            // 拿到正文的长度
            int len = stoi(buff.substr(0, pos));
            // 判断inbuff的长度是否等于整个数据的长度
            // 如果相等说明读到了完成的数据
            int max_len = len + 2 * LINE_SEP_LEN + buff.substr(0, pos).size(); // 整个数据的长度
            if (buff.size() < max_len)
                continue;
            cout << "目前拿到的所有报文:\n" << buff << endl;

            // 到这一步说明至少有一个完整的数据
            // 将整个完整的数据传回指针
            *text = buff.substr(0, max_len);
            cout << "完整的报文:\n" << *text << endl;
            buff.erase(0, max_len);
            return true;
        }

        else
            return false;
    }

    return true;
}

Server.hpp

服务端就定义一个函数将整个的读取、反序列化、计算、序列化、发送的过程全部编写好,然后服务端启动加上一个函数参数,也就是计算过程的函数。

#pragma once

#include "log.hpp"
#include "Protocol.hpp"
#include <sys/types.h>
#include <sys/socket.h>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include <sys/types.h>
#include <sys/wait.h>

typedef function<bool(const Request &req, Response &res)> func_t;

void HandlerEntery(int sock, func_t func)
{
    string buff;
    while (1)
    {
        // 读取
        // 需要保证读到的是一个完整的请求
        string req_text;
        if (!recvPackage(sock, buff, &req_text))
            return;
        cout << "带报头的请求: \n" << req_text << endl;

        // 将读到的数据的头部去掉,也就是数据长度
        string req_str;
        if (!delength(req_text, &req_str))
            return;
        cout << "原始数据: " << req_str << endl;

        // 对读到的原始数据进行反序列化
        Request req;
        if (!req.unserialize(req_str))
            return;

        // 将反序列化后的结果计算出来后
        // 将结果放到响应类对象里
        // 通过响应类对象提取到结果
        Response res;
        func(req, res);

        string res_str;
        // 得到响应类对象序列化结果
        res.serialize(&res_str);
        cout << "计算完成,结果序列化:" << res_str << endl;

        // 再将得到的结果加上报头
        // 也就是数据长度确保数据的精确读取
        // 得到最终的序列化数据
        res_str = enlength(res_str);
        cout << "构建完整序列化数据完成:\n" << res_str << endl;

        // 将最终的数据发送回去
        send(sock, res_str.c_str(), res_str.size(), 0);
        cout << "服务端发送完成" << endl;
    }
}

class Server
{
public:
    Server(const uint16_t &port = 8000)
        : _port(port)
    {
    }

    void Init()
    {
        // 创建负责监听的套接字 面向字节流
        _listenSock = socket(AF_INET, SOCK_STREAM, 0);
        if (_listenSock < 0)
        {
            LogMessage(FATAL, "create socket error!");
            exit(1);
        }
        LogMessage(NORMAL, "create socket %d success!", _listenSock);

        // 绑定网络信息
        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(3);
        }
        LogMessage(NORMAL, "bind socket success!");

        // 设置socket为监听状态
        if (listen(_listenSock, 5) < 0)
        {
            LogMessage(FATAL, "listen socket error!");
            exit(4);
        }
        LogMessage(NORMAL, "listen socket success!");
    }

    void start(func_t func)
    {
        while (1)
        {
            // server获取建立新连接
            struct sockaddr_in peer;
            memset(&peer, 0, sizeof(peer));
            socklen_t len = sizeof(peer);
            // 创建通信的套接字
            // accept的返回值才是真正用于通信的套接字
            _sock = accept(_listenSock, (struct sockaddr *)&peer, &len);
            if (_sock < 0)
            {
                // 获取通信的套接字失败并不影响未来的操作,只是当前的链接失败而已
                LogMessage(ERROR, "accept socket error, next");
                continue;
            }
            LogMessage(NORMAL, "accept socket %d success", _sock);
            cout << "sock: " << _sock << endl;

            // 利用多进程实现
            pid_t id = fork();
            if (id == 0) // child
            {
                close(_listenSock);
                // 调用方法包括读取、反序列化、计算、序列化、发送
                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"); // ?
            }
        }
    }

private:
    int _listenSock; // 负责监听的套接字
    int _sock;       // 通信的套接字
    uint16_t _port;  // 端口号
};

Server.cc

这个服务端的计算函数就通过结构体的对象去作为参数完成,因为需要的数据都在结构体里

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

// 输出命令错误函数
void Usage(string proc)
{
    cout << "Usage:\n\t" << proc << " local_ip local_port\n\n";
}

// 计算方式
bool cal(const Request &req, Response &res)
{
    res._exitcode = 0;
    res._result = 0;
    switch (req._op)
    {
    case '+':
        res._result = req._x + req._y;
        break;
    case '-':
        res._result = req._x - req._y;
        break;
    case '*':
        res._result = req._x * req._y;
        break;
    case '/':
    {
        if (req._y == 0)
            res._exitcode = 1;
        else
            res._result = req._x / req._y;
    }
    break;

    default:
        res._exitcode = 2;
        break;
    }

    return true;
}

int main(int argc, char *argv[])
{
    // 启动服务端不需要指定IP
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(1);
    }

    uint16_t port = atoi(argv[1]);

    unique_ptr<Server> server(new Server(port));

    // 服务端初始化
    server->Init();
    //服务端启动
    server->start(cal);

    return 0;
}

Client.hpp

客户端和服务端一样也需要接收发送,不过客户端是先发送再接收。并且上述提过因为客户的输入方式无法控制,所以要定义一个函数将客户输入的数据提取到两个数和计算符号才能够构造请求的结构体对象

    #pragma once

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

using namespace std;

class Client
{
public:
    Client(const string &serverip, const uint16_t &port)
        : _serverip(serverip), _port(port), _sock(-1)
    {
    }

    void Init()
    {
        // 创建套接字
        _sock = socket(AF_INET, SOCK_STREAM, 0);
        if (_sock < 0)
        {
            LogMessage(FATAL, "create socket error");
            exit(1);
        }

        // TCP的客户端也不需要显示绑定端口,让操作系统随机绑定
        // TCP的客户端也不需要监听,因为并没有去主动链接客户端,所以不需要accept
        // TCP的客户端也不需要监听,因为并没有去主动链接客户端,所以不需要accept
    }

    void start()
    {
        // 向服务端发起链接请求
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = inet_addr(_serverip.c_str());
        if (connect(_sock, (struct sockaddr *)&local, sizeof(local)) < 0)
            LogMessage(ERROR, "connect socket error");

        // 和服务端通信
        else
        {
            string line;
            string buffer;
            while (1)
            {
                cout << "Please cin: " << endl;
                getline(cin, line);

                Request req = ParseLine(line);
                string text;
                req.serialize(&text);
                cout << "序列化后的数据:" << text << endl;
                string send_str = enlength(text);
                cout << "添加报头后的数据: \n" << send_str << endl;

                send(_sock, send_str.c_str(), send_str.size(), 0);

                // read
                // 拿到完整报文
                string package;
                if (!recvPackage(_sock, buffer, &package))
                    continue;
                cout << "拿到的完整报文: \n" << package << endl;

                // 拿到正文
                string end_text;
                if (!delength(package, &end_text))
                    continue;
                cout << "拿到的正文:" << end_text << endl;

                // 反序列化
                Response res;
                res.unserialize(end_text);
                cout << "exitCode: " << res._exitcode << " result: " << res._result << endl;
            }
        }
    }

    ~Client()
    {
        if (_sock >= 0)
            close(_sock);
    }

    // 将客户输入的数据提取构造请求结构体对象
    Request ParseLine(const string &line)
    {
        auto it = line.begin();
        // 提取左边的数字
        string left;
        while (it != line.end() && *it >= '0' && *it <= '9')
        {
            left += *it;
            ++it;
        }
        int leftnum = atoi(left.c_str());

        // 提取符号
        while (it != line.end() && *it != '+' && *it != '-' && *it != '+' && *it != '/')
            ++it;
        char op = *it;

        // 提取右边数字
        while (it != line.end() && (*it < '0' || *it > '9'))
            ++it;
        string right;
        while (it != line.end() && *it >= '0' && *it <= '9')
        {
            right += *it;
            ++it;
        }
        int rightnum = atoi(right.c_str());

        return Request(leftnum, rightnum, op);
    }

private:
    int _sock;
    string _serverip;
    uint16_t _port;
};

Client.cc

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

// 输出命令错误函数
void Usage(string proc)
{
    cout << "Usage:\n\t" << proc << " local_ip local_port\n\n";
}

int main(int argc, char *argv[])
{
    // 再运行客户端时,输入的指令需要包括主机ip和端口号
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }

    string serverip = argv[1];
    uint16_t port = atoi(argv[2]);
    unique_ptr<Client> client(new Client(serverip, port));

    client->Init();
    client->start();

    return 0;
}

log.txt

这里还加了一个记录日志的方法,可加可不加

#pragma once

#include <iostream>
#include <string>
#include <cstdarg>
#include <ctime>
#include <unistd.h>

using namespace std;

#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4

const char *to_levelstr(int level)
{
    switch (level)
    {
    case DEBUG:
        return "DEBUG";
    case NORMAL:
        return "NORMAL";
    case WARNING:
        return "WARNING";
    case ERROR:
        return "ERROR";
    case FATAL:
        return "FATAL";
    default:
        return nullptr;
    }
}

void LogMessage(int level, const char *format, ...)
{
#define NUM 1024
    char logpre[NUM];
    snprintf(logpre, sizeof(logpre), "[%s][%ld][%d]", to_levelstr(level), (long int)time(nullptr), getpid());

    char line[NUM];
    // 可变参数
    va_list arg;
    va_start(arg, format);

    vsnprintf(line, sizeof(line), format, arg);

    // 保存至文件
    FILE* log = fopen("log.txt", "a");
    FILE* err = fopen("log.error", "a");

    if(log && err)
    {
        FILE *curr = nullptr;
        if(level == DEBUG || level == NORMAL || level == WARNING) 
            curr = log;
        if(level == ERROR || level == FATAL) 
            curr = err;
        if(curr) fprintf(curr, "%s%s\n", logpre, line);

        fclose(log);
        fclose(err);
    }
}

通过结果再次理解通信过程

image-20230806220726845

所以最终的流程可以分解为几个步骤:

image-20230806221206524

Json

上面的序列化和反序列化过程呢都是自己定义的,所以看起来并不好看,而且可读性也不美观。

其实也会第三方库是帮我们做好了序列化和反序列化工作的,例如 Json,protobuf。因为Json的使用比较简单,所以这里就使用Json

首先需要安装第三方的 Jsoncpp的库

yum install jsoncpp-devel

安装好之后就可以使用第三方库了,需要注意因为是第三方库和线程库一样,编译的时候需要加上 -lJsoncpp的选项

肯定下面的代码注释就可以了解到Json的使用了,注:为了不修改上述的一些代码,下面使用条件编译,只看Json部分即可

// 请求
class Request
{
public:
    int _x;
    int _y;
    char _op;

    Request()
        : _x(0), _y(0), _op('0')
    {
    }

    Request(int x, int y, char op)
        : _x(x), _y(y), _op(op)
    {
    }

    // 序列化过程
    // 因为通信的数据一开始分为了好几个独立的元素
    // 所以将这些独立的元素合并成一个数据
    bool serialize(string *out)
    {
#ifdef MYSELF
        *out = "";
        *out += to_string(_x);
        *out += SEP;
        *out += _op;
        *out += SEP;
        *out += to_string(_y);
#else
        // Value是万能类型
        // 需要先定义出对象
        Json::Value root;
        // Json是kv结构存储的,所以需要定义k值标识v值
        root["first"] = _x;
        root["second"] = _y;
        root["op"] = _op;

        // Json要写入值给别的变量也需要先定义对象
        // 写的对象类型可以有几种,这里采用FastWriter
        Json::FastWriter w;
        // 调用write方法就可以写入
        *out = w.write(root);
#endif
        return true;
    }

    // 反序列化过程
    // 将合并的一整个数据分解回原始的几个独立数据
    bool unserialize(const string &in)
    {
#ifdef MYSELF
        auto left = in.find(SEP);
        auto right = in.rfind(SEP);

        if (left == string::npos || right == string::npos || left == right)
            return false;
        // 因为对于计算器而言,计算符号只有1位
        if (right - left - SEP_LEN != 1)
            return false;

        _x = stoi(in.substr(0, left));
        _y = stoi(in.substr(right + SEP_LEN));
        _op = in[left + SEP_LEN];
#else
        // 同样的需要先定义对象
        // 读的对象也需要定义
        Json::Value root;
        Json::Reader reader;
        // 调用读方法,将root的值读到in中
        reader.parse(in, root);

        // asInt表示切换为整形类型
        // 通过k值就可以得到v值
        _x = root["first"].asInt();
        _y = root["second"].asInt();
        _op = root["op"].asInt();
#endif
        return true;
    }
};

// 响应请求
class Response
{
public:
    int _exitcode; // 返回码
    int _result;   // 返回结果

    Response()
        : _exitcode(0), _result(0)
    {
    }

    Response(int exitcode, int result)
        : _exitcode(exitcode), _result(result)
    {
    }

    bool serialize(string *out)
    {
#ifdef MYSELF
        *out = "";
        *out += to_string(_exitcode);
        *out += SEP;
        *out += to_string(_result);
#else
        Json::Value root;
        root["exitcode"] = _exitcode;
        root["result"] = _result;

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

    bool unserialize(const string &in)
    {
#ifdef MYSELF
        auto pos = in.find(SEP);

        if (pos == string::npos)
            return false;

        _exitcode = stoi(in.substr(0, pos));
        _result = stoi(in.substr(pos + SEP_LEN));
#else
        Json::Value root;
        Json::Reader reader;
        reader.parse(in, root);

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

只需要更改序列化反序列化过程即可,外面的协定不需要改变

效果

image-20230806222713722

使用 Json序列化的就很美观

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

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

相关文章

【网络编程】利用套接字实现一个简单的网络通信(UDP实现聊天室 附上源码)

网络编程套接字 &#x1f41b;预备知识&#x1f98b;理解源IP地址和目的IP地址&#x1f40c;认识端口号&#x1f41e; 理解 "端口号" 和 "进程ID"&#x1f41c;简单认识TCP协议&#x1f99f;简单认识UDP协议&#x1f997; 什么是网络字节序 &#x1f577;相…

MySQL的关键指标及采集方法

MySQL 是个服务&#xff0c;所以我们可以借用 Google 四个黄金指标的思路来解决问题。 1、延迟 应用程序会向 MySQL 发起 SELECT、UPDATE 等操作&#xff0c;处理这些请求花费了多久&#xff0c;是非常关键的&#xff0c;甚至我们还想知道具体是哪个 SQL 最慢&#xff0c;这样…

【Linux】进程间通信——System V信号量

目录 写在前面的话 一些概念的理解 信号量的引入 信号量的概念及使用 写在前面的话 System V信号量是一种较低级的IPC机制&#xff0c;使用的时候需要手动进行操作和同步。在现代操作系统中&#xff0c;更常用的是POSIX信号量&#xff08;通过sem_*系列的函数进行操作&…

linux系统虚拟主机开启支持Swoole Loader扩展

特别说明&#xff1a;只是安装支持Swoole扩展&#xff0c;主机并没有安装服务端。目前支持版本php5.4-php7.2。 1、登陆主机控制面板&#xff0c;找到【远程文件下载】这个功能。 2、远程下载文件填写http://download.myhostadmin.net/vps/SwooleLoader_linux.zip 下载保存的路…

SpringBoot 升级内嵌Tomcat

SpringBoot 更新 Tomcat 最近公司的一个老项目需要升级下Tomcat&#xff0c;由于这个项目我完全没有参与&#xff0c;所以一开始我以为是一个老的Tomcat项目&#xff0c;升级它的Tomcat依赖或者是Tomcat容器镜像&#xff0c;后面发现是一个SpringBoot项目&#xff0c;升级的是…

加速中产 “返贫” 的4个迹象

没有消息&#xff0c;就是好消息。这话放在现在的朋友圈子里&#xff0c;似乎很合适。最近接到两个朋友的电话&#xff0c;一个是朋友的诉苦电话&#xff0c;这位朋友曾是某大厂的高管&#xff0c;被裁后失业近1年&#xff0c;虽然当初赔了N1&#xff0c;但架不住这位朋友“房贷…

Maven 打包生成Windows和Liunx启动文件

新建一个springboot项目。 1、项目结构 2、pom.xml <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocati…

ASP.NET Core MVC -- 将视图添加到 ASP.NET Core MVC 应用

Index页 右键单击“视图”文件夹&#xff0c;然后单击“添加”>>“新文件夹”&#xff0c;并将文件夹命名为“HelloWorld”。 右键单击“Views/HelloWorld”文件夹&#xff0c;然后单击“添加”>“新项”。 在“添加新项 - MvcMovie”对话框中&#xff1a; 在右上…

Misc取证学习

文章目录 Misc取证学习磁盘取证工具veracryto挂载fat文件DiskGenius 磁盘取证例题[RCTF2019]disk 磁盘[](https://ciphersaw.me/ctf-wiki/misc/disk-memory/introduction/#_2)内存取证工具volatility 内存取证例题数字取证赛题0x01.从内存中获取到用户admin的密码并且破解密码 …

gin和gorm框架安装

理论上只要这两句命令 go get -u gorm.io/gorm go get -u github.com/gin-gonic/gin然而却出现了问题 貌似是代理问题&#xff0c;加上一条命令 go env -w GOPROXYhttps://goproxy.cn,direct 可以成功安装 安装gorm的数据库驱动程序 go get -u gorm.io/driver/mysql

香港人力资源服务商迦里仕人才,申请纳斯达克IPO上市

来源&#xff1a;猛兽财经 作者&#xff1a;猛兽财经 猛兽财经获悉&#xff0c;总部位于香港的人力资源服务商迦里仕人才&#xff08;Galaxy Payroll Group&#xff09;近期已向美国证券交易委员会&#xff08;SEC&#xff09;提交招股书&#xff0c;申请在纳斯达克IPO上市&am…

VUE框架:vue2转vue3全面细节总结(4)滚动行为

大家好&#xff0c;我是csdn的博主&#xff1a;lqj_本人 这是我的个人博客主页&#xff1a; lqj_本人_python人工智能视觉&#xff08;opencv&#xff09;从入门到实战,前端,微信小程序-CSDN博客 最新的uniapp毕业设计专栏也放在下方了&#xff1a; https://blog.csdn.net/lbcy…

Redis可视化工具

Redis可视化工具 1、RedisInsight 下载地址&#xff1a;https://redis.com/redis-enterprise/redis-insight/ 双击软件进行安装&#xff0c;安装完后弹出如下界面&#xff1a; 安装完成后在主界面选择添加Redis数据库&#xff1b; 选择手动添加数据库&#xff0c;输入Redis…

GD32F103VE独立看门狗

GD32F103VE独立看门狗 看门狗定时器(WDGT)有两个&#xff1a; 1&#xff0c;独立看门狗定时器(FWDGT) 2&#xff0c;窗口看门狗定时器(WWDGT) 独立看门狗定时器(FWDGT)有一个独立的时钟源(IRC40K). 独立看门狗定时器有一个向下计数器,当计数器到达0&#xff0c;会让CPU复位。…

二叉树的前序遍历、中序遍历、后序遍历、层次遍历的实现

DLR–前序遍历&#xff08;根在前&#xff0c;从左往右&#xff0c;一棵树的根永远在左子树前面&#xff0c;左子树又永远在右子树前面 &#xff09; LDR–中序遍历&#xff08;根在中&#xff0c;从左往右&#xff0c;一棵树的左子树永远在根前面&#xff0c;根永远在右子树前…

从0到1开发go-tcp框架【4实战片— — 开发MMO之玩家聊天篇】

从0到1开发go-tcp框架【实战片— — 开发MMO】 MMO&#xff08;MassiveMultiplayerOnlineGame&#xff09;&#xff1a;大型多人在线游戏&#xff08;多人在线网游&#xff09; 1 AOI兴趣点的算法 游戏中的坐标模型&#xff1a; 场景相关数值计算 ● 场景大小&#xff1a; 250…

Vue2:组件基础(下)

Vue2&#xff1a;组件基础&#xff08;下&#xff09; Date: April 12, 2023 Sum: props验证、计算属性、自定义时间、组件上的v-model、任务列表案例 Tags: * 目标&#xff1a; 能够知道如何对 props 进行验证 能够知道如何使用计算属性 令能够知道如何为组件自定义事件 …

K8S系列文章 之 容器网络基础 Docker0

什么是Docker0 使用ip addr命令看一下网卡&#xff1a; rootKitDevVps:~# ip addr 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00inet 127.0.0.1/8 scope host…

火车头标题伪原创【php源码】

大家好&#xff0c;给大家分享一下python怎么读取文件中的数据&#xff0c;很多人还不知道这一点。下面详细解释一下。现在让我们来看看&#xff01; 火车头采集ai伪原创插件截图&#xff1a; python是一门非常火爆且相对易学的编程语言&#xff0c;应用在各种场景。许多人想学…

VS code:Task

Task 微软官方连接&#xff1a; https://code.visualstudio.com/docs/editor/tasks what is Task 我们知道&#xff0c;vscode可以支持许多编程语言&#xff0c;很多语言是需要进行编译的&#xff0c;打包&#xff0c;测试… 有许多已有的工具支持这些流程&#xff0c;例如A…