changed 3 years ago
Linked with GitHub

Chapter 11: Network Programming

課程錄影 Part I / 課程錄影 Part II

這裡部會講述太難的網路概念,僅說明一個系統軟體工程師對於網路所需要的基本概念

網路基本概念

一切的概念都是從 client-server 開始的

舉例來說:現在你想在亞馬遜上買東西

  1. 你想點進去看一個商品
  2. 亞馬遜收到你要看的需求
  3. 亞馬遜提供那個商品的資料給你,透過網頁顯示的方式
  4. 你決定下一步你要怎麼做,可能是加入購物車、看其他商品等

當然,你也可以同時是客戶端與服務器,例如:你現在打電話給別人


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

這邊我們如果是用 "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)透過路由器連接起來,而路由器之間就會有通訊協定,通訊協定就是不同的地區網路用同一種語言溝通
  • 上面這張投影片中,LAN1LAN2是不同的區域網路,因此這兩種網域的溝通就要經過協調,也就是通訊協定


現實生活中也是利用這些概念進行,只是範圍擴大到全世界,這種網路稱為廣域網路(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
  1. 從LAN1將幀複製到網路上(區域網路中,由主機A→路由器)
  2. 當幀到達路由器時,路由器的LAN1適配器會讀取他,並將其送到協議軟體內
  3. 路由器從PH找到目的地,並將他送到該網域的適配器。
    • 此時路由器會將LAN1的FH消除,加上LAN2的FH
  4. 路由器的LAN2適配器複製幀到網路上(區域網路,由路由器→主機B)
  5. 當幀到達主機時,適配器到從電纜中讀取,並將他送到協議軟體內
  6. 剝落PH, FH取得資料

將頭部加上去的動作就叫做封裝

延伸問題:

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

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


這邊可以看到,可以利用 socketUser Mode 就可以進行資料傳輸

  • IP address:由四個十進位的數字用點連起來,這四個數字只能從0~255
    • IP address用十進位表示,但是我們也可以使用16進位表示他
    • 這邊需要注意,IP addressBig 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 地址

  • 這邊我們做個實驗

    ​​​​$ 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
    • 如果我們給予一個不存在的網域,則會返回沒有回應
      ​​​​​​​​Not find www.cs.standford.edu: No answer
    • 但是網域名稱與IP並不是一一對應的,是多對多映射的
      ​​​​​​​​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是甚麼類型的?

例如:TCPIPv6UDP


本圖的左邊是客戶端、右邊是伺服器端

  • 前置動作
    • 客戶端:先搞清楚目的地在哪、想要什麼服務
    • 伺服器端:
  • 客戶端連接伺服器端,接收之後開始資料傳輸
    • 這邊的函數加上roi代表 reliable i/o
  • 如果客戶端掉線了
  • 伺服器端也會取消客戶端的連接

以 linux 核心的角度來說,一個套接字就是通信的一個端點,以程式碼的角度來說,套接字就是一個文件

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 函式 (套接字描述符)
    ​​​​int socket(int domain, int type, int protocol);
    
    如果想將 socket 變成一個端點,可以用編碼的參數調用:
    ​​​​clientfd = Socket(AF_INET, SOCK_STREAM, 0);
    ​​​​// AF_INET: 使用32位元的IP地址
    ​​​​// SOCK_STREAM: socket連接的一的端點
    
    • 最好是使用 getaddrinfo 來自動生成這些參數
    • socketclientfd 是部分打開的,還不能用於讀寫。如何打開套接字的工作取決於我們是客戶端還是服務器。
  • 客戶端的函數
    • connect 函式

      ​​​​​​​​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 函式
      ​​​​​​​​int bind(int sockfd, const struct *addr, socklen_t addrlen);
      
      bind函數告訴系統核心將 addr 中的伺服器套接字 (socket) 地址和套接字描述符 sockfd 連接起來
    • listen 函式
      • 客戶端是發起連接的主動實體、服務氣勢等待客戶端連接請求被動實體
      • listen 函數是將 sockfd 轉換成 listening socket,這個 socket 可以接受客戶端的連線請求
    • accept 函式
      ​​​​​​​​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 給應用程式,流程如下圖所示

為什麼要分為監聽描述符和已連接描述符?

因為在並行 (Concurrency) 的服務器中,監聽描述符可以一次處理很多客戶端的連接,每次請求一個連接時,監聽描述符都可以 fork 初一個新的行程,使得已連接描述符可以與之連接。

  • 主機和伺服器的轉換函數
    • getaddrinfo 函式
      將相應的主機名稱、主機地址、服務器名稱和字串符號轉換成套接字地址結構

      ​​​​​​​​#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 服務器

Select a repo