| 導購 | 订阅 | 在线投稿
分享
 
 
 

Beej的網絡socket編程指南

2008-06-01 02:08:30  編輯來源:互聯網  简体版  手機版  評論  字體: ||
 
  介紹Socket 編程讓你沮喪嗎?從man pages中很難得到有用的信息嗎?你想跟上時代去編Internet相關的程序,但是爲你在調用 connect() 前的bind() 的結構而不知所措?等等…

  好在我已經將這些事完成了,我將和所有人共享我的知識了。假如你了解 C 語言並想穿過網絡編程的沼澤,那麽你來對地方了。

  讀者對象

  這個文檔是一個指南,而不是參考書。假如你剛開始 socket 編程並想找一本入門書,那麽你是我的讀者。但這不是一本完全的 socket 編程書。

  平台和編譯器

  這篇文檔中的大多數代碼都在 Linux 平台PC 上用 GNU 的 gcc 成功編譯過。而且它們在 HPUX平台 上用 gcc 也成功編譯過。但是注重,並不是每個代碼片段都獨立測試過。

  目錄:

  1) 什麽是套接字?

  2) Internet 套接字的兩種類型

  3) 網絡理論

  4) 結構體

  5) 本機轉換

  6) IP 地址和如何處理它們

  7) socket()函數

  8) bind()函數

  9) connect()函數

  10) listen()函數

  11) accept()函數

  12) send()和recv()函數

  13) sendto()和recvfrom()函數

  14) close()和shutdown()函數

  15) getpeername()函數

  16) gethostname()函數

  17) 域名服務(DNS)

  18) 客戶-服務器背景知識

  19) 簡單的服務器

  20) 簡單的客戶端

  21) 數據報套接字Socket

  22) 阻塞

  23) select()--多路同步I/O

  24) 參考資料

  什麽是 socket?

  你經常聽到人們談論著 「socket」,或許你還不知道它的確切含義。現在讓我告訴你:它是使用 標准Unix 文件描述符 (file descriptor) 和其它程序通訊的方式。

  什麽?

  你也許聽到一些Unix高手(hacker)這樣說過:「呀,Unix中的一切就是文件!」那個家夥也許正在說到一個事實:Unix 程序在執行任何形式的 I/O 的時候,程序是在讀或者寫一個文件描述符。一個文件描述符只是一個和打開的文件相關聯的整數。但是(注重後面的話),這個文件可能是一個網絡連接,FIFO,管道,終端,磁盤上的文件或者什麽其它的東西。Unix 中所有的東西就是文件!所以,你想和Internet上別的程序通訊的時候,你將要使用到文件描述符。你必須理解剛才的話。現在你腦海中或許冒出這樣的念頭:「那麽我從哪裏得到網絡通訊的文件描述符呢?」,這個問題無論如何我都要回答:你利用系統調用 socket(),它返回套接字描述符 (socket descriptor),然後你再通過它來進行send() 和 recv()調用。

  「但是...」,你可能有很大的迷惑,「假如它是個文件描述符,那麽爲什 麽不用一般調用read()和write()來進行套接字通訊?」簡單的答案是:「你可以使用!」。具體的答案是:「你可以,但是使用send()和recv()讓你更好的控制數據傳輸。」

  存在這樣一個情況:在我們的世界上,有很多種套接字。有DARPA Internet 地址 (Internet 套接字),本地節點的路徑名 (Unix套接字),CCITT X.25地址 (你可以將X.25 套接字完全忽略)。也許在你的Unix 機器上還有其它的。我們在這裏只講第一種:Internet 套接字。

  Internet 套接字的兩種類型

  什麽意思?有兩種類型的Internet 套接字?是的。不,我在撒謊。其實還有很多,但是我可不想嚇著你。我們這裏只講兩種。除了這些, 我打算另外介紹的 "Raw Sockets" 也是非常強大的,很值得查閱。

  那麽這兩種類型是什麽呢?一種是"Stream Sockets"(流格式),另外一種是"Datagram Sockets"(數據包格式)。我們以後談到它們的時候也會用到 "SOCK_STREAM" 和 "SOCK_DGRAM"。數據報套接字有時也叫「無連接套接字」(假如你確實要連接的時候可以用connect()。) 流式套接字是可靠的雙向通訊的數據流。假如你向套接字按順序輸出「1,2」,那麽它們將按順序「1,2」到達另一邊。它們是無錯誤的傳遞的,有自己的錯誤控制,在此不討論。

  有什麽在使用流式套接字?你可能聽說過 telnet,不是嗎?它就使用流式套接字。你需要你所輸入的字符按順序到達,不是嗎?同樣,WWW浏覽器使用的 HTTP 協議也使用它們來下載頁面。實際上,當你通過端口80 telnet 到一個 WWW 站點,然後輸入 「GET pagename」 的時候,你也可以得到 Html 的內容。爲什麽流式套接字可以達到高質量的數據傳輸?這是因爲它使用了「傳輸控制協議 (The Transmission Control Protocol)」,也叫 「TCP」 (請參考 RFC-793 獲得具體資料。)TCP 控制你的數據按順序到達並且沒有錯

  

   誤。你也許聽到 「TCP」 是因爲聽到過 「TCP/IP」。這裏的 IP 是指「Internet 協議」(請參考 RFC-791。) IP 只是處理 Internet 路由而已。

  那麽數據報套接字呢?爲什麽它叫無連接呢?爲什麽它是不可靠的呢?有這樣的一些事實:假如你發送一個數據報,它可能會到達,它可能次序顛倒了。假如它到達,那麽在這個包的內部是無錯誤的。數據報也使用 IP 作路由,但是它不使用 TCP。它使用「用戶數據報協議 (User Datagram Protocol)」,也叫 「UDP」 (請參考 RFC-768。)

  爲什麽它們是無連接的呢?主要是因爲它並不象流式套接字那樣維持一個連接。你只要建立一個包,構造一個有目標信息的IP 頭,然後發出去。無需連接。它們通常使用于傳輸包-包信息。簡單的應用程序有:tFTP, bootp等等。

  你也許會想:「假如數據丟失了這些程序如何正常工作?」我的朋友,每個程序在 UDP 上有自己的協議。例如,tftp 協議每發出的一個被接受到包,收到者必須發回一個包來說「我收到了!」 (一個「命令正確應答」也叫「ACK」 包)。假如在一定時間內(例如5秒),發送方沒有收到應答,它將重新發送,直到得到 ACK。這一ACK過程在實現 SOCK_DGRAM 應用程序的時候非常重要。

  網絡理論

  既然我剛才提到了協議層,那麽現在是討論網絡究竟如何工作和一些 關于 SOCK_DGRAM 包是如何建立的例子。當然,你也可以跳過這一段, 假如你認爲已經熟悉的話。

  現在是學習數據封裝 (Data Encapsulation) 的時候了!它非常非常重 要。它重要性重要到你在網絡課程學

  (圖1:數據封裝)

  習中無論如何也得也得把握它。主要 的內容是:一個包,先是被第一個協議(在這裏是TFTP )在它的報頭(也許 是報尾)包裝(「封裝」),然後,整個數據(包括 TFTP 頭)被另外一個協議 (在這裏是 UDP )封裝,然後下一個( IP ),一直重複下去,直到硬件(物理) 層( 這裏是以太網 )。

  當另外一台機器接收到包,硬件先剝去以太網頭,內核剝去IP和UDP 頭,TFTP程序再剝去TFTP頭,最後得到數據。

  現在我們終于講到聲名狼藉的網絡分層模型 (Layered Network Model)。這種網絡模型在描述網絡系統上相對其它模型有很多優點。例如, 你可以寫一個套接字程序而不用關心數據的物理傳輸(串行口,以太網,連 接單元接口 (AUI) 還是其它介質),因爲底層的程序會爲你處理它們。實際 的網絡硬件和拓撲對于程序員來說是透明的。

  不說其它廢話了,我現在列出整個層次模型。假如你要參加網絡考試, 可一定要記住:

  應用層 (Application)

  表示層 (Presentation)

  會話層 (Session)

  傳輸層(Transport)

  網絡層(Network)

  數據鏈路層(Data Link)

  物理層(Physical)

  物理層是硬件(串口,以太網等等)。應用層是和硬件層相隔最遠的--它 是用戶和網絡交互的地方。

  這個模型如此通用,假如你想,你可以把它作爲修車指南。把它對應 到 Unix,結果是:

  應用層(Application Layer) (telnet, ftp,等等)

  傳輸層(Host-to-Host Transport Layer) (TCP, UDP)

  Internet層(Internet Layer) (IP和路由)

  網絡訪問層 (Network Access Layer) (網絡層,數據鏈路層和物理層)

  現在,你可能看到這些層次如何協調來封裝原始的數據了。

  看看建立一個簡單的數據包有多少工作?哎呀,你將不得不使用 "cat" 來建立數據包頭!這僅僅是個玩笑。對于流式套接字你要作的是 send() 發 送數據。對于數據報式套接字,你按照你選擇的方式封裝數據然後使用 sendto()。內核將爲你建立傳輸層和 Internet 層,硬件完成網絡訪問層。 這就是現代科技。

  現在結束我們的網絡理論速成班。哦,忘記告訴你關于路由的事情了。 但是我不預備談它,假如你真的關心,那麽參考 IP RFC。

  結構體

  終于談到編程了。在這章,我將談到被套接字用到的各種數據類型。 因爲它們中的一些內容很重要了。

  首先是簡單的一個:socket描述符。它是下面的類型:

  int

  僅僅是一個常見的 int。

  從現在起,事情變得不可思議了,而你所需做的就是繼續看下去。注 意這樣的事實:有兩種字節排列順序:重要的字節 (有時叫 "octet",即八 位位組) 在前面,或者不重要的字節在前面。前一種叫「網絡字節順序 (Network Byte Order)」。有些機器在內部是按照這個順序儲存數據,而另外 一些則不然。當我說某數據必須按照 NBO 順序,那麽你要調用函數(例如 htons() )來將它從本機字節順序 (Host Byte Order) 轉換過來。假如我沒有 提到 NBO, 那麽就讓它保持本機字節順序。

  我的第一個結構(在這個技術手冊TM中)--strUCt sockaddr.。這個結構 爲許多類型的套接字儲存套接字地址信息:

  struct sockaddr {

  unsigned short sa_family; /* 地址家族, AF_xxx */

  char sa_data[14]; /*14字節協議地址*/

  };

  sa_family 能夠是各種各樣的類型,但是在這篇文章中都是 "AF_INET"。 sa_data包含套接字中的目標地址和端口信息。這似乎有點 不明智。

  

   爲了處理struct sockaddr,程序員創造了一個並列的結構: struct sockaddr_in ("in" 代表 "Internet"。)

  struct sockaddr_in {

  short int sin_family; /* 通信類型 */

  unsigned short int sin_port; /* 端口 */

  struct in_addr sin_addr; /* Internet 地址 */

  unsigned char sin_zero[8]; /* 與sockaddr結構的長度相同*/

  };

  用這個數據結構可以輕松處理套接字地址的基本元素。注重 sin_zero (它被加入到這個結構,並且長度和 struct sockaddr 一樣) 應該使用函數 bzero() 或 memset() 來全部置零。 同時,這一重要的字節,一個指向 sockaddr_in結構體的指針也可以被指向結構體sockaddr並且代替它。這 樣的話即使 socket() 想要的是 struct sockaddr *,你仍然可以使用 struct sockaddr_in,並且在最後轉換。同時,注重 sin_family 和 struct sockaddr 中的 sa_family 一致並能夠設置爲 "AF_INET"。最後,sin_port和 sin_addr 必須是網絡字節順序 (Network Byte Order)!

  你也許會反對道:"但是,怎麽讓整個數據結構 struct in_addr sin_addr 按照網絡字節順序呢?" 要知道這個問題的答案,我們就要仔細的看一看這 個數據結構: struct in_addr, 有這樣一個聯合 (unions):

  /* Internet 地址 (一個與曆史有關的結構) */

  struct in_addr {

  unsigned long s_addr;

  };

  它曾經是個最壞的聯合,但是現在那些日子過去了。假如你聲明 "ina" 是數據結構 struct sockaddr_in 的實例,那麽 "ina.sin_addr.s_addr" 就儲 存4字節的 IP 地址(使用網絡字節順序)。假如你不幸的系統使用的還是恐 怖的聯合 struct in_addr ,你還是可以放心4字節的 IP 地址並且和上面 我說的一樣(這是因爲使用了「#define」。)

  本機轉換

  我們現在到了新的章節。我們曾經講了很多網絡到本機字節順序的轉 換,現在可以實踐了!

  你能夠轉換兩種類型: short (兩個字節)和 long (四個字節)。這個函 數對于變量類型 unsigned 也適用。假設你想將 short 從本機字節順序轉 換爲網絡字節順序。用 "h" 表示 "本機 (host)",接著是 "to",然後用 "n" 表 示 "網絡 (network)",最後用 "s" 表示 "short": h-to-n-s, 或者 htons() ("Host to Network Short")。

  太簡單了...

  假如不是太傻的話,你一定想到了由"n","h","s",和 "l"形成的正確 組合,例如這裏肯定沒有stolh() ("Short to Long Host") 函數,不僅在這裏 沒有,所有場合都沒有。但是這裏有:

  htons()--"Host to Network Short"

  htonl()--"Host to Network Long"

  ntohs()--"Network to Host Short"

  ntohl()--"Network to Host Long"

  現在,你可能想你已經知道它們了。你也可能想:「假如我想改變 char 的順序要怎麽辦呢?」 但是你也許馬上就想到,「用不著考慮的」。你也許 會想到:我的 68000 機器已經使用了網絡字節順序,我沒有必要去調用 htonl() 轉換 IP 地址。你可能是對的,但是當你移植你的程序到別的機器 上的時候,你的程序將失敗。可移植性!這裏是 Unix 世界!記住:在你 將數據放到網絡上的時候,確信它們是網絡字節順序的。

  最後一點:爲什麽在數據結構 struct sockaddr_in 中, sin_addr 和 sin_port 需要轉換爲網絡字節順序,而sin_family 需不需要呢? 答案是: sin_addr 和 sin_port 分別封裝在包的 IP 和 UDP 層。因此,它們必須要 是網絡字節順序。但是 sin_family 域只是被內核 (kernel) 使用來決定在數 據結構中包含什麽類型的地址,所以它必須是本機字節順序。同時, sin_family 沒有發送到網絡上,它們可以是本機字節順序。

  IP 地址和如何處理它們

  現在我們很幸運,因爲我們有很多的函數來方便地操作 IP 地址。沒有 必要用手工計算它們,也沒有必要用"<<"操作來儲存成長整字型。 首先,假設你已經有了一個sockaddr_in結構體ina,你有一個IP地 址"132.241.5.10"要儲存在其中,你就要用到函數inet_addr(),將IP地址從 點數格式轉換成無符號長整型。使用方法如下:

  ina.sin_addr.s_addr = inet_addr("132.241.5.10");

  注重,inet_addr()返回的地址已經是網絡字節格式,所以你無需再調用 函數htonl()。

  我們現在發現上面的代碼片斷不是十分完整的,因爲它沒有錯誤檢查。 顯而易見,當inet_addr()發生錯誤時返回-1。記住這些二進制數字?(無符 號數)-1僅僅和IP地址255.255.255.255相符合!這可是廣播地址!大錯特 錯!記住要先進行錯誤檢查。

  好了,現在你可以將IP地址轉換成長整型了。有沒有其相反的方法呢? 它可以將一個in_addr結構體輸出成點數格式?這樣的話,你就要用到函數 inet_ntoa()("ntoa"的含義是"network to ascii"),就像這樣:

  printf("%s",inet_ntoa(ina.sin_addr));

  它將輸出IP地址。需要注重的是inet_ntoa()將結構體in-addr作爲一 個參數,不是長整形。同樣需要注重的是它返回的是一個指向一個字符的 指針。它是一個由inet_ntoa()控制的靜態的固定的指針,所以每次調用 inet_ntoa(),它就將覆蓋上次調用時所得的IP地址。例如:

  char *a1, *a2;

  .

  .

  a1 = inet_ntoa(ina1.sin_addr); /* 這是198.92.129.1 */

  

   a2 = inet_ntoa(ina2.sin_addr); /* 這是132.241.5.10 */

  printf("address 1: %s\n",a1);

  printf("address 2: %s\n",a2);

  輸出如下:

  address 1: 132.241.5.10

  address 2: 132.241.5.10

  假如你需要保存這個IP地址,使用strcopy()函數來指向你自己的字符 指針。

  上面就是關于這個主題的介紹。稍後,你將學習將一個類 似"wintehouse.gov"的字符串轉換成它所對應的IP地址(查閱域名服務,稍 後)。

  socket()函數

  我想我不能再不提這個了-下面我將討論一下socket()系統調用。

  下面是具體介紹:

  #include

  #include

  int socket(int domain, int type, int protocol);

  但是它們的參數是什麽? 首先,domain 應該設置成 "AF_INET",就 象上面的數據結構struct sockaddr_in 中一樣。然後,參數 type 告訴內核 是 SOCK_STREAM 類型還是 SOCK_DGRAM 類型。最後,把 protocol 設置爲 "0"。(注重:有很多種 domain、type,我不可能一一列出了,請看 socket() 的 man幫助。當然,還有一個"更好"的方式去得到 protocol。同 時請查閱 getprotobyname() 的 man 幫助。)

  socket() 只是返回你以後在系統調用種可能用到的 socket 描述符,或 者在錯誤的時候返回-1。全局變量 errno 中將儲存返回的錯誤值。(請參考 perror() 的 man 幫助。)

  bind()函數

  一旦你有一個套接字,你可能要將套接字和機器上的一定的端口關聯 起來。(假如你想用listen()來偵聽一定端口的數據,這是必要一步--MUD 告 訴你說用命令 "telnet x.y.z 6969"。)假如你只想用 connect(),那麽這個步 驟沒有必要。但是無論如何,請繼續讀下去。

  這裏是系統調用 bind() 的大概:

  #include

  #include

  int bind(int sockfd, struct sockaddr *my_addr, int addrlen);

  sockfd 是調用 socket 返回的文件描述符。my_addr 是指向數據結構 struct sockaddr 的指針,它保存你的地址(即端口和 IP 地址) 信息。 addrlen 設置爲 sizeof(struct sockaddr)。

  簡單得很不是嗎? 再看看例子:

  #include

  #include

  #include

  #define MYPORT 3490

  main()

  {

  int sockfd;

  struct sockaddr_in my_addr;

  sockfd = socket(AF_INET, SOCK_STREAM, 0); /*需要錯誤檢查 */

  my_addr.sin_family = AF_INET; /* host byte order */

  my_addr.sin_port = htons(MYPORT); /* short, network byte order */

  my_addr.sin_addr.s_addr = inet_addr("132.241.5.10");

  bzero(&(my_addr.sin_zero),; /* zero the rest of the struct */

  /* don't forget your error checking for bind(): */

  bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr));

  .

  .

  .

  這裏也有要注重的幾件事情。my_addr.sin_port 是網絡字節順序, my_addr.sin_addr.s_addr 也是的。另外要注重到的事情是因系統的不同, 包含的頭文件也不盡相同,請查閱本地的 man 幫助文件。

  在 bind() 主題中最後要說的話是,在處理自己的 IP 地址和/或端口的 時候,有些工作是可以自動處理的。

  my_addr.sin_port = 0; /* 隨機選擇一個沒有使用的端口 */

  my_addr.sin_addr.s_addr = INADDR_ANY; /* 使用自己的IP地址 */

  通過將0賦給 my_addr.sin_port,你告訴 bind() 自己選擇合適的端 口。同樣,將 my_addr.sin_addr.s_addr 設置爲 INADDR_ANY,你告訴 它自動填上它所運行的機器的 IP 地址。

  假如你一向小心謹慎,那麽你可能注重到我沒有將 INADDR_ANY 轉 換爲網絡字節順序!這是因爲我知道內部的東西:INADDR_ANY 實際上就 是 0!即使你改變字節的順序,0依然是0。但是完美主義者說應該處處一 致,INADDR_ANY或許是12呢?你的代碼就不能工作了,那麽就看下面 的代碼:

  my_addr.sin_port = htons(0); /* 隨機選擇一個沒有使用的端口 */

  my_addr.sin_addr.s_addr = htonl(INADDR_ANY);/* 使用自己的IP地址 */

  你或許不相信,上面的代碼將可以隨便移植。我只是想指出,既然你 所碰到的程序不會都運行使用htonl的INADDR_ANY。

  bind() 在錯誤的時候依然是返回-1,並且設置全局錯誤變量errno。

  在你調用 bind() 的時候,你要小心的另一件事情是:不要采用小于 1024的端口號。所有小于1024的端口號都被系統保留!你可以選擇從1024 到65535的端口(假如它們沒有被別的程序使用的話)。

  你要注重的另外一件小事是:有時候你根本不需要調用它。假如你使 用 connect() 來和遠程機器進行通訊,你不需要關心你的本地端口號(就象 你在使用 telnet 的時候),你只要簡單的調用 connect() 就可以了,它會檢 查套接字是否綁定端口,假如沒有,它會自己綁定一個沒有使用的本地端 口。

  connect()程序

  現在我們假設你是個 telnet 程序。你的用戶命令你得到套接字的文件 描述符。你服從命令調用了socket()。下一步,你的用戶告訴你通過端口 23(標准 telnet 端口)連接到"132.241.5.10"。你該怎麽做呢? 幸運的是,你正在閱讀 connect()--如何連接到遠程主機這一章。你可 不想讓你的用戶失望。

  

   connect() 系統調用是這樣的:

  #include

  #include

  int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);

  sockfd 是系統調用 socket() 返回的套接字文件描述符。serv_addr 是 保存著目的地端口和 IP 地址的數據結構 struct sockaddr。addrlen 設置 爲 sizeof(struct sockaddr)。

  想知道得更多嗎?讓我們來看個例子:

  #include

  #include

  #include

  #define DEST_IP "132.241.5.10"

  #define DEST_PORT 23

  main()

  {

  int sockfd;

  struct sockaddr_in dest_addr; /* 目的地址*/

  sockfd = socket(AF_INET, SOCK_STREAM, 0); /* 錯誤檢查 */

  dest_addr.sin_family = AF_INET; /* host byte order */

  dest_addr.sin_port = htons(DEST_PORT); /* short, network byte order */

  dest_addr.sin_addr.s_addr = inet_addr(DEST_IP);

  bzero(&(dest_addr.sin_zero),; /* zero the rest of the struct */

  /* don't forget to error check the connect()! */

  connect(sockfd, (struct sockaddr *)&dest_addr, sizeof(struct sockaddr));

  .

  .

  .

  再一次,你應該檢查 connect() 的返回值--它在錯誤的時候返回-1,並 設置全局錯誤變量 errno。

  同時,你可能看到,我沒有調用 bind()。因爲我不在乎本地的端口號。 我只關心我要去那。內核將爲我選擇一個合適的端口號,而我們所連接的 地方也自動地獲得這些信息。一切都不用擔心。

  listen()函數

  是換換內容得時候了。假如你不希望與遠程的一個地址相連,或者說, 僅僅是將它踢開,那你就需要等待接入請求並且用各種方法處理它們。處 理過程分兩步:首先,你聽--listen(),然後,你接受--accept() (請看下面的 內容)。

  除了要一點解釋外,系統調用 listen 也相當簡單。

  int listen(int sockfd, int backlog);

  sockfd 是調用 socket() 返回的套接字文件描述符。backlog 是在進入 隊列中答應的連接數目。什麽意思呢? 進入的連接是在隊列中一直等待直 到你接受 (accept() 請看下面的文章)連接。它們的數目限制于隊列的答應。 大多數系統的答應數目是20,你也可以設置爲5到10。

  和別的函數一樣,在發生錯誤的時候返回-1,並設置全局錯誤變量 errno。

  你可能想象到了,在你調用 listen() 前你或者要調用 bind() 或者讓內 核隨便選擇一個端口。假如你想偵聽進入的連接,那麽系統調用的順序可 能是這樣的:

  socket();

  bind();

  listen();

  /* accept() 應該在這 */

  因爲它相當的明了,我將在這裏不給出例子了。(在 accept() 那一章的 代碼將更加完全。)真正麻煩的部分在 accept()。

  accept()函數

  預備好了,系統調用 accept() 會有點古怪的地方的!你可以想象發生 這樣的事情:有人從很遠的地方通過一個你在偵聽 (listen()) 的端口連接 (connect()) 到你的機器。它的連接將加入到等待接受 (accept()) 的隊列 中。你調用 accept() 告訴它你有空閑的連接。它將返回一個新的套接字文 件描述符!這樣你就有兩個套接字了,原來的一個還在偵聽你的那個端口, 新的在預備發送 (send()) 和接收 ( recv()) 數據。這就是這個過程!

  函數是這樣定義的:

  #include

  int accept(int sockfd, void *addr, int *addrlen);

  sockfd 相當簡單,是和 listen() 中一樣的套接字描述符。addr 是個指 向局部的數據結構 sockaddr_in 的指針。這是要求接入的信息所要去的地 方(你可以測定那個地址在那個端口呼叫你)。在它的地址傳遞給 accept 之 前,addrlen 是個局部的整形變量,設置爲 sizeof(struct sockaddr_in)。 accept 將不會將多余的字節給 addr。假如你放入的少些,那麽它會通過改

  變 addrlen 的值反映出來。

  同樣,在錯誤時返回-1,並設置全局錯誤變量 errno。

  現在是你應該熟悉的代碼片段。

  #include

  #include

  #include

  #define MYPORT 3490 /*用戶接入端口*/

  #define BACKLOG 10 /* 多少等待連接控制*/

  main()

  {

  int sockfd, new_fd; /* listen on sock_fd, new connection on new_fd */

  struct sockaddr_in my_addr; /* 地址信息 */

  struct sockaddr_in their_addr; /* connector's address information */

  int sin_size;

  sockfd = socket(AF_INET, SOCK_STREAM, 0); /* 錯誤檢查*/

  my_addr.sin_family = AF_INET; /* host byte order */

  my_addr.sin_port = htons(MYPORT); /* short, network byte order */

  my_addr.sin_addr.s_addr = INADDR_ANY; /* auto-fill with my IP */

  bzero(&(my_addr.sin_zero),; /* zero the rest of the struct */

  /* don't forget your error checking for these calls: */

  

   bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr));

  listen(sockfd, BACKLOG);

  sin_size = sizeof(struct sockaddr_in);

  new_fd = accept(sockfd, &their_addr, &sin_size);

  .

  .

  .

  注重,在系統調用 send() 和 recv() 中你應該使用新的套接字描述符 new_fd。假如你只想讓一個連接進來,那麽你可以使用 close() 去關閉原 來的文件描述符 sockfd 來避免同一個端口更多的連接。

  send() and recv()函數

  這兩個函數用于流式套接字或者數據報套接字的通訊。假如你喜歡使 用無連接的數據報套接字,你應該看一看下面關于sendto() 和 recvfrom() 的章節。

  send() 是這樣的:

  int send(int sockfd, const void *msg, int len, int flags);

  sockfd 是你想發送數據的套接字描述符(或者是調用 socket() 或者是 accept() 返回的。)msg 是指向你想發送的數據的指針。len 是數據的長度。 把 flags 設置爲 0 就可以了。(具體的資料請看 send() 的 man page)。

  這裏是一些可能的例子:

  char *msg = "Beej was here!";

  int len, bytes_sent;

  .

  .

  len = strlen(msg);

  bytes_sent = send(sockfd, msg, len, 0);

  .

  .

  .

  send() 返回實際發送的數據的字節數--它可能小于你要求發送的數 目! 注重,有時候你告訴它要發送一堆數據可是它不能處理成功。它只是 發送它可能發送的數據,然後希望你能夠發送其它的數據。記住,假如 send() 返回的數據和 len 不匹配,你就應該發送其它的數據。但是這裏也 有個好消息:假如你要發送的包很小(小于大約 1K),它可能處理讓數據一 次發送完。最後要說得就是,它在錯誤的時候返回-1,並設置 errno。

  recv() 函數很相似:

  int recv(int sockfd, void *buf, int len, unsigned int flags);

  sockfd 是要讀的套接字描述符。buf 是要讀的信息的緩沖。len 是緩 沖的最大長度。flags 可以設置爲0。(請參考recv() 的 man page。) recv() 返回實際讀入緩沖的數據的字節數。或者在錯誤的時候返回-1, 同時設置 errno。

  很簡單,不是嗎? 你現在可以在流式套接字上發送數據和接收數據了。 你現在是 Unix 網絡程序員了!

  sendto() 和 recvfrom()函數

  「這很不錯啊」,你說,「但是你還沒有講無連接數據報套接字呢?」 沒問題,現在我們開始這個內容。

  既然數據報套接字不是連接到遠程主機的,那麽在我們發送一個包之 前需要什麽信息呢? 不錯,是目標地址!看看下面的:

  int sendto(int sockfd, const void *msg, int len, unsigned int flags,

  const struct sockaddr *to, int tolen);

  你已經看到了,除了另外的兩個信息外,其余的和函數 send() 是一樣 的。 to 是個指向數據結構 struct sockaddr 的指針,它包含了目的地的 IP 地址和端口信息。tolen 可以簡單地設置爲 sizeof(struct sockaddr)。 和函數 send() 類似,sendto() 返回實際發送的字節數(它也可能小于 你想要發送的字節數!),或者在錯誤的時候返回 -1。

  相似的還有函數 recv() 和 recvfrom()。recvfrom() 的定義是這樣的:

  int recvfrom(int sockfd, void *buf, int len, unsigned int flags, struct sockaddr *from, int *fromlen);

  又一次,除了兩個增加的參數外,這個函數和 recv() 也是一樣的。from 是一個指向局部數據結構 struct sockaddr 的指針,它的內容是源機器的 IP 地址和端口信息。fromlen 是個 int 型的局部指針,它的初始值爲 sizeof(struct sockaddr)。函數調用返回後,fromlen 保存著實際儲存在 from 中的地址的長度。

  recvfrom() 返回收到的字節長度,或者在發生錯誤後返回 -1。

  記住,假如你用 connect() 連接一個數據報套接字,你可以簡單的調 用 send() 和 recv() 來滿足你的要求。這個時候依然是數據報套接字,依 然使用 UDP,系統套接字接口會爲你自動加上了目標和源的信息。

  close()和shutdown()函數

  你已經整天都在發送 (send()) 和接收 (recv()) 數據了,現在你預備關 閉你的套接字描述符了。這很簡單,你可以使用一般的 Unix 文件描述符 的 close() 函數:

  close(sockfd);

  它將防止套接字上更多的數據的讀寫。任何在另一端讀寫套接字的企 圖都將返回錯誤信息。

  假如你想在如何關閉套接字上有多一點的控制,你可以使用函數 shutdown()。它答應你將一定方向上的通訊或者雙向的通訊(就象close()一 樣)關閉,你可以使用:

  int shutdown(int sockfd, int how);

  sockfd 是你想要關閉的套接字文件描述複。how 的值是下面的其中之 一:

  0 – 不答應接受

  1 – 不答應發送

  2 – 不答應發送和接受(和 close() 一樣)

  shutdown() 成功時返回 0,失敗時返回 -1(同時設置 errno。) 假如在無連接的數據報套接字中使用shutdown(),那麽只不過是讓 send() 和 recv() 不能使用(記住你在數據報套接字中使用了 connect 後 是可以使用它們的)。

  getpeername()函數

  這個函數太簡單了。

  它太簡單了,以至我都不想單列一章。但是我還是這樣做了。 函數 getpeername() 告訴你在連接的流式套接字上誰在另外一邊。函 數是這樣的:

  #include

  int getpeername(int sockfd, struct sockaddr *addr, int *addrlen);

  

   sockfd 是連接的流式套接字的描述符。addr 是一個指向結構 struct sockaddr (或者是 struct sockaddr_in) 的指針,它保存著連接的另一邊的 信息。addrlen 是一個 int 型的指針,它初始化爲 sizeof(struct sockaddr)。 函數在錯誤的時候返回 -1,設置相應的 errno。

  一旦你獲得它們的地址,你可以使用 inet_ntoa() 或者 gethostbyaddr() 來打印或者獲得更多的信息。但是你不能得到它的帳號。(假如它運行著愚 蠢的守護進程,這是可能的,但是它的討論已經超出了本文的範圍,請參 考 RFC-1413 以獲得更多的信息。)

  gethostname()函數

  甚至比 getpeername() 還簡單的函數是 gethostname()。它返回你程 序所運行的機器的主機名字。然後你可以使用 gethostbyname() 以獲得你 的機器的 IP 地址。

  下面是定義:

  #include

  int gethostname(char *hostname, size_t size);

  參數很簡單:hostname 是一個字符數組指針,它將在函數返回時保存

  主機名。size是hostname 數組的字節長度。

  函數調用成功時返回 0,失敗時返回 -1,並設置 errno。

  域名服務(DNS)

  假如你不知道 DNS 的意思,那麽我告訴你,它代表域名服務(Domain Name Service)。它主要的功能是:你給它一個輕易記憶的某站點的地址, 它給你 IP 地址(然後你就可以使用 bind(), connect(), sendto() 或者其它 函數) 。當一個人輸入:

  $ telnet whitehouse.gov

  telnet 能知道它將連接 (connect()) 到 "198.137.240.100"。

  但是這是如何工作的呢? 你可以調用函數 gethostbyname():

  #include

  struct hostent *gethostbyname(const char *name);

  很明白的是,它返回一個指向 struct hostent 的指針。這個數據結構 是這樣的:

  struct hostent {

  char *h_name;

  char **h_aliases;

  int h_addrtype;

  int h_length;

  char **h_addr_list;

  };

  #define h_addr h_addr_list[0]

  這裏是這個數據結構的具體資料:

  struct hostent:

  h_name – 地址的正式名稱。

  h_aliases – 空字節-地址的預備名稱的指針。

  h_addrtype –地址類型; 通常是AF_INET。

  h_length – 地址的比特長度。

  h_addr_list – 零字節-主機網絡地址指針。網絡字節順序。

  h_addr - h_addr_list中的第一地址。

  gethostbyname() 成功時返回一個指向結構體 hostent 的指針,或者 是個空 (NULL) 指針。(但是和以前不同,不設置errno,h_errno 設置錯 誤信息。請看下面的 herror()。)

  但是如何使用呢? 有時候(我們可以從電腦手冊中發現),向讀者灌輸 信息是不夠的。這個函數可不象它看上去那麽難用。

  這裏是個例子:

  #include

  #include

  #include

  #include

  #include

  #include

  int main(int argc, char *argv[])

  {

  struct hostent *h;

  if (argc != 2) { /* 檢查命令行 */

  fprintf(stderr,"usage: getip address\n");

  exit(1);

  }

  if ((h=gethostbyname(argv[1])) == NULL) { /* 取得地址信息 */

  herror("gethostbyname");

  exit(1);

  }

  printf("Host name : %s\n", h->h_name);

  printf("IP Address : %s\n",inet_ntoa(*((struct in_addr *)h->h_addr)));

  return 0;

  }

  在使用 gethostbyname() 的時候,你不能用 perror() 打印錯誤信息 (因爲 errno 沒有使用),你應該調用 herror()。

  相當簡單,你只是傳遞一個保存機器名的字符串(例如 "whitehouse.gov") 給 gethostbyname(),然後從返回的數據結構 struct hostent 中獲取信息。

  唯一也許讓人不解的是輸出 IP 地址信息。h->h_addr 是一個 char *, 但是 inet_ntoa() 需要的是 struct in_addr。因此,我轉換 h->h_addr 成 struct in_addr *,然後得到數據。

  客戶-服務器背景知識

  這裏是個客戶--服務器的世界。在網絡上的所有東西都是在處理客戶進 程和服務器進程的交談。舉個telnet 的例子。當你用 telnet (客戶)通過23 號端口登陸到主機,主機上運行的一個程序(一般叫 telnetd,服務器)激活。 它處理這個連接,顯示登陸界面,等等。

  圖2:客戶機和服務器的關系

  圖 2 說明了客戶和服務器之間的信息交換。

  注重,客戶--服務器之間可以使用SOCK_STREAM、SOCK_DGRAM 或者其它(只要它們采用相同的)。一些很好的客戶--服務器的例子有 telnet/telnetd、 ftp/ftpd 和 bootp/bootpd。每次你使用 ftp 的時候,在遠 端都有一個 ftpd 爲你服務。

  一般,在服務端只有一個服務器,它采用 fork() 來處理多個客戶的連 接。基本的程序是:服務器等待一個連接,接受 (accept()) 連接,然後 fork() 一個子進程處理它。這是下一章我們的例子中會講到的。

  簡單的服務器

  這個服務器所做的全部工作是在流式連接上發送字符串 "Hello, World!\n"。你要測試這個程序的話,可以在一台機器上運行該程序,然後 在另外一機器上登陸:

  

   $ telnet remotehostname 3490

  remotehostname 是該程序運行的機器的名字。

  服務器代碼:

  #include

  #include

  #include

  #include

  #include

  #include

  #include

  #include

  #define MYPORT 3490 /*定義用戶連接端口*/

  #define BACKLOG 10 /*多少等待連接控制*/

  main()

  {

  int sockfd, new_fd; /* listen on sock_fd, new connection on new_fd

  */

  struct sockaddr_in my_addr; /* my address information */

  struct sockaddr_in their_addr; /* connector's address information */

  int sin_size;

  if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {

  perror("socket");

  exit(1);

  }

  my_addr.sin_family = AF_INET; /* host byte order */

  my_addr.sin_port = htons(MYPORT); /* short, network byte order */

  my_addr.sin_addr.s_addr = INADDR_ANY; /* auto-fill with my IP */

  bzero(&(my_addr.sin_zero),; /* zero the rest of the struct */

  if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct

  sockaddr))== -1) {

  perror("bind");

  exit(1);

  }

  if (listen(sockfd, BACKLOG) == -1) {

  perror("listen");

  exit(1);

  }

  while(1) { /* main accept() loop */

  sin_size = sizeof(struct sockaddr_in);

  if ((new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &sin_size)) == -1) {

  perror("accept");

  continue;

  }

  printf("server: got connection from %s\n", inet_ntoa(their_addr.sin_addr));

  if (!fork()) { /* this is the child process */

  if (send(new_fd, "Hello, world!\n", 14, 0) == -1)

  perror("send");

  close(new_fd);

  exit(0);

  }

  close(new_fd); /* parent doesn't need this */

  while(waitpid(-1,NULL,WNOHANG) > 0); /* clean up child processes */

  }

  }

  假如你很挑剔的話,一定不滿足我所有的代碼都在一個很大的main() 函數中。假如你不喜歡,可以劃分得更細點。

  你也可以用我們下一章中的程序得到服務器端發送的字符串。

  簡單的客戶程序

  這個程序比服務器還簡單。這個程序的所有工作是通過 3490 端口連接到命令行中指定的主機,然後得到服務器發送的字符串。

  客戶代碼:

  #include

  #include

  #include

  #include

  #include

  #include

  #include

  #include

  #define PORT 3490 /* 客戶機連接遠程主機的端口 */

  #define MAXDATASIZE 100 /* 每次可以接收的最大字節 */

  int main(int argc, char *argv[])

  {

  int sockfd, numbytes;

  char buf[MAXDATASIZE];

  struct hostent *he;

  struct sockaddr_in their_addr; /* connector's address information */

  if (argc != 2) {

  fprintf(stderr,"usage: client hostname\n");

  exit(1);

  }

  if ((he=gethostbyname(argv[1])) == NULL) { /* get the host info */

  herror("gethostbyname");

  exit(1);

  }

  if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {

  perror("socket");

  exit(1);

  }

  their_addr.sin_family = AF_INET; /* host byte order */

  their_addr.sin_port = htons(PORT); /* short, network byte order */

  their_addr.sin_addr = *((struct in_addr *)he->h_addr);

  bzero(&(their_addr.sin_zero),; /* zero the rest of the struct */

  if (connect(sockfd, (struct sockaddr *)&their_addr,sizeof(struct

  

   sockaddr)) == -1) {

  perror("connect");

  exit(1);

  }

  if ((numbytes=recv(sockfd, buf, MAXDATASIZE, 0)) == -1) {

  perror("recv");

  exit(1);

  }

  buf[numbytes] = '\0';

  printf("Received: %s",buf);

  close(sockfd);

  return 0;

  }

  注重,假如你在運行服務器之前運行客戶程序,connect() 將返回 "Connection refused" 信息,這非常有用。

  數據包 Sockets

  我不想講更多了,所以我給出代碼 talker.c 和 listener.c。

  listener 在機器上等待在端口 4590 來的數據包。talker 發送數據包到 一定的機器,它包含用戶在命令行輸入的內容。

  這裏就是 listener.c:

  #include

  #include

  #include

  #include

  #include

  #include

  #include

  #include

  #define MYPORT 4950 /* the port users will be sending to */

  #define MAXBUFLEN 100

  main()

  {

  int sockfd;

  struct sockaddr_in my_addr; /* my address information */

  struct sockaddr_in their_addr; /* connector's address information */

  int addr_len, numbytes;

  char buf[MAXBUFLEN];

  if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {

  perror("socket");

  exit(1);

  }

  my_addr.sin_family = AF_INET; /* host byte order */

  my_addr.sin_port = htons(MYPORT); /* short, network byte order */

  my_addr.sin_addr.s_addr = INADDR_ANY; /* auto-fill with my IP */

  bzero(&(my_addr.sin_zero),; /* zero the rest of the struct */

  if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr))

  == -1) {

  perror("bind");

  exit(1);

  }

  addr_len = sizeof(struct sockaddr);

  if ((numbytes=recvfrom(sockfd, buf, MAXBUFLEN, 0, (struct sockaddr *)&their_addr, &addr_len)) == -1) {

  perror("recvfrom");

  exit(1);

  }

  printf("got packet from %s\n",inet_ntoa(their_addr.sin_addr));

  printf("packet is %d bytes long\n",numbytes);

  buf[numbytes] = '\0';

  printf("packet contains "%s"\n",buf);

  close(sockfd);

  }

  注重在我們的調用 socket(),我們最後使用了 SOCK_DGRAM。同時, 沒有必要去使用 listen() 或者 accept()。我們在使用無連接的數據報套接 字!

  下面是 talker.c:

  #include

  #include

  #include

  #include

  #include

  #include

  #include

  #include

  #define MYPORT 4950 /* the port users will be sending to */

  int main(int argc, char *argv[])

  {

  int sockfd;

  struct sockaddr_in their_addr; /* connector's address information */

  struct hostent *he;

  int numbytes;

  if (argc != 3) {

  fprintf(stderr,"usage: talker hostname message\n");

  exit(1);

  }

  if ((he=gethostbyname(argv[1])) == NULL) { /* get the host info */

  herror("gethostbyname");

  exit(1);

  }

  if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {

  perror("socket");

  exit(1);

  }

  their_addr.sin_family = AF_INET; /* host byte order */

  their_addr.sin_port = htons(MYPORT); /* short, network byte order

  */

  their_addr.sin_addr = *((struct in_addr *)he->h_addr);

  bzero(&(their_addr.sin_zero),; /* zero the rest of the struct */

  if ((numbytes=sendto(sockfd, argv[2], strlen(argv[2]), 0,

  

   (struct sockaddr *)&their_addr, sizeof(struct sockaddr))) == -1) {

  perror("sendto");

  exit(1);

  }

  printf("sent %d bytes to

  %s\n",numbytes,inet_ntoa(their_addr.sin_addr));

  close(sockfd);

  return 0;

  }

  這就是所有的了。在一台機器上運行 listener,然後在另外一台機器上 運行 talker。觀察它們的通訊!

  除了一些我在上面提到的數據套接字連接的小細節外,對于數據套接 字,我還得說一些,當一個講話者呼叫connect()函數時並指定接受者的地 址時,從這點可以看出,講話者只能向connect()函數指定的地址發送和接 受信息。因此,你不需要使用sendto()和recvfrom(),你完全可以用send() 和recv()代替。

  阻塞

  阻塞,你也許早就聽說了。"阻塞"是 "sleep" 的科技行話。你可能注重 到前面運行的 listener 程序,它在那裏不停地運行,等待數據包的到來。 實際在運行的是它調用 recvfrom(),然後沒有數據,因此 recvfrom() 說" 阻塞 (block)",直到數據的到來。

  很多函數都利用阻塞。accept() 阻塞,所有的 recv*() 函數阻塞。它 們之所以能這樣做是因爲它們被答應這樣做。當你第一次調用 socket() 建 立套接字描述符的時候,內核就將它設置爲阻塞。假如你不想套接字阻塞, 你就要調用函數 fcntl():

  #include

  #include

  .

  .

  sockfd = socket(AF_INET, SOCK_STREAM, 0);

  fcntl(sockfd, F_SETFL, O_NONBLOCK);

  .

  .

  通過設置套接字爲非阻塞,你能夠有效地"詢問"套接字以獲得信息。如 果你嘗試著從一個非阻塞的套接字讀信息並且沒有任何數據,它不答應阻 塞--它將返回 -1 並將 errno 設置爲 EWOULDBLOCK。

  但是一般說來,這種詢問不是個好主意。假如你讓你的程序在忙等狀 態查詢套接字的數據,你將浪費大量的 CPU 時間。更好的解決之道是用 下一章講的 select() 去查詢是否有數據要讀進來。

  select()--多路同步 I/O

  雖然這個函數有點希奇,但是它很有用。假設這樣的情況:你是個服 務器,你一邊在不停地從連接上讀數據,一邊在偵聽連接上的信息。 沒問題,你可能會說,不就是一個 accept() 和兩個 recv() 嗎? 這麽 輕易嗎,朋友? 假如你在調用 accept() 的時候阻塞呢? 你怎麽能夠同時接 受 recv() 數據? 「用非阻塞的套接字啊!」 不行!你不想耗盡所有的 CPU 吧? 那麽,該如何是好?

  select() 讓你可以同時監視多個套接字。假如你想知道的話,那麽它就 會告訴你哪個套接字預備讀,哪個又預備寫,哪個套接字又發生了例外 (exception)。

  閑話少說,下面是 select():

  #include

  #include

  #include

  int select(int numfds, fd_set *readfds, fd_set *writefds,fd_set

  *exceptfds, struct timeval *timeout);

  這個函數監視一系列文件描述符,非凡是 readfds、writefds 和 exceptfds。假如你想知道你是否能夠從標准輸入和套接字描述符 sockfd 讀入數據,你只要將文件描述符 0 和 sockfd 加入到集合 readfds 中。參 數 numfds 應該等于最高的文件描述符的值加1。在這個例子中,你應該 設置該值爲 sockfd+1。因爲它一定大于標准輸入的文件描述符 (0)。 當函數 select() 返回的時候,readfds 的值修改爲反映你選擇的哪個 文件描述符可以讀。你可以用下面講到的宏 FD_ISSET() 來測試。 在我們繼續下去之前,讓我來講講如何對這些集合進行操作。每個集 合類型都是 fd_set。下面有一些宏來對這個類型進行操作:

  FD_ZERO(fd_set *set) – 清除一個文件描述符集合

  FD_SET(int fd, fd_set *set) - 添加fd到集合

  FD_CLR(int fd, fd_set *set) – 從集合中移去fd

  FD_ISSET(int fd, fd_set *set) – 測試fd是否在集合中

  最後,是有點古怪的數據結構 struct timeval。有時你可不想永遠等待 別人發送數據過來。也許什麽事情都沒有發生的時候你也想每隔96秒在終 端上打印字符串 "Still Going..."。這個數據結構答應你設定一個時間,假如 時間到了,而 select() 還沒有找到一個預備好的文件描述符,它將返回讓 你繼續處理。

  數據結構 struct timeval 是這樣的:

  struct timeval {

  int tv_sec; /* seconds */

  int tv_usec; /* microseconds */

  };

  只要將 tv_sec 設置爲你要等待的秒數,將 tv_usec 設置爲你要等待 的微秒數就可以了。是的,是微秒而不是毫秒。1,000微秒等于1毫秒,1,000 毫秒等于1秒。也就是說,1秒等于1,000,000微秒。爲什麽用符號 "usec" 呢? 字母 "u" 很象希臘字母 Mu,而 Mu 表示 "微" 的意思。當然,函數 返回的時候 timeout 可能是剩余的時間,之所以是可能,是因爲它依靠于 你的 Unix 操作系統。

  哈!我們現在有一個微秒級的定時器!別計算了,標准的 Unix 系統 的時間片是100毫秒,所以無論你如何設置你的數據結構 struct timeval, 你都要等待那麽長的時間。

  還有一些有趣的事情:假如你設置數據結構 struct timeval 中的數據爲 0,select() 將立即超時,這樣就可以有效地輪詢集合中的所有的文件描述 符。假如你將參數 timeout 賦值爲 NULL,那麽將永遠不會發生超時,即 一直等到第一個文件描述符就緒。最後,假如你不是很關心等待多長時間, 那麽就把它賦爲 NULL 吧。

  下面的代碼演示了在標准輸入上等待 2.5 秒:

  

   #include

  #include

  #include

  #define STDIN 0 /* file descriptor for standard input */

  main()

  {

  struct timeval tv;

  fd_set readfds;

  tv.tv_sec = 2;

  tv.tv_usec = 500000;

  FD_ZERO(&readfds);

  FD_SET(STDIN, &readfds);

  /* don't care about writefds and exceptfds: */

  select(STDIN+1, &readfds, NULL, NULL, &tv);

  if (FD_ISSET(STDIN, &readfds))

  printf("A key was pressed!\n");

  else

  printf("Timed out.\n");

  }

  假如你是在一個 line buffered 終端上,那麽你敲的鍵應該是回車 (RETURN),否則無論如何它都會超時。

  現在,你可能回認爲這就是在數據報套接字上等待數據的方式--你是對 的:它可能是。有些 Unix 系統可以按這種方式,而另外一些則不能。你 在嘗試以前可能要先看看本系統的 man page 了。

  最後一件關于 select() 的事情:假如你有一個正在偵聽 (listen()) 的套 接字,你可以通過將該套接字的文件描述符加入到 readfds 集合中來看是 否有新的連接。

  這就是我關于函數select() 要講的所有的東西。

  參考書目:

  Internetworking with TCP/IP, volumes I-III by Douglas E. Comer and

  David L. Stevens. Published by Prentice Hall. Second edition ISBNs:

  0-13-468505-9, 0-13-472242-6, 0-13-474222-2. There is a third edition of

  this set which covers IPv6 and IP over ATM.

  Using C on the UNIX System by David A. Curry. Published by

  O'Reilly & Associates, Inc. ISBN 0-937175-23-4.

  TCP/IP Network Administration by Craig Hunt. Published by O'Reilly

  & Associates, Inc. ISBN 0-937175-82-X.

  TCP/IP Illustrated, volumes 1-3 by W. Richard Stevens and Gary R.

  Wright. Published by Addison Wesley. ISBNs: 0-201-63346-9,

  0-201-63354-X, 0-201-63495-3.

  Unix Network Programming by W. Richard Stevens. Published by

  Prentice Hall. ISBN 0-13-949876-1.

  On the web:

  BSD Sockets: A Quick And Dirty Primer

  (http://www.cs.umn.edu/~bentlema/unix/--has other great Unix

  system programming info, too!)

  Client-Server Computing

  (http://pandonia.canberra.edu.au/ClientServer/socket.html)

  Intro to TCP/IP (gopher)

  (gopher://gopher-chem.ucdavis.edu/11/Index/Internet_aw/Intro_the_Inter

  net/intro.to.ip/)

  Internet Protocol Frequently Asked Questions (France)

  (http://web.cnam.fr/Network/TCP-IP/)

  The Unix Socket FAQ

  (http://www.ibrado.com/sock-faq/)

  RFCs--the real dirt:

  RFC-768 -- The User Datagram Protocol (UDP)

  (ftp://nic.ddn.mil/rfc/rfc768.txt)

  RFC-791 -- The Internet Protocol (IP)

  (ftp://nic.ddn.mil/rfc/rfc791.txt)

  RFC-793 -- The Transmission Control Protocol (TCP)

  (ftp://nic.ddn.mil/rfc/rfc793.txt)

  RFC-854 -- The Telnet Protocol

  (ftp://nic.ddn.mil/rfc/rfc854.txt)

  RFC-951 -- The Bootstrap Protocol (BOOTP)

  

   (ftp://nic.ddn.mil/rfc/rfc951.txt)

  RFC-1350 -- The Trivial File Transfer Protocol (TFTP)

  (ftp://nic.ddn.mil/rfc/rfc1350.txt)
 
 介紹 Socket 編程讓你沮喪嗎?從man pages中很難得到有用的信息嗎?你想跟上時代去編Internet相關的程序,但是爲你在調用 connect() 前的bind() 的結構而不知所措?等等… 好在我已經將這些事完成了,我將和所有人共享我的知識了。假如你了解 C 語言並想穿過網絡編程的沼澤,那麽你來對地方了。 讀者對象   這個文檔是一個指南,而不是參考書。假如你剛開始 socket 編程並想找一本入門書,那麽你是我的讀者。但這不是一本完全的 socket 編程書。 平台和編譯器   這篇文檔中的大多數代碼都在 Linux 平台PC 上用 GNU 的 gcc 成功編譯過。而且它們在 HPUX平台 上用 gcc 也成功編譯過。但是注重,並不是每個代碼片段都獨立測試過。 目錄: 1) 什麽是套接字? 2) Internet 套接字的兩種類型 3) 網絡理論 4) 結構體 5) 本機轉換 6) IP 地址和如何處理它們 7) socket()函數 8) bind()函數 9) connect()函數 10) listen()函數 11) accept()函數 12) send()和recv()函數 13) sendto()和recvfrom()函數 14) close()和shutdown()函數 15) getpeername()函數 16) gethostname()函數 17) 域名服務(DNS) 18) 客戶-服務器背景知識 19) 簡單的服務器 20) 簡單的客戶端 21) 數據報套接字Socket 22) 阻塞 23) select()--多路同步I/O 24) 參考資料 什麽是 socket?   你經常聽到人們談論著 「socket」,或許你還不知道它的確切含義。現在讓我告訴你:它是使用 標准Unix 文件描述符 (file descriptor) 和其它程序通訊的方式。 什麽? 你也許聽到一些Unix高手(hacker)這樣說過:「呀,Unix中的一切就是文件!」那個家夥也許正在說到一個事實:Unix 程序在執行任何形式的 I/O 的時候,程序是在讀或者寫一個文件描述符。一個文件描述符只是一個和打開的文件相關聯的整數。但是(注重後面的話),這個文件可能是一個網絡連接,FIFO,管道,終端,磁盤上的文件或者什麽其它的東西。Unix 中所有的東西就是文件!所以,你想和Internet上別的程序通訊的時候,你將要使用到文件描述符。你必須理解剛才的話。現在你腦海中或許冒出這樣的念頭:「那麽我從哪裏得到網絡通訊的文件描述符呢?」,這個問題無論如何我都要回答:你利用系統調用 socket(),它返回套接字描述符 (socket descriptor),然後你再通過它來進行send() 和 recv()調用。 「但是...」,你可能有很大的迷惑,「假如它是個文件描述符,那麽爲什 麽不用一般調用read()和write()來進行套接字通訊?」簡單的答案是:「你可以使用!」。具體的答案是:「你可以,但是使用send()和recv()讓你更好的控制數據傳輸。」 存在這樣一個情況:在我們的世界上,有很多種套接字。有DARPA Internet 地址 (Internet 套接字),本地節點的路徑名 (Unix套接字),CCITT X.25地址 (你可以將X.25 套接字完全忽略)。也許在你的Unix 機器上還有其它的。我們在這裏只講第一種:Internet 套接字。 Internet 套接字的兩種類型   什麽意思?有兩種類型的Internet 套接字?是的。不,我在撒謊。其實還有很多,但是我可不想嚇著你。我們這裏只講兩種。除了這些, 我打算另外介紹的 "Raw Sockets" 也是非常強大的,很值得查閱。 那麽這兩種類型是什麽呢?一種是"Stream Sockets"(流格式),另外一種是"Datagram Sockets"(數據包格式)。我們以後談到它們的時候也會用到 "SOCK_STREAM" 和 "SOCK_DGRAM"。數據報套接字有時也叫「無連接套接字」(假如你確實要連接的時候可以用connect()。) 流式套接字是可靠的雙向通訊的數據流。假如你向套接字按順序輸出「1,2」,那麽它們將按順序「1,2」到達另一邊。它們是無錯誤的傳遞的,有自己的錯誤控制,在此不討論。 有什麽在使用流式套接字?你可能聽說過 telnet,不是嗎?它就使用流式套接字。你需要你所輸入的字符按順序到達,不是嗎?同樣,WWW浏覽器使用的 HTTP 協議也使用它們來下載頁面。實際上,當你通過端口80 telnet 到一個 WWW 站點,然後輸入 「GET pagename」 的時候,你也可以得到 Html 的內容。爲什麽流式套接字可以達到高質量的數據傳輸?這是因爲它使用了「傳輸控制協議 (The Transmission Control Protocol)」,也叫 「TCP」 (請參考 RFC-793 獲得具體資料。)TCP 控制你的數據按順序到達並且沒有錯 誤。你也許聽到 「TCP」 是因爲聽到過 「TCP/IP」。這裏的 IP 是指「Internet 協議」(請參考 RFC-791。) IP 只是處理 Internet 路由而已。 那麽數據報套接字呢?爲什麽它叫無連接呢?爲什麽它是不可靠的呢?有這樣的一些事實:假如你發送一個數據報,它可能會到達,它可能次序顛倒了。假如它到達,那麽在這個包的內部是無錯誤的。數據報也使用 IP 作路由,但是它不使用 TCP。它使用「用戶數據報協議 (User Datagram Protocol)」,也叫 「UDP」 (請參考 RFC-768。) 爲什麽它們是無連接的呢?主要是因爲它並不象流式套接字那樣維持一個連接。你只要建立一個包,構造一個有目標信息的IP 頭,然後發出去。無需連接。它們通常使用于傳輸包-包信息。簡單的應用程序有:tFTP, bootp等等。 你也許會想:「假如數據丟失了這些程序如何正常工作?」我的朋友,每個程序在 UDP 上有自己的協議。例如,tftp 協議每發出的一個被接受到包,收到者必須發回一個包來說「我收到了!」 (一個「命令正確應答」也叫「ACK」 包)。假如在一定時間內(例如5秒),發送方沒有收到應答,它將重新發送,直到得到 ACK。這一ACK過程在實現 SOCK_DGRAM 應用程序的時候非常重要。 網絡理論   既然我剛才提到了協議層,那麽現在是討論網絡究竟如何工作和一些 關于 SOCK_DGRAM 包是如何建立的例子。當然,你也可以跳過這一段, 假如你認爲已經熟悉的話。 現在是學習數據封裝 (Data Encapsulation) 的時候了!它非常非常重 要。它重要性重要到你在網絡課程學 (圖1:數據封裝) 習中無論如何也得也得把握它。主要 的內容是:一個包,先是被第一個協議(在這裏是TFTP )在它的報頭(也許 是報尾)包裝(「封裝」),然後,整個數據(包括 TFTP 頭)被另外一個協議 (在這裏是 UDP )封裝,然後下一個( IP ),一直重複下去,直到硬件(物理) 層( 這裏是以太網 )。 當另外一台機器接收到包,硬件先剝去以太網頭,內核剝去IP和UDP 頭,TFTP程序再剝去TFTP頭,最後得到數據。 現在我們終于講到聲名狼藉的網絡分層模型 (Layered Network Model)。這種網絡模型在描述網絡系統上相對其它模型有很多優點。例如, 你可以寫一個套接字程序而不用關心數據的物理傳輸(串行口,以太網,連 接單元接口 (AUI) 還是其它介質),因爲底層的程序會爲你處理它們。實際 的網絡硬件和拓撲對于程序員來說是透明的。 不說其它廢話了,我現在列出整個層次模型。假如你要參加網絡考試, 可一定要記住: 應用層 (Application)    表示層 (Presentation) 會話層 (Session)   傳輸層(Transport)   網絡層(Network)   數據鏈路層(Data Link)   物理層(Physical) 物理層是硬件(串口,以太網等等)。應用層是和硬件層相隔最遠的--它 是用戶和網絡交互的地方。 這個模型如此通用,假如你想,你可以把它作爲修車指南。把它對應 到 Unix,結果是: 應用層(Application Layer) (telnet, ftp,等等)   傳輸層(Host-to-Host Transport Layer) (TCP, UDP)   Internet層(Internet Layer) (IP和路由)   網絡訪問層 (Network Access Layer) (網絡層,數據鏈路層和物理層) 現在,你可能看到這些層次如何協調來封裝原始的數據了。 看看建立一個簡單的數據包有多少工作?哎呀,你將不得不使用 "cat" 來建立數據包頭!這僅僅是個玩笑。對于流式套接字你要作的是 send() 發 送數據。對于數據報式套接字,你按照你選擇的方式封裝數據然後使用 sendto()。內核將爲你建立傳輸層和 Internet 層,硬件完成網絡訪問層。 這就是現代科技。 現在結束我們的網絡理論速成班。哦,忘記告訴你關于路由的事情了。 但是我不預備談它,假如你真的關心,那麽參考 IP RFC。 結構體   終于談到編程了。在這章,我將談到被套接字用到的各種數據類型。 因爲它們中的一些內容很重要了。 首先是簡單的一個:socket描述符。它是下面的類型: int 僅僅是一個常見的 int。 從現在起,事情變得不可思議了,而你所需做的就是繼續看下去。注 意這樣的事實:有兩種字節排列順序:重要的字節 (有時叫 "octet",即八 位位組) 在前面,或者不重要的字節在前面。前一種叫「網絡字節順序 (Network Byte Order)」。有些機器在內部是按照這個順序儲存數據,而另外 一些則不然。當我說某數據必須按照 NBO 順序,那麽你要調用函數(例如 htons() )來將它從本機字節順序 (Host Byte Order) 轉換過來。假如我沒有 提到 NBO, 那麽就讓它保持本機字節順序。 我的第一個結構(在這個技術手冊TM中)--strUCt sockaddr.。這個結構 爲許多類型的套接字儲存套接字地址信息: struct sockaddr {    unsigned short sa_family; /* 地址家族, AF_xxx */    char sa_data[14]; /*14字節協議地址*/    }; sa_family 能夠是各種各樣的類型,但是在這篇文章中都是 "AF_INET"。 sa_data包含套接字中的目標地址和端口信息。這似乎有點 不明智。 爲了處理struct sockaddr,程序員創造了一個並列的結構: struct sockaddr_in ("in" 代表 "Internet"。) struct sockaddr_in {    short int sin_family; /* 通信類型 */    unsigned short int sin_port; /* 端口 */    struct in_addr sin_addr; /* Internet 地址 */    unsigned char sin_zero[8]; /* 與sockaddr結構的長度相同*/    }; 用這個數據結構可以輕松處理套接字地址的基本元素。注重 sin_zero (它被加入到這個結構,並且長度和 struct sockaddr 一樣) 應該使用函數 bzero() 或 memset() 來全部置零。 同時,這一重要的字節,一個指向 sockaddr_in結構體的指針也可以被指向結構體sockaddr並且代替它。這 樣的話即使 socket() 想要的是 struct sockaddr *,你仍然可以使用 struct sockaddr_in,並且在最後轉換。同時,注重 sin_family 和 struct sockaddr 中的 sa_family 一致並能夠設置爲 "AF_INET"。最後,sin_port和 sin_addr 必須是網絡字節順序 (Network Byte Order)! 你也許會反對道:"但是,怎麽讓整個數據結構 struct in_addr sin_addr 按照網絡字節順序呢?" 要知道這個問題的答案,我們就要仔細的看一看這 個數據結構: struct in_addr, 有這樣一個聯合 (unions): /* Internet 地址 (一個與曆史有關的結構) */    struct in_addr {    unsigned long s_addr;    }; 它曾經是個最壞的聯合,但是現在那些日子過去了。假如你聲明 "ina" 是數據結構 struct sockaddr_in 的實例,那麽 "ina.sin_addr.s_addr" 就儲 存4字節的 IP 地址(使用網絡字節順序)。假如你不幸的系統使用的還是恐 怖的聯合 struct in_addr ,你還是可以放心4字節的 IP 地址並且和上面 我說的一樣(這是因爲使用了「#define」。) 本機轉換   我們現在到了新的章節。我們曾經講了很多網絡到本機字節順序的轉 換,現在可以實踐了! 你能夠轉換兩種類型: short (兩個字節)和 long (四個字節)。這個函 數對于變量類型 unsigned 也適用。假設你想將 short 從本機字節順序轉 換爲網絡字節順序。用 "h" 表示 "本機 (host)",接著是 "to",然後用 "n" 表 示 "網絡 (network)",最後用 "s" 表示 "short": h-to-n-s, 或者 htons() ("Host to Network Short")。 太簡單了... 假如不是太傻的話,你一定想到了由"n","h","s",和 "l"形成的正確 組合,例如這裏肯定沒有stolh() ("Short to Long Host") 函數,不僅在這裏 沒有,所有場合都沒有。但是這裏有: htons()--"Host to Network Short"   htonl()--"Host to Network Long"   ntohs()--"Network to Host Short"   ntohl()--"Network to Host Long" 現在,你可能想你已經知道它們了。你也可能想:「假如我想改變 char 的順序要怎麽辦呢?」 但是你也許馬上就想到,「用不著考慮的」。你也許 會想到:我的 68000 機器已經使用了網絡字節順序,我沒有必要去調用 htonl() 轉換 IP 地址。你可能是對的,但是當你移植你的程序到別的機器 上的時候,你的程序將失敗。可移植性!這裏是 Unix 世界!記住:在你 將數據放到網絡上的時候,確信它們是網絡字節順序的。 最後一點:爲什麽在數據結構 struct sockaddr_in 中, sin_addr 和 sin_port 需要轉換爲網絡字節順序,而sin_family 需不需要呢? 答案是: sin_addr 和 sin_port 分別封裝在包的 IP 和 UDP 層。因此,它們必須要 是網絡字節順序。但是 sin_family 域只是被內核 (kernel) 使用來決定在數 據結構中包含什麽類型的地址,所以它必須是本機字節順序。同時, sin_family 沒有發送到網絡上,它們可以是本機字節順序。 IP 地址和如何處理它們 現在我們很幸運,因爲我們有很多的函數來方便地操作 IP 地址。沒有 必要用手工計算它們,也沒有必要用"<<"操作來儲存成長整字型。 首先,假設你已經有了一個sockaddr_in結構體ina,你有一個IP地 址"132.241.5.10"要儲存在其中,你就要用到函數inet_addr(),將IP地址從 點數格式轉換成無符號長整型。使用方法如下: ina.sin_addr.s_addr = inet_addr("132.241.5.10"); 注重,inet_addr()返回的地址已經是網絡字節格式,所以你無需再調用 函數htonl()。 我們現在發現上面的代碼片斷不是十分完整的,因爲它沒有錯誤檢查。 顯而易見,當inet_addr()發生錯誤時返回-1。記住這些二進制數字?(無符 號數)-1僅僅和IP地址255.255.255.255相符合!這可是廣播地址!大錯特 錯!記住要先進行錯誤檢查。 好了,現在你可以將IP地址轉換成長整型了。有沒有其相反的方法呢? 它可以將一個in_addr結構體輸出成點數格式?這樣的話,你就要用到函數 inet_ntoa()("ntoa"的含義是"network to ascii"),就像這樣: printf("%s",inet_ntoa(ina.sin_addr)); 它將輸出IP地址。需要注重的是inet_ntoa()將結構體in-addr作爲一 個參數,不是長整形。同樣需要注重的是它返回的是一個指向一個字符的 指針。它是一個由inet_ntoa()控制的靜態的固定的指針,所以每次調用 inet_ntoa(),它就將覆蓋上次調用時所得的IP地址。例如: char *a1, *a2; . . a1 = inet_ntoa(ina1.sin_addr); /* 這是198.92.129.1 */ a2 = inet_ntoa(ina2.sin_addr); /* 這是132.241.5.10 */ printf("address 1: %s\n",a1); printf("address 2: %s\n",a2); 輸出如下: address 1: 132.241.5.10 address 2: 132.241.5.10 假如你需要保存這個IP地址,使用strcopy()函數來指向你自己的字符 指針。 上面就是關于這個主題的介紹。稍後,你將學習將一個類 似"wintehouse.gov"的字符串轉換成它所對應的IP地址(查閱域名服務,稍 後)。 socket()函數 我想我不能再不提這個了-下面我將討論一下socket()系統調用。 下面是具體介紹: #include #include int socket(int domain, int type, int protocol); 但是它們的參數是什麽? 首先,domain 應該設置成 "AF_INET",就 象上面的數據結構struct sockaddr_in 中一樣。然後,參數 type 告訴內核 是 SOCK_STREAM 類型還是 SOCK_DGRAM 類型。最後,把 protocol 設置爲 "0"。(注重:有很多種 domain、type,我不可能一一列出了,請看 socket() 的 man幫助。當然,還有一個"更好"的方式去得到 protocol。同 時請查閱 getprotobyname() 的 man 幫助。) socket() 只是返回你以後在系統調用種可能用到的 socket 描述符,或 者在錯誤的時候返回-1。全局變量 errno 中將儲存返回的錯誤值。(請參考 perror() 的 man 幫助。) bind()函數   一旦你有一個套接字,你可能要將套接字和機器上的一定的端口關聯 起來。(假如你想用listen()來偵聽一定端口的數據,這是必要一步--MUD 告 訴你說用命令 "telnet x.y.z 6969"。)假如你只想用 connect(),那麽這個步 驟沒有必要。但是無論如何,請繼續讀下去。 這裏是系統調用 bind() 的大概: #include #include int bind(int sockfd, struct sockaddr *my_addr, int addrlen); sockfd 是調用 socket 返回的文件描述符。my_addr 是指向數據結構 struct sockaddr 的指針,它保存你的地址(即端口和 IP 地址) 信息。 addrlen 設置爲 sizeof(struct sockaddr)。 簡單得很不是嗎? 再看看例子: #include #include #include #define MYPORT 3490 main()    {    int sockfd;    struct sockaddr_in my_addr; sockfd = socket(AF_INET, SOCK_STREAM, 0); /*需要錯誤檢查 */ my_addr.sin_family = AF_INET; /* host byte order */    my_addr.sin_port = htons(MYPORT); /* short, network byte order */    my_addr.sin_addr.s_addr = inet_addr("132.241.5.10");    bzero(&(my_addr.sin_zero),; /* zero the rest of the struct */ /* don't forget your error checking for bind(): */    bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr));    .    .    . 這裏也有要注重的幾件事情。my_addr.sin_port 是網絡字節順序, my_addr.sin_addr.s_addr 也是的。另外要注重到的事情是因系統的不同, 包含的頭文件也不盡相同,請查閱本地的 man 幫助文件。 在 bind() 主題中最後要說的話是,在處理自己的 IP 地址和/或端口的 時候,有些工作是可以自動處理的。 my_addr.sin_port = 0; /* 隨機選擇一個沒有使用的端口 */   my_addr.sin_addr.s_addr = INADDR_ANY; /* 使用自己的IP地址 */ 通過將0賦給 my_addr.sin_port,你告訴 bind() 自己選擇合適的端 口。同樣,將 my_addr.sin_addr.s_addr 設置爲 INADDR_ANY,你告訴 它自動填上它所運行的機器的 IP 地址。 假如你一向小心謹慎,那麽你可能注重到我沒有將 INADDR_ANY 轉 換爲網絡字節順序!這是因爲我知道內部的東西:INADDR_ANY 實際上就 是 0!即使你改變字節的順序,0依然是0。但是完美主義者說應該處處一 致,INADDR_ANY或許是12呢?你的代碼就不能工作了,那麽就看下面 的代碼: my_addr.sin_port = htons(0); /* 隨機選擇一個沒有使用的端口 */ my_addr.sin_addr.s_addr = htonl(INADDR_ANY);/* 使用自己的IP地址 */ 你或許不相信,上面的代碼將可以隨便移植。我只是想指出,既然你 所碰到的程序不會都運行使用htonl的INADDR_ANY。 bind() 在錯誤的時候依然是返回-1,並且設置全局錯誤變量errno。 在你調用 bind() 的時候,你要小心的另一件事情是:不要采用小于 1024的端口號。所有小于1024的端口號都被系統保留!你可以選擇從1024 到65535的端口(假如它們沒有被別的程序使用的話)。 你要注重的另外一件小事是:有時候你根本不需要調用它。假如你使 用 connect() 來和遠程機器進行通訊,你不需要關心你的本地端口號(就象 你在使用 telnet 的時候),你只要簡單的調用 connect() 就可以了,它會檢 查套接字是否綁定端口,假如沒有,它會自己綁定一個沒有使用的本地端 口。 connect()程序   現在我們假設你是個 telnet 程序。你的用戶命令你得到套接字的文件 描述符。你服從命令調用了socket()。下一步,你的用戶告訴你通過端口 23(標准 telnet 端口)連接到"132.241.5.10"。你該怎麽做呢? 幸運的是,你正在閱讀 connect()--如何連接到遠程主機這一章。你可 不想讓你的用戶失望。 connect() 系統調用是這樣的: #include #include int connect(int sockfd, struct sockaddr *serv_addr, int addrlen); sockfd 是系統調用 socket() 返回的套接字文件描述符。serv_addr 是 保存著目的地端口和 IP 地址的數據結構 struct sockaddr。addrlen 設置 爲 sizeof(struct sockaddr)。 想知道得更多嗎?讓我們來看個例子: #include #include #include #define DEST_IP "132.241.5.10"   #define DEST_PORT 23 main()    { int sockfd; struct sockaddr_in dest_addr; /* 目的地址*/ sockfd = socket(AF_INET, SOCK_STREAM, 0); /* 錯誤檢查 */ dest_addr.sin_family = AF_INET; /* host byte order */ dest_addr.sin_port = htons(DEST_PORT); /* short, network byte order */ dest_addr.sin_addr.s_addr = inet_addr(DEST_IP); bzero(&(dest_addr.sin_zero),; /* zero the rest of the struct */ /* don't forget to error check the connect()! */ connect(sockfd, (struct sockaddr *)&dest_addr, sizeof(struct sockaddr));    .    .    .   再一次,你應該檢查 connect() 的返回值--它在錯誤的時候返回-1,並 設置全局錯誤變量 errno。 同時,你可能看到,我沒有調用 bind()。因爲我不在乎本地的端口號。 我只關心我要去那。內核將爲我選擇一個合適的端口號,而我們所連接的 地方也自動地獲得這些信息。一切都不用擔心。 listen()函數   是換換內容得時候了。假如你不希望與遠程的一個地址相連,或者說, 僅僅是將它踢開,那你就需要等待接入請求並且用各種方法處理它們。處 理過程分兩步:首先,你聽--listen(),然後,你接受--accept() (請看下面的 內容)。 除了要一點解釋外,系統調用 listen 也相當簡單。 int listen(int sockfd, int backlog); sockfd 是調用 socket() 返回的套接字文件描述符。backlog 是在進入 隊列中答應的連接數目。什麽意思呢? 進入的連接是在隊列中一直等待直 到你接受 (accept() 請看下面的文章)連接。它們的數目限制于隊列的答應。 大多數系統的答應數目是20,你也可以設置爲5到10。 和別的函數一樣,在發生錯誤的時候返回-1,並設置全局錯誤變量 errno。 你可能想象到了,在你調用 listen() 前你或者要調用 bind() 或者讓內 核隨便選擇一個端口。假如你想偵聽進入的連接,那麽系統調用的順序可 能是這樣的: socket();   bind(); listen();   /* accept() 應該在這 */ 因爲它相當的明了,我將在這裏不給出例子了。(在 accept() 那一章的 代碼將更加完全。)真正麻煩的部分在 accept()。 accept()函數   預備好了,系統調用 accept() 會有點古怪的地方的!你可以想象發生 這樣的事情:有人從很遠的地方通過一個你在偵聽 (listen()) 的端口連接 (connect()) 到你的機器。它的連接將加入到等待接受 (accept()) 的隊列 中。你調用 accept() 告訴它你有空閑的連接。它將返回一個新的套接字文 件描述符!這樣你就有兩個套接字了,原來的一個還在偵聽你的那個端口, 新的在預備發送 (send()) 和接收 ( recv()) 數據。這就是這個過程! 函數是這樣定義的: #include int accept(int sockfd, void *addr, int *addrlen); sockfd 相當簡單,是和 listen() 中一樣的套接字描述符。addr 是個指 向局部的數據結構 sockaddr_in 的指針。這是要求接入的信息所要去的地 方(你可以測定那個地址在那個端口呼叫你)。在它的地址傳遞給 accept 之 前,addrlen 是個局部的整形變量,設置爲 sizeof(struct sockaddr_in)。 accept 將不會將多余的字節給 addr。假如你放入的少些,那麽它會通過改 變 addrlen 的值反映出來。 同樣,在錯誤時返回-1,並設置全局錯誤變量 errno。 現在是你應該熟悉的代碼片段。 #include #include #include #define MYPORT 3490 /*用戶接入端口*/ #define BACKLOG 10 /* 多少等待連接控制*/ main()    {   int sockfd, new_fd; /* listen on sock_fd, new connection on new_fd */   struct sockaddr_in my_addr; /* 地址信息 */   struct sockaddr_in their_addr; /* connector's address information */   int sin_size; sockfd = socket(AF_INET, SOCK_STREAM, 0); /* 錯誤檢查*/ my_addr.sin_family = AF_INET; /* host byte order */   my_addr.sin_port = htons(MYPORT); /* short, network byte order */   my_addr.sin_addr.s_addr = INADDR_ANY; /* auto-fill with my IP */   bzero(&(my_addr.sin_zero),; /* zero the rest of the struct */ /* don't forget your error checking for these calls: */   bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)); listen(sockfd, BACKLOG); sin_size = sizeof(struct sockaddr_in);   new_fd = accept(sockfd, &their_addr, &sin_size);    .    .    . 注重,在系統調用 send() 和 recv() 中你應該使用新的套接字描述符 new_fd。假如你只想讓一個連接進來,那麽你可以使用 close() 去關閉原 來的文件描述符 sockfd 來避免同一個端口更多的連接。 send() and recv()函數   這兩個函數用于流式套接字或者數據報套接字的通訊。假如你喜歡使 用無連接的數據報套接字,你應該看一看下面關于sendto() 和 recvfrom() 的章節。 send() 是這樣的: int send(int sockfd, const void *msg, int len, int flags); sockfd 是你想發送數據的套接字描述符(或者是調用 socket() 或者是 accept() 返回的。)msg 是指向你想發送的數據的指針。len 是數據的長度。 把 flags 設置爲 0 就可以了。(具體的資料請看 send() 的 man page)。 這裏是一些可能的例子: char *msg = "Beej was here!";   int len, bytes_sent;   .   .   len = strlen(msg);   bytes_sent = send(sockfd, msg, len, 0);   .   .   . send() 返回實際發送的數據的字節數--它可能小于你要求發送的數 目! 注重,有時候你告訴它要發送一堆數據可是它不能處理成功。它只是 發送它可能發送的數據,然後希望你能夠發送其它的數據。記住,假如 send() 返回的數據和 len 不匹配,你就應該發送其它的數據。但是這裏也 有個好消息:假如你要發送的包很小(小于大約 1K),它可能處理讓數據一 次發送完。最後要說得就是,它在錯誤的時候返回-1,並設置 errno。 recv() 函數很相似: int recv(int sockfd, void *buf, int len, unsigned int flags); sockfd 是要讀的套接字描述符。buf 是要讀的信息的緩沖。len 是緩 沖的最大長度。flags 可以設置爲0。(請參考recv() 的 man page。) recv() 返回實際讀入緩沖的數據的字節數。或者在錯誤的時候返回-1, 同時設置 errno。 很簡單,不是嗎? 你現在可以在流式套接字上發送數據和接收數據了。 你現在是 Unix 網絡程序員了! sendto() 和 recvfrom()函數   「這很不錯啊」,你說,「但是你還沒有講無連接數據報套接字呢?」 沒問題,現在我們開始這個內容。 既然數據報套接字不是連接到遠程主機的,那麽在我們發送一個包之 前需要什麽信息呢? 不錯,是目標地址!看看下面的: int sendto(int sockfd, const void *msg, int len, unsigned int flags,   const struct sockaddr *to, int tolen); 你已經看到了,除了另外的兩個信息外,其余的和函數 send() 是一樣 的。 to 是個指向數據結構 struct sockaddr 的指針,它包含了目的地的 IP 地址和端口信息。tolen 可以簡單地設置爲 sizeof(struct sockaddr)。 和函數 send() 類似,sendto() 返回實際發送的字節數(它也可能小于 你想要發送的字節數!),或者在錯誤的時候返回 -1。 相似的還有函數 recv() 和 recvfrom()。recvfrom() 的定義是這樣的: int recvfrom(int sockfd, void *buf, int len, unsigned int flags,  struct sockaddr *from, int *fromlen); 又一次,除了兩個增加的參數外,這個函數和 recv() 也是一樣的。from 是一個指向局部數據結構 struct sockaddr 的指針,它的內容是源機器的 IP 地址和端口信息。fromlen 是個 int 型的局部指針,它的初始值爲 sizeof(struct sockaddr)。函數調用返回後,fromlen 保存著實際儲存在 from 中的地址的長度。 recvfrom() 返回收到的字節長度,或者在發生錯誤後返回 -1。 記住,假如你用 connect() 連接一個數據報套接字,你可以簡單的調 用 send() 和 recv() 來滿足你的要求。這個時候依然是數據報套接字,依 然使用 UDP,系統套接字接口會爲你自動加上了目標和源的信息。 close()和shutdown()函數   你已經整天都在發送 (send()) 和接收 (recv()) 數據了,現在你預備關 閉你的套接字描述符了。這很簡單,你可以使用一般的 Unix 文件描述符 的 close() 函數:   close(sockfd); 它將防止套接字上更多的數據的讀寫。任何在另一端讀寫套接字的企 圖都將返回錯誤信息。 假如你想在如何關閉套接字上有多一點的控制,你可以使用函數 shutdown()。它答應你將一定方向上的通訊或者雙向的通訊(就象close()一 樣)關閉,你可以使用: int shutdown(int sockfd, int how); sockfd 是你想要關閉的套接字文件描述複。how 的值是下面的其中之 一:   0 – 不答應接受   1 – 不答應發送   2 – 不答應發送和接受(和 close() 一樣) shutdown() 成功時返回 0,失敗時返回 -1(同時設置 errno。) 假如在無連接的數據報套接字中使用shutdown(),那麽只不過是讓 send() 和 recv() 不能使用(記住你在數據報套接字中使用了 connect 後 是可以使用它們的)。 getpeername()函數   這個函數太簡單了。 它太簡單了,以至我都不想單列一章。但是我還是這樣做了。 函數 getpeername() 告訴你在連接的流式套接字上誰在另外一邊。函 數是這樣的: #include int getpeername(int sockfd, struct sockaddr *addr, int *addrlen); sockfd 是連接的流式套接字的描述符。addr 是一個指向結構 struct sockaddr (或者是 struct sockaddr_in) 的指針,它保存著連接的另一邊的 信息。addrlen 是一個 int 型的指針,它初始化爲 sizeof(struct sockaddr)。 函數在錯誤的時候返回 -1,設置相應的 errno。 一旦你獲得它們的地址,你可以使用 inet_ntoa() 或者 gethostbyaddr() 來打印或者獲得更多的信息。但是你不能得到它的帳號。(假如它運行著愚 蠢的守護進程,這是可能的,但是它的討論已經超出了本文的範圍,請參 考 RFC-1413 以獲得更多的信息。) gethostname()函數   甚至比 getpeername() 還簡單的函數是 gethostname()。它返回你程 序所運行的機器的主機名字。然後你可以使用 gethostbyname() 以獲得你 的機器的 IP 地址。   下面是定義:   #include int gethostname(char *hostname, size_t size); 參數很簡單:hostname 是一個字符數組指針,它將在函數返回時保存 主機名。size是hostname 數組的字節長度。 函數調用成功時返回 0,失敗時返回 -1,並設置 errno。 域名服務(DNS)   假如你不知道 DNS 的意思,那麽我告訴你,它代表域名服務(Domain Name Service)。它主要的功能是:你給它一個輕易記憶的某站點的地址, 它給你 IP 地址(然後你就可以使用 bind(), connect(), sendto() 或者其它 函數) 。當一個人輸入:    $ telnet whitehouse.gov telnet 能知道它將連接 (connect()) 到 "198.137.240.100"。 但是這是如何工作的呢? 你可以調用函數 gethostbyname(): #include   struct hostent *gethostbyname(const char *name); 很明白的是,它返回一個指向 struct hostent 的指針。這個數據結構 是這樣的:    struct hostent {    char *h_name;    char **h_aliases;    int h_addrtype;    int h_length;    char **h_addr_list;    };    #define h_addr h_addr_list[0] 這裏是這個數據結構的具體資料: struct hostent:   h_name – 地址的正式名稱。   h_aliases – 空字節-地址的預備名稱的指針。   h_addrtype –地址類型; 通常是AF_INET。   h_length – 地址的比特長度。   h_addr_list – 零字節-主機網絡地址指針。網絡字節順序。   h_addr - h_addr_list中的第一地址。 gethostbyname() 成功時返回一個指向結構體 hostent 的指針,或者 是個空 (NULL) 指針。(但是和以前不同,不設置errno,h_errno 設置錯 誤信息。請看下面的 herror()。) 但是如何使用呢? 有時候(我們可以從電腦手冊中發現),向讀者灌輸 信息是不夠的。這個函數可不象它看上去那麽難用。 這裏是個例子: #include   #include   #include   #include   #include   #include int main(int argc, char *argv[])    {    struct hostent *h; if (argc != 2) { /* 檢查命令行 */    fprintf(stderr,"usage: getip address\n");    exit(1);    } if ((h=gethostbyname(argv[1])) == NULL) { /* 取得地址信息 */    herror("gethostbyname");    exit(1);    } printf("Host name : %s\n", h->h_name);   printf("IP Address : %s\n",inet_ntoa(*((struct in_addr *)h->h_addr))); return 0;    } 在使用 gethostbyname() 的時候,你不能用 perror() 打印錯誤信息 (因爲 errno 沒有使用),你應該調用 herror()。 相當簡單,你只是傳遞一個保存機器名的字符串(例如 "whitehouse.gov") 給 gethostbyname(),然後從返回的數據結構 struct hostent 中獲取信息。 唯一也許讓人不解的是輸出 IP 地址信息。h->h_addr 是一個 char *, 但是 inet_ntoa() 需要的是 struct in_addr。因此,我轉換 h->h_addr 成 struct in_addr *,然後得到數據。 客戶-服務器背景知識   這裏是個客戶--服務器的世界。在網絡上的所有東西都是在處理客戶進 程和服務器進程的交談。舉個telnet 的例子。當你用 telnet (客戶)通過23 號端口登陸到主機,主機上運行的一個程序(一般叫 telnetd,服務器)激活。 它處理這個連接,顯示登陸界面,等等。 圖2:客戶機和服務器的關系 圖 2 說明了客戶和服務器之間的信息交換。 注重,客戶--服務器之間可以使用SOCK_STREAM、SOCK_DGRAM 或者其它(只要它們采用相同的)。一些很好的客戶--服務器的例子有 telnet/telnetd、 ftp/ftpd 和 bootp/bootpd。每次你使用 ftp 的時候,在遠 端都有一個 ftpd 爲你服務。 一般,在服務端只有一個服務器,它采用 fork() 來處理多個客戶的連 接。基本的程序是:服務器等待一個連接,接受 (accept()) 連接,然後 fork() 一個子進程處理它。這是下一章我們的例子中會講到的。 簡單的服務器   這個服務器所做的全部工作是在流式連接上發送字符串 "Hello, World!\n"。你要測試這個程序的話,可以在一台機器上運行該程序,然後 在另外一機器上登陸:    $ telnet remotehostname 3490 remotehostname 是該程序運行的機器的名字。 服務器代碼: #include   #include   #include   #include   #include   #include   #include   #include #define MYPORT 3490 /*定義用戶連接端口*/ #define BACKLOG 10 /*多少等待連接控制*/ main()    {    int sockfd, new_fd; /* listen on sock_fd, new connection on new_fd */    struct sockaddr_in my_addr; /* my address information */    struct sockaddr_in their_addr; /* connector's address information */    int sin_size; if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {    perror("socket");    exit(1);    } my_addr.sin_family = AF_INET; /* host byte order */    my_addr.sin_port = htons(MYPORT); /* short, network byte order */    my_addr.sin_addr.s_addr = INADDR_ANY; /* auto-fill with my IP */    bzero(&(my_addr.sin_zero),; /* zero the rest of the struct */ if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr))== -1) {    perror("bind");    exit(1);    } if (listen(sockfd, BACKLOG) == -1) {    perror("listen");    exit(1);    } while(1) { /* main accept() loop */    sin_size = sizeof(struct sockaddr_in);    if ((new_fd = accept(sockfd, (struct sockaddr *)&their_addr,    &sin_size)) == -1) {    perror("accept");    continue;    }    printf("server: got connection from %s\n",    inet_ntoa(their_addr.sin_addr));    if (!fork()) { /* this is the child process */    if (send(new_fd, "Hello, world!\n", 14, 0) == -1)    perror("send");    close(new_fd);    exit(0);    }    close(new_fd); /* parent doesn't need this */ while(waitpid(-1,NULL,WNOHANG) > 0); /* clean up child processes */    }    } 假如你很挑剔的話,一定不滿足我所有的代碼都在一個很大的main() 函數中。假如你不喜歡,可以劃分得更細點。 你也可以用我們下一章中的程序得到服務器端發送的字符串。 簡單的客戶程序   這個程序比服務器還簡單。這個程序的所有工作是通過 3490 端口連接到命令行中指定的主機,然後得到服務器發送的字符串。 客戶代碼: #include   #include   #include   #include   #include   #include   #include   #include #define PORT 3490 /* 客戶機連接遠程主機的端口 */ #define MAXDATASIZE 100 /* 每次可以接收的最大字節 */ int main(int argc, char *argv[])    {    int sockfd, numbytes;    char buf[MAXDATASIZE];    struct hostent *he;    struct sockaddr_in their_addr; /* connector's address information */ if (argc != 2) {    fprintf(stderr,"usage: client hostname\n");    exit(1);    } if ((he=gethostbyname(argv[1])) == NULL) { /* get the host info */    herror("gethostbyname");    exit(1);    } if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {    perror("socket");    exit(1);    } their_addr.sin_family = AF_INET; /* host byte order */   their_addr.sin_port = htons(PORT); /* short, network byte order */   their_addr.sin_addr = *((struct in_addr *)he->h_addr);   bzero(&(their_addr.sin_zero),; /* zero the rest of the struct */ if (connect(sockfd, (struct sockaddr *)&their_addr,sizeof(struct sockaddr)) == -1) {    perror("connect");    exit(1);    } if ((numbytes=recv(sockfd, buf, MAXDATASIZE, 0)) == -1) {    perror("recv");    exit(1);    } buf[numbytes] = '\0'; printf("Received: %s",buf); close(sockfd); return 0;    } 注重,假如你在運行服務器之前運行客戶程序,connect() 將返回 "Connection refused" 信息,這非常有用。 數據包 Sockets   我不想講更多了,所以我給出代碼 talker.c 和 listener.c。 listener 在機器上等待在端口 4590 來的數據包。talker 發送數據包到 一定的機器,它包含用戶在命令行輸入的內容。 這裏就是 listener.c: #include   #include   #include   #include   #include   #include   #include   #include #define MYPORT 4950 /* the port users will be sending to */ #define MAXBUFLEN 100 main()    {    int sockfd;    struct sockaddr_in my_addr; /* my address information */    struct sockaddr_in their_addr; /* connector's address information */    int addr_len, numbytes;    char buf[MAXBUFLEN]; if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {    perror("socket");    exit(1);    } my_addr.sin_family = AF_INET; /* host byte order */    my_addr.sin_port = htons(MYPORT); /* short, network byte order */    my_addr.sin_addr.s_addr = INADDR_ANY; /* auto-fill with my IP */    bzero(&(my_addr.sin_zero),; /* zero the rest of the struct */ if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr))    == -1) {    perror("bind");    exit(1);    } addr_len = sizeof(struct sockaddr);    if ((numbytes=recvfrom(sockfd, buf, MAXBUFLEN, 0,    (struct sockaddr *)&their_addr, &addr_len)) == -1) {    perror("recvfrom");    exit(1);    } printf("got packet from %s\n",inet_ntoa(their_addr.sin_addr));    printf("packet is %d bytes long\n",numbytes);    buf[numbytes] = '\0';    printf("packet contains "%s"\n",buf); close(sockfd);    } 注重在我們的調用 socket(),我們最後使用了 SOCK_DGRAM。同時, 沒有必要去使用 listen() 或者 accept()。我們在使用無連接的數據報套接 字! 下面是 talker.c: #include   #include   #include   #include   #include   #include   #include   #include #define MYPORT 4950 /* the port users will be sending to */ int main(int argc, char *argv[])    {    int sockfd;    struct sockaddr_in their_addr; /* connector's address information */    struct hostent *he;    int numbytes; if (argc != 3) {    fprintf(stderr,"usage: talker hostname message\n");    exit(1);    } if ((he=gethostbyname(argv[1])) == NULL) { /* get the host info */    herror("gethostbyname");    exit(1);    } if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {    perror("socket");    exit(1);    } their_addr.sin_family = AF_INET; /* host byte order */    their_addr.sin_port = htons(MYPORT); /* short, network byte order */    their_addr.sin_addr = *((struct in_addr *)he->h_addr);    bzero(&(their_addr.sin_zero),; /* zero the rest of the struct */ if ((numbytes=sendto(sockfd, argv[2], strlen(argv[2]), 0,    (struct sockaddr *)&their_addr, sizeof(struct sockaddr))) == -1) {    perror("sendto");    exit(1);    } printf("sent %d bytes to %s\n",numbytes,inet_ntoa(their_addr.sin_addr)); close(sockfd); return 0;    } 這就是所有的了。在一台機器上運行 listener,然後在另外一台機器上 運行 talker。觀察它們的通訊! 除了一些我在上面提到的數據套接字連接的小細節外,對于數據套接 字,我還得說一些,當一個講話者呼叫connect()函數時並指定接受者的地 址時,從這點可以看出,講話者只能向connect()函數指定的地址發送和接 受信息。因此,你不需要使用sendto()和recvfrom(),你完全可以用send() 和recv()代替。 阻塞   阻塞,你也許早就聽說了。"阻塞"是 "sleep" 的科技行話。你可能注重 到前面運行的 listener 程序,它在那裏不停地運行,等待數據包的到來。 實際在運行的是它調用 recvfrom(),然後沒有數據,因此 recvfrom() 說" 阻塞 (block)",直到數據的到來。 很多函數都利用阻塞。accept() 阻塞,所有的 recv*() 函數阻塞。它 們之所以能這樣做是因爲它們被答應這樣做。當你第一次調用 socket() 建 立套接字描述符的時候,內核就將它設置爲阻塞。假如你不想套接字阻塞, 你就要調用函數 fcntl(): #include   #include    .    .    sockfd = socket(AF_INET, SOCK_STREAM, 0);    fcntl(sockfd, F_SETFL, O_NONBLOCK);    .    .   通過設置套接字爲非阻塞,你能夠有效地"詢問"套接字以獲得信息。如 果你嘗試著從一個非阻塞的套接字讀信息並且沒有任何數據,它不答應阻 塞--它將返回 -1 並將 errno 設置爲 EWOULDBLOCK。 但是一般說來,這種詢問不是個好主意。假如你讓你的程序在忙等狀 態查詢套接字的數據,你將浪費大量的 CPU 時間。更好的解決之道是用 下一章講的 select() 去查詢是否有數據要讀進來。 select()--多路同步 I/O   雖然這個函數有點希奇,但是它很有用。假設這樣的情況:你是個服 務器,你一邊在不停地從連接上讀數據,一邊在偵聽連接上的信息。 沒問題,你可能會說,不就是一個 accept() 和兩個 recv() 嗎? 這麽 輕易嗎,朋友? 假如你在調用 accept() 的時候阻塞呢? 你怎麽能夠同時接 受 recv() 數據? 「用非阻塞的套接字啊!」 不行!你不想耗盡所有的 CPU 吧? 那麽,該如何是好? select() 讓你可以同時監視多個套接字。假如你想知道的話,那麽它就 會告訴你哪個套接字預備讀,哪個又預備寫,哪個套接字又發生了例外 (exception)。 閑話少說,下面是 select(): #include   #include   #include int select(int numfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout); 這個函數監視一系列文件描述符,非凡是 readfds、writefds 和 exceptfds。假如你想知道你是否能夠從標准輸入和套接字描述符 sockfd 讀入數據,你只要將文件描述符 0 和 sockfd 加入到集合 readfds 中。參 數 numfds 應該等于最高的文件描述符的值加1。在這個例子中,你應該 設置該值爲 sockfd+1。因爲它一定大于標准輸入的文件描述符 (0)。 當函數 select() 返回的時候,readfds 的值修改爲反映你選擇的哪個 文件描述符可以讀。你可以用下面講到的宏 FD_ISSET() 來測試。 在我們繼續下去之前,讓我來講講如何對這些集合進行操作。每個集 合類型都是 fd_set。下面有一些宏來對這個類型進行操作: FD_ZERO(fd_set *set) – 清除一個文件描述符集合   FD_SET(int fd, fd_set *set) - 添加fd到集合   FD_CLR(int fd, fd_set *set) – 從集合中移去fd   FD_ISSET(int fd, fd_set *set) – 測試fd是否在集合中 最後,是有點古怪的數據結構 struct timeval。有時你可不想永遠等待 別人發送數據過來。也許什麽事情都沒有發生的時候你也想每隔96秒在終 端上打印字符串 "Still Going..."。這個數據結構答應你設定一個時間,假如 時間到了,而 select() 還沒有找到一個預備好的文件描述符,它將返回讓 你繼續處理。 數據結構 struct timeval 是這樣的: struct timeval {    int tv_sec; /* seconds */    int tv_usec; /* microseconds */    }; 只要將 tv_sec 設置爲你要等待的秒數,將 tv_usec 設置爲你要等待 的微秒數就可以了。是的,是微秒而不是毫秒。1,000微秒等于1毫秒,1,000 毫秒等于1秒。也就是說,1秒等于1,000,000微秒。爲什麽用符號 "usec" 呢? 字母 "u" 很象希臘字母 Mu,而 Mu 表示 "微" 的意思。當然,函數 返回的時候 timeout 可能是剩余的時間,之所以是可能,是因爲它依靠于 你的 Unix 操作系統。 哈!我們現在有一個微秒級的定時器!別計算了,標准的 Unix 系統 的時間片是100毫秒,所以無論你如何設置你的數據結構 struct timeval, 你都要等待那麽長的時間。 還有一些有趣的事情:假如你設置數據結構 struct timeval 中的數據爲 0,select() 將立即超時,這樣就可以有效地輪詢集合中的所有的文件描述 符。假如你將參數 timeout 賦值爲 NULL,那麽將永遠不會發生超時,即 一直等到第一個文件描述符就緒。最後,假如你不是很關心等待多長時間, 那麽就把它賦爲 NULL 吧。 下面的代碼演示了在標准輸入上等待 2.5 秒: #include   #include   #include #define STDIN 0 /* file descriptor for standard input */ main()    {   struct timeval tv;   fd_set readfds; tv.tv_sec = 2;   tv.tv_usec = 500000; FD_ZERO(&readfds);   FD_SET(STDIN, &readfds); /* don't care about writefds and exceptfds: */   select(STDIN+1, &readfds, NULL, NULL, &tv); if (FD_ISSET(STDIN, &readfds))   printf("A key was pressed!\n");   else   printf("Timed out.\n");   } 假如你是在一個 line buffered 終端上,那麽你敲的鍵應該是回車 (RETURN),否則無論如何它都會超時。 現在,你可能回認爲這就是在數據報套接字上等待數據的方式--你是對 的:它可能是。有些 Unix 系統可以按這種方式,而另外一些則不能。你 在嘗試以前可能要先看看本系統的 man page 了。 最後一件關于 select() 的事情:假如你有一個正在偵聽 (listen()) 的套 接字,你可以通過將該套接字的文件描述符加入到 readfds 集合中來看是 否有新的連接。 這就是我關于函數select() 要講的所有的東西。   參考書目:   Internetworking with TCP/IP, volumes I-III by Douglas E. Comer and David L. Stevens. Published by Prentice Hall. Second edition ISBNs: 0-13-468505-9, 0-13-472242-6, 0-13-474222-2. There is a third edition of this set which covers IPv6 and IP over ATM.   Using C on the UNIX System by David A. Curry. Published by O'Reilly & Associates, Inc. ISBN 0-937175-23-4.   TCP/IP Network Administration by Craig Hunt. Published by O'Reilly & Associates, Inc. ISBN 0-937175-82-X.   TCP/IP Illustrated, volumes 1-3 by W. Richard Stevens and Gary R. Wright. Published by Addison Wesley. ISBNs: 0-201-63346-9, 0-201-63354-X, 0-201-63495-3. Unix Network Programming by W. Richard Stevens. Published by Prentice Hall. ISBN 0-13-949876-1.   On the web:   BSD Sockets: A Quick And Dirty Primer   (http://www.cs.umn.edu/~bentlema/unix/--has other great Unix system programming info, too!) Client-Server Computing   (http://pandonia.canberra.edu.au/ClientServer/socket.html) Intro to TCP/IP (gopher) (gopher://gopher-chem.ucdavis.edu/11/Index/Internet_aw/Intro_the_Inter net/intro.to.ip/) Internet Protocol Frequently Asked Questions (France)   (http://web.cnam.fr/Network/TCP-IP/) The Unix Socket FAQ   (http://www.ibrado.com/sock-faq/) RFCs--the real dirt:   RFC-768 -- The User Datagram Protocol (UDP)    (ftp://nic.ddn.mil/rfc/rfc768.txt) RFC-791 -- The Internet Protocol (IP)   (ftp://nic.ddn.mil/rfc/rfc791.txt) RFC-793 -- The Transmission Control Protocol (TCP)    (ftp://nic.ddn.mil/rfc/rfc793.txt) RFC-854 -- The Telnet Protocol    (ftp://nic.ddn.mil/rfc/rfc854.txt) RFC-951 -- The Bootstrap Protocol (BOOTP)  (ftp://nic.ddn.mil/rfc/rfc951.txt) RFC-1350 -- The Trivial File Transfer Protocol (TFTP)    (ftp://nic.ddn.mil/rfc/rfc1350.txt)
󰈣󰈤
 
 
 
>>返回首頁<<
 
 
 
 
 熱帖排行
 
王朝網路微信公眾號
微信掃碼關註本站公眾號 wangchaonetcn
 
  免責聲明:本文僅代表作者個人觀點,與王朝網絡無關。王朝網絡登載此文出於傳遞更多信息之目的,並不意味著贊同其觀點或證實其描述,其原創性以及文中陳述文字和內容未經本站證實,對本文以及其中全部或者部分內容、文字的真實性、完整性、及時性本站不作任何保證或承諾,請讀者僅作參考,並請自行核實相關內容。
 
© 2005- 王朝網路 版權所有