Linux是一個可靠性非常高的作業系統,但是所有用過Linux的朋友都會感覺到,Linux和Windows這樣的"傻瓜"作業系統(這裡絲毫沒有貶低Windows的意思,相反這應該是Windows的優點)相比,後者無疑在易操作性上更勝一籌。但是為什麼又有那麼多的愛好者鐘情于Linux呢,當然自由是最吸引人的一點,另外Linux強大的功能也是一個非常重要的原因,尤其是Linux強大的網絡功能更是引人注目。放眼今天的WAP業務、銀行網絡業務和曾經紅透半邊天的電子商務,都越來越倚重基于Linux的解決方案。是以Linux網絡程式設計是非常重要的,而且當我們一接觸到Linux網絡程式設計,我們就會發現這是一件非常有意思的事情,因為以前一些關于網絡通信概念似是而非的地方,在這一段段代碼面前馬上就豁然開朗了。在剛開始學習程式設計的時候總是讓人感覺有點理不清頭緒,不過隻要多讀幾段代碼,很快我們就能體會到其中的樂趣了。下面我就從一段Proxy源代碼開始,談談如何進行Linux網絡程式設計。
首先聲明,這段源代碼不是我編寫的,讓我們感謝這位名叫Carl Harris的大蝦,是他編寫了這段代碼并将其散播到網上供大家學習讨論。這段代碼雖然隻是描述了最簡單的proxy操作,但它的确是經典,它不僅清晰地描述了客戶機/伺服器系統的概念,而且幾乎包括了Linux網絡程式設計的方方面面,非常适合Linux網絡程式設計的初學者學習。
這段Proxy程式的用法是這樣的,我們可以使用這個proxy登入其它主機的服務端口。假如編譯後生成了名為Proxy的可執行檔案,那麼指令及其參數的描述為:
./Proxy <proxy_port> <remote_host> <service_port>
其中參數proxy_port是指由我們指定的代理伺服器端口。參數remote_host是指我們希望連接配接的遠端主機的主機名,IP位址也同樣有效。這個主機名在網絡上應該是唯一的,如果您不确定的話,可以在遠端主機上使用uname -n指令檢視一下。參數service_port是遠端主機可提供的服務名,也可直接鍵入服務對應的端口号。這個指令的相應操作是将代理伺服器的proxy_port端口綁定到remote_host的service_port端口。然後我們就可以通過代理伺服器的proxy_port端口通路remote_host了。例如一台計算機,網絡主機名是legends,IP位址為10.10.8.221,如果在我的計算機上執行:
[[email protected] /root]#./proxy 8000 legends telnet
那麼我們就可以通過下面這條指令通路legends的telnet端口。
-----------------------------------------------------------------
[[email protected] /root]#telnet legends 8000
Trying 10.10.8.221...
Connected to legends(10.10.8.221).
Escape character is '^]'
Red Hat Linux release 6.2(Zoot)
Kernel 2.2.14-5.0 on an i686
Login:
-----------------------------------------------------------------
上面的綁定操作也可以使用下面的指令:
[[email protected] /root]#./proxy 8000 10.10.8.221 23
23是telnet服務的标準端口号,其它服務的對應端口号我們可以在/etc/services中檢視。
下面我就從這段代碼出發談談我對Linux網絡程式設計的一些粗淺的認識,不對的地方還請各位大蝦多多批評指正。
◆main()函數
-----------------------------------------------------------------
#include <stdio.h>
#include <ctype.h>
#include <errno.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/file.h>
#include <sys/ioctl.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <netdb.h>
#define TCP_PROTO "tcp"
int proxy_port;
struct sockaddr_in hostaddr;
extern int errno;
extern char *sys_myerrlist[];
void parse_args (int argc, char **argv);
void daemonize (int servfd);
void do_proxy (int usersockfd);
void reap_status (void);
void errorout (char *msg);
typedef void Signal(int);
main (argc,argv)
int argc;
char **argv;
{
int clilen;
int childpid;
int sockfd, newsockfd;
struct sockaddr_in servaddr, cliaddr;
parse_args(argc,argv);
bzero((char *) &servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = proxy_port;
if ((sockfd = socket(AF_INET,SOCK_STREAM,0)) < 0) {
fputs("failed to create server socket\r\n",stderr);
exit(1);
}
if (bind(sockfd,(struct sockaddr_in *) &servaddr,sizeof(servaddr)) < 0) {
fputs("faild to bind server socket to specified port\r\n",stderr);
exit(1);
}
listen(sockfd,5);
daemonize(sockfd);
while (1) {
clilen = sizeof(cliaddr);
newsockfd = accept(sockfd, (struct sockaddr_in *) &cliaddr, &clilen);
if (newsockfd < 0 && errno == EINTR)
continue;
else if (newsockfd < 0)
errorout("failed to accept connection");
if ((childpid = fork()) == 0) {
close(sockfd);
do_proxy(newsockfd);
exit(0);
}
lose(newsockfd);
}
}
-----------------------------------------------------------------
上面就是Proxy源代碼的主程式部分,也許您在網上也曾經看到過這段代碼,不過細心的您會發現在上面這段代碼中我修改了兩個地方,都是在預編譯部分。一個地方是在定義外部字元型指針數組時,我将原代碼中的
extern char *sys_errlist[];
修改為
extern char *sys_myerrlist[];原因是在我的Linux環境下頭檔案"stdio.h"已經對sys_errlist[]進行了如下定義:
extern __const char *__const sys_errlist[];
也許Carl Harris在94年編寫這段代碼時系統還沒有定義sys_errlist[],不過現在我們不修改一下的話,編譯時系統就會告訴我們sys_errlist發生了定義沖突。
另外我添加了一個函數類型定義:
typedef void Sigfunc(int);
具體原因我将在後面向大家解釋。
套接字和套接字位址結構定義
這段主程式是一段典型的伺服器程式。網絡通訊最重要的就是套接字的使用,在程式的一開始就對套接字描述符sockfd和newsockfd進行了定義。接下來定義客戶機/伺服器的套接字位址結構cliaddr和servaddr,存儲客戶機/伺服器的有關通信資訊。然後調用parse_args(argc,argv)函數處理指令參數。關于這個parse_args()函數我們待會兒再做介紹。
建立通信套接字
下面就是建立一個伺服器的詳細過程。伺服器程式的第一個操作是建立一個套接字。這是通過調用函數socket()來實作的。socket()函數的具體描述為:
-----------------------------------------------------------------
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
-----------------------------------------------------------------
參數domain指定套接字使用的協定族,AF_INET表示使用TCP/IP協定族,AF_UNIX表示使用Unix協定族,AF_ISO表示套接字使用ISO協定族。type指定套接字類型,一般的面向連接配接通信類型(如TCP)設定為SOCK_STREAM,當套接字為資料報類型時,type應設定為SOCK_DGRAM,如果是可以直接通路IP協定的原始套接字則type應設定為SOCK_RAW。參數protocol一般設定為"0",表示使用預設協定。當socket()函數成功執行時,傳回一個标志這個套接字的描述符,如果出錯則傳回"-1",并設定errno為相應的錯誤類型。
設定伺服器套接字位址結構
在通常情況下,首先要将描述伺服器資訊的套接字位址結構清零,然後在位址結構中填入相應的内容,準備接受客戶機送來的連接配接建立請求。這個清零操作可以用多種位元組處理函數來實作,例如bzero()、bcopy()、memset()、memcpy()等,以字母"b"開始的兩個函數是和BSD系統相容的,而後面兩個是ANSI C提供的函數。這段代碼中使用的bzero()其描述為:
void bzero(void *s, int n);
函數的具體操作是将參數s指定的記憶體的前n個位元組清零。memset()同樣也很常用,其描述為:
void *memset(void *s, int c, size_t n);
具體操作是将參數s指定的記憶體區域的前n個位元組設定為參數c的内容。
下一步就是在已經清零的伺服器套接字位址結構中填入相應的内容。Linux系統的套接字是一個通用的網絡程式設計接口,它應該支援多種網絡通信協定,每一種協定都使用專門為自己定義的套接字位址結構(例如TCP/IP網絡的套接字位址結構就是struct sockaddr_in)。不過為了保持套接字函數調用參數的一緻性,Linux系統還定義了一種通用的套接字位址結構:
-----------------------------------------------------------------
<linux/socket.h>
struct sockaddr
{
unsigned short sa_family;
char sa_data[14];
}
-----------------------------------------------------------------
其中sa_family意指套接字使用的協定族位址類型,對于我們的TCP/IP網絡,其值應該是AF_INET,sa_data中存儲具體的協定位址,不同的協定族有不同的位址格式。這個通用的套接字位址結構一般不用做定義具體的執行個體,但是常用做套接字位址結構的強制類型轉換,如我們經常可以看到這樣的用法:
bind(sockfd,(struct sockaddr *) &servaddr,sizeof(servaddr))
用于TCP/IP協定族的套接字位址結構是sockaddr_in,其定義為:
-----------------------------------------------------------------
<linux/in.h>
struct in_addr
{
__u32 s_addr;
};
struct sochaddr_in
{
short int sin_family;
unsigned short int sin_port;
struct in_addr sin_addr;
nsigned char_ _ pad[_ _ SOCK_SIZE__- sizeof(short int) -sizeof(unsigned short int) - sizeof(struct in_addr)];
};
#define sin_zero_ - pad
-----------------------------------------------------------------
其中sin_zero成員并未使用,它是為了和通用套接字位址struct sockaddr相容而特意引入的。在程式設計時,一般都通過bzero()或是memset()将其置零。其他成員的設定一般是這樣的:
servaddr.sin_family = AF_INET;
表示套接字使用TCP/IP協定族。
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
設定伺服器套接字的IP位址為特殊值INADDR_ANY,這表示伺服器願意接收來自任何網絡裝置接口的客戶機連接配接。htonl()函數的意思是将主機順序的位元組轉換成網絡順序的位元組。
servaddr.sin_port = htons(PORT);
設定通信端口号,PORT應該是我們已經定義好的。在本例中servaddr.sin_port = proxy_port;這是表示端口号是函數的傳回值proxy_port。
另外需要說明的一點是,在本例中,我們并沒有看到在預編譯部分中包含有<linux/socket.h>和<linux/in.h>這兩個頭檔案,那是因為這兩個頭檔案已經分别被包含在<sys/types.h>和<sys/types.h>中了,而且後面這兩個頭檔案是與平台無關的,是以在網絡通信中一般都使用這兩個頭檔案。
伺服器公開位址
如果伺服器要接受客戶機的連接配接請求,那麼它必須先要在整個網絡上公開自己的位址。在設定了伺服器的套接字位址結構之後,可以通過調用函數bind()綁定伺服器的位址和套接字來完成公開位址的操作。函數bind()的較長的描述為:
-----------------------------------------------------------------
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, struct sockaddr *addr, int addrlen);
-----------------------------------------------------------------
參數sockfd是我們通過調用socket()建立的套接字描述符。參數addr是本機位址,參數addrlen是套接字位址結構的長度。函數執行成功時傳回"0",否則傳回"-1",并設定errno變量為EADDRINUAER。
如果是伺服器調用bind()函數,如果設定了套接字的IP位址為某個本地IP位址,那麼這表示伺服器隻接受來自于這個IP位址的特定主機發出的連接配接請求。不過一般情況下都是将IP位址設定為INADDR_ANY,以便接受所有網絡裝置接口送來的連接配接請求。
客戶機一般是不會調用bind()函數的,因為客戶機在連接配接時不用指定自己的套接字位址端口号,系統會自動為客戶機選擇一個未用端口号,并且用本地IP位址自動填充客戶機套接字位址結構中的相應項。但是在某些特定的情況下客戶機需要使用特定的端口号,例如Linux中的rlogin指令就要求使用保留端口号,而系統是不能為客戶機自動配置設定保留端口号的,這就需要調用bind()來綁定一個保留端口号了。不過在一些特殊的環境下,這樣綁定特定端口号也會帶來一些負面影響,如在HTTP伺服器進入TIME_WAIT狀态後,客戶機如果要求再次與伺服器建立連接配接,則伺服器會拒絕這一連接配接請求。如果客戶機最後進入TIME_WAIT狀态,則馬上再次執行bind()函數時會傳回出錯資訊"-1",原因是系統會認為同時有兩次連接配接綁定同一個端口。
轉換Listening套接字
接下來,伺服器需要将我們剛才與IP位址和端口号完成綁定的套接字轉換成傾聽listening套接字。隻有伺服器程式才需要執行這一步操作。我們通過調用函數listen()實作這一操作。listen()的較長的描述為:
-----------------------------------------------------------------
#include <sys/socket.h>
int listen(int sockfd, int backlog);
-----------------------------------------------------------------
參數sockfd指定我們要求轉換的套接字描述符,參數backlog設定請求隊列的最大長度。函數listen()主要完成以下操作。
首先是将套接字轉換成傾聽套接字。因為函數socket()建立的套接字都是主動套接字,是以客戶機可以通過調用函數connect()來使用這樣的套接字主動和伺服器建立連接配接。而伺服器的情況恰恰相反,伺服器需要通過套接字接收客戶機的連接配接請求,這就需要一個"被動"套接字。listen()就可将一個尚未連接配接的主動套接字轉換成為這樣的"被動"套接字,也就是傾聽套接字。在執行了listen()函數之後,伺服器的TCP就由CLOSED變成LISTEN狀态了。
另外listen()可以設定連接配接請求隊列的最大長度。雖然參數backlog的用法非常簡單,隻是一個簡單的整數。但搞清楚請求隊列的含義對了解TCP協定的通信過程建立非常重要。TCP協定為每個傾聽套接字實際上維護兩個隊列,一個是未完成連接配接隊列,這個隊列中的成員都是未完成3次握手的連接配接;另一個是完成連接配接隊列,這個隊列中的成員都是雖然已經完成了3次握手,但是還未被伺服器調用accept()接收的連接配接。參數backlog實際上指定的是這個傾聽套接字完成連接配接隊列的最大長度。在本例中我們是這樣用的:listen(sockfd,5);表示完成連接配接隊列的最大長度為5。
接收連接配接
接下來我們在主程式中看到通過名為daemonize()的自定義函數建立一個守護程序,關于這個daemonize()以及守護程序的相關概念,我們等一會兒再做詳細介紹。然後伺服器程式進入一個無條件循環,用于監聽接收客戶機的連接配接請求。在此過程中如果有客戶機調用connect()請求連接配接,那麼函數accept()可以從傾聽套接字的完成連接配接隊列中接受一個連接配接請求。如果完成連接配接隊列為空,這個程序就睡眠。accept()的較長的描述為:
-----------------------------------------------------------------
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, int *addrlen);
-----------------------------------------------------------------
參數sockfd是我們轉換成功的傾聽套接字描述符;參數addr是一個指向套接字位址結構的指針,參數addrlen為一個整型指針。當函數成功執行時,傳回3個結果,函數傳回一個新的套接字描述符,伺服器可以通過這個新的套接字描述符和客戶機進行通信。參數addr所指向的套接字位址結構中将存放客戶機的相關資訊,addrlen指針将描述前述套接字位址結構的長度。在通常情況下伺服器對這些資訊不是很感興趣,是以我們經常可以看到一些源代碼中将accept()函數的後兩個參數都設定為NULL。不過在這段proxy源代碼中需要用到有關的客戶機資訊,是以我們看到通過執行
newsockfd = accept(sockfd, (struct sockaddr_in *) &cliaddr, &clilen);
将客戶機的詳細資訊存放在位址結構cliaddr中。而proxy就通過套接字newsockfd與客戶機進行通信。值得注意的是這個傳回的套接字描述符與我們轉換的傾聽套接字是不同的。在一段伺服器程式中,可以始終隻用一個傾聽套接字來接收多個客戶機的連接配接請求;而如果我們要和客戶機建立一個實際的連接配接的話,對每一個請求我們都需要調用accept()傳回一個新的套接字。當伺服器處理完畢客戶機的請求後,一定要将相應的套接字關閉;如果整個伺服器程式将要結束,那麼一定要将傾聽套接字關閉。
如果accept()函數執行失敗,則傳回"-1",如果accept()函數阻塞等待客戶機調用connect()建立連接配接,程序在此時恰好捕捉到信号,那麼函數在傳回"-1"的同時将變量errno的值設定為EINTR。這和accept()函數執行失敗是有差別的。是以我們在代碼中可以看到這樣的語句:
-----------------------------------------------------------------
if (newsockfd < 0 && errno == EINTR)
continue;
else if (newsockfd < 0)
errorout("failed to accept connection");
-----------------------------------------------------------------
可以看出程式在處理這兩種情況時操作是完全不同的,同樣是accept()傳回"-1",如果有errno == EINTR,那麼系統将再次調用accept()接受連接配接請求,否則伺服器程序将直接結束。
處理客戶機請求
當伺服器與客戶機建立連接配接以後,就可以處理客戶機的請求了。一般情況下伺服器程式都要建立一個子程序用于處理客戶機請求;而父程序則繼續監聽,時刻準備接受其它客戶機的連接配接請求。我們這段proxy程式也不例外。它通過調用fork()建立處理客戶機請求的子程序。我想在linux/Unix程式設計中,fork()的重要性不用我再多說什麼了,在大型的伺服器程式中,一般都要在子程序裡,根據客戶機請求的不同而通過exec()系列函數調用不同的處理程式,這也是在學習linux/Unix程式設計中一個非常重要的地方。不過我們這個proxy程式旨在講述一些linux網絡程式設計的基本概念,是以在子程式部分就直接調用了一個完成proxy功能的函數do_proxy(),其實際參數newsockfd就是accept()傳回的套接字描述符。另外值得注意的一點就是,因為子程序繼承了所有父程序中可用的檔案描述符,是以我們必須在子程序中關閉傾聽套接字(代碼中子程序部分的close(sockfd);),同時在父程序中關閉accept()傳回的套接字描述符(例如代碼中父程序部分的close(newsockfd);)。
◆函數parse_args()
此函數的定義是:void parse_args (int argc, char **argv);
-----------------------------------------------------------------
void parse_args (argc,argv)
int argc;
char **argv;
{
int i;
struct hostent *hostp;
struct servent *servp;
unsigned long inaddr;
struct {
char proxy_port [16];
char isolated_host [64];
char service_name [32];
} pargs;
if (argc < 4) {
printf("usage: %s <proxy-port> <host> <service-name|port-number>\r\n", argv[0]);
exit(1);
}
strcpy(pargs.proxy_port,argv[1]);
strcpy(pargs.isolated_host,argv[2]);
strcpy(pargs.service_name,argv[3]);
for (i = 0; i < strlen(pargs.proxy_port); i++)
if (!isdigit(*(pargs.proxy_port + i)))
break;
if (i == strlen(pargs.proxy_port))
proxy_port = htons(atoi(pargs.proxy_port));
else {
printf("%s: invalid proxy port\r\n",pargs.proxy_port);
exit(0);
}
bzero(&hostaddr,sizeof(hostaddr));
hostaddr.sin_family = AF_INET;
if ((inaddr = inet_addr(pargs.isolated_host)) != INADDR_NONE)
bcopy(&inaddr,&hostaddr.sin_addr,sizeof(inaddr));
else if ((hostp = gethostbyname(pargs.isolated_host)) != NULL)
bcopy(hostp->h_addr,&hostaddr.sin_addr,hostp->h_length);
else {
printf("%s: unknown host\r\n",pargs.isolated_host);
exit(1);
}
if ((servp = getservbyname(pargs.service_name,TCP_PROTO)) != NULL)
hostaddr.sin_port = servp->s_port;
else if (atoi(pargs.service_name) > 0)
hostaddr.sin_port = htons(atoi(pargs.service_name));
else {
printf("%s: invalid/unknown service name or port number\r\n", pargs.service_name);
exit(1);
}
}
-----------------------------------------------------------------
這個函數的作用是傳遞指令行參數。參數的傳遞是通過兩個全局變量來實作的,這兩個變量是int proxy_port和struct sockaddr_in hostaddr。分别用于傳遞等待連接配接請求的proxy端口和被綁定的主機網絡資訊。
檢驗指令行參數
在進行了局部變量定義以後,函數首先要檢測指令行參數是否符合程式的要求,即在指令後緊跟代理伺服器端口、遠端主機名和服務端口号,如果不滿足上述要求,則代理伺服器程式結束。如果滿足上述要求,則将指令行的這三個參數存儲進我們自定義的pargs結構之中。注意pargs結構的三個成員都是以字元形式存放指令行參數資訊的,後面我們需要調用函數将這些參數資訊都轉換成為數字形式的。
傳遞參數
接下來就要将指令行的三個參數變換成合适的形式指派給全局變量proxy_port和hostaddr,以供其它函數調用。首先傳送代理伺服器端口pargs.proxy_port,在這裡程式調用了一個系統函數isdigit()檢驗使用者輸入的端口号是否有效。isdigit()的具體描述為:
-----------------------------------------------------------------
#include <ctype.h>
int isdigit(int c)
-----------------------------------------------------------------
isdigit()函數用來檢測參數"c"是否是數字1~9中間的一個,如果答案是肯定的,則傳回非"0"值,反之,傳回"0"。程式中采用了這樣的方法來對使用者的輸入進行逐位檢驗:
if (!isdigit(*(pargs.proxy_port + i)))
break;
在将有效端口号傳遞給全局變量proxy_port之前,還要将其轉換成為網絡位元組順序。這是因為網絡中存在着多個公司的不同裝置,這些裝置表示資料的位元組順序是不同的。例如在記憶體位址0x1000處存儲一個16位的整數FF11,不同公司的機器在記憶體中的存儲方式也不相同,有的将FF置于記憶體指針的起始位置0x1000,11置于0x1001,這稱為big-endian順序;有的卻恰恰相反,即little-endian順序。這種基于主機的資料存儲順序就稱為主機位元組順序(host byte order)。為了在不同類型的主機之間進行通信,網絡協定就規定了一種統一的網絡位元組順序,這種順序被規定為little-endian順序。是以資料的網絡位元組順序和主機位元組順序有可能是不同的,是以在編寫通信程式時一定要注意不同順序之間的轉換。是以,程式中一定要有例程中這樣的語句:
proxy_port = htons(atoi(pargs.proxy_port));
函數htons()的作用就是将主機位元組順序轉換為網絡位元組順序。它的具體描述為:
-----------------------------------------------------------------
#include <netinet/in.h>
unsigned short int htons(unsigned short int data)
-----------------------------------------------------------------
與htons()相似的函數還有三個,它們分别是htonl()、ntohs()和ntohl(),都用于網絡與主機位元組順序之間的轉換。如果這幾個名字比較容易混淆的話,我們可以這樣記憶:函數名中的h代表host,n代表network,s代表unsigned short,l代表unsigned long。是以"hton"即為"host-to-network":變換主機位元組為網絡位元組。接收資料的就要用到"ntoh"("network-to-host")函數了。
在我們的例程中,由于端口号一般情況下最多不會超過4位數字,是以選用unsigned short型的htons()即可。
注意在例程中htons()的參數是另一個函數atoi()的傳回結果。atoi()函數的具體描述為:
-----------------------------------------------------------------
#include <stdlib.h>
int atoi(const char *nptr)
-----------------------------------------------------------------
它的作用是将字元指針nptr指向的字元串轉換成相應的整數并将其作為結果傳回。這個操作與函數調用strtol(nptr,(char **)NULL,10)的效果幾乎完全相同,唯一的差別是atoi()沒有出錯傳回資訊。之是以要調用這個函數是因為,系統在讀取指令行的時候将所有的參數都作為字元串處理,是以我們必須将其轉換為整數形式。
接下來,例程先将全局變量hostaddr的所有成員清零,然後将成員hostaddr.sin_family設定為TCP/IP協定族标志AF_INET。下面就可将指令行的另外兩個參數<remote_host>和<service_port>傳遞給全局變量hostaddr的兩個成員hostaddr.sin_port和hostaddr.sin_addr了。這裡我們用到了兩個局部變量struct hostent *hostp和struct servent *servp來傳遞參數資訊。struct hostent的較長的描述為:
-----------------------------------------------------------------
struct hostent {
char *h_name;
char **h_aliases;
int h_addrtype;
int h_length;
char **h_addr_list;
};
#define h_addr h_addrlist[0]
-----------------------------------------------------------------
hostent成員的含義是h_name代表主機在網絡上的的正式名稱,h_aliases是所有主機名稱的清單,h_addrtype是指主機的位址類型,一般設定為TCP/IP協定族AF_INET,h_length是主機的位址長度,一般設定為4個位元組。h_addr_list是主機的IP位址清單。
我們要用它來傳遞我們期望綁定的遠端主機名或是IP位址。因為指令行中的主機名參數已經被存儲進pargs.isolated_host,是以我們就調用inet_addr()函數對主機名或主機的IP位址進行二進制和位元組順序轉換。inet_addr()函數的描述為:
-----------------------------------------------------------------
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
unsigned long int inet_addr(const char *cp)
-----------------------------------------------------------------
inet_addr()的作用就是将參數cp指向的Internet主機位址從數字/點的形式轉換成二進制形式并同時轉換為網絡位元組順序,并将轉換結果直接傳回。如果cp指向的IP位址不可用,則函數傳回INADDR_NONE或"-1"。
雖然Carl Harris在編寫這段程式時使用了這個inet_addr()函數,但是我還是建議大家在編寫自己的程式時使用另外一個函數inet_aton()來完成這些功能。原因是inet_addr()在IP位址不可用時傳回"-1",但我們想想,IP位址255.255.255.255絕對是一個有效位址,那麼其二進制傳回值也将是"-1",是以inet_addr()無法對這個IP位址進行處理。而函數inet_aton()則采用了一種更好的方法來傳回出錯資訊,它的具體描述為:
-----------------------------------------------------------------
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int inet_aton(const char *cp, struct in_addr *inp)
-----------------------------------------------------------------
函數執行成功時傳回非零,轉換結果存入指針inp指向的in_addr結構。這個結構定義我們在前面的文章裡已經介紹過了。如果參數cp指向的IP位址不可用,則傳回"0"。這就避免發生inet_addr()那樣的問題。
如果說使用者在指令行中鍵入的是遠端主機的IP位址,那麼隻用inet_addr()就算完成任務了,但如果使用者鍵入的是主機域名那該怎麼辦呢?是以我們在例程中可以看到這樣的語句:
-----------------------------------------------------------------
if ((inaddr = inet_addr(pargs.isolated_host)) != INADDR_NONE)
bcopy(&inaddr,&hostaddr.sin_addr,sizeof(inaddr));
else if ((hostp = gethostbyname(pargs.isolated_host)) != NULL)
bcopy(hostp->h_addr,&hostaddr.sin_addr,hostp->h_length);
else {
printf("%s: unknown host\r\n",pargs.isolated_host);
exit(1);
}
-----------------------------------------------------------------
其中gethostbyname()函數就是用來轉換主機域名的。它的具體描述為:
-----------------------------------------------------------------
#include <netdb.h>
struct hostent *gethostbyname(const char *hostname);
-----------------------------------------------------------------
參數hostname指向我們需要轉換的域名位址,函數直接傳回轉換結果,如果函數執行成功,則結果直接傳回到一個指向hostent結構的指針中,否則傳回空指針NULL。
例程就是這樣調用inet_addr()和gethostbyname()将指令行參數中的主機域名或是主機IP位址傳遞給全局變量hostaddr的成員sin_addr以便代理執行函數do_proxy()調用。
下面是傳遞服務名或是服務端口号。這裡要用到結構servent做傳遞中介,struct servent的較長的描述為:
-----------------------------------------------------------------
struct servent {
char *s_name;
char **s_aliases;
int s_port;
char *s_proto;
};
-----------------------------------------------------------------
其各成員的含義是s_name為服務的正式名稱,如ftp、http等,s_aliases是服務的别名清單,s_port是服務的端口号,例如在一般情況下ftp的端口号為21,http服務的端口号為80,注意此端口号應該存儲為網絡位元組順序,s_proto是應用協定的類型。
例程中使用getservbyname()函數轉換指令行參數中的服務名,此函數的較長的描述為:
-----------------------------------------------------------------
#include <netdb.h>
struct servent * getservbyname(const char *servname, const char *protoname);
-----------------------------------------------------------------
它的作用就是轉換指針servname指向的服務名為相應的整數表示的端口号,參數protoname表示服務使用的協定,例程中protoname 參數的值為TCP_PROTO,這表示使用TCP協定。函數成功時就傳回一個struct servent型的指針,其中的s_port成員就是我們關心的服務端口号。如果使用者在指令中鍵入的是端口号而不是服務名,那麼和處理代理端口資訊一樣,使用下面的語句進行處理:
hostaddr.sin_port = htons(atoi(pargs.service_name));
到這裡,指令行的參數已經全部被轉換成為網絡通信所要求的位元組順序和數字類型,并且存儲在三個全局變量中,就等着do_proxy()函數來調用了。
◆daemonize()函數建立守護程序
在對main()函數進行介紹的時候我就提到過,一般伺服器程式在接收客戶機連接配接請求之前,都要建立一個守護程序。守護程序是linux/Unix程式設計中一個非常重要的概念,因為在建立一個守護程序的時候,我們要接觸到子程序、程序組、會晤期、信号機制以及檔案、目錄、控制終端等多個概念,是以詳細地讨論一下守護程序,對初學者學習程序間關系是非常有幫助的。下面就是例程中的daemonize()函數:
-----------------------------------------------------------------
void daemonize (servfd)
int servfd;
{
int childpid, fd, fdtablesize;
signal(SIGTTOU,SIG_IGN);
signal(SIGTTIN,SIG_IGN);
signal(SIGTSTP,SIG_IGN);
if ((childpid = fork()) < 0) {
fputs("failed to fork first child\r\n",stderr);
exit(1);
}
else if (childpid > 0)
exit(0);
if (setpgrp(0,getpid())<0) {
fputs("failed to become process group leader\r\n",stderr);
exit(1);
}
if ((fd = open("/dev/tty",O_RDWR)) >= 0) {
ioctl(fd,TIOCNOTTY,NULL);
close(fd);
}
for (fd = 0, fdtablesize = getdtablesize(); fd < fdtablesize; fd++)
if (fd != servfd)
close(fd);
chdir("/");
umask(0);
signal(SIGCLD,(Sigfunc *)reap_status);
}
-----------------------------------------------------------------
此函數的作用就是建立一個守護程序。在Linux系統中,如果要将一個普通程序轉換成為守護程序,必須要執行下面的步驟:
1. 調用函數fork()建立子程序,然後父程序終止,保留子程序繼續運作。之是以要讓父程序終止是因為,當一個程序是以前台程序方式由shell啟動時,在父程序終止之後子程序自動轉為背景程序。另外,我們在下一步要建立一個新的會晤期,這就要求建立會晤期的程序不是一個程序組的組長程序。當父程序終止,子程序運作,這就保證了程序組的組ID與子程序的程序ID不會相等。
函數fork()的定義為:
-----------------------------------------------------------------
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
-----------------------------------------------------------------
該函數被調用一次,但是傳回兩次,這兩次傳回的差別是子程序的傳回值為"0",而父程序的傳回值為子程序的ID。如果出錯則傳回"-1"。
2. 保證程序不會獲得任何控制終端。通常的做法是調用函數setsid()建立一個新的會晤期。setsid()的較長的描述為:
-----------------------------------------------------------------
#include <sys/types.h>
#include <unistd.h>
pid_t setsid(void);
-----------------------------------------------------------------
第一步的操作已經保證調用此函數的程序不是程序組的組長,那麼此函數将建立一個新的會晤,其結果是:首先,此程序變成該會晤期的首程序(session leader,系統預設會晤期的首程序是建立該會晤期的程序)。而且,此程序是該會晤期中的唯一程序。然後,此程序将成為一個新的程序組的組長程序,新程序組的組ID就是該程序的程序ID。最後,保證此程序沒有控制終端,即使在調用setsid()之前此程序擁有控制終端,在建立會晤期後這種聯系也将被解除。如果調用該函數的程序為一個程序組的組長,那麼函數将傳回出錯資訊"-1"。
當然我們還有其他的辦法讓程序無法獲得控制終端,就象例程中所做的那樣,
-----------------------------------------------------------------
if ((fd = open("/dev/tty",O_RDWR)) >= 0) {
ioctl(fd,TIOCNOTTY,NULL);
close(fd);
}
-----------------------------------------------------------------
其中/dev/tty是一個流裝置,也是我們的終端映射。調用close()函數将終端關閉。
3.信号處理。一般是要忽略掉某些信号。這裡就涉及到信号的概念了。信号其實相當于軟體中斷,Linux/Unix下的信号機制提供了一種處理異步事件的方法,終端使用者鍵入印發中斷的鍵,或是系統異常發出信号,這都會通過信号處理機制終止一個或多個程式的運作。
不同情況下引發的信号是不同的。不過所有的信号都有自己的名字,所有的名字都是以"SIG"開頭的,隻是後面有所不同,我們可以通過這些名字了解到系統中到底發生了些什麼事。
當信号出現時,我們可以要求系統進行以下三種操作:
◇忽略信号。大多數信号都是采取這種方式進行處理的,在例程中我們就可以見到這種用法。但值得注意的是有兩個例外,那就是對SIGKILL和SIGSTOP信号不能做忽略處理。
◇捕捉信号。這是一種最為靈活的操作方式。這種處理方式的意思就是,當某種信号發生時,我們可以調用一個函數對這種情況進行相應的處理。最常見的情況就是,如果捕捉到SIGCHID信号,則表示子程序已經終止,然後可在此信号的捕捉函數中調用waitpid()函數取得該子程序的程序ID以及它的終止狀态。在我們這段例程中,就有這種用法的一個執行個體。還有就是如果程序建立了臨時檔案,那麼就要為程序終止信号SIGTERM編寫一個信号捕捉函數來清除這些臨時檔案。
◇執行系統的預設動作。對絕大多數信号而言,系統的預設動作都是終止該程序。
在Linux下,信号有很多種,我在這裡就不一一介紹了,如果想詳細地對這些信号進行了解,可以檢視頭檔案<sigal.h>,這些信号都被定義為正整數,也就是它們的信号編号。在對信号進行處理時,必須要用到函數signal(),此函數的較長的描述為:
-----------------------------------------------------------------
#include <signal.h>
void (*signal (int signo, void (*func)(int)))(int);
-----------------------------------------------------------------
其中參數signo為信号名,參數func的值根據我們的需要可以是以下幾種情況:(1)常數SIG_DFL,表示執行系統的預設動作。(2)常數SIG_IGN,表示忽略信号。(3)收到信号後需要調用的處理函數的位址,此信号捕捉程式應該有一個整型參數但是沒有傳回值。signal()函數傳回一個函數指針,而該指針指向的函數應該無傳回值(void),這個指針其實指向以前的信号捕捉程式。
下面 回到我們的daemonize()函數上來。這個函數在建立守護程序時忽略了三個信号:
signal(SIGTTOU,SIG_IGN);
signal(SIGTTIN,SIG_IGN);
signal(SIGTSTP,SIG_IGN);
這三個信号的含義分别是:SIGTTOU表示背景程序寫控制終端,SIGTTIN表示背景程序讀控制終端,SIGTSTP表示終端挂起。
4.關閉不再需要的檔案描述符,并為标準輸入、标準輸出和标準錯誤輸出打開新的檔案描述符(也可以繼承父程序的标準輸入、标準輸出和标準錯誤輸出檔案描述符,這個操作是可選的)。在我們這段例程中,因為是代理伺服器程式,而且是在執行了listen()函數之後執行這個daemonize()的,是以要保留已經轉換成功的傾聽套接字,是以我們可以見到這樣的語句:
if (fd != servfd)
close(fd);
5.調用函數chdir("/")将目前工作目錄更改為根目錄。這是為了保證我們的程序不使用任何目錄。否則我們的守護程序将一直占用某個目錄,這可能會造成超級使用者不能解除安裝一個檔案系統。
6.調用函數umask(0)将檔案方式建立屏蔽字設定為"0"。這是因為由繼承得來的檔案建立方式屏蔽字可能會禁止某些許可權。例如我們的守護程序需要建立一組可讀可寫的檔案,而此守護程序從父程序那裡繼承來的檔案建立方式屏蔽字卻有可能屏蔽掉了這兩種許可權,則新建立的一組檔案其讀或寫操作就不能生效。是以要将檔案方式建立屏蔽字設定為"0"。
在daemonize()函數的最後,我們可以看到這樣的信号捕捉處理語句:
signal(SIGCLD,(Sigfunc *)reap_status);
這不是建立守護程序過程中必須的一步,它的作用是調用我們自定義的reap_status()函數來處理僵死程序。reap_status()在例程中的定義為:
-----------------------------------------------------------------
void reap_status()
{
int pid;
union wait status;
while ((pid = wait3(&status,WNOHANG,NULL)) > 0)
;
}
-----------------------------------------------------------------
上面信号捕捉語句的原文為:
signal(SIGCLD, reap_status);
我們剛才說過,signal()函數的第二個參數一定要有有一個整型參數但是沒有傳回值。而reap_status()是沒有參數的,是以原來的語句在編譯時無法通過。是以我在預編譯部分加入了對Sigfunc()的類型定義,在這裡用做對reap_status進行強制類型轉換。而且在BSD系統中通常都使用SIGCHLD信号來處理子程序終止的有關資訊,SIGCLD是System V中定義的一個信号名,如果将SIGCLD信号的處理方式設定為捕捉,那麼核心将馬上檢查系統中是否存在已經終止等待處理的子程序,如果有,則立即調用信号捕捉處理程式。
一般在信号捕捉處理程式中都要調用wait()、waitpid()、wait3()或是wait4()來傳回子程序的終止狀态。這些"等待"函數的差別是,當要求函數"等待"的子程序還沒有終止時,wait()将使其調用者阻塞;而在waitpid()的參數中可以設定使調用者不發生阻塞,wait()函數不被設定為等待哪個具體的子程序,它等待調用者所有子程序中首先終止的那個,而在調用waitpid()時卻必須在參數中設定被等待的子程序ID。而wait3()和wait4()的參數分别比wait()和waitpid()還要多一個"rusage"。例程中的reap_status()就調用了函數wait3(),這個函數是BSD系統支援的,我們把它和wait4()的定義一起列出來:
-----------------------------------------------------------------
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <sys/resource.h>
pid_t wait3(int *statloc, int options, struct rusage *rusage);
pid_t wait4(pid_t pid, int *statloc, int options, struct rusage *rusage);
-----------------------------------------------------------------
其中指針statloc如果不為"NULL",那麼它将指向傳回的子程序終止狀态。參數pid是我們指定的被等待的子程序的程序ID。參數options是我們的控制選擇項,一般為WNOHANG或是WUNTRACED。例程中使用了選項WNOHANG,意即如果不能立即傳回子程序的終止狀态(譬如由于子程序還未結束),那麼等待函數不阻塞,此時傳回"0"。 WUNTRACED選項的意思是如果系統支援作業控制,如果要等待的子程序的狀态已經暫停,而且其狀态自從暫停以來還從未報告過,則傳回其狀态。參數rusage如果不為"NULL",則它将指向核心傳回的由終止程序及其所有子程序使用的資源摘要,該摘要包括使用者CPU時間總量、缺頁次數、接收到信号的次數等。
◆代理服務程式do_proxy()
在例程main()函數快要結束時,我們看到,在伺服器接受了客戶機的連接配接請求後,将為其建立子程序,并在子程序中執行代理服務程式do_proxy()。
-----------------------------------------------------------------
void do_proxy (usersockfd)
int usersockfd;
{
int isosockfd;
fd_set rdfdset;
int connstat;
int iolen;
char buf[2048];
if ((isosockfd = socket(AF_INET,SOCK_STREAM,0)) < 0)
errorout("failed to create socket to host");
connstat = connect(isosockfd,(struct sockaddr *) &hostaddr, sizeof(hostaddr));
switch (connstat) {
case 0:
break;
case ETIMEDOUT:
case ECONNREFUSED:
case ENETUNREACH:
strcpy(buf,sys_myerrlist[errno]);
strcat(buf,"\r\n");
write(usersockfd,buf,strlen(buf));
close(usersockfd);
exit(1);
break;
default:
errorout("failed to connect to host");
}
while (1) {
FD_ZERO(&rdfdset);
FD_SET(usersockfd,&rdfdset);
FD_SET(isosockfd,&rdfdset);
if (select(FD_SETSIZE,&rdfdset,NULL,NULL,NULL) < 0)
errorout("select failed");
if (FD_ISSET(usersockfd,&rdfdset)) {
if ((iolen = read(usersockfd,buf,sizeof(buf))) <= 0)
break;
rite(isosockfd,buf,iolen);
}
if (FD_ISSET(isosockfd,&rdfdset)) {
f ((iolen = read(isosockfd,buf,sizeof(buf))) <= 0)
break;
rite(usersockfd,buf,iolen);
}
}
close(isosockfd);
lose(usersockfd);
}
-----------------------------------------------------------------
在我們這段代理伺服器例程中,真正連接配接使用者主機和遠端主機的一段操作,就是由這個do_proxy()函數來完成的。回想一下我們一開始對這段proxy程式用法的介紹。先将我們的proxy與遠端主機綁定,然後使用者通過proxy的綁定端口與遠端主機建立連接配接。而在main()函數中,我們的proxy由一段伺服器程式與使用者主機建立了連接配接,而在這個do_proxy()函數中,proxy将與遠端主機的相應服務端口(由使用者在指令行參數中指定)建立連接配接,并負責傳遞使用者主機和遠端主機之間交換的資料。
由于要和遠端主機建立連接配接,是以我們看到do_proxy()函數的前半部分實際上相當于一段标準的客戶機程式。首先建立一個新的套接字描述符isosockfd,然後調用函數connect()與遠端主機之間建立連接配接。函數connect()的定義為:
-----------------------------------------------------------------
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, struct sockaddr *servaddr, int addrlen);
-----------------------------------------------------------------
參數sockfd是調用函數socket()傳回的套接字描述符,參數servaddr指向遠端伺服器的套接字位址結構,參數addrlen指定這個套接字位址結構的長度。函數connect()執行成功時傳回"0",如果執行失敗則傳回"-1",并将全局變量errno設定為相應的錯誤類型。在例程中的switch()函數調用中對以下三種出錯類型進行了處理: ETIMEDOUT、ECONNREFUSED和ENETUNREACH。這三個出錯類型的意思分别為:ETIMEDOUT代表逾時,産生這種情況的原因有很多,最常見的是伺服器忙,無法應答客戶機的連接配接請求;ECONNREFUSED代表連接配接拒絕,即伺服器端沒有準備好的傾聽套接字,或是沒有對傾聽套接字的狀态進行監聽;ENETUNREACH表示網絡不可達。
在本例中,connect()函數的第二個參數servaddr是全局變量hostaddr,其中存儲着函數parse_args()轉換好的指令行參數。如果連接配接建立失敗,在例程中就調用我們自定義的函數errorout()輸出資訊"failed to connect to host"。errorout()函數的定義為:
-----------------------------------------------------------------
void errorout (msg)
char *msg;
{
FILE *console;
console = fopen("/dev/console","a");
fprintf(console,"proxyd: %s\r\n",msg);
fclose(console);
exit(1);
}
-----------------------------------------------------------------
do_proxy()函數的後半部分是通過proxy建立使用者主機與遠端主機之間的連接配接。我們既有proxy與使用者主機連接配接的套接字(do_proxy()函數的參數usersockfd),又有proxy與遠端主機連接配接的套接字isosockfd,那麼最簡單直接的通信建立方式就是從一個套接字讀,然後直接寫到另一個套接字去。如:
-----------------------------------------------------------------
int n;
char buf[2048];
while((n=read(usersockfd, buf, sizeof(buf))>0)
if(write(isosockfd, buf, n)!=n)
err_sys("write wrror\n");
-----------------------------------------------------------------
這種形式的阻塞I/O在單向資料傳遞的時候是非常有效的,但是在我們的proxy操作中是要求使用者主機和遠端主機雙向通信的,這樣就要求我們對兩個套接字描述符既能夠讀由能夠寫。如果還是采用這種方式的阻塞I/O的話,很有可能長時間阻塞在一個描述符上。是以例程在處理這個問題的時候調用了select()函數,這個函數允許我們執行I/O多路轉接。其具體含義就是select()函數可以構造一個表,在這個表中包含了我們所有要用到的檔案描述符。然後我們可以調用一個函數,這個函數可以檢測這些檔案描述符的狀态,當某個(我們指定的)檔案描述符準備好進行I/O操作時,此函數就傳回,告知程序哪個檔案描述符已經可以執行I/O操作了。這樣就避免了長時間的阻塞。
還有一個函數poll()可以實作I/O多路轉接,由于在例程中調用的是select(),我們就隻對select()進行一下比較詳細的介紹。select()系列函數的較長的描述為:
-----------------------------------------------------------------
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int n, fd_set *readfds, fd_set *writefds, fd_est *exceptfds, struct timeval *timeout);
FD_CLR(int fd, fd_set *set);
FD_ISSET(int fd, fd_set *set);
FD_SET(int fd, fd_set *set);
FD_ZERO(fd_set *set);
-----------------------------------------------------------------
select()函數将建立一個我們所關心的檔案描述符表,它的參數将在核心中為這些檔案描述符設定我們所關心的條件,例如是否是可讀、是否可寫以及是否異常,而且在參數中還可以設定我們希望等待的最大時間。在select()成功執行時,它将傳回目前已經準備好的描述符數量,同時核心可以告訴我們各個描述符的狀态資訊。如果逾時,則傳回"0",如果出錯,則函數傳回"-1",并同時設定errno為相應的值。
select()的最後一個參數timeout将設定等待時間。其中結構timeval是在檔案<bits/time.h>中定義的。
-----------------------------------------------------------------
struct timeval
{
__time_t tv_sec;
__time_t tv_usec;
};
-----------------------------------------------------------------
參數timeout的設定有三種情況。象例程中這樣timeout==NULL時,這表示使用者希望永遠等待,直到我們指定的檔案描述符中的一個已準備好,或者是捕捉到一個信号。如果是由于捕捉到信号而中斷了這個無限期的等待過程的話,select()将傳回"-1",同時設定errno的值為EINTR。
如果timeout->tv_sec==0&&timeout->tv_usec==0,那麼這表示完全不等待。Select()測試了所有指定檔案描述符後立即傳回。這是得到多個描述符狀态而不阻塞select()函數的輪詢方法。
如果timeout->tv_sec!=0||timeout->tv_usec!=0,那麼這兩個參數的值即為我們希望函數等待的時間。其中tv_sec設定時間機關為秒,tv_usec設定時間機關為微秒。如果在逾時的時候,在我們指定的所有檔案描述符裡面仍然沒有任何一個準備好的話,則select()将傳回"0"。
中間三個參數的資料類型是fd_set,它的意思是檔案描述符集,而readfds, writefds和exceptfds則分别是指向檔案描述符集的指針,他們分别描述了我們所關心的可讀、可寫以及狀态異常的各個檔案描述符。之是以我們稱select()可以建立一個檔案描述符"表",那個所謂的表就是由這三個參數指向的資料結構組成的。其具體結構如圖1所示。其中在每個set_fd資料類型中都為我們關心的所有檔案描述符保留了一位。是以在監測檔案描述符狀态的時候,就在這些set_fd資料結構中查詢相關的位。
第一個參數n用來說明到底需要周遊多少個描述符位。n的值一般是這樣設定的,從我們關心的所有檔案描述符中選出最大值再加1。例如我們設定的所有檔案描述符中最大的為6,那麼将n設定為7,則系統在檢測描述符狀态的時候,就隻用周遊前7位(fd0~fd6)的狀态。不過如果不想這樣麻煩的話,我們可以象例程中那樣将n的值直接設定為FD_SETSIZE。這是系統中設定的最大檔案描述符個數,不同的系統這個值也不相同,一般是256或是1024。這樣在檢測描述符狀态的時候,函數将周遊所有的描述符位。
在調用select()函數實作多路I/O轉接時,首先我們要聲明一個新的檔案描述符集,就象例程中這樣:
fd_set rdfdset;
然後調用FD_ZERO()清空此檔案描述符集的所有位,以免下面檢測描述符位的時候傳回錯誤結果:
FD_ZERO(&rdfdset);
然後調用FD_SET()在檔案描述符集中設定我們關心的位。在本例中,我們關心的就是分别與使用者主機和遠端主機連接配接的兩個套接字描述符,是以執行這樣的語句:
FD_SET(usersockfd,&rdfdset);
FD_SET(isosockfd,&rdfdset);
然後調用select()傳回描述符狀态,此時描述符狀态被存儲進描述符集,也就是set_fd資料結構中。在圖1中我們看到所有的描述符位狀态都是"0",在select()傳回後,例如fd0可讀,則在readfds描述符集中fd0對應的位上将狀态标志設定為"1",如果fd1可寫,則writefds描述符集中fd1對應的位上将狀态标志設定為"1",狀态異常的情況也也與此相同。在本例中,我們隻關心兩個套接字描述符是否可寫,是以執行這樣的select()函數:
select(FD_SETSIZE,&rdfdset,NULL,NULL,NULL)
那麼在select()傳回後怎樣檢測set_fd資料結構中描述符位的狀态呢?這就要調用函數FD_ISSET(),如果對應檔案描述符的狀态為"已準備好"(即描述符位為"1"),則FD_ISSET()傳回"1",否則傳回"0"。
-----------------------------------------------------------------
if (FD_ISSET(usersockfd,&rdfdset)) {
if ((iolen = read(usersockfd,buf,sizeof(buf))) <= 0)
break;
write(isosockfd,buf,iolen);
-----------------------------------------------------------------
這一段代碼就實作從套接字usersockfd(使用者主機)到套接字isosockfd(遠端主機)的無阻塞傳輸。而下一段代碼實作反方向的無阻塞傳輸:
-----------------------------------------------------------------
if (FD_ISSET(isosockfd,&rdfdset)) {
if ((iolen = read(isosockfd,buf,sizeof(buf))) <= 0)
break;
write(usersockfd,buf,iolen);
-----------------------------------------------------------------
這樣就通過proxy實作了使用者主機與遠端主機之間的通信。
對這段proxy代碼我隻是寫了一些自己的了解,大多數是一些函數的用法,這些都是linux網絡程式設計中一些最基礎的知識,如果有不對的地方,還請各位大蝦批評指正。