介紹 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 套介面),本地節點的路徑名 (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 是指 “網際網路通訊協定 (IP)”(請參考 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() 自己選擇合適的連接埠。同樣, 將 my_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 connection