TinyHttpd分析
HTTP协议请求格式
标准POST请求格式:
GET / HTTP/1.1
Host: 192.168.0.23:47310
Connection: keep-alive
...
标准GET请求格式:
POST / color1.cgi HTTP / 1.1
Host: 192.168.0.23 : 47310
Connection : keep - alive
Content - Length : 10
...
Form Data
color=gray
TinyHttpd源码分析
源码地址:
https:/EZLippi/Tinyhttpd
首先是TinyHttpd包含的函数:
void accept_request(void *);//处理从套接字上监听到的一个HTTP请求
void bad_request(int);//返回给客户端这是一个错误请求,400响应码
void cat(int, FILE *);//读取服务器上某个文件写到socket套接字
void cannot_execute(int);//处理发生在执行cgi程序时出现的错误
void error_die(const char *);//把错误信息写到 perror perror(用来将上一个函数发生错误的原因输出到标准设备stderr)
void execute_cgi(int, const char *, const char *, const char *);//运行cgi脚本
int get_line(int, char *, int);//读取一行HTTP报文
void headers(int, const char *);//返回HTTP响应头
void not_found(int);//返回找不到请求文件
void serve_file(int, const char *);//调用cat将服务器文件内容返回给浏览器
int startup(u_short *);//开启http服务,绑定端口,监听,开启线程处理链接
void unimplemented(int);//返回给浏览器 表明收到的HTTP请求所用的method(方法)不被支持
main
套接字:socket,对网络中不同主机上的应用进程之间进行双向通信的端点的抽象
int main(void)
{
int server_sock = -1; //定义服务器socket的描述符
u_short port = 4000; //定义监听端口
int client_sock = -1; //定义客户端socket的描述符
//定义了一个结构体
//sockaddr_in是ipv4的套接字地址结构
struct sockaddr_in client_name;
socklen_t client_name_len = sizeof(client_name);//客户端地址长度
pthread_t newthread; //定义线程id
server_sock = startup(&port); //初始化服务器
printf("httpd running on port %d\n", port); //控制台打印端口
//循环创建链接和子线程
//该循环的作用就是提供服务,等待与客户端建立链接
while (1)
{
//通过accept接收客户端请求
//阻塞,等待客户端连接
//&client_name是客户端的地址信息
//client_name_len是客户端地址长度
client_sock = accept(server_sock,
(struct sockaddr *)&client_name,
&client_name_len);
/*
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
accept函数接收了一个套接字的文件描述符,以及ipv4地址结构,返回一个新的套接字
通过该套接字是实现与客户端通信
注意点:
1、该函数若没有connect请求,函数调用会被阻塞,直到接收到connect请求
2、在与客户端的套接字进行连接后,accept会创建一个新的套接字,并使用新的套接字与客户端进行连接
,原始的套接字并不会关闭,依然打开,并可以使用于监听端口
*/
if (client_sock == -1)
error_die("accept");
/* accept_request(&client_sock); */
//如果有请求,就通过pthread_create建立一个新的线程
//在新线程中调用accept_request函数,处理请求,实现多线程同步服务(提高效率)
//&newthread是线程ID,NULL是线程属性
//accept_request是线程要执行的函数
//client_sock强制转换成void *
if (pthread_create(&newthread , NULL, (void *)accept_request, (void *)(intptr_t)client_sock) != 0)
perror("pthread_create");
}
close(server_sock);
return(0);
}
main函数处理流程:startup初始化一个和4000端口绑定的套接字==》进入循环,accept接收客户端请求==》通过accept_create创建线程,调用accept_request处理请求
startup
int startup(u_short *port)
{
int httpd = 0; //定义服务器的socket描述符
int on = 1;
struct sockaddr_in name; //服务器端的IP地址
httpd = socket(PF_INET, SOCK_STREAM, 0); //创建一个套接口(服务器端的socket)
if (httpd == -1)
error_die("socket");
memset(&name, 0, sizeof(name));//结构体初始化为0
//结构体的三个成员变量
name.sin_family = AF_INET; //地址类型指定为ipv4
name.sin_port = htons(*port); //传入的端口,转化为网络字序节(大端)
name.sin_addr.s_addr = htonl(INADDR_ANY); //本机可用的ip地址
/*
int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);
setsockopt:设置任意类型,任意状态套接口的选项值
第一个参数为套接字描述符
第二个参数为设置的选项的级别,若想在套接字上设置选项,则必须设置为SOL_SOCKET
第三个参数为要设置的选项
第四个参数为指针,指向存放选项待设置的新值的缓冲区
第五个参数为缓冲区长度
*/
//此处是将套接字设置为SO_REUSEADDR选项,实现允许复用本地地址和端口
if ((setsockopt(httpd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on))) < 0)
{
error_die("setsockopt failed");
}
//bind:将本地地址与一个套接口进行绑定
//若传入的name中的成员变量sin_port为0,此时系统会选择一个临时端口
if (bind(httpd, (struct sockaddr *)&name, sizeof(name)) < 0)
error_die("bind");
//若在bind后端口依旧为0
if (*port == 0) /* if dynamically allocating a port */
{
socklen_t namelen = sizeof(name);
//调用getsockname()获取系统给httpd随机分配端口号
if (getsockname(httpd, (struct sockaddr *)&name, &namelen) == -1)
error_die("getsockname");
*port = ntohs(name.sin_port);
}
//调用listen函数监听request
//第二个参数代表请求队列的长度
//请求队列:当套接字在处理客户端请求时,若有新的请求进来,套接字无法处理,会将它放入缓冲区
//当当前请求处理完毕后再从缓冲区中读取并处理
//若有不断的新的请求进来,就将按照先后顺序在缓冲区中排队,直到缓冲区满
//该缓冲区称为请求队列,请求队列满时不再接收新的请求
if (listen(httpd, 5) < 0)
error_die("listen");
return(httpd);
}
startup函数流程:调用socket创建一个套接字==》创建sockaddr_in结构体,设置对应的IP和端口==》调用bind绑定套接字的IP和端口==》调用listen监听请求
sockaddr_in结构体
struct in_addr { /* IPv4 4-byte address */
in_addr_t s_addr; /* Unsigned 32-bit integer */
};
struct sockaddr_in { /* IPv4 socket address */
sa_family_t sin_family; /* Address family (AF_INET) */
in_port_t sin_port; /* Port number */
struct in_addr sin_addr; /* IPv4 address */
unsigned char __pad[X]; /* Pad to size of 'sockaddr'
}; structure (16 bytes) */
在in_addr中保存了以32bit的ipv4地址==》使用htonl函数将ip地址从字符串转换为整型
accept_request
void accept_request(void *arg)
{
int client = (intptr_t)arg;
char buf[1024];
size_t numchars;
char method[255];
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;
//读取http请求的第一行数据
//将请求方法放在method中
numchars = get_line(client, buf, sizeof(buf));
i = 0; j = 0;
//循环条件:
//判断第i个字符是否是空格
//判断是否超过method缓冲大小
//-1是为了在最后添加一个'\0'作为标识符
while (!ISspace(buf[i]) && (i < sizeof(method) - 1))
{
method[i] = buf[i];//若不是空格,复制到method中
i++;
}
j=i;
method[i] = '\0';//添加'\0'标识符
//判断其中是否是GET方法或者POST方法
if (strcasecmp(method, "GET") && strcasecmp(method, "POST"))
{
unimplemented(client);//都不是的话调用501错误处理
return;
}
//若是POST方法,cgi设置为1
if (strcasecmp(method, "POST") == 0)
cgi = 1;
i = 0;
while (ISspace(buf[j]) && (j < numchars))
j++;
//此处循环作用为读取URL,与读取请求方法一样
while (!ISspace(buf[j]) && (i < sizeof(url) - 1) && (j < numchars))
{
url[i] = buf[j];
i++; j++;
}
url[i] = '\0';//添加'\0'标识符
//若是GET方法
if (strcasecmp(method, "GET") == 0)
{
query_string = url;
//GET方法在URL后有'?',若找到'?',说明是GET
while ((*query_string != '?') && (*query_string != '\0'))
query_string++;
//在GET请求中,'?'后面为参数
if (*query_string == '?')
{
cgi = 1; //CGI设置为1
*query_string = '\0';
query_string++;
}
}
//与'htdocs'(根目录)拼接
sprintf(path, "htdocs%s", url);
//如果最后一个字符是'/'
if (path[strlen(path) - 1] == '/')
strcat(path, "index.html");//在后面加上 index.html
//检查拼接后的文件是否存在,-1为不存在
//stat函数:确认路径对应的文件状态
if (stat(path, &st) == -1) {
//读取并丢弃headers
while ((numchars > 0) && strcmp("\n", buf)) /* read & discard headers */
numchars = get_line(client, buf, sizeof(buf));
not_found(client); //不存在文件返回404
}
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
cgi = 1;
if (!cgi)
serve_file(client, path); //cgi为0,不需要调用cgi,相当于请求页面
else
//cgi为1,调用cgi,执行cgi程序
//参数:
//client:描述符
//path:路径
//method:请求方法
//query_string:判断是否有?以使用GET请求发送数据
execute_cgi(client, path, method, query_string);
}
close(client);//断开与客户端连接
}
accept_request函数流程:提取request类型(GET或者是POST)==》
如果是POST==》开启cgi
如果是GET==》提取URL以及URL中的参数
==》如果不是CGI,调用serve_file,返回地址中内容(页面)
==》如果是CGI,调用execute_cgi,执行CGI脚本程序
execute_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';
//判断是不是GET,若是,丢弃headers
if (strcasecmp(method, "GET") == 0)
while ((numchars > 0) && strcmp("\n", buf)) /* read & discard headers */
//丢弃
numchars = get_line(client, buf, sizeof(buf));
else if (strcasecmp(method, "POST") == 0) /*POST*/
{
numchars = get_line(client, buf, sizeof(buf));
while ((numchars > 0) && strcmp("\n", buf))
{
buf[15] = '\0';
//比较前15个字符是否为"Content-Length:"
if (strcasecmp(buf, "Content-Length:") == 0)
content_length = atoi(&(buf[16])); //记录Content-Length,int型
numchars = get_line(client, buf, sizeof(buf));
}
if (content_length == -1) { //如果不等于
bad_request(client); //错误处理
return;
}
}
else/*HEAD or other*/
{
}
//管道:为了在子线程中的cgi和服务器调用cgi程序进程间通信
//创建管道:子线程向服务器端写
if (pipe(cgi_output) < 0) {
cannot_execute(client);//创建失败
return;
}
//创建管道:子线程向服务器端读
if (pipe(cgi_input) < 0) {
cannot_execute(client);//创建失败
return;
}
//管道创建成功后创建子线程
if ( (pid = fork()) < 0 ) {
cannot_execute(client);//创建失败
return;
}
sprintf(buf, "HTTP/1.0 200 OK\r\n");
//发送200状态码
send(client, buf, strlen(buf), 0);
//PID为0,说明fork
//子进程用于执行CGI
if (pid == 0) /* child: CGI script */
{
char meth_env[255];
char query_env[255];
char length_env[255];
//将子线程的stdout重定向至cgi_output管道,1:写端
dup2(cgi_output[1], STDOUT);
//将子线程的stdin重定向至cgi_input管道,0:读端
dup2(cgi_input[0], STDIN);
//关闭不需要的另一端
close(cgi_output[0]);
close(cgi_input[1]);
//拼接
sprintf(meth_env, "REQUEST_METHOD=%s", method);
//设置环境变量
putenv(meth_env);
if (strcasecmp(method, "GET") == 0) {
sprintf(query_env, "QUERY_STRING=%s", query_string);
//设置环境变量
putenv(query_env);
}
else { /* POST */
sprintf(length_env, "CONTENT_LENGTH=%d", content_length);
//设置环境变量
putenv(length_env);
}
execl(path, NULL);//执行可执行程序
exit(0);
//父进程
} else { /* parent */
//关闭不需要的管道端口
close(cgi_output[1]);//关闭管道写端
close(cgi_input[0]);//关闭管道读端
//通过管道与子进程进行通信
if (strcasecmp(method, "POST") == 0)
//根据content_length信息读取客户端信息
for (i = 0; i < content_length; i++) {
recv(client, &c, 1, 0);//读取
write(cgi_input[1], &c, 1);//通过cgi_input传入子进程的标准输出
}
//通过cgi_output,获取子进程的标准输出
while (read(cgi_output[0], &c, 1) > 0)
send(client, &c, 1, 0);//将从输出管道中读入执行结果发送给客户端
//关闭管道端口
close(cgi_output[0]);
close(cgi_input[1]);
//等待子进程结束,退出程序
waitpid(pid, &status, 0);
}
}
execute_cgi函数执行流程:
对于GET请求==》丢弃
对于POST请求==》提取content_length信息==》创建两个管道用以通信,创建子进程==》
==》子进程==》调用dup2将标准输入与标准输出分别重定向到对应管道的读端和写端==》设置环境变量,调用execl,执行CGI脚本
==》父进程==》通过管道向CGI脚本传入参数,获取脚本返回结果==》将结果返回给客户端
管道通信原理:
使用pipe函数得到两个文件描述符,分别对应管道的读端(filedes[0])和写端(filedes[1]),当程序在写端写入数据时,在读端可以读取到写入的数据
==》通过fork创建子进程==》父进程与子进程拥有相同的变量==》
子进程也有对应管道的读端和写端两个文件描述符==》
再关闭一侧的读端和另外一侧的写端==》实现进程间的通信
剩余函数
//400错误
void bad_request(int client)
{
char buf[1024];
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);
}
//读取文件内容,发送客户端
void cat(int client, FILE *resource)
{
char buf[1024];//缓冲区
//逐行读取,遇到换行符eof就停止
fgets(buf, sizeof(buf), resource);
while (!feof(resource)) //是否已经读取至文件结尾
{
send(client, buf, strlen(buf), 0);//发送给客户端
fgets(buf, sizeof(buf), resource);
}
}
//500错误
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);
}
//错误处理
void error_die(const char *sc)
{
perror(sc);
exit(1);
}
//从缓冲区读取一行
int get_line(int sock, char *buf, int size)
{
int i = 0;
char c = '\0';
int n;
while ((i < size - 1) && (c != '\n'))
{
n = recv(sock, &c, 1, 0);
/* DEBUG printf("%02X\n", c); */
if (n > 0)
{
if (c == '\r')
{
n = recv(sock, &c, 1, MSG_PEEK);
/* DEBUG printf("%02X\n", c); */
if ((n > 0) && (c == '\n'))
recv(sock, &c, 1, 0);
else
c = '\n';
}
buf[i] = c;
i++;
}
else
c = '\n';
}
buf[i] = '\0';
return(i);
}
//处理头部信息
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);
}
//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);
}
//静态文件请求
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);
else
{
headers(client, filename);
cat(client, resource);
}
fclose(resource);
}
//返回给浏览器 表明收到的HTTP请求所用的method(方法)不被支持
void unimplemented(int client)
{
char buf[1024];
sprintf(buf, "HTTP/1.0 501 Method Not Implemented\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><HEAD><TITLE>Method Not Implemented\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, "</TITLE></HEAD>\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, "<BODY><P>HTTP request method not supported.\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, "</BODY></HTML>\r\n");
send(client, buf, strlen(buf), 0);
}
总结
整体工作流程:
服务器启动==》在指定端口或随机选取端口绑定httpd服务==》
在收到一个http请求时(listen的端口accept),派生一个线程运行accept_request函数==》
读取HTTP中的相关信息:method(请求方法),url==》
如果是POST==》开启cgi
如果是GET==》提取URL以及URL中的参数
==》如果不是CGI,调用serve_file,返回地址中内容(页面)
==》如果是CGI,调用execute_cgi,执行CGI脚本程序
==》调用cgi执行函数
创建两个管道与一个子进程==》cgi_input绑定stdin,cgi_output绑定stdout==》
设置环境变量==》
最后,关闭与浏览器的连接,完成一次HTTP请求和回应