linux-socket詳解與實例

介紹
Hey! Socket 編程讓你沮喪嗎?從 man pages 中很難得到有用的信息嗎?你想 跟上時代去做一做 Internet 程序,但是爲你在調用 connect() 前的 bind() 的結構而愁眉不展?…
好了,我現在已經來了,我將和所有人共享我的知識了。如果你瞭解 C 語言並想穿過 網絡編程的沼澤,那麼你來對地方了。
讀者
這個文檔是寫成一個指南,而不是參考書。如果你剛開始 socket 編程並想找一本 入門書,那麼你是我的讀者。這可不是一本完全的 socket 編程書。
平臺和編譯器
這篇文章中的大多數代碼都在一臺 Linux PC 上用 GNU 的 gcc 成功編譯過。 而且他們在一臺 HPUX 上用 gcc 也成功編譯過。但是注意,並不是每個代碼 片段都獨立測試過。
--------------------------------------------------------------------------------
目錄:
什麼是套接口?
Internet 套接口的兩種類型
網絡理論
struct--要麼瞭解他們,要麼等異形入侵地球
Convert the Natives!
IP 地址和如何處理他們
socket()--得到文件描述符!
bind()--我們在哪個端口?
connect()--Hello!
listen()--有人給我打電話嗎?
accept()--"Thank you for calling port 3490."
send() 和 recv()--Talk to me, baby!
sendto() 和 recvfrom()--Talk to me, DGRAM-style
close() 和 shutdown()--滾開!
getpeername()--你是誰?
gethostname()--我是誰?
DNS--你說“白宮”,我說 "198.137.240.100"
客戶-服務器背景知識
簡單的服務器
簡單的客戶端
數據報 Socket
阻塞
select()--多路同步 I/O,酷!

參考資料
Disclaimer and Call for Help
--------------------------------------------------------------------------------
什麼是 socket?
你始終聽到人們談論着 "socket",而你不知道他的確切含義。那麼,現在我告訴你: 他是使用 Unix 文件描述符 (fiel descriptor) 和其他程序通訊的方式。
什麼?
Ok--你也許聽到一些 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 套接口?是的。不,我在撒謊。其實還有很多,但是我可不想 嚇着你。我們這裏只講兩種。 Except for this sentence, where I'm going to tell you that "Raw Sockets" are also very powerful and you should look them up.
好了,好了。那兩種類型是什麼呢?一種是 "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。這一點在實現 SOCK_DGRAM 應用程序的時候非常重要。
--------------------------------------------------------------------------------
網絡理論
既然我剛纔提到了協議層,那麼現在是討論網絡究竟如何工作和演示 SOCK_DGRAM 的工作。當然,你也可以跳過這一段,如果你認爲 已經熟悉的話。
朋友們,現在是學習 數據封裝 (Data Encapsulation) 的時候了! 這非常非常重要。It's so important that you might just learn about it if you take the networks course here at Chico State ;-). 主要的內容是:一個包,先是被第一個協議(在這裏是 TFTP )包裝(“封裝”), 然後,整個數據(包括 TFTP 頭)被另外一個協議(在這裏是 UDP )封裝,然後下 一個( IP ),一直重複下去,直到硬件(物理)層( Ethernet )。
當另外一臺機器接收到包,硬件先剝去 Ethernet 頭,內核剝去 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。如果你從來不曾瞭解他,也沒有 關係,你還活着不是嗎。
--------------------------------------------------------------------------------
structs
終於到達這裏了,終於談到編程了。在這章,我將談到被套接口用到的各種數據類型。因爲 他們中的一些太重要了。
首先是簡單的一個:socket descriptor。他是下面的類型:
  int
僅僅是一個常見的 int。
從現在起,事情變得不可思議了。請跟我一起忍受苦惱吧。注意這樣的事實: 有兩種字節排列順序:重要的字節在前面(有時叫 "octet"),或者不重要的字節在前面。 前一種叫“網絡字節順序 (Network Byte Order)”。有些機器在內部是按照這個順序儲存數據,而另外一些則不然。當我說某數據必須按照 NBO 順序,那麼你要調用函數(例 如 htons() )來將他從本機字節順序 (Host Byte Order) 轉換過來。如果我 沒有提到 NBO, 那麼就讓他是本機字節順序吧。
我的第一個結構(TM)--struct sockaddr. 這個數據結構 爲許多類型的套接口儲存套接口地址信息:
struct sockaddr {
    unsigned short  sa_family;  /* address family, AF_xxx    */
    char       sa_data[14]; /* 14 bytes of protocol address */
};
sa_family 能夠是各種各樣的事情,但是在這篇文章中是 "AF_INET"。 sa_data 爲套接口儲存目標地址和端口信息。看上去很笨拙,不是嗎。
爲了對付 struct sockaddr,程序員創造了一個並列的結構: struct sockaddr_in ("in" 代表 "Internet".)
struct sockaddr_in {
    short int     sin_family; /* Address family        */
    unsigned short int sin_port;  /* Port number         */
    struct in_addr   sin_addr;  /* Internet address       */
    unsigned char   sin_zero[8]; /* Same size as struct sockaddr */

};
這個數據結構讓可以輕鬆處理套接口地址的基本元素。注意 sin_zero (他 被加入到這個結構,並且長度和 struct sockaddr 一樣) 應該使用函數 bzero() 或 memset() 來全部置零。 Also, and this is the important bit, a pointer to a struct sockaddr_in can be cast to a pointer to a struct sockaddr and vice-versa. 這樣的話 即使 socket() 想要的是 struct sockaddr *, 你仍然可以使用 struct sockaddr_in,and cast it at the last minute! 同時,注意 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 address (a structure for historical reasons) */
  struct in_addr {
    unsigned long s_addr;
  };
他曾經是個最壞的聯合,但是現在那些日子過去了。如果你聲明 "ina" 是 數據結構 struct sockaddr_in 的實例,那麼 "ina.sin_addr.s_addr" 就儲存4字節的 IP 地址(網絡字節順序)。如果你不幸的 系統使用的還是恐怖的聯合 struct in_addr ,你還是可以放心4字 節的 IP 地址是和上面我說的一樣(這是因爲 #define。)
--------------------------------------------------------------------------------
Convert the Natives!
我們現在到達下個章節。我們曾經講了很多網絡到本機字節順序,現在是採取行動的時刻了!
你能夠轉換兩種類型: 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 地址。沒有必要用手工計算 他們,也沒有必要用 << 操作符來操作 long。
首先,假設你用 struct sockaddr_in ina,你想將 IP 地址 "132.241.5.10" 儲存到其中。你要用的函數是 inet_addr(),轉換 numbers-and-dots 格式的 IP 地址到 unsigned long。這個工作可以這樣來做:
  ina.sin_addr.s_addr = inet_addr("132.241.5.10");
注意:inet_addr() 返回的地址已經是按照網絡字節順序的,你沒有必要再去調用 htonl()。
上面的代碼可不是很健壯 (robust),因爲沒有錯誤檢查。inet_addr() 在發生錯誤 的時候返回-1。記得二進制數嗎? 在 IP 地址爲 255.255.255.255 的時候返回的是 (unsigned)-1!這是個廣播地址!記住正確的使用錯誤檢查。
好了,你現在可以轉換字符串形式的 IP 地址爲 long 了。那麼你有一個數據結構 struct in_addr,該如何按照 numbers-and-dots 格式打印呢? 在這個 時候,也許你要用函數 inet_ntoa() ("ntoa" 意思是 "network to ascii"):
  printf("%s",inet_ntoa(ina.sin_addr));
他將打印 IP 地址。注意的是:函數 inet_ntoa() 的參數是 struct in_addr,而不是 long。同時要注意的是他返回的是一個指向字符的指針。 在 inet_ntoa 內部的指針靜態地儲存字符數組,因此每次你調用 inet_ntoa() 的時候他將覆蓋以前的內容。例如:
  char *a1, *a2;
  .
  .
  a1 = inet_ntoa(ina1.sin_addr); /* this is 198.92.129.1 */
  a2 = inet_ntoa(ina2.sin_addr); /* this is 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
如果你想保存地址,那麼用 strcpy() 保存到自己的字符數組中。
這就是這章的內容了。以後,我們將學習轉換 "whitehouse.gov" 形式的字符串到正確 的 IP 地址(請看後面的 DNS 一章。)
--------------------------------------------------------------------------------
socket()--得到文件描述符!
我猜我不會再扯遠了--我必須講 socket() 這個系統調用了。這裏是詳細的定義:
  #include <sys/types.h>
  #include <sys/socket.h>
  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 page。當然,還有一個"更好"的方式 去得到 protocol。請看 getprotobyname() 的 man page。)
socket() 只是返回你以後在系統調用種可能用到的 socket 描述符,或者在錯誤 的時候返回-1。全局變量 errno 中儲存錯誤值。(請參考 perror() 的 man page。)
--------------------------------------------------------------------------------
bind()--我在哪個端口?
一旦你得到套接口,你可能要將套接口和機器上的一定的端口關聯起來。(如果你想用 listen() 來偵聽一定端口的數據,這是必要一步--MUD 經常告訴你說用命令 "telnet x.y.z 6969".)如果你只想用 connect(),那麼這個步驟沒有必要。但是無論如何,請繼續讀下去。
這裏是系統調用 bind() 的大略:
  #include <sys/types.h>
  #include <sys/socket.h>
  int bind(int sockfd, struct sockaddr *my_addr, int addrlen);
sockfd 是調用 socket 返回的文件描述符。my_addr 是指向 數據結構 struct sockaddr 的指針,他保存你的地址(即端口和 IP 地址) 信息。addrlen 設置爲 sizeof(struct sockaddr)。
簡單得很不是嗎? 再看看例子:
  #include <string.h>
  #include <sys/types.h>
  #include <sys/socket.h>
  #define MYPORT 3490
  main()
  {
    int sockfd;
    struct sockaddr_in my_addr;
    sockfd = socket(AF_INET, SOCK_STREAM, 0); /* do some error checking! */
    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), 8);  /* 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 page。
在 bind() 主題中最後要說的話是,在處理自己的 IP 地址和/或端口的時候,有些工作 是可以自動處理的。
    my_addr.sin_port = 0; /* choose an unused port at random */
    my_addr.sin_addr.s_addr = INADDR_ANY; /* use my IP address */
通過將0賦給 my_addr.sin_port,你告訴 bind() 自己選擇合適的端口。同樣, 將 y_addr.sin_addr.s_addr 設置爲 INADDR_ANY,你告訴他自動填上 他所運行的機器的 IP 地址。
如果你一向小心謹慎,那麼你可能注意到我沒有將 INADDR_ANY 轉換爲網絡字節順序!這是因爲我知道內部的東西:INADDR_ANY 實際上就是 0!即使你 改變字節的順序,0依然是0。但是完美主義者說安全第一,那麼看下面的代碼:
    my_addr.sin_port = htons(0); /* choose an unused port at random */
    my_addr.sin_addr.s_addr = htonl(INADDR_ANY); /* use my IP address */
你可能不相信,上面的代碼將可以隨便移植。
bind() 在錯誤的時候依然是返回-1,並且設置全局變量 errno。
在你調用 bind() 的時候,你要小心的另一件事情是:不要採用小於1024的端口號。所有小於1024的端口號都 被系統保留!你可以選擇從1024到65535(如果他們沒有被別的程序使用的話)。
你要注意的另外一件小事是:有時候你根本不需要調用他。如果你使用 connect() 來和遠程機器通訊,你不要關心你的本地端口號(就象你在使用 telnet 的時候),你只要 簡單的調用 connect() 就夠可,他會檢查套接口是否綁定,如果沒有,他會自己綁定 一個沒有使用的本地端口。
--------------------------------------------------------------------------------
connect()--Hello!
現在我們假設你是個 telnet 程序。你的用戶命令你(就象電影 TRON 中一樣)得到套接口 的文件描述符。你聽從命令調用了 socket()。下一步,你的用戶告訴你通過端口23(標 準 telnet 端口)連接到"132.241.5.10"。你該怎麼做呢?
幸運的是,你正在瘋狂地閱讀 connect()--如何連接到遠程主機這一章。你可不想讓 你的用戶失望。
connect() 系統調用是這樣的:
  #include <sys/types.h>
  #include <sys/socket.h>
  int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);
sockfd 是系統調用 socket() 返回的套接口文件描述符。serv_addr 是保存着目的地端口和 IP 地址的數據結構 struct sockaddr。addrlen 設置爲 sizeof(struct sockaddr)。
讓我們來看個例子:
  #include <string.h>
  #include <sys/types.h>
  #include <sys/socket.h>
  #define DEST_IP  "132.241.5.10"
  #define DEST_PORT 23
  main()
  {
    int sockfd;
    struct sockaddr_in dest_addr;  /* will hold the destination addr */
   sockfd = socket(AF_INET, SOCK_STREAM, 0); /* do some error checking! */
    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), 8);    /* 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()--Will somebody please call me?
Ok, time for a change of pace. What if you don't want to connect to a remote host. Say, just for kicks, that you want to wait for incoming connections and handle them in some way. 處理過程分兩步:首先,你聽--listen(),然後,你接受--accept() (請看 下面的內容)。
除了要一點解釋外,系統調用 listen 相當簡單。
 int listen(int sockfd, int backlog);
sockfd 是調用 socket() 返回的套接口文件描述符。backlog 是 在進入隊列中允許的連接數目。是什麼意思呢? 進入的連接是在隊列中一直等待直到你接受 (accept() 請看下面的文章)的連接。他們的數目限制於隊列的允許。大多數系統的允許數目是20,你也可以設置爲5到10。
和別的函數一樣,在發生錯誤的時候返回-1,並設置全局變量 errno。
你可能想象到了,在你調用 listen() 前你或者要調用 bind() 或者讓 內核隨便選擇一個端口。如果你想偵聽進入的連接,那麼系統調用的順序可能是這樣的:
  socket();
  bind();
  listen();
  /* accept() goes here */
因爲他相當的明瞭,我將在這裏不給出例子了。(在 accept() 那一章的代碼將更加 完全。)真正麻煩的部分在 accept()。
--------------------------------------------------------------------------------
accept()--"Thank you for calling port 3490."
準備好了,系統調用 accept() 會有點古怪的地方的!你可以想象發生這樣的事情: 有人從很遠的地方通過一個你在偵聽 (listen()) 的端口連接 (connect()) 到你的機器。他的連接將加入到等待接受 (accept()) 的隊列中。你調用 accept() 告訴他你有空閒的連接。他將返回一個新的套接口文件描述符! 原來的一個還在偵聽你的那個端口,新的最後在準備發送 (send()) 和接收 ( recv()) 數據。這就是這個過程!
函數是這樣定義的:
  #include <sys/socket.h>
  int accept(int sockfd, void *addr, int *addrlen);
sockfd 相當簡單,是和 listen() 中一樣的套接口描述符。addr 是個指向局部的數據結構 struct sockaddr_in 的指針。This is where the information about the incoming connection will go (and you can determine which host is calling you from which port). 在他的地址傳遞給 accept 之前,addrlen 是個局部的整形變量,設置爲 sizeof(struct sockaddr_in)。accept 將 不會將多餘的字節給 addr。如果你放入的少些,那麼在 addrlen 的值中反映 出來。
同樣,在錯誤時返回-1並設置全局變量 errno。
現在是你應該熟悉的代碼片段。
  #include <string.h>
  #include <sys/types.h>
  #include <sys/socket.h>
  #define MYPORT 3490  /* the port users will be connecting to */
  #define BACKLOG 10   /* how many pending connections queue will hold */
  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;
    sockfd = socket(AF_INET, SOCK_STREAM, 0); /* do some error checking! */
    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), 8);    /* 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() 中你應該使用新的文件描述符。 如果你只想讓一個連接進來,那麼你可以使用 close() 去關閉原來的文件描述 符 sockfd 來避免同一個端口更多的連接。
--------------------------------------------------------------------------------
send() and recv()--Talk to me, baby!
這兩個函數用於流式套接口和數據報套接口的通訊。如果你喜歡使用無連接的數據報 套接口,你應該看一看下面關於 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()--Talk to me, DGRAM-style
"這很不錯啊",我聽到你說,"但是你還沒有講無連接數據報套接口呢。"沒問題,現在我們開始 這個內容。
既然數據報套接口不是連接到遠程主機的,那麼在我們發送一個包之前需要什麼信息呢? 不錯,是目標地址!看下面的:
  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()--Get outta my face!
你已經整天都在發送 (send()) 和接收 (recv()) 數據了,現在你準備 關閉你的套接口描述符了。這很簡單,你可以使用一般的 Unix 文件描述符的 close() 函 數:
  close(sockfd);
他將防止套接口上更多的數據的讀寫。任何在另一端讀寫套接口的企圖都將返回錯誤信息。
如果你想在如何關閉套接口上有多一點的控制,你可以使用函數 shutdown()。他能夠讓 你將一定方向的通訊或者雙向的通訊(就象 close() 一樣)關閉,你可以使用:
  int shutdown(int sockfd, int how);
sockfd 是你想要關閉的套接口文件描述復。how 的值是下面的其中之一:
0 - Further receives are disallowed
1 - Further sends are disallowed
2 - Further sends and receives are disallowed (和 close() 一樣
shutdown() 成功時返回 0,失敗時返回 -1(同時設置 errno。)
如果在無連接的數據報套接口中使用 shutdown(),那麼只不過是讓 send() 和 recv() 不能使用(記得你在數據報套接口中使用了 connect 後是可以 使用他們的嗎?)
--------------------------------------------------------------------------------
getpeername()--Who are you?
這個函數太簡單了。
他太簡單了,以至我都不想單列一章。但是我還是這樣做了。
函數 getpeername() 告訴你在連接的流式套接口上誰在另外一邊。函數是這樣的:
  #include <sys/socket.h>
  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()--Who am I?
甚至比 getpeername() 還簡單的函數是 gethostname()。他返回你程序 所運行的機器的主機名字。然後你可以使用 gethostbyname() 以獲得你的機器的 IP 地址。
下面是定義:
  #include <unistd.h>
  int gethostname(char *hostname, size_t size);
參數很簡單:hostname 是一個字符數組指針,他將在函數返回時保存 主機名。size 是 hostname 數組的字節長度。
函數調用成功時返回 0,失敗時返回 -1,並設置 errno。
--------------------------------------------------------------------------------
DNS--You say "whitehouse.gov", I say "198.137.240.100"
如果你不知道 DNS 的意思,那麼我告訴你,他代表"域名服務 (Domain Name Service)"。他主要的功能是:你給他一個容易記憶的某站點的地址,他給你 IP 地址(然後你就可以 使用 bind(), connect(), sendto() 或者其他函數。)當一個人 輸入:
  $ telnet whitehouse.gov
telnet 能知道他將連接 (connect()) 到 "198.137.240.100"。
但是這是如何工作的呢? 你可以調用函數 gethostbyname():
  #include <netdb.h>
  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 - Official name of the host.
h_aliases - A NULL-terminated array of alternate names for the host.
h_addrtype - The type of address being returned; usually AF_INET.
h_length - The length of the address in bytes.
h_addr_list - A zero-terminated array of network addresses for the host. Host addresses are in Network Byte Order.
h_addr - The first address in h_addr_list.
gethostbyname() 成功時返回一個指向 struct hostent 的 指針,或者是個空 (NULL) 指針。(但是和以前不同,errno 不設置,h_errno 設置錯誤信息。請看下面的 herror()。)
但是如何使用呢? 這個函數可不象他看上去那麼難用。
這裏是個例子:
  #include <stdio.h>
  #include <stdlib.h>
  #include <errno.h>
  #include <netdb.h>
  #include <sys/types.h>
  #include <netinet/in.h>
  int main(int argc, char *argv[])
  {
    struct hostent *h;
    if (argc != 2) { /* error check the command line */
      fprintf(stderr,"usage: getip address/n");
      exit(1);
    }
    if ((h=gethostbyname(argv[1])) == NULL) { /* get the host info */
      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 *,然後得到數據。
--------------------------------------------------------------------------------
Client-Server Background
這裏是個客戶--服務器的世界。在網絡上的所有東西都是在處理客戶進程和服務器進程的交談。 舉個 telnet 的例子。當你用 telnet (客戶)通過 23 號端口登陸到主機,主機上運行 的一個程序(一般叫 telnetd,服務器)激活。他處理這個連接,顯示登陸界面,等等。
Figure 2. The Client-Server Relationship.
圖 2 說明了客戶和服務器之間的信息交換。
注意,客--服務器之間可以使用SOCK_STREAM、SOCK_DGRAM 或者其他(只要他們採用相同的)。一些很好的客戶--服務器的例子有 telnet/telnetd、 ftp/ftpd 和 bootp/bootpd。每次你使用 ftp 的 時候,在遠端都有一個 ftpd 爲你服務。
一般,在服務端只有一個服務器,他採用 fork() 來處理多個客戶的連接。基本的 程序是:服務器等待一個連接,接受 (accept()) 連接,然後 fork() 一個 子進程處理他。這是下一章我們的例子中會講到的。
--------------------------------------------------------------------------------
簡單的服務器
這個服務器所做的全部工作是在留式連接上發送字符串 "Hello, World!/n"。你要 測試這個程序的話,可以在一臺機器上運行該程序,然後在另外一機器上登陸:
  $ telnet remotehostname 3490
remotehostname 是該程序運行的機器的名字。
服務器代碼:
  #include <stdio.h>
  #include <stdlib.h>
  #include <errno.h>
  #include <string.h>
  #include <sys/types.h>
  #include <netinet/in.h>
  #include <sys/socket.h>
  #include <sys/wait.h>
  #define MYPORT 3490  /* the port users will be connecting to */
  #define BACKLOG 10   /* how many pending connections queue will hold */
  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), 8);    /* 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 <stdio.h>
  #include <stdlib.h>
  #include <errno.h>
  #include <string.h>
  #include <netdb.h>
  #include <sys/types.h>
  #include <netinet/in.h>
  #include <sys/socket.h>
  #define PORT 3490  /* the port client will be connecting to */
  #define MAXDATASIZE 100 /* max number of bytes we can get at once */
  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), 8);   /* 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 <stdio.h>
  #include <stdlib.h>
  #include <errno.h>
  #include <string.h>
  #include <sys/types.h>
  #include <netinet/in.h>
  #include <sys/socket.h>
  #include <sys/wait.h>
  #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), 8);    /* 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 <stdio.h>
  #include <stdlib.h>
  #include <errno.h>
  #include <string.h>
  #include <sys/types.h>
  #include <netinet/in.h>
 #include <netdb.h>
  #include <sys/socket.h>
  #include <sys/wait.h>
  #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), 8);   /* 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。觀察他們的通訊!
Except for one more tiny detail that I've mentioned many times in the past: connected datagram sockets. I need to talk about this here, since we're in the datagram section of the document. Let's say that talker calls connect() and specifies the listener's address. From that point on, talker may only sent to and receive from the address specified by connect(). For this reason, you don't have to use sendto() and recvfrom(); you can simply use send() and recv().
--------------------------------------------------------------------------------
阻塞
阻塞,你也許早就聽說了。"阻塞"是 "sleep" 的科技行話。你可能注意到前面運行的 listener 程序,他在那裏不停地運行,等待數據包的到來。實際在運行的是 他調用 recvfrom(),然後沒有數據,因此 recvfrom() 說"阻塞 (block)" 直到數據的到來。
很多函數都利用阻塞。accept() 阻塞,所有的 recv*() 函數阻塞。他們之所以能這樣做是因爲他們被允許這樣做。當你第一次調用 socket() 建立套接口描述符的時候,內核就將他設置爲阻塞。如果你不想套接口阻塞,你就要調用函數 fcntl():
  #include <unistd.h>
  #include <fcntl.h>
 .
 .
  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 <sys/time.h>
   #include <sys/types.h>
   #include <unistd.h>
   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) - clears a file descriptor set
FD_SET(int fd, fd_set *set) - adds fd to the set
FD_CLR(int fd, fd_set *set) - removes fd from the set
FD_ISSET(int fd, fd_set *set) - tests to see if fd is in the set
最後,是有點古怪的數據結構 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 <sys/time.h>
   #include <sys/types.h>
   #include <unistd.h>
   #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() 要講的所有的東西。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章