Linux Socket 網絡編程 (IBM網站)第一章

第一章

Socket 是做什麼的?
雖然 socket 接口理論上還允許訪問除 IP 以外的協議系列,然而在實際上,socket應用程序中使用的每個網絡層都將使用 IP。對於本教程來說,我們僅介紹 IPv4;將來 IPv6 也會變得很重要,但是它們在原理是相同的。在傳輸層,socket 支持兩個特殊協議:TCP (transmission control protocol,傳輸控制協議) 和 UDP (user datagram protocol,用戶數據報協議)。

Socket不能用來訪問較低(或較高)的網絡層。例如,socket 應用程序不知道它是運行在以太網、令牌環網還是撥號連接上。Socket 的僞層(pseudo-layer)也不知道高層協議(比如 NFS、HTTP、FTP等)的任何情況(除非您自己編寫一個 socket 應用程序來實現那些高層協議)。

在很多情況下,socket接口並不是用於網絡編程 API 的最佳選擇。特別地,由於存在很多很優秀的庫可以直接使用高層協議,您不必關心 socket 的細節;那些庫會爲您處理 socket 的細節。例如,雖然編寫您自己的 SSH 客戶機並沒有什麼錯,但是對於僅只是爲了讓應用程序安全地傳輸數據來說,就沒有必要做得這樣複雜。低級層比 socket 所訪問的層更適合歸入設備驅動程序編程領域。

IP、TCP 和 UDP
正如上一小節所指出的,當您編寫 socket 應用程序的時候,您可以在使用 TCP 還是使用 UDP 之間做出選擇。它們都有各自的優點和缺點。

TCP 是流協議,而UDP是數據報協議。換句話說,TCP 在客戶機和服務器之間建立持續的開放連接,在該連接的生命期內,字節可以通過該連接寫出(並且保證順序正確)。然而,通過 TCP 寫出的字節沒有內置的結構,所以需要高層協議在被傳輸的字節流內部分隔數據記錄和字段。

另一方面,UDP 不需要在客戶機和服務器之間建立連接,它只是在地址之間傳輸報文。UDP 的一個很好特性在於它的包是自分隔的(self-delimiting),也就是一個數據報都準確地指出它的開始和結束位置。然而,UDP 的一個可能的缺點在於,它不保證包將會按順序到達,甚至根本就不保證。當然,建立在 UDP 之上的高層協議可能會提供握手和確認功能。

對於理解 TCP 和 UDP 之間的區別來說,一個有用的類比就是電話呼叫和郵寄信件之間的區別。在呼叫者用鈴聲通知接收者,並且接收者拿起聽筒之前,電話呼叫不是活動的。只要沒有一 方掛斷,該電話信道就保持活動,但是在通話期間,他們可以自由地想說多少就說多少。來自任何一方的談話都按臨時的順序發生。另一方面,當你發一封信的時 候,郵局在投遞時既不對接收方是否存在作任何保證,也不對信件投遞將花多長時間做出有力保證。接收方可能按與信件的發送順序不同的順序接收不同的信件,並 且發送方也可能在他們發送信件是交替地接收郵件。與(理想的)郵政服務不同,無法送達的信件總是被送到死信辦公室處理,而不再返回給發送者。

對等方、端口、名稱和地址
除了 TCP 和 UDP 協議以外,通信一方(客戶機或者服務器)還需要知道的關於與之通信的對方機器的兩件事情:IP 地址或者端口。IP 地址是一個 32 位的數據值,爲了人們好記,一般用圓點分開的 4 組數字的形式來表示,比如:64.41.64.172。端口是一個 16 位的數據值,通常被簡單地表示爲一個小於 65536 的數字。大多數情況下,該值介於 10 到 100 的範圍內。一個 IP 地址獲取送到某臺機器的一個數據包,而一個端口讓機器決定將該數據包交給哪個進程/服務(如果有的話)。這種解釋略顯簡單,但基本思路是正確的。

上面的描述幾乎都是正確的,但它也遺漏了一些東西。大多數時候,當人們考慮 Internet 主機(對等方)時,我們都不會記憶諸如 64.41.64.172這樣的數字,而是記憶諸如 gnosis.cx 這樣的名稱。爲了找到與某個特定主機名稱相關聯的 IP 地址,一般都使用域名服務器(DNS),但是有時會首先使用本地查找(經常是通過 /etc/hosts 的內容)。對於本教程,我們將一般地假設有一個 IP 地址可用,不過下一小節將討論編寫名稱/地址查找代碼。

主機名稱解析
在 C 中,標準庫調用 gethostbyname() 用於名稱查找。下面是 nslookup 的一個簡單的命令行工具實現;要改編它以用於大型應用程序是一件簡單的事情。當然,使用 C 要比使用 Python 稍微複雜一點。


          /* Bare nslookup utility (w/ minimal error checking) */
          #include <stdio.h>          /* stderr, stdout */
          #include <netdb.h>          /* hostent struct, gethostbyname() */
          #include <arpa/inet.h>      /* inet_ntoa() to format IP address */
          #include <netinet/in.h>     /* in_addr structure */

          int main(int argc, char **argv) {
            struct hostent *host;     /* host information */
            struct in_addr h_addr;    /* Internet address */
            if (argc != 2) {
              fprintf(stderr, "USAGE: nslookup <inet_address>/n");
              exit(1);
            }
            if ((host = gethostbyname(argv[1])) == NULL) {
              fprintf(stderr, "(mini) nslookup failed on '%s'/n", argv[1]);
              exit(1);
            }
            h_addr.s_addr = *((unsigned long *) host->h_addr_list[0]);
            fprintf(stdout, "%s/n", inet_ntoa(h_addr));
            exit(0);
          }
         

注意,gethostbyname() 的返回值是一個 hostent 結構,它描述該名稱的主機。該結構的成員 host->h_addr_list 包含一個地址表,其中的每一項都是一個按照“網絡字節順序”排列的 32 位值;換句話說,字節順序可能是也可能不是機器的本機順序。爲了將這個 32 位值轉換成圓點隔開的四組數字的形式,請使用 inet_ntoa() 函數。


編寫 socket 客戶機的步驟
編寫客戶機應用程序所涉及的步驟在 TCP 和 UDP 之間稍微有些區別。對於二者來說,您首先都要創建一個 socket;單對 TCP 來說,下一步是建立一個到服務器的連接;向該服務器發送一些數據;然後再將這些數據接收回來;或許發送和接收會在短時間內交替;最後,在 TCP 的情況下,您要關閉連接。

TCP 回顯客戶機(客戶機設置)
首先,我們來看一個 TCP 客戶機。在本教程系列的第二部分,我們將做一些調整,用 UDP 來(粗略地)做同樣的事情。我們首先來看前面幾行:一些 include 語句,以及創建 socket 的語句。


          #include <stdio.h>
          #include <sys/socket.h>
          #include <arpa/inet.h>
          #include <stdlib.h>
          #include <string.h>
          #include <unistd.h>
          #include <netinet/in.h>

          #define BUFFSIZE 32
          void Die(char *mess) { perror(mess); exit(1); }
         

這裏沒有太多的設置,只是分配了特定的緩衝區大小,它限定了每個過程中回顯的數據量(但如果必要的話,我們可以循環通過多個過程)。我們還定義了一個小的錯誤函數。

TCP 回顯客戶機(創建 socket)
socket()調用的參數決定了 socket 的類型:PF_INET 只是意味着它使用 IP(您將總是使用它); SOCK_STREAM 和 IPPROTO_TCP 配合用於創建 TCP socket。


            int main(int argc, char *argv[]) {
            int sock;
            struct sockaddr_in echoserver;
            char buffer[BUFFSIZE];
            unsigned int echolen;
            int received = 0;

            if (argc != 4) {
              fprintf(stderr, "USAGE: TCPecho <server_ip> <word> <port>/n");
              exit(1);
            }
            /* Create the TCP socket */
            if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
              Die("Failed to create socket");
            }
         

說返回的值是一個 socket 句柄,它類似於文件句柄。特別地,如果 socket 創建失敗,它將返回 -1 而不是正數形式的句柄。

TCP 回顯客戶機(建立連接)
現在我們已經創建了一個 socket 句柄,還需要建立與服務器的連接。連接需要有一個描述服務器的 sockaddr 結構。特別地,我們需要使用echoserver.sin_addr.s_addr 和 echoserver.sin_port 來指定要連接的服務器和端口。我們正在使用 IP 地址這一事實是通過echoserver.sin_family 來指定的,但它總是被設置爲 AF_INET。


            /* Construct the server sockaddr_in structure */
            memset(&echoserver, 0, sizeof(echoserver));       /* Clear struct */
            echoserver.sin_family = AF_INET;                  /* Internet/IP */
            echoserver.sin_addr.s_addr = inet_addr(argv[1]);  /* IP address */
            echoserver.sin_port = htons(atoi(argv[3]));       /* server port */
            /* Establish connection */
            if (connect(sock,
                        (struct sockaddr *) &echoserver,
                        sizeof(echoserver)) < 0) {
              Die("Failed to connect with server");
            }
         

與創建 socket 類似,在嘗試建立連接時,如果失敗,則返回-1,否則 socket 現在就準備好發送或接收數據了。

TCP回顯客戶機(發送/接收數據)
現在連接已經建立起來,我們準備好可以發送和接收數據了。send() 調用接受套接字句柄本身、要發送的字符串、所發送的字符串的長度(用於驗證)和一個標記作爲參數。一般情況下,表記的默認值爲 0。send() 調用的返回值是成功發送的字節的數目。


            /* Send the word to the server */
            echolen = strlen(argv[2]);
            if (send(sock, argv[2], echolen, 0) != echolen) {
              Die("Mismatch in number of sent bytes");
            }
            /* Receive the word back from the server */
            fprintf(stdout, "Received: ");
            while (received < echolen) {
              int bytes = 0;
              if ((bytes = recv(sock, buffer, BUFFSIZE-1, 0)) < 1) {
                Die("Failed to receive bytes from server");
              }
              received += bytes;
              buffer[bytes] = '/0';        /* Assure null terminated string */
              fprintf(stdout, buffer);
            }
         

rcv() 調用不保證會獲得某個特定調用中傳輸的每個字節。在接收到某些字節之前,它只是處於阻塞狀態。所以我們讓循環一直進行,直到收回所發送的全部字節。很明顯,不同的協議可能決定以不同的方式(或許是字節流中的分隔符)決定何時終止接收字節。

TCP 回顯客戶機(包裝)
對 send() 和 recv() 的調用在默認的情況下都是阻塞的,但是通過改變套接字的選項以允許非阻塞的套接字是可能的。然而,本教程不會介紹創建非阻塞套接字的細節,也不介紹在生產 服務器中使用的諸如分支、線程或者一般異步處理(建立在非阻塞套接字基礎上)之類的細節。這些問題將在本教程的第二部分介紹。

在這個過程的末尾,我們希望在套接字上調用 close() ,這很像我們對文件句柄所做的那樣:


         fprintf(stdout, "/n");
         close(sock);
         exit(0);
       }

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