經典的Linux Socket 編程 示例代碼 (下)
摘自:http://fanqiang.chinaunix.net/a4/b7/20010810/1200001101.html
在例程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 <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()函數的定義為:
-----------------------------------------------------------------
/****************************************************************
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 <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; /* 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!=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; /* 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網絡編程中一些最基礎的知識,如果有不對的地方,還請各位大蝦批評指正。