C 程式設計/UNIX 中的網路
在 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 來獲取更多詳細資訊。