TinyHTTPd是一个超轻量型Http Server,使用C语言开发,全部代码不到600行,附带一个简单的Client,可以通过阅读这段代码理解一个Http Server的本质。源码下载链接http://sourceforge.net/projects/tinyhttpd/
分析这段源码前,需要对网络协议,Unix编程,以及HTTP有一定的了解,这里假设大家对http有一定的了解,如果有时间,会额外介绍下Http。
本文先全篇分析下该开源项目的源码,最后给出测试。
服务器端代码:httpd.c
建议源码阅读顺序:main —> startup —> accept_request —> excute_cgi
TinyHTTPd 项目流程图
先介绍几个中间辅助函数:(完整代码参见前面源码链接)
从客户端读取一行数据,以\r或\r\n为行结束符
/**********************************************************************/
/* Get a line from a socket, whether the line ends in a newline,
* carriage return, or a CRLF combination. Terminates the string read
* with a null character. If no newline indicator is found before the
* end of the buffer, the string is terminated with a null. If any of
* the above three line terminators is read, the last character of the
* string will be a linefeed and the string will be terminated with a
* null character.
* Parameters: the socket descriptor
* the buffer to save the data in
* the size of the buffer
* Returns: the number of bytes stored (excluding null) */
/**********************************************************************/
/**********************************************************************/
/* 从socket读取一行数据。以\r或\r\n为行结束符
* Parameters: the socket descriptor
* the buffer to save the data in
* the size of the buffer
* Returns: the number of bytes stored (excluding null) */
/**********************************************************************/
int get_line(int sock, char *buf, int size)
{
int i = 0;
char c = '\0';
int n;
//至多读取size-1个字符,最后一个字符置'\0'
while ((i < size - 1) && (c != '\n'))
{
n = recv(sock, &c, 1, 0);//单个字符接收
if (n > 0)
{
if (c == '\r')//如果是回车符,继续读取
{
/*使用 MSG_PEEK 标志使下一次读取依然可以得到这次读取的内容,可认为接收窗口不滑动*/
n = recv(sock, &c, 1, MSG_PEEK);
if ((n > 0) && (c == '\n'))//如果是回车换行符
recv(sock, &c, 1, 0);//继续接收单个字符,实际上和上面那个标志位MSG_PEEK读取同样的字符,读完后删除输入队列的数据,即滑动窗口,c=='\n'
else
c = '\n';//只是读取到回车符,则置为换行符,也终止了读取
}
buf[i] = c;//放入缓冲区
i++;
}
else//没有读取到任何数据
c = '\n';
}
buf[i] = '\0';
return(i);//返回读到的字符个数(包括'\0')
}
请求出错情况处理
/**********************************************************************/
/* 告知客户端该请求有错误 400
* Parameters: client socket */
/**********************************************************************/
void bad_request(int client)
{
char buf[1024];
/*将字符串存入缓冲区,再通过send函数发送给客户端*/
sprintf(buf, "HTTP/1.0 400 BAD REQUEST\r\n");
send(client, buf, sizeof(buf), 0);
sprintf(buf, "Content-type: text/html\r\n");
send(client, buf, sizeof(buf), 0);
sprintf(buf, "\r\n");
send(client, buf, sizeof(buf), 0);
sprintf(buf, "<P>Your browser sent a bad request, ");
send(client, buf, sizeof(buf), 0);
sprintf(buf, "such as a POST without a Content-Length.\r\n");
send(client, buf, sizeof(buf), 0);
}
/**********************************************************************/
/* 通知客户端CGI脚本不能被执行 500
* Parameter: the client socket descriptor. */
/**********************************************************************/
void cannot_execute(int client)
{
char buf[1024];
/*回馈出错信息*/
sprintf(buf, "HTTP/1.0 500 Internal Server Error\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, "Content-type: text/html\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, "\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, "<P>Error prohibited CGI execution.\r\n");
send(client, buf, strlen(buf), 0);
}
/**********************************************************************/
/* 打印出错信息,详见《Unix 环境高级编程》并终止*/
/**********************************************************************/
void error_die(const char *sc)
{
perror(sc);
exit(1);
}
/**********************************************************************/
/* 返回客户端404错误信息 404(万恶的404) */
/**********************************************************************/
void not_found(int client)
{
char buf[1024];
sprintf(buf, "HTTP/1.0 404 NOT FOUND\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, SERVER_STRING);
send(client, buf, strlen(buf), 0);
sprintf(buf, "Content-Type: text/html\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, "\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, "<HTML><TITLE>Not Found</TITLE>\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, "<BODY><P>The server could not fulfill\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, "your request because the resource specified\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, "is unavailable or nonexistent.\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, "</BODY></HTML>\r\n");
send(client, buf, strlen(buf), 0);
}
读取文件中的数据到client
/**********************************************************************/
/* Put the entire contents of a file out on a socket. This function
* is named after the UNIX "cat" command, because it might have been
* easier just to do something like pipe, fork, and exec("cat").
* Parameters: the client socket descriptor
* FILE pointer for the file to cat */
/*Unix shell 命令cat file 即打印文件file中的数据*/
/**********************************************************************/
/*将文件结构指针resource中的数据发送至client*/
void cat(int client, FILE *resource)
{
char buf[1024];
fgets(buf, sizeof(buf), resource);//从文件结构指针resource中读取数据,保存至buf中
//处理文件流中剩下的字符
while (!feof(resource))//检测流上的文件结束符,文件结束返回非0值,结束返回0
{
send(client, buf, strlen(buf), 0);//文件流中的字符全部发送给client
fgets(buf, sizeof(buf), resource);/*从文件结构体指针resource中读取至多bufsize-1个数据
(第bufsize个字符赋'\0')每次读取一行,如果不足bufsize,
则读完该行结束。这里通过feof函数来判断fgets是否因出错而终止
另外,这里有文件偏移位置,下一轮读取会从上一轮读取完的位置继续*/
}
}
返回文件信息给client
/**********************************************************************/
/* Return the informational HTTP headers about a file. */
/* Parameters: the socket to print the headers on
* the name of the file */
/*返回文件头部信息*/
/**********************************************************************/
void headers(int client, const char *filename)
{
char buf[1024];
(void)filename; /* could use filename to determine file type */
strcpy(buf, "HTTP/1.0 200 OK\r\n");
send(client, buf, strlen(buf), 0);
strcpy(buf, SERVER_STRING);
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);
}
/**********************************************************************/
/* Send a regular file to the client. Use headers, and report
* errors to client if they occur.
* Parameters: a pointer to a file structure produced from the socket
* file descriptor
* the name of the file to serve */
/*返回文件数据,用于静态页面返回*/
/**********************************************************************/
void serve_file(int client, const char *filename)
{
FILE *resource = NULL;
int numchars = 1;
char buf[1024];
buf[0] = 'A'; buf[1] = '\0';
while ((numchars > 0) && strcmp("\n", buf)) /* read & discard headers */
numchars = get_line(client, buf, sizeof(buf));
resource = fopen(filename, "r");//只读方式打开文件
if (resource == NULL)
not_found(client);//如果文件不存在,返回404错误
else
{
headers(client, filename);//先返回文件头部信息
cat(client, resource);//将resource描述符指定文件中的数据发送给client
}
fclose(resource);//关闭
}
下面就是tynyhttpd服务器端的核心代码部分。
为了更好地理解源码,这里提出http的请求报文格式
服务器端套接字初始化设置
/**********************************************************************/
/* This function starts the process of listening for web connections
* on a specified port. If the port is 0, then dynamically allocate a
* port and modify the original port variable to reflect the actual
* port.
* Parameters: pointer to variable containing the port to connect on
* Returns: the socket */
/**********************************************************************/
/*服务器端套接字初始化设置*/
int startup(u_short *port)
{
int httpd = 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 dynamically allocating a port *///动态分配一个端口
{
int namelen = sizeof(name);
/*在以端口号0调用bind后,getsockname用于返回由内核赋予的本地端口号*/
if (getsockname(httpd, (struct sockaddr *)&name, &namelen) == -1)
error_die("getsockname");
*port = ntohs(name.sin_port);//网络字节顺序转换为主机字节顺序,返回主机字节顺序表达的数
}
if (listen(httpd, 5) < 0)//服务器监听客户端请求。套接字排队的最大连接个数5
error_die("listen");
return(httpd);
}
接收客户端的请求报文
/**********************************************************************/
/* A request has caused a call to accept() on the server port to
* return. Process the request appropriately.
* Parameters: the socket connected to the client */
/**********************************************************************/
/**********************************************************************/
/* HTTP协议规定,请求从客户端发出,最后服务器端响应该请求并返回。
* 这是目前HTTP协议的规定,服务器不支持主动响应,所以目前的HTTP
* 协议版本都是基于客户端请求,然后响应的这种模型。 */
/*accept_request函数解析客户端请求,判断是请求静态文件还是cgi代码
(通过请求类型以及参数来判定),如果是静态文件则将文件输出给前端,
如果是cgi则进入cgi处理函数*/
/**********************************************************************/
void accept_request(int client)
{
char buf[1024];
int numchars;
char method[255];//请求方法GET or POST
char url[255];//请求的文件路径
char path[512];//文件相对路径
size_t i, j;
struct stat st;
int cgi = 0; /* becomes true if server decides this is a CGI
* program */
char *query_string = NULL;
numchars = get_line(client, buf, sizeof(buf));//从client中读取指定大小数据到buf
i = 0; j = 0;
//解析客户端的http请求报文
/*接收字符处理:提取空格字符前的字符,至多254个*/
while (!ISspace(buf[j]) && (i < sizeof(method) - 1))
{
method[i] = buf[j];//根据http请求报文格式,这里得到的是请求方法
i++; j++;
}
method[i] = '\0';
//忽略大小写比较字符串,用于判断是哪种类型
if (strcasecmp(method, "GET") && strcasecmp(method, "POST"))
{
unimplemented(client);//两种method都不是,告知客户端所请求的方法未能实现
return;
}
if (strcasecmp(method, "POST") == 0)//POST 类型
cgi = 1;//设置标志位
i = 0;
while (ISspace(buf[j]) && (j < sizeof(buf)))//过滤空格字符,空格后面是URL
j++;
/*将buf中的非空格字符转存进url缓冲区,遇空格字符或满退出*/
while (!ISspace(buf[j]) && (i < sizeof(url) - 1) && (j < sizeof(buf)))
{
url[i] = buf[j];//获取的是URL(互联网标准资源的地址)
i++; j++;
}
url[i] = '\0';
if (strcasecmp(method, "GET") == 0)//GET method
{
query_string = url;//请求信息
while ((*query_string != '?') && (*query_string != '\0'))//截取'?'前的字符
query_string++;//问号前面是路径,后面是参数
if (*query_string == '?')//有'?',表明动态请求
{
cgi = 1;
*query_string = '\0';
query_string++;
}
}
//下面是TinyHTTPd项目htdocs文件下的文件
sprintf(path, "htdocs%s", url);//获取请求文件路径
if (path[strlen(path) - 1] == '/')//如果文件类型是目录(/),则加上index.html
strcat(path, "index.html");//
//根据路径找文件,并获取path文件信息保存到结构体st中
if (stat(path, &st) == -1) {//执行失败,文件未找到
/*丢弃所有 headers 的信息*/
while ((numchars > 0) && strcmp("\n", buf)) /* read & discard headers */
numchars = get_line(client, buf, sizeof(buf));//从客户端读取数据进buf
not_found(client);//回应客户端找不到
}
else//获取文件信息,执行成功
{
/*如果是个目录,则默认使用该目录下 index.html 文件*/
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;
if (!cgi)//静态页面请求
serve_file(client, path);//直接返回文件信息给客户端,静态页面返回
else//动态页面请求
execute_cgi(client, path, method, query_string);//执行cgi脚本
}
close(client);//关闭客户端套接字
}
执行CGI脚本,动态页面申请
/**********************************************************************/
/* 执行CGI(公共网卡接口)脚本,需要设定合适的环境变量
* Parameters: client socket descriptor
* path to the CGI script */
/*execute_cgi函数负责将请求传递给cgi程序处理,
服务器与cgi之间通过管道pipe通信,首先初始化两个管道,并创建子进程去执行cgi函数*/
/*子进程执行cgi程序,获取cgi的标准输出通过管道传给父进程,由父进程发送给客户端*/
/**********************************************************************/
void execute_cgi(int client, const char *path,
const char *method, const char *query_string)
{
char buf[1024];
int cgi_output[2];
int cgi_input[2];
pid_t pid;
int status;
int i;
char c;
int numchars = 1;
int content_length = -1;
buf[0] = 'A'; buf[1] = '\0';
if (strcasecmp(method, "GET") == 0)//GET方法:一般用于获取/查询资源信息
while ((numchars > 0) && strcmp("\n", buf)) /* read & discard headers读取并丢弃 HTTP 请求 */
numchars = get_line(client, buf, sizeof(buf));//从客户端读取
else /* POST 一般用于更新资源信息*/
{
numchars = get_line(client, buf, sizeof(buf));
//获取HTTP消息实体的传输长度
while ((numchars > 0) && strcmp("\n", buf))//不为空且不为换行符
{
buf[15] = '\0';
if (strcasecmp(buf, "Content-Length:") == 0)//是否为Content-Length字段
content_length = atoi(&(buf[16]));//Content-Length用于描述HTTP消息实体的传输长度
numchars = get_line(client, buf, sizeof(buf));
}
if (content_length == -1) {
bad_request(client);//请求的页面数据为空,没有数据,就是我们打开网页经常出现空白页面
return;
}
}
sprintf(buf, "HTTP/1.0 200 OK\r\n");//
send(client, buf, strlen(buf), 0);
//建立管道,两个通道cgi_output[0]:读取端,cgi_output[1]:写入端
if (pipe(cgi_output) < 0) {
cannot_execute(client);//管道建立失败,打印出错信息
return;
}//管道只能具有公共祖先的进程间进行,这里是父子进程之间
if (pipe(cgi_input) < 0) {
cannot_execute(client);
return;
}
//fork子进程,这样就创建了父子进程间的IPC通道
if ((pid = fork()) < 0) {
cannot_execute(client);
return;
}
//实现进程间的管道通信机制
/*子进程继承了父进程的pipe,然后通过关闭子进程output管道的输出端,input管道的写入端;
关闭父进程output管道的写入端,input管道的输出端*/
//子进程,
if (pid == 0) /* child: CGI script */
{
char meth_env[255];
char query_env[255];
char length_env[255];
//复制文件句柄,重定向进程的标准输入输出
//dup2的第一个参数描述符关闭
dup2(cgi_output[1], 1);//标准输出重定向到output管道的写入端
dup2(cgi_input[0], 0);//标准输入重定向到input管道的读取端
close(cgi_output[0]);//关闭output管道的写入端
close(cgi_input[1]);//关闭输出端
sprintf(meth_env, "REQUEST_METHOD=%s", method);
putenv(meth_env);
if (strcasecmp(method, "GET") == 0) {//GET
/*设置 query_string 的环境变量*/
sprintf(query_env, "QUERY_STRING=%s", query_string);
putenv(query_env);
}
else { /* POST */
/*设置 content_length 的环境变量*/
sprintf(length_env, "CONTENT_LENGTH=%d", content_length);
putenv(length_env);
}
execl(path, path, NULL);//exec函数簇,执行CGI脚本,获取cgi的标准输出作为相应内容发送给客户端
//通过dup2重定向,标准输出内容进入管道output的输入端
exit(0);//子进程退出
}
else { /* parent */
close(cgi_output[1]);//关闭管道的一端,这样可以建立父子进程间的管道通信
close(cgi_input[0]);
/*通过关闭对应管道的通道,然后重定向子进程的管道某端,这样就在父子进程之间构建一条单双工通道
如果不重定向,将是一条典型的全双工管道通信机制
*/
if (strcasecmp(method, "POST") == 0)//POST方式,将指定好的传输长度字符发送
/*接收 POST 过来的数据*/
for (i = 0; i < content_length; i++) {
recv(client, &c, 1, 0);//从客户端接收单个字符
write(cgi_input[1], &c, 1);//写入input,然后重定向到了标准输入
//数据传送过程:input[1](父进程) ——> input[0](子进程)[执行cgi函数] ——> STDIN ——> STDOUT
// ——> output[1](子进程) ——> output[0](父进程)[将结果发送给客户端]
}
while (read(cgi_output[0], &c, 1) > 0)//读取output的管道输出到客户端,output输出端为cgi脚本执行后的内容
send(client, &c, 1, 0);//即将cgi执行结果发送给客户端,即send到浏览器,如果不是POST则只有这一处理
close(cgi_output[0]);//关闭剩下的管道端,子进程在执行dup2之后,就已经关闭了管道一端通道
close(cgi_input[1]);
waitpid(pid, &status, 0);//等待子进程终止
}
}
上面父子进程间的管道通信可以用下图表示:父子进程各司其职,分工合作,通过管道建立通信通道。
上面最终完整状态是 POST 方式,如果不是 POST 方式,则只有 output[0] ——> 客户端。
上面即服务器端的程序:这里简单罗列一下:
上面即服务器端的程序:这里简单罗列一下:
#define ISspace(x) isspace((int)(x))//若x为空格字符,返回true
#define SERVER_STRING "Server: jdbhttpd/0.1.0\r\n"
void accept_request(int);//客户端向服务器端发送请求
void bad_request(int);//告诉客户端请求出错,400
void cat(int, FILE *);//读取文件并发送给客户端
void cannot_execute(int);//通知客户端不能执行CGI脚本(perl)
void error_die(const char *);//打印出错信息
void execute_cgi(int, const char *, const char *, const char *);//执行CGI脚本,内部调用exec函数簇
int get_line(int, char *, int);//从套接字读取数据,返回读取到的字符个数
void headers(int, const char *);//返回HTTP头文件信息
void not_found(int);//通知客户端页面未找到,404
void serve_file(int, const char *);//发送消息给客户端,用于静态页面返回
int startup(u_short *);//服务器端套接字设置,创建,绑定,监听(TCP协议)
void unimplemented(int);//通知客户端所请求的网络方法没有实现(GET、POST)
下面这个就是服务器端的main.c
int main(void)
{
int server_sock = -1;
u_short port = 0;//传入的端口为0,
int client_sock = -1;
struct sockaddr_in client_name;
int client_name_len = sizeof(client_name);
pthread_t newthread;
server_sock = startup(&port);//服务器端监听套接字设置
printf("httpd running on port %d\n", port);
/*多线程并发服务器模型*/
while (1)
{
//主线程
client_sock = accept(server_sock,
(struct sockaddr *)&client_name,
&client_name_len);//阻塞等待客户端连接请求
if (client_sock == -1)
error_die("accept");
/* accept_request(client_sock); */
if (pthread_create(&newthread , NULL, accept_request, client_sock) != 0)//创建工作线程,执行回调函数accept_request,参数client_sock
perror("pthread_create");
}
close(server_sock);//关闭套接字,就协议栈而言,即关闭TCP连接
return(0);
}
从上面我们可以的出Tinyhttp的工作流程:
- 服务器启动,指定端口或随机选取端口绑定httpd服务,监听客户端的连接请求。(startup 函数)
- 收到客户端的 HTTP 请求,派生一个线程去相应客户端请求(多线程服务器模型),即执行 accept_request 函数
- 服务器端解析客户端 HTTP 请求报文。判断是何种 method (GET or POST)以及获取 url。对于 GET 方法,如果携带参数,则 query_string 指针指向 url 中 ? 后面的 GET 参数(http 协议)
- 拷贝 url 数据到 path数组,表示浏览器请求的服务器文件路径,在 tinyhttpd 中服务器文件是在 htdocs 文件夹下,若 url 以 / 结尾,或 url 本身是个目录(stat 文件信息),则默认在 path 中加上 index.html,表示访问主页。
- 在文件路径合法的前提下,如果是静态页面访问 ,直接输出服务器文件到浏览器,即用 HTTP 格式写到客户端套接字上,然后跳到。如果是动态页面申请(带?的GET方式,POST方式,utl 为可执行文件),则转调用 excute_cgi 函数执行cgi脚本。
- 读取整个 HTTP 请求并丢弃,如果是 POST 则找出Content-Length。把"HTTP/1.0 200 OK\r\n" 状态码写到套接字。
- 建立两个管道,cgi_input 和 cgi_output ,并 fork 一个进程(必须 fork 子进程,pipe 管道才有意义)。建立父子进程间的通信机制。
- 在子进程中,对其进程下的管道进行重定向,并设置对应的环境变量(method、query_string、content_length),这些环境变量都是为了给 cgi 脚本调用,接着用 execl 运行 cgi 脚本,可以看出 cgi 脚本的执行在子进程中进行,然后结果通过管道以及重定向返回给父进程。
- 父进程中,关闭管道一端,如果是 POST 方式,则把 POST 数据写入 cgi_intput,已被重定向到 STDIN,读取 cgi_output 管道输出到客户端(浏览器输出),具体流程图参见上面的管道最终状态图。接着关闭所有管道,等待子进程结束。
- 关闭连接,完成一次 HTTP 请求与回应。
HTTP 是无连接的,在进行 Web 应用前无须建立专门的 HTTP 应用层会话连接,仅需要直接利用传输层已为它建立好的 TCP 传输连接即可。即虽然是不可靠的无连接协议,但使用可可靠的 TCP 传输层协议,所以从数据传输角度来讲,HTTP 的报文传输仍是可靠的。
值得说明的是,这个项目是不能直接在Linux环境下编译运行的,它本来是在Solaris上实现的,需要修改几处地方,由于篇幅问题,下一篇TinyHTTPd 在Linux 下编译 给出修改地方以及最后运行测试结果。
如果错误,欢迎指出,交流进步,谢谢。