---
tags: CSAPP
---
# Chapter 11: Network Programming
[課程錄影 Part I](https://www.youtube.com/watch?v=OynSMXfNtiM&list=PLcQU3vbfgCc9sVAiHf5761UUApjZ3ZD3x&index=22&ab_channel=RobbieZhou) / [課程錄影 Part II]()
這裡部會講述太難的網路概念,僅說明一個系統軟體工程師對於網路所需要的基本概念
[TOC]
## 網路基本概念

> 一切的概念都是從 `client-server` 開始的
舉例來說:現在你想在亞馬遜上買東西
1. 你想點進去看一個商品
2. 亞馬遜收到你要看的需求
3. 亞馬遜提供那個商品的資料給你,透過網頁顯示的方式
4. 你決定下一步你要怎麼做,可能是加入購物車、看其他商品等
當然,你也可以同時是客戶端與服務器,例如:你現在打電話給別人

對於網路,電腦將他視為 `I/O` 對應硬碟,從網路獲取的資料經過`I/O` 和內存總線(BUS)複製到內存;相反的,也可以從內存將資料複製到網路。這個概念適用於目前常見的作業系統,例如:windows, macOS, UNIX...

:::info
這邊我們如果是用 "internet" 代表描述一般的網路概念;"Internet"則是描述一種具體的實現手法,例如通訊協定等等
:::

- 這邊介紹最低階的網路(LAN, Local Area Network),利用集線器(Hub)將主機相連。利用集線器的端口,可以將接收到的資訊複製到其他端口上
- 這種網路結構通常出現在校園或同一棟建築物內
- 每個乙太網路適配器都有一個實體位置(MAC address, Media Access Control address),他是48位元組(bit)
- 例如:`46:85:fd:b3:3b:f2`
- 但是因為集線器成本太高,目前都換成路由器(Routers)
- 任何一個主機都可以發送資料給其他主機,其資料名為幀(Frame),每一個幀都有固定位元的頭(header)及有效載荷(Payload)。頭(header)是用來看這一幀的目的與長度,有效載荷是存放資料用的
> Hosts send bits to any other host in chunks called frames

- 多個乙太網路可以連接成更大的乙太網路,透過網橋(Bridge)來連接,往橋間傳輸的數據線速度與一般的路由器不同,往橋和往橋間的帶寬是 1Gb/s,一般的主機與路由器的帶寬是 100Mb/s。
- 利用網橋可以將網路的範圍擴大

我們可以將區域網路簡化成上圖,可以想像成一堆主機在對話

- 網路則是很多個地方網路(LAN)透過路由器連接起來,而路由器之間就會有通訊協定,通訊協定就是不同的地區網路用同一種語言溝通
- 上面這張投影片中,`LAN1`和`LAN2`是不同的區域網路,因此這兩種網域的溝通就要經過協調,也就是通訊協定

現實生活中也是利用這些概念進行,只是範圍擴大到全世界,這種網路稱為廣域網路(Wide Area Network, WAN)

- 要怎麼在這些不同的網域傳輸資料呢?
- 首先定義一個通訊協定,他是用來規範主機與路由器在各個網域之間傳輸資料的方式
- 這個通訊協定可以將不同網域的差異變小

- 通訊協定是用來做什麼的呢?
- 定義命名方式
- 當我們要寄信時,我們必須說出地址,而地址就會有規範,這樣郵差才可以把信送到你想寄的地方
- 然而這種規範在剛開始執行時並不順利,因為網路在傳輸時就像寄信一樣,也有信件的大小限制。剛開始執行時,一個封包大約限制在2000位元組,因此如果有很長的資訊,必須拆成很多個封包寄送
- 但是電話不是,電話有專門的線路,而電信公司也會負責這點,確保你打電話時可以保持通訊,不會有人打擾
- 定義傳輸方式
- 定義一個封包(packet)的標準單位
- 這個資料包必須包含頭部(header)和有效載荷(Payload)
- 頭部(header):包含這個封包的大小、來源地及目的地
- 有效載荷(Payload):這個封包的資料

這張投影片介紹傳輸資料的8個基本步驟:
1. 主機A將資料及主機B的虛擬地址複製到記憶體(內存)中
2. 將資料包在網路的封包內,此時網路封包的有效載荷(Payload)為要傳遞的資訊,封包頭部(Packet Header, PH)為傳輸目的地的地址。再將這個封包裝在地區網路的幀內,此時幀的頭部(Frame Header)為路由器的地址,幀的有效載荷(Payload)為整個網路封包
- 先將幀從主機A封裝後送到協議軟體,再將幀送到LAN1上
| data| Packet Header(PH) | Frame Header(FH) |
| -------- | -------- | -------- |
|Frame Payload|Frame Payload| Frame Header|
3. 從LAN1將幀複製到網路上(區域網路中,==由主機A→路由器==)
4. 當幀到達路由器時,路由器的LAN1適配器會讀取他,並將其送到協議軟體內
5. 路由器從PH找到目的地,並將他送到該網域的適配器。
- 此時路由器會將LAN1的FH消除,加上LAN2的FH
6. 路由器的LAN2適配器複製幀到網路上(區域網路,==由路由器→主機B==)
7. 當幀到達主機時,適配器到從電纜中讀取,並將他送到協議軟體內
8. 剝落PH, FH取得資料
> 將頭部加上去的動作就叫做封裝

:::success
延伸問題:
1. 路由器怎知道要轉發到哪
2. 網路拓樸變化時,怎麼通知路由器
3. 封包會不會丟失
:::

- 常用的網路協定包含以下三種:`IP`、`UDP`及`TCP`
- IP(Internet Protocol)
- 定義主機的命名方式
- 定義怎麼傳送封包(Packet, Datagram)
- 資料丟失的現象:數據線過熱,如果數據丟失,IP層不會試圖恢復,因此這一層的傳輸不是可靠的
- ==主機對主機()==
- UDP(Unreliable Datagram Protocol)
- 這層比IP層還要高一點
- 這裡可以傳輸電腦遊戲(線上遊戲),就是沒有傳到也不會怎麼樣
- TCP(Transmission Control Protocol)
- 百分之99都是使用這個TCP進行傳輸,他也在IP層之上
- TCP提供一個和電話一樣穩定通訊的方式,他會將資料分為很多個封包,如果對方沒有收到資料,就會重複發到接收為止
- 使用 `socket` 介面寫程式

這邊可以看到,可以利用 `socket` 從 `User Mode` 就可以進行資料傳輸

- `IP address`:由四個十進位的數字用點連起來,這四個數字只能從0~255
- `IP address`用十進位表示,但是我們也可以使用16進位表示他
- 這邊需要注意,`IP address`是`Big Endian`,而一般的作業系統都是使用`Little Endian`
- 利用`IP address`可以知道你在用哪個網域,例如`128.2`就是CMU的`IP address`

- 因為`IPv4` 是 32 位元的地址,但是世界太多人了,地址可能不夠用,因此有了`IPv6`,也就是128位元的地址
- 但是到了現在,儘管`IPv6`已經出現了15年,人類已經找出如何用`IPv4`的表示很多地址,因此`IPv6`還是很少人用

- `IP address` 需要將 `Big Endian`轉換成 `Little Endian`,這邊可以使用標準函式庫

- 剛剛談到, `IP Address` 是用四個十進位表示,而他也可以轉換成16進位,這時利用標準函式庫即可得到
- `0x8002C2F2 = 128.2.194.242`

- 今天我們使用 `GOOGLE` ,我們不會使用他的IP,相反的我會使用網址,`www.google.com`
- 我們也都知道,`.com`是指商業公司、`.edu`是指教育機構等等,當然不同的地區也會有不同的後綴,例如`.de`代表德國、`.us`代表美國等等
- 但是機器並不認得這些網址,因此會有一個字典找到`IP`與網域間的關係,也就是 `Domain Naming System(DNS)`

- 不用把 `DNS` 養得太複雜,他其實就是一個映射函數,也就是給他網域名稱,他就給你 `IP` 地址,反之亦然



- 這邊我們可以利用系統,給予電腦網域名稱就可以讓他給我 `IP` 地址
- 這邊我們做個實驗
```shell=
$ nslookup www.cs.cmu.edu
Server: 127.0.0.53
Address: 127.0.0.53#53
Non-authoritative answer:
www.cs.cmu.edu canonical name = scs-web-lb.andrew.cmu.edu.
Name: scs-web-lb.andrew.cmu.edu
Address: 128.2.42.95
```
- 如果我們給予一個不存在的網域,則會返回沒有回應
```shell=
Not find www.cs.standford.edu: No answer
```
- 但是網域名稱與`IP`並不是一一對應的,是==多對多映射的==
```shell=
Name: twitter.com
Address: 104.244.42.65
Name: twitter.com
Address: 104.244.42.129
```
- 這邊也可以看到,如果我們想要到同一個網域,他可能會給我們不同地址,這些地址都在變化
- `IP`地址的分配分為兩種,一種是靜態分配,這種地址不會變;另外一種是動態分配,這種地址可能是一段時間之內分配給你的

- 目前主流的連接方式是透過`TCP`,它可以使兩台主機進行連接,而且類似電話依樣不會有數據突然消失的問題
- `socket` 包含一個`IP`地址和一個端口,端口號是16個位元組
- 一個機器有很多服務,而不同端口號可以對應到不同服務
- 臨時端口(Ephemeral Port):客戶端的端口是由客戶端核心分配的
- 知名端口(Well-Known Port)

目前比較熱門的服務器都會指派這些知名端口(Well-Known Ports)進行,如上

假設現在連線開始,客戶端視使用臨時接口,要求連接伺服器第80號接口

伺服器第80號接口是網路服務,如果想要使用其他服務的話需要連接其他端口,而這些端口是獨立的不會互相影響

Socket Interface(台譯:插座介面、陸譯:套接字接口)
- 對於核心來說,`socket`是一個連線的終點
- 對於應用程式來說,`socket`就像是硬碟一樣,只是對象不是電腦中的硬體而是網路

## `socket API` 套接字接口
前兩的字節(Bytes)是描述這個`socket`是甚麼類型的?
例如:`TCP`、`IPv6`、`UDP`

本圖的左邊是客戶端、右邊是伺服器端
- 前置動作
- 客戶端:先搞清楚目的地在哪、想要什麼服務
- 伺服器端:
- 客戶端連接伺服器端,接收之後開始資料傳輸
- 這邊的函數加上`roi`代表 reliable i/o
- 如果客戶端掉線了
- 伺服器端也會取消客戶端的連接
以 linux 核心的角度來說,一個套接字就是通信的一個端點,以程式碼的角度來說,套接字就是一個文件
```c=
struct sockaddr_in{
uint16_t sin_family; /* Protocol Family (Always AF_INET) */
uint16_t sin_port; /* Port number in network byte order */
struct in_addr sin_addr; /* IP Address */
unsigned char sin_zero[8];
};
struct sockaddr{
uint16_t sa_family;
char sa_data[14];
};
```
---
### `socket` 常用函式
- `socket` 函式 (套接字描述符)
```c
int socket(int domain, int type, int protocol);
```
如果想將 `socket` 變成一個端點,可以用編碼的參數調用:
```c
clientfd = Socket(AF_INET, SOCK_STREAM, 0);
// AF_INET: 使用32位元的IP地址
// SOCK_STREAM: socket連接的一的端點
```
- 最好是使用 `getaddrinfo` 來自動生成這些參數
- `socket` 的 `clientfd` 是部分打開的,還不能用於讀寫。如何打開套接字的工作取決於我們是客戶端還是服務器。
- 客戶端的函數
- `connect` 函式
```c
int connect(int clientfd, const struct sockaddr *addr, socklen_t addrlen);
```
`connect` 函數是將客戶端和伺服器連接,啟動連接後,`connect` 會停止一陣子,直到確定連接伺服器或是連接失敗才會繼續動作
如果成功,`clientfd` 描述現在就準備好可以讀寫了,得到的連接是socket字對
```
(x:y, addr.sin_addr:addr.sin_port)
```
其中 `x`, `y` 分別對應客戶端的地址與端口,它唯一確定了客戶端主機上的客戶端進程。對於socket最好的方式是用 `getaddrinfo` 來未connect提供參數
- 伺服器的函數
- `bind` 函式
```c
int bind(int sockfd, const struct *addr, socklen_t addrlen);
```
bind函數告訴系統核心將 `addr` 中的伺服器套接字 (socket) 地址和套接字描述符 `sockfd` 連接起來
- `listen` 函式
- ==客戶端是發起連接的主動實體、服務氣勢等待客戶端連接請求被動實體==
- listen 函數是將 sockfd 轉換成 listening socket,這個 socket 可以接受客戶端的連線請求
- `accept` 函式
```c
int accept(int listenfd, struct sockaddr *addr, int *addrlen);
```
- `accept` 等待來自可護端的連線請求到達偵聽描述符 listenfd,然後在 addr中填寫客戶端的套接字地址,返回一個已連接描述符(Connected Descriptor)
- 過程如下:
- 第一步、服務器調用 accept,等待連接請求到達監聽描述符 listenfd,此時設定描述符為 3
- 第二步、客戶端調用 connect 函式,發送請求到 listenfd
- 第三步、accept 打開一個新的已連接描述符 connfd,描述符為 4,在 clientfd 和 connfd 建立連線,最後返回 connfd 給應用程式,流程如下圖所示

:::info
為什麼要分為監聽描述符和已連接描述符?
因為在並行 (Concurrency) 的服務器中,監聽描述符可以一次處理很多客戶端的連接,每次請求一個連接時,監聽描述符都可以 fork 初一個新的行程,使得已連接描述符可以與之連接。
:::
- 主機和伺服器的轉換函數
- getaddrinfo 函式
將相應的主機名稱、主機地址、服務器名稱和字串符號轉換成套接字地址結構
```c
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
int getaddrinfo(const char *host, const char *service,
const struct addrinfo *hints,
struct addrinfo **results);
void freeaddrinfo(struct addrinfo *result);
```

在客戶端調用 getaddrinfo 之後會一次走訪這個資料結構中的地址,直到調用 socket 和 connect 成功;相似的、主機則是會走訪這個資料結構中的地址,直到 socket 和 bind 成功。
- getnameinfo 函式
與 getaddrinfo 相反,他是將套接字地址結構轉回相應的主機與服務器的字串
- 套接字接口的輔助函式
- open_clientfd 函式
- open_listenfd 函式
- echo 客戶端和服務器的示例
## Web 服務器
- Web 基礎
- Web 內容
- HTTP 事務
- 服務動態內容
## Tiny Web 服務器