【Linux网络】应用层自定义协议与序列化及Socket模拟封装

news2025/4/25 11:20:53

📢博客主页:https://blog.csdn.net/2301_779549673
📢博客仓库:https://gitee.com/JohnKingW/linux_test/tree/master/lesson
📢欢迎点赞 👍 收藏 ⭐留言 📝 如有错误敬请指正!
📢本文由 JohnKi 原创,首发于 CSDN🙉
📢未来很长,值得我们全力奔赴更美好的生活✨

在这里插入图片描述

在这里插入图片描述

文章目录

  • 🏳️‍🌈一、应用层
    • 1.1 再谈 "协议"
    • 1.2 网络版计算器
    • 1.3 序列化和反序列化
  • 🏳️‍🌈二、什么是全双工
  • 🏳️‍🌈三、Socket封装
    • 3.1 整体结构
    • 3.2 Socket 基类
    • 3.3 TcpSocket 子类
      • 3.3.1 基本结构
      • 3.3.2 构造、析构函数
      • 3.3.3 创建套接字
      • 3.3.4 绑定套接字
      • 3.3.5 监听套接字
      • 3.3.6 获取连接
      • 3.3.7 建立连接
      • 3.3.8 其他方法
      • 3.3.10 整体代码
  • 👥总结


11111111
11111111
11111111
11111111
**** 11111111

🏳️‍🌈一、应用层

我们程序员写的一个个解决我们实际问题, 满足我们日常需求的网络程序, 都是在应用层.

1.1 再谈 “协议”

协议是一种 “约定”. socket api 的接口, 在读写数据时, 都是按 “字符串” 的方式来发送接收的. 如果我们要传输一些 “结构化的数据” 怎么办呢?

其实,协议就是双方约定好的结构化的数据!

1.2 网络版计算器

例如, 我们需要实现一个服务器版的加法器. 我们需要客户端把要计算的两个加数发过去, 然后由服务器进行计算, 最后再把结果返回给客户端.

约定方案一(传结构体对象):

  • 客户端发送一个形如"1+1"的字符串;
  • 这个字符串中有两个操作数, 都是整形;
  • 两个数字之间会有一个字符是运算符, 运算符只能是 + ;
  • 数字和运算符之间没有空格;

不推荐直接传结构体对象,从技术和业务角度解释?

1、技术角度

  • 1、跨平台与兼容性:

    • 结构体的大小和内存布局可能因编译器、操作系统或硬件平台的不同而有所差异。这可能导致在一个平台上发送的结构体在另一个平台上无法正确解析。
  • 2、内存对齐与填充:

    • 为了优化内存访问速度,编译器可能会对结构体成员进行对齐和填充。这会导致结构体的实际大小大于其成员大小的总和。
    • 直接传输结构体可能会因为内存对齐和填充的问题而导致数据解析错误。
  • 3、指针与动态内存:

    • 结构体中可能包含指针,这些指针指向动态分配的内存。直接传输结构体无法传递指针所指向的数据,而只能传递指针值,这可能导致数据丢失或内存泄漏。

2、业务角度

  • 1、数据安全性:
    • 直接传输结构体可能会暴露数据的内部结构和实现细节,从而增加数据被恶意攻击的风险。
  • 2、数据版本控制:
    • 随着业务的发展和变化,数据结构和格式可能需要进行调整和升级。

约定方案二(传字符串):

  • 定义结构体来表示我们需要交互的信息;
  • 发送数据时将这个结构体按照一个规则转换成字符串, 接收到数据的时候再按照相同的规则把字符串转化回结构体;
  • 这个过程叫做 “序列化” 和 “反序列化

1.3 序列化和反序列化

在这里插入图片描述

无论我们采用方案一, 还是方案二, 还是其他的方案, 只要保证, 一端发送时构造的数据,在另一端能够正确的进行解析, 就是 ok 的. 这种约定, 就是 应用层协议

但是,为了让我们深刻理解协议,我们打算自定义实现一下协议的过程。

  • 我们采用方案 2,我们也要体现协议定制的细节
  • 我们要引入序列化和反序列化,只不过我们课堂直接采用现成的方案 – jsoncpp库
  • 我们要对 socket 进行字节流的读取处理

🏳️‍🌈二、什么是全双工

在这里插入图片描述

所以:

  • 在任何一台主机上,TCP 连接既有发送缓冲区,又有接受缓冲区,所以,在内核中,可以在发消息的同时,也可以收消息,即全双工
  • 这就是为什么一个 tcp sockfd 读写都是它的原因
  • 实际数据什么时候发,发多少,出错了怎么办,由 TCP 控制,所以 TCP 叫做传输控制协议

1、read,write,send,recv本质是拷贝函数
2、发送数据的本质:是从发送方的发送缓冲区把数据通过协议栈和网络拷贝给接收方大的接收缓冲区!
3、tcp支持全双工通信的原因(有发送和接收缓冲区)!
4、有两个缓冲区这种模式就是生产者消费者模型
5、为什么IO函数要阻塞?本质是在维护同步关系!

TCP协议是面向字节流的,客户端发的,不一定是全部是服务端收的,怎么保证读到的是一个完整的请求呢?

  • 分割完整的报文!

🏳️‍🌈三、Socket封装

Socket类以模板方法类的设计模式进行封装,将算法的不变部分封装在抽象基类中而将可变部分延迟到子类中实现!

3.1 整体结构

  • 抽象类(Abstract Class)

    1. 定义了多个抽象操作,这些操作在抽象类中不具体实现,由子类实现。
    2. 定义了两个模板方法,这个方法通常调用了上面提到的抽象操作。模板方法的算法骨架是固定的,但其中一些步骤的具体实现会延迟到子类中。
  • 具体子类(Concrete Class)

    1. 实现抽象类中的抽象操作,提供具体的算法步骤实现。
    2. 可以重写父类中的模板方法,但通常情况下不需要这么做,因为模板方法已经在抽象类中定义好了算法的骨架。

3.2 Socket 基类

将套接字创建、绑定、监听等 ​通用流程​ 抽象为模板方法,如 BuildListenSocket而将具体步骤(如 CreaterSocketOrDie)延迟到子类实现。

​角色划分​:

  • ​抽象类(Abstract Class)​​:Socket 定义了纯虚函数(步骤方法)和模板方法(流程框架)。​
  • 具体子类(Concrete Class)​​:由用户继承 Socket 并实现纯虚函数(例如 TcpSocket、UdpSocket)。
using SockPtr = std::shared_ptr<Socket>;

class Socket {

public:
    virtual void CreateSocketOrDie() = 0;                 // 创建套接字
    virtual void BindOrDie(uint16_t port) = 0;            // 绑定套接字
    virtual void ListenOrDie(int backlog = gbacklog) = 0; // 监听套接字
    virtual SockPtr Accepter(InetAddr* cli) = 0;          // 获取链接
    virtual bool Connector(const std::string& peerip,
                           uint16_t peerport) = 0; // 简历连接
    virtual int Sockfd() = 0;
    virtual void Close() = 0;
    virtual ssize_t Recv(std::string* out) = 0;      // 接收数据
    virtual ssize_t Send(const std::string& in) = 0; // 发送数据

public:
    // 创建监听套接字
    void BuildListenSocket(uint16_t port) {
        CreateSocketOrDie(); // 创建
        BindOrDie(port);     // 绑定
        ListenOrDie();       // 监听
    }

    // 创建客户端套接字
    void BuildConnectorSocket(const std::string& peerip, uint16_t peerport) {
        CreateSocketOrDie();         // 创建
        Connector(peerip, peerport); // 连接
    }
};

3.3 TcpSocket 子类

TcpSocket类 继承 Socket类,并具体实现父类的抽象操作!

3.3.1 基本结构

TcpSocket 子类就是具体实现父类的抽象操作,所以所有 TCP 可能会用到的父类方法都要具体实现

class TcpSocket : public Socket {
public:
    TcpSocket() {}
    TcpSocket(int sockfd) {}

    // 创建套接字
    void CreateSocketOrDie() {}
    // 绑定套接字
    void BindOrDie(uint16_t port) {}
    // 监听套接字
    void ListenOrDie(int backlog = gbacklog) {}
    // 获取链接
    SockPtr Accepter(InetAddr* cli) {}
    // 建立连接
    bool Connector(const std::string& peerip, uint16_t peerport) {}
    // 获取套接字描述符
    int Sockfd() {}
    // 关闭套接字
    void Close() {}

    ~TcpSocket() {}

private:
    int _sockfd;
};

3.3.2 构造、析构函数

  • 构造函数 可以实现两个,一个无参构造,一个有参构造(传参sockfd),用于初始化成员变量
  • 析构函数 可以不做处理,后面关闭套接字自己调用关闭函数即可!
TcpSocket() {}
TcpSocket(int sockfd) : _sockfd(sockfd) {}
~TcpSocket() {}

3.3.3 创建套接字

使用库函数 socket 按照格式创建套接字

// 创建套接字
void CreateSocketOrDie() override {
    _sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (_sockfd < 0) {
        LOG(LogLevel::ERROR) << "create socket error";
        exit(SOCKET_ERR);
    }
    LOG(LogLevel::DEBUG) << "sockfd create success : " << _sockfd;
}

3.3.4 绑定套接字

这部分用于服务端,所以需要先构建服务端的网络字节序地址,然后绑定套接字和网络字节序地址

// 绑定套接字
            void BindOrDie(uint16_t port) override{
                // sockaddr_in 的头文件是 #include <netinet/in.h>
                struct sockaddr_in local;
                memset(&local, 0, sizeof(local));
                local.sin_family = AF_INET;
                local.sin_port = htons(port);
                local.sin_addr.s_addr = htonl(INADDR_ANY);
                
                int n = ::bind(_sockfd, CONV(&local), sizeof(local));
                if(n < 0){
                    LOG(LogLevel::ERROR) << "bind socket error";
                    exit(BIND_ERR);
                }
                LOG(LogLevel::DEBUG) << "bind success";
            }

3.3.5 监听套接字

在绑定好网络字节序地址后,我们需要形成老板模式,设置最大连接数量,然后不断监听

// 监听套接字
void ListenOrDie(int backlog = gbacklog) override {
    int n = ::listen(_sockfd, backlog);
    if (n < 0) {
        LOG(LogLevel::ERROR) << "listen socket error";
        exit(LISTEN_ERR);
    }
    LOG(LogLevel::DEBUG) << "listen success";
}

3.3.6 获取连接

在为监听套接字设置好最大连接长度后,我们不断使用 accept 监听这个套接字,将获取到的客户端ip和端口号等信息 与 我们的服务端的连接 整合起来,形成一个整体套接字,实现面向对象连接

// 获取链接
SockPtr Accepter(InetAddr* cli) override {
    struct sockaddr_in client;
    socklen_t clientlen = sizeof(client);
    // accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)
    // 返回一个新的套接字,该套接字与调用进程间接地建立了连接。
    int sockfd = ::accept(_sockfd, CONV(&client), &clientlen);
    if (sockfd < 0) {
        LOG(LogLevel::ERROR) << "accept socket error";
        return nullptr;
    }
    *cli = InetAddr(client);
    LOG(LogLevel::DEBUG) << "get a new connection from "
                         << cli->AddrStr().c_str() << ", sockfd : " << sockfd;

    return std::make_shared<TcpSocket>(sockfd);
}

3.3.7 建立连接

当客户端想要连接上服务端的时候,我们需要先为服务端创建一个网络字节序地址,再与客户端的套接字连接起来

// 建立连接
bool Connector(const std::string& serverip, uint16_t serverport) override {
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;         // IPv4协议
    server.sin_port = htons(serverport); // 端口号
    // 这句话表示将字符串形式的IP地址转换为网络字节序的IP地址
    // inet_pton函数的作用是将点分十进制的IP地址转换为网络字节序的IP地址
    // 这里的AF_INET表示IPv4协议
    // 这里的serverip.c_str()表示IP地址的字符串形式
    // &server.sin_addr表示将IP地址存储到sin_addr成员变量中
    ::inet_pton(AF_INET, serverip.c_str(), &server.sin_addr); // IP地址

    int n = ::connect(_sockfd, CONV(&server), sizeof(server));
    if (n < 0) {
        LOG(LogLevel::ERROR) << "connect socket error";
        return false;
    }
    LOG(LogLevel::DEBUG) << "connect success";
    return true;
}

3.3.8 其他方法

还有一些其他的方法,难度不大,就不一一介绍了

// 获取套接字描述符
int Sockfd() override { return _sockfd; }

// 关闭套接字
void Close() override {
    if (_sockfd >= 0)
        ::close(_sockfd);
}

// 接收数据
ssize_t Recv(std::string* out) override {
    char inbuffer[4096];
    ssize_t n = ::recv(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0);
    if (n > 0) {
        inbuffer[n] = 0;
        *out = inbuffer;
    }
    return n;
}

// 发送数据
ssize_t Send(const std::string& in) override {
    return ::send(_sockfd, in.c_str(), in.size(), 0);
}

3.3.10 整体代码

#pragma once 

#include <iostream>
#include <cstring>
#include <Socket.h>
#include <memory>

#include <netinet/in.h>

#include "InetAddr.hpp"
#include "Log.hpp"
#include "Common.hpp"

using namespace LogModule;

const int gbacklog = 8;


// common.hpp
// #define Die(code)   \
//     do              \
//     {               \
//         exit(code); \
//     } while (0)

// #define CONV(v) (struct sockaddr *)(v)

// enum 
// {
//     USAGE_ERR = 1,
//     SOCKET_ERR,
//     BIND_ERR,
//     LISTEN_ERR,
//     CONNECTION_ERR
// };

namespace SocketModule{
    using SockPtr = std::shared_ptr<Socket>;

    class Socket{
    
        public:
            virtual void CreateSocketOrDie() = 0;                                       // 创建套接字
            virtual void BindOrDie(uint16_t port) = 0;                                  // 绑定套接字
            virtual void ListenOrDie(int backlog = gbacklog) = 0;                       // 监听套接字
            virtual SockPtr Accepter(InetAddr* cli) = 0;                                // 获取链接
            virtual bool Connector(const std::string& serverip, uint16_t serverport) = 0;   // 简历连接
            virtual int Sockfd() = 0;
            virtual void Close() = 0;
            virtual ssize_t Recv(std::string* out) = 0;                                  // 接收数据
            virtual ssize_t Send(const std::string& in) = 0;                             // 发送数据 
    
        public:
            // 创建监听套接字
            void BuildListenSocket(uint16_t port){
                CreateSocketOrDie();        // 创建
                BindOrDie(port);            // 绑定
                ListenOrDie();              // 监听
            }
    
            // 创建客户端套接字
            void BuildConnectorSocket(const std::string& serverip, uint16_t serverport){
                CreateSocketOrDie();        // 创建
                Connector(serverip, serverport); // 连接
            }
    };



    class TcpSocket : public Socket{
        public:
            TcpSocket(){}
            TcpSocket(int sockfd) : _sockfd(sockfd){ }

            // 创建套接字
            void CreateSocketOrDie() override{
                _sockfd = socket(AF_INET, SOCK_STREAM, 0);
                if(_sockfd < 0){
                    LOG(LogLevel::ERROR) << "create socket error";
                    exit(SOCKET_ERR);
                }
                LOG(LogLevel::DEBUG) << "sockfd create success : " << _sockfd;
            }
            // 绑定套接字
            void BindOrDie(uint16_t port) override{
                // sockaddr_in 的头文件是 #include <netinet/in.h>
                struct sockaddr_in local;
                memset(&local, 0, sizeof(local));
                local.sin_family = AF_INET;
                local.sin_port = htons(port);
                local.sin_addr.s_addr = htonl(INADDR_ANY);
                
                int n = ::bind(_sockfd, CONV(&local), sizeof(local));
                if(n < 0){
                    LOG(LogLevel::ERROR) << "bind socket error";
                    exit(BIND_ERR);
                }
                LOG(LogLevel::DEBUG) << "bind success";
            }                                  
            // 监听套接字
            void ListenOrDie(int backlog = gbacklog) override{
                int n = ::listen(_sockfd, backlog);
                if(n < 0){
                    LOG(LogLevel::ERROR) << "listen socket error";
                    exit(LISTEN_ERR);
                }
                LOG(LogLevel::DEBUG) << "listen success";
            }
            // 获取链接
            SockPtr Accepter(InetAddr* cli) override{
                struct sockaddr_in client;
                socklen_t clientlen = sizeof(client);
                // accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)
                // 返回一个新的套接字,该套接字与调用进程间接地建立了连接。
                int sockfd = ::accept(_sockfd, CONV(&client), &clientlen);
                if(sockfd < 0){
                    LOG(LogLevel::ERROR) << "accept socket error";
                    return nullptr;
                }
                *cli = InetAddr(client);
                LOG(LogLevel::DEBUG) << "get a new connection from " << cli->AddrStr().c_str() << ", sockfd : " << sockfd;

                return std::make_shared<TcpSocket>(sockfd);
            }
            // 建立连接
            bool Connector(const std::string& serverip, uint16_t serverport) override{
                struct sockaddr_in server;
                memset(&server, 0, sizeof(server));
                server.sin_family = AF_INET;    // IPv4协议
                server.sin_port = htons(serverport); // 端口号
                // 这句话表示将字符串形式的IP地址转换为网络字节序的IP地址
                // inet_pton函数的作用是将点分十进制的IP地址转换为网络字节序的IP地址
                // 这里的AF_INET表示IPv4协议
                // 这里的serverip.c_str()表示IP地址的字符串形式
                // &server.sin_addr表示将IP地址存储到sin_addr成员变量中
                ::inet_pton(AF_INET, serverip.c_str(), &server.sin_addr);   // IP地址

                int n = ::connect(_sockfd, CONV(&server), sizeof(server));
                if(n < 0){
                    LOG(LogLevel::ERROR) << "connect socket error" ;
                    return false;
                }
                LOG(LogLevel::DEBUG) << "connect success";
                return true;
            }
            
            // 获取套接字描述符
            int Sockfd() override{ return _sockfd; }
            // 关闭套接字
            void Close() override{ if(_sockfd >= 0) ::close(_sockfd); }
            // 接收数据
            ssize_t Recv(std::string* out) override{
                char inbuffer[4096];
                ssize_t n = ::recv(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0);
                if(n > 0){
                    inbuffer[n] = 0;
                    *out = inbuffer;
                }
                return n;
            }           
            // 发送数据                        
            ssize_t Send(const std::string& in) override{
                return ::send(_sockfd, in.c_str(), in.size(), 0);
            }

            ~TcpSocket(){}
        private:
            int _sockfd;
    };
}

👥总结

本篇博文对 【Linux网络】应用层自定义协议与序列化及Socket模拟封装 做了一个较为详细的介绍,不知道对你有没有帮助呢

觉得博主写得还不错的三连支持下吧!会继续努力的~

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

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

相关文章

客户案例:西范优选通过日事清实现流程与项目管理的优化

近几年来&#xff0c;新零售行业返璞归真&#xff0c;从线上销售重返线下发展&#xff0c;满足消费者更加多元化的需求&#xff0c;国内家居集合店如井喷式崛起。为在激烈的市场竞争中立于不败之地&#xff0c;西范优选专注于加强管理能力、优化协作效率的“内功修炼”&#xf…

LabVIEW实现Voronoi图绘制功能

该 LabVIEW 虚拟仪器&#xff08;VI&#xff09;借助 MathScript 节点&#xff0c;实现基于手机信号塔位置计算 Voronoi 图的功能。通过操作演示&#xff0c;能直观展示 Voronoi 图在空间划分上的应用。 各部分功能详细说明 随机地形创建部分 功能&#xff1a;根据 “Maximum a…

爬虫学习——获取动态网页信息

对于静态网页可以直接研究html网页代码实现内容获取&#xff0c;对于动态网页绝大多数都是页面内容是通过JavaScript脚本动态生成(也就是json数据格式)&#xff0c;而不是静态的&#xff0c;故需要使用一些新方法对其进行内容获取。凡是通过静态方法获取不到的内容&#xff0c;…

创新项目实训开发日志4

一、开发简介 核心工作内容&#xff1a;logo实现、注册实现、登录实现、上传gitee 工作时间&#xff1a;第十周 二、logo实现 1.设计logo 2.添加logo const logoUrl new URL(/assets/images/logo.png, import.meta.url).href <div class"aside-first">…

常见接口测试常见面试题(JMeter)

JMeter 是 Apache 提供的开源性能测试工具&#xff0c;主要用于对 Web 应用、REST API、数据库、FTP 等进行性能、负载和功能测试。​它支持多种协议&#xff0c;如 HTTP、HTTPS、JDBC、SOAP、FTP 等。 在一个线程组中&#xff0c;JMeter 的执行顺序通常为&#xff1a;配置元件…

计算机组成与体系结构:缓存(Cache)

目录 为什么需要 Cache&#xff1f; &#x1f9f1; Cache 的分层设计 &#x1f539; Level 1 Cache&#xff08;L1 Cache&#xff09;一级缓存 &#x1f539; Level 2 Cache&#xff08;L2 Cache&#xff09;二级缓存 &#x1f539; Level 3 Cache&#xff08;L3 Cache&am…

Flutter 在全新 Platform 和 UI 线程合并后,出现了什么大坑和变化?

Flutter 在全新 Platform 和 UI 线程合并后&#xff0c;出现了什么大坑和变化&#xff1f; 在两个月前&#xff0c;我们就聊过 3.29 上《Platform 和 UI 线程合并》的具体原因和实现方式&#xff0c;而事实上 Platform 和 UI 线程合并&#xff0c;确实为后续原生语言和 Dart 的…

stm32之GPIO函数详解和上机实验

目录 1.LED和蜂鸣器1.1 LED1.2 蜂鸣器 2.实验2.1 库函数&#xff1a;RCC和GPIO2.1.1 RCC函数1. RCC_AHBPeriphClockCmd2. RCC_APB2PeriphClockCmd3. RCC_APB1PeriphClockCmd 2.1.2 GPIO函数1. GPIO_DeInit2. GPIO_AFIODeInit3. GPIO_Init4. GPIO_StructInit5. GPIO_ReadInputDa…

用 PyQt5 和 asyncio 打造接口并发测试 GUI 工具

接口并发测试是测试工程师日常工作中的重要一环&#xff0c;而一个直观的 GUI 工具能有效提升工作效率和体验。本篇文章将带你用 PyQt5 和 asyncio 从零实现一个美观且功能实用的接口并发测试工具。 我们将实现以下功能&#xff1a; 请求方法选择器 添加了一个下拉框 QComboBo…

Qt实战之将自定义插件(minGW)显示到Qt Creator列表的方法

Qt以其强大的跨平台特性和丰富的功能&#xff0c;成为众多开发者构建图形用户界面&#xff08;GUI&#xff09;应用程序的首选框架。而在Qt开发的过程中&#xff0c;自定义插件能够极大地拓展应用程序的功能边界&#xff0c;让开发者实现各种独特的、个性化的交互效果。想象一下…

【Vue】TypeScript与Vue3集成

个人主页&#xff1a;Guiat 归属专栏&#xff1a;Vue 文章目录 1. 前言2. 环境准备与基础搭建2.1. 安装 Node.js 与 npm/yarn/pnpm2.2. 创建 Vue3 TypeScript 项目2.2.1. 使用 Vue CLI2.2.2. 使用 Vite&#xff08;推荐&#xff09;2.2.3. 目录结构简述 3. Vue3 TS 基础语法整…

Linux之七大难命令(The Seven Difficult Commands of Linux)

Linux之七大难命令 、背景 作为Linux的初学者&#xff0c;肯定要先掌握高频使用的指令&#xff0c;这样才能让Linux的学习在短时间内事半功倍。但是&#xff0c;有些指令虽然功能强大&#xff0c;但因参数多而让初学者们很害怕&#xff0c;今天介绍Linux中高频使用&#xff0…

5.3.1 MvvmLight以及CommunityToolkit.Mvvm介绍

MvvmLight、CommunityToolkit.Mvvm是开源包,他们为实现 MVVM(Model-View-ViewModel)模式提供了一系列实用的特性和工具,能帮助开发者更高效地构建 WPF、UWP、MAUI 等应用程序。 本文介绍如下: 一、使用(旧)的MvvmLight库 其特点如下,要继承的基类是ViewModelBase;且使用…

Dbeaver 执行 SQL 语句和执行 SQL 脚本的区别

执行 SQL 语句 执行 SQL 语句对应图标&#xff1a; 适用于执行单个 SQL 的情形&#xff0c;默认是在光标处或选中的文本上执行 SQL 查询。 实际上同时选择多个 SQL 并通过该方式去执行也可能成功&#xff0c;只是有失败的风险。因此不建议使用它来同时执行多个 SQL 语句。 情况…

《Python3网络爬虫开发实战(第二版)》配套案例 spa6

Scrape | Moviehttps://spa6.scrape.center/ 请求影片列表api时&#xff0c;不仅有分页参数&#xff0c;还多了一个token&#xff0c;通过重发请求发现token有时间限制&#xff0c;所以得逆向token的生成代码。 通过xhr断点定位到接口请求位置 刷新页面或者点翻页按钮&#x…

Python基础语法:字面量,注释,关键字,标识符,变量和引用,程序执行的3大流程

目录 字面量&#xff08;数据的类型&#xff09; 字面量的含义 常见字面量类型&#xff08;6种&#xff09; 输出各类字面量&#xff08;print语句&#xff09; 注释&#xff08;单行和多行注释&#xff09; 注释的作用 单行注释和多行注释 单行注释&#xff08;ctrl/&a…

SPL 量化 获取数据

下载数据 我们将股票数据分享在百度网盘上供下载&#xff0c;每工作日更新。 目前可供下载的数据有 A 股的日 K 线数据、股票代码列表和上市公司的基本面数据 下载链接&#xff1a; 百度网盘 下载数据的文件格式为 btx&#xff0c;是 SPL 的特有二进制格式。 btx 称为集文…

Rust 学习笔记:安装 Rust

Rust 学习笔记&#xff1a;安装 Rust Rust 学习笔记&#xff1a;安装 Rust在 Windows 上安装 Rust命令行创建 Rust 项目在 Mac/Linux 上安装 Rust一些命令升级卸载cargo -hrustc -h 安装 RustRoverrust-analyzer Rust 学习笔记&#xff1a;安装 Rust 在 Windows 上安装 Rust …

编译 C++ 报错“找不到 g++ 编译器”的终极解决方案(含 Windows/Linux/macOS)

前言 在使用终端编译 C 程序时&#xff0c;报错&#xff1a; 或类似提示&#xff0c;意味着你的系统尚未正确安装或配置 g 编译器。本篇将从零手把手教你在 Windows / Linux / macOS 下安装并配置 g&#xff0c;适用于新手或 C 入门阶段的你。 什么是 g&#xff1f; g 是 GN…

html单页业务介绍源码

源码介绍 html单页业务介绍源码&#xff0c;源码由HTMLCSSJS组成&#xff0c;记事本打开源码文件可以进行内容文字之类的修改&#xff0c;双击html文件可以本地运行 效果预览 源码免费获取 html单页业务介绍源码