1. 程式人生 > >網路程式設計(一):埠那些事兒

網路程式設計(一):埠那些事兒

TCP和UDP協議都存在一個叫做埠的東西,但埠卻不是IP協議的一部分。

埠被設計出來主要是為了給協議棧和應用對應:

  • 協議棧用埠號將資料分配給不同的應用層程式
  • 應用層程式用埠號去區分不同的連線,參見之前提到過的“四元組”

TCP和UDP協議都使用了埠號(Port number)的概念來標識傳送方和接收方的應用層。 對每個TCP連線的一端都有一個相關的16位的無符號埠號分配給它們。 即使是UDP這種沒有連線的協議,依舊有一個16位的無符號埠號。 可能的、被正式承認的埠號有 2^16 -1 = 65535 個。

三類埠

埠被分為三類:著名埠、監聽埠和動態埠。

  • 著名埠是由因特網賦號管理局(IANA)來分配的,並且通常被用於系統程序。 IANA對於埠號的分配見這裡 

    Service Name and Transport Protocol Port Number Registry 。 系統的/etc/services也有相應埠和服務名的對應,主要是用來給netstat、nmap 等系統命令做埠名反解用。

    著名的應用程式作為伺服器程式來執行,並偵聽經常使用這些埠的連線。 這些埠的一個顯著特徵就是限定在0~1023,並且在Linux、UNIX平臺均需要 Root許可權才能監聽這些埠。

    在UNIX剛剛興起的年代,伺服器資源是十分稀缺的, 通常一臺伺服器上會有很多的使用者,同時這臺伺服器往往還兼任一個學院、公司的郵件、 網站等服務。為了保證這些服務的埠不被普通使用者佔用, 當時UNIX的設計者就把使用這些埠的許可權限制在系統管理員(Root)手裡。

    常見的`著名埠`有:FTP:21、SSH:22、SMTP:25、HTTP:80、HTTPS:443等。
    
  • 監聽埠通常被用來執行各種使用者自己寫的服務,服務監聽在這些埠下不需要特別的許可權。

    • BSD使用的監聽埠範圍是1024到4999。
    • IANA建議49152至65535作為“監聽埠”。
    • 許多Linux核心使用32768至61000範圍。 配置檔案 /proc/sys/net/ipv4/ip_local_port_range 有當前系統設定。
  • 動態埠通常被用來在主動發起連線時隨機分配使用,在任何特定的TCP連線外不具有任何意義。 這是由於TCP等協議是通過四元組來區分不同的網路連線。 當本機主動發起TCP連線的時候如果目的IP、目的埠、本地IP都是一樣的, 只能通過佔用不同的本地埠來區分不同的連線。

    0~65535除去上述著名埠、監聽埠兩種埠號,剩下的埠都是備用的動態埠。 所以在某些特殊用途的需要主動發起大量連線的伺服器上(例如:爬蟲、代理), 需要調整 /proc/sys/net/ipv4/ip_local_port_range 的數值,來保留更多的 動態埠以供使用。

0號埠

埠號裡有一個極為特殊的埠,各種文件書籍中都鮮有記載,就是0號埠。

在IANA官方的標準裡0號埠是保留埠。

也就是說無論是TCP還是UDP網路通訊,0號埠都是不能使用的。

然而,標準歸標準,在UNIX/Linux網路程式設計中0號埠被賦予了特殊的涵義:

如果在bind繫結的時候指定埠0,意味著由系統隨機選擇一個可用埠來繫結。

用Python實現一個獲取可用監聽埠的示例:

def findFreePort():
  """
  函式返回值是當前可用來監聽的一個隨機埠。
  """
  import socket
  s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  s.bind(('localhost', 0))
  # 用getsockname來獲取我們實際繫結的埠號
  addr, port = s.getsockname()
  # 釋放埠
  s.close()
  return port

網路地址轉換NAT

既然說到了埠,不得不提一下NAT。

NAT是"Network Address Translation"的縮寫,直譯就是網路地址轉換。 1990年代中期,為了應對IPv4地址短缺,NAT技術流行起來。

WikiPedia的解釋為:

在一個典型的配置中,一個本地網路使用一個專有網路的指定子網 (比如192.168.x.x或10.x.x.x)和連在這個網路上的一個路由器。 這個路由器佔有這個網路地址空間的一個專有地址(比如192.168.0.1), 同時它還通過一個或多個因特網服務提供商提供的公有的IP地址(叫做“過載”NAT) 連線到因特網上。當資訊由本地網路向因特網傳遞時,源地址被立即從專有地址轉換為公用地址。 由路由器跟蹤每個連線上的基本資料,主要是目的地址和埠。當有回覆返回路由器時, 它通過輸出階段記錄的連線跟蹤資料來決定該轉發給內部網的哪個主機; 如果有多個公用地址可用,當資料包返回時,TCP或UDP客戶機的埠號可以用來分解資料包。 對於因特網上的一個系統,路由器本身充當通訊的源和目的地址。

這個技術能夠被廣泛使用還要感謝當時埠號的記錄欄位是2Bytes而不是1Byte。

NAT技術的廣泛應用也給很多應用帶來了極大的麻煩: 處於NAT網路環境內的伺服器很難被外部的網路程式主動連線,受這一點傷害最大的莫過於: 點對點視訊、語音、檔案傳輸類的程式。

當然我們聰明的工程師經過長時間的努力,發明了“NAT打洞”技術,一定程度上解決了此類問題。

如果沒有他們的努力,我們現在各種QQ視訊、微信實時語音、網路電話都是需要使用者連線到 服務商的伺服器上進行資料傳輸。這樣對服務商的網路消耗將是十分巨大的, 服務質量也是很難以提高的,具體的技術實現,我們以後再表。

多程序埠監聽

我們都有一個計算機網路的常識:不同的程序不能使用同一埠。

如果一個埠正在被使用,無論是TIME_WAIT、CLOSE_WAIT、還是ESTABLISHED狀態。 這個埠都不能被複用,這裡面自然也是包括不能被用來LISTEN(監聽)。

但這件事也不是絕對的,之前跟大家講程序的建立過程提到過一件事: 當程序呼叫fork(2)系統呼叫的時候,會發生一系列資源的複製,其中就包括控制代碼。 所以,在呼叫fork(2)之前,開啟任何檔案,監聽埠產生的控制代碼也將會被複制。

通過這種方式,我們就可以達成"多程序埠監聽"。

但,這又有什麼用呢?

我們大名鼎鼎的Nginx就是通過這種手法讓多個程序同時監聽在HTTP的服務埠上的, 這麼做的好處就在於,當外部請求到達,Linux核心會保證多個程序只會有一個accept(2) 成功,這種情況下此埠的服務可用性就和單個程序存在與否無關。 Nginx正是利用這一點達成“不停服務reload、restart”的。

SO_REUSEADDR

要說SO_REUSEADDR,我們需要先需要說一段歷史: 記得大學的時候面試我們學校的“星辰工作室”,有一個問題就是

為什麼有時候重啟Apache會失敗,報“Address already in use”?

當時答得不太好,不太明白這個問題的關鍵點在哪裡,後來逐漸明白了。

TCP的原理會導致這樣的一個結果:

主動close socket的一方會進入TIME_WAIT,這個狀況持續的時間取決於三件事:

  • TCP關閉連線的五次揮手包什麼時候到達
  • SO_LINGER的設定
  • /proc/sys/net/ipv4/tcp_tw_recycle 和 /proc/sys/net/ipv4/tcp_tw_reuse 的設定

總之預設情況下,處於TIME_WAIT狀態的埠是不能用來LISTEN的。 這就導致,Apache重啟時產生80埠TIME_WAIT,進而導致Apache再次嘗試LISTEN失敗。

在很多開原始碼裡我們會看到如下程式碼:

int reuseaddr = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &reuseaddr, sizeof(int));

有了上面這段神奇的程式碼,就不會出現上面的慘劇。但SO_REUSEADDR的作用不僅限於上述

Linux 的 SO_REUSEADDR 設定為 1 有四種效果:

  1. 當埠處在TIME_WAIT時候,可以複用監聽。

  2. 可以允許多個程序監聽同一埠,但是必須不同IP。

    這裡說的比較隱晦,如果程序A監聽0.0.0.0:80,B程序可以成功監聽127.0.0.1:80, 順序反過來也是可以的。

  3. 允許單個程序繫結相同的埠到多個socket上,但每個socket繫結的IP地址不同。

  4. 使用UDP時候,可以允許多個例項或者單程序同時監聽同個埠同個IP。