通過tinyhttpd-0.1.0源碼理解服務器原理

tinyhttpd是一個demo版的服務器。代碼幾百行。源碼分析在。從中可用一窺服務器的基礎原理。他採用的是一個請求新開一個線程處理的方式。裏面涉及了多進程、多線程、進程間通信等知識。
    我們從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_sock = startup(&port);

 while (1)
 {
   // 等待連接到來
  client_sock = accept(server_sock,
                       (struct sockaddr *)&client_name,
                       &client_name_len);
 // 每一個連接用一個線程處理,主函數是accept_request
 if (pthread_create(&newthread , NULL, accept_request, client_sock) != 0)
   perror("pthread_create");
 }
// 服務器退出
 close(server_sock);

 return(0);
}

main函數的邏輯很簡單。首先調用startup拿到一個監聽型的文件描述符。然後啓動服務器。阻塞在accept等待請求的到來。我們看看startup。

// 傳統的服務器啓動流程
int startup(u_short *port)
{
 int httpd = 0;
 struct sockaddr_in name;
 // 拿到一個用於監聽的文件描述符
 httpd = socket(PF_INET, SOCK_STREAM, 0);
 memset(&name, 0, sizeof(name));
 name.sin_family = AF_INET;
 name.sin_port = htons(*port);
 name.sin_addr.s_addr = htonl(INADDR_ANY);
  // 綁定一個地址到socket,沒有的話ip是本機地址,端口隨機
 if (bind(httpd, (struct sockaddr *)&name, sizeof(name)) < 0)
  error_die("bind");
 // port等於0,系統會隨機分配一個端口(bind函數裏實現)。這裏通過文件描述符拿到系統隨機分配的端口
 if (*port == 0)  /* if dynamically allocating a port */
 {
  int namelen = sizeof(name);
  // 獲取socket綁定的地址信息
  if (getsockname(httpd, (struct sockaddr *)&name, &namelen) == -1)
   error_die("getsockname");
  *port = ntohs(name.sin_port);
 }
 if (listen(httpd, 5) < 0)
  error_die("listen");
 return(httpd);
}

startup函數是經典的socket編程流程。這時候服務器已經啓動。等待請求的到來。我們回憶main函數裏的accept函數。他返回的是一個和客戶端通信的文件描述符。然後新開一個線程,線程裏執行accept_request函數。把這個描述符傳給線程,讓他處理。accept_request函數的主要邏輯如下。

// 文件路徑htdocs下
 sprintf(path, "htdocs%s", url);
 // 最後一個字符是/說明是個目錄,則取該目錄下的index.html文件
 if (path[strlen(path) - 1] == '/')
  strcat(path, "index.html");
  // 找不到該文件返回404
 if (stat(path, &st) == -1) {
  while ((numchars > 0) && strcmp("\n", buf))  /* read & discard headers */
   numchars = get_line(client, buf, sizeof(buf));
  not_found(client);
 }
 else
 {
   // 是個目錄
  if ((st.st_mode & S_IFMT) == S_IFDIR)
   strcat(path, "/index.html");
  // 是可執行文件則說明是cgi程序
  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
  // 執行cgi程序
   execute_cgi(client, path, method, query_string);
 }

主要是兩個邏輯。
1 靜態文件的請求,則直接讀取文件內容,然後返回給客戶端。線程退出。
2 執行動態腳本。
下面我們只分析2。通過執行execute_cgi函數執行動態腳本。該函數比較長,分開分析。

void execute_cgi(int client, const char *path,
                 const char *method, const char *query_string)
{
 // 一系列簡單解析http協議的邏輯
 // 獲取兩個匿名管道
 if (pipe(cgi_output) < 0) {
  cannot_execute(client);
  return;
 }
 if (pipe(cgi_input) < 0) {
  cannot_execute(client);
  return;
 }
}

申請兩個管道。

// fork進程
 if ( (pid = fork()) < 0 ) {
  cannot_execute(client);
  return;
 }

創建一個進程。我們看看父進程和子進程都做了什麼事情。先看子進程

char meth_env[255];
  char query_env[255];
  char length_env[255];
  // 先斷開文件描述符1和標準輸出file結構的關聯,然後使1指向cgi_ouput[1]指向的file結構
  dup2(cgi_output[1], 1);
  dup2(cgi_input[0], 0);
  // 關閉讀端
  close(cgi_output[0]);
  // 關閉寫端
  close(cgi_input[1]);
  // 輸入參數給處理請求的cgi進程使用
  sprintf(meth_env, "REQUEST_METHOD=%s", method);
  putenv(meth_env);
  if (strcasecmp(method, "GET") == 0) {
   sprintf(query_env, "QUERY_STRING=%s", query_string);
   putenv(query_env);
  }
  else {   /* POST */
   sprintf(length_env, "CONTENT_LENGTH=%d", content_length);
   putenv(length_env);
  }
  // 執行cgi進程
  execl(path, path, NULL);
  exit(0);

子進程輸入關閉管道的一端,然後輸入環境變量給cgi進程。然後執行cgi程序。再看父進程。

  close(cgi_output[1]);
  close(cgi_input[0]);
  if (strcasecmp(method, "POST") == 0)
   for (i = 0; i < content_length; i++) {
    /*
      讀數據然後寫入寫端cgi_input[1],對端是子進程的cgi_input[0],作爲子進程的標準讀入,
      即子進程可以讀到這裏寫入的數據
    */
    recv(client, &c, 1, 0);
    write(cgi_input[1], &c, 1);
   }
   /*
     等待子進程寫入,然後返回給客戶端,cgi_output[1]是子進程的標準輸出端,
     從cgi_output[1]寫入的數據可以從cgi_output[0]讀取
   */
  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);

父進程同樣關閉管道一端。如果是post則把客戶端的body輸入給子進程。然後在read函數阻塞等待子進程的輸入。最後兩個進程退出。整個服務器的處理過程是,每次來一個請求(假設是cgi)。新開一個處理線程。主線程繼續監聽。然後新開的處理線程fork出一個進程執行cgi。這時候就相當於有兩個進程。父子進程互相通信完成一個客戶端的處理,然後退出。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章