經(jīng)典的Linux Socket 編程 示例代碼 (下)
摘自:http://fanqiang.chinaunix.net/a4/b7/20010810/1200001101.html
在例程main()函數(shù)快要結(jié)束時(shí),我們看到,在服務(wù)器接受了客戶(hù)機(jī)的連接請(qǐng)求后,將為其創(chuàng)建子進(jìn)程,并在子進(jìn)程中執(zhí)行代理服務(wù)程序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);
}
-----------------------------------------------------------------
在 我們這段代理服務(wù)器例程中,真正連接用戶(hù)主機(jī)和遠(yuǎn)端主機(jī)的一段操作,就是由這個(gè)do_proxy()函數(shù)來(lái)完成的?;叵胍幌挛覀円婚_(kāi)始對(duì)這段 proxy程序用法的介紹。先將我們的proxy與遠(yuǎn)端主機(jī)綁定,然后用戶(hù)通過(guò)proxy的綁定端口與遠(yuǎn)端主機(jī)建立連接。而在main()函數(shù)中,我們的 proxy由一段服務(wù)器程序與用戶(hù)主機(jī)建立了連接,而在這個(gè)do_proxy()函數(shù)中,proxy將與遠(yuǎn)端主機(jī)的相應(yīng)服務(wù)端口(由用戶(hù)在命令行參數(shù)中指 定)建立連接,并負(fù)責(zé)傳遞用戶(hù)主機(jī)和遠(yuǎn)端主機(jī)之間交換的數(shù)據(jù)。
由于要和遠(yuǎn)端主機(jī)建立連接,所以我們看到do_proxy()函數(shù)的前半部分實(shí)際上相當(dāng)于一段標(biāo)準(zhǔn)的客戶(hù)機(jī)程序。首先創(chuàng)建一個(gè)新的套接字描述符 isosockfd,然后調(diào)用函數(shù)connect()與遠(yuǎn)端主機(jī)之間建立連接。函數(shù)connect()的定義為:
-----------------------------------------------------------------
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, struct sockaddr *servaddr, int addrlen);
-----------------------------------------------------------------
參數(shù)sockfd是調(diào)用函數(shù)socket()返回的套接字描述符,參數(shù)servaddr指向遠(yuǎn)程服務(wù)器的套接字地址結(jié)構(gòu),參數(shù)addrlen指定這個(gè) 套接字地址結(jié)構(gòu)的長(zhǎng)度。函數(shù)connect()執(zhí)行成功時(shí)返回"0",如果執(zhí)行失敗則返回"-1",并將全局變量errno設(shè)置為相應(yīng)的錯(cuò)誤類(lèi)型。在例程 中的switch()函數(shù)調(diào)用中對(duì)以下三種出錯(cuò)類(lèi)型進(jìn)行了處理: ETIMEDOUT、ECONNREFUSED和ENETUNREACH。這三個(gè)出錯(cuò)類(lèi)型的意思分別為:ETIMEDOUT代表超時(shí),產(chǎn)生這種情況的原因 有很多,最常見(jiàn)的是服務(wù)器忙,無(wú)法應(yīng)答客戶(hù)機(jī)的連接請(qǐng)求;ECONNREFUSED代表連接拒絕,即服務(wù)器端沒(méi)有準(zhǔn)備好的傾聽(tīng)套接字,或是沒(méi)有對(duì)傾聽(tīng)套接 字的狀態(tài)進(jìn)行監(jiān)聽(tīng);ENETUNREACH表示網(wǎng)絡(luò)不可達(dá)。
在本例中,connect()函數(shù)的第二個(gè)參數(shù)servaddr是全局變量hostaddr,其中存儲(chǔ)著函數(shù)parse_args()轉(zhuǎn)換好的命令行 參數(shù)。如果連接建立失敗,在例程中就調(diào)用我們自定義的函數(shù)errorout()輸出信息"failed to connect to host"。errorout()函數(shù)的定義為:
-----------------------------------------------------------------
/****************************************************************
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()函數(shù)的后半部分是通過(guò)proxy建立用戶(hù)主機(jī)與遠(yuǎn)端主機(jī)之間的連接。我們既有proxy與用戶(hù)主機(jī)連接的套接字 (do_proxy()函數(shù)的參數(shù)usersockfd),又有proxy與遠(yuǎn)端主機(jī)連接的套接字isosockfd,那么最簡(jiǎn)單直接的通信建立方式就是 從一個(gè)套接字讀,然后直接寫(xiě)到另一個(gè)套接字去。如:
-----------------------------------------------------------------
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在單向數(shù)據(jù)傳遞的時(shí)候是非常有效的,但是在我們的proxy操作中是要求用戶(hù)主機(jī)和遠(yuǎn)端主機(jī)雙向通信的,這樣就要求我們對(duì)兩個(gè)套 接字描述符既能夠讀由能夠?qū)?。如果還是采用這種方式的阻塞I/O的話(huà),很有可能長(zhǎng)時(shí)間阻塞在一個(gè)描述符上。因此例程在處理這個(gè)問(wèn)題的時(shí)候調(diào)用了 select()函數(shù),這個(gè)函數(shù)允許我們執(zhí)行I/O多路轉(zhuǎn)接。其具體含義就是select()函數(shù)可以構(gòu)造一個(gè)表,在這個(gè)表中包含了我們所有要用到的文件 描述符。然后我們可以調(diào)用一個(gè)函數(shù),這個(gè)函數(shù)可以檢測(cè)這些文件描述符的狀態(tài),當(dāng)某個(gè)(我們指定的)文件描述符準(zhǔn)備好進(jìn)行I/O操作時(shí),此函數(shù)就返回,告知 進(jìn)程哪個(gè)文件描述符已經(jīng)可以執(zhí)行I/O操作了。這樣就避免了長(zhǎng)時(shí)間的阻塞。
還有一個(gè)函數(shù)poll()可以實(shí)現(xiàn)I/O多路轉(zhuǎn)接,由于在例程中調(diào)用的是select(),我們就只對(duì)select()進(jìn)行一下比較詳細(xì)的介紹。select()系列函數(shù)的詳細(xì)描述為:
-----------------------------------------------------------------
#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()函數(shù)將創(chuàng)建一個(gè)我們所關(guān)心的文件描述符表,它的參數(shù)將在內(nèi)核中為這些文件描述符設(shè)置我們所關(guān)心的條件,例如是否是可讀、是否可寫(xiě)以及 是否異常,而且在參數(shù)中還可以設(shè)置我們希望等待的最大時(shí)間。在select()成功執(zhí)行時(shí),它將返回目前已經(jīng)準(zhǔn)備好的描述符數(shù)量,同時(shí)內(nèi)核可以告訴我們各 個(gè)描述符的狀態(tài)信息。如果超時(shí),則返回"0",如果出錯(cuò),則函數(shù)返回"-1",并同時(shí)設(shè)置errno為相應(yīng)的值。
select()的最后一個(gè)參數(shù)timeout將設(shè)置等待時(shí)間。其中結(jié)構(gòu)timeval是在文件<bits/time.h>中定義的。
-----------------------------------------------------------------
struct timeval
{
__time_t tv_sec; /* Seconds */
__time_t tv_usec; /* Microseconds */
};
-----------------------------------------------------------------
參數(shù)timeout的設(shè)置有三種情況。象例程中這樣timeout==NULL時(shí),這表示用戶(hù)希望永遠(yuǎn)等待,直到我們指定的文件描述符中的一個(gè)已準(zhǔn)備 好,或者是捕捉到一個(gè)信號(hào)。如果是由于捕捉到信號(hào)而中斷了這個(gè)無(wú)限期的等待過(guò)程的話(huà),select()將返回"-1",同時(shí)設(shè)置errno的值為 EINTR。
如果timeout->tv_sec==0&&timeout->tv_usec==0,那么這表示完全不等待。 Select()測(cè)試了所有指定文件描述符后立即返回。這是得到多個(gè)描述符狀態(tài)而不阻塞select()函數(shù)的輪詢(xún)方法。
如果timeout->tv_sec!=0||timeout->tv_usec!=0,那么這兩個(gè)參數(shù)的值即為我們希望函數(shù)等待的時(shí) 間。其中tv_sec設(shè)置時(shí)間單位為秒,tv_usec設(shè)置時(shí)間單位為微秒。如果在超時(shí)的時(shí)候,在我們指定的所有文件描述符里面仍然沒(méi)有任何一個(gè)準(zhǔn)備好的 話(huà),則select()將返回"0"。
中間三個(gè)參數(shù)的數(shù)據(jù)類(lèi)型是fd_set,它的意思是文件描述符集,而readfds, writefds和exceptfds則分別是指向文件描述符集的指針,他們分別描述了我們所關(guān)心的可讀、可寫(xiě)以及狀態(tài)異常的各個(gè)文件描述符。之所以我們 稱(chēng)select()可以創(chuàng)建一個(gè)文件描述符"表",那個(gè)所謂的表就是由這三個(gè)參數(shù)指向的數(shù)據(jù)結(jié)構(gòu)組成的。其具體結(jié)構(gòu)如圖1所示。其中在每個(gè)set_fd數(shù) 據(jù)類(lèi)型中都為我們關(guān)心的所有文件描述符保留了一位。所以在監(jiān)測(cè)文件描述符狀態(tài)的時(shí)候,就在這些set_fd數(shù)據(jù)結(jié)構(gòu)中查詢(xún)相關(guān)的位。
第一個(gè)參數(shù)n用來(lái)說(shuō)明到底需要遍歷多少個(gè)描述符位。n的值一般是這樣設(shè)置的,從我們關(guān)心的所有文件描述符中選出最大值再加1。例如我們?cè)O(shè)置的所有文件 描述符中最大的為6,那么將n設(shè)置為7,則系統(tǒng)在檢測(cè)描述符狀態(tài)的時(shí)候,就只用遍歷前7位(fd0~fd6)的狀態(tài)。不過(guò)如果不想這樣麻煩的話(huà),我們可以 象例程中那樣將n的值直接設(shè)置為FD_SETSIZE。這是系統(tǒng)中設(shè)定的最大文件描述符個(gè)數(shù),不同的系統(tǒng)這個(gè)值也不相同,一般是256或是1024。這樣 在檢測(cè)描述符狀態(tài)的時(shí)候,函數(shù)將遍歷所有的描述符位。
在調(diào)用select()函數(shù)實(shí)現(xiàn)多路I/O轉(zhuǎn)接時(shí),首先我們要聲明一個(gè)新的文件描述符集,就象例程中這樣:
fd_set rdfdset;
然后調(diào)用FD_ZERO()清空此文件描述符集的所有位,以免下面檢測(cè)描述符位的時(shí)候返回錯(cuò)誤結(jié)果:
FD_ZERO(&rdfdset);
然后調(diào)用FD_SET()在文件描述符集中設(shè)置我們關(guān)心的位。在本例中,我們關(guān)心的就是分別與用戶(hù)主機(jī)和遠(yuǎn)端主機(jī)連接的兩個(gè)套接字描述符,所以執(zhí)行這樣的語(yǔ)句:
FD_SET(usersockfd,&rdfdset);
FD_SET(isosockfd,&rdfdset);
然后調(diào)用select()返回描述符狀態(tài),此時(shí)描述符狀態(tài)被存儲(chǔ)進(jìn)描述符集,也就是set_fd數(shù)據(jù)結(jié)構(gòu)中。在圖1中我們看到所有的描述符位狀態(tài)都是 "0",在select()返回后,例如fd0可讀,則在readfds描述符集中fd0對(duì)應(yīng)的位上將狀態(tài)標(biāo)志設(shè)置為"1",如果fd1可寫(xiě),則 writefds描述符集中fd1對(duì)應(yīng)的位上將狀態(tài)標(biāo)志設(shè)置為"1",狀態(tài)異常的情況也也與此相同。在本例中,我們只關(guān)心兩個(gè)套接字描述符是否可寫(xiě),因此 執(zhí)行這樣的select()函數(shù):
select(FD_SETSIZE,&rdfdset,NULL,NULL,NULL)
那么在select()返回后怎樣檢測(cè)set_fd數(shù)據(jù)結(jié)構(gòu)中描述符位的狀態(tài)呢?這就要調(diào)用函數(shù)FD_ISSET(),如果對(duì)應(yīng)文件描述符的狀態(tài)為"已準(zhǔn)備好"(即描述符位為"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);
-----------------------------------------------------------------
這一段代碼就實(shí)現(xiàn)從套接字usersockfd(用戶(hù)主機(jī))到套接字isosockfd(遠(yuǎn)端主機(jī))的無(wú)阻塞傳輸。而下一段代碼實(shí)現(xiàn)反方向的無(wú)阻塞傳輸:
-----------------------------------------------------------------
if (FD_ISSET(isosockfd,&rdfdset)) {
if ((iolen = read(isosockfd,buf,sizeof(buf))) <= 0)
break; /* zero length means the host disconnected */
write(usersockfd,buf,iolen);
-----------------------------------------------------------------
這樣就通過(guò)proxy實(shí)現(xiàn)了用戶(hù)主機(jī)與遠(yuǎn)端主機(jī)之間的通信。
對(duì)這段proxy代碼我只是寫(xiě)了一些自己的理解,大多數(shù)是一些函數(shù)的用法,這些都是linux網(wǎng)絡(luò)編程中一些最基礎(chǔ)的知識(shí),如果有不對(duì)的地方,還請(qǐng)各位大蝦批評(píng)指正。
posted @ 2009-11-17 21:14 石頭@ 閱讀(754) | 評(píng)論 (0) | 編輯 收藏