網路程式設計(自制伺服器)

劉小緒同學發表於2019-01-27

    現在的網路應用隨處可見,無法想象生活離開了網路會變得怎樣,最常見的就是通過瀏覽器上網,在位址列輸入 URL 敲擊回車,然後瀏覽器就呈現出相應的頁面。雖然現在的網路應用五花八門,但是它們都是基於相同的程式設計模型,依賴相同的程式設計介面。

    每個網路應用都是基於客戶端-服務端程式設計模型的,採用這個模型,一個應用是由一個伺服器程式和一個或多個客戶端程式組成。由伺服器管理著某種資源,通過操作這些資源來為客戶端提供某種服務。需要注意的是,客戶端和服務端是程式,而不是我們常提到的機器或主機。

    在系統級 I/O中已經提到過,網路也僅僅是一種檔案而已。一個插到 I/O 匯流排擴充套件槽的網路介面卡提供了網路的物理介面。從網路上接收到的資料從介面卡經過 I/O 和記憶體匯流排複製到記憶體;當然資料也同樣可以從記憶體複製到網路。

    網際網路絡由大大小小的區域網和廣域網組成,這些區域網和廣域網可能採用了完全不同和不相容的技術,網際網路絡需要解決的問題是把這些不相容的網路連線起來。解決辦法就是一層執行在每臺機器和路由器上的協議軟體,這種解決方案隨處可見,比如 java 之所以誇平臺是因為不同平臺上的 JVM 做了適配。

    這個協議軟體控制路由器和主機如何協同工作來實現資料傳輸,在命名上定義一種統一的主機地址格式消除差異;在資料傳輸上通過定義一種把資料位捆綁成不連續的片(包)來消除差異,一個包由包頭和有效載荷組成,包頭包括包的大小以及源主機和目的主機的地址,有效載荷包括從源主機發出的資料位。

    全球 IP 因特網是最著名最成功的網際網路絡實現,每臺因特網主機都執行實現了 TCP/IP 協議的軟體。一個 IP 地址就是一個 32 位無符號整數,如下是 IP 網路程式存放的 IP 地址結構,它總是以大端法(網路位元組順序)存放。

struct in_addr {
    uint32_t s_addr;
}

    因特網客戶端和伺服器相互通訊使用的是 IP 地址,但是這對於人們而言太不友好了,所以因特網定義了一組更加人性化的域名,並且定義了域名集合和 IP 地址集合之間的對映。在 1988 年之前,這個對映都是通過一個叫做HOSTS.TXT的文字檔案來手工維護的,此後改為通過 DNS(域名系統)來維護。

    客戶端和服務端通過在連線上傳送和接收位元組流來通訊,這個連線是點對點的,並且是全雙工的。一個套接字就是連線的一個端點,每個套接字都有相應的套接字地址,由一個因特網地址和一個 16 位整數埠組成。客戶端套接字地址中的埠是核心自動隨機分配的,稱之為臨時埠;而服務端通常是很著名的埠,比如 Web 伺服器用 80 埠,電子郵件伺服器用 25 埠,並且每個知名埠都有其相應的服務名。下面是套接字地址的結構。

// _in 是 internet 的縮寫
struct scoket_in {
    uint16_t sin_family; // 協議族
    uint16_t sin_port; // 埠號
    struct sin_addr; // IP 地址
    unsigned char sin_zero[8]; // sizeof(struct sockaddr)
}

struct socketaddr {
    uint16_t sa_family;
    char sa_data[14];
}

    核心提供了編寫網路應用所需要的函式,基本看函式名稱就知道它的功能了,下面展示了一小部分函式。

int socket(int domain, int type, int protocol);
int connect(int clientfd, const struct sockadd *addr,
            socklen_t addrlen);
int bind(int sockfd, const struct sockaddr *addr,
            socklen_t addrlen);
int listen(int sockfd, int backlog);
int accept(int listenfd, struct sockaddr *addr,
            int *addrlen);

int getaddrinfo(const char *host, const char *service,
            const struct addrinfo *hints,
            struct addrinfo **result);
int getnameinfo(const struct sockaddr *sa, 
            socklen_t salen, char *host, size_t hostlen,
            char *service, size_t servlen, int flags);
............

    像用於轉換二進位制和字串表示的getaddrinfo()getnameinfo()函式是可重入的。通過核心提供的這些函式就可以編寫出自己的 web 伺服器。

    可重入函式主要用於多工系統中,簡單講就是可中斷函式,在這個函式執行的任何階段打斷它,作業系統去呼叫另一段程式碼,再返回控制時不會出現什麼錯誤。因為它除了使用自己棧上的變數外不依賴於任何環境。注意可重入函式不一定是執行緒安全的。

    最後是原書作者自己實現的一個 TINY Wed 伺服器,用 C 實現的,在 linux 環境下編譯執行即可訪問,下圖是這個伺服器執行的效果,訪問連結http://127.0.0.1:8000/cgi-bin/adder?3&7就可以得到運算結果。整個程式程式碼也不長,我準備好好看看再抄一遍,原始碼可訪問連結

image

相關文章