httpd服务器代码详解
github链接:https://github.com/EZLippi/Tinyhttpd
1. main
返回一个新的 socket 描述符专门用于和这个客户端通信
client_sock = accept(server_sock,
(struct sockaddr *)&client_name,
&client_name_len);
if (client_sock == -1)
error_die("accept");
创建一个线程处理连接
accept_request表示要执行的函数
因为后续没有pthread_join进行线程回收,不需要记录newthread
if (pthread_create(&newthread, NULL, (void *)accept_request, (void *)(intptr_t)client_sock) != 0)
perror("pthread_create");
2. startup
创建socket,地址清零和初始化,设置端口复用,绑定绑定地址和端口
httpd = socket(PF_INET, SOCK_STREAM, 0);
//如果初始化失败error_die
//error_die封装了perror();exit(1);
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 ((setsockopt(httpd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on))) < 0) {
error_die("setsockopt failed");
}
if (bind(httpd, (struct sockaddr *)&name, sizeof(name)) < 0)
error_die("bind");
3. accept_request
strcasecmp逐字节比较,忽略字符的大小写
如果 method 不是 GET 也不是 POST
if (strcasecmp(method, "GET") && strcasecmp(method, "POST")) {
unimplemented(client);
return;
}
如果是POST就需要执行cgi
if (strcasecmp(method, "POST") == 0)
cgi = 1;
提取中间内容放入url
i = 0;
while (ISspace(buf[j]) && (j < numchars)) //跳过空格
j++;
while (!ISspace(buf[j]) && (i < sizeof(url) - 1) && (j < numchars)) {
url[i] = buf[j];
i++; j++;
}
如果是 GET 请求,就去 URL 中查找是否含有 ?
如果找到了,就说明这个 GET 请求有参数
if (strcasecmp(method, "GET") == 0) {
query_string = url;
while ((*query_string != '?') && (*query_string != '\0'))
query_string++;
if (*query_string == '?') {
cgi = 1;
*query_string = '\0';
query_string++;
}
}
sprintf(path, "htdocs%s", url);
if (path[strlen(path) - 1] == '/') //如果是目录
strcat(path, "index.html");
if (stat(path, &st) == -1) { //stat() 用来检查文件是否存在
while ((numchars > 0) && strcmp("\n", buf)) /* read & discard headers */
numchars = get_line(client, buf, sizeof(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);
}
4. get_line
见注释
int get_line(int sock, char *buf, int size) {
int i = 0;
char c = '\0';
int n;
//每次最多读取 size - 1 个字符,一旦读取到换行符 \n 就停止
while ((i < size - 1) && (c != '\n')) {
//从 socket 读取 1 个字节放入 c,逐字节读取
n = recv(sock, &c, 1, 0);
/* DEBUG printf("%02X\n", c); */
//如果遇到回车符 \r,看看后面是不是换行 \n,
//如果是,就一起吃掉,如果不是,就把 \r 当作换行
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);
}
5. execute_cgi
-
创建两个管道:cgi_input 和 cgi_output
-
创建一个子进程(fork())
-
子进程负责运行 CGI 程序,输入/输出通过管道通信
-
父进程负责:
-
给子进程输入参数(POST body)
-
读取 CGI 输出(HTML 或 JSON)并发给客户端
-
创建管道
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;
}
重定向输入输出
dup2(cgi_output[1], STDOUT);
dup2(cgi_input[0], STDIN);