转载请注明出处
http://blog.csdn.net/pony_maggie/article/details/49838107
作者:小马
一 介绍
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);
}
有一个细节稍微注意一下,如果传来的端口是0,程序会随机分配一个监听的端口。并通过指向port变量的地址返回该值。
接下来进入循环,服务器通过调用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;
}
只能识别get或post
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++;
}
}
如果是get方法,请求参数和对应的值附加在URL后面, 利用一个问号(“?”)代表URL的结尾与请求参数的开始,传递参数长度受限制。例如,/index.jsp?10023
其中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);
}
}
未完待续。
下一篇地址:
http://blog.csdn.net/pony_maggie/article/details/49838191