TinyHttp源碼分析

主函數

1.服務器端初始化:html

建立socket => 設置端口複用 => 綁定socket與服務器地址 => 若是未指定端口,動態分配 => 監聽
int on = 1;
unsigned int port = 4000;
struct sockaddr_in name;
int lfd = socket(PF_INET, SOCK_STREAM, 0);  //建立socket
memset(&name, 0, sizeof(name));  //初始化name
name.sin_family = AF_INET;
name.sin_port = htons(*port);
name.sin_addr.s_addr = htonl(INADDR_ANY);
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) //端口複用
bind(lfd, (struct sockaddr *)&name, sizeof(name))
if (*port == 0) {   //動態分配端口
    socklen_t namelen = sizeof(name);
    getsockname(lfd, (struct sockaddr *)&name, &namelen);
    *port = ntohs(name.sin_port);
}
listen(httpd, 5);

2.服務器阻塞等待客戶端鏈接,一旦鏈接,開闢一個線程並執行對應響應函數:git

while(1) {
    client_sock = accept(server_sock, (struct sockaddr*)&client_name, &client_name_len);
    printf("received from %s at PORT %d\n", 
            inet_ntop(AF_INET, &client_name.sin_addr, str, sizeof(str)), 
            ntohs(client_name.sin_port));
    pthread_create(&newthread , NULL, (void *)accept_request, (void *)(intptr_t)client_sock);
}

響應函數

1.獲取請求方法
請求報文github

上圖所示是一個http請求的報文格式請求方法是指上圖中請求行中第一個字段。服務器

int i = 0; 
char buf[1024];
char method[255];
/*get_line返回獲取到的字節數*/
int numchars = get_line(client_sock, buf, sizeof(buf)); 

/*不爲空,以及未到達上限,將buf中Get或Post拷貝進method*/
while (!isspace((int)buf[i]) && (i < sizeof(method) - 1)) { 
    method[i] = buf[i];
    i++;
}
method[i] = '\0';

注意:若是既不是post也不是get請求,報錯退出;另外值得注意的是報文中的空格值爲"rn"。 socket

2.獲取url
一個URL格式(圖片招領)函數

上圖所示是一個常見的URL,而請求行中的url指的是上圖resource path,在請求頭部中會包含host字段。post

int j = i;
i = 0;
char url[255];
while (isspace((int)buf[j]) && (j < numchars)) j++;  //跳過空格

/*取出報文頭的url,該url不包含host*/
while (!isspace((int)buf[j]) && (i < sizeof(url) - 1) && (j < numchars)) {
    url[i] = buf[j];
    i++; j++;
}
url[i] = '\0';

3.設置cgi值(能夠理解爲一個標誌位):url

  • 若是是post請求, cgi=1;
  • 若是是帶參數的get請求, cgi=1, 並使用一個指針指向參數;
  • 若是url指向的地址是可執行文件, cgi=1;
  • 其他狀況cgi=0;

4.組合url,讓其指向服務器上的一個絕對路徑(path),若是path是一個目錄(文件夾),修改path指向默認的主頁地址,path = host+urlspa

5.使用stat函數綁定path線程

失敗,即路徑錯誤,將剩餘請求內容讀出並丟棄,返回404錯誤;
成功,若 cgi != 1 (get無參請求), 直接將path內容讀出並send回客戶端;若**cgi == 1, 執行cgi腳本,函數參數:client_sock, path, method, query(get請求的請求參數)。

cgi腳本函數

1.什麼是cgi

帶有參數的get請求,和post請求,服務器沒有辦法簡單的返回文件的內容, 服務器須要將對應的的頁面找出來,再send給客戶端。這個就須要cgi來幫忙,cgi能夠理解爲是服務器端可執行的小腳本。服務器收到這個請求以後,執行.cgi文件,這個文件是提早寫好了,專門來處理這樣的請求,而後獲得相應的網頁數據,再send給客戶端。

2.判斷是什麼請求

get: 讀出剩餘報文請求頭內容並丟棄,直到遇到兩個換行符,後面再讀就是請求數據;
post: 讀報文請求頭部中的 content-length字段,判斷是否有有效內容,該字段的值爲post請求的數據長度。
//post請求,獲取請求主體數據長度
int numchars = get_line(client_sock, buf, sizeof(buf));
while ((numchars > 0) && strcmp("\n", buf)) {
    /*Content-Length:這個字符串一共長爲15位,
      因此取出頭部一句後,將第16位設置結束符,
      進行比較,第16位置爲結束*/
    buf[15] = '\0';
    if (strcasecmp(buf, "Content-Length:") == 0)
        content_length = atoi(&(buf[16]));
    numchars = get_line(client_sock, buf, sizeof(buf));  //剩餘頭內容讀出丟棄
}
if (content_length == -1) {
    bad_request(client);
    return;
}

3.建立兩個管道並fork

pipe(cgi_output);  //輸出
pipe(cgi_input);  //輸入
pid = fork();  //建立進程

管道中的數據流向

4.子進程

  • 關閉輸出讀端,輸入寫端;
  • 複製輸出讀端到stdout, 複製輸入讀端到stdin
  • 配置cgi環境變量;
  • execl執行請求地址。
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]);  //輸出讀
close(cgi_input[1]);   //輸入寫

//CGI環境變量
sprintf(meth_env, "REQUEST_METHOD=%s", method);
putenv(meth_env);
if (strcasecmp(method, "GET") == 0) {   //get請求
    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, NULL);
exit(0);  //子進程退出

5.父進程

  • 關閉輸出寫端,輸出讀端;
  • 若是是post請求,接收post內容,並寫進輸入寫端;
  • 循環讀輸入讀端的數據,send給客戶端。
close(cgi_output[1]);  //輸出寫
close(cgi_input[0]);   //輸入讀
if(strcasecmp(method, "POST") == 0) {
    //經過cgi_input[1](寫端)寫入到CGI的標準輸入
    for (int i = 0; i < content_length; i++) {
        recv(client_sock, &c, 1, 0);
        write(cgi_input[1], &c, 1);
    }
}
//讀取CGI的標準輸出,發送到客戶端
while (read(cgi_output[0], &c, 1) > 0)
    send(client_sock, &c, 1, 0);

close(cgi_output[0]);
close(cgi_input[1]);
waitpid(pid, &status, 0);

其餘重要函數

1.get_line:讀取一行數據

int get_line(int sock, char *buf, int size)
{
    int n;
    int i = 0;
    char c = '\0';  
    while ((i < size - 1) && (c != '\n')) {
        //從sock中讀取一個字節
        n = recv(sock, &c, 1, 0);
        if (n > 0) {
            // 將 \r\n 或 \r 轉換爲'\n'
            if (c == '\r') {
                // 讀到了'\r'就再預讀一個字節
                n = recv(sock, &c, 1, MSG_PEEK);
                // 若是讀取到的是'\n',就讀取,不然c='\n'
                if ((n > 0) && (c == '\n')) recv(sock, &c, 1, 0);
                else c = '\n';
            }
            // 讀取數據放入buf
            buf[i] = c;
            i++;
        }
        else c = '\n';
    }
    buf[i] = '\0';
    return(i); // 返回寫入buf的字節數
}

注意事項

  • index.html必須沒有執行權限,不然不能顯示內容,可經過chmod 600 index.html更改。
  • 編譯gcc server.c -o server -lpthread
  • 完整代碼請請訪問github
相關文章
相關標籤/搜索