Created
January 24, 2021 08:59
-
-
Save ShaneTsui/ecac1dac085e4c7bd2c051752cb489ff to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <stdio.h> | |
#include <sys/socket.h> | |
#include <sys/types.h> | |
#include <netinet/in.h> | |
#include <arpa/inet.h> | |
#include <unistd.h> | |
#include <ctype.h> | |
#include <strings.h> | |
#include <string.h> | |
#include <sys/stat.h> | |
#include <pthread.h> | |
#include <sys/wait.h> | |
#include <stdlib.h> | |
#define ISspace(x) isspace((int)(x)) | |
#define SERVER_STRING "Server: jdbhttpd/0.1.0\r\n" | |
#define MAXDATASIZE 1 | |
#define MAXBUFFSIZE 1024 | |
#define CONTENT_LENGTH_POS 15 | |
#define STDIN 0 | |
#define STDOUT 1 | |
#define STDERR 2 | |
void accept_request(int); | |
void bad_request(int); | |
void cat(int, FILE *); | |
void cannot_execute(int); | |
void error_die(const char *); | |
void execute_cgi(int, const char *, const char *, const char *); | |
int get_line(int, char *, int); | |
void headers(int, const char *); | |
void not_found(int); | |
void serve_file(int, const char *); | |
int setup_server(u_short *); | |
void unimplemented(int); | |
/**********************************************************************/ | |
/* 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 */ | |
/**********************************************************************/ | |
void accept_request(int client_sock) { | |
char buf[MAXBUFFSIZE]; | |
int numchars; | |
char method[255]; | |
char url[255]; | |
char path[512]; | |
size_t i, buff_ptr; | |
struct stat st; | |
int is_cgi = 0; | |
char *query_string = NULL; | |
// Step 1: 读 http 请求的第一行(request line),并把请求方法存进 method 中 | |
numchars = get_line(client_sock, buf, sizeof(buf)); | |
i = 0; | |
buff_ptr = 0; | |
while (!ISspace(buf[buff_ptr]) && (i < sizeof(method) - 1)) { | |
method[i] = buf[buff_ptr]; | |
i++; | |
buff_ptr++; | |
} | |
method[i] = '\0'; | |
i = 0; | |
while (ISspace(buf[buff_ptr]) && (buff_ptr < sizeof(buf))) // 跳过所有的空白字符(空格) | |
buff_ptr++; | |
// Step 2: 把 URL 存到 url 数组中 | |
while (!ISspace(buf[buff_ptr]) && (i < sizeof(url) - 1) && (buff_ptr < sizeof(buf))) { | |
url[i] = buf[buff_ptr]; | |
i++; | |
buff_ptr++; | |
} | |
url[i] = '\0'; | |
// Step 3: 分析当前请求是否为 CGI 请求 | |
if (strcasecmp(method, "POST") == 0) { // POST 请求一定是 CGI | |
is_cgi = 1; | |
} else if (strcasecmp(method, "GET") == 0) { // GET 请求不一定是 CGI,如果带参数则是,不带则默认为请求 index.html | |
query_string = url; | |
while ((*query_string != '?') && (*query_string != '\0')) // 检查 url 中有无参数, 如 /color.cgi?color=red | |
query_string++; | |
if (*query_string == '?') { // 如果是 CGI 请求,则将 query_string 指向 ? 后的参数,如 color=red | |
is_cgi = 1; | |
*query_string = '\0'; | |
query_string++; | |
} | |
} else { // Neither POST nor GET | |
unimplemented(client_sock); | |
return; | |
} | |
sprintf(path, "htdocs%s", url); | |
// "/" => "/index.html" | |
if (path[strlen(path) - 1] == '/') | |
strcat(path, "index.html"); | |
// Step 4: 查找 path 指向的文件 | |
if (stat(path, &st) == -1) { // 如果不存在,那把这次 http 的请求后续的内容(head 和 body)全部读完并忽略 | |
while ((numchars > 0) && strcmp("\n", buf)) /* read & discard headers */ | |
numchars = get_line(client_sock, buf, sizeof(buf)); | |
not_found(client_sock); | |
} | |
else { | |
// 如果 path 是目录,则默认使用该目录下 index.html 文件 | |
if ((st.st_mode & S_IFMT) == S_IFDIR) | |
strcat(path, "/index.html"); | |
// 如果 path 是可执行文件,设置 cgi 标识 | |
if ((st.st_mode & S_IXUSR) || | |
(st.st_mode & S_IXGRP) || | |
(st.st_mode & S_IXOTH)) | |
is_cgi = 1; | |
if (!is_cgi) // 静态页面请求 | |
serve_file(client_sock, path); | |
else // 动态页面请求 | |
execute_cgi(client_sock, path, method, query_string); | |
} | |
close(client_sock); | |
} | |
/**********************************************************************/ | |
/* Inform the client that a request it has made has a problem. | |
* Parameters: client socket */ | |
/**********************************************************************/ | |
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); | |
} | |
/**********************************************************************/ | |
/* 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 */ | |
/**********************************************************************/ | |
void cat(int client, FILE *resource) { | |
char buf[1024]; | |
do { | |
fgets(buf, sizeof(buf), resource); | |
send(client, buf, strlen(buf), 0); | |
} while(!feof(resource)); | |
} | |
/**********************************************************************/ | |
/* Inform the client that a CGI script could not be executed. | |
* 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); | |
} | |
/**********************************************************************/ | |
/* Print out an error message with perror() (for system errors; based | |
* on value of errno, which indicates system call errors) and exit the | |
* program indicating an error. */ | |
/**********************************************************************/ | |
void error_die(const char *sc) | |
{ | |
//包含于<stdio.h>,基于当前的 errno 值,在标准错误上产生一条错误消息。参考《TLPI》P49 | |
perror(sc); | |
exit(1); | |
} | |
/**********************************************************************/ | |
/* Execute a CGI script. Will need to set environment variables as | |
* appropriate. | |
* Parameters: client socket descriptor | |
* path to the CGI script */ | |
/**********************************************************************/ | |
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)) // 读取并忽略 header,因为信息都在 url (query_string) 里 | |
numchars = get_line(client, buf, sizeof(buf)); | |
else { /* POST */ | |
// 找出 header 中的 Content-Length (size of the entity-body) 的值,其余都忽略。因为只有该值对确定客户端传来的请求参数有用 | |
numchars = get_line(client, buf, sizeof(buf)); | |
while ((numchars > 0) && strcmp("\n", buf)) { | |
buf[CONTENT_LENGTH_POS] = '\0'; | |
if (strcasecmp(buf, "Content-Length:") == 0){ | |
content_length = atoi(&(buf[16])); | |
} | |
numchars = get_line(client, buf, sizeof(buf)); | |
} | |
// 如果 http 请求的 header 没有 Content-Length,则报错返回 | |
if (content_length == -1) { | |
bad_request(client); | |
return; | |
} | |
} | |
sprintf(buf, "HTTP/1.0 200 OK\r\n"); | |
send(client, buf, strlen(buf), 0); | |
// 创建两个管道,用于父子进程间通信 | |
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; | |
} | |
if (pid == 0) /* 子进程: CGI script */ { | |
char meth_env[255]; | |
char query_env[255]; | |
char length_env[255]; | |
// 复制文件句柄,重定向进程的标准输入输出 | |
dup2(cgi_output[1], STDOUT); // 将子进程输入管道的读端口和 STDOUT 绑定(重定向) | |
dup2(cgi_input[0], STDIN); // 将子进程输出管道的写端口和 STDIN 绑定 | |
close(cgi_output[0]); // 关闭 cgi_ouput 管道的读端 | |
close(cgi_input[1]); // 关闭 cgi_input 管道的写端 | |
sprintf(meth_env, "REQUEST_METHOD=%s", method); | |
putenv(meth_env); | |
// 根据 http 请求的不同方法,构造并存储不同的环境变量 | |
if (strcasecmp(method, "GET") == 0) { | |
/* 设置 query_string 的环境变量 */ | |
// 如果服务器与CGI程序信息的传递方式是GET,这个环境变量的值即使所传递的信息。这个信息经跟在CGI程序名的后面,两者中间用一个问号'?'分隔。 | |
sprintf(query_env, "QUERY_STRING=%s", query_string); | |
putenv(query_env); | |
} else { /* POST */ | |
/* 设置 content_length 的环境变量 */ | |
// 如果服务器与 CGI 程序信息的传递方式是POST,这个环境变量即使从标准输入 STDIN 中可以读到的有效数据的字节数。这个环境变量在读取所输入的数据时必须使用。 | |
sprintf(length_env, "CONTENT_LENGTH=%d", content_length); | |
putenv(length_env); | |
} | |
// exec 函数簇,执行 CGI 脚本,获取 cgi 的标准输出作为相应内容发送给客户端 | |
// 将子进程替换成另一个进程并执行 cgi 脚本 | |
execl(path, path, NULL); | |
// 子进程退出 | |
exit(0); | |
} else { /* parent */ | |
// 父进程则关闭了 cgi_output 管道的写端和 cgi_input 管道的读端 | |
close(cgi_output[1]); | |
close(cgi_input[0]); | |
// 如果是 POST 方法的话就继续读 body 的内容,并写到 cgi_input 管道里让子进程去读 | |
if (strcasecmp(method, "POST") == 0) { | |
for (i = 0; i < content_length; i++) { | |
recv(client, &c, MAXDATASIZE, 0); | |
write(cgi_input[1], &c, 1); | |
} | |
} | |
// 从 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); | |
} | |
} | |
/**********************************************************************/ | |
/* 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) */ | |
/**********************************************************************/ | |
// 该函数不管行原来是以 \r 还是 \r\n 结束,均转化为以 \n 再加 \0 字符结束。 | |
int get_line(int sock, char *buf, int size) { | |
int num_bytes = 0; | |
char c = '\0'; | |
int n; | |
while ((num_bytes < size - 1) && (c != '\n')) | |
{ | |
n = recv(sock, &c, MAXDATASIZE, 0); // int recv(int sockfd, void *buf, int len, unsigned int flags); 读一个字节的数据存放在 c 中, 包含于<sys/socket.h> | |
if (n > 0) { | |
if (c == '\r') { | |
n = recv(sock, &c, MAXDATASIZE, MSG_PEEK); // Peek | |
if ((n > 0) && (c == '\n')) { | |
recv(sock, &c, MAXDATASIZE, 0); // "\r\n" | |
} | |
else | |
c = '\n'; // "\r" | |
} | |
buf[num_bytes] = c; // "\n" or other common c | |
num_bytes++; | |
} | |
else | |
c = '\n'; | |
// printf("%c", c); | |
} | |
buf[num_bytes] = '\0'; | |
return (num_bytes); | |
} | |
/**********************************************************************/ | |
/* 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); | |
} | |
/**********************************************************************/ | |
/* Give a client a 404 not found status message. */ | |
/**********************************************************************/ | |
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); | |
} | |
/**********************************************************************/ | |
/* 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[MAXBUFFSIZE]; | |
buf[0] = 'A'; // 确保 buf 里面有东西,能进入下面的 while 循环 | |
buf[1] = '\0'; | |
while ((numchars > 0) && strcmp("\n", buf)) // 读取并忽略掉这个 http 请求后面的所有内容 | |
numchars = get_line(client, buf, sizeof(buf)); | |
resource = fopen(filename, "r"); | |
if (resource == NULL) | |
not_found(client); | |
else { | |
headers(client, filename); // 将请求文件的基本信息封装成 response 的头部 (header) 并返回 | |
cat(client, resource); // 把请求文件的内容读出来作为 response 的 body 发送到客户端 | |
} | |
fclose(resource); | |
} | |
/**********************************************************************/ | |
/* 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 setup_server(u_short *port) { | |
// 创建 socket | |
int server_sock = 0; | |
server_sock = socket(PF_INET, SOCK_STREAM, 0); // PF_INET 其实是与 AF_INET 同义 | |
if (server_sock == -1) | |
error_die("socket"); | |
// 创建 sockaddr,指定了 IP 和 port | |
struct sockaddr_in server_addr; | |
memset(&server_addr, 0, sizeof(server_addr)); | |
server_addr.sin_family = AF_INET; | |
server_addr.sin_port = htons(*port); // htons(),ntohs() 和 htonl()包含于<arpa/inet.h>, 将 *port 转换成以网络字节序表示的16位整数 | |
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // INADDR_ANY是一个 IPV4 通配地址的常量,包含于<netinet/in.h>,大多实现都将其定义成了0.0.0.0 | |
// 将 socket 和 sockaddr 绑定 | |
if (bind(server_sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) // 由于传进去的 sockaddr 结构中的 sin_port 指定为 0,系统会选择一个临时的端口号 | |
error_die("bind"); | |
// 如果调用 bind 后端口号仍然是0,则手动调用 getsockname() 获取端口号 | |
if (*port == 0) /* if dynamically allocating a port */ { | |
int namelen = sizeof(server_addr); | |
if (getsockname(server_sock, (struct sockaddr *)&server_addr, &namelen) == -1) //调用 getsockname() 获取系统给 httpd 这个 socket 随机分配的端口号 | |
error_die("getsockname"); | |
*port = ntohs(server_addr.sin_port); | |
} | |
// 进入监听状态,等待用户发起请求 | |
if (listen(server_sock, 5) < 0) // 最初的 BSD socket 实现中,backlog 的上限是5 | |
error_die("listen"); | |
printf("httpd running on http://localhost:%d\n", *port); | |
return (server_sock); | |
} | |
/**********************************************************************/ | |
/* Inform the client that the requested web method has not been | |
* implemented. | |
* Parameter: the client socket */ | |
/**********************************************************************/ | |
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); | |
} | |
int main(void) { | |
int server_sock = -1; | |
u_short port = 0; | |
server_sock = setup_server(&port); | |
int client_sock = -1; | |
struct sockaddr_in client_name; | |
int client_name_len = sizeof(client_name); | |
pthread_t newthread; | |
while (1) { | |
client_sock = accept(server_sock, | |
(struct sockaddr *)&client_name, | |
&client_name_len); | |
if (client_sock == -1) | |
error_die("accept"); | |
if (pthread_create(&newthread, NULL, accept_request, client_sock) != 0) | |
perror("pthread_create"); | |
} | |
close(server_sock); | |
return (0); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment