TinyHttpd源碼分析
TinyHttpd是一個C編寫的極小HTTP伺服器,代碼量(包括注釋)不到500行,但可以基本說明HTTP伺服器的工作原理,從中也能知道如何進行Linux下的C編程(這是我看這個代碼的主要原因)。
在Linux下面,我採用Eclipse CPP作為C/C++的IDE,設置好相應的環境變數,以便IDE可以查找鏈接相應的頭文件和庫。
源碼分析
// 函數聲明
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 *); //執行cgi程序
int get_line(int, char *, int); // 讀取每行
void headers(int, const char *); //獲取頭部信息
void not_found(int); // 404處理
void serve_file(int, const char *); //靜態文件請求
int startup(u_short *); //啟動服務
void unimplemented(int); //未實現
下面先看啟動函數--main
int main(void) {
int server_sock = -1;
u_short port = 0;
int client_sock = -1;
struct sockaddr_in client_name;
int client_name_len = sizeof(client_name);
pthread_t newthread;
//初始化server socket port->返回隨機綁定的埠,當然,port可以指定埠
server_sock = startup(&port);
printf("httpd running on port %d
", port);
//輪詢,接受客戶端請求並完成請求
while (1) {
client_sock = accept(server_sock, (struct sockaddr *) &client_name,
&client_name_len);
if (client_sock == -1)
error_die("accept");
//對於每個客戶端請求,使用一個線程來處理,線程函數為accept_request
if (pthread_create(&newthread, NULL, accept_request, client_sock) != 0)
perror("pthread_create");
}
//關閉server socket
close(server_sock);
return (0);
}
對於server部分,都是socket編程的標準套路。http server對於客戶請求的響應邏輯在accept_request函數中。
void accept_request(int client) {
char buf[1024];
int 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;
//從客戶socket中按行讀取到buf中
numchars = get_line(client, buf, sizeof(buf));
i = 0;
j = 0;
/**
HTTP頭第一行格式如下
METHOD QUERY_URL
**/
//讀取mthod
while (!ISspace(buf[j]) && (i
method[i] = buf[j];
i++;
j++;
}
method[i] = " ";
//方法僅支持GET和POST兩種,其他返回「未實現」信息
if (strcasecmp(method, "GET") && strcasecmp(method, "POST")) {
unimplemented(client);
return;
}
//如果是POST請求,說明有參數存在,交給CGI處理
if (strcasecmp(method, "POST") == 0)
cgi = 1;
i = 0;
while (ISspace(buf[j]) && (j
j++;
//解析請求的URL
while (!ISspace(buf[j]) && (i
url[i] = buf[j];
i++;
j++;
}
url[i] = " ";
if (strcasecmp(method, "GET") == 0) {
query_string = url;
while ((*query_string != "?") && (*query_string != " "))
query_string++;
//如果GET方法的請求串中帶有?,說明有參數存在,交給CGI處理
if (*query_string == "?") {
cgi = 1;
// 加入c字元串終結符 ,url就是?前部分
*query_string = " ";
//query_string指向?後面的參數串
query_string++;
}
}
//先將url與htdocs合併,查找是否有相關文件
sprintf(path, "htdocs%s", url);
//如果以/結尾,自動定位路徑下的index.html
if (path[strlen(path) - 1] == "/")
strcat(path, "index.html");
if (stat(path, &st) == -1) { //如果文件不存在
while ((numchars > 0) && strcmp("
", buf)) //將剩餘的內容讀取並丟棄
numchars = get_line(client, buf, sizeof(buf));
not_found(client); // 顯示404 not found
} else {
if ((st.st_mode & S_IFMT) == S_IFDIR)
strcat(path, "/index.html"); //如果是目錄,指向目錄下的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); // 處理完畢,關閉客戶端連接
}
// 文件服務,將文件讀取並傳到客戶端
void serve_file(int client, const char *filename) {
FILE *resource = NULL;
int numchars = 1;
char buf[1024];
buf[0] = "A";
buf[1] = " ";
while ((numchars > 0) && strcmp("
", buf)) //讀取丟棄剩餘頭部內容
numchars = get_line(client, buf, sizeof(buf));
//按文本方式讀取文件
resource = fopen(filename, "r");
if (resource == NULL)
not_found(client);
else {
headers(client, filename); //組裝http響應頭
cat(client, resource); //返迴文件內容
}
fclose(resource); //關閉文件
}
//組裝http響應頭
void headers(int client, const char *filename) {
char buf[1024];
(void) filename; /* could use filename to determine file type */
//將內容拷貝到buf中並發出,返回狀態碼 200
strcpy(buf, "HTTP/1.0 200 OK
");
send(client, buf, strlen(buf), 0);
//返回server標識
strcpy(buf, SERVER_STRING);
//#define SERVER_STRING "Server: jdbhttpd/0.1.0
"
send(client, buf, strlen(buf), 0);
//返回mime-type信息
sprintf(buf, "Content-Type: text/html
");
send(client, buf, strlen(buf), 0);
strcpy(buf, "
");
//發生分割符
,準備發送實際內容
send(client, buf, strlen(buf), 0);
}
//發送文件內容
void cat(int client, FILE *resource) {
char buf[1024];
//按行讀取文件,並按行發送到客戶端
fgets(buf, sizeof(buf), resource);
while (!feof(resource)) {
send(client, buf, strlen(buf), 0);
fgets(buf, sizeof(buf), resource);
}
}
以上的內容是靜態文件處理方式。下面分析cgi的處理方式,從execute_cgi開始分析。
個人認為execute_cgi是最具價值的代碼塊,從這部分代碼可以理解linux下面如何創建進程,如何共享環境變數,如何使用管道來進行進程間通信。
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] = " ";
if (strcasecmp(method, "GET") == 0) //GET方法
while ((numchars > 0) && strcmp("
", buf)) //忽略剩餘的頭部內容
numchars = get_line(client, buf, sizeof(buf));
else //POST方法
{
numchars = get_line(client, buf, sizeof(buf));
while ((numchars > 0) && strcmp("
", buf)) {
buf[15] = " ";
if (strcasecmp(buf, "Content-Length:") == 0)
content_length = atoi(&(buf[16]));
numchars = get_line(client, buf, sizeof(buf));
}
if (content_length == -1) {
//如果POST沒有Content-Length域,則認為是錯誤請求,因為後面會通過這個
//域的大小讀取內容並交給cgi處理
bad_request(client);
return;
}
}
//先發送狀態碼 200
sprintf(buf, "HTTP/1.0 200 OK
");
send(client, buf, strlen(buf), 0);
//創建輸出管道
if (pipe(cgi_output)
cannot_execute(client); //如果管道創建失敗,返回錯誤信息
//個人感覺,狀態碼應該在這裡發送,並返回50x狀態碼說明內部錯誤
return;
}
//創建輸入管道
if (pipe(cgi_input)
cannot_execute(client);
return;
}
//fork一個進程,如果失敗,同樣處理
if ((pid = fork())
cannot_execute(client);
return;
}
if (pid == 0) /* child: CGI script */
{
//pid為0,說明fork分支路徑在執行,下面是子進程處理邏輯
char meth_env[255];
char query_env[255];
char length_env[255];
dup2(cgi_output[1], 1);//標準輸出重定向為cgi_output[1]
dup2(cgi_input[0], 0); //標準輸入重定向為cgi_input[0]
close(cgi_output[0]);
close(cgi_input[1]);
//即使在fork子進程中,所有的變數還是可以讀取的
sprintf(meth_env, "REQUEST_METHOD=%s", method);
//將REQUEST_METHOD加入到env中
putenv(meth_env);
if (strcasecmp(method, "GET") == 0) {
//將請求串也加入到env中 QUERY_STRING
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, path, NULL);
//如果execl執行失敗,退出
exit(0);
} else { /* parent */
//pid不為0,說明還在自身的進程路徑下執行
close(cgi_output[1]);
close(cgi_input[0]);
//通過管道與子進程通信
if (strcasecmp(method, "POST") == 0) //POST將請求內容發給管道
for (i = 0; i
recv(client, &c, 1, 0);
write(cgi_input[1], &c, 1);
}
//返回子進程的輸出內容
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);
}
}
分析完核心函數,cannot_execute,bad_request,not_found,unimplemented等函數均是顯示一些錯誤提示信息,這裡僅貼出unimplemented函數實現。
void unimplemented(int client) {
char buf[1024];
sprintf(buf, "HTTP/1.0 501 Method Not Implemented
");
send(client, buf, strlen(buf), 0);
sprintf(buf, SERVER_STRING);
send(client, buf, strlen(buf), 0);
sprintf(buf, "Content-Type: text/html
");
send(client, buf, strlen(buf), 0);
sprintf(buf, "
");
send(client, buf, strlen(buf), 0);
sprintf(buf, "
");
send(client, buf, strlen(buf), 0);
sprintf(buf, "
HTTP request method not supported.
");
send(client, buf, strlen(buf), 0);
sprintf(buf, "
");
send(client, buf, strlen(buf), 0);
}
最後介紹一下startup函數,它啟動server socket。
int startup(u_short *port) {
int httpd = 0;
struct sockaddr_in name;
//標準socket初始化過程
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))
error_die("bind");
if (*port == 0) /* if dynamically allocating a port */
{
int namelen = sizeof(name);
if (getsockname(httpd, (struct sockaddr *) &name, &namelen) == -1)
error_die("getsockname");
*port = ntohs(name.sin_port);
}
if (listen(httpd, 5)
error_die("listen");
return (httpd);
}
整個項目很簡單,可以理解linux下的socket和常規的進程啟動、通信的編寫方法。
![](https://pic.pimg.tw/zzuyanan/1488615166-1259157397.png)
![](https://pic.pimg.tw/zzuyanan/1482887990-2595557020.jpg)
TAG:全球大搜羅 |