C语言开发Linux下web服务器(支持GET/POST,SSL,目录显示等)

2023-05-16

这个主要是在CSAPP基础上做的,添加了POST,SSL,目录显示等功能。

一、 实现功能:
1. 支持GET/POST方法
2. 支持SSL安全连接即HTTPS
3. 支持CGI
4. 基于IP地址和掩码的认证
5. 目录显示
6. 日志功能

7. 错误提示页面


github地址:https://github.com/Skycrab/Linux-C-Web-Server


源代码下载地址:点击打开链接

二、设计原理

首先介绍一些HTTP协议基本知识。
#1.GET/POST
本实现支持GET/POST方法,都是HTTP协议需要支持的标准方法。
GET方法主要是通过URL发送请求和传送数据,而POST方法在请求头空一格之后传送数据,所以POST方法比GET方法安全性高,因为GET方法可以直接看到传送的数据。另外一个区别就是GET方法传输的数据较小,而POST方法很大。所以一般表单,登陆页面等都是通过POST方法。

#2.MIME类型
   当服务器获取客户端的请求的文件名,将分析文件的MIME类型,然后告诉浏览器改文件的MIME类型,浏览器通过MIME类型解析传送过来的数据。具体来说,浏览器请求一个主页面,该页面是一个HTML文件,那么服务器将”text/html”类型发给浏览器,浏览器通过HTML解析器识别发送过来的内容并显示。

下面将描述一个具体情景。
   客户端使用浏览器通过URL发送请求,服务器获取请求。
如浏览器URL为:127.0.0.1/postAuth.html,
那么服务器获取到的请求为:GET  /postAuth.html  HTTP/1.1
意思是需要根目录下postAuth.html文件的内容,通过GET方法,使用HTTP/1.1协议(1.1是HTTP的版本号)。这是服务器将分析文件名,得知postAuth.html是一个HTML文件,所以将”text/html”发送给浏览器,然后读取postAuth.html内容发给浏览器。

实现简单的MIME类型识别代码如下:
主要就是通过文件后缀获取文件类型。

static void get_filetype(const char *filename, char *filetype) 
{
    if (strstr(filename, ".html"))
		strcpy(filetype, "text/html");
    else if (strstr(filename, ".gif"))
		strcpy(filetype, "image/gif");
    else if (strstr(filename, ".jpg"))
		strcpy(filetype, "image/jpeg");
    else if (strstr(filename, ".png"))
		strcpy(filetype, "image/png");
    else
	strcpy(filetype, "text/plain");
}  

如果支持HTTPS的话,那么我们就#define HTTPS,这主要通过gcc 的D选项实现的,具体细节可参考man手册。

静态内容显示实现如下:

static void serve_static(int fd, char *filename, int filesize) 
{
    int srcfd;
    char *srcp, filetype[MAXLINE], buf[MAXBUF];
 
    /* Send response headers to client */
    get_filetype(filename, filetype);
    sprintf(buf, "HTTP/1.0 200 OK\r\n");
    sprintf(buf, "%sServer: Tiny Web Server\r\n", buf);
    sprintf(buf, "%sContent-length: %d\r\n", buf, filesize);
    sprintf(buf, "%sContent-type: %s\r\n\r\n", buf, filetype);

    /* Send response body to client */
    srcfd = Open(filename, O_RDONLY, 0);
    srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0);
    Close(srcfd);

    #ifdef HTTPS 
    if(ishttps)
    {
    	SSL_write(ssl, buf, strlen(buf));
	SSL_write(ssl, srcp, filesize);
    }
    else
    #endif
    {
	Rio_writen(fd, buf, strlen(buf));
	Rio_writen(fd, srcp, filesize);
    }
    Munmap(srcp, filesize);
}

#3.CGI规范
   如果只能显示页面那么无疑缺少动态交互能力,于是CGI产生了。CGI是公共网关接口(Common Gateway Interface),是在CGI程序和Web服务器之间传递信息的规则。CGI允许Web服务器执行外部程序,并将它们的输出发送给浏览器。这样就提供了动态交互能力。 

那么服务器是如何分开处理静态页面和动态CGI程序的呢?这主要是通过解析URL的方式。我们可以定义CGI程序的目录,如cgi-bin,那么如果URL包含”cgi-bin”字符串则这是动态程序,且将URL的参数给cgiargs。如果是静态页面,parse_uri返回1,反正返回0。所以我们可以通过返回值区别不同的服务类型。
具体解析URL方式如下:

static int parse_uri(char *uri, char *filename, char *cgiargs) 
{
    char *ptr;
    char tmpcwd[MAXLINE];
    strcpy(tmpcwd,cwd);
    strcat(tmpcwd,"/");

    if (!strstr(uri, "cgi-bin")) 
    {  /* Static content */
	strcpy(cgiargs, "");
	strcpy(filename, strcat(tmpcwd,Getconfig("root")));
	strcat(filename, uri);
	if (uri[strlen(uri)-1] == '/')
	    strcat(filename, "home.html");
	return 1;
    }
    else 
    {  /* Dynamic content */
	ptr = index(uri, '?');
	if (ptr) 
	{
	    strcpy(cgiargs, ptr+1);
	    *ptr = '\0';
	}
	else 
	    strcpy(cgiargs, "");
	strcpy(filename, cwd);
	strcat(filename, uri);
	return 0;
    }
}

GET方式的CGI规范实现原理:
   服务器通过URL获取传给CGI程序的参数,设置环境变量QUERY_STRING,并将标准输出重定向到文件描述符,然后通过EXEC函数簇执行外部CGI程序。外部CGI程序获取QUERY_STRING并处理,处理完后输出结果。由于此时标准输出已重定向到文件描述符,即发送给了浏览器。
实现细节如下:由于涉及到HTTPS,所以稍微有点复杂。

void get_dynamic(int fd, char *filename, char *cgiargs) 
{
    char buf[MAXLINE], *emptylist[] = { NULL },httpsbuf[MAXLINE];
    int p[2];

    /* Return first part of HTTP response */
    sprintf(buf, "HTTP/1.0 200 OK\r\n");
    sprintf(buf, "%sServer: Web Server\r\n",buf);
    #ifdef HTTPS 
    if(ishttps)
    	SSL_write(ssl,buf,strlen(buf));
    else
    #endif
       	Rio_writen(fd, buf, strlen(buf));
	
    #ifdef HTTPS 
    if(ishttps)
    {
    	Pipe(p);
       	if (Fork() == 0)
	{  /* child  */ 
		Close(p[0]);
		setenv("QUERY_STRING", cgiargs, 1); 
		Dup2(p[1], STDOUT_FILENO);         /* Redirect stdout to p[1] */
		Execve(filename, emptylist, environ); /* Run CGI program */	
	}
	Close(p[1]);
	Read(p[0],httpsbuf,MAXLINE);   /* parent read from p[0] */
	SSL_write(ssl,httpsbuf,strlen(httpsbuf));
    }
    else
    #endif
    {
	if (Fork() == 0) 
	{ /* child */
		/* Real server would set all CGI vars here */
		setenv("QUERY_STRING", cgiargs, 1); 
		Dup2(fd, STDOUT_FILENO);         /* Redirect stdout to client */
		Execve(filename, emptylist, environ); /* Run CGI program */
	}
}
}

POST方式的CGI规范实现原理:
   由于POST方式不是通过URL传递参数,所以实现方式与GET方式不一样。
POST方式获取浏览器发送过来的参数长度设置为环境变量CONTENT-LENGTH。并将参数重定向到CGI的标准输入,这主要通过pipe管道实现的。CGI程序从标准输入读取CONTENT-LENGTH个字符就获取了浏览器传送的参数,并将处理结果输出到标准输出,同理标准输出已重定向到文件描述符,所以浏览器就能收到处理的响应。
具体实现细节如下:

static void post_dynamic(int fd, char *filename, int contentLength,rio_t *rp)
{
    char buf[MAXLINE],length[32], *emptylist[] = { NULL },data[MAXLINE];
    int p[2];


    #ifdef HTTPS 
    int httpsp[2];
    #endif


    sprintf(length,"%d",contentLength);
    memset(data,0,MAXLINE);


    Pipe(p);


    /*       The post data is sended by client,we need to redirct the data to cgi stdin.
    *  	 so, child read contentLength bytes data from fp,and write to p[1];
    *    parent should redirct p[0] to stdin. As a result, the cgi script can
    *    read the post data from the stdin. 
    */


    /* https already read all data ,include post data  by SSL_read() */
   
    	if (Fork() == 0)
	{                     /* child  */ 
		Close(p[0]);
		#ifdef HTTPS 
		if(ishttps)
		{
			Write(p[1],httpspostdata,contentLength);	
		}
		else
		#endif
		{
			Rio_readnb(rp,data,contentLength);
			Rio_writen(p[1],data,contentLength);
		}
		exit(0)	;
	}
    
    /* Send response headers to client */
    sprintf(buf, "HTTP/1.0 200 OK\r\n");
    sprintf(buf, "%sServer: Tiny Web Server\r\n",buf);


    #ifdef HTTPS 
    if(ishttps)
    	SSL_write(ssl,buf,strlen(buf));
    else
    #endif
        Rio_writen(fd, buf, strlen(buf));


    Dup2(p[0],STDIN_FILENO);  /* Redirct p[0] to stdin */
    Close(p[0]);
    Close(p[1]);
    setenv("CONTENT-LENGTH",length , 1); 


    #ifdef HTTPS 
    if(ishttps)  /* if ishttps,we couldnot redirct stdout to client,we must use SSL_write */
    {
    	Pipe(httpsp);
	   if(Fork()==0)
	  {
    	Dup2(httpsp[1],STDOUT_FILENO);        /* Redirct stdout to https[1] */ 
		Execve(filename, emptylist, environ); 
	}
	Read(httpsp[0],data,MAXLINE);
	SSL_write(ssl,data,strlen(data));
    }
    else
    #endif
    {
    	Dup2(fd,STDOUT_FILENO);        /* Redirct stdout to client */ 
	    Execve(filename, emptylist, environ); 
    }
}

目录显示功能原理:
   主要是通过URL获取所需目录,然后获取该目录下所有文件,并发送相应信息,包括文件格式对应图片,文件名,文件大小,最后修改时间等。由于我们发送的文件名是通过超链接的形式,所以我们可以点击文件名继续浏览信息。
具体实现细节如下:

static void serve_dir(int fd,char *filename)
{
	DIR *dp;
	struct dirent *dirp;
    	struct stat sbuf;
	struct passwd *filepasswd;
	int num=1;
	char files[MAXLINE],buf[MAXLINE],name[MAXLINE],img[MAXLINE],modifyTime[MAXLINE],dir[MAXLINE];
	char *p;

	/*
	* Start get the dir   
	* for example: /home/yihaibo/kerner/web/doc/dir -> dir[]="dir/";
	*/
	p=strrchr(filename,'/');
	++p;
	strcpy(dir,p);
	strcat(dir,"/");
	/* End get the dir */

	if((dp=opendir(filename))==NULL)
		syslog(LOG_ERR,"cannot open dir:%s",filename);

    	sprintf(files, "<html><title>Dir Browser</title>");
	sprintf(files,"%s<style type=""text/css""> a:link{text-decoration:none;} </style>",files);
	sprintf(files, "%s<body bgcolor=""ffffff"" font-family=Arial color=#fff font-size=14px>\r\n", files);

	while((dirp=readdir(dp))!=NULL)
	{
		if(strcmp(dirp->d_name,".")==0||strcmp(dirp->d_name,"..")==0)
			continue;
		sprintf(name,"%s/%s",filename,dirp->d_name);
		Stat(name,&sbuf);
		filepasswd=getpwuid(sbuf.st_uid);

		if(S_ISDIR(sbuf.st_mode))
		{
			sprintf(img,"<img src=""dir.png"" width=""24px"" height=""24px"">");
		}
		else if(S_ISFIFO(sbuf.st_mode))
		{
			sprintf(img,"<img src=""fifo.png"" width=""24px"" height=""24px"">");
		}
		else if(S_ISLNK(sbuf.st_mode))
		{
			sprintf(img,"<img src=""link.png"" width=""24px"" height=""24px"">");
		}
		else if(S_ISSOCK(sbuf.st_mode))
		{
			sprintf(img,"<img src=""sock.png"" width=""24px"" height=""24px"">");
		}
		else
			sprintf(img,"<img src=""file.png"" width=""24px"" height=""24px"">");


	sprintf(files,"%s<p><pre>%-2d%s""<a href=%s%s"">%-15s</a>%-10s%10d %24s</pre></p>\r\n",files,num++,img,dir,dirp->d_name,dirp->d_name,filepasswd->pw_name,(int)sbuf.st_size,timeModify(sbuf.st_mtime,modifyTime));
	}
	closedir(dp);
	sprintf(files,"%s</body></html>",files);

	/* Send response headers to client */
	sprintf(buf, "HTTP/1.0 200 OK\r\n");
	sprintf(buf, "%sServer: Tiny Web Server\r\n", buf);
	sprintf(buf, "%sContent-length: %d\r\n", buf, strlen(files));
	sprintf(buf, "%sContent-type: %s\r\n\r\n", buf, "text/html");

	#ifdef HTTPS
	if(ishttps)
	{
		SSL_write(ssl,buf,strlen(buf));
		SSL_write(ssl,files,strlen(files));
	}
	else
	#endif
	{
		Rio_writen(fd, buf, strlen(buf));
		Rio_writen(fd, files, strlen(files));
	}
	exit(0);

}

HTTPS的实现:
   HTTPS主要基于openssl的开源库实现。如果没有安装,那么我们就不#define HTTPS。
HTTPS的功能主要就是提供安全的连接,服务器和浏览器之间传送的数据是通过加密的,加密方式可以自己选定。
   开始连接时,服务器需要发送CA,由于我们的CA是自己签发的,所以需要我们自己添加为可信。


访问控制功能:
主要是通过获取客户端IP地址,并转换为整数,与上配置文件中定义的掩码,如果符合配置文件中允许的网段,那么可以访问,否则不可以。
具体实现如下。

static long long ipadd_to_longlong(const char *ip)
{
	const char *p=ip;
	int ge,shi,bai,qian;
	qian=atoi(p);

	p=strchr(p,'.')+1;
	bai=atoi(p);

	p=strchr(p,'.')+1;
	shi=atoi(p);

	p=strchr(p,'.')+1;
	ge=atoi(p);

	return (qian<<24)+(bai<<16)+(shi<<8)+ge;
}


int access_ornot(const char *destip) // 0 -> not 1 -> ok
{
	//192.168.1/255.255.255.0
	char ipinfo[16],maskinfo[16];
	char *p,*ip=ipinfo,*mask=maskinfo;
	char count=0;
	char *maskget=Getconfig("mask");
	const char *destipconst,*ipinfoconst,*maskinfoconst;
	if(maskget=="")
	{
		printf("ok:%s\n",maskget);
		return 1;
	}	
	p=maskget;
/* get ipinfo[] start */
	while(*p!='/')
	{
		if(*p=='.')
			++count;
		*ip++=*p++;
	}
	while(count<3)
	{
		*ip++='.';
		*ip++='0';
		++count;
	}
	*ip='\0';
/* get ipinfo[] end */
/* get maskinfo[] start */
	++p;
	while(*p!='\0')
	{
		if(*p=='.')
			++count;
		*mask++=*p++;
	}
	while(count<3)
	{
		*mask++='.';
		*mask++='0';
		++count;
	}
	*mask='\0';

/* get maskinfo[] end */
	destipconst=destip;
	ipinfoconst=ipinfo;
	maskinfoconst=maskinfo;
	return ipadd_to_longlong(ipinfoconst)==(ipadd_to_longlong(maskinfoconst)&ipadd_to_longlong(destipconst));
}

配置文件的读取:
主要选项信息都定义与配置文件中。
格式举例如下;
#HTTP PORT
PORT = 8888
所以读取配置文件函数具体如下:

static char* getconfig(char* name)
{
/*
pointer meaning:

...port...=...8000...
   |  |   |   |  |
  *fs |   |   |  *be    f->forward  b-> back
      *fe |   *bs       s->start    e-> end
          *equal
*/
	static char info[64];
	int find=0;
	char tmp[256],fore[64],back[64],tmpcwd[MAXLINE];
	char *fs,*fe,*equal,*bs,*be,*start;

	strcpy(tmpcwd,cwd);
	strcat(tmpcwd,"/");
	FILE *fp=getfp(strcat(tmpcwd,"config.ini"));
	while(fgets(tmp,255,fp)!=NULL)
	{
		start=tmp;
		equal=strchr(tmp,'=');

		while(isblank(*start))
			++start;
		fs=start;

		if(*fs=='#')
			continue;
		while(isalpha(*start))
			++start;
		fe=start-1;

		strncpy(fore,fs,fe-fs+1);
		fore[fe-fs+1]='\0';
		if(strcmp(fore,name)!=0)
			continue;
		find=1;

		start=equal+1;
		while(isblank(*start))
			++start;
		bs=start;

		while(!isblank(*start)&&*start!='\n')
			++start;
		be=start-1;

		strncpy(back,bs,be-bs+1);
		back[be-bs+1]='\0';
		strcpy(info,back);
		break;
	}
	if(find)
		return info;
	else
		return NULL;
}


二、 测试
本次测试使用了两台机器。一台Ubuntu的浏览器作为客户端,一台Redhat作为服务器端,其中Redhat是Ubuntu上基于VirtualBox的一台虚拟机。

IP地址信息如下:

Ubuntu的vboxnet0:



RedHateth0:



RedHat主机编译项目:


由于我们同事监听了8000和4444,所以有两个进程启动。


HTTP的首页:



目录显示功能:



HTTP GET页面:



HTTPGET响应:


从HTTP GET响应中我们观察URL,参数的确是通过URL传送过去的。

其中getAuth.c如下:

#include "wrap.h"
#include "parse.h"

int main(void) {
    char *buf, *p;
    char name[MAXLINE], passwd[MAXLINE],content[MAXLINE];

    /* Extract the two arguments */
    if ((buf = getenv("QUERY_STRING")) != NULL) {
	p = strchr(buf, '&');
	*p = '\0';
	strcpy(name, buf);
	strcpy(passwd, p+1);
    }


    /* Make the response body */
    sprintf(content, "Welcome to auth.com:%s and %s\r\n<p>",name,passwd);
    sprintf(content, "%s\r\n", content);

    sprintf(content, "%sThanks for visiting!\r\n", content);
  
    /* Generate the HTTP response */
    printf("Content-length: %d\r\n", strlen(content));
    printf("Content-type: text/html\r\n\r\n");
    printf("%s", content);
    fflush(stdout);
    exit(0);
}

HTTPS的首页:由于我们的CA不可信,所以需要我们认可



认可后HTTPS首页:



HTTPS POST页面:



HTTPS POST响应:


从上我们可以看出,POST提交的参数的确不是通过URL传送的。



本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C语言开发Linux下web服务器(支持GET/POST,SSL,目录显示等) 的相关文章

  • 关于TrackMouseEvent用法总结

    对于这个函数我也是最近想研究控件自绘才知道它真正怎么用 以前只是见到过 嗯 废话不多说 我先说下我的问题 如何响应鼠标离开某个窗体 控件 事件 先大概讲下步骤 然后再集中对 TrackMouseEvent 进行详解 为按钮添加以下几个函数
  • 关于CComboBox的自绘

    我想 如果大家学过一些控件的自绘的话 CComboBox算是很难的一种了 首先是它本身的复杂度 它由三个控件组成 CEdit CListBox CButton 我想但就CEdit来讲 就够你受得了 还要想想他们之间的消息传递 不禁让人无从下
  • 2011年总结

    又是一年年终时 亦是一年总结时 想想自己从去年写年终总结到现在 已经很久没有写过字了 时间过得真快 又是一年过去了 这一年也是我出来工作的第二年 这一年总体来说自己无论在技术还是心态方面有了很大的进步 记得刚出学校那会 啥都不知道 对于工作
  • 内部链接与外部链接

    在说内部连接与外部连接前 xff0c 先说明一些概念 1 声明 一个声明将一个名称引入一个作用域 在c 43 43 中 xff0c 在一个作用域中重复一个声明是合法的 以下都是声明 xff1a int foo int int 函数前置声明
  • partition/stable_partition详解

    Partition 将满足条件的元素向前移动 TEMPLATE FUNCTION partition template lt class BidIt class Pr gt inline BidIt Partition BidIt Firs
  • jsoncpp解析拼装数组

    int main 数组创建与分析 例子一 string strValue 61 34 34 ldh 34 34 001 34 34 gfc 34 34 002 34 34 yyj 34 34 003 34 34 andy 34 34 005
  • 查看静态库(.lib)和动态库(.dll)的导出函数的信息

    一般情况下 xff0c 我们需要查看一个DLL或EXE中的包含的函数或是依赖的函数之类的信息 xff0c 可以使用VS自带的工具dumpbin xff1b 可以直接在命令行下输入dumpbin就可以查看他的使用说明 xff0c 如果未显示
  • do {...} while (0) 在宏定义中的作用

    http www cnblogs com lanxuezaipiao p 3535674 html 如果你是一名C程序员 xff0c 你肯定很熟悉宏 xff0c 它们非常强大 xff0c 如果正确使用可以让你的工作事半功倍 然而 xff0c
  • 即插即用型设备驱动的加载过程

    现假设驱动程序已被正确安装 xff1a 1 某种PnP总线驱动发现了即插即用设备的存在 xff1a 对于热插拔设备 xff0c 则发现过程发生于插入设备的瞬间 xff1b 如果是非热插拔设备 xff0c 则发现过程发生于系统启动时 2 Pn
  • C++如何编写属于自己的头文件 ---- 自己动手,丰衣足食

    自己动手 xff0c 丰衣足食 ps 其实这一篇文章老早以前就写了 xff0c 一直扔在草稿箱 xff0c 今天想起来了然后就发了出来 首先 xff0c 熟悉熟悉这些 是的没错 xff0c 这就是我们的Dev cpp 忽略其他东西 xff0
  • ubuntu安装vnc server-x11vnc并设置开机自动启动

    ubuntu安装x11vnc并设置开机自动启动 安装x11vnc 打开终端 xff0c 使用如下命令x11vnc span class hljs built in sudo span apt get install x11vnc 手动连接
  • 笔记本安装ubuntu18.04步骤及分区方法

    家中闲置一台08年的笔记本 xff08 没有无线无卡 xff09 xff0c 自己加装了一个2G的内存条 xff0c 食之无味弃之可惜 xff0c 思量再三准备重装Ubuntu18 04的系统当做小型服务器使用 因此记录下安装步骤以及分区方
  • 滑模控制学习笔记(三)

    滑模控制学习笔记 xff08 三 xff09 基于趋近律的滑模控制几种典型的趋近律等速趋近律指数趋近律幂次趋近律一般趋近律 基于趋近律的控制器设计仿真实例状态空间模型建立滑模控制器模型建立仿真结果 基于趋近律的滑模鲁棒控制仿真实例 基于趋近
  • 滑模控制学习笔记(六)

    滑模控制学习笔记 xff08 六 xff09 等效滑模控制等效滑模控制器设计等效控制设计滑模控制设计 仿真实例 等效滑模控制 滑模控制率可由等效控制 u e q u eq
  • 文件描述符 和 流的关系

    任何一种操作系统中 xff0c 程序在开始读写一个文件的内容之前 xff0c 必须首先在程序与文件之间建立连接或通信通道 xff0c 这一过程称为打开文件 打开一个文件的目的可以是为了读或者为了写 xff0c 也可以是即读又写 UNIX系统
  • 试用了5款BI分析工具,终于找到了上手最快的那一个!

    前几天 xff0c 领导甩给我一个任务 xff0c 考察几个BI工具 xff0c 下季度立项用 潜心做ETL的我 xff0c 对BI只是略懂 之前上的BO xff0c 由于开发模式不适应 人员用不惯 xff0c 再加上负责这块的同事走的走
  • JAVA多线程(二十一)Java多线程之SingleThreadExecutor单线程化线程池

    1 JAVA多线程 二十一 Java多线程之SingleThreadExecutor单线程化线程池 1 1 单线程化线程池SingleThreadExecutor SingleThreadExecutor 是只有一个线程的线程池 通过源代码
  • mysql: 常用函数总结以及高级函数用法

    数值型函数 函数名称作 用ABS求绝对值SQRT求二次方根MOD求余数CEIL 和 CEILING两个函数功能相同 xff0c 都是返回不小于参数的最小整数 xff0c 即向上取整FLOOR向下取整 xff0c 返回值转化为一个BIGINT
  • 双系统重装Ubuntu

    完全删除Ubuntu 1 右键此电脑 管理 磁盘管理 xff0c 删除Ubuntu所在卷 xff08 Ubuntu EFI分区无法删除 xff09 2 删除Ubuntu EFI分区 Win 43 R 输入cmd打开终端 xff0c 输入 d
  • 在Dockerfile CMD中使用变量

    如何在Dockerfile CMD中使用变量 xff1f 在我的Dockerfile中 xff1a ENV PROJECTNAME mytestwebsite CMD 34 django admin 34 34 startproject 3

随机推荐