Skip to content

Instantly share code, notes, and snippets.

@ShaneTsui
Created January 24, 2021 08:59
Show Gist options
  • Save ShaneTsui/ecac1dac085e4c7bd2c051752cb489ff to your computer and use it in GitHub Desktop.
Save ShaneTsui/ecac1dac085e4c7bd2c051752cb489ff to your computer and use it in GitHub Desktop.
#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