1、项目介绍
HTTP协议是应用层的面向对象的协议,由于其简捷、快速的方式,适用于分布式超媒体信息系统。协议的详细内容,前面一篇HTTP协议详解已经详细介绍了,这里不再赘述。
项目总体描述:HTTP支持客户端/服务器模式,终端用户可通过浏览器或网络爬虫与服务器建立连接,所以首先需要自主实现服务器Server端,具体由头文件httpd.h、main函数文件httpd.c、模块功能函数文件httpd.c组成,主要实现客户端与服务器通过socket建立通信机制。首先由用户主动发起一个到服务器上指定端口(默认端口为80)的请求,服务器则在那个端口监听客户端发送过来的请求。服务器一行一行读取请求,通过请求信息判断用户请求资源的方法和路径,若方法和路径没有问题,则方法和路径通过CGI模式或非CGI向用户提供不同的HTML网页信息。处理完请求客户端向用户发送响应,包括状态行如:“HTTP/1.1 200 OK”、响应报头、消息正文,消息体即为服务器上的资源。
实现功能一:静态首页展示(图片、文字文字信息);
实现二:支持表单提交,可以借助浏览器或telnet工具使用GET、POST方法访问服务器,实现数据的简单计算功能;
实现三:引入MYSQL,用户可通过页面表单进行数据操作,服务器拿到客户提交的数据后,会把数据存入到远端数据库,客户端也可请求查看数据库信息。
整个项目的文件目录:
目录:
conf:配置文件,存放需要绑定的服务器的ip和port ;
log:shell的日志文件以及http错误处理的日志文件 ;
sql_client:mysql部分的API及CGI实现;
thread_pool:线程池实现;
wwwroot:web服务器工作的根目录,包含各种资源页面(例如默认的index.html页面,差错处理的404页面),以及执行cgi的可执行程序。下面还有一个 cgi-bin目录,是存放CGI脚本的地方。这些脚本使WWW服务器和浏览器能运行外部程序,而无需启动另一个程序。它是运行在Web服务器上的一个程序,并由来自于浏览者的输入触发。
整个项目的框架图:
2、各模块功能介绍
头文件httpd.h,包含该项目代码所使用的全部函数的头文件以及宏定义,和函数声明;
1 #ifndef _HTTPD_ 2 #define _HTTPD_ 3 4 #include5 #include 6 #include 7 #include 8 #include in.h> 9 #include 10 #include 11 #include 12 #include <string.h> 13 #include 14 #include 15 #include 16 17 #define SUCCESS 0 18 #define NOTICE 1 19 #define WARNING 2 20 #define ERROR 3 21 #define FATAL 4 22 23 #define SIZE 1024 24 25 void print_log(char *msg, int level); //打印日志 26 int startup(const char *ip, int port); //创建监听套接字 27 void *handler_request(void *arg); //处理请求 28 29 #endif
main函数文件main.c实现主要通信逻辑,通过socket建立连接的,监听和接受套接字,然后创建新线程处理请求。
1 #include2 #include "httpd.h" 3 4 static void usage(const char *proc) 5 { 6 printf("Usage: %s [local_ip] [local_port]\n", proc); 7 } 8 9 int main(int argc, char *argv[]) 10 { 11 if(argc != 3){ 12 usage(argv[0]); 13 return 1; 14 } 15 16 int listen_sock = startup(argv[1], atoi(argv[2]));//监听套接字 17 //daemon(0, 0); 18 while(1){ 19 struct sockaddr_in client; 20 socklen_t len = sizeof(client); 21 int new_sock = accept(listen_sock, (struct sockaddr*)&client, &len);//接收套接字 22 if(new_sock < 0){ 23 print_log(strerror(errno), NOTICE); 24 continue; 25 } 26 27 printf("get client [%s:%d]\n",\ 28 inet_ntoa(client.sin_addr),\ 29 ntohs(client.sin_port)); //链接到一个客户端之后打印其IP及端口号 30 31 pthread_t id; 32 int ret = pthread_create(&id, NULL,\ //创建新线程 33 handler_request, (void *)new_sock); 34 if(ret != 0){ 35 print_log(strerror(errno), WARNING); 36 close(new_sock); 37 }else{ 38 pthread_detach(id); //将子线程分离,该线程结束后会自动释放所有资源 39 } 40 } 41 close(listen_sock); 42 return 0; 43 }
模块功能函数在httpd.c文件
1 #include "httpd.h" 2 3 void print_log(char *msg, int level) 4 { 5 #ifdef _STDOUT_ 6 const char * const level_msg[]={ 7 "SUCCESS", 8 "NOTICE", 9 "WARNING", 10 "ERROR", 11 "FATAL", 12 }; 13 printf("[%s][%s]\n", msg, level_msg[level%5]); 14 #endif 15 } 16 17 int startup(const char *ip, int port) // 18 { 19 int sock = socket(AF_INET, SOCK_STREAM, 0); //创建套接字 20 if(sock < 0){ 21 print_log(strerror(errno), FATAL); //strerror()将错误码转换为对应的错误码描述 22 exit(2); 23 } 24 25 int opt = 1; 26 setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); //将该套接字设置为地址复用状态,若服务器挂掉可实现立即重启 27 28 struct sockaddr_in local; 29 local.sin_family = AF_INET; 30 local.sin_port = htons(port); //端口号转换 31 local.sin_addr.s_addr = inet_addr(ip); //ip转换 32 if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0){ //绑定 33 print_log(strerror(errno), FATAL); 34 exit(3); 35 } 36 if(listen(sock, 10) < 0){ //监听 37 print_log(strerror(errno), FATAL); 38 exit(4); 39 } 40 return sock; 41 } 42 43 //ret > 1, line != '\0'读成功,正常字符; ret=1&line='\n' ret<=0&&line=='\0' 44 static int get_line(int sock, char line[], int size) //得到一行请求内容 45 { 46 // read 1 char , one by one 47 char c = '\0'; 48 int len = 0; 49 while( c != '\n' && len < size-1){ 50 int r = recv(sock, &c, 1, 0); 51 if(r > 0){ 52 if(c == '\r'){ 53 //窥探,只把缓冲区的东西拿出来看看 54 int ret = recv(sock, &c, 1, MSG_PEEK); 55 if(ret > 0){ 56 if(c == '\n'){ 57 recv(sock, &c, 1, 0); 58 }else{ 59 c = '\n'; 60 } 61 } 62 }// \r->\n \r\n -> \n 63 line[len++] = c; 64 }else{ 65 c = '\n'; 66 } 67 } 68 line[len]='\0'; 69 return len; 70 } 71 //不同平台下\n、\r、\n+\r,意义不同,这里将其统一成\n 72 73 static void echo_string(int sock) 74 {} 75 76 static int echo_www(int sock, char *path, int size) 77 { 78 int fd = open(path, O_RDONLY); 79 if(fd < 0){ 80 echo_string(sock); 81 print_log(strerror(errno), FATAL); 82 return 8; 83 } 84 85 const char *echo_line="HTTP/1.0 200 OK\r\n"; //状态行 86 send(sock, echo_line, strlen(echo_line), 0); 87 const char *null_line="\r\n"; 88 send(sock, null_line, strlen(null_line), 0); //空行 89 90 if(sendfile(sock, fd, NULL, size) < 0){//在内核区实现两个文件描述符的拷贝,不用定义临时变量,省略两次数据拷贝,效率提高 91 echo_string(sock); 92 print_log(strerror(errno), FATAL); 93 return 9; 94 } 95 96 close(fd); 97 return 0; 98 } 99 100 static void drop_header(int sock) 101 { 102 char line[1024]; 103 int ret = -1; 104 do{ 105 ret = get_line(sock, line, sizeof(line)); 106 }while(ret>0 && strcmp(line, "\n")); 107 } 108 109 static int exe_cgi(int sock, char *method, \ 110 char *path, char *query_string) 111 { 112 int content_len = -1; 113 char method_env[SIZE/10]; 114 char query_string_env[SIZE]; 115 char content_len_env[SIZE/10]; 116 117 if( strcasecmp(method, "GET") == 0 ){//忽略大小写的字符比较,此处为判断请求资源的方法是否为GET方法 118 drop_header(sock);//如果是GET方法则已从URL中知道用户请求资源所传参数 119 }else{//POST 120 char line[1024]; 121 int ret = -1; 122 do{ 123 ret = get_line(sock, line, sizeof(line)); 124 if(ret > 0 &&\ 125 strncasecmp(line,"Content-Length: ", 16)== 0){ 126 content_len = atoi(&line[16]);//消息正文字长描述 127 } 128 }while(ret>0 && strcmp(line, "\n")); 129 if(content_len == -1){ 130 echo_string(sock); 131 return 10; 132 } 133 } 134 const char *echo_line="HTTP/1.0 200 OK\r\n"; //状态行 135 send(sock, echo_line, strlen(echo_line), 0); 136 const char *type="Content-Type:text/html;charset=ISO-8859-1\r\n"; 137 send(sock, type, strlen(type), 0); 138 const char *null_line="\r\n"; 139 send(sock, null_line, strlen(null_line), 0); //空行 140 141 printf("query_string: %s\n", query_string); 142 //path-> exe 143 int input[2]; 144 int output[2]; 145 if(pipe(input) < 0 || pipe(output) < 0){ 146 echo_string(sock); 147 return 11; 148 } 149 pid_t id = fork(); 150 if(id < 0){ 151 echo_string(sock); 152 return 12; 153 }else if(id == 0){//child 154 close(input[1]); 155 close(output[0]); 156 sprintf(method_env, "METHOD=%s", method); 157 putenv(method_env); 158 159 if(strcasecmp(method, "GET") == 0){ 160 sprintf(query_string_env, "QUERY_STRING=%s", query_string); 161 putenv(query_string_env); 162 }else{ // POST 163 sprintf(content_len_env, "CONTENT_LENGTH=%d", content_len); 164 putenv(content_len_env); 165 } 166 dup2(input[0], 0);//重定向 167 dup2(output[1], 1); 168 execl(path, path, NULL); //第一个参数:路径及名字,第二个参数:怎么执行,传什么参数 169 printf("execl error!\n"); 170 exit(1); 171 }else{ 172 close(input[0]); 173 close(output[1]); 174 175 int i = 0; 176 char c = '\0'; 177 if(strcasecmp(method, "POST") == 0){ 178 for( ; i < content_len; i++ ){ 179 recv(sock, &c, 1, 0); 180 write(input[1], &c, 1); 181 } 182 } 183 184 c='\0'; 185 while(read(output[0], &c, 1) > 0){ 186 send(sock, &c, 1, 0); 187 } 188 189 waitpid(id, NULL, 0); 190 close(input[1]); 191 close(output[0]); 192 } 193 } 194 195 //thread 196 void *handler_request(void *arg) 197 { 198 int sock = (int)arg; 199 #ifdef _DEBUG_ //测试代码 200 char line[1024]; 201 do{ 202 int ret = get_line(sock, line, sizeof(line)); 203 if(ret > 0){ 204 printf("%s", line); 205 }else{ 206 printf("request ...... done!\n"); 207 break; 208 } 209 }while(1); 210 #else 211 int ret = 0; 212 char buf[SIZE]; //读到的请求内容 213 char method[SIZE/10]; //请求资源的方法 214 char url[SIZE]; //统一资源标识符 215 char path[SIZE]; //有效资源路径 216 int i, j; 217 int cgi = 0; //设置CGI模式 218 char *query_string = NULL; //请求资源字符串(URL中问号后的内容) 219 if(get_line(sock, buf, sizeof(buf)) <= 0){ //获得一行请求内容 220 echo_string(sock); 221 ret = 5; 222 goto end; 223 } 224 i=0;//method ->index 225 j=0;//buf -> index 226 227 while( !isspace(buf[j]) &&\ 228 j < sizeof(buf) &&\ 229 i < sizeof(method)-1){ 230 method[i]=buf[j]; 231 i++, j++; 232 } 233 method[i] = 0; 234 if(strcasecmp(method, "GET") &&\ //忽略大小写的字符比较,此处为判断请求资源的方法是否为GET方法或POST方法 235 strcasecmp(method, "POST") ){ 236 echo_string(sock); 237 ret = 6; 238 goto end; 239 } 240 if(strcasecmp(method, "POST") == 0){ //如果使用POST方法必定是CGI模式 241 cgi = 1; 242 } 243 //buf -> "GET / http/1.0" 244 while(isspace(buf[j]) && j < sizeof(buf)){ 245 j++; 246 } 247 i=0; 248 while(!isspace(buf[j]) && j < sizeof(buf) && i < sizeof(url)-1){ 249 url[i] = buf[j]; 250 i++, j++; 251 } 252 url[i] = 0; 253 printf("method: %s, url: %s\n", method, url); 254 query_string = url; 255 while(*query_string != '\0'){ 256 if(*query_string == '?'){//如果是GET方法且传参,必定是CGI模式 257 *query_string = '\0'; 258 query_string++; 259 cgi = 1; 260 break; 261 } 262 query_string++; 263 } 264 sprintf(path, "wwwroot%s", url); 265 //method, url, query_string, cgi 266 if(path[strlen(path)-1] == '/'){ // '/' 267 strcat(path, "index.html");//如果是GET方法且无参,拼接上首页信息 268 } 269 struct stat st; 270 if(stat(path, &st) != 0){ 271 echo_string(sock); 272 ret = 7; 273 goto end; 274 }else{ 275 if(S_ISDIR(st.st_mode)){ //如果是目录,则拼接上首页信息,默认任何目录下都可以访问首页 276 strcat(path, "/index.html"); 277 }else if( (st.st_mode & S_IXUSR) || \ //如果是二进制文件 278 (st.st_mode & S_IXGRP) || \ 279 (st.st_mode & S_IXOTH) ){ 280 cgi=1; 281 }else{ 282 } 283 //ok->cgi=?, path, query_string, method 284 if(cgi){ 285 printf("enter CGI\n"); //进入CGI模式处理 286 exe_cgi(sock, method, path, query_string); 287 }else{//非CGI处理 288 printf("method: %s, url: %s, path: %s, cgi: %d, query_string: %s\n", method, url, path, cgi, query_string); 289 drop_header(sock); //!!!!!!!!!!!!!!清除信息(不关心的内容) 290 echo_www(sock, path, st.st_size);//非CGI模式时的响应 291 } 292 } 293 294 end: 295 printf("quit client...\n"); //出错退出 296 close(sock); 297 return (void*)ret; 298 #endif 299 }
3、相关技术解释:
(1)CGI:通用网关接口
基本原理:通用网关接口是一个Web服务器主机提供信息服务的标准接口。通过CGI接口,Web服务器根据客户端提交的资源请求信息,转交给服务器端对应的CGI程序进行处理,最后返回结果给客户端。简单来说就是HTTP服务器与客户端进行“交谈”的一种工具,其程序须运行在网络服务器上。
组成CGI通信系统的是两部分:一部分是html页面,就是在用户端浏览器上显示的页面。另一部分则是运行在服务器上的Cgi程序。绝大多数的CGI程序被用来解释处理来自表单的输入信息,并在服务器产生相应的处理,或将相应的信息反馈给浏览器。CGI程序使网页具有交互功能。
CGI在客户端与服务器通讯中的处理步骤:
1)通过Internet把用户请求送到服务器;
2)服务器接收用户请求并交给相应CGI程序处理;
3)CGI程序把处理结果传送给服务器;
4)服务器把结果返回给用户。
前面已经介绍过服务器和客户端之间的通信,实际上是客户端的浏览器和服务器端的http服务器之间的HTTP通信,我们只需要知道浏览器请求执行服务器上哪个CGI程序就可以了,其他不必深究细节,因为这些过程不需要程序员去操作。服务器和CGI程序之间的通讯才是我们关注的。一般情况下,服务器和CGI程序之间是通过标准输入输出来进行数据传递的,而这个过程需要环境变量的协作方可实现。在服务器端执行步骤:1)服务器将URL指向一个应用程序 2)服务器为应用程序执行做准备 3)应用程序执行,读取标准输入和有关环境变量 4)应用程序进行标准输出。
(2)CGI关于环境变量
对于CGI程序来说,它继承了系统的环境变量。CGI环境变量在CGI程序启动时初始化,在结束时销毁。
当一个CGI程序不是被HTTP服务器调用时,它的环境变量几乎是系统环境变量的复制。
当这个CGI程序被HTTP服务器调用时,它的环境变量就会多了以下关于HTTP服务器、客户端、CGI传输过程等项目。
CONTENT_TYPE:如application/x-www-form-urlencoded,表示数据来自HTML表单,并且经过了URL编码。
ACCEPT:客户机所支持的MIME类型清单,内容如:”image/gif,image/jpeg”
REQUEST_METHOD:本项目涉及常见的两种方法:POST和GET,但我们写CGI程序时,最后还要考虑其他的情况。
环境变量是一个保存用户信息的内存区。当客户端的用户通过浏览器发出CGI请求时,服务器就寻找本地的相应CGI程序并执行它。在执行CGI程序的同时,服务器把该用户的信息保存到环境变量里。接下来,CGI程序的执行流程是这样的:查询与该CGI程序进程相应的环境变量:第一步是request_method,如果是POST,就从环境变量的len,然后到该进程相应的标准输入取出len长的数据。如果是GET,则用户数据就在环境变量的QUERY_STRING里。
(3)POST/GET传输方式详解
1)POST方法
如果采用POST方法,那么客户端发送的用户数据将存放在CGI进程的标准输入中,即消息正文内,较为隐蔽,且一般没有上限。同时将用户数据的长度赋予环境变量中的CONTENT_LENGTH。客户端用POST方式发送数据有一个相应的MIME类型(通用Internet邮件扩充服务:Multi-purpose Internet Mail Extensions)。目前,MIME类型一般是:application/x-wwww-form-urlencoded,该类型表示数据来自HTML表单。该类型记录在环境变量CONTENT_TYPE中,CGI程序应该检查该变量的值。
2)GET方法
在该方法下,CGI程序无法直接从服务器的标准输入(用户发送的消息正文)中获取数据,因为服务器把它从标准输入接收到得数据编码到环境变量QUERY_STRING(或PATH_INFO)。
采用GET方法提交HTML表单数据的时候,客户机将把这些数据附加到由ACTION标记命名的URL的末尾,用一个包括把经过URL编码后的信息与CGI程序的名字分开:http://www.mycorp.com/hello.html?name=hgq$id=1,QUERY_STRING的值为name=hgq&id=1(?左侧为要请求的资源,右侧为参数,参数形式一般为name=value形式,以“&”连接)。或者使用nomal形式的GET方法,无参数,不带正文,只有请求行+消息报头+空行。有些程序员不愿意采用GET方法,因为在他们看来,把动态信息附加在URL的末尾有违URL的出发点:URL作为一种标准用语,一般是用作网络资源的唯一定位标示。
3)POST与GET的区别
以 GET方式接收的数据是有长度限制,而用 POST方式接收的数据是没有长度限制的。并且,以 GET方式发送数据,可以通过 URL的形式来发送,但 POST方式发送的数据必须要通过 Form才到发送。
CGI程序示例 mathcgi.h :
1 #include2 #include 3 4 void mymath(char *arg) 5 { 6 //data1=1000&data2=2000 7 char *argv[3]; 8 int i = 0; 9 char *start = arg; 10 while(*start){ 11 if(*start == '='){ 12 start++; 13 argv[i++] = start; 14 continue; 15 } 16 if(*start== '&'){ 17 *start = '\0'; 18 } 19 start++; 20 } 21 argv[i] = NULL; 22 int data1 = atoi(argv[0]); 23 int data2 = atoi(argv[1]); 24 printf(""); 25 printf("%d + %d = %d
", data1, data2, data1 + data2); 26 printf("%d - %d = %d
", data1, data2, data1 - data2); 27 printf("%d * %d = %d
", data1, data2, data1 * data2); 28 printf("%d / %d = %d
", data1, data2, data2==0? 0 : data1 / data2); 29 printf("%d %% %d = %d
", data1, data2, data2==0? 0 : data1 % data2); 30 printf(""); 31 } 32 33 int main() 34 { 35 char *method = NULL; 36 char *query_string = NULL; 37 char *string_arg = NULL; 38 int content_len = -1; 39 char buf[1024]; 40 if((method=getenv("METHOD"))){ 41 if(strcasecmp(method, "GET") == 0){ 42 if((query_string=getenv("QUERY_STRING"))){ 43 string_arg = query_string; 44 } 45 }else{ 46 if(getenv("CONTENT_LENGTH")){ 47 content_len = atoi(getenv("CONTENT_LENGTH")); 48 int i = 0; 49 for(; i < content_len; i++){ 50 read(0, &buf[i], 1); 51 } 52 buf[i] = '\0'; 53 string_arg = buf; 54 } 55 } 56 } 57 58 mymath(string_arg); 59 return 0; 60 }
(2)HTML
本项目中只是使用了一些基本的HTML知识,下面是简单的 index.html :
1 2 hello http 3 4Hello My Web!
5 "imag/mgh.jpg" alt="default" width="100" height="100"> 6 "cgi-bin/select_cgi">select 7 16 17
到这里就基本可以访问网页信息了:
4、本机进行环回测试,用的IP是127.0.0.1,Http协议的TCP连接默认端口号为80:
图片自己选择,此页面实现的是两个数的加减乘除,当点击submit时跳转页面如下:
此时跳转到cgi_bin目录下的可执行文件debug_cgi,显示加减乘除的结果。
一个简陋的http服务器就完成了。后面还需要一些其它的扩展,再更新……
5、遇到的一些问题:
1)本地环回测试ok,Linux下的浏览器测试也可以,但不能接外部的浏览器访问(没有设置桥接模式)嗯~要是在外部浏览器测试的话千万别忘记关闭防火墙。
解决:切换超级用户:$service iptables stop
2)服务器应答时,没有将html格式的页面发送,而是将底层的实现代码展示在浏览器,并且在调试时将本来要打印的调试信息会打印到网页上(在回应空行时将send期望发送的数值写的太大,本来只需要发送两个字节的内容)
解决:先检查代码,思路正确,在容易出现问题的地方加入调试信息,最后将问题定位在echo_www()函数内 。
3)不能显示图片(这个问题是没有将所有发送的情况考虑完全,只考虑到目录、可执行程序,但没有考虑到如果请求的是一个路径明确的普通文件)
解决:测试请求一个路径明确的test.html文件,加入调试信息 ,将问题定位在:如果请求的资源存在,应该如何处理。对于普通文件,找到后并回显给浏览器;如果是目录,应答的是默认页面;如果是可执行程序,执行后返回结果
4)能显示图片后,但显示的不完整(原因:echo_www中,期望读取一行信息的line值太小,不能存下一张图片)
5)运行cgi模式时,每次提交数据并进行submit后都会自动出现提醒下载的页面
原因:在响应报头中,将Content-Type中的”text”写成”test”。而浏览器对于不能识别或解析的实体,都会提醒用户下载。