跳轉到內容

C 程式設計/UNIX 中的網路

來自華夏公益教科書,開放的書籍,開放的世界
上一個:複雜型別 C 程式設計 下一個:常見做法

在 UNIX 下的網路程式設計在 C 中相對簡單。

本指南假設您已經對 C、UNIX 和網路有了一般性的瞭解。


一個簡單的客戶端

[編輯 | 編輯原始碼]

首先,我們將看一下您可以做的最簡單的事情之一:初始化一個流連線並從遠端伺服器接收一條訊息。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
 
#define MAXRCVLEN 500
#define PORTNUM 2300
 
int main(int argc, char *argv[])
{
   char buffer[MAXRCVLEN + 1]; /* +1 so we can add null terminator */
   int len, mysocket;
   struct sockaddr_in dest; 
 
   mysocket = socket(AF_INET, SOCK_STREAM, 0);
  
   memset(&dest, 0, sizeof(dest));                /* zero the struct */
   dest.sin_family = AF_INET;
   dest.sin_addr.s_addr = htonl(INADDR_LOOPBACK); /* set destination IP number - localhost, 127.0.0.1*/ 
   dest.sin_port = htons(PORTNUM);                /* set destination port number */
 
   connect(mysocket, (struct sockaddr *)&dest, sizeof(struct sockaddr_in));
  
   len = recv(mysocket, buffer, MAXRCVLEN, 0);
 
   /* We have to null terminate the received data ourselves */
   buffer[len] = '\0';
 
   printf("Received %s (%d bytes).\n", buffer, len);
 
   close(mysocket);
   return EXIT_SUCCESS;
}

這是最基本的客戶端;在實踐中,我們會檢查我們呼叫的每個函式是否失敗,但是為了清楚起見,錯誤檢查被省略了。

如您所見,程式碼主要圍繞 dest 展開,它是一個型別為 sockaddr_in 的結構體。此結構體儲存有關我們要連線的機器的資訊。

mysocket = socket(AF_INET, SOCK_STREAM, 0);

socket() 函式告訴我們的作業系統,我們想要一個用於套接字的檔案描述符,我們可以用它來進行網路流連線;引數的含義現在大多無關緊要。

memset(&dest, 0, sizeof(dest));                /* zero the struct */
dest.sin_family = AF_INET;
dest.sin_addr.s_addr = inet_addr("127.0.0.1"); /* set destination IP number */ 
dest.sin_port = htons(PORTNUM);                /* set destination port number */

現在我們開始進入有趣的部分

第一行使用 memset() 將結構體清零。

第二行設定地址族。這應該是與作為 socket() 的第一個引數傳遞的值相同的值;對於大多數目的,AF_INET 將起作用。

第三行是我們設定要連線的機器的 IP 的地方。變數 dest.sin_addr.s_addr 只是一個以大端格式儲存的整數,但我們不必知道這一點,因為 inet_addr() 函式會為我們執行從字串到大端整數的轉換。

第四行設定目標埠號。htons() 函式將埠號轉換為大端短整數。如果您的程式將僅在使用大端數字作為預設值的機器上執行,那麼 dest.sin_port = 21 也可以使用。但是,為了可移植性,應該始終使用 htons()

現在所有準備工作都完成了,我們可以實際建立連線並使用它了

connect(mysocket, (struct sockaddr *)&dest, sizeof(struct sockaddr_in));

這告訴我們的作業系統使用套接字 mysocket 建立與 dest 中指定的機器的連線。

len = recv(mysocket, buffer, MAXRCVLEN, 0);

現在這從連線中接收最多 MAXRCVLEN 位元組的資料,並將它們儲存在緩衝區字串中。接收到的字元數量由 recv() 返回。需要注意的是,接收到的資料不會在儲存在緩衝區中時自動以空值結尾,因此我們需要使用 buffer[len] = '\0' 手動進行。

就是這樣了!

在學習如何接收資料之後的下一步是學習如何傳送資料。如果您理解了上一節的內容,那麼這非常容易。您只需使用 send() 函式,該函式使用與 recv() 相同的引數。如果在我們之前的示例中,buffer 包含我們要傳送的文字,其長度儲存在 len 中,我們就會寫 send(mysocket, buffer, len, 0)send() 返回傳送的位元組數。請記住,send() 可能由於各種原因無法傳送所有位元組,因此檢查其返回值是否等於您嘗試傳送的位元組數非常重要。在大多數情況下,可以透過重新發送未傳送的資料來解決此問題。

一個簡單的伺服器

[編輯 | 編輯原始碼]
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
 
#define PORTNUM 2300
 
int main(int argc, char *argv[])
{
    char* msg = "Hello World !\n";
  
    struct sockaddr_in dest; /* socket info about the machine connecting to us */
    struct sockaddr_in serv; /* socket info about our server */
    int mysocket;            /* socket used to listen for incoming connections */
    socklen_t socksize = sizeof(struct sockaddr_in);

    memset(&serv, 0, sizeof(serv));           /* zero the struct before filling the fields */
    serv.sin_family = AF_INET;                /* set the type of connection to TCP/IP */
    serv.sin_addr.s_addr = htonl(INADDR_ANY); /* set our address to any interface */
    serv.sin_port = htons(PORTNUM);           /* set the server port number */    

    mysocket = socket(AF_INET, SOCK_STREAM, 0);
  
    /* bind serv information to mysocket */
    bind(mysocket, (struct sockaddr *)&serv, sizeof(struct sockaddr));

    /* start listening, allowing a queue of up to 1 pending connection */
    listen(mysocket, 1);
    int consocket = accept(mysocket, (struct sockaddr *)&dest, &socksize);
  
    while(consocket)
    {
        printf("Incoming connection from %s - sending welcome\n", inet_ntoa(dest.sin_addr));
        send(consocket, msg, strlen(msg), 0); 
        close(consocket);
        consocket = accept(mysocket, (struct sockaddr *)&dest, &socksize);
    }

    close(mysocket);
    return EXIT_SUCCESS;
}

表面上看,這與客戶端非常相似。第一個重要的區別是,我們不是使用有關我們要連線的機器的資訊來建立 sockaddr_in,而是使用有關伺服器的資訊來建立它,然後我們將它 bind() 到套接字。這使機器知道在 sockaddr_in 中指定的埠上接收到的資料應該由我們指定的套接字處理。

然後,listen() 函式告訴我們的程式使用給定的套接字開始監聽。listen() 的第二個引數允許我們指定可以排隊的連線的最大數量。每次與伺服器建立連線時,它都會被新增到佇列中。我們使用 accept() 函式從佇列中獲取連線。如果沒有連線在佇列中等待,則程式會一直等待直到收到連線。accept() 函式返回另一個套接字。此套接字本質上是一個“會話”套接字,並且可以專門用於與我們從佇列中取出的連線進行通訊。原始套接字 (mysocket) 繼續在指定埠上監聽以獲取更多連線。

獲得“會話”套接字後,我們可以像客戶端一樣處理它,使用 send()recv() 來處理資料傳輸。

請注意,此伺服器一次只能接受一個連線;如果您想同時處理多個客戶端,則需要 fork() 掉獨立的程序,或者使用執行緒來處理連線。

有用的網路函式

[編輯 | 編輯原始碼]
int gethostname(char *hostname, size_t size);

引數是字元陣列的指標和該陣列的大小。如果可能,它會找到主機名並將其儲存在陣列中。失敗時返回 -1。

struct hostent *gethostbyname(const char *name);

此函式獲取有關域名資訊並將其儲存在 hostent 結構體中。hostent 結構體中最有用的部分是 (char**) h_addr_list 欄位,它是一個以空值結尾的與該域名關聯的 IP 地址陣列。欄位 h_addr 是指向 h_addr_list 陣列中第一個 IP 地址的指標。失敗時返回 NULL

常見問題解答

[編輯 | 編輯原始碼]

無狀態連線怎麼樣?

[編輯 | 編輯原始碼]

如果您不想在您的程式中利用 TCP 的特性,而想使用 UDP 連線,那麼您只需在 socket() 的呼叫中將 SOCK_STREAM 替換為 SOCK_DGRAM,並以相同的方式使用結果。請記住,UDP 不保證資料包的傳遞和傳遞順序,因此檢查非常重要。

如果您想利用 UDP 的特性,那麼您可以使用 sendto()recvfrom(),它們的功能與 send()recv() 類似,只是您需要提供額外的引數來指定您與誰進行通訊。

如何檢查錯誤?

[編輯 | 編輯原始碼]

函式 socket()recv()connect() 都會在失敗時返回 -1,並使用 errno 來獲取更多詳細資訊。

上一個:複雜型別 C 程式設計 下一個:常見做法
華夏公益教科書