TCP远程命令执行

目录

一.   命令集

二.   命令执行模块实现

三.   服务端模块实现

四.   服务端调用模块实现

五.   客户端模块实现

六.   效果展示


此篇教大家如何利用TCP进行远程命令执行。

一.   命令集

将值得信任的命令放进一个txt文件中,执行命令时,就去这个文件里面找,有就执行命令,没有就不执行。

ls -a -l
pwd
tree
whoami
who
uname -a
cat
touch

注意我们是用空格隔开的。

二.   命令执行模块实现

依然封装成类,将上述命令集写进类中。

class Command
{
public:
    Command(const string& cond_path)
    :_cond_path(cond_path)
    {}

    ~Command()
    {}
private:
    set<string> _safe_cmd;
    string _cond_path;
};

有着两个成员,首先是set类型,即命令集。其次是上述文件集的路径。

 初始化路径之后,我们再来将命令写进集合中。

const string sep=" ";

string PrefixCommand(const string& cmd)
{
    if(cmd.empty())
    {
        return string();
    }
    auto pos=cmd.find(sep);
    if(pos==string::npos)
    {
        return cmd;
    }
    else
    {
        return cmd.substr(0,pos);
    }
}

void LoadConf(const string& conf)
{
    ifstream in(conf);
    if(!in.is_open())
    {
        LOG(FATAL,"open %s error\n",conf);
        return;
    }
    string line;
    while(getline(in,line))
    {
        LOG(DEBUG,"load command [%s] success\n",line.c_str());
        _safe_cmd.insert(PrefixCommand(line));
    }

    in.close();
}

LoadConf函数即初始化集合函数,利用文件读取流打开文件,一行一行读取命令集,并且只取第一个空格前面的字符插入进集合中。下面会讲为什么。

这个操作可以在构造函数的时候实现,所以可以将这个函数加入到构造函数中。

Command(const string& cond_path)
:_cond_path(cond_path)
{
    LoadConf(_cond_path);
}

 此处肯定需要检测输入的命令是否在命令集中。我们只检查输入命令第一个空格前的字符是否相同即可。例如"ls -a -l"只需检测ls即可。为什么呢?

例如touch指令,后面肯定要加创建的文件名字,这样就需要连着文件名一起检查在不在命令集中,这显然是不合理的,文件名有无数个,怎么能列举完呢。所以只需检测第一个空格前面相不相同即可。

来看如何检查:

const string sep=" ";

string PrefixCommand(const string& cmd)
{
    if(cmd.empty())
    {
        return string();
    }
    auto pos=cmd.find(sep);
    if(pos==string::npos)
    {
        return cmd;
    }
    else
    {
        return cmd.substr(0,pos);
    }
}

bool SafeCheck(const string& cmd)
{
    string prefix=PrefixCommand(cmd);
    auto iter=_safe_cmd.find(prefix);
    if(iter==_safe_cmd.end())
    {
        return false;
    }
    return true;
}

string Excute(const string& cmd)
{
    string result;
    if(SafeCheck(cmd))
    {
        FILE* fp=popen(cmd.c_str(),"r");
        if(fp==nullptr)
        {
            return "failed";
        }

        char buffer[1024];
        while(fgets(buffer,sizeof(buffer),fp)!=NULL)
        {
            result+=buffer;
        }
        pclose(fp);
    }
    else
    {
        result="坏人\n";
    }

    return result;
}

Excute函数就是具体如何处理输入的正确指令,重点就是运用popen函数

FILE *popen(const char *command, const char *type);

popen() 函数通过创建一个管道,调用 fork 产生一个子进程,执行一个 shell 以运行命令来开启一个进程。 

参数说明:

command: 是一个指向以 NULL 结束的 shell 命令字符串的指针。命令将被传到 bin/sh 并使用 -c 标志,shell 将执行这个命令,比如sh -c ls

type: 只能是读或者写中的一种,得到的返回值(标准 I/O 流)也具有和 type 相应的只读或只写类型。如果 type 是 “r” 则文件指针连接到 command 的标准输出;如果 type 是 “w” 则文件指针连接到 command 的标准输入。

返回值:

如果调用 fork() 或 pipe() 失败,或者不能分配内存将返回NULL,否则返回一个读或者打开文件的指针。

 最后合起来就是命令指令模块:

const string sep=" ";

class Command
{
private:
    void LoadConf(const string& conf)
    {
        ifstream in(conf);
        if(!in.is_open())
        {
            LOG(FATAL,"open %s error\n",conf);
            return;
        }
        string line;
        while(getline(in,line))
        {
            LOG(DEBUG,"load command [%s] success\n",line.c_str());
            _safe_cmd.insert(PrefixCommand(line));
        }

        in.close();
    }
public:
    Command(const string& cond_path)
    :_cond_path(cond_path)
    {
        LoadConf(_cond_path);
    }

    string PrefixCommand(const string& cmd)
    {
        if(cmd.empty())
        {
            return string();
        }
        auto pos=cmd.find(sep);
        if(pos==string::npos)
        {
            return cmd;
        }
        else
        {
            return cmd.substr(0,pos);
        }
    }

    bool SafeCheck(const string& cmd)
    {
        string prefix=PrefixCommand(cmd);
        auto iter=_safe_cmd.find(prefix);
        if(iter==_safe_cmd.end())
        {
            return false;
        }
        return true;
    }

    string Excute(const string& cmd)
    {
        string result;
        if(SafeCheck(cmd))
        {
            FILE* fp=popen(cmd.c_str(),"r");
            if(fp==nullptr)
            {
                return "failed";
            }

            char buffer[1024];
            while(fgets(buffer,sizeof(buffer),fp)!=NULL)
            {
                result+=buffer;
            }
            pclose(fp);
        }
        else
        {
            result="坏人\n";
        }

        return result;
    }

    ~Command()
    {}
private:
    set<string> _safe_cmd;
    string _cond_path;
};

其中LOG函数是封装的日志功能:

#pragma once

//日志
#include<iostream>
#include<fstream>
#include<cstdio>
#include<string>
#include<ctime>
#include<unistd.h>
#include<sys/types.h>
#include<stdarg.h>
#include<pthread.h>
#include"LockGuard.hpp"

using namespace std;

bool gIsSave=false;
const string logname="log.txt";


void SaveFile(const string& filename,const string& message)
{
    ofstream out(filename,ios::app);
    if(!out.is_open())
    {
        return;
    }
    out<<message;
    out.close();
}

//1.日志是有等级的
enum Level
{
    DEBUG=0,
    INFO,
    WARNING,
    ERROR,
    FATAL
};


string LevelToString(int level)
{
    switch(level)
    {
        case DEBUG: return "Debug";break;
        case INFO: return "Info";break;
        case WARNING: return "Warning";break;
        case ERROR: return "Error";break;
        case FATAL: return "Fatal";break;
        default: return "Unknown";break;
    }
}

string GetTimeString()
{
    time_t curr_time=time(nullptr);
    struct tm* format_time=localtime(&curr_time);
    if(format_time==nullptr) return "None";
    char time_buffer[64];
    snprintf(time_buffer,sizeof(time_buffer),"%d-%d-%d %d:%d:%d",
    format_time->tm_year+1900,format_time->tm_mon+1,format_time->tm_mday,
    format_time->tm_hour,format_time->tm_min,format_time->tm_sec);

    return time_buffer;
}

pthread_mutex_t lock=PTHREAD_MUTEX_INITIALIZER;

//2.日志是由格式的
// 日志等级 时间 代码所在的文件名/行数 日志的内容...
void LogMessage(string filename,int line,bool issave,int level,const char* format,...)
{
    string levelstr=LevelToString(level);
    string timestr=GetTimeString();
    pid_t selfid=getpid();

    //可变参数部分处理
    char buffer[1024];
    va_list arg;
    va_start(arg,format);
    vsnprintf(buffer,sizeof(buffer),format,arg);
    va_end(arg);

    LockGuard lockguard(&lock);

    string message;
    message="["+timestr+"]"+"["+levelstr+"]"+"[pid: "
            +to_string(selfid)+"]"+"["+filename+"]"
            +"["+to_string(line)+"]"+buffer+"\n";
    if(!issave)
    {
        cout<<message;
    }
    else
    {
        SaveFile(logname,message);
    }
}

void Test(int num,...)
{
    va_list arg;
    va_start(arg,num);

    while(true)
    {
        int data=va_arg(arg,int);
        cout<<"data: "<<data<<endl;
        num--;
    }

    va_end(arg);//arg==NULL
}

//C99新特性 __VA_ARGS__
#define LOG(level,format,...) do {LogMessage(__FILE__,__LINE__,gIsSave,level,format,##__VA_ARGS__);} while(0)
#define EnableFile() do {gIsSave=true;} while(0)
#define EnableScreen() do {gIsSave=false;} while(0)

三.   服务端模块实现

具体的实现跟前面TCP服务端模块实现一样(点此查看)。此处我们采用多线程来实现,就不过多赘述。

#include<iostream>
#include<string>
#include<unistd.h>
#include<cstring>
#include<pthread.h>
#include<functional>
#include<sys/wait.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>

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

const static int defaultsockfd=-1;
const static int gbacklog=10;

class TcpServer;

struct ThreadData
{
public:
    ThreadData(int fd,InetAddr addr,TcpServer* s)
    :sockfd(fd)
    ,clientaddr(addr)
    ,self(s)
    {}
public:
    int sockfd;
    InetAddr clientaddr;
    TcpServer* self;
};

enum
{
    SOCKET_ERROR = 1,
    BIND_ERROR,
    LISTEN_ERROR,
    USAGE_ERROR
};

using func_t=function<string(const string&)>;

class TcpServer
{
public:
    TcpServer(int port,func_t func)
    :_port(port)
    ,_listensock(defaultsockfd)
    ,_isrunning(false)
    ,_func(func)
    {}

    void InitServer()
    {
        //1.创建流式套接字
        _listensock=::socket(AF_INET,SOCK_STREAM,0);
        if(_listensock<0)
        {
            LOG(FATAL,"socket error\n");
            exit(SOCKET_ERROR);
        }
        LOG(DEBUG,"socket create success, sockfd is: %d\n",_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;
        int n=::bind(_listensock,(struct sockaddr*)&local,sizeof(local));
        if(n<0)
        {
            LOG(FATAL,"bind error\n");
            exit(BIND_ERROR);
        }
        LOG(DEBUG,"bind success,sockfd is: %d\n",_listensock);

        //3.tcp是面向连接的,所以通信之前,必须先建立连接,服务器是被连接的
        //tcpserver启动,未来首先要一直等待客户的连接到来
        n=::listen(_listensock,gbacklog);
        if(n<0)
        {
            LOG(FATAL,"listen error\n");
            exit(LISTEN_ERROR);
        }
        LOG(DEBUG,"listen success,sockfd is: %d\n",_listensock);

    }

    void Service(int sockfd,InetAddr client)
    {
        LOG(DEBUG,"get a new link,info %s:%d,fd: %d\n",client.Ip().c_str(),client.Port(),sockfd);
        string clientaddr="["+client.Ip()+":"+to_string(client.Port())+"]# ";
        while(true)
        {
            char inbuffer[1024];
            ssize_t n=recv(sockfd,inbuffer,sizeof(inbuffer)-1,0);
            if(n>0)
            {
                inbuffer[n]=0;
                cout<<clientaddr<<inbuffer<<endl;

                string result=_func(inbuffer);

                send(sockfd,result.c_str(),result.size(),0);
            }
            else if(n==0)
            {
                //client退出&&关闭连接了
                LOG(INFO,"%s quit\n",clientaddr.c_str());
                break;
            }
            else
            {
                LOG(ERROR,"read error\n",clientaddr.c_str());
                break;
            }
        }

        ::close(sockfd);//不关闭会发生文件描述符泄露
    }

    static void* HandlerSock(void* args)//IO和业务解耦
    {
        pthread_detach(pthread_self());
        ThreadData* td=static_cast<ThreadData*>(args);
        td->self->Service(td->sockfd,td->clientaddr);
        delete td;
        return nullptr;
    }

    void Loop()
    {
        _isrunning=true;
        //4.不能直接接收数据,应该先获取连接
        while(true)
        {
            struct sockaddr_in peer;
            socklen_t len=sizeof(peer);
            int sockfd=::accept(_listensock,(struct sockaddr*)&peer,&len);
            if(sockfd<0)
            {
                LOG(WARNING,"accept error\n");
                continue;
            }

            //采用多线程
            //此处不能像多进程一样关闭文件描述符,因为多线程文件描述符表是共享的
            pthread_t t;
            ThreadData* td=new ThreadData(sockfd,InetAddr(peer),this);
            pthread_create(&t,nullptr,HandlerSock,td);//将线程分离
        }
        _isrunning=false;
    }

    ~TcpServer()
    {
        if(_listensock>defaultsockfd)
        {
            ::close(_listensock);
        }
    }
private:
    uint16_t _port;
    int _listensock;
    bool _isrunning;

    func_t _func;
};

其中InetAddr.hpp文件是我们封装的。

#include<iostream>
#include<string>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>

using namespace std;

class InetAddr
{
private:
    void GetAddress(string* ip,uint16_t* port)
    {
        *port=ntohs(_addr.sin_port);//网络字节序转为主机字节序
        *ip=inet_ntoa(_addr.sin_addr);//将网络字节序IP转为点分式十进制IP
    }
public:
    InetAddr(const struct sockaddr_in &addr)
    :_addr(addr)
    {
        GetAddress(&_ip,&_port);
    }

    string Ip()
    {
        return _ip;
    }

    uint16_t Port()
    {
        return _port;
    }

    ~InetAddr()
    {}
private:
    struct sockaddr_in _addr;
    string _ip;
    uint16_t _port;
};

四.   服务端调用模块实现

只需创建出服务端类的对象,依次调用InitServer和Loop函数即可。并创建出执行命令类对象。

#include"CommandExcute.hpp"
#include"TcpServer.hpp"
#include<memory>

using namespace std;

void Usage(string proc)
{
    cout<<"Usage:\n\t"<<proc<<" local_port\n"<<endl;
}

// ./tcpserver port
int main(int argc,char *argv[])
{
    if(argc!=2)
    {
        Usage(argv[0]);
        return 1;
    }
    EnableScreen();
    uint16_t port=stoi(argv[1]);
    Command cmd("./safe.txt");

    func_t cmdExec=bind(&Command::Excute,&cmd,placeholders::_1);
    unique_ptr<TcpServer> tsvr=make_unique<TcpServer>(port,cmdExec);
    tsvr->InitServer();
    tsvr->Loop();


    return 0;
}

五.   客户端模块实现

客户端还是没有变化。不懂得可看此处(点此查看)。

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

#include"Log.hpp"

using namespace std;

void Usage(string proc)
{
    cout<<"Usage:\n\t"<<proc<<" serverip serverport\n"<<endl;
}


// ./tcpclient serverip serverport
int main(int argc,char *argv[])
{
    if(argc!=3)
    {
        Usage(argv[0]);
        exit(1);
    }
    string serverip=argv[1];
    uint16_t serverport=stoi(argv[2]);

    int sockfd=::socket(AF_INET,SOCK_STREAM,0);
    if(sockfd<0)
    {
        cerr<<"socket error\n"<<endl;
        exit(2);
    }

    //与udpclient一样,不需显式bind

    //构建目标主机的socket信息
    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());

    int n=connect(sockfd,(struct sockaddr*)&server,sizeof(server));
    if(n<0)
    {
        cerr<<"connect error\n"<<endl;
        exit(3);
    }

    while(true)
    {
        cout<<"Please Enter#";
        string outstring;
        getline(cin,outstring);

        ssize_t s=send(sockfd,outstring.c_str(),outstring.size(),0);//write
        if(s>0)
        {
            char inbuffer[1024];
            ssize_t m=recv(sockfd,inbuffer,sizeof(inbuffer)-1,0);
            if(m>0)
            {
                inbuffer[m]=0;
                cout<<inbuffer<<endl;
            }
            else
            {
                break;
            }
        }
        else
        {
            break;
        }
    }
}

六.   效果展示

可以看见只要在命令集中的命令都能执行。


总结:

好了,到这里今天的知识就讲完了,大家有错误一点要在评论指出,我怕我一人搁这瞎bb,没人告诉我错误就寄了。

祝大家越来越好,不用关注我(疯狂暗示)

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

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

相关文章

elementUI根据列表id进行列合并@莫成尘

本文章提供了elementUI根据列表id进行列合并的demo&#xff0c;效果如图&#xff08;可直接复制代码粘贴&#xff09; <template><div id"app"><el-table border :data"tableList" style"width: 100%" :span-method"objectS…

【STM32】SPI通信-软件与硬件读写SPI

SPI通信-软件与硬件读写SPI 软件SPI一、SPI通信协议1、SPI通信2、硬件电路3、移位示意图4、SPI时序基本单元(1)开始通信和结束通信(2)模式0---用的最多(3)模式1(4)模式2(5)模式3 5、SPI时序(1)写使能(2)指定地址写(3)指定地址读 二、W25Q64模块介绍&#xff11;、W25Q64简介&am…

小怡分享之数据结构LinkedList与链表

前言&#xff1a; &#x1f308;✨前面小怡给大家介绍了ArrayList&#xff0c;今天小怡给大家介绍一下链表。 1.ArrayList的缺陷 当在ArrayList任意位置插入或者删除元素时&#xff0c;就需要后续元素整体往前或者往后搬移&#xff0c;时间复杂度为O&#xff08;n&#xff09;…

网恋照妖镜源码搭建教程

文章目录 前言原理创建网站1.打开网站设置 配置ssl2.要打开强制HTTPS&#xff0c;用宝塔免费的ssl证书即可&#xff0c;也可以使用其他证书&#xff0c;必须是与域名匹配的3.上传文件至根目录进行解压4.解压后&#xff0c;修改文件 sc.php 里面的内容5.其余探索 结语 前言 前俩…

Excel和Word日常使用记录:

Excel使用总结 表格颜色填充&#xff1a; 合并单元格&#xff1a; 选中你要合并的单元格区域。 按下快捷键 Alt H&#xff0c;然后松开这些键。 再按下 M&#xff0c;接着按 C。 这个组合键执行的操作是&#xff1a;Alt H&#xff1a;打开“主页”选项卡。 M&#xff1a;选…

“阡陌云旅”黄河九省文化旅游平台

“阡陌云旅”黄河九省文化旅游平台 GitHub地址&#xff1a;https://github.com/guoJiaQi-123/Yellow-River-Cloud-Journey 项目背景 “阡陌云旅”黄河九省文化旅游平台 “阡陌云旅” 黄河九省文化旅游平台是一个专注于黄河流域九省文化旅游资源整合与推广的项目。 黄河是中…

[oeasy]python0004_游乐场_和python一起玩耍_python解释器_数学运算

和python玩耍 &#x1f94a; Python 回忆 上次 了解shell环境中的命令 <colgroup><col span"1"><col span"1"></colgroup> | 命令 | 作用 | | whoami | 显示当前用户名 | | pwd | 显示当前文件夹 | | ls | 列出当前文件夹下的内容…

51单片机的无线病床呼叫系统【proteus仿真+程序+报告+原理图+演示视频】

1、主要功能 该系统由AT89C51/STC89C52单片机LCD1602显示模块温湿度传感器模块矩阵按键时钟模块等模块构成。适用于病床呼叫系统、16床位呼叫等相似项目。 可实现基本功能: 1、LCD1602实时显示北京时间、温湿度信息、呼叫床位等信息&#xff1b; 2、DHT11采集病房温湿度信息&…

WSL 下的 CentOS 装 Docker

WSL 下的 CentOS 装 Docker 卸载旧版本安装前的准备工作1. 安装 yum-utils2. 添加阿里云的 yum 镜像仓库3. 快速生成 Yum 缓存 安装Docker启动docker运行 hello-world设置镜像加速器&#xff08;阿里云的&#xff09;卸载 Docker 引擎参考资料 卸载旧版本 sudo yum remove doc…

鸿蒙(API 12 Beta6版)超帧功能开发【ABR功能开发】

业务流程 基于相机运动感知策略的ABR主要业务流程如下&#xff1a; 用户进入ABR适用的游戏场景。游戏应用调用[HMS_ABR_CreateContext]接口并指定图形API类型&#xff0c;创建ABR上下文实例。游戏应用调用[HMS_ABR_SetTargetFps]接口初始化ABR实例&#xff0c;配置目标帧率属性…

excel透视图、看板案例(超详细)

一、简介 Excel透视图&#xff08;Pivot Table&#xff09; 功能&#xff1a;透视图是一种强大的数据分析工具&#xff0c;用于汇总、分析和展示数据。它允许用户对数据进行重新排列和分类&#xff0c;从而更容易发现数据中的模式和趋势。用途&#xff1a;可以用来生成动态报表…

【Python机器学习】词向量推理——词向量

目录 面向向量的推理 使用词向量的更多原因 如何计算Word2vec表示 skip-gram方法 什么是softmax 神经网络如何学习向量表示 用线性代数检索词向量 连续词袋方法 skip-gram和CBOW&#xff1a;什么时候用哪种方法 word2vec计算技巧 高频2-gram 高频词条降采样 负采样…

fastadmin 文件上传七牛云

1-安装七牛云官方SDK composer require qiniu/php-sdk 2-七牛云配置 <?phpnamespace app\common\controller;use Qiniu\Storage\BucketManager; use think\Config; use Qiniu\Auth; use Qiniu\Storage\UploadManager; use think\Controller; use think\Db;/*** 七牛基类*…

echarts 实现签到记录日历组件

以下笔记来源&#xff1a;编程导航 分析 有三种基本图表可以选择&#xff1a; 基础日历图&#xff1a;https://echarts.apache.org/examples/zh/editor.html?ccalendar-simple日历热力图&#xff1a;https://echarts.apache.org/examples/zh/editor.html?ccalendar-heatmap…

使用paddlerocr识别固定颜色验证码

1 引言 本文使用opencv和paddlerocr识别出固定颜色的验证码&#xff0c;原理不解释&#xff0c;安装包的方法自行查找&#xff0c;只提供代码和思路。 1 使用opencv对特定颜色区域进行提取2 使用paddlerocr识别并输出验证码 2 代码 2.1 读取图片&#xff0c;提取蓝色区域 …

C语言深入理解指针4

1.回调函数 回调函数是通过函数指针调用的函数 将函数指针作为参数传递给另一个函数&#xff0c;当这个函数指针被用来调用其所指向的函数时&#xff0c;被调用的函数就是回调函数&#xff0c;回调函数不是应该由该函数的实现方直接调用&#xff0c;而是在特定的事件或条件发生…

ASIO网络调试助手之一:简介

多年前&#xff0c;写过几篇《Boost.Asio C网络编程》的学习文章&#xff0c;一直没机会实践。最近项目中用到了Asio&#xff0c;于是抽空写了个网络调试助手。 开发环境&#xff1a; Win10 Qt5.12.6 Asio(standalone) spdlog 支持协议&#xff1a; UDP TCP Client TCP Ser…

JavaWeb【day11】--(SpringBootWeb案例)

SpringBootWeb案例 前面我们已经实现了员工信息的条件分页查询以及删除操作。 关于员工管理的功能&#xff0c;还有两个需要实现&#xff1a; 新增员工 修改员工 首先我们先完成"新增员工"的功能开发&#xff0c;再完成"修改员工"的功能开发。而在&quo…

【C++】STL学习——priority_queue(了解仿函数)

目录 priority_queue介绍迭代器种类priority_queue实现仿函数仿函数的使用 priority_queue介绍 优先队列是一种容器适配器&#xff0c;根据严格的弱排序标准&#xff0c;它的第一个元素总是它所包含的元素中最大的。此上下文类似于堆&#xff0c;在堆中可以随时插入元素&#x…

SQL 编程基础

SQL&#xff08;结构化查询语言&#xff09;广泛应用于数据库操作&#xff0c;是每个程序员都需要掌握的技能之一。这篇文章将带你从基础入门&#xff0c;了解SQL编程中的常量、变量及流程控制语句。我们将采用简单易懂的语言&#xff0c;结合实际示例&#xff0c;帮助你轻松理…