動手用c寫一個HTTP服務器

動手用c寫一個HTTP服務器

源碼地址https://github.com/zhuangqh/a...html

看到像這個給tinyhttpd寫README的倉庫都有1k star的時候,我真的好氣?,因此我也寫一個用c寫HTTP靜態文件服務器的教程,並且性能更好。git

c socket編程面向的是傳輸層。咱們在這一層上來收發HTTP報文。github

HTTP請求報文格式以下:編程

clipboard.png

因爲咱們是靜態文件服務器,因此有效的請求報文是 GET url 的格式。咱們只要解析這個url,而後發送對應的文件就OK了。這個是基本的思路。數組

函數包裝

我仿照UNP中對函數進行包裝的方式。對基礎函數進行包裝,在代碼中只使用包裝過的函數。
UNIX函數大多會將函數的調用狀態做爲返回值。如Socket函數,若是返回值小於零,則是調用出錯,這種狀況咱們直接結束進程並報錯。瀏覽器

int
Socket(int family, int type, int protocol)
{
    int        n;

    if ( (n = socket(family, type, protocol)) < 0)
        err_sys("socket error");
    return(n);
}

監聽並處理請求

clipboard.png

服務器啓動並監聽的流程是這樣的:首先調用socket()建立一個服務端的套接字,而後使用bind()將套接字綁定在一個指定的端口上。調用listen()將套接字從CLOSED狀態轉換到LISTEN狀態。而accept會返回已鏈接隊列的對頭。咱們對accept返回的描述符的讀寫就是對客戶端的收發操做。服務器

這篇教程選用的併發模型是線程池,每一個線程分別accept的形式。併發

ReqHandler這個struct存放的是對客戶端描述符的處理函數和在pthread_t數組的下標。下標用於後面的pthread_createsocket

主進程在建立完線程後任務就完成了,因此它一直阻塞等待就好。函數

tptr = Calloc(THREAD_NUM, sizeof(pthread_t));

// tptr是一個pthread_t的數組。在啓動的時候可給出線程池線程的數量,不指明則使用默認值8。
ReqHandler rh;
rh.handler = accept_request;
for (int i = 0; i < nthreads; i++) {
rh.index = i;
thread_make(rh);
}

for ( ; ; )
pause();    // everything done by threads

在線程建立時,將ReqHandler裏的請求處理函數傳遞給它

Pthread_create(&tptr[rh.index], NULL, &thread_main, (void *) (rh.handler));

在各個線程中分別accept,這個有個問題,他們不該該同時accept。因此咱們在進入accept這個函數前加上互斥鎖。

Pthread_mutex_lock(&mmlock);
connfd = Accept(listenfd, cliaddr, &clilen);
Pthread_mutex_unlock(&mmlock);

處理請求

在服務器搭起來以後,咱們就能夠幹正事了。accept_request這個函數解析出HTTP的請求方法和URL並做出響應。

咱們知道HTTP的每一行是以/r/n結束,那麼getline該怎麼作呢?一個字符一個字符地讀,並逐一判斷是否爲/r/n序列的方法顯然比較慢。因此咱們作一個本身的緩衝區。預先在客戶端描述符connfd讀入多個字符。再在緩衝區裏一個字符一個字符地判斷。緩衝區讀完後,再讀一次connfd。這樣能大大減小讀取connfd的次數。

請求方法錯誤處理

獲取到HTTP的請求方法後,若是方法不是GET,咱們直接返回501錯誤。說明這個方法咱們尚未實現。

可能有人會對下面這種寫法感到疑惑。其實編譯器在作預處理的時候會把連着的字符串合併的。因此下面這種寫法跟寫在一對雙引號裏是同樣的。

void
unimplemented(int sockfd)
{
  char msg[] =
  "HTTP/1.1 501 Method Not Implemented\r\n"
  SERVER_STRING
  "Content-Type: text/plain\r\n"
  "\r\n"
  "method not implemented\r\n";

  Write(sockfd, msg, strlen(msg));
}

請求文件不存在處理

而後判斷URL的文件是否存在。這裏咱們多作了一步處理,若是URL是以/結尾的,瀏覽器會自動給它加上index.html,因此咱們也按照這個來。

咱們用open的形式打開文件,而不是標準IO的fopen。open能拿到該文件的描述符。這在咱們下一步傳輸文件時比較方便。若是文件不存在,直接返回404。

void
serve_file(int sockfd, const char *filepath)
{
  int filefd = open(filepath, O_RDONLY); // open file for read

  if (filefd == -1) {
    not_found(sockfd);
  } else {
    set_header(sockfd, filepath);
    send_file(sockfd, filefd);
    Close(filefd);
  }
}

傳輸文件

向客戶端發送文件,還得設置好響應報文中Content-Type的值,告訴對方這是一個什麼文件。這裏咱們須要一張表,根據文件的後綴名查詢Content-Type。天然是使用Hash表,衝突用鏈表的形式解決。具體請看源碼。

傳輸文件時,就是一對read write,UNIX一切皆文件的優雅就此體現。

void
send_file(int sockfd, int filefd)
{
  char buf[MAXLINE];
  int cnt = 0;

  while ((cnt = read(filefd, buf, MAXLINE)) > 0) {
    Write(sockfd, buf, cnt);
  }
}

具體代碼請查看文章開頭的github地址,我把要點和總體思路講完,剩下就是看代碼了?

相關文章
相關標籤/搜索