Linux 套接字程式設計中的 5 個隱患
在 4.2 BSD UNIX® 作業系統中首次引入,Sockets API 現在是任何作業系統的標準特性。事實上,很難找到一種不支援 Sockets API 的現代語言。該 API 相當簡單,但新的開發人員仍然會遇到一些常見的隱患。
本文識別那些隱患並向您顯示如何避開它們。
隱患 1.忽略返回狀態
第一個隱患很明顯,但它是開發新手最容易犯的一個錯誤。如果您忽略函式的返回狀態,當它們失敗或部分成功的時候,您也許會迷失。反過來,這可能傳播錯誤,使定位問題的源頭變得困難。
捕獲並檢查每一個返回狀態,而不是忽略它們。考慮清單 1 顯示的例子,一個套接字 send
函式。
清單 1. 忽略 API 函式返回狀態
int status, sock, mode; /* Create a new stream (TCP) socket */ sock = socket( AF_INET, SOCK_STREAM, 0 ); ... status = send( sock, buffer, buflen, MSG_DONTWAIT ); if (status == -1) { /* send failed */ printf( "send failed: %s\n", strerror(errno) ); } else { /* send succeeded -- or did it? */ }
清單 1 探究一個函式片斷,它完成套接字 send
send
在無阻塞模式(由 MSG_DONTWAIT
標誌啟用)下的一個特性。
send
API 函式有三類可能的返回值:
- 如果資料成功地排到傳輸佇列,則返回 0。
- 如果排隊失敗,則返回 -1(通過使用
errno
變數可以瞭解失敗的原因)。 - 如果不是所有的字元都能夠在函式呼叫時排隊,則最終的返回值是傳送的字元數。
由於 send
的 MSG_DONTWAIT
變數的無阻塞性質,函式呼叫在傳送完所有的資料、一些資料或沒有傳送任何資料後返回。在這裡忽略返回狀態將導致不完全的傳送和隨後的資料丟失。
隱患 2.對等套接字閉包
UNIX 有趣的一面是您幾乎可以把任何東西看成是一個檔案。檔案本身、目錄、管道、裝置和套接字都被當作檔案。這是新穎的抽象,意味著一整套的 API 可以用在廣泛的裝置型別上。
考慮 read
API 函式,它從檔案讀取一定數量的位元組。read
函式返回讀取的位元組數(最高為您指定的最大值);或者 -1,表示錯誤;或者 0,如果已經到達檔案末尾。
如果在一個套接字上完成一個 read
操作並得到一個為 0 的返回值,這表明遠端套接字端的對等層呼叫了 close
API 方法。該指示與檔案讀取相同 —— 沒有多餘的資料可以通過描述符讀取(參見 清單 2)。
清單 2.適當處理 read API 函式的返回值
int sock, status; sock = socket( AF_INET, SOCK_STREAM, 0 ); ... status = read( sock, buffer, buflen ); if (status > 0) { /* Data read from the socket */ } else if (status == -1) { /* Error, check errno, take action... */ } else if (status == 0) { /* Peer closed the socket, finish the close */ close( sock ); /* Further processing... */ }
同樣,可以用 write
API 函式來探測對等套接字的閉包。在這種情況下,接收 SIGPIPE
訊號,或如果該訊號阻塞,write
函式將返回 -1 並設定
errno
為 EPIPE
。
隱患 3.地址使用錯誤(EADDRINUSE)
在學習中避免該錯誤的例子,參看文章:《unix網路程式設計》(16)epoll函式;在該文章之前的系列文章中對伺服器都沒有使用SO_RESUEADDR套接字,因此之前的文章都存在該問題。
您可以使用 bind
API 函式來繫結一個地址(一個介面和一個埠)到一個套接字端點。可以在伺服器設定中使用這個函式,以便限制可能有連線到來的介面。也可以在客戶端設定中使用這個函式,以便限制應當供出去的連線所使用的介面。bind
最常見的用法是關聯埠號和伺服器,並使用萬用字元地址(INADDR_ANY
),它允許任何介面為到來的連線所使用。
bind
普遍遭遇的問題是試圖繫結一個已經在使用的埠。該陷阱是也許沒有活動的套接字存在,但仍然禁止繫結埠(bind
返回
EADDRINUSE
),它由 TCP 套接字狀態 TIME_WAIT
引起。該狀態在套接字關閉後約保留 2 到 4 分鐘。在
TIME_WAIT
狀態退出之後,套接字被刪除,該地址才能被重新繫結而不出問題。
等待 TIME_WAIT
結束可能是令人惱火的一件事,特別是如果您正在開發一個套接字伺服器,就需要停止伺服器來做一些改動,然後重啟。幸運的是,有方法可以避開
TIME_WAIT
狀態。可以給套接字應用 SO_REUSEADDR
套接字選項,以便埠可以馬上重用。
考慮清單 3 的例子。在繫結地址之前,我以 SO_REUSEADDR
選項呼叫 setsockopt
。為了允許地址重用,我設定整型引數(on
)為 1 (不然,可以設為 0 來禁止地址重用)。
清單 3.使用 SO_REUSEADDR 套接字選項避免地址使用錯誤
int sock, ret, on; struct sockaddr_in servaddr; /* Create a new stream (TCP) socket */ sock = socket( AF_INET, SOCK_STREAM, 0 ): /* Enable address reuse */ on = 1; ret = setsockopt( sock, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on) ); /* Allow connections to port 8080 from any available interface */ memset( &servaddr, 0, sizeof(servaddr) ); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl( INADDR_ANY ); servaddr.sin_port = htons( 45000 ); /* Bind to the address (interface/port) */ ret = bind( sock, (struct sockaddr *)&servaddr, sizeof(servaddr) );
在應用了 SO_REUSEADDR
選項之後,bind
API 函式將允許地址的立即重用。
隱患 4.傳送結構化資料
套接字是傳送無結構二進位制位元組流或 ASCII 資料流(比如 HTTP 上的 HTTP 頁面,或 SMTP 上的電子郵件)的完美工具。但是如果試圖在一個套接字上傳送二進位制資料,事情將會變得更加複雜。
比如說,您想要傳送一個整數:您可以肯定,接收者將使用同樣的方式來解釋該整數嗎?執行在同一架構上的應用程式可以依賴它們共同的平臺來對該型別的資料做出相同的解釋。但是,如果一個執行在高位優先的 IBM PowerPC 上的客戶端傳送一個 32 位的整數到一個低位優先的 Intel x86,那將會發生什麼呢?位元組排列將引起不正確的解釋。
位元組交換還是不呢?
Endianness 是指記憶體中位元組的排列順序。高位優先(big endian) 按最高有效位元組在前排列,然而 低位優先(little endian) 按照最低有效位元組在前排序。
高位優先架構(比如 PowerPC®)比低位優先架構(比如 Intel® Pentium® 系列,其網路位元組順序是高位優先)有優勢。這意味著,對高位優先的機器來說,在 TCP/IP 內控制資料是自然有序的。低位優先架構要求位元組交換 —— 對網路應用程式來說,這是一個輕微的效能弱點。
通過套接字傳送一個 C 結構會怎麼樣呢?這裡,也會遇到麻煩,因為不是所有的編譯器都以相同的方式排列一個結構的元素。結構也可能被壓縮以便使浪費的空間最少,這進一步使結構中的元素錯位。
幸好,有解決這個問題的方案,能夠保證兩端資料的一致解釋。過去,遠端過程呼叫(Remote Procedure Call,RPC)套裝工具提供所謂的外部資料表示(External Data Representation,XDR)。XDR 為資料定義一個標準的表示來支援異構網路應用程式通訊的開發。
現在,有兩個新的協議提供相似的功能。可擴充套件標記語言/遠端過程呼叫(XML/RPC)以 XML 格式安排 HTTP 上的過程呼叫。資料和元資料用 XML 進行編碼並作為字串傳輸,並通過主機架構把值和它們的物理表示分開。SOAP 跟隨 XML-RPC,以更好的特性和功能擴充套件了它的思想。參見 參考資料 小節,獲取更多關於每個協議的資訊。
隱患 5.TCP 中的幀同步假定
TCP 不提供幀同步,這使得它對於面向位元組流的協議是完美的。這是 TCP 與 UDP(User Datagram Protocol,使用者資料報協議)的一個重要區別。UDP 是面向訊息的協議,它保留髮送者和接收者之間的訊息邊界。TCP 是一個面向流的協議,它假定正在通訊的資料是無結構的,如圖 1 所示。
圖 1.UDP 的幀同步能力和缺乏幀同步的 TCP
圖 1 的上部說明一個 UDP 客戶端和伺服器。左邊的對等層完成兩個套接字的寫操作,每個 100 位元組。協議棧的 UDP 層追蹤寫的數量,並確保當右邊的接收者通過套接字獲取資料時,它以同樣數量的位元組到達。換句話說,為讀者保留了寫者提供的訊息邊界。
現在,看圖 1 的底部.它為 TCP 層演示了相同粒度的寫操作。兩個獨立的寫操作(每個 100 位元組)寫入流套接字。但在本例中,流套接字的讀者得到的是 200 位元組。協議棧的 TCP 層聚合了兩次寫操作。這種聚合可以發生在 TCP/IP 協議棧的傳送者或接收者中任何一方。重要的是,要注意到聚合也許不會發生 —— TCP 只保證資料的有序傳送。
對大多數開發人員來說,該陷阱會引起困惑。您想要獲得 TCP 的可靠性和 UDP 的幀同步。除非改用其他的傳輸協議,比如流傳輸控制協議(STCP),否則就要求應用層開發人員來實現緩衝和分段功能。
除錯套接字應用程式的工具
GNU/Linux 提供幾個工具,它們可以幫助您發現套接字應用程式中的一些問題。此外,使用這些工具還有教育意義,而且能夠幫助解釋應用程式和 TCP/IP 協議棧的行為。在這裡,您將看到對幾個工具的概述。查閱下面的 參考資料 瞭解更多的資訊。
檢視網路子系統的細節
netstat
工具提供檢視 GNU/Linux 網路子系統的能力。使用 netstat
,可以檢視當前活動的連線(按單個協議進行檢視),檢視特定狀態的連線(比如處於監聽狀態的伺服器套接字)和許多其他的資訊。清單 4 顯示了
netstat
提供的一些選項和它們啟用的特性。
清單 4.netstat 實用程式的用法模式
View all TCP sockets currently active $ netstat --tcp View all UDP sockets $ netstat --udp View all TCP sockets in the listening state $ netstat --listening View the multicast group membership information $ netstat --groups Display the list of masqueraded connections $ netstat --masquerade View statistics for each protocol $ netstat --statistics
儘管存在許多其他的實用程式,但 netstat
的功能很全面,它覆蓋了 route
、ifconfig
和其他標準 GNU/Linux 工具的功能。
監視流量
可以使用 GNU/Linux 的幾個工具來檢查網路上的低層流量。tcpdump
工具是一個比較老的工具,它從網上“嗅探”網路資料包,列印到
stdout
或記錄在一個檔案中。該功能允許檢視應用程式產生的流量和 TCP 生成的低層流控制機制。一個叫做 tcpflow
的新工具與
tcpdump
相輔相成,它提供協議流分析和適當地重構資料流的方法,而不管資料包的順序或重發。清單 5 顯示 tcpdump
的兩個用法模式。
清單 5.tcpdump 工具的用法模式
Display all traffic on the eth0 interface for the local host $ tcpdump -l -i eth0 Show all traffic on the network coming from or going to host plato $ tcpdump host plato Show all HTTP traffic for host camus $ tcpdump host camus and (port http) View traffic coming from or going to TCP port 45000 on the local host $ tcpdump tcp port 45000
tcpdump
和 tcpflow
工具有大量的選項,包括建立複雜過濾表示式的能力。查閱下面的
參考資料 獲取更多關於這些工具的資訊。
tcpdump
和 tcpflow
都是基於文字的命令列工具。如果您更喜歡圖形使用者介面(GUI),有一個開放原始碼工具
Ethereal
也許適合您的需要。Ethereal
是一個專業的協議分析軟體,它可以幫助除錯應用層協議。它的插入式架構(plug-in architecture)可以分解協議,比如 HTTP 和您能想到的任何協議(寫本文的時候共有 637 個協議)。
總結
套接字程式設計是容易而有趣的,但是您要避免引入錯誤或至少使它們容易被發現,這就需要考慮本文中描述的這 5 個常見的陷阱,並且採用標準的防錯性程式設計實踐。GNU/Linux 工具和實用程式還可以幫助發現一些程式中的小問題。記住:在檢視實用程式的幫助手冊時候,跟蹤相關的或“請參見”工具。您也許會發現一個必要的新工具。