【应用层协议】自定义协议 {定义结构化数据;数据格式转换:序列化和反序列化,使用json库进行数据格式交换;分包和解包:为报文内容添加报头}

news2024/12/30 2:09:58

一、简单了解TCP协议(引子)

在这里插入图片描述

1.1 三次握手

三次握手就是客户端向服务端发起连接的过程

在这里插入图片描述

服务器初始化

  1. 调用socket,创建套接字文件

  2. 调用bind,将当前的文件描述符和ip/port绑定在一起;如果这个端口已经被其他进程占用了,就会bind失败

  3. 调用listen,将套接字文件设置为监听状态,为后面的accept做好准备

  4. 调用accept,阻塞等待客户端的链接请求

客户端发起连接

  1. 调用socket,创建套接字文件

  2. 调用connect,向服务器发起连接请求:

    • connect会发出SYN并阻塞等待服务器应答 (第一次)

    • 服务器收到客户端的SYN,会应答一个SYN+ACK段表示"同意建立连接" (第二次)

    • 客户端收到SYN+ACK后会从connect返回,同时应答一个ACK段 (第三次)

    • 最后服务端收到ACK段后从accept返回,建立连接成功。

这个建立连接的过程, 通常称为“三次握手”


1.2 数据传输

双方建立好连接之后就可以进行数据传输了

在这里插入图片描述

TCP协议提供全双工通信服务

在这里插入图片描述

  • tcp是全双工通信,因为每个链接(每个套接字文件)都有对应的发送和接收缓冲区,收发可以并发执行(但是多线程单独收或发必须串行执行)
  • read, write只负责应用层和传输层缓冲区之间数据的拷贝,而传输层缓冲区和网络之间数据的收发(发多少,何时发)则完全是由tcp协议负责,所以TCP协议称为传输控制协议。

TCP是面向字节流协议

  • 当tcp在传输层发送消息时,一个tcp报文可能会被分割成多条消息转发给网络层。我们不能认为一条消息就是一个tcp报文,所以tcp是面向字节流的协议
  • 由于一条消息对应的不是一个tcp报文,如果接受方不知道一个tcp报文的长度或者分割的边界在哪里,就会无法组装成一个单独完整的报文,这就形成了粘包问题
  • TCP协议只负责适时收发缓冲区中的数据,至于数据包是否完整(可能只发送了一部分),是否是单独的数据包(可能一次读到多份数据),数据包内容的解析等则需要由应用层协议负责,这些内容都会在后面的自定义协议(应用层)当中体现。

1.3 四次挥手

如果不想通信了,双方就要断开连接

在这里插入图片描述

断开连接的过程

  1. 如果客户端没有更多的请求了,就调用close关闭连接, 客户端会向服务器发送FIN段 (第一次)

  2. 此时服务器收到FIN后, 会回应一个ACK,同时read会返回0 (第二次)

  3. read返回之后,服务器就知道客户端关闭了连接,也调用close关闭连接,这个时候服务器会向客户端发送一个FIN (第三次)

  4. 客户端收到FIN,再返回一个ACK给服务器 (第四次)

这个断开连接的过程, 通常称为 ”四次挥手“


二、传输结构化数据

2.1 当前存在的问题

在之前的socket编程当中, 读写数据时都是按 “字符串” 的方式来发送接收的,如果我们要传输一些"结构化的数据" 怎么办呢?

什么是结构化的数据?

比如QQ聊天,就不能单纯发送消息过去,还要把头像url、时间昵称等打包形成一个报文,把这个报文的数据一起发送给对方,这个打包形成的报文就是一个结构化的数据

能不能将将结构体直接发送给对方呢?

不可以,不同的编程语言和编译环境对同一个结构体的描述可能不同,如内存对齐规则和数据的大小端模式等。如果选择直接收发二进制形式的结构体数据,可能会导致数据读写不一致的问题。

如何保证读取到完整、单独的数据?

在前面TCP协议的数据传输部分,我们就提到了这个问题,解决该问题的方法就是明确报文的大小和边界:

  1. 对报文进行定长
  2. 用特殊符号划分每条报文
  3. 自描述方式

2.2 序列化和反序列化

什么是序列化和反序列化?

  • 序列化是将数据结构或对象转换为字节流的过程。在序列化过程中,对象的状态信息被转换为字节序列,可以将其存储在文件中或通过网络传输
  • 反序列化是将字节流或其他存储形式转换回数据结构或对象的过程。在反序列化过程中,字节序列被重新转换为对象的状态信息,以便可以重新创建对象并使用其数据

序列化和反序列化的目的

  • 数据持久化:通过序列化,可以将对象的状态保存到文件或数据库中,以便在程序重新启动或重新加载时可以从中恢复对象的状态
  • 数据传输:通过序列化,可以将对象转换为字节流,以便在网络传输中进行传递
  • 跨平台和跨语言交互:通过序列化,可以将对象转换为通用的字节流格式,使得不同平台和不同编程语言之间可以进行数据交换和共享。无论是Java、Python、C++还是其他编程语言,只要能够进行序列化和反序列化操作,就可以实现跨平台和跨语言的数据交互

发送报文到网络时候,报文首先需要进行序列化,然后再发送,报文通过协议栈发送给对方后,接收报文的一方也需要对报文进行反序列化,才能正常使用该报文

在这里插入图片描述


2.3 为报文内容添加报头

我们将序列化后的结构化数据称为报文内容,为了保证能够读取到完整、单独的数据,我们需要:

  1. 用特殊符号划分每条报文:在报文和报文之间添加换行符进行划分(方便后期调试,也可以用其他符号)
  2. 自描述:为报文内容添加报头,其中可以包含协议类型、报文内容的长度等信息。

报头中包含协议类型:可以使用多态技术将应用层协议拓展出多种数据类型以适应各种应用需求。此时就需要报头中包含协议类型,使通信双方使用同一子类型协议。

报头中包含报文内容的长度:保证读取到完整、单独的数据

对比UDP协议

  • UDP协议的特点是面向数据包传输,系统会自动进行数据的格式处理,将数据和数据分开。发送几次就需要接收几次。
  • TCP协议的特点是面向字节流传输,系统不会对数据进行分包,需要用户自己在应用层定制协议进行分包和解包操作。

三、网络计算器程序

3.1 封装socket API

首先我们要对socket API进行封装,方便后续客户端和服务端使用socket API进行网络通信。

#pragma once

#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include "logmessage.hpp"

class Socket
{
    int _sockfd;
    static const int s_backlog = 20;

public:
    Socket()
    {
        _sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (_sockfd == -1)
        {
            LogMessage(FATAL, "socket(%d): %s", errno, strerror(errno));
            exit(errno);
        }
        LogMessage(DEBUG, "create socket success!");
    }

    void Bind(const std::string &ip, uint16_t port)
    {
        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(ip.c_str());

        if (bind(_sockfd, (sockaddr *)&local, sizeof(local)) == -1)
        {
            LogMessage(FATAL, "bind(%d): %s", errno, strerror(errno));
            exit(errno);
        }
        LogMessage(DEBUG, "bind socket success!");
    }

    void Listen()
    {
        if (listen(_sockfd, s_backlog))
        {
            LogMessage(FATAL, "listen(%d): %s", errno, strerror(errno));
            exit(errno);
        }
        LogMessage(DEBUG, "listen socket success!");
    }

    int Accept(std::string *ip, uint16_t *port)
    {
        sockaddr_in src;
        socklen_t len;
        int svcsock = accept(_sockfd, (sockaddr *)&src, &len);
        if (svcsock == -1)
        {
            LogMessage(ERROR, "accept(%d): %s", errno, strerror(errno));
            return -1;
        }
        if (ip != nullptr)
            *ip = inet_ntoa(src.sin_addr);
        if (port != nullptr)
            *port = ntohs(src.sin_port);
        return svcsock;
    }

    void Connect(const std::string &ip, uint16_t port)
    {
        sockaddr_in dst;
        memset(&dst, 0, sizeof(dst));
        dst.sin_family = AF_INET;
        dst.sin_port = htons(port);
        dst.sin_addr.s_addr = inet_addr(ip.c_str());

        if (connect(_sockfd, (sockaddr *)&dst, sizeof(dst)) == -1)
        {
            LogMessage(FATAL, "connect(%d): %s", errno, strerror(errno));
            exit(errno);
        }
    }

    int getsock()
    {
        return _sockfd;
    }

    ~Socket()
    {
        if (_sockfd >= 0)
        {
            close(_sockfd);
        }
    }
};

3.2 自定义协议

定制的协议,必须保证通信双方(客户端、服务端)能够遵守协议的约定。

应用层协议需要解决的问题:

  1. 定义结构化数据:定义通信双方都能够解释的结构化数据,数据可以分为请求数据(request)和响应数据(response),因此我们分别需要对请求数据和响应数据进行约定,可以用两个结构体进行封装数据的请求和响应
  2. 数据格式转换:序列化和反序列化,正确的传输结构化数据
  3. 分包和解包:为报文内容添加报头,保证读取到完整、单独的数据

提示:客户端和服务端要包含同一份协议代码,这样才能保证双方使用的是同一个协议。

protocol.hpp

#pragma once
#include <sstream>

#define SEP "|"
#define SPACE " "


struct Request
{
    int _l;
    int _r;
    char _op;

    Request() {}

    Request(int l, int r, char op)
        : _l(l), _r(r), _op(op)
    {
    }

    //序列化
    std::string Serialize()
    {
        std::stringstream oss;
        oss << _l << SPACE << _op << SPACE << _r;
        return oss.str();
    }
	//反序列化
    bool Deserialize(const std::string &str)
    {
        std::size_t left = str.find(SPACE);
        if (left == std::string::npos)
            return false;
        std::size_t right = str.rfind(SPACE);
        if (right == std::string::npos)
            return false;
        _l = std::stoi(str.substr(0, left));
        _r = std::stoi(str.substr(right + strlen(SPACE)));
        if (left + strlen(SPACE) > str.size())
            return false;
        else
            _op = str[left + strlen(SPACE)];

        return true;
    }
};

struct Response
{
    int _ret;
    int _code;

    Response() {}

    Response(int ret, int code)
        : _ret(ret), _code(code)
    {
    }
	//序列化
    std::string Serialize()
    {
        std::stringstream oss;
        oss << _ret << SPACE << _code;
        return oss.str();
    }
	//反序列化
    bool Deserialize(const std::string &str)
    {
        std::size_t pos = str.find(SPACE);
        if (pos == std::string::npos)
            return false;
        _ret = std::stoi(str.substr(0, pos));
        _code = std::stoi(str.substr(pos + strlen(SPACE)));
        return true;
    }
};

//报头的添加和解析是以上两个结构通用的,所以定义成全局函数。
//解析报头
std::string Decode(std::string &buffer)
{
    size_t pos = buffer.find(SEP);
    if (pos == std::string::npos)
        return "";
    int len = std::stoi(buffer.substr(0, pos)); //报头中的报文内容长度
    int content_len = buffer.size() - pos - 2 * strlen(SEP); //减去报头和2个sep的剩余长度
    if (content_len >= len)
    {
        //报文完整,获取报文内容,将完成解析的报文从缓冲区中去除
        buffer.erase(0, pos+strlen(SEP));
        std::string package;
        package = buffer.substr(0, len);
        buffer.erase(0, len+strlen(SEP));
        return package;
    }
    else
    {
        //报文不完整,返回空串
        return ""; 
    }
}

//添加报头和报文间分隔符
std::string Encode(const std::string &str)
{
    std::stringstream oss;
    oss << std::to_string(str.size()) << SEP << str << SEP;
    return oss.str();
}

//也封装以下读写函数
bool Receive(int sock, std::string *out)
{
    char buffer[1024];
    ssize_t s = recv(sock, buffer, sizeof(buffer), 0);
    if (s > 0)
    {
        buffer[s] = 0;
        *out += buffer;
        return true;
    }
    else
    {
        return false;
    }
}

void Send(int sock, const std::string &str)
{
    send(sock, str.c_str(), str.size(), 0);
}

3.2.1 send、recv函数

send函数,用于TCP发送数据

函数原型:

ssize_t send(int sockfd, const void *buf, size_t len, int flags);

参数说明:

  • sockfd:特定的文件描述符,表示将数据写入该文件描述符对应的套接字。
  • buf:需要发送的数据。
  • len:需要发送数据的字节个数。
  • flags:发送的方式,一般设置为0,表示阻塞式发送。

返回值说明:

  • 写入成功返回实际写入的字节数,写入失败返回-1,同时错误码会被设置。

提示:该函数与write函数功能一致

recv函数,用于TCP接收数据

函数原型:

ssize_t recv(int sockfd, void *buf, size_t len, int flags);

参数说明:

  • sockfd:特定的文件描述符,表示从该文件描述符中读取数据。
  • buf:数据的存储位置,表示将读取到的数据存储到该位置。
  • len:数据的个数,表示从该文件描述符中读取数据的字节数。
  • flags:读取的方式,一般设置为0,表示阻塞式读取。

返回值说明:

  • 如果返回值大于0,则表示本次实际读取到的字节个数。
  • 如果返回值等于0,则表示对端已经把连接关闭了。
  • 如果返回值小于0,则表示读取时遇到了错误。

提示:该函数的功能与read一致


3.2.2 使用json库进行数据格式交换

我们一般不用自己编写序列和反序列化,因为有现成的库支持像这样的数据格式交换。

常见的序列化和反序列化的库:

  • json

  • protobuf

  • xml

其中,json 是简单易上手的,C++、Java,Python等都支持,protobuf 和 json 是C++常用的

安装json库

yum install -y jsoncpp-devel

注意:普通用户需要 sudo 提权

安装完成,在系统默认路径下查看头文件和库文件

img

使用json需要包含头文件

#include <jsoncpp/json/json.h>

编译时需要指明链接jsoncpp库

img

使用json库进行数据格式交换

序列化

  1. 定义Json::Value对象,可以嵌套定义
  2. 使用value["x"] = n的方式添加键值对
  3. 定义Json::StyledWriter(序列化结果方便查看)或Json::FastWriter对象(常用于数据传输),两者唯一的区别就是序列化字符串的格式不同。
  4. 调用writer.write(value)成员函数返回Json::Value对象的序列化结果(类型string)

反序列化

  1. 定义Json::Value对象
  2. 定义Json::Reader对象
  3. 调用reader.parse(str,value)函数反序列化,将键值对填充到Json::Value对象中
  4. 取用键值对,使用value["x"].asInt()的方法按类型将value取出。
#pragma once
#include <sstream>
#include <jsoncpp/json/json.h>

#define SEP "|"
#define SPACE " "

struct Request
{
    int _l;
    int _r;
    char _op;

    Request() {}

    Request(int l, int r, char op)
        : _l(l), _r(r), _op(op)
    {
    }
	//序列化
    std::string Serialize()
    {
        //定义万能对象Json::Value
        Json::Value root; 
        //填充对象 key-value
        root["l"] = _l;
        root["r"] = _r;
        root["op"] = _op;
        //定义序列化对象Json::FastWriter或StyledWriter
        Json::FastWriter writer;
        //调用Json::FastWriter::write将Value对象序列化
        return writer.write(root);
    }
	//反序列化
    bool Deserialize(const std::string &str)
    {
        //定义万能对象Json::Value
        Json::Value root;
        //定义反序列化对象Json::Reader
        Json::Reader reader;
        //调用Json::Reader::parse将字节流反序列化
        reader.parse(str, root);
        //按类型取出Value对象中的数据
        _l = root["l"].asInt();
        _r = root["r"].asInt();
        _op = root["op"].asInt();
        return true;
    }
};

3.3 服务端代码

tcp_server.hpp

#pragma once

#include <string>
#include <vector>
#include <pthread.h>
#include <cstdio>
#include <functional>
#include "mysocket.hpp"
#include "protocol.hpp"
#include "logmessage.hpp"

class TcpServer
{
    using func_t = std::function<void(int)>;
    std::string _ip;
    uint16_t _port;
    Socket _listensock;
    std::vector<func_t> _funcs; //任务队列,可以提供多项服务

    struct ThreadData
    {
        int _sock;
        TcpServer *_self;

        ThreadData(int sock, TcpServer *self)
            : _sock(sock), _self(self)
        {
        }
    };

public:
    TcpServer(const std::string &ip, uint16_t port)
        : _ip(ip), _port(port)
    {
        _listensock.Bind(_ip, _port);
        _listensock.Listen();
    }

    void AddService(func_t func)
    {
        _funcs.push_back(func);
    }

    void Excute(int sock)
    {
        for (auto f : _funcs)
        {
            f(sock);
        }
    }

    void Start()
    {
        while (true)
        {
            // 1.获取新链接
            std::string client_ip;
            uint16_t client_port;
            int svcsock = _listensock.Accept(&client_ip, &client_port);
            if (svcsock == -1)
                continue;

            LogMessage(NORMAL, "[%s:%d] join, accept a new link!", client_ip.c_str(), client_port);
			
            // 2.创建新线程为连接提供服务
            pthread_t tid;
            ThreadData *ptd = new ThreadData(svcsock, this);
            pthread_create(&tid, nullptr, ThreadRoutine, ptd);
        }
    }

    static void *ThreadRoutine(void *args)
    {
        pthread_detach(pthread_self());
        // ThreadData *td = (ThreadData *)args;
        ThreadData *td = static_cast<ThreadData *>(args);
        td->_self->Excute(td->_sock); //执行任务队列中的任务
        LogMessage(NORMAL, "client quit!");
        close(td->_sock);
        delete td;
    }
};

tcp_server.cc

#include <memory>
#include <signal.h>
#include <iostream>
#include "tcp_server.hpp"
#include <unistd.h>

void Usage(const char *proc)
{
    printf("\nUsage: %s port\n", proc);
}

Response calchelper(const Request &req)
{
    Response resp(0, 0);
    switch (req._op)
    {
    case '+':
        resp._ret = req._l + req._r;
        break;
    case '-':

        resp._ret = req._l - req._r;
        break;
    case '*':
        resp._ret = req._l * req._r;
        break;
    case '/':
        if (req._r == 0)
            resp._code = 1;
        else
            resp._ret = req._l / req._r;
        break;
    case '%':
        if (req._r == 0)
            resp._code = 2;
        else
            resp._ret = req._l % req._r;
        break;
    default:
        resp._code = 3;
        break;
    }
    return resp;
}

void *calculator(int sock)
{
    std::string inbuffer; //应用层缓冲区
    while (true)
    {
        // 1.读取成功
        bool ret = Receive(sock, &inbuffer);
        if (!ret)
            break;
        // std::cout << inbuffer << std::endl;
        // 2.协议解析,得到一个完整的报文
        std::string package = Decode(inbuffer);
        if (package.empty()) //报文不完成,继续读取
            continue;
        // std::cout << package << std::endl;
        // std::cout << inbuffer << std::endl;
        LogMessage(NORMAL, "%s", package.c_str());
        // 3.反序列化,字节流->结构化数据
        Request req;
        req.Deserialize(package);
        // 4.业务逻辑
        Response resp = calchelper(req);
        // 5.序列化业务的处理结果
        std::string respstr = resp.Serialize();
        // std::cout << respstr << std::endl;
        // 6.添加报头(长度信息),形成一个完成的报文
        respstr = Encode(respstr);
        // std::cout << respstr << std::endl;
        Send(sock, respstr);
    }
}

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(1);
    }

    uint16_t port = atoi(argv[1]);
    // server在编写时要有较为严谨的判断逻辑
    // 一般服务器是要忽略SIGPIPE信号的,防止在运行过程中出现非法写入的问题
    std::unique_ptr<TcpServer> psvr(new TcpServer("0.0.0.0", port));
    psvr->AddService(calculator);
    daemon(0,0); //守护进程化
    psvr->Start();
    return 0;
}

3.4 客户端代码

tcp_client.hpp

#pragma once

#include <iostream>
#include <string>
#include "mysocket.hpp"
#include "protocol.hpp"
#include "logmessage.hpp"

class TcpClient
{
    Socket _sockfd;
    std::string _svrip;
    uint16_t _svrport;

public:
    TcpClient(const std::string &ip, uint16_t port)
        : _svrip(ip), _svrport(port)
    {
        _sockfd.Connect(ip, port);
    }

    void Run()
    {
        std::string inbuffer; //应用层缓冲区
        bool quit = false;
        while (!quit)
        {
            // 1.获取需求
            Request req;
            std::cin >> req._l >> req._op >> req._r;
            // 2.序列化
            std::string reqstr = req.Serialize();
            std::string temp = reqstr;
            // 3.添加长度报头
            reqstr = Encode(reqstr);
            // 4.向服务端发送数据
            Send(_sockfd.getsock(), reqstr);

            while (true)
            {
                // 5.从服务端接收数据
                bool ret = Receive(_sockfd.getsock(), &inbuffer);
                if (!ret)
                {
                    quit = true;
                    break;
                }
                // 6.获取一个完成的报文
                std::string respstr = Decode(inbuffer);
                if (respstr.empty()) //报文不完整,继续接收
                    continue;
                // 7.反序列化
                Response resp;
                resp.Deserialize(respstr);
                // 8.输出结果
                // std::cout << "ret: " << resp._ret << std::endl;
                // std::cout << "code: " << resp._code << std::endl;
                std::string err;
                switch (resp._code)
                {
                case 1:
                    err = "除0错误";
                    break;
                case 2:
                    err = "模0错误";
                    break;
                case 3:
                    err = "非法操作";
                    break;
                default:
                    std::cout << temp << " = " << resp._ret << " [success]" << std::endl;
                    break;
                }
                if (!err.empty())
                    std::cerr << err << std::endl;
                break;
            }
        }
    }
};

tcp_client.cc

#include "tcp_client.hpp"
#include <memory>
#include <string>

void Usage(const char *proc)
{
    printf("\nUsage: %s svrip svrport\n", proc);
}

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }

    std::string svrip = argv[1];
    uint16_t svrport = atoi(argv[2]);
    std::unique_ptr<TcpClient> pcli(new TcpClient(svrip, svrport));
    pcli->Run();
    return 0;
}

四、重谈协议分层

在这里插入图片描述
我们设计的网络服务器自然而然就完成了OSI七层模型中的上三层功能:

  1. 应用层:解释结构体中的每个字段,并完成了网络计算器的服务
  2. 表示层:定义固有的结构化数据,并通过序列化和反序列化完成了数据格式的转换,通过添加报头保证了报文的单独性和完整性。
  3. 会话层:服务器接收到新链接后通过创建新线程为每个链接提供服务,同时新线程也负责管理链接的断开

在这里插入图片描述

在TCP/IP五层模型中,将OSI模型中的上三层归为一层应用层。这是因为对于不同的网络服务上三层所对应的具体工作可能不同,操作系统不能统一设计一种协议适用于所有服务,因此将这三层归为一层交给用户自行实现或选择。

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

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

相关文章

整理 酷炫 Flutter 开源UI框架 按钮

flutter_percent_indicator Flutter 百分比指示器库 项目地址&#xff1a;https://github.com/diegoveloper/flutter_percent_indicator 项目Demo&#xff1a;https://download.csdn.net/download/qq_36040764/89631340

springboot整合doris(doris创建表)

Doris 的数据模型主要分为 3 类: 明细模型(Duplicate Key Model):允许指定的 Key 列重复;适用于必须保留所有原始数据记录的情况主键模型(Unique Key Model):每一行的 Key 值唯一;可确保给定的 Key 列不会存在重复行聚合模型(Aggregate Key Model):可根据 Key 列聚…

力扣每日一题 特殊数组 II 前缀和

Problem: 3152. 特殊数组 II &#x1f468;‍&#x1f3eb; 参考题解 Code class Solution {public boolean[] isArraySpecial(int[] nums, int[][] queries) {int[] s new int[nums.length];for (int i 1; i < nums.length; i) {s[i] s[i - 1] (nums[i - 1] % 2 num…

用R语言进行数据的特征缩放主要方法

下面内容摘录自《R 语言与数据科学的终极指南》专栏文章的部分内容&#xff0c;每篇文章都在 5000 字以上&#xff0c;质量平均分高达 94 分&#xff0c;看全文请点击下面链接&#xff1a; 4章11节&#xff1a;用R做数据重塑&#xff0c;数据的特征缩放和特征可视化-CSDN博客文…

第八季完美童模全球冠军·韩嘉滢 破浪扬帆写就传奇

在这个充满奇迹与梦想的舞台上&#xff0c;星光少女韩嘉滢以她独有的光芒&#xff0c;照亮了第八季完美童模的每一个角落。从亚特兰蒂斯的神秘海域到典故里的中国古韵&#xff0c;她以多变的风格与卓越的表现&#xff0c;征服了全球观众的心&#xff0c;最终荣登全球冠军的宝座…

K8S资源之Service

概念 将一组 Pods 公开为网络服务的抽象方法。 ClientIP 模型 集群内访问类型。 命令行 # 暴露端口 kubectl expose deployment my-dep-nginx --port8000 --target-port80Yml文件 apiVersion: v1 kind: Service metadata:labels:app: my-dep-nginxname: my-dep-nginx spe…

中小型企业可用的数据采集监控平台 为生产带来众多改变

中小型企业采用数据采集监控平台可以显著提升生产效率、优化生产流程、增强决策能力&#xff0c;并带来一系列积极的改变。 数据采集监控平台可提供从边缘感知设备到云端的数据采集、存储、分析、可视化等服务&#xff0c;实现生产工艺流程仿真、设备运行状态监控、数据报表、趋…

渗透测试实战-菠菜站渗透测试(Nacos反序列化漏洞利用)

免责声明&#xff1a;文章来源于真实渗透测试&#xff0c;已获得授权&#xff0c;且关键信息已经打码处理&#xff0c;请勿利用文章内的相关技术从事非法测试&#xff0c;由于传播、利用此文所提供的信息或者工具而造成的任何直接或者间接的后果及损失&#xff0c;均由使用者本…

leetcode 103.二叉树的锯齿形层序遍历

1.题目要求: 给你二叉树的根节点 root &#xff0c;返回其节点值的 锯齿形层序遍历 。&#xff08;即先从左往右&#xff0c;再从右往左进行下一层遍历&#xff0c;以此类推&#xff0c;层与层之间交替进行&#xff09;。2.做题思路:由题我们可以判断&#xff0c;树中每到偶数…

IV(电流-电压)测试和CV(电容-电压)测试

IV&#xff08;电流-电压&#xff09;测试和CV&#xff08;电容-电压&#xff09;测试是半导体参数表征中非常重要的两种测试方法。 一、IV测试&#xff08;电流-电压测试&#xff09; 1. 定义与目的 IV测试是测量半导体器件在不同电压下的电流响应&#xff0c;以评估其电学…

树莓派智能语音助手之ASR – SpeechRecognition+PocketSphinx

想要让树莓派成为智能语音助手&#xff0c;除了会“说”&#xff0c;也要会“听”。接下来&#xff0c;就要让它实现ASR&#xff08;语音识别&#xff09;功能了。 本来想用sherpa-onnx来实现离线语音识别的&#xff0c;可惜&#xff0c;我的树莓派系统太老了&#xff0c;折腾…

mybatis常见面试问题

0.原生JDBC样例: public class MainClass { public static void main(String[] args) { Connection conn = null; Statement stmt = null; ResultSet rs = null; try { // 加载数据库驱动 Class.forName("com.mysql.cj.jdbc.Driver"); // 建立连接 String url = &q…

视觉SLAM ch3补充——在Linux中配置VScode以及CMakeLists如何添加Eigen库

ch3中的所有代码&#xff0c;除了在kdevelop中运行&#xff0c;还可以在VScode中运行。下面将简要演示配置过程&#xff0c;代码不再做解答&#xff0c;详细内容在下面的文章中。&#xff08;这一节中的pangolin由于安装过程中会出现很多问题&#xff0c;且后续内容用不到该平台…

重磅官宣!追光少年【彭禹锦】荣担任中国美育促进网amp;IPA美育中国行代言人

在美育事业蓬勃发展的今天&#xff0c;一位年仅15岁的少年以其非凡的艺术才华和积极向上的精神风貌&#xff0c;成为了万众瞩目的焦点。中国美育促进网与IPA美育中国行正式宣布&#xff0c;才华横溢、正能量满满的追光少年彭禹锦受邀担任“中国美育促进网&IPA美育中国行”代…

c++中__int128的使用

需要10^30才可以存&#xff0c;我们可以用__int128来算 #include<bits/stdc.h>using namespace std;bool is_sqr(__int128 x) {__int128 l -1, r 1e16;while (r - l > 1) {__int128 m (l r) / 2;__int128 m2 m * m;if (m2 < x) {l m;} else {r m;}}if (l *…

【机器学习】RNN循环神经网络的基本概念、工作原理(含python代码)和应用领域

引言 递归神经网络&#xff08;RNN&#xff09;是一类用于处理序列数据的神经网络。它们在处理如时间序列数据、语音、文本和其他序列格式数据时特别有用 文章目录 引言一、RNN的基本概念1.1 RNN的定义组成1.1.1 单个神经元1.1.2 网络结构 1.2 RNN的工作原理及代码示例1.2.1 循…

8月7日-8日学习

首先是昨天看到的gemma 2 中训练2B模型用了知识蒸馏&#xff0c;找了一下技术报告 结果先找到了一代的半天没有看到知识蒸馏的部分 然后在二代里面找到了 只有很小的一部分 就是用小模型学习大模型的概率预测分布 然后这里的话又找到了华为发布的小模型论文 Rethinking Optim…

Centos7安装Redis(采用docker安装方式)

文章目录 1 拉取Redis镜像2 上传并修改配置文件3 启动Docker容器4 查看Docker是否正常启动 linux系统安装redis可以自己上传程序&#xff0c;手动启动&#xff0c;也可以用docker以容器形式启动。 redis建议可采用docker安装&#xff0c;如果是例如mysql这种追求稳定的关系型数…

mmdeployv0.6 mmdetectionv2.4、mmcv-full1.6安装及环境搭建

一、安装条件 Step 1.创建conda虚拟环境并激活 conda create -n mmdeploy python3.8 -y #创建环境 conda activate mmdeploy #激活环境Step 2.安装 PyTorch #如果网不好&#xff0c;可以这样安装 pip3 install torch1.8.1cu111 torchvision0.9.1cu111 -…

基于python的百度迁徙迁入、迁出数据分析(十)

副标题&#xff1a;从百度迁徙数据看——迁徙数据特征分析 书接上文&#xff0c;我们先回顾一下我们统计的时间口径&#xff1a;2024年2月1日——2024年8月1日&#xff0c;这里包含了春运、清明、五一、端午、四个中大型国定假日&#xff0c;我们详细分析一下在当下的统计周期…