Linux命令行解释器的模拟实现

欢迎拜访羑悻的小杀马特.-CSDN博客

本篇主题:Linux命令行解释器

制作日期:2024.12.04

隶属专栏:linux之旅

本篇简介: 

主线带你用ubuntu版系统步步分析实现基础版本的shell;比如支持重定向操作,内建命令:cd,echo,alias等;

其中本篇应用主要相关知识点:环境变量,文件基础IO,进程控制,程序替换等;

本篇编写语言:C/C++混编。

目录

​编辑

本篇简介: 

一·整体框架:

二·初始化myshell的环境变量表和命令行参数表:

三·命令行提示行的打印:

四·获取命令参数:

五·重定向判断:

六·语义分析:

七· 内建命令判断:

7.1 cd:

7.1.1 cd :

7.1.2 cd -:

7.1.3 cd / :

7.1.4 cd ~:

7.1.5 cd +dirname:

7.2 echo:

7.3 export:

7.4 alias:

八·子进程执行操作:

九·myshell代码汇总:


一·整体框架:

首先我们把这个myshell大致进行框架展示出:

 我们首先创建数组cl保存要输入的字符串;而只要读取失败就要一直读取故我们在获取,命令行输入的时候利用了while循环;其次就是如果是内建命令;我们就要直接父进程执行完;无需execute再让子进程执行了。

先说一下想法:这里可执行程序,把它当成真正shell的bash;大部分命令都是通过调用子进程来程序替换完成;有些命令是内建的,故需要自己完成;而首先这个程序会继承原本bash的那张环境变量表;这里我们模拟实现一下真正的bash的那两张表:也就是说我们用数组,通过拷贝原bash的表,改变environ指针来维护我们的数组(也就是我们自己的可执行程序要调用的那张环境变量表) :这里补充一点:对于环境变量如果我们env命令:它是通过environ指针来进行查找打印的;局部打印就不一定了。 后面我们具体实现的时候会有所体现,之后我们道来。

然后下面就是一步步对这些拆开的函数进行实现了。

二·初始化myshell的环境变量表和命令行参数表:

这里我们自己开了两个数组来模拟这两张表;也就是拷贝父bash的那两种表拷贝过来(简单模拟一下)这俩张表的内容就可以作为我们后面程序替换执行命令要传递的参数等。

void initenv(){memset(env, 0, sizeof(env));envs= 0;for(int i=0;environ[i];i++){env[i]=(char*)malloc(strlen(environ[i])+1);//这里模拟的也可以不开空间,直接栈上strcpy(env[i], environ[i]);envs++;}env[envs] = NULL;// for(int i = 0; env[i]; i++)// {//     putenv(env[i]);// }environ = env;//用自己生成的env表
}

这里我们的命令行参数表暂时不需要填充,但是需要把环境变量表由bash那里拷贝过来;并改变了environ指针指向,也就是说等我们执行env操作的时候它就会打印我们的这个env数组了;比如后序我们用putenv等命令的话,它就会通过environ指针对我们的这个数组进行一些增加/覆盖环境变量的操作了。 

三·命令行提示行的打印:

我们让它格式输出这样的格式:

#define FT "my simulate shell:%s@%s %s# "//snprintf的format最大值

首先我们对比一下真正的命令解释器:

我们此刻需要替换掉%s的就是通过环境变量找到USER,HOSTHOME ,PWD了:

const char* getuser(){const char*u=getenv("USER");return u==NULL?"NONE":u;
}const char* gethostname(){const char*h=getenv("HOSTNAME");return h==NULL?"NONE":h;
}const char* getpwd(){const char*p=getenv("PWD");return p==NULL?"NONE":p;
}

但是这样我们会发现因为我们维护的这张环境变量表,未添加其他修改功能,这里需要我们手动修改;这样pwd就不会变了(当换目录的时候):因此我们手动维护一下:

下面是我们要添加的全局变量(因为导入environ维护的二维数组应该是地址;故给它整成全局): 

 

//这里获得环境变量和其他上面不同;因为当我们通过chdir改变当前目录的时候它在环境变量中的记录(真正的bash实现了)而我们没有实现,因此我们
//可以通过getcwd每次调完新的目录开始就使用它不仅能改变了env的pwd也就是新的位置;还能打印命令行提示符的时候变化
const  char *getpwd(){char *pwd = getcwd(cwd, sizeof(cwd));if(pwd != NULL){snprintf(cwdenv, sizeof(cwdenv), "PWD=%s", cwd);putenv(cwdenv);//put进去的是环境变量的地址也就是一个指针指向的是那段量,因此指针要么是全局要么在堆上}return  pwd==NULL?"NONE":pwd;
}

普及一下用到的getcwd:

参数:放入的数组;最大字节数;成功返回这段pwd失败就是NULL。

这里我们再做一个小优化;也就是把路径变短一下就乘当下目录:

std::string convert(const char *pwd){std::string s=pwd;auto pos=s.rfind('/');if(pos==std::string::npos)return "error";if(pos==s.size()-1)return "/";else return s.substr(pos+1);}

因此我们从末尾给它分割了一下:最后调用它返回的string对象的c_str接口就好;

这里顺便说一下;因为后面很多都用到这一点:就是经常操作的时候把char串变成string然后调用它的c_str()为了方便;以及后面很多要注意作用域:因此考虑了全局设计。 

再下面就是命令行打印了:

void ptcmdprompt(){char p[CS]={0};snprintf(p,sizeof(p),FT, getuser(),gethostname(),convert(getpwd()).c_str() );printf("%s",p);//无\n及时刷新标准输出缓冲区fflush(stdout);
}

下面展示下效果: 

四·获取命令参数:

这里逻辑比较简单就是把我们输入的字符读入然后把后面的\n去掉:

bool gtcmdline(char *p,int size){char *j=fgets(p,size,stdin);// printf("参数是:%d",size);if(j==NULL)return false;//printf("%s\n",p);p[strlen(p)-1]=0;//干掉\nif(strlen(p)==0) return false;else return true;
}

这里用到了fgets:

也就是:从流中获得字符最多是size个到串s中;读取成功返回这个s;否则返回NULL。

五·重定向判断:

这里我们封装的是redirect函数来完成;简单说就是让它检查我们输入的cl中是否有> < >>等重定向标识符;然后根据左右分别是命令,文件等给它分离开了;并给对应的文件重定向(dup2一下):

预处理:首先利用标识来枚举一下重定向状态:输出,输入,还是追加:

下面就说一下细节处理:

这里值得关注的是:我们从数组末尾开始找

标识符的;这样然后利用覆盖0的操作来完成前方命令的截断:判断顺序:<  >  >>;其次就是它可能文件前面存在空格;故我们再构建一个去除空格函数。

​
void eliminatesp(char cl[],int &fg){while(isspace(cl[fg])) fg++;}
void redirect(char cl[]){status=NOPUT_RE;int start=0;int end=strlen(cl)-1;while(start<end){if(cl[end]=='<'){cl[end++] = 0;eliminatesp(cl, end);status = INPUT_RE;filename = cl+end;break;}else if(cl[end]=='>'){if(cl[end-1]=='>'){status=APPPUT_RE;cl[end-1]=0;}else{status=OUTPUT_RE;cl[end]=0;}end++;eliminatesp(cl,end);filename = cl+end;break;}else{end--;}}}​

下面我们把获得了重定向左边的命令和右边的文件下面就是利用dup2完成重定向操作了:

这里由于不是内建命令;故我们还是放在子进程来执行:

 if(status==INPUT_RE){int fd=open(filename.c_str(),O_RDONLY);if(fd < 0) exit(1);dup2(fd,0);close(fd);}else if(status==OUTPUT_RE){int fd=open(filename.c_str(),O_CREAT | O_WRONLY | O_TRUNC, 0666);if(fd < 0) exit(1);dup2(fd,1);close(fd);}else if(status==APPPUT_RE){int fd=open(filename.c_str(),O_CREAT | O_WRONLY | O_APPEND, 0666);if(fd < 0) exit(1);dup2(fd,1);close(fd);}else { }//NOPUT_RE无重定向操作

 故我们只需当分开始fork后对我们标志的进行判断即可;但是当子进程完成重定向执行完退出后我们又要对status这个状态给它重置一下。

六·语义分析:

简单来说就是利用我们的strtok函数完成对空格的分割;然后把它填入到我们自己创建的argv数组中;注:这里最后也要补上NULL;注意好边界处理:

bool  cmdparse(char *c){argc=0;std::string cc=c;//  浅拷贝://   if(_alias[cc]!="")  c = &((_alias[cc])[0]);//   借助string的深拷贝赋值完成对hash内数据的深拷贝:if(_alias[cc]!=""){hash_cp =_alias[cc];c=&(hash_cp[0]);}  argv[argc++]=strtok(c,sp);//此处c这个指针将会随之变化最后分割结束为nullwhile(argv[argc++]=strtok(NULL,sp)){}argc--;return true;
}

这里我们先暂时忽略对alias重命名的内建命令的设计(后面会谈到) 。

七· 内建命令判断:

下面我们把整体框架展示一下:

当我们在main函数主体内分析是不是内建命令;如果是内建命令那么就直接由main这个进程执行完然后直接开始下一层循环,就不往下走了;否则就走我们的execute函数。

//内建命令:和execute执行是分开的
bool checkinkeycmd()
{std::string cmd = argv[0];if(cmd == "cd"){// printf("cd进入!\n");Cd();return true;}else if(cmd == "echo"){Echo();return true;}else if(cmd == "export"){//}else if(cmd == "alias"&&argc>=2){//}return false;
}

下面我们分四个部分来对相关内建命令进行单独处理: 

7.1 cd:

这里cd其实就是change directory;它完成的操作其实就是帮我们改变目录;但是我们另外让它把我们对应的环境变量表也给改变;其实就要操作我们所维护的那个env数组了。

下面我们就不对应把cd 的相关都实现一遍;大概实现常用的这几个:

注意:这里我们为了可以实现cd -:也就是会定义好变量保存上一次访问的目录;方便回去;故当每次chdir都会保存一下;并改变env表中的pwd

这里我们用的是string;也就是利用了它可以被const类型的字符串初始化;也可以通过c_str完成对应转换。 

7.1.1 cd :

这里单纯的cd也就是只有命令无参数此时argc=1;故直接跳到家目录:

const char *gethome()
{const char *home = getenv("HOME");return home == NULL ? "" : home;
}

保存原先目录位置然后改变再覆盖env对应pwd即可:

if(argc == 1)//直接返回到家目录,但是此时没有更改env的pwd,故我们后面调用getpwd()完成更改env标记  借助string完成了否则对返回const多次覆盖保存较为麻烦{std::string home = gethome();if(home.empty()) return false;std::string tmp=getpwd();lastpwd=tmp;chdir(home.c_str());getpwd();return true;}

下面我们保存第二个参数:

std::string where = argv[1];

效果展示:

7.1.2 cd -:

 if(where=="-") //上一个工作目录{   // std::cout<<lastpwd<<std::endl;chdir(lastpwd.c_str());//这里的lastpwd是我们在新切换目录更改env前记录的;故是先前的pwdgetpwd();}

效果展示:

7.1.3 cd / :

 else if(where=="/"){std::string tmp=getpwd();lastpwd=tmp;chdir("/");getpwd();}

效果展示: 

7.1.4 cd ~:

这里分为普通用户还是root:普通用户是家目录而root就是登机目录了:

if(!strcmp(getuser(),"root")){ std::string tmp=getpwd();lastpwd=tmp;chdir("/root");getpwd();}else {std::string home = gethome();std::string tmp=getpwd();lastpwd=tmp;chdir(home.c_str());getpwd();}

效果展示: 

7.1.5 cd +dirname:

    else{     std::string tmp=getpwd();lastpwd=tmp;// std::cout<<lastpwd<<std::endl;chdir(where.c_str());getpwd();}

演示效果:

7.2 echo:

这里我们分为 echo $?;echo $+环境变量:

全局变量lastcode保存上次的子进程退出码;方便下一次打印:

对echo $?我们规定只要走了子进程就会返回1;比如内建命令等就返回0。 

void Echo(){
//echo $? echo $PATHif(argc==2){std::string func=argv[1];if(func=="$?"){std::cout << lastcode << std::endl;lastcode = 0;}else if(func[0]=='$'){std::string envname = func.substr(1);const char * envvalue= getenv(envname.c_str());if(envvalue)std::cout << envvalue << std::endl;}else{std::cout << func << std::endl;}}
}

演示效果:

 

7.3 export:

这里我们只需在我们维护的env数组多开一个空间然后把我们要导入的串记录一下完成深拷贝(注意最后一个置空):

   void Export(){env_str=argv[1];env[envs]=(char*)calloc(strlen(argv[1])+1,1);for(int i=0;i<env_str.size();i++)env[envs][i]=env_str[i];envs++;env[envs]=NULL;
}

效果展示: 

当我们退出后重新进入:

发现没了;符合我们的预期。 

7.4 alias:

这里用到了映射,故我们采用了哈希表;

全局变量:

cur,pre是分别是别名和原名 ;

hash_cp是命令行分析过程的对hash表内取得值的一个深拷贝;反之strtok函数破坏了;导致再次使用这个别名就会出现找原名时候被破坏的结果。

封装Alias函数:

void Alias(){std::string sec=argv[1];auto pos=sec.find('=');cur=sec.substr(0,pos);pre=sec.substr(pos+1,std::string::npos);for(int i=2;i<argc;i++){pre+=" ";pre+=argv[i];}_alias[cur]=pre;}

对语义分析部分修改:

  argc=0;std::string cc=c;//  浅拷贝://   if(_alias[cc]!="")  c = &((_alias[cc])[0]);//   借助string的深拷贝赋值完成对hash内数据的深拷贝:if(_alias[cc]!=""){hash_cp =_alias[cc];c=&(hash_cp[0]);}  argv[argc++]=strtok(c,sp);//此处c这个指针将会随之变化最后分割结束为nullwhile(argv[argc++]=strtok(NULL,sp)){}argc--;return true;

效果展示:

八·子进程执行操作:

main函数的进程fork后让子进程得到相关指令和参数并用exec系列函数进行程序替换(这里选用的execvp):

然后父进程阻塞等待回收资源和相关信息:

int  execute(){pid_t pid=fork();if(pid==0){//printf("argv[0]: %s\n", argv[0]);if(status==INPUT_RE){int fd=open(filename.c_str(),O_RDONLY);if(fd < 0) exit(1);dup2(fd,0);close(fd);}else if(status==OUTPUT_RE){int fd=open(filename.c_str(),O_CREAT | O_WRONLY | O_TRUNC, 0666);if(fd < 0) exit(1);dup2(fd,1);close(fd);}else if(status==APPPUT_RE){int fd=open(filename.c_str(),O_CREAT | O_WRONLY | O_APPEND, 0666);if(fd < 0) exit(1);dup2(fd,1);close(fd);}else { }//NOPUT_RE无重定向操作execvp(argv[0],argv);exit(1);}int status=0;pid_t id=waitpid(pid,&status,0);if(id > 0){lastcode = WEXITSTATUS(status);//对于这里规定当execute的子进程执行完就返回1;内建命令或者其他都是返回0}return 0;}

九·myshell代码汇总:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include<unordered_map>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>extern char**environ;
#define CS 1024//命令行提示符最大值
#define FT "my simulate shell:%s@%s %s# "//snprintf的format最大值
#define sp " "//space
#define MC 128//命令行参数最大值//这里模拟了bash的两张表,而不是main直接继承下(改变environ指针)而是重新布置了一下数组,让environ指针指向我们所布置的数组。
// 1. 命令行参数表
#define MAXARGC 128
char *argv[MAXARGC];
int argc = 0;// 2. 环境变量表
#define MAX_ENVS 100
char *env[MAX_ENVS];
int envs = 0;char cwd[1024];
char cwdenv[1029];//char *lastpwd=(char*)calloc(1024,1);
std::string lastpwd;
int lastcode=0;//char export_env[1024];
std::string env_str;
//对alias的适用:
std::unordered_map<std::string,std::string>_alias;
std::string cur,pre;
std::string hash_cp;//重定向:
std::string filename;
#define NOPUT_RE 0
#define INPUT_RE 1
#define OUTPUT_RE 2
#define APPPUT_RE 3
int status;//重定向方式void initenv(){memset(env, 0, sizeof(env));envs= 0;for(int i=0;environ[i];i++){env[i]=(char*)malloc(strlen(environ[i])+1);//这里模拟的也可以不开空间,直接栈上strcpy(env[i], environ[i]);envs++;}env[envs] = NULL;// for(int i = 0; env[i]; i++)// {//     putenv(env[i]);// }environ = env;//用自己生成的env表
}//获取一些环境变量:
const char* getuser(){const char*u=getenv("USER");return u==NULL?"NONE":u;
}const char* gethostname(){const char*h=getenv("HOSTNAME");return h==NULL?"NONE":h;
}//const char* getpwd(){
//    const char*p=getenv("PWD");
//    return p==NULL?"NONE":p;
//}
const char *gethome()
{const char *home = getenv("HOME");return home == NULL ? "" : home;
}
//这里获得环境变量和其他上面不同;因为当我们通过chdir改变当前目录的时候它在环境变量中的记录(真正的bash实现了)而我们没有实现,因此我们
//可以通过getcwd每次调完新的目录开始就使用它不仅能改变了env的pwd也就是新的位置;还能打印命令行提示符的时候变化
const  char *getpwd(){char *pwd = getcwd(cwd, sizeof(cwd));if(pwd != NULL){snprintf(cwdenv, sizeof(cwdenv), "PWD=%s", cwd);putenv(cwdenv);//put进去的是环境变量的地址也就是一个指针指向的是那段量,因此指针要么是全局要么在堆上}return  pwd==NULL?"NONE":pwd;
}
//const char *getpwd(){
//    // 调用了这个,在Getpwd中
//    return Getpwd();
//}
//打印命令行提示符:把pwd的最后一个名称得到
std::string convert(const char *pwd){std::string s=pwd;auto pos=s.rfind('/');if(pos==std::string::npos)return "error";if(pos==s.size()-1)return "/";else return s.substr(pos+1);}
void ptcmdprompt(){char p[CS]={0};snprintf(p,sizeof(p),FT, getuser(),gethostname(),convert(getpwd()).c_str() );printf("%s",p);//无\n及时刷新标准输出缓冲区fflush(stdout);
}
//获得命令行参数:
bool gtcmdline(char *p,int size){char *j=fgets(p,size,stdin);// printf("参数是:%d",size);if(j==NULL)return false;//printf("%s\n",p);p[strlen(p)-1]=0;//干掉\nif(strlen(p)==0) return false;else return true;
}
//命令行解释:把输入的命令行参数分出来方便后序传给要调用的main的argv
bool  cmdparse(char *c){// printf("%s\n",c);argc=0;std::string cc=c;//  std::cout<<"内容:"<<_alias[cc]<<std::endl;//  浅拷贝://   if(_alias[cc]!="")  c = &((_alias[cc])[0]);//   借助string的深拷贝赋值完成对hash内数据的深拷贝:if(_alias[cc]!=""){hash_cp =_alias[cc];c=&(hash_cp[0]);}  //printf("%s\n",c);argv[argc++]=strtok(c,sp);//此处c这个指针将会随之变化最后分割结束为nullwhile(argv[argc++]=strtok(NULL,sp)){}argc--;//printf("%s\n%s\n",argv[0],argv[1]);// printf("%s%s\n",argv[0],argv[1]);return true;
}//void lastpwd(){
//   // printf("11111111111111111");
// //  printf(" %s%d\n ",argv[0],argc);
//   if(!strcmp(argv[0],"cd")&&argc==2){
//        // printf("执行\n");
//        std::string s("LASTPWD");
//        s+="=";
//        s+=argv[1];
//        //std::cout<<s<<std::endl;
//        // char p[s.size()+1]={0};
//        char* p = (char*)calloc(s.size() + 1, 1);
//        for(int i=0;i<s.size();i++) p[i]=s[i];
//        printf("huanbian:%s\n",p);
//        environ[envs]=(char*)calloc(strlen(p)+1,1);
//        putenv(p);
//   }
//}
bool Cd(){
if(argc == 1)//直接返回到家目录,但是此时没有更改env的pwd,故我们后面调用getpwd()完成更改env标记  借助string完成了否则对返回const多次覆盖保存较为麻烦{std::string home = gethome();if(home.empty()) return false;std::string tmp=getpwd();lastpwd=tmp;chdir(home.c_str());getpwd();return true;}
else{std::string where = argv[1];// printf("%s\n",argv[1]);// cd - / cd ~if(where=="-") //上一个工作目录{   // std::cout<<lastpwd<<std::endl;chdir(lastpwd.c_str());//这里的lastpwd是我们在新切换目录更改env前记录的;故是先前的pwdgetpwd();}else if(where=="/"){std::string tmp=getpwd();lastpwd=tmp;chdir("/");getpwd();}else if(where=="~")//家目录{if(!strcmp(getuser(),"root")){ std::string tmp=getpwd();lastpwd=tmp;chdir("~");getpwd();}else {std::string home = gethome();std::string tmp=getpwd();lastpwd=tmp;chdir(home.c_str());getpwd();}}//  else if(where==".."){} 上级目录else{     std::string tmp=getpwd();lastpwd=tmp;// std::cout<<lastpwd<<std::endl;chdir(where.c_str());getpwd();}return true;}}
void Echo(){
//echo $? echo $PATHif(argc==2){std::string func=argv[1];if(func=="$?"){std::cout << lastcode << std::endl;lastcode = 0;}else if(func[0]=='$'){std::string envname = func.substr(1);const char * envvalue= getenv(envname.c_str());if(envvalue)std::cout << envvalue << std::endl;}else{std::cout << func << std::endl;}}
}
void Export(){env_str=argv[1];env[envs]=(char*)calloc(strlen(argv[1])+1,1);for(int i=0;i<env_str.size();i++)env[envs][i]=env_str[i];envs++;env[envs]=NULL;}
void Alias(){std::string sec=argv[1];auto pos=sec.find('=');cur=sec.substr(0,pos);pre=sec.substr(pos+1,std::string::npos);for(int i=2;i<argc;i++){pre+=" ";pre+=argv[i];}_alias[cur]=pre;}
//分子进程执行:
int  execute(){pid_t pid=fork();if(pid==0){//printf("argv[0]: %s\n", argv[0]);if(status==INPUT_RE){int fd=open(filename.c_str(),O_RDONLY);if(fd < 0) exit(1);dup2(fd,0);close(fd);}else if(status==OUTPUT_RE){int fd=open(filename.c_str(),O_CREAT | O_WRONLY | O_TRUNC, 0666);if(fd < 0) exit(1);dup2(fd,1);close(fd);}else if(status==APPPUT_RE){int fd=open(filename.c_str(),O_CREAT | O_WRONLY | O_APPEND, 0666);if(fd < 0) exit(1);dup2(fd,1);close(fd);}else { }//NOPUT_RE无重定向操作execvp(argv[0],argv);exit(1);}int status=0;pid_t id=waitpid(pid,&status,0);if(id > 0){lastcode = WEXITSTATUS(status);//对于这里规定当execute的子进程执行完就返回1;内建命令或者其他都是返回0}return 0;}
//内建命令:和execute执行是分开的
bool checkinkeycmd()
{std::string cmd = argv[0];if(cmd == "cd"){// printf("cd进入!\n");Cd();return true;}else if(cmd == "echo"){Echo();return true;}else if(cmd == "export"){Export();}else if(cmd == "alias"&&argc>=2){Alias();}return false;
}void eliminatesp(char cl[],int &fg){while(isspace(cl[fg])) fg++;}
void redirect(char cl[]){status=NOPUT_RE;int start=0;int end=strlen(cl)-1;while(start<end){if(cl[end]=='<'){cl[end++] = 0;eliminatesp(cl, end);status = INPUT_RE;filename = cl+end;break;}else if(cl[end]=='>'){if(cl[end-1]=='>'){status=APPPUT_RE;cl[end-1]=0;}else{status=OUTPUT_RE;cl[end]=0;}end++;eliminatesp(cl,end);filename = cl+end;break;}else{end--;}}}
void destroy(){for(int i=0;env[i];i++){free(env[i]);}
}int main() {//自己的环境变量和命令行参数表的初始化:initenv();while(1) {//命令提示行打印:ptcmdprompt();char cl[CS]={0};//把命令参数输入到clwhile(!gtcmdline(cl,sizeof(cl))){}redirect(cl);//把命令参数这个串拆解到argv里:cmdparse(cl);//判断是否是内建命令由bash自己完成(这里模拟的是main自己执行)if(checkinkeycmd()) {// lastpwd();continue;} execute();}//销毁表所开辟的空间destroy();
}

目前功能比较基本,会不断补充;感谢支持!! !

本文临近尾声;相信读到此处的你,肯定对它有了不一样的理解,或多或少,都是一种收获;如遇到问题的地方也欢迎大家积极留言,真诚感谢!

 

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

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

相关文章

微信 创建小程序码-有数量限制

获取小程序码&#xff1a;小程序码为圆图&#xff0c;有数量限制。 目录 文档 接口地址 功能描述 注意事项 请求参数 对接 获取小程序码 调用获取 小程序码示例 总结 文档 接口地址 https://api.weixin.qq.com/wxa/getwxacode?access_tokenaccess_token 功能描述 …

【spring mvc】全局处理请求体和响应体

目录 说明实现效果逻辑图 实现步骤创建公共处理的请求和响应的类api接口测试前端请求响应结果 扩展Response响应格式实体ResponseCode 响应状态码RSA工具类 RequestBodyAdvice 介绍使用场景 ResponseBodyAdvice 介绍使用场景 说明 由于项目中需要进行加密传输数据提高项目安全…

消息中间件-Kafka2-3.9.0源码构建

消息中间件-Kafka2-3.9.0源码构建 1、软件环境 JDK Version 1.8Scala Version 2.12.0Kafka-3.9.0 源码包 下载地址&#xff1a;https://downloads.apache.org/kafka/3.9.0/kafka-3.9.0-src.tgzGradle Version > 8.8Apache Zookeeper 3.7.0 2、源码编译 打开源码根目录修改…

AMEYA360 | 杭晶电子:晶振在AR/VR中的应用

晶振在AR/VR设备中扮演重要角色&#xff0c;为其核心电子系统提供稳定的时钟信号&#xff0c;确保设备的高性能运行。 以下是晶振在AR/VR应用中的具体作用&#xff1a; 01、图像处理与同步 1、晶振为图形处理单元(GPU)和显示芯片提供精准的时钟信号&#xff0c;支持高速图像渲染…

【SARL】单智能体强化学习(Single-Agent Reinforcement Learning)《纲要》

&#x1f4e2;本篇文章是博主强化学习&#xff08;RL&#xff09;领域学习时&#xff0c;用于个人学习、研究或者欣赏使用&#xff0c;并基于博主对相关等领域的一些理解而记录的学习摘录和笔记&#xff0c;若有不当和侵权之处&#xff0c;指出后将会立即改正&#xff0c;还望谅…

Java刷题常见的集合类,各种函数的使用以及常见的类型转化等等

前言 相信大家在刷算法题的过程中&#xff0c;好不容易想出来大概的思路&#xff0c;也知道去用哪个集合类&#xff0c;但各个集合类的一些命令都长得太像&#xff0c;很容易将他们弄错&#xff0c;并且在各集合之间的转化也是特别烦人&#xff0c;还有很多实用的函数都知道可…

Linux CentOS

​阿里云开源镜像下载链接 https://mirrors.aliyun.com/centos/7/isos/x86_64/ VMware 安装 CentOS7 自定义 下一步 选择稍后安装操作系统 选择 输入 查看物理机CPU内核数量 CtrlShiftEsc 总数不超过物理机内核数量 推荐内存 自选 推荐 推荐 默认 拆分成多个 默认 自定义硬件…

大数据新视界 -- Hive 数据湖集成与数据治理(下)(26 / 30)

&#x1f496;&#x1f496;&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎你们来到 青云交的博客&#xff01;能与你们在此邂逅&#xff0c;我满心欢喜&#xff0c;深感无比荣幸。在这个瞬息万变的时代&#xff0c;我们每个人都在苦苦追寻一处能让心灵安然栖息的港湾。而 我的…

数据结构 (26)图的遍历

前言 数据结构中的图遍历是指从图中的任一顶点出发&#xff0c;按照某种方法访问图中的所有顶点&#xff0c;且每个顶点只访问一次。 一、遍历方法 遍历主要有两种方法&#xff1a;深度优先搜索&#xff08;DFS&#xff09;和广度优先搜索&#xff08;BFS&#xff09;。 1.深度…

B站狂神说Mybatis+Spring+SpringMVC整合理解(ssm框架整合)

文章目录 0.写在前面(对mybatis,spring的理解)&#xff08;不看可跳过&#xff09;0.1 为什么需要mybatis0.2 为什么需要spring0.3为什么需要springmvc 1.新建ssmbuild数据库2.新建Maven项目3.初始化步骤3.1 配置下载maven依赖&#xff0c;构建资源导出3.2 连接数据库3.3建包&a…

el-cascader 使用笔记

1.效果 2.官网 https://element.eleme.cn/#/zh-CN/component/cascader 3.动态加载&#xff08;官网&#xff09; <el-cascader :props"props"></el-cascader><script>let id 0;export default {data() {return {props: {lazy: true,lazyLoad (…

MySQL事件

1、在数据库d_eams中创建一个名为E_enent的事件&#xff0c;用于每隔10s向数据表T_event 中插入一条数据 use d_eams; create event E_event on schedule every 10 second on completion preserve do insert into T_event(用户, 创建时间) values(Root, now()); 2、查看当前…

C语言(分支结构)

问题引出 我们在程序设计往往会遇到如下的问题&#xff0c;比如下的函数的计算。 也就是我们是必须要通过一个条件的结果来选择下一步的操作&#xff0c;算法上属于一个分支结构&#xff0c;C语言中实现分支结构主要使用if语句。 条件判断 根据某个条件成立与否&#xff0c…

SpringAop(1)

Spring中有两大重心&#xff1a;springIoc和springAop springioc将bean的控制权交给了spring管理&#xff0c;直接从spring中获取&#xff0c;通过五大注解来告诉spring帮我管理这个对象。通过Autowired和构造方法和set方法注入。 不仅仅是学习重点也是面试常考的难点。 spr…

GoReplay开源工具使用教程

目录 一、GoReplay环境搭建 1、Mac、Linux安装GoReplay环境 二、GoReplay录制与重播 1、搭建练习接口 2、录制命令 3、重播命令 三、GoReplay单个命令 1、常用命令 2、其他命令 3、命令示例 4、性能测试 5、正则表达式 四、gorepaly组合命令 1、组合命令实例 2、…

Nginx:ssl

目录 部署ssl前提 nginx部署ssl证书 部署ssl部署建议 部署ssl前提 网站有域名根据域名申请到ssl证书&#xff0c;并下载证书部署到nginx中 部署了ssl证书后&#xff0c;访问的流量是加密的。 nginx部署ssl证书 #80端口跳转到443 server {listen 80;return 302 https://1…

USB 声卡全解析:提升音频体验的得力助手

在当今数字化的时代&#xff0c;音频领域的追求愈发多元。无论是热衷聆听高品质音乐的爱好者&#xff0c;还是在专业音频工作中精雕细琢的人士&#xff0c;亦或是在游戏世界里渴望极致音效沉浸的玩家&#xff0c;都始终在寻觅能让音频体验更上一层楼的妙法。而 USB 声卡&#x…

[TPAMI 2024]Vision-Language Models for Vision Tasks: A Survey

论文网址&#xff1a;Vision-Language Models for Vision Tasks: A Survey | IEEE Journals & Magazine | IEEE Xplore 论文Github页面&#xff1a;GitHub - jingyi0000/VLM_survey: Collection of AWESOME vision-language models for vision tasks 英文是纯手打的&…

深度学习模型:门控循环单元(GRU)详解

本文深入探讨了门控循环单元&#xff08;GRU&#xff09;&#xff0c;它是一种简化版的长短期记忆网络&#xff08;LSTM&#xff09;&#xff0c;在处理序列数据方面表现出色。文章详细介绍了 GRU 的基本原理、与 LSTM 的对比、在不同领域的应用以及相关的代码实现&#xff0c;…

HCIA笔记6--路由基础与静态路由:浮动路由、缺省路由、迭代查找

文章目录 0. 概念1.路由器工作原理2. 跨网访问流程3. 静态路由配置4. 静态路由的应用场景4.1 路由备份4.2 浮动路由4.3 缺省路由 5. 迭代路由6 问题6.1 为什么路由表中有的下一跳的地址有接口&#xff1f;6.2 个人电脑的网关本质是什么&#xff1f; 0. 概念 自治系统&#xff…