contributed by <kaeteyaruyo
>
Computer Networking
在本節中,我們會來探討傳輸層的多工與解多工,也就是把網路層所提供的主機對主機傳輸服務擴展成為執行在主機上的應用程式們提供的程序對程序傳輸服務。為了讓本節的說明內容更具體,我們會基於網際網路的情境來探討基本的傳輸層服務。但我們必須再三強調,多工和解多工這樣的服務,對所有的電腦網路來說都是需要的。
在目標主機中,傳輸層會收到他正下方的網路層所丟上來的區段。傳輸層有義務把他收到的這個區段裡頭所裝的資料確切送給執行在這台主機上的某個對應的應用程式。我們來看一個例子:假設你現在就坐在你的電腦前面,正在從網頁上下載東西,同時你的電腦上還執行著一個 FTP 連線和兩個 Telnet 連線。因此現在總共有四個應用程式正在執行 —— 兩個 Telnet 程序、一個 FTP 程序和一個 HTTP 程序。當你電腦上的傳輸層收到底下的網路層丟上來的資料,他必須要把這筆資料導向給這四個應用程式的其中一個。接著讓我們來看看這件事是怎麼做到的。
Figure 3.2 傳輸層的多工和解多工
首先,回想我們在 2.7 節的時候有提過,每一支(網際網路應用)程序都可以擁有一到多個 socket,也就是資料從這支程序進出網路世界的門。因此,就如同在 Figure 3.2 當中所展示的一樣,傳輸層並不是直接把收到的資料送給應用程式,而是送給居中的 socket。因為在任意時間點,收信的主機上都有可能會有一到多個 socket 正在運作,所以每一個 socket 都會有一個獨一無二的識別號碼。這個識別號碼的格式會根據他是 UDP 還是 TCP 的 socket 而有所不同,稍後我們會簡短說明。
接著讓我們來思考收信端主機是怎麼把傳輸層收到的區段導向給正確的 socket 的。每個傳輸層封包上都帶有一系列用來協助區段正確轉發的欄位。在收信端主機,傳輸層會檢查這些欄位的內容、辨識出這個區段是要送給哪一個 socket,然後再把區段導向過去該 socket。這個把傳輸層區段內的資料傳送給正確 socket 的工作,就稱作解多工 (demultiplexing)。而在來源主機上從不同的 socket 那裡蒐集資料塊,把每個資料塊貼上(稍後要用來解多工的)標頭資訊封裝成區段,並把區段傳送到網路層去的工作,則稱作多工 (multiplexing)。注意到 Figure 3.2 裡中間那台主機的傳輸層必須把從底下的網路層那裡收到的區段解多工給上面的 P1 或 P2 程序;這件事情是透過把收到的區塊當中的資料導向給這些程序對應的 socket 做到的。中間那台主機的傳輸層也必須蒐集所有從這些 socket 往外送的資料、把他們包成傳輸層區段,然後把這些區段往下丟給網路層。雖然我們是在網際網路所使用的傳輸層協定的情境底下介紹多工和解多工的,但是讀者必須了解一個很重要的概念:只要有某一層(無論是傳輸層或其他)的單一一個協定,會被他上方那一層的多個不同協定一起使用到的話,就需要做多工和解多工的動作。
為了示範解多工的時候要做些什麼,請回想我們在上一個小節中提過的那群表兄弟姊妹的比喻。每個小孩都可以透過他們的名字來辨認誰是誰。當小明從郵差那邊收到一整疊的信之後,他就會進行解多工的動作:查看信上的收件人是誰,然後把那封信親手交給他的其中一個兄弟姊妹們。而小美則是會在要寄信之前進行多工的動作:從她的兄弟姊妹們那邊蒐集信件,然後把蒐集起來的信交給郵差。
Figure 3.3 傳輸層區段中的來源與目標埠號欄位
現在我們已經知道傳輸層的多工和解多工是做什麼用的了,接著我們就來看看,在一台主機當中這些動作實際上是怎麼做到的。從上面的說明我們會發現,要進行傳輸層多工我們會需要 (1) 每個 socket 有自己獨一無二的識別編號 (2) 每個區段有一些特別的欄位來註記這個區段是要送給哪個 socket 的。在 Figure 3.3 中,我們可以看到這些欄位就是來源埠號欄位 (source port number field) 和目標埠號欄位 (destination port number field)(當然 UDP 和 TCP 的區段也是有他們自己的其他特殊欄位,這些我們會在本章接下來的小節當中跟各位做介紹)。每個埠號都是一個 16-bit 的數字,值從 0 到 65535。在 0 到 1023 這個範圍內的埠號被稱為常用埠號 (well-known port number) 並且被限制不能由一般的開發者使用,意思是說他們是被保留下來給那些很常見的網際網路服務來使用的,像是 HTTP(使用的埠號是 80)或是 FTP(使用的埠號是 21)等等。常用埠號的列表被列在 RFC 1700 當中,並且會在 http://www.iana.org [RFC 3232] 上隨時更新。當我們要開發一個新的應用程式(像是我們在 2.7 節中開發的那種簡單的應用程式)時,我們都必須要指定一個埠號來使用。
這樣一來,傳輸層可以怎麼實作解多工服務應該就很清楚了:主機上的每個 socket 都可以被指定一個埠號,而每當一個區段抵達這台主機時,傳輸層會查看區段上的目標埠號欄位,然後把這個區段導向給對應的 socket。接著區段內的資料會透過 socket 再傳送給上面的程序。接下來我們就會看到,基本上這就是 UDP 做的事情。但我們也會再進一步發現,TCP 當中的多工/解多工服務有更多精妙的設計。
還記得我們在 2.7.1 小節中有提到,一支跑在某台主機上的 Python 程序可以透過以下這行程式碼創造一個 UDP socket:
clientSocket = socket(AF_INET, SOCK_DGRAM)
當一個 UDP socket 透過這行程式碼被創造出來時,傳輸層會自動指定一個埠號給該 socket。準確來說,傳輸層會指定一個介於 1024 到 65535 中的、目前沒有被任何其他的 UDP 程序(課本此處寫成 port,但應該是 process)佔用的埠號。除了讓傳輸層自己指定埠號之外,我們也可以在這個 Python 程式中再加一行程式碼,在創造出一個 socket 之後,透過 bind()
方法指定一個特定的埠號(例如:19157)綁定到這個 UDP socket 上:
clientSocket.bind((’’, 19157))
如果這個應用程式開發者是在實作那些「常見協定」的應用程式的伺服器端,那他就有必要透過這種方式來指定對應的常用埠號。一般來說,應用程式的客戶端通常會讓傳輸層自動地(且不知不覺地)指定一個埠號,而伺服器端則是會手動指定一個特定的埠號。
現在我們的 UDP socket 已經被指定了一個埠號了,我們就能精確地描述 UDP 的多工/解多工在做些什麼。假設在主機 A 上面有一個程序,他的 UDP 埠號是 19157,他想要傳送一大塊應用程式資料給跑在主機 B 上、UDP 埠號是 46428 的另一支程序。主機 A 上的傳輸層會創造一個傳輸層區段出來,這裡頭包含了應用程式資料、來源埠號 (19157)、目標埠號 (46428),和其他兩個數值(我們等等會介紹這兩個數值是什麼,但在現在的討論情境底下他們並不重要)。傳輸層接著會把這個包好的區段傳送給網路層。網路層會再把這個區段封裝成一個 IP 資料塊,然後盡力而為地試著把這個區塊傳送到收信主機上。如果該區段有順利抵達收信主機 B,該主機上的傳輸層就會查看區段上寫著的目標埠號 (46428),接著把這個區段送給被指定了埠號 46428 的那一個 socket。要注意在主機 B 上可能同時執行著多個程序,每一個程序都有他自己的 UDP socket 和對應的埠號。每當有 UDP 區段從網路上送過來時,主機 B 都會透過查看區段上的目標埠號來把各個區段導向(解多工)給對應的 socket。
注意到很重要的一點:一個 UDP socket 可以完全由目標 IP 位址和目標埠號兩個資訊所組成的二元數對 (two-tuple) 來決定。因此,如果有兩個 UDP 區段,即便他們有不同的來源 IP 位址和/或來源埠號,只要他們有一樣的目標 IP 位址和目標埠號,那麼這兩個區段就會透過同一個目標 socket 被送到同一個目標程序去。
Figure 3.4 反轉來源埠號和目標埠號
現在你可能會疑惑,那來源埠號是幹嘛用的?如同 Figure 3.4 所描述的,在一個 A 到 B 的區段當中,來源埠號有著「回傳地址」的功能 —— 當 B 想要回送一個區段給 A 時,在 B 到 A 的區段中目標埠號的值可以直接照抄 A 到 B 區段中的來源埠號(完整的回傳地址其實是 A 的 IP 位址加上來源埠號)。舉個例子:在 2.7 節內,我們所寫的 UDP 伺服器程式 UDPServer.py
當中,伺服器就有使用到 recvfrom()
方法來從客戶端送來的區段當中取出客戶端(來源)的埠號;接著他會送一個新的區段回去給客戶端,而在這個新的區段當中,目標埠號就是剛才所取出來的來源埠號。
為了理解 TCP 解多工是怎麼運作的,我們需要先來認識 TCP socket 和 TCP 的連線建立機制。TCP socket 和 UDP socket 一個細微的差異是:一個 TCP socket 是由 (來源 IP 位址, 來源埠號, 目標 IP 位址, 目標埠號) 這樣的四元數對 (four-tuple) 來決定的。因此,每當有 TCP 區段從網路上送到某台主機時,這台主機需要把這四個數值全用上才能把區段導向(解多工)到正確的 socket 去。具體來說,不同於 UDP, (除了一開始那個帶有建立連線的請求資料的區段之外,)若兩個 TCP 區段帶有不同的來源 IP 位址和來源埠號的話,他們會被導向到兩個不同的 socket 去。為了讓我們能了解更多細節,讓我們回想在 2.7.2 小節中所實作的那個 TCP 的主從式架構應用程式:
clientSocket = socket(AF_INET, SOCK_STREAM)
clientSocket.connect((serverName, 12000))
connectionSocket, addr = serverSocket.accept()
一台伺服器主機可以擁有很多個同時運作的 TCP 連線的 socket,每個 socket 都會附著在某個程序上面,並且都有一個自己的四元數對來辨識身份。每當有一個 TCP 區段抵達主機時,這四個欄位(來源 IP 位址、來源埠號、目標 IP 位址、目標埠號)都要用上,才能把區段導向(解多工)到恰當的 socket。
Figure 3.5 兩台不同的客戶端,指定了同一個目標埠號 (80),依然可以跟同一個網頁伺服器的程序通訊
Figure 3.5 展示了這個情境:主機 C 和伺服器 B 建立了兩個 HTTP session,而主機 A 則和 伺服器 B 建立了一個 HTTP session。主機 A、C 和伺服器 B 都有他們自己獨一無二的 IP 位址,依序是 A、C 和 B。主機 C 給他的兩個 HTTP 連線指定了兩個不同的來源埠號(26145 和 7532)。因為主機 A 要怎麼選擇來源埠號跟主機 C 是完全沒有關係的,所以他也是有可能選到 26145 當作他 HTTP 連線的來源埠號。這完全不成問題 —— 伺服器 B 依然可以正確的把兩個有相同來源埠號的連線解多工,因為這兩個連線有不同的來源 IP 位址。
Port Scanning
在前文中我們看到,伺服器上的程序會在一個開放的通訊埠上耐心地等待遠端的客戶端送來請求。有些通訊埠是保留給那些常見的應用程式的(例如:Web, FTP, DNS, SMTP 伺服器等);而也有一些通訊埠是慣例上會留給一些知名的應用程式用的(例如:Microsoft 2000 SQL 伺服器通常會聽 UDP 的 1434 埠號)。因此,如果我們能夠確定一台主機上的某個通訊埠現在是開放的,那我們就有可能可以確定那個通訊埠正對應到某個執行在那台主機上的應用程式。這對於系統管理者來說是非常有幫助的,因為他們常常需要知道在他們所管理的網路當中有哪些網路應用程式正在運作。然而對於攻擊者來說,為了要「刺探敵情」,他們也會想知道在攻擊目標的主機上有哪些通訊埠是開放的。如果有一台主機被發現正在執行一支具有安全性漏洞的應用程式(例如:曾經有一台監聽 1434 埠號的 SQL 伺服器被發現有緩衝區溢位的漏洞,導致任意的遠端使用者都可以在那台主機上執行任意的程式碼。這個漏洞後來被 Slammer worm [CERT 2003–04] 所惡用),那這台主機就隨時都有可能被攻擊了。
要確定哪些應用程式正在聽哪些通訊埠其實蠻簡單的。現在就有幾個開放公眾使用的程式,叫作 port scanner,可以做到這件事情。這當中最廣為使用的應該是 nmap,可以在 http://nmap.org 上免費取得,並且也預設包含在幾乎所有的 Linux 發行版中。要探測 TCP 的通訊埠時,nmap 會先逐一掃過每一個埠號,並找出那些接受建立 TCP 連線的埠號。而要探測 UDP 的通訊埠時,一樣 nmap 會先逐一掃過每一個埠號,然後找出那些有對他送過去的 UDP 區段進行回應的埠號。不管是哪一種情境,nmap 都會回傳一個列表,列出哪些通訊埠是開放的、是關閉的,或是是無法抵達的。一台執行 nmap 的主機可以嘗試探測網際網路上的任何目標主機。稍後我們將會在 3.5.6 小節討論 TCP 的連線管理時,再次介紹 nmap 這個工具。
在結束本節的內容之前,我們再稍微介紹一下 Web 伺服器是怎麼使用埠號的會對讀者的學習很有幫助。考慮一台正在執行 Web 伺服器的主機,這個伺服器有可能是 Apache 之類的,聽在埠號 80 上。當客戶端們(例如瀏覽器)要傳送區段給這台伺服器時,所有區段的目標埠號都會是 80。具體而言,無論是一開始要求建立連線的區段還是後來那些實際帶著 HTTP 請求訊息的區段,他們的目標埠號都是 80。就如同我們剛剛所說的,伺服器會利用客戶端之間不同的來源 IP 位址和來源埠號來區別這些區段。
在 Figure 3.5 中有展示出一個 Web 伺服器會為每一個新建立的連線生出一支新的程序來服務。就跟 Figure 3.5 當中畫的一樣,這每一個程序都會有一個自己的連線 socket 用來收發 HTTP 請求/回應。但在這裡我們要強調,連線 socket 和程序並不是每次都是一對一的對應關係。事實上,現今有很多高效能的 Web 伺服器,往往只使用一個程序,然後在每一次有新的客戶端連線時,都創造一個備有新的連線 socket 的執行緒來服務客戶端(執行緒可以被視為一種輕量化的程序)。如果你有做第二章的第一個程式作業,那麼你就是在做這件事情。對於這種伺服器來說,同一個程序是有可能同時對應到很多個(有不同識別編號的)連線 socket 的。
如果客戶端和伺服器之間是使用持續性的 HTTP 連線,那麼在整個連線維持運作的期間,客戶端和伺服器都會透過同一個伺服器端的 socket 來交換 HTTP 訊息。然而,如果客戶端和伺服器端使用的是非持續性 HTTP 連線的話,那每當有一組請求/回應要傳送時,就會有一個新的 TCP 連線被創造/關閉,也就是說每次要傳送請求/回應時,都會有新的 socket 被創造出來,然後再被關起來。這麼頻繁的創造又關閉 socket 會對忙碌的(流量很多的) Web 伺服器造成很嚴重的效能影響(雖然可以用一些作業系統層面的小技巧來減緩這個問題)。讀者若是對作業系統在處理持續性/非持續性 HTTP 連線時會產生的議題有興趣,可以去閱讀 [Nielsen 1997; Nahum 2002]。
現在我們已經介紹完傳輸層的多工和解多工是什麼了,接著就讓我們來認識網際網路所使用的兩個傳輸層協定的其中之一:UDP。在下一個小節中,我們會看到 UDP 給網路層協定帶來的服務,會比單純做多工/解多工還要再多一點點東西。
<< 3.1 Introduction and Transport-Layer Services | 目錄 | 3.3 Connectionless Transport: UDP >>