前言:我从2007-2008赛季,加索尔来湖人之后,湖人三进总决赛的第一个赛季开始喜欢科比,那时候我五年级,现在十年过去了,我大三了,科比已经退役两年了。
我目前的技能足以支持我实现一个简单的关于科比的Web服务器,这是我两前,2016-4-14日(科比最后一场比赛)结束以后,就想完成的事情。

使用技术
网络编程
多线程
cgi机制
shell脚本

开发环境
centos 6.5
vim/gcc/gdb
c

项目描述
采用C/S(客户端/服务器)模型,实现支持中小型应用的http。
(.使用http/1.0版本)

http分层解剖

与http相关的一些协议
我在之前的博客都有总结,附上链接:
TCP:https://blog.csdn.net/han8040laixin/article/details/81300018
IP:https://blog.csdn.net/han8040laixin/article/details/81354588
DNS:https://blog.csdn.net/han8040laixin/article/details/81389509

介绍http协议

特点
客户端/服务器模式;
简单快速,因为http服务器规模小,通信速度快;
灵活,可传输任意类型数据,正在传输的类型由Content-Type标记;
无连接,基于请求响应(客户端发起http请求,服务器响应http请求,连接断开);
无状态,不会保留之前的一切请求或响应;

了解URI,URL的区别
URI:用来唯一的标识一个资源。
URL:是一种具体的URI,不仅唯一的标识了一个资源,还指明了如何定位该资源。
总的来说,如果可以把一个资源唯一的标识出来,就可以说该标识是URI,如果这个标识还可以定位该资源,那么它也可以是一个URL。

URL格式
http://host[“:”port][url]
若没有给url,则浏览器自动以”/”的形式输出

http请求与响应格式

请求
请求行:包括方法,URL和版本,用空格作间隔,以换行符分隔请求报头。
请求报头:属性采用name: vlaue形式,(注意value前面有空格),每个属性以换行符分隔;遇到空行就表示走完了头部,所以空行有效的划分了头部与数据。
请求正文:可以为空,如果正文存在,则在报头里有一个Content-Length字段来标识正文的大小。
响应
状态行:包括版本,状态码和状态码描述,以换行符结束。
响应报头:格式与请求报头相同。
响应正文:可以为空,如果正文存在,则在报头里有一个Content-Length字段来标识正文的大小。

http请求方法
GET:获取被URL标识的资源,通过URL传参。
POST:传输实体主体,通过正文传参。
PUT:传输文件,会有安全问题,大多web不会使用。
DELETE:和PUT相反,删除资源,一样不安全。
HEAD:获取报文首部,和GET类似只是不要正文。
OPTIONS:询问服务器支持的方法。
……
我在该项目中只实现了GET和POST方法,关于GET和POST,代码中会细细说明。

http状态码
状态码分5类,分别为1开头,2开头,3开头,4开头,5开头;

总结常见状态码
200:客户端的请求被正确处理
204:请求被正确处理,但是请求正文为空
206:表示客户单对服务器进行了范围请求,服务器成功处理,响应报头中有一个属性Content-Range指明范围

301:永久性重定向,表示客户端请求的资源已经分配了新的URI
302:临时性重定向,表示客户端请求的资源已被分配了新的URI,让用户本次使用新的URI访问
307:也是临时性重定向


301和302的区别,举个例子:
一个饭店,换地方开了,那就是301;
如果因为一些原因,暂时搬到了别的地方,过段时间又回来继续开,就是302.
________________________________________________________4
303:也是临时性重定向,但明确应使用GET方法定向获取请求的资源;
当301,302,303作为状态码返回,几乎所有浏览器都会把POST改成GET方法,即使301,302禁止将POST改成GET方法
307:也是临时性重定向,但是307会按规矩办事,不会把POST变成GET

400:表示请求报文中存在语法错误,需修改之后再请求
403:表示客户端请求的资源被服务器拒绝
404:not found,表示服务器上没有你要请求的资源

500:表示服务器在响应时发生了错误
503:表示服务器处于超负载或正在维护,无法处理请求

难点:HTTP CGI机制
cgi机制是外部应用程序(cgi程序)与服务器之间的桥梁。
因为客户端不仅要在服务器上拿资源,还要往服务器里上传一些东西(比如说注册),为了让服务器实现交互式,所以有了cgi技术。
注意cgi机制和cgi程序是两回事。
真正解释清楚cgi,需要知道GET方法和POST方法的区别
GET方法如果不传参,就不是cgi,返回资源就会,如果传参,就是cgi,参数在url里
POST方法一定是cgi,参数在正文,也就是说POST方法一定有参数

图解我的服务器

如图,蓝线表示普通http请求响应流,红线表示cgi模式下http请求响应流。

普通模式很好理解,由于我只支持GET和POST,所以普通模式一定是GET方法且GET方法不带参数,所以只要根据格式构建一个响应就行;
cgi模式有点棘手,首先cgi模式一定是POST方法或者GET方法带参,然后需要提参:GET方法直接从url里拿到参数,而POST方法只能通过请求报头中的Content-Length字段拿到正文长度;
由于要执行可执行程序,所以用到了exec进程替换的技术,由于我的服务器跑的是多线程,所以直接替换就知道把我的服务器替换了,所以得fork子进程,让子进程替换;
现在我需要把父进程的参数传给子进程,子进程跑完之后再把结果返回给父进程。
首先来看父进程给子进程传参,如果是GET,我可以把参数设为环境变量,是全局的,那么就实现了通信,然后如果是POST,我则需要一个匿名管道把正文传过去;
再看子进程把结果返给父进程,直接用匿名管道传就好了。
父进程拿到了执行结果,就可以构建响应返回给客户端了。

服务器源码

mian函数讲解:
我想把端口号拿到命令行参数,IP地址用INADDR_ANY,所以先判断一下命令行参数个数是否为2,不是的话打印使用手册,并退出;
然后通过StartUp函数来创建监听套接字;
之后开始三次握手建立连接,然后创建一个线程来处理这个请求,最后把线程分离,因为如果让主线程如果等待的话,会使服务器阻塞。

int main(int argc,char* argv[])
{if(argc != 2){//printf("./server [port]\n");usage(argv[0]);return 1;}int listen_sock = StartUp(argv[1]);while(1){struct sockaddr_in client;socklen_t len = sizeof(len);int new_sock = accept(listen_sock,(struct sockaddr*)&client,&len);if(new_sock < 0){perror("accept");continue;}printf("get a new client!\n");pthread_t tid;pthread_create(&tid,NULL,handler_request,(void*)new_sock);pthread_detach(tid);//pthread_join是阻塞的等待,所以分离}return 0;
}

使用手册讲解:通过命令行第一个参数(比如./server)+ 端口号,告之使用方法

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

创建监听套接字函数讲解:这个没什么好说的,先socket,再bind,再listen

int StartUp(const char* port){int sock = socket(AF_INET,SOCK_STREAM,0);if(sock < 0){perror("socket");return 2;}int opt = 1;setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));struct sockaddr_in local;local.sin_family = AF_INET;local.sin_addr.s_addr = htons(INADDR_ANY);local.sin_port = htons(atoi(port));if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0){perror("bind");return 3;}if(listen(sock,5) < 0){perror("listen");return 4;}return sock;
}

处理请求函数讲解:通过参数拿到与客户端通信的sock之后,首先要拿到请求的第一行,也就是请求行,之前说过,请求行是以换行符结尾的,然后不同客户端,他们的换行符可能不同,常见的换行符有三种:\n \r \r\n,所以get_line函数的功能就是:拿到请求报文的一行,并且把换行符统一为’\n’;
所以第一步就是,通过get_line函数把请求行拿到line数组里;
现在拿到了请求行,需要拿到具体的方法,既然是以空格作分隔,那就很简单了,挨个拷贝到我的method数组里;
现在拿到了方法,通过字符串比较,如果是GET方法,先不做处理,因为不知道是否带参,如果是POST方法,先把cgi设为1,如果是其他方法,直接把退出码设为400(客户端报文语法出错),然后通过我的echo_error函数给出具体的做法;
现在用同样的方法取到url,就需要直到url里是否带参,我的做法是再创建一个quert_string指针,一直走到指着url走,如果遇见’?’就说明有参数,就把‘?’变为’\0’,分离资源(?之前)和参数(?之后),然后把cgi设为1,跳出循环,如果一直没碰见’?’就说明无参,那cgi还是0;
走到这里,已经知道了方法(method),资源(url),参数(query_string),以及是否为cgi;
然而浏览器访问页面的时候,是默认会带上’/’的,而’/’是linux的根目录,我的服务器根目录为:wwwroot
所以要在url里加上wwwroot,利用sprintf函数,把wwwroot+url放在path数组里;
path有两种,举个例子,一种是wwwroot/a/b/c.html的形式,一种是wwwroot/的形式;
为了能简单一点,如果是以请求是以‘/’结尾的,我就通过exe_www函数把我写好的首页响应给客户,否则需要判断:
通过stat函数提取到文件的属性,如果文件不存在,错误码设为404(服务器没有该资源),然后通过我的echo_error函数给出具体的做法;
如果请求的是目录,我还是通过exe_www函数把写好的首页给客户端;
如果它是可执行程序,那一定是cgi,通过exe_cgi函数来完成;
如果不是目录也不是可执行程序,那就直接把资源给通过exe_www函数给客户端;

void* handler_request(void* arg)
{int sock = (int)arg;int status_code = 0;//响应状态码char line[MAX] = {0};//放请求的第一行int cgi = 0;//后续处理客户端的数据,默认没有cgi//应该先处理请求(第一行),要知道客户端向我怎么用什么方法,发起什么请求,请求什么资源等//怎么拿到一行,以换行符分隔,换行符可能为: \\n \r\n \r等三种,如果不统一,可能出现黏包问题//所以要统一,把这三种全部转化为\n,然后再获取第一行get_line(sock,line,sizeof(line));//把读入的第一行放在line里,统一三种换行符为\n //GET /a/b/c HTTP/1.1char method[MAX/10];//方法char url[MAX];//urlint i = 0;int j = 0;while(i < sizeof(method)-1 && j < sizeof(line) && !isspace(line[j])){method[i] = line[j];i++;j++;}method[i] = '\0';//只作GET和POST方法//GET方法和POST方法的区别\GET和POST的传参形式不同:GET通过url传参,POST通过请求正文传参if(strcasecmp(method,"GET") == 0){//strcasecmp忽略大小写比较,GET,Get都行}else if(strcasecmp(method,"POST") == 0){cgi = 1;//POST方法必须要以cgi方式运行}else{//不是GET方法也不是POST方法,就不支持clear_header(sock);//把头部信息清理,要把这一行数据读完才能给客户端响应status_code = 400;//响应状态码goto end;}//此时,已经提取了方法,要么是GET,要么是POST,下一步该提url了i = 0;//j先把metod走完,到url部分while(j < sizeof(line) && isspace(line[j])){j++;}while(i < sizeof(url)-1 && j < sizeof(line) && !isspace(line[j])){url[i] = line[j];i++;j++;}url[i] = '\0';#ifdef DEBUG//调试信息printf("method: %s,url: %s\n",method,url);printf("%s",line);
#endif//已经知道了method和url以及是否为cgi// url: /a/b/c.html?wd=世界杯&rsv_spt=1&rsv_iqid=0xdc00b0d4000084ae&issp=1&f=8// 以问号分隔资源和参数,参数之间用&分隔char* query_string = NULL;//url指向资源,query_string指向后面的内容if(strcasecmp(method,"GET") == 0){//query_string = url;while(*query_string){if(*query_string == '?'){//有参数*query_string = '\0';//让url指向前面的资源query_string++;//query_string指向后面的参数cgi = 1;//break;}query_string++;}//如果没有参数,那cgi就还是0,url还是以'\0'结尾,不影响}//现在已经知道了method url已经被分离 //GET的参数放在(query_string)里,如果有参数,cgi//POST为cgichar path[MAX] = {0};//  /为linux跟目录,要把根目录变为wwwroot//int sprintf(char *str, const char *format, ...);sprintf(path,"wwwroot%s",url);//此时path里有wwwroot+url// path有两种风格:// wwwroot/ 首页   // wwwroot/a/b/c.html//判断最后一个字符是否是/,是的话给首页if(path[strlen(path)-1] == '/'){strcat(path,HOME_PAGE);}//int stat(const char *path, struct stat *buf); 提取文件属性信息,第一个参数为资源,第二个为输出型struct stat st;if(stat(path,&st) < 0){//资源不存在clear_header(sock);status_code = 404;goto end;}else{//如果访问的是目录,就把对应的界面返回给用户;//如果访问的是二进制可执行程序,则把跑完的结果给用户,cgi模式下跑if(S_ISDIR(st.st_mode)){//是目录strcat(path,HOME_PAGE);}else if((st.st_mode & S_IXUSR) || (st.st_mode & S_IXGRP) || (st.st_mode & S_IXOTH)){//是可执行文件cgi = 1;}else{//不是目录,不是可执行,为普通文件//do nothing}if(cgi){//path后面表示的是可执行程序,要传参://GET:参数在query_string  POST在请求正文status_code = exe_cgi(sock,method,path,query_string);}else{//非cgi,一定是GET方法,且没有传参//第三个参数为文件的大小,通过st的size获取printf("path: %s\n", path);status_code = echo_www(sock,path,st.st_size);}}end:if(status_code != 200){echo_error(sock,status_code);}close(sock);
}

非cgi响应函数讲解:首先注意,通过返回值把状态码传到处理函数,因为是一个文件,不管是首页还是其他文件,所以我们先把文件打开;接下来先通过clear_header函数清掉报头(其实就是读到正文),然后通过line数组逐行把资源返回给客户端,我写的是构建一行,发一行,最后文件是通过sendfile函数发送的。

int echo_www(int sock,char* path,int size)
{//打开这个普通文件int fd = open(path,O_RDONLY);if(fd < 0){return 404;  }clear_header(sock);//构建响应char line[MAX];//报头sprintf(line,"HTTP/1.0 200 OK\r\n");//ssize_t send(int sockfd, const void *buf, size_t len, int flags);  flags设为0,用法和write一样send(sock,line,strlen(line),0);sprintf(line,"Content-Type:text/html;charset=ISO-8859-1\r\n");send(sock,line,strlen(line),0);sprintf(line,"\r\n");send(sock,line,strlen(line),0);//正文//ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);//out_fd:为了读 in_fd:为了写sendfile(sock,fd,NULL,size);close(fd);return 200;
}

cgi响应函数讲解:同样通过返回值把状态码传给处理函数;
与客户端通信的sock,方法method,资源path,GET的参数query_string都作为参数拿了过来;
所以接下来先判断方法,如果是GET方法,则直接清掉报头,因为资源和参数都拿到了;
如果是POST方法,参数在正文里,而请求报头中有一个Content_Length字段描述正文长度,所以在清掉报头的同时,把Content_Length的大小拿到;
读完之后如果content-length还是-1,则没有抓到,返回400(客户端语法错了);
走到这里已经读到正文了,我们知道path为可执行程序,GET的参数在query_string,POST的参数知道了长度,长度为content-length,之后的操作就是http的cgi机制最核心的部分:
我要把可执行程序path的执行结果返回给客户端,就需要exec进程替换,因为我用的是多线程,直接替换就把我的服务器替换了,所以要fork子进程,让子进程替换;
然后进程之间是独立的,父进程要把参数传给子进程,子进程拿到参数,要把程序跑完的结果返回给父进程,所以用到了进程间通信,我的方法是,对于父进程传参数给子进程:如果是GET,就把参数用环境变量传,如果是POST,既然知道了正文大小,就通过匿名管道把请求正文传给子进程;对于子进程,直接通过匿名管道把跑完的结果传给父进程,所以我创建了两个管道,之后fork子进程,如果fork失败,返回503(服务器超负载了);
最后一个问题:进程替换之后,就把input和output替换掉了,没办法通信了,所以在exec之前,用dup2把标准输入0重定向到input,把标准输出1重定向到output,而exec不会替换fd。

int exe_cgi(int sock,char* method,char* path,char* query_string)
{char line[MAX];int content_length = -1;if(strcasecmp(method,"GET") == 0){//程序在path,参数在query_string,所以直接清报头clear_header(sock);}else{//POST,需要读到正文获取参数(空行下一个)//但是得要直到正文大小,不然读多读少容易黏包//就得从报头中的[Content-Legth]字段获取正文长度//一行一行读,一直读到Content-Length字段,然后读完报头do{get_line(sock,line,sizeof(line));if(strncmp(line,"Content-Legth: ",16) == 0){//Content-Legth后面一定有长度,比如Conent-Legth: 13//所以只用看数字之前的前16个content_length = atoi(line+16);//line+16到数字}}while(strcmp(line,"\n"));//已经到正文了if(content_length = -1){//没有抓到content-lengthreturn 400;}}//path为可执行程序,GET的参数在query_string,POST的参数知道了长度//现在要执行指定路径的程序,用exec替换//线程不能直接exec,得fork子进程来exec//子进程跑的结果要返给父进程,父进程的参数要给子进程,需要匿名管道来处理进程间通信//而管道只能单向通信,所以得两个管道//子进程的视角int input[2];//子进程读,父进程写int output[2];//子进程写,父进程读pipe(input);pipe(output);pid_t id = fork();if(id < 0){return 503;}else if(id == 0){//childclose(sock);close(input[1]);close(output[0]);//**************************************************************************************************//环境变量为全局,可以传给子进程//所以通过环境变量把method传给子进程,如果是GET,再把query_string给过去,POST则把content_length给过去char method_env[MAX/10] = {0};sprintf(method_env,"METHOD=%s",method);//METHOD=GET/POSTputenv(method_env);if(strcasecmp(method,"GET") == 0){char query_string_env[MAX] = {0};sprintf(query_string_env,"QUERY_STRING=%s",query_string);putenv(query_string_env);}else{//POSTchar content_length_env[MAX/10];sprintf(content_length_env,"CONTENT-LENGTH=%s",content_length);putenv(content_length_env);}//***************************************************************************************************//进程替换之后,就把input和output替换掉了,没办法通信了//所以在exec之前,用dup2把标准输入0重定向到input,把标准输出1重定向到output//而exec不会替换fddup2(input[0],0);dup2(output[1],1);execl(path,path,NULL);//执行path路径下的可执行文件exit(1);}else{//parentclose(input[0]);close(output[1]); //若是POST,则只知道了content-length,要把正文给子进程char c;if(strcasecmp(method,"POST") == 0){int i = 0;for( ; i<content_length; i++){recv(sock,&c,1,0);//读请求正文write(input[1],&c,1);//给子进程}}//构建响应报头sprintf(line,"HTTP/1.0 200 OK\r\n");send(sock,line,strlen(line),0);sprintf(line,"Content-Type:text/html;charset=ISO-8859-1\r\n");send(sock,line,strlen(line),0);sprintf(line,"\r\n");send(sock,line,strlen(line),0);//已经把程序参数都给子进程了,父进程该拿执行结果了while(read(output[0],&c,1) > 0){send(sock,&c,1,0);//从子进程拿到执行结果,给客户端响应正文}waitpid(id,NULL,0);//阻塞等子进程没问题,因为是线程阻塞,不影响主线程}return 200;
}

读一行并且把换行符统一为’\n’函数,读至正文函数,处理错误函数,以及404响应函数:我暂时只处理了404,把一个出错的页面返回给客户端;

int get_line(int sock,char* line,int size)
{assert(line);assert(size > 0);//一次从sock里读一个字符,读到\n,则读到了换行符;//若读到了\r,就再检测下一个int i = 0;char c = 'A';while(c != '\n' && i < size-1){//读到\n就是换行符,一定读完了第一行//ssize_t recv(int sockfd, void *buf, size_t len, int flags);//当flags为0时,则recv和read一样;当flags为 MSG_PEEK 时,则不读,只是窥探一下ssize_t s = recv(sock,&c,1,0);//每次从sock里读一个字符到cif(s > 0){if(c == '\r'){//要么是\r,要么是\r\n//所以要把\r和\r\n都转化为\n//所以此时要再读下一个来判断,下一个如果是\n,则把\r\n转为\n//下一个若是正常字符(以\r换行),那么就会把下一行的第一个字符读走,多读了一个//所以就需要先知道下一个是什么,用MSG_PEEK选项窥探recv(sock,&c,1,MSG_PEEK);if(c == '\n')//读到了\r\nrecv(sock,&c,1,0);//就再读一次,读到\n,那么就读到了完整的一行else//以\r换行c = '\n';//直接把\r变为\n}//不是\r,要么是正常字符,要么是\nline[i++] = c;//放入line}}line[i++] = '\0';return i;
}void clear_header(int sock)
{char line[MAX];do{get_line(sock,line,sizeof(line));}while(strcmp(line,"\n"));
}void show_404(int sock)
{char line[1024];sprintf(line,"HTTP/1.0 404 NOT Found\r\n");send(sock,line,strlen(line),0);sprintf(line,"Content-Type:text/html;charset=ISO-8859-1\r\n");send(sock,line,strlen(line),0);sprintf(line,"\r\n");send(sock,line,strlen(line),0);struct stat st;stat(PAGE_404,&st);int fd = open(PAGE_404,O_RDONLY);sendfile(sock,fd,NULL,st.st_size);close(fd);
}void echo_error(int sock,int code)
{switch(code){case 400://show_404();break;case 403://show_404();break;case 404:show_404(sock);break;case 500://show_404();break;case 503://show_404();break;default:break;}
}

cgi程序讲解:由于在cgi机制里,把method,content-length,query_string都设为了环境变量,所以在cgi程序里,直接拿就好了,cgi程序的目的就是拿到参数放入buf,然后处理参数。

void data_begin(char* buf)
{//Season=19961997&Type=0//int sscanf(const char *str, const char *format, ...);//把str里格式化输入到某个变量int x = 0;int y = 0;sscanf(buf,"Season=%d&Type=%d",&x,&y);//printf("x: %d,y: %d\n",x,y);switch(x){case 19961998:if(y == 0)printf("pts:7.6 ass:1.3 rebs:1.9 \n");elseprintf("pts:8.2 ass:1.2 rebs:1.2 \n\n");break;//中间是老科二十个赛季的数据,我就不在文章里放了,有点长case 20152016:if(y == 0)printf("pts:17.6 ass:2.8 rebs:3.7 \n");elseprintf("Lakers haven't been in the playoffs this season\n");break;default:printf("enter again!\n");}
}int main()
{char method[64];char buf[1024];//放参数if(getenv("METHOD")){strcpy(method,getenv("METHOD"));if(strcasecmp(method,"GET") == 0){//GET方法直接从QUERY_STRING里获取参数strcpy(buf,getenv("QUERY_STRING"));}else{int content_length = -1;content_length = atoi(getenv("CONTENT-LENGTH"));//读content_length个长度获取参数int i = 0;for( ; i < content_length; i++){read(0,buf+i,1);}buf[i] = '\0';}//参数已经在buf里了//printf("cgi arg: %s\n",buf);data_begin(buf);}return 0;
}

项目文件

build.sh:利用shell脚本执行make clean;make
start.sh:利用shell脚本启动服务器
http.c:我的服务器程序
comm.h:头文件以及宏

#pragma once#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>#include <ctype.h>
#include <pthread.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/sendfile.h>
#include <sys/wait.h>#define MAX 1024
#define HOME_PAGE "index.html"
#define PAGE_404 "wwwroot/404.html"

wwwroot:服务器的根目录,内容:

404.html:404页面
cgi:里面放的cgi程序
imag:存的我的图片
index.html:首页

Makefile:一旦make,生成的是http服务器程序以及cgi程序的可执行文件。

WORK_PATH=$(shell pwd)  #当前工作目录放在WORK_PATH
BIN=httpd  #目标
SRC=httpd.c  #依赖
CC=gcc #方法
LDFLAGS=-lpthread -DDEBUG#链接选项
#gcc的-D选项,指定宏.PHONY:all
all:$(BIN) cgi$(BIN):$(SRC)$(CC) -o $@ $^ $(LDFLAGS)
cgi:cd wwwroot/cgi/; make clean;make; cd -.PHONY:output
output:mkdir -p output/wwwroot/cgi #-p:批量化生成路径cp $(BIN) outputcp wwwroot/*.html output/wwwrootcp -rf wwwroot/imag output/wwwrootcp wwwroot/cgi/data_cgi output/wwwroot/cgi.PHONY:clean
clean:rm -f $(BIN)rm -rf outputcd wwwroot/cgi/; make clean; cd-

服务器效果:
首页:输入赛季以及季后赛还是常规带来获取科比的数据

cgi:

404界面:

项目里遇到的问题
1.服务器应答时,没有将html格式的页面发送,而是将代码展示在浏览器:
原因:检查代码无误,发现是浏览器兼容问题,换一个浏览器就解决了问题。

2.首页可以打开了,html页面展现了出来,但是虽然文字无误,但是图片无法打开:
原因:
构建响应,send时发送了写错了,写成了strlen(path),而不是line。

3.POST方法无法执行cgi:
原因:
父进程确认方法之后,如果是POST就要把正文发给子进程,这里if判断写错了,少写了==0。

致敬科比,实现查询科比每赛季数据的Web服务器相关推荐

  1. 如何将iPhone核心数据与Web服务器同步,然后推送到其他设备? [关闭]

    本文翻译自:How to Sync iPhone Core Data with web server, and then push to other devices? [closed] I have ...

  2. 网页html通过隐藏域传送数据给web服务器

    form表单输入框input设置disable属性后,提交表单,该input不会一并提交.解决该类问题可以使用一个type=hidden隐藏要获取值的input,再使用disabled显示给用户看的i ...

  3. 返回固定数据的web服务器

    import socketdef handle_client(socket_con):"""接收来自客户端的请求,并接收请求报文,解析,返回""&qu ...

  4. 《网络是怎样连接的》第一章第二节:向DNS服务器查询Web服务器的IP地址

    <网络是怎样连接的>第一章:浏览器生成消息 概述:这本书以 "从在浏览器输入网址,到屏幕显示出网页,当中到底发生了什么?"为疑问,探究其中的过程.本章讲的是浏览器怎么把 ...

  5. 6. 毕业设计温湿度监控系统(ESP8266 + DHT11 +OLED 实时上传温湿度数据给公网服务器并在OLED显示屏上显示实时温湿度)

    文章目录 硬件环境 软件环境 1. WiFi联网和HttpPost配置 2. DHT11温湿度读取和OLED显示配置 3. Web服务器配置(用于接收HTTP数据请求) 实验过程 1. ESP8266 ...

  6. mysql选出重复的字段_mysql查询表里的重复数据方法:

    INSERT INTO hk_test(username, passwd) VALUES ('qmf1', 'qmf1'),('qmf2', 'qmf11') delete from hk_test  ...

  7. python使用pandas基于时间条件查询多个oracle数据表

    python使用pandas基于时间条件查询多个oracle数据表 目录 python使用pandas基于时间条件查询多个orcale数据表 #orcale数据连接

  8. 关于SQL查询效率,100w数据,查询只要1秒

    1.关于SQL查询效率,100w数据,查询只要1秒,与您分享: 机器情况 p4: 2.4 内存: 1 G os: windows 2003 数据库: ms sql server 2000 目的: 查询 ...

  9. MYsql 查询 查询当前月份的数据

    select   date_format( curdate(),'%Y%m') ; 查询当前这周的数据 SELECT name,submittime FROM enterprise WHEREYEAR ...

  10. gin post 数据参数_Gin 使用示例(四):绑定查询字符串或 POST 数据

    Gin 使用示例(四):绑定查询字符串或 POST 数据 由 学院君 创建于5个月前, 最后更新于 5个月前 版本号 #3 780 views 0 likes 0 collects 示例代码(src/ ...

最新文章

  1. 人对光波的三种特性_花友小叶投稿:养花一年了,三种绿植基本不用管,没光也不怕...
  2. git常用命令之stash
  3. 什么用于创建python与数据库之间的链接_python3连接数据库用什么
  4. 在Linux上安装nginx时遇到的问题,真的好坑啊!!!!
  5. Why is HttpContext.Current null after await?
  6. DotNet程序员是不是最不幸福?
  7. mybatis-generator一些注意点 2021-04-21
  8. webbench接口并发测试
  9. 代写python作业 费用_代写dither method作业、代做python程序设计作业、代写python语言作业、代做Image Dithering作...
  10. 【2019杭电多校第五场1007=HDU6630】permutation 2(打表找规律+分情况讨论)
  11. python生成双层pdf
  12. 用matlab求三次方程根,三次方程的根式求解(通俗版本)
  13. 802.1x 命令说明以及配置方法
  14. python练手项目pdf_一个不错的练手项目!
  15. 2021 智慧养老整体解决方案
  16. 给仍在「 选品 」的跨境卖家提个醒!
  17. 基于Kinect Azure的多相机数据采集(一)
  18. 在任意文件夹下以管理员的身份运行powershell
  19. 转:飝兒物語的“Linux创建、删除文件夹”
  20. 股票价格综合指数(上证指数、深证指数)笔记

热门文章

  1. 这一刻我學會了堅強、給我一雙翅膀,我会向天空去翱翔。
  2. Krpano元素的一些解析
  3. 数字键盘(触屏键盘)
  4. 超帅的C核心相关总结
  5. 2010年新版俏皮话
  6. 微信扫码支付 支付模式二
  7. 64位计算机装32位系统,32位装64位系统教程
  8. leetcode 有效的括号
  9. 报错解决:Lammps中lmp_mpi编译出错
  10. 计算机管理格式化没有顺利完成,格式化没有顺利完成怎么办?