--- tags: CSAPP, 作業系統 --- # CSAPP: Chapter 11 :::info 僅記錄最粗淺的概念,詳細的例子請直接參考原書籍或者課程投影片: * [Network Programming: Part I](http://www.cs.cmu.edu/afs/cs/academic/class/15213-m19/www/lectures/21-netprog1.pdf) * [Network Programming: Part II](http://www.cs.cmu.edu/afs/cs/academic/class/15213-m19/www/lectures/22-netprog2.pdf) ::: ## Network Concept 大多的網路應用都是基於 client-server 模型,每個應用由一個 server process 和一個或者多個 client process 組成。而 client 和 server 間的操作主要由 4 個步驟所組成: 1. client 需要服務時,發出請求(request)給 server 2. server 解讀該請求並操作本身的資源以提供服務 3. server 響應(response)請求回 client 4. client 處理來自 server 的響應 ![](https://i.imgur.com/qobnPLe.png) 物理上,網路是地理的遠近層層組織起的系統。最低層次的 [LAN](https://zh.wikipedia.org/zh-tw/%E5%B1%80%E5%9F%9F%E7%BD%91) 是連接校園或者建築等有限範圍內的網路,大致由 hub,bridge 和 host 的集合形成,多個*不相容*的 LAN 則可由 router 所連接起,形成更高層次的 [WAN](https://zh.wikipedia.org/zh-tw/%E5%B9%BF%E5%9F%9F%E7%BD%91),建構起一個 internet。 ![](https://i.imgur.com/N9t8Jvr.png) internet 的重要特性是可以由不兼容的 LAN 和 WAN 所構成,何以使得數據的傳輸可以跨越這些不兼容的網路呢? 關鍵在於 host 和 router 之間的 protocal software。Protocol software 定義了一系列的規則使得 host 和 router 之間可以協同工作以傳輸數據。這種協議必須提供兩種基本的能力: 1. 命名機制: 每個 host 和 rounter 需要擁有至少一個且唯一的地址 2. 傳輸機制: 標準的資料傳送單元 -- 封包 (packet),每個封包包含了 header 和 payload。header 儲存封包的大小、其來源以及傳送的地址等資訊,payload 則乘載來自 host 所傳輸的數據本身。 如圖粗略的展示了 host A 如何與 router 協同,運用 protocal software 傳遞到 host B(顯然 protocal 背後還有複雜的議題需要考量,但並非本文之重點)。簡而言之,internet 的思想精隨在於層層的封裝。 ![](https://i.imgur.com/iWAAwSS.png) 一個 client-server 的網路應用之基本硬體與軟體的組成如下圖所示。在現代的電腦,作業系統一般都支持 [TCP/IP protocal](https://zh.wikipedia.org/zh-tw/TCP/IP%E5%8D%8F%E8%AE%AE%E6%97%8F),client 和 server 之間可以透過 I/O 和 sockets interface 函式的使用來進行溝通,後者經常被實現為 system call,當呼叫時會陷入 kernel 之中並且使用 kernel 中的 TCP/IP 函數。 ![](https://i.imgur.com/2GH6CBa.png) 如果從 programmer 的角度來看,可以把 internet 當成是存在世界的 host 集合: 1. 每個 host 被映射到一個 32 位元的 IP address 2. 這個 IP address 被映射到一個 [Internet domain names](https://en.wikipedia.org/wiki/Domain_name) 3. Internet 上的 host process 可以通過 "connection" 和其他的 host 進行 通信(communication) ### IP address ```cpp struct in_addr { uint32_t s_addr; /* network byte order (big-endian) */ }; ``` 32 位元的 IP 位址被存在如上的結構中: * IP 位址為 network byte order / big-endian order 由於 host 的記憶體存放不一定總是 big-endian order,UNIX 提供了一系列的函數來協助轉換: > [htonl](https://linux.die.net/man/3/htonl) ```cpp #include <arpa/inet.h> uint32_t htonl(uint32_t hostlong); uint16_t htons(uint16_t hostshort); uint32_t ntohl(uint32_t netlong); uint16_t ntohs(uint16_t netshort); ``` 此外,IP 位址通常是透過 [Dotted Decimal Notation](https://en.wikipedia.org/wiki/Dot-decimal_notation) 來表示的,例如 128.2.194.242 就是 s_addr = 0x80_02_c2_f2。而 programmer 可以通過函數來對進行兩者之間的轉換: * [inet_pton](https://man7.org/linux/man-pages/man3/inet_pton.3.html) * [inet_ntop](https://man7.org/linux/man-pages/man3/inet_ntop.3.html) ### Internet Domain Names client 和 server 間的通信是透過 IP address,不過對人類來說這些數字的組成並不直觀也不容易記憶,因此 domain name 也被定義。domain name 由一串用句點分割的 單詞組成。 domain name 具有層次結構的關係,可以由樹狀的結構被表示出來,如下圖。Internet 從 IP 到 domain name 的關係,被稱為 [Domain Naming System (DNS)](https://zh.wikipedia.org/zh-tw/%E5%9F%9F%E5%90%8D%E7%B3%BB%E7%BB%9F) 的分散式資料庫所維護。 ![](https://i.imgur.com/RCPxGNl.png) 在 Linux 下可以藉由 nslookup 命令可以探究 DNS 的映射關係,展示每個 domain name 對應的 IP address。 ### Connetion client 和 server 通過在 connection 上傳遞 bytes stream 來進行溝通,對於一對 process 來說,連接是點對點的,且數據可以同時雙向的傳遞。socket 是連接的端點,由 **IP 地址: port** 組成。port 是用來區分 process 的 16 位元整數,當 client 端發起連接請求時,通常 port 是由 kernel 自動分配的臨時 port(Ephemeral port);而 server 端一般則是有固定的 port number(Well-known port),例如 80 代表 web server。一個 connection 則都過一對 socket address 來確定 ## Web server ### Overview Web client 和 server 間的交互是透過 [HyperText Transfer Protocol (HTTP)](https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol) 進行的: 一個 client(瀏覽器) 會透過 TCP 和 server 連接,發出某些請求後,server 會響應某些內容,然後關閉連接,client 讀取這些響應的內容並展示在畫面上。 ### Web 內容 Web 的具體內容是一個 [MIME](https://en.wikipedia.org/wiki/MIME) 類型相關的 bytes 序列,常用類型例如: * text/html: Web 內容可以通過 [HTML](https://en.wikipedia.org/wiki/HTML) 來編寫,後者可以告訴瀏覽器如何顯示頁面中的文本和圖形 * text/plain: 無格式的文檔 * image/jpeg: jpeg 格式編碼的 binary image * image/png: png 格式編碼的 binary image HTTP response 之內容首先可以分為動態或者靜態: ![](https://i.imgur.com/dl8XjzS.png) 每個 Web server 返回的內容都是和其管理的文件相關的,這些文件都有唯一的名稱,叫做 URL (Universal Resource Locator)。例如 http://www.cmu.edu:80/index.html 這個 URL 表示: * http:// : protocal * www.cmu.edu : server 位址 * :80 : 使用的 port * /index.html: 1. server 用來決定請求的是動態還是靜態內容(沒有標準,每個 server 有自己的規則) 2. 最開始的 `/` 不代表 Linux 的根目錄,而是根據請求內容類型的主目錄 3. 最小的 URL suffix 是 `/` ,server 會將其擴展成某個預設的文件名稱(通常是 `index.html`) 4. url 可以在文件名後面加上參數,由 `?` 分隔且每個參數由 `&` 分隔 ### HTTP request HTTP request 的組成是一個 request line,後面跟隨著 0 個或者多個 request header,並結束於 "\r\n": * request line: `<method> <uri> <version>` * method: GET / POST 等 HTTP 支援的方法 * uri: url 的 suffix (文件名稱+可選參數) * version: HTTP 版本(HTTP/1.0 or HTTP/1.1) * request headers: `<header name>: <header data>` 提供額外的資訊給 server ### HTTP response HTTP response 的組成是一個 response line,後面跟隨著 0 個或者多個 response header,再後面是 "\r\n",最後跟隨一個 response body * response line: `<version> <status code> <status msg>` * version: HTTP 版本 * status code: 一個 3 位的正整數表示對 request 的處理 * status msg: 對 status code 的描述 * 200 OK Request was handled without error * 301 Moved Provide alternate URL * 404 Not found Server couldn’t find the file * response header: : `<header name>: <header data>` 提供額外的資訊給 client ## Sockets Interface Sockets interface 是一組系統層級的函數,藉由與 UNIX I/O 函數的結合,可以用來建立網路應用。 ### Socket 從 kernel 的角度來看,socket 即為連接的端點;而從應用程式的角度來看,socket 則是一個 file descriptor 使得應用程式可以讀寫網路。下面的圖給出了一個典型的 client-server model。 ![](https://i.imgur.com/qQuzQs3.png) ### Socket address socket address 透過下面的資料結構 `struct sockaddr_in` 儲存。`sin_family` 恆為 `AF_INET`,`sin_port` 是一個 16 位元的 port number,`sin_addr` 是一個 32 位元的 ip address,後兩者是以 network byte order 存放在記憶體中的 ```cpp struct sockaddr_in { uint16_t sin_family; /* Protocol family (always AF_INET) */ uint16_t sin_port; /* Port num in network byte order */ struct in_addr sin_addr; /* IP addr in network byte order */ unsigned char sin_zero[8]; /* Pad to sizeof(struct sockaddr) */ }; ``` 值得一提的是 `struct sockaddr` 這個結構。此結構大小和 `struct sockaddr_in` 相同。因為 `connect`, `bind`, and `accept` 需要允許接受的不同類型的 socket address structure,而早期的 C 並沒有 `void *` 這種通用指標,因此定義了通用型的結構 `struct sockaddr`,然後應用程式在使用這些函數時再自己的特定結構強制轉換成通用結構來進行使用。 ```cpp struct sockaddr { unsigned short sa_family; char sa_data[14]; }; ``` ### `getaddrinfo` Linux 中提供了強大的 [getaddrinfo](https://man7.org/linux/man-pages/man3/getaddrinfo.3.html),可以將 hostnames, host addresses, ports, service names 轉成對應的 `struct sockaddr`。相較於已經棄用的 `gethostbyname` 和 `getservbyname`,其優勢為 reentrant,且適用於 IPv4 或 IPv6。 ```cpp int getaddrinfo(const char *host, /* Hostname or address */ const char *service, /* Port or service name */ const struct addrinfo *hints,/* Input parameters */ struct addrinfo **result); /* Output linked list */ struct addrinfo { int ai_flags; /* Hints argument flags */ int ai_family; /* First arg to socket function */ int ai_socktype; /* Second arg to socket function */ int ai_protocol; /* Third arg to socket function */ char *ai_canonname; /* Canonical host name */ size_t ai_addrlen; /* Size of ai_addr struct */ struct sockaddr *ai_addr; /* Ptr to socket address structure */ struct addrinfo *ai_next; /* Ptr to next item in linked list */ }; ``` 給定 `host` 和 `service`(socket address 組成的兩部分),`getaddrinfo` 返回 `result`,後者是一個指向 `addrinfo` 的 linked list。使用的情境通常是: * 對於 client: 遍歷這個 list,並嘗試對每個 socket address 進行 `socket` + `connect` 直到成功 * 對於 server: 遍歷這個 list,並嘗試對每個 socket address 進行 `socket` + `bind` 直到成功 ![](https://i.imgur.com/rs0TwEK.png) 其他注意事項: * linked list 需要透過 [`freeaddrinfo`](https://linux.die.net/man/3/freeaddrinfo) 釋放 * 如果 getaddrinfo 返回非零的 error code,可以呼叫 [`gai_streeror`](https://linux.die.net/man/3/gai_strerror) 得到對應的錯誤訊息 getaddrinfo 的好處是其所得到的返回結構可以直接傳遞給 socket interface 對應的參數使用,這讓 server 和 client 端可以獨立於特定版本的 IP 協議。 ### `getnameinfo` [`getnameinfo`](https://man7.org/linux/man-pages/man3/getnameinfo.3.html) 是 `getaddrinfo` 的相反運作。可以將 `struct sockaddr` 轉換為對應的 hostnames 和 service names。相較於已經棄用的 `getservbyport` 和 `gethostbyaddr`,其優勢為 reentrant,且適用於 IPv4 或 IPv6。 ```cppp int getnameinfo(const SA *sa, socklen_t salen, /* In: socket addr */ char *host, size_t hostlen, /* Out: host */ char *serv, size_t servlen, /* Out: service */ int flags); /* optional flags */ ``` :::info note: 之後為了方便我們都使用如下定義 ```cpp typedef struct sockaddr SA; ``` ::: ### `socket` 我們可以將透過 `getaddrinfo` 得到的資訊輸入到 [`socket`](https://man7.org/linux/man-pages/man2/socket.2.html) 來開啟一個 socket 的 file descriptor。 ```cpp int socket(int domain, int type, int protocol) ``` 或者也可以透過下面的方式 hard code 參數內容,其中 `AF_INET` 代表 32 bits IP address,而 `SOCK_STREAM` 則表示該 socket 是連接的一個端點。 ```cpp int sockfd = socket(AF_INET, SOCK_STREAM, 0); ``` 注意到 `socket` 所返回的 fd 僅僅是部分開啟的,要對其讀寫需要透過其他 API 來決定他到底是 server 還是 client。 ### `bind` server 可以使用 [`bind`](https://man7.org/linux/man-pages/man2/bind.2.html) 來將 server address `addr` 和 sokcet 連結起來。 ```cpp int bind(int sockfd, SA *addr, socklen_t addrlen); ``` * `addrlen` 就是 `sizeof(sockaddr_in)` ### `listen` server 透過 [`listen`](https://man7.org/linux/man-pages/man2/listen.2.html) 函數告訴 kernel descriptor 將由 server 而不是 client 做使用,讓 socket 可以等待來自 client 的連接請求。 ```cpp int listen(int sockfd, int backlog); ``` ### `connect` client 端則透過 [`connect`](https://man7.org/linux/man-pages/man2/connect.2.html) 來建立與 server 的連接。 ```cpp int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); ``` * addrlen 是 `sizeof(socketaddr_in)` * connect 呼叫後會被 block 住直到建立成功或是發生錯誤 ### `accept` 透過 `listen` 確定特定 socket 為 server 端後,可以對其使用 [`accept`](https://man7.org/linux/man-pages/man2/accept.2.html),等待來自 client 的請求到達參數的 `listenfd`,然後參數的 `addr` 會被填入 client 的 socket address,並返回連接對象的 descriptor,後者可被用來透過 UNIX I/O 進行通信。 ```cpp int accept(int listenfd, SA *addr, int *addrlen); ``` :::info 在原書本中的 echo server 和 tiny server 展示了實際使用 socket interface 的操作,推薦去看看! :::