當前位置:
首頁 > 最新 > TinyHttpd源碼分析

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和常規的進程啟動、通信的編寫方法。


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 全球大搜羅 的精彩文章:

莎莎你怎麼這麼瘦的啊!
體檢的那些事兒

TAG:全球大搜羅 |