Try   HackMD

N01: lab0

主講人: jserv / 課程討論區: 2025 年系統軟體課程

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
返回「Linux 核心設計」課程進度表

整合 tiny-web-server

接下來,我們思考同時處理命令列輸入與網頁伺服器,先找出程式等待輸入時的主要迴圈,位於 linenoise()->linenoiseRaw()->linenoiseEdit() 內的 while(1)。但 linenoise 是用 read 等待使用者輸入,當 read 阻塞時,便無法接收 web 傳來的資訊。

  • 嘗試用 select() 同時處理 stdin 及 socket:

    • linenoiseEdit 中加入以下程式碼:
    ​​​​while (1) {
    ​​​​    char c;
    ​​​​    int nread;
    ​​​​    char seq[3];
    
    ​    fd_set set;
    ​​​​    
    ​​​​    FD_ZERO(&set);
    ​​​​    FD_SET(listenfd, &set);
    ​​​​    FD_SET(stdin_fd, &set);
    ​​​​    int rv = select(listenfd + 1, &set, NULL, NULL, NULL);
    ​​​​    struct sockaddr_in clientaddr;
    ​​​​    socklen_t clientlen = sizeof clientaddr;
    ​​​​    int connfd;
    
    ​​​​    switch (rv) {
    ​​​​    case -1:
    ​​​​        perror("select"); /* an error occurred */
    ​​​​        continue;
    ​​​​    case 0:
    ​​​​        printf("timeout occurred\n"); /* a timeout occurred */
    ​​​​        continue;
    ​​​​    default: 
    ​​​​        if (FD_ISSET(listenfd, &set)) {
    ​​​​            connfd = accept(listenfd,(SA *) &clientaddr, &clientlen);
    ​​​​            char *p = process(connfd, &clientaddr);
    ​​​​            strncpy(buf, p, strlen(p) + 1);
    ​​​​            close(connfd);
    ​​​​            free(p);
    ​​​​            return strlen(p);
    ​​​​        } else if (FD_ISSET(stdin_fd, &set)) {
    ​​​​            nread = read(l.ifd, &c, 1);
    ​​​​            if (nread <= 0)
    ​​​​                return l.len;
    ​​​​        }
    ​​​​        break;
    ​​​​    }
    ​​​​}
    
    • I/O Multiplexing Model
      Image Not Showing Possible Reasons
      • The image was uploaded to a note which you don't have access to
      • The note which the image was originally uploaded to has been deleted
      Learn More →

    selectpoll 皆使用此種 Model,好處是可以同時檢查多個 descriptor,但缺點是需要兩次的 system call,從圖中也可以看到,先透過 select system call 呼叫到 kernel space 內等待資料,接著 kernel 接收到資料後通知 user space,再重新發送 recvfrom system call 至 kernel space,意味著需要多次 context switch,成本相對比其他 Model (如 Blocking I/O) 來得高。

  • 嘗試修改 console.c,讓程式讀到 web 命令後手動按下 Enter 鍵繼續:

    • 先將 socket 改為 non-blocking,防止程式停止接收使用者輸入:
    ​​​​int flags = fcntl(listenfd, F_GETFL);
    ​​​​fcntl(listenfd, F_SETFL, flags | O_NONBLOCK);
    
    • 接著修改 run_console()
    ​​​​if (!has_infile) {
    ​​​​    char *cmdline;
    ​​​​    while ((cmdline = linenoise(prompt)) != NULL) {
    ​​​​        int connfd = accept(*listenfd, (SA *)&clientaddr, &clientlen);
    ​​​​        if (connfd == -1) {
    ​​​​            interpret_cmd(cmdline);
    ​​​​            linenoiseHistoryAdd(cmdline);       /* Add to the history. */
    ​​​​            linenoiseHistorySave(HISTORY_FILE); /* Save the history on disk. */
    ​​​​            linenoiseFree(cmdline);
    ​​​​        }
    ​​​​        else {
    ​​​​            cmdline = process(connfd, &clientaddr);
    ​​​​            interpret_cmd(cmdline);
    ​​​​            free(cmdline);
    ​​​​            close(connfd);
    ​​​​        }
    ​​​​    }
    ​​​​} else {
    ​​​​    while (!cmd_done())
    ​​​​        cmd_select(0, NULL, NULL, NULL, NULL);
    ​​​​}
    
    • 透過 HTTP 的 GET method 來傳送想要執行的 function,process() 處理 URL 字串並將 function name 與 parameter 以跟 cmdline 一樣的格式回傳 ([function name][space][parameter]):
    ​​​​char *process(int fd, struct sockaddr_in *clientaddr) {
    ​​​​#ifdef LOG_ACCESS
    ​​​​    printf("accept request, fd is %d, pid is %d\n", fd, getpid());
    ​​​​#endif
    ​​​​    http_request req;
    ​​​​    parse_request(fd, &req);
    ​​​​    int status = 200;
    ​​​​    handle_request(fd, req.function_name);
    
    ​​​​    char *p = req.function_name;
    ​​​​    /* Change '/' to ' ' */
    ​​​​    while (*p && (*p) != '\0') {
    ​​​​        ++p;
    ​​​​        if (*p == '/') {
    ​​​​            *p = ' ';
    ​​​​        }
    ​​​​    }
    ​​​​#ifdef LOG_ACCESS
    ​​​​    log_access(status, clientaddr, &req);
    ​​​​#endif
    ​​​​    char *ret = malloc(strlen(req.function_name) + 1);
    ​​​​    strncpy(ret, req.function_name, strlen(req.function_name) + 1);
    
    ​​​​    return ret;
    ​​​​}
    
  • 在分析完 console.c 後,發現使用 cmd_select() 能夠滿足需求:

    • 因為我們需要同時控制 web 輸入及使用者輸入,必須關閉 linenoise API 才能達成,在輸入 web 命令後將 linenoise 關閉,並儲存監聽的 file descriptor:
    ​​​​static bool do_web_cmd(int argc, char *argv[])
    ​​​​{
    ​​​​    listenfd = socket_init();
    ​​​​    noise = false;
    ​​​​    return true;
    ​​​​}
    
    • run_console() 依照 linenoise 開啟與否來選擇要使用 linenoise() 還是 cmd_select()
    ​​​​if (!has_infile) {
    ​​​​    char *cmdline;
    ​​​​    while (noise && (cmdline = linenoise(prompt)) != NULL) {
    ​​​​        interpret_cmd(cmdline);
    ​​​​        linenoiseHistoryAdd(cmdline); /* Add to the history. */
    ​​​​        linenoiseHistorySave(
    ​​​​            HISTORY_FILE); /* Save the history on disk. */
    ​​​​        linenoiseFree(cmdline);
    ​​​​    }
    ​​​​    if (!noise) {
    ​​​​        while (!cmd_done()) {
    ​​​​            cmd_select(0, NULL, NULL, NULL, NULL);
    ​​​​        }
    ​​​​    }
    ​​​​} else {
    ​​​​    while (!cmd_done()) {
    ​​​​        cmd_select(0, NULL, NULL, NULL, NULL);
    ​​​​    }
    ​​​​}
    
    • cmd_select() 新增對應的處理
    ​​​​int cmd_select(int nfds, ​​​​ fd_set *readfds, ​​​​ fd_set *writefds, ​​​​ fd_set *exceptfds, ​​​​ struct timeval *timeout) ​​​​{ ​​​​ int infd; ​​​​ fd_set local_readset; ​​​​ if (cmd_done()) ​​​​ return 0; ​​​​ ​​​​ if (!block_flag) { ​​​​ /* Process any commands in input buffer */ ​​​​ if (!readfds) ​​​​ readfds = &local_readset; ​​​​ /* Add input fd to readset for select */ ​​​​ infd = buf_stack->fd; ​​​​+ FD_ZERO(readfds); ​​​​ FD_SET(infd, readfds); ​​​​+ /* If web_fd is available, add to readfds */ ​​​​+ if (listenfd != -1) ​​​​+ FD_SET(listenfd, readfds); ​​​​ if (infd == STDIN_FILENO && prompt_flag) { ​​​​ printf("%s", prompt); ​​​​ fflush(stdout); ​​​​ prompt_flag = true; ​​​​ } ​​​​ if (infd >= nfds) ​​​​ nfds = infd + 1; ​​​​+ if (listenfd >= nfds) ​​​​+ nfds = listenfd + 1; ​​​​ } ​​​​ if (nfds == 0) ​​​​ return 0; ​​​​ ​​​​ int result = select(nfds, readfds, writefds, exceptfds, timeout) ​​​​ if (result <= 0) ​​​​ return result; ​​​​ infd = buf_stack->fd; ​​​​ if (readfds && FD_ISSET(infd, readfds)) { ​​​​ /* Commandline input available */ ​​​​ FD_CLR(infd, readfds); ​​​​ result--; ​​​​- if (has_infile) { ​​​​ char *cmdline; ​​​​ cmdline = readline(); ​​​​ if (cmdline) ​​​​ interpret_cmd(cmdline); ​​​​- } ​​​​+ } else if (readfds && FD_ISSET(listenfd, readfds)) { ​​​​+ FD_CLR(listenfd, readfds); ​​​​+ result--; ​​​​+ int connfd; ​​​​+ struct sockaddr_in clientaddr; ​​​​+ socklen_t clientlen = sizeof(clientaddr); ​​​​+ connfd = accept(listenfd,(SA *) &clientaddr, &clientlen); ​​​​+ char *p = process(connfd, &clientaddr); ​​​​+ if (p) ​​​​+ interpret_cmd(p); ​​​​+ free(p); ​​​​+ close(connfd); ​​​​+ } ​​​​ return result; ​​​​}

process() 中處理 http 請求與對應的狀態碼,與上方 process() 相同。

執行結果

cmd> web
listen on port 9999, fd is 3
cmd> accept request, fd is 4, pid is 100592 # curl http://localhost:9999/new
127.0.0.1:54346 200 - 'new' (text/plain)
q = []
cmd> accept request, fd is 4, pid is 100592 # curl http://localhost:9999/ih/1
127.0.0.1:54348 200 - 'ih 1' (text/plain)
q = [1]
cmd> accept request, fd is 4, pid is 100592 # curl http://localhost:9999/ih/2
127.0.0.1:54350 200 - 'ih 2' (text/plain)
q = [2 1]
cmd> accept request, fd is 4, pid is 100592 # curl http://localhost:9999/ih/3
127.0.0.1:54352 200 - 'ih 3' (text/plain)
q = [3 2 1]
cmd> accept request, fd is 4, pid is 100592 # curl http://localhost:9999/sort
127.0.0.1:54356 200 - 'sort' (text/plain)
q = [1 2 3]
cmd> accept request, fd is 4, pid is 100592 # curl http://localhost:9999/quit
127.0.0.1:54358 200 - 'quit' (text/plain)
Freeing queue

根據 I'm getting favicon.ico error 描述,在 <head> 欄位中增加可讀取圖示位址的程式碼 <link rel="shortcut icon" href="#" 即可。此原因是因為某些網頁瀏覽器 (如 Chrome 瀏覽器) 會要求給予網頁圖案的需求,而原本提供的 head request 中沒有對應的資訊,故瀏覽器不會回傳正確的內容,而是一直提示要求給予 favicon.ico 檔案位置。

上述程式碼已整合進 qtest,可在終端機執行以下命令:

$ ./qtest 
cmd> web
listen on port 9999, fd is 3
cmd> 

如訊息所示,這個小型的網頁伺服器正在等待埠號 9999 的網路連線。 接著開啟另一個終端機,用 curl 命令進行連線,測試命令如下:

$ curl http://localhost:9999/new
$ curl http://localhost:9999/ih/1
$ curl http://localhost:9999/ih/2
$ curl http://localhost:9999/ih/3
$ curl http://localhost:9999/sort
$ curl http://localhost:9999/quit

解決 favicon.ico 的問題

從瀏覽器發 request 後,可以看到上圖 favicon.ico 的 status 是 404。因為部分瀏覽器會要求 request 中要提供網頁圖案的對應資訊,而我上面的 response 沒有提供。

I'm getting favicon.ico error 有提供做法,就是將下面這行加進去 head 裡面即可:

<link rel="shortcut icon" href="#">

因此修改 send_response(),將上面的程式加進去:

void send_response(int out_fd)
{
    char *buf = 
        "HTTP/1.1 200 OK\r\n%s%s%s%s%s%s"
        "Content-Type: text/html\r\n\r\n" "<html><head><style>"
        "body{font-family: monospace; font-size: 13px;}"
        "td {padding: 1.5px 6px;}"
        "</style><link rel=\"shortcut icon\" href=\"#\">"
        "</head><body><table>\n";
    writen(out_fd, buf, strlen(buf));
}

再次嘗試,就都是 200 OK 了!

檢驗網頁伺服器行為是否正確

準備以下程式碼: (檔名: testweb.c)

#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

#define TARGET_HOST "127.0.0.1"
#define TARGET_PORT 9999

/* length of unique message (TODO below) should shorter than this */
#define MAX_MSG_LEN 1024
static const char *msg_dum = "GET /new HTTP/1.1\n\n";

int main(void)
{
    int sock_fd;
    char dummy[MAX_MSG_LEN];
    //struct timeval start, end;

    sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (sock_fd == -1) {
        perror("socket");
        exit(-1);
    }

    struct sockaddr_in info = {
        .sin_family = PF_INET,
        .sin_addr.s_addr = inet_addr(TARGET_HOST),
        .sin_port = htons(TARGET_PORT),
    };

    if (connect(sock_fd, (struct sockaddr *) &info, sizeof(info)) == -1) {
        perror("connect");
        exit(-1);
    }
    send(sock_fd, msg_dum, strlen(msg_dum), 0);
    recv(sock_fd, dummy, MAX_MSG_LEN, 0);

    shutdown(sock_fd, SHUT_RDWR);
    close(sock_fd);

    printf("%s\n", dummy);

    int count = 0, last_pos = 0;
    for (int i = 0; i < strlen(dummy); i++) {
        if (dummy[i] == '\n') {
            count++;
        }
        if (count == 3) {
            last_pos = i + 1;
            break;
        }
    }

    char *answer = "l = []";
    printf("%ld %ld\n", strlen(answer), strlen(dummy + last_pos));

    return 0;
}

更改 bench.c,傳送 HTTP request 給開著的 web server,並接收 HTTP response,查看是否與預期的結果相同。

testweb.c 中,傳送 new 的命令,但是回傳回來除了執行結果還多了換行和一個隨機的字元

HTTP/1.1 200 OK
Content-Type: text/plain

l = []
v
report 和 report_noreturn
extern int connfd;
void report(int level, char *fmt, ...)
{
    if (!verbfile)
        init_files(stdout, stdout);

    int bufferSize = 4096;
    char buffer[bufferSize];
    if (level <= verblevel) {
        va_list ap;
        va_start(ap, fmt);
        vfprintf(verbfile, fmt, ap);
        fprintf(verbfile, "\n");
        fflush(verbfile);
        va_end(ap);

        if (logfile) {
            va_start(ap, fmt);
            vfprintf(logfile, fmt, ap);
            fprintf(logfile, "\n");
            fflush(logfile);
            va_end(ap);
        }
        va_start(ap, fmt);
        vsnprintf(buffer, bufferSize, fmt, ap);
        va_end(ap);
    }
    if (connfd) {
        int len = strlen(buffer);
        buffer[len] = '\n';
        buffer[len + 1] = '\0';
        send_response(connfd, buffer);
    }
}

void report_noreturn(int level, char *fmt, ...)
{
    if (!verbfile)
        init_files(stdout, stdout);

    int bufferSize = 4096;
    char buffer[bufferSize];
    if (level <= verblevel) {
        va_list ap;
        va_start(ap, fmt);
        vfprintf(verbfile, fmt, ap);
        fflush(verbfile);
        va_end(ap);

        if (logfile) {
            va_start(ap, fmt);
            vfprintf(logfile, fmt, ap);
            fflush(logfile);
            va_end(ap);
        }
        va_start(ap, fmt);
        vsnprintf(buffer, bufferSize, fmt, ap);
        va_end(ap);
    }
    if (connfd) {
        send_response(connfd, buffer);
    }
}

因為原本的 web 實作中,HTTP 的 body 只會回傳 ok,現在要將原本輸出到標準輸出的結果也當作 HTTP response 傳回給 client。

qtest.c 的 do_operation 系列的函式,常常呼叫 report.c 的 reportreport_noreturn,為了將執行結果輸出到標準輸出,其中兩者差別在是否有輸出換行,輸出後有需要換行就會呼叫 report,不需要會呼叫 report_noreturn

reportreport_noreturn 檢查是否有連線,如果有連線,就將結果一起寫進 response 中,如此一來即可回傳執行結果。