天天看點

socket的五大誤區

<a>隐患 1.忽略傳回狀态</a>

第一個隐患很明顯,但它是開發新手最容易犯的一個錯誤。如果您忽略函數的傳回狀态,當它們失敗或部分成功的時候,您也許會迷失。反過來,這可能傳播錯誤,使定位問題的源頭變得困難。

捕獲并檢查每一個傳回狀态,而不是忽略它們。考慮清單 1 顯示的例子,一個套接字 send 函數。

<a><b>清單 1. 忽略 API 函數傳回狀态</b></a>

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 變量的無阻塞性質,函數調用在發送完所有的資料、一些資料或沒有發送任何資料後傳回。在這裡忽略傳回狀态将導緻不完全的發送和随後的資料丢失。

<a>隐患 2.對等套接字閉包</a>

UNIX 有趣的一面是您幾乎可以把任何東西看成是一個檔案。檔案本身、目錄、管道、裝置和套接字都被當作檔案。這是新穎的抽象,意味着一整套的 API 可以用在廣泛的裝置類型上。

考慮 read API 函數,它從檔案讀取一定數量的位元組。read 函數傳回讀取的位元組數(最高為您指定的最大值);或者 -1,表示錯誤;或者 0,如果已經到達檔案末尾。

如果在一個套接字上完成一個 read 操作并得到一個為 0 的傳回值,這表明遠端套接字端的對等層調用了 close API 方法。該訓示與檔案讀取相同 —— 沒有多餘的資料可以通過描述符讀取(參見 清單 2)。

<a><b>清單 2.适當處理 read API 函數的傳回值</b></a>

int sock, status; sock = socket( AF_INET, SOCK_STREAM, 0 ); ... status = read( sock, buffer, buflen ); if (status &gt; 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。

<a>隐患 3.位址使用錯誤(EADDRINUSE)</a>

您可以使用 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 來禁止位址重用)。

<a><b>清單 3.使用 SO_REUSEADDR 套接字選項避免位址使用錯誤</b></a>

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, &amp;on, sizeof(on) ); /* Allow connections to port 8080 from any available interface */ memset( &amp;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 *)&amp;servaddr, sizeof(servaddr) );

在應用了 SO_REUSEADDR 選項之後,bind API 函數将允許位址的立即重用。

<a>隐患 4.發送結構化資料</a>

套接字是發送無結構二進制位元組流或 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,以更好的特性和功能擴充了它的思想。,擷取更多關于每個協定的資訊。

<a>隐患 5.TCP 中的幀同步假定</a>

TCP 不提供幀同步,這使得它對于面向位元組流的協定是完美的。這是 TCP 與 UDP(User Datagram Protocol,使用者資料報協定)的一個重要差別。UDP 是面向消息的協定,它保留發送者和接收者之間的消息邊界。TCP 是一個面向流的協定,它假定正在通信的資料是無結構的,

圖 1 的上部說明一個 UDP 用戶端和伺服器。左邊的對等層完成兩個套接字的寫操作,每個 100 位元組。協定棧的 UDP 層追蹤寫的數量,并確定當右邊的接收者通過套接字擷取資料時,它以同樣數量的位元組到達。換句話說,為讀者保留了寫者提供的消息邊界。

現在,看圖 1 的底部.它為 TCP 層示範了相同粒度的寫操作。兩個獨立的寫操作(每個 100 位元組)寫入流套接字。但在本例中,流套接字的讀者得到的是 200 位元組。協定棧的 TCP 層聚合了兩次寫操作。這種聚合可以發生在 TCP/IP 協定棧的發送者或接收者中任何一方。重要的是,要注意到聚合也許不會發生 —— TCP 隻保證資料的有序發送。

對大多數開發人員來說,該陷阱會引起困惑。您想要獲得 TCP 的可靠性和 UDP 的幀同步。除非改用其他的傳輸協定,比如流傳輸控制協定(STCP),否則就要求應用層開發人員來實作緩沖和分段功能。

<a>調試套接字應用程式的工具</a>

GNU/Linux 提供幾個工具,它們可以幫助您發現套接字應用程式中的一些問題。此外,使用這些工具還有教育意義,而且能夠幫助解釋應用程式和 TCP/IP 協定棧的行為。在這裡,您将看到對幾個工具的概述。查閱下面的 了解更多的資訊。

<a>檢視網絡子系統的細節</a>

netstat 工具提供檢視 GNU/Linux 網絡子系統的能力。使用 netstat,可以檢視目前活動的連接配接(按單個協定進行檢視),檢視特定狀态的連接配接(比如處于監聽狀态的伺服器套接字)和許多其他的資訊。清單 4 顯示了 netstat 提供的一些選項和它們啟用的特性。

<a><b>清單 4.netstat 實用程式的用法模式</b></a>

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 工具的功能。

<a>監視流量</a>

可以使用 GNU/Linux 的幾個工具來檢查網絡上的低層流量。tcpdump 工具是一個比較老的工具,它從網上“嗅探”網絡資料包,列印到 stdout 或記錄在一個檔案中。該功能允許檢視應用程式産生的流量和 TCP 生成的低層流控制機制。一個叫做 tcpflow 的新工具與 tcpdump 相輔相成,它提供協定流分析和适當地重構資料流的方法,而不管資料包的順序或重發。清單 5 顯示 tcpdump 的兩個用法模式。

<a><b>清單 5.tcpdump 工具的用法模式</b></a>

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 個協定)。

<a>總結</a>

套接字程式設計是容易而有趣的,但是您要避免引入錯誤或至少使它們容易被發現,這就需要考慮本文中描述的這 5 個常見的陷阱,并且采用标準的防錯性程式設計實踐。GNU/Linux 工具和實用程式還可以幫助發現一些程式中的小問題。記住:在檢視實用程式的幫助手冊時候,跟蹤相關的或“請參見”工具。您也許會發現一個必要的新工具。

繼續閱讀