同一程序中同一埠如何區分不同的Socket
為了區分不同應用程序間的網路通訊和連線,主要有3個引數:通訊的目的IP地址、使用的傳輸層協議(TCP 或 UDP)和使用的埠號。
Socket的原意是“插座”。通過將這3個引數結合起來,與一個“插座”Socket繫結,應用層就可以和傳輸層通過套接字介面,區分來自不同應用程式程序或網路連線的通訊,實現資料傳輸的併發服務。
accept()產生的Socket埠號是多少?
要寫網路程式就必須用Socket,這是程式設計師都知道的。而且,面試的時候,我們也會問對方會不會Socket程式設計?一般來說,很多人都會說,Socket程式設計基本就是listen, accept, 以及send, write等幾個基本的操作。是的,就跟常見的檔案操作一樣,只要寫過就一定知道。
對於網路程式設計,我們也言必稱TCP/IP,似乎其他網路協議已經不存在了。對於TCP/IP,我們還知道TCP和UDP,前者可以保證資料的正確和可靠性,後者則允許資料丟失。最後,我們還知道,在建立連線前,必須知道對方的IP地址和埠號。除此,普通的程式設計師就不會知道太多了,很多時候這些知識已經夠用了。最多,寫服務程式的時候,會使用多執行緒來處理併發訪問。
我們還知道如下幾個事實:
1. 一個指定的埠號不能被多個應用程式共用。比如,如果IIS佔用了80埠,那麼Apache就不能也用80埠了;
2. 很多防火牆只允許特定目標埠的資料包通過。
3. 服務程式在listen某個埠並accept某個連線請求後,會生成一個新的socket來對請求進行處理。
於是,一個困惑了我很久的問題就產生了,如果一個socket建立後並與80埠繫結後,是否就意味著該socket佔用了80埠呢?
如果是這樣的,那麼當其accept一個請求後,生成的新的socket到底使用的是什麼埠呢(我一直以為系統會預設給其分配一個空閒的埠號)?
如果是一個空閒的埠,那麼一定不是80埠了,於是以後的TCP資料包的目標埠就不是80了——防火牆一定會阻止其通過的!
實際上,我們可以看到,防火牆並沒有阻止這樣的連線,而且這是最常見的連線請求和處理方式。我不理解的就是,為什麼防火牆沒有阻止這樣的連線?它是如何判斷那條連線是因為connect80埠而生成的?是不是TCP資料包裡有什麼特別的標誌?或者防火牆記住了什麼東西?
後來,我又仔細研讀了TCP/IP的協議棧原理,對很多概念有了更深刻的認識。比如,TCP和UDP同屬傳輸層,共同架設在IP層(網路層)之上。而IP層主要負責的是在節點之間(End to End)的資料包傳送,這裡的節點是一臺網路裝置,比如計算機。因為IP層只負責把資料送到節點上,而不能區分上面的不同應用,所以TCP和UDP協議在其基礎上加入了埠的資訊,埠於是標識的是一個節點上的一個應用。除了增加埠資訊,UDP協議基本就沒有對IP層的資料進行任何處理了。而TCP協議還加入了更復雜的傳輸控制,比如滑動的資料傳送視窗(Slice Window),以及接收確認和重發機制,以達到資料的可靠傳送。不管應用層看到的是怎樣一個穩定的TCP資料流,下面傳送的都是一個個的IP資料包,需要由TCP協議來進行資料重組。
所以,我有理由懷疑,防火牆並沒有足夠的資訊判斷TCP資料包的更多資訊,除了IP地址和埠號。而且,我們也看到,所謂的埠,是為了區分不同的應用的,以在不同的IP包來到的時候能夠正確轉發。
TCP/IP只是一個協議棧,就像作業系統的執行機制一樣,必須要具體實現,同時還要提供對外的操作介面。就像作業系統會提供標準的程式設計介面,比如Win32程式設計介面一樣,TCP/IP也必須對外提供程式設計介面,這就是Socket程式設計介面——原來是這麼回事啊!
在Socket程式設計接口裡,設計者提出了一個很重要的概念,那就是socket。這個socket跟檔案控制代碼很相似,實際上,在BSD系統裡就是跟檔案控制代碼一樣存放在一樣的程序控制代碼裡。這個socket其實是一個序號,表示其在控制代碼表中的位置。這一點,我們已經見過很多了,比如檔案控制代碼,視窗控制代碼等。這些控制代碼,其實是代表了系統中的某些特定的物件,用於在各種函式中作為引數傳入,以對特定物件進行操作——這其實是C語言的問題,在C++語言裡,這個控制代碼其實就是this指標,實際就是物件指標啦。
現在我們知道,socket跟TCP/IP並沒有必然的聯絡。Socket程式設計介面在設計的時候,就希望也能適應其他的網路協議。所以,socket的出現只是可以更方便的使用TCP/IP協議棧而已,其對TCP/IP進行了抽象,形成了幾個最基本的函式介面。比如create, listen, accept, connect, read和write等。
現在我們明白,如果一個程式建立了一個socket,並讓其監聽80埠,其實是向TCP/IP協議棧聲明瞭其對80埠的佔有。以後,所有目標是80埠的TCP資料包都會轉發給該程式(這裡的程式,因為使用的是Socket程式設計介面,所以首先由Socekt層來處理)。所謂的accept函式,其實抽象的是TCP的連線建立過程。accept函式返回的新socket其實指代的是本次建立的連線,而一個連線是包括兩部分資訊的,一個是源IP和源埠,另一個宿IP和宿埠。這樣的話,這些socket宿埠就可以都是80!而同時,防火牆的對IP包的處理規則也是清晰明瞭,不存在前面設想的種種複雜的情形。
明白socket只是對TCP/IP協議棧操作的抽象,而不是簡單的對映關係,這很重要!
昨天和朋友聊了下網路程式設計,關於Socket,這裡寫一下我個人的一些理解:)
程式裡可以建立Socket,分為普通Socket和原始Socket兩種型別。
一:普通Socket是對TCP/IP協議棧中傳輸層的操作的程式設計介面(一種API)。
有面向連線的流式套接字(SOCK_STREAM),屬於針對TCP方式的應用;
有無連線資料包式套接字(SOCK_DGRAM),屬於針對UDP方式的應用。
對於普通Socket,我曾經有個模糊的問題,在多執行緒情況下,伺服器端監聽(listen)某個埠(假設8080)後,每accept一個客戶端的連線就會產生一個新的Socket。那麼這些新產生的Socket的埠是什麼?程式裡肯定沒有指定,那就應該有兩種可能,1:產生隨機埠。2:還是8080埠。第一種假設想了就覺得不可能,防火牆非常有可能會阻止這些隨機埠的包。那麼就是第二種假設了,服務端埠還是8080。但這推翻了我原有的認識,就是“一個埠被程式佔有,其他程式就不能用該埠了”。我覺得其實最有可能的是範圍不同:就是在程式與程式間不能用同一埠,但是在程式內部不同的Socket還是可以用同一埠的。所以,為了能夠使“客戶端發給服務端的同一埠(8080)不同執行緒(即不同的Socket連線)的包能夠被區分開並進行組合”,必須得有一個區分包是來自不同連線的顯著特徵,那就是傳輸層包頭裡的源埠了,即一個Socket連線裡客戶端那方的埠。總結一下,對於這種情況,就是傳輸層包頭裡源埠(客戶端)會隨著產生的Socket不同,而宿埠相同(伺服器端)。
二:原始Socket,建立在網路層上,所以我們可以在傳輸層上構建自己的協議。
如果是自己做個Sniffer(網路嗅探器),那麼監聽到的包是來自同一網段的普通Socket包(TCP方式或UDP方式),所以在程式裡我們要自己寫資料結構(IP頭和TCP或UDP頭),並繫結資料。
如果是客戶端和服務端都是由自己用原始Socket寫的,那麼可以自己控制協議,像一些網路應用(MSN, skype等),可以在網路層往上重寫協議。