一 介绍
Tinyhttpd是一个非常轻量级的http sever。代码不超过一千行。麻雀虽小,五脏俱全。反正我看完之后觉得很是畅快,收获很大。细心研究一下会对linux网络编程,http协议等概念有新的认识。
源码下载地址:
http://sourceforge.net/projects/tinyhttpd/
二 关于CGI
CGI要单独说一下,这是整个源码的核心,也是比较难理解的地方。
我们通过浏览器访问一个网站,会发送http请求给http 服务器,如果请求的是一个静态的页面或图片,服务器会直接返回结果给浏览器。但如果要完成一个动态的请求,比如需要查询数据库这样的操作,服务器会运行一个单独的程序来执行,这个程序处理完成后会把结果转化为服务器(或者浏览器)可以识别的格式输出。
这样的程序就是CGI程序,因为它一般都是以脚本的形式存在的,所以也叫CGI脚本。
CGI现在很少使用了,目前主流的webserver技术大多基于JSP。CGI的主要问题一是效率比较低,毕竟它是作为一个独立的进程被调用的。另一个是CGI的实现是依赖于服务器所用的操作系统,移植性差。
三 源码分析
如果你对http的协议相关内容不了解,开始阅读之前,建议先自行学习一下。本文只关注代码,不会对协议做过多的介绍。
第一部分客户端
客户端程序比较简单,创建一个socket对象连接服务器,然后读写一个字符:
- int main(int argc, char *argv[])
- {
- intsockfd;
- intlen;
- struct sockaddr_in address;
- intresult;
- charch = 'A';
- sockfd = socket(AF_INET, SOCK_STREAM, 0);
- address.sin_family = AF_INET;
- address.sin_addr.s_addr =inet_addr("127.0.0.1");
- address.sin_port = htons(49590);
- len= sizeof(address);
- result = connect(sockfd, (struct sockaddr*)&address, len);
- if(result == -1)
- {
- perror("oops: client1");
- exit(1);
- }
- write(sockfd, &ch, 1);
- read(sockfd,&ch, 1);
- printf("char from server = %c\n",ch);
- close(sockfd);
- exit(0);
- }
第二部分服务器
Main函数,
- int main(void)
- {
- intserver_sock = -1;
- u_short port = 0;
- intclient_sock = -1;
- struct sockaddr_in client_name;
- intclient_name_len = sizeof(client_name);
- pthread_t newthread;
- server_sock = startup(&port);
- printf("httpd running on port %d\n",port);
- printf("httpd server_sock: %d\n",server_sock);
- while (1)
- {
- /*套接字收到客户端连接请求*/
- client_sock = accept(server_sock,
- (struct sockaddr*)&client_name,
- (socklen_t*)&client_name_len);
- printf("httpd client_sock: %d\n", client_sock);
- if(client_sock == -1)
- error_die("accept");
- /*派生新线程用accept_request 函数处理新请求*/
- /*accept_request(client_sock); */
- if(pthread_create(&newthread , NULL, (void *)accept_request, client_sock) !=0)
- perror("pthread_create");
- }
- close(server_sock);
- return(0);
- }
程序创建一个服务器socket对象,然后绑写(bind),并监听指定的端口(listen),这些动作都是在startup函数实现的,
- int startup(u_short *port)
- {
- inthttpd = 0;
- struct sockaddr_in name;
- httpd = socket(PF_INET, SOCK_STREAM, 0);
- if(httpd == -1)
- error_die("socket");
- memset(&name, 0, sizeof(name));
- name.sin_family = AF_INET;
- name.sin_port = htons(*port);
- name.sin_addr.s_addr = htonl(INADDR_ANY);
- if(bind(httpd, (struct sockaddr *)&name, sizeof(name)) < 0)
- error_die("bind");
- if(*port == 0) /* if dynamicallyallocating a port */
- {
- intnamelen = sizeof(name);
- if(getsockname(httpd, (struct sockaddr *)&name, (socklen_t *)&namelen) ==-1)
- error_die("getsockname");
- *port = ntohs(name.sin_port);
- }
- if(listen(httpd, 5) < 0)
- error_die("listen");
- return(httpd);
- }
接下来进入循环,服务器通过调用accept等待客户端的连接,Accept会以阻塞的方式运行,直接有客户端连接才会返回。连接成功后,服务器启动一个新的线程来处理客户端的请求(accept_request),处理完成后,重新等待新的客户端请求。
核心函数是accept_request,它的实现如下,
- numchars = get_line(client, buf,sizeof(buf));
- i =0; j = 0;
- while (!ISspace(buf[j]) && (i <sizeof(method) - 1))
- {
- method[i] = buf[j];
- i++; j++;
- }
- method[i] = '\0';
一个HTTP请求报文由请求行(requestline)、请求头部(header)、空行和请求数据4个部分组成 ,请求行由请求方法字段(get或post)、URL字段和HTTP协议版本字段3个字段组成,它们用空格分隔。例如,
GET /index.html HTTP/1.1。
上面这段代码就是解析请求行,把方法字段保存在method变量中。关于http协议的方法有不明白的可以自已查下。
继续看,
- if (strcasecmp(method, "GET")&& strcasecmp(method, "POST"))
- {
- unimplemented(client);
- return NULL;
- }
- i =0;
- while (ISspace(buf[j]) && (j <sizeof(buf)))
- j++;
- while (!ISspace(buf[j]) && (i <sizeof(url) - 1) && (j < sizeof(buf)))
- {
- url[i] = buf[j];
- i++; j++;
- }
- url[i] = '\0'; //保存url
上面这一段代码解析并保存请求的URL(如有问号,也包括问号及之后的内容,后面会讲到)。
继续看,
- if(strcasecmp(method, "GET") == 0)
- {
- query_string = url;
- while ((*query_string != '?') && (*query_string != '\0'))
- query_string++;
- if(*query_string == '?')
- {
- ///*开启 cgi */
- cgi = 1;
- *query_string = '\0';
- query_string++;
- }
- }
其中10023就是要传递的参数。这段代码把参数保存在query_string中。
下面这段代码保存有效的url地址并加上请求地址的主页索引。默认的根目录是在htdocs下。
- sprintf(path, "htdocs%s", url);//如果有问号,取url问号前面的部分
- if(path[strlen(path) - 1] == '/')
- strcat(path, "index.html");
接下来的代码,访问请求的文件,如果文件不存在直接返回,如果存在就调用CGI程序来处理。
- if(stat(path, &st) == -1)
- {
- while ((numchars > 0) && strcmp("\n", buf)) /* read & discard headers */
- numchars = get_line(client, buf, sizeof(buf));
- not_found(client);
- }
- else
- {
- if((st.st_mode & S_IFMT) == S_IFDIR)//目录类型
- strcat(path, "/index.html");
- if ((st.st_mode & S_IXUSR) ||
- (st.st_mode & S_IXGRP) ||
- (st.st_mode & S_IXOTH) )
- cgi = 1;
- printf("accept_request cgi:%d\n", cgi);
- if(!cgi) serve_file(client, path);
- else
- execute_cgi(client, path, method, query_string);//query_string }
- close(client);
- return NULL;
- }
如果需要调用cgi(cgi标志位置1)在调用cgi之前有一段是对用户权限的判断,对应的含义如下:
- S_IXUSR:用户可以执行
- S_IXGRP:组可以执行
- S_IXOTH:其它人可以执行
先来看看serve_file函数,它返回一个静态文本,比较简单。
- buf[0] = 'A'; buf[1] = '\0';
- while ((numchars > 0) &&strcmp("\n", buf)) /* read& discard headers */
- numchars = get_line(client, buf, sizeof(buf));
这里是继续读完客户端发来的请求头(前面已经读完了请求行),最后一个请求头之后是一个空行,通过换行符可以读完所有的请求头。成功打开文件之后,先组织http响应报文的头部,headers函数处理,
- void headers(int client, const char*filename)
- {
- charbuf[1024];
- (void)filename; /* could use filename to determine file type*/
- strcpy(buf, "HTTP/1.0 200 OK\r\n");//status line
- send(client, buf, strlen(buf), 0);
- strcpy(buf, SERVER_STRING);//server header
- send(client, buf, strlen(buf), 0);
- sprintf(buf, "Content-Type:text/html\r\n");
- send(client, buf, strlen(buf), 0);
- strcpy(buf, "\r\n");
- send(client, buf, strlen(buf), 0);
- }
接下来调用cat函数,把文件中读到的内容作为http响应报文的数据部分发送回客户端:
- void cat(int client, FILE *resource)
- {
- charbuf[1024] = {0};
- fgets(buf, sizeof(buf), resource);
- while (!feof(resource))
- {
- send(client, buf, strlen(buf), 0);
- fgets(buf, sizeof(buf), resource);
- }
- }
未完待续。