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是指我們希望連接的遠程主機的主機名,IP地址也同樣有效。這個主機名在網絡上應該是唯一的,如果您不確定的話,可以在遠程主機上使用uname -n命令查看一下。參數service_port是遠程主機可提供的服務名,也可直接鍵入服務對應的端口號。這個命令的相應操作是將代理服務器的proxy_port端口綁定到remote_host的service_port端口。然後我們就可以通過代理服務器的proxy_port端口訪問remote_host了。例如一台計算機,網絡主機名是legends,IP地址為10.10.8.221,如果在我的計算機上執行: [root@lee /root]#./proxy 8000 legends telnet 那麼我們就可以通過下面這條命令訪問legends的telnet端口。 ----------------------------------------------------------------- [root@lee /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: ----------------------------------------------------------------- 上面的綁定操作也可以使用下面的命令: [root@lee /root]#./proxy 8000 10.10.8.221 23 23是telnet服務的標准端口號,其它服務的對應端口號我們可以在/etc/services中查看。 下面我就從這段代碼出發談談我對Linux網絡編程的一些粗淺的認識,不對的地方還請各位大蝦多多批評指正。 ◆main()函數 ----------------------------------------------------------------- #include #include #include #include #include #include #include #include #include #include #include #define TCP_PROTO "tcp" int proxy_port; /* port to listen for proxy connections on */ strUCt sockaddr_in hostaddr; /* host addr assembled from gethostbyname() */ extern int errno; /* defined by libc.a */ 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); /*This is my modification. I'll tell you why we must do this later*/ typedef void Signal(int); /**************************************************************** function: main description: Main level driver. After daemonizing the process, a socket is opened to listen for connections on the proxy port, connections are accepted and children are spawned to handle each new connection. arguments: argc,argv you know what those are. return value: none. calls: parse_args, do_proxy. globals: reads proxy_port. ****************************************************************/ main (argc,argv) int argc; char **argv; { int clilen; int childpid; int sockfd, newsockfd; struct sockaddr_in servaddr, cliaddr; parse_args(argc,argv); /* prepare an address struct to listen for connections */ bzero((char *) &servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = proxy_port; /* get a socket... */ if ((sockfd = socket(AF_INET,SOCK_STREAM,0)) < 0) { fputs("failed to create server socket\r\n",stderr); exit(1); } /* ...and bind our address and port to it */ if (bind(sockfd,(struct sockaddr_in *) &servaddr,sizeof(servaddr)) < 0) { fputs("faild to bind server socket to specified port\r\n",stderr); exit(1); } /* get ready to accept with at most 5 clients waiting to connect */ listen(sockfd,5); /* turn ourselves into a daemon */ daemonize(sockfd); /* fall into a loop to accept new connections and spawn children */ while (1) { /* accept the next connection */ clilen = sizeof(cliaddr); newsockfd = accept(sockfd, (struct sockaddr_in *) &cliaddr, &clilen); if (newsockfd < 0 && errno == EINTR) continue; /* a signal might interrupt our accept() call */ else if (newsockfd < 0) /* something quite amiss -- kill the server */ errorout("failed to accept connection"); /* fork a child to handle this connection */ if ((childpid = fork()) == 0) { close(sockfd); do_proxy(newsockfd); exit(0); } /* if fork() failed, the connection is silently dropped -- oops! */ 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 #include 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系統還定義了一種通用的套接字地址結構: ----------------------------------------------------------------- struct sockaddr { unsigned short sa_family; /* address type */ char sa_data[14]; /* protocol address */ } ----------------------------------------------------------------- 其中sa_family意指套接字使用的協議族地址類型,對於我們的TCP/IP網絡,其值應該是AF_INET,sa_data中存儲具體的協議地址,不同的協議族有不同的地址格式。這個通用的套接字地址結構一般不用做定義具體的實例,但是常用做套接字地址結構的強制類型轉換,如我們經常可以看到這樣的用法: bind(sockfd,(struct sockaddr *) &servaddr,sizeof(servaddr)) 用於TCP/IP協議族的套接字地址結構是sockaddr_in,其定義為: ----------------------------------------------------------------- struct in_addr { __u32 s_addr; }; struct sochaddr_in { short int sin_family; unsigned short int sin_port; struct in_addr sin_addr; /*This part has not been taken into use yet*/ 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。 另外需要說明的一點是,在本例中,我們並沒有看到在預編譯部分中包含有和這兩個頭文件,那是因為這兩個頭文件已經分別被包含在和中了,而且後面這兩個頭文件是與平台無關的,所以在網絡通信中一般都使用這兩個頭文件。 服務器公開地址 如果服務器要接受客戶機的連接請求,那麼它必須先要在整個網絡上公開自己的地址。在設置了服務器的套接字地址結構之後,可以通過調用函數bind()綁定服務器的地址和套接字來完成公開地址的操作。函數bind()的詳細描述為: ----------------------------------------------------------------- #include #include 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 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 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; /* a signal might interrupt our accept() call */ else if (newsockfd < 0) /* something quite amiss -- kill the server */ 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); ----------------------------------------------------------------- /**************************************************************** function: parse_args description: parse the command line args. arguments: argc,argv you know what these are. return value: none. calls: none. globals: writes proxy_port, writes hostaddr. ****************************************************************/ 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
\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 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 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 int atoi(const char *nptr) ----------------------------------------------------------------- 它的作用是將字符指針nptr指向的字符串轉換成相應的整數並將其作為結果返回。這個操作與函數調用strtol(nptr,(char **)NULL,10)的效果幾乎完全相同,唯一的區別是atoi()沒有出錯返回信息。之所以要調用這個函數是因為,系統在讀取命令行的時候將所有的參數都作為字符串處理,所以我們必須將其轉換為整數形式。 接下來,例程先將全局變量hostaddr的所有成員清零,然後將成員hostaddr.sin_family設置為TCP/IP協議族標志AF_INET。下面就可將命令行的另外兩個參數和傳遞給全局變量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 #include #include 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 #include #include 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 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 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()函數: ----------------------------------------------------------------- /**************************************************************** function: daemonize description: detach the server process from the current context, creating a pristine, predictable environment in which it will execute. arguments: servfd file descriptor in use by server. return value: none. calls: none. globals: none. ****************************************************************/ void daemonize (servfd) int servfd; { int childpid, fd, fdtablesize; /* ignore terminal I/O, stop signals */ signal(SIGTTOU,SIG_IGN); signal(SIGTTIN,SIG_IGN); signal(SIGTSTP,SIG_IGN); /* fork to put us in the background (whether or not the user specified '&' on the command line */ if ((childpid = fork()) < 0) { fputs("failed to fork first child\r\n",stderr); exit(1); } else if (childpid > 0) exit(0); /* terminate parent, continue in child */ /* dissociate from process group */ if (setpgrp(0,getpid())<0) { fputs("failed to become process group leader\r\n",stderr); exit(1); } /* lose controlling terminal */ if ((fd = open("/dev/tty",O_RDWR)) >= 0) { ioctl(fd,TIOCNOTTY,NULL); close(fd); } /* close any open file descriptors */ for (fd = 0, fdtablesize = getdtablesize(); fd < fdtablesize; fd++) if (fd != servfd) close(fd); /* set working Directory to allow filesystems to be unmounted */ chdir("/"); /* clear the inherited umask */ umask(0); /* setup zombie prevention */ signal(SIGCLD,(Sigfunc *)reap_status); } ----------------------------------------------------------------- 此函數的作用就是創建一個守護進程。在Linux系統中,如果要將一個普通進程轉換成為守護進程,必須要執行下面的步驟: 1. 調用函數fork()創建子進程,然後父進程終止,保留子進程繼續運行。之所以要讓父進程終止是因為,當一個進程是以前台進程方式由shell啟動時,在父進程終止之後子進程自動轉為後台進程。另外,我們在下一步要創建一個新的會晤期,這就要求創建會晤期的進程不是一個進程組的組長進程。當父進程終止,子進程運行,這就保證了進程組的組ID與子進程的進程ID不會相等。 函數fork()的定義為: ----------------------------------------------------------------- #include #include pid_t fork(void); ----------------------------------------------------------------- 該函數被調用一次,但是返回兩次,這兩次返回的區別是子進程的返回值為"0",而父進程的返回值為子進程的ID。如果出錯則返回"-1"。 2. 保證進程不會獲得任何控制終端。通常的做法是調用函數setsid()創建一個新的會晤期。setsid()的詳細描述為: ----------------------------------------------------------------- #include #include 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下,信號有很多種,我在這裡就不一一介紹了,如果想詳細地對這些信號進行了解,可以查看頭文件,這些信號都被定義為正整數,也就是它們的信號編號。在對信號進行處理時,必須要用到函數signal(),此函數的詳細描述為: ----------------------------------------------------------------- #include 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()在例程中的定義為: ----------------------------------------------------------------- /**************************************************************** function: reap_status description: handle a SIGCLD signal by reaping the exit status of the perished child, and discarding it. arguments: none. return value: none. calls: none. globals: none. ****************************************************************/ void reap_status() { int pid; union wait status; while ((pid = wait3(&status,WNOHANG,NULL)) > 0) ; /* loop while there are more dead children */ } ----------------------------------------------------------------- 上面信號捕捉語句的原文為: 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 #include #include #include 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()。 -----------------------------------------------------------------/**************************************************************** function: do_proxy description: does the actual work of virtually connecting a client to the telnet service on the isolated host. arguments: usersockfd socket to which the client is connected. return value: none. calls: none. globals: reads hostaddr. ****************************************************************/ void do_proxy (usersockfd) int usersockfd; { int isosockfd; fd_set rdfdset; int connstat; int iolen; char buf[2048]; /* open a socket to connect to the isolated host */ if ((isosockfd = socket(AF_INET,SOCK_STREAM,0)) < 0) errorout("failed to create socket to host"); /* attempt a connection */ 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); /* die peacefully if we can't establish a connection */ break; default: errorout("failed to connect to host"); } /* now we're connected, serve fall into the data echo loop */ while (1) { /* Select for readability on either of our two sockets */ FD_ZERO(&rdfdset); FD_SET(usersockfd,&rdfdset); FD_SET(isosockfd,&rdfdset); if (select(FD_SETSIZE,&rdfdset,NULL,NULL,NULL) < 0) errorout("select failed"); /* is the client sending data? */ if (FD_ISSET(usersockfd,&rdfdset)) { if ((iolen = read(usersockfd,buf,sizeof(buf))) <= 0) break; /* zero length means the client disconnected */ rite(isosockfd,buf,iolen); /* copy to host -- blocking semantics */ } /* is the host sending data? */ if (FD_ISSET(isosockfd,&rdfdset)) { f ((iolen = read(isosockfd,buf,sizeof(buf))) <= 0) break; /* zero length means the host disconnected */ rite(usersockfd,buf,iolen); /* copy to client -- blocking semantics */ } } /* we're done with the sockets */ close(isosockfd); lose(usersockfd); } ----------------------------------------------------------------- 在我們這段代理服務器例程中,真正連接用戶主機和遠端主機的一段操作,就是由這個do_proxy()函數來完成的。回想一下我們一開始對這段proxy程序用法的介紹。先將我們的proxy與遠端主機綁定,然後用戶通過proxy的綁定端口與遠端主機建立連接。而在main()函數中,我們的proxy由一段服務器程序與用戶主機建立了連接,而在這個do_proxy()函數中,proxy將與遠端主機的相應服務端口(由用戶在命令行參數中指定)建立連接,並負責傳遞用戶主機和遠端主機之間交換的數據。 由於要和遠端主機建立連接,所以我們看到do_proxy()函數的前半部分實際上相當於一段標准的客戶機程序。首先創建一個新的套接字描述符isosockfd,然後調用函數connect()與遠端主機之間建立連接。函數connect()的定義為: ----------------------------------------------------------------- #include #include 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()函數的定義為: ----------------------------------------------------------------- /**************************************************************** function: errorout description: displays an error message on the console and kills the current process. arguments: msg -- message to be displayed. return value: none -- does not return. calls: none. globals: none. ****************************************************************/ 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 #include #include 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是在文件中定義的。 ----------------------------------------------------------------- struct timeval { __time_t tv_sec; /* Seconds */ __time_t tv_usec; /* Microseconds */ }; ----------------------------------------------------------------- 參數timeout的設置有三種情況。象例程中這樣timeout==NULL時,這表示用戶希望永遠等待,直到我們指定的文件描述符中的一個已准備好,或者是捕捉到一個信號。如果是由於捕捉到信號而中斷了這個無限期的等待過程的話,select()將返回"-1",同時設置errno的值為EINTR。 如果timeout->tv_sec==0&&timeout->tv_usec==0,那麼這表示完全不等待。Select()測試了所有指定文件描述符後立即返回。這是得到多個描述符狀態而不阻塞select()函數的輪詢方法。 如果timeout->tv_sec!=0timeout->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; /* zero length means the host disconnected */ write(isosockfd,buf,iolen); ----------------------------------------------------------------- 這一段代碼就實現從套接字usersockfd(用戶主機)到套接字isosockfd(遠端主機)的無阻塞傳輸。而下一段代碼實現反方向的無阻塞傳輸: ----------------------------------------------------------------- if (FD_ISSET(isosockfd,&rdfdset)) { if ((iolen = read(isosockfd,buf,sizeof(buf))) <= 0) break; /* zero length means the host disconnected */ write(usersockfd,buf,iolen); ----------------------------------------------------------------- 這樣就通過proxy實現了用戶主機與遠端主機之間的通信。 對這段proxy代碼我只是寫了一些自己的理解,大多數是一些函數的用法,這些都是linux網絡編程中一些最基礎的知識,如果有不對的地方,還請各位大蝦批評指正。