TCP/IP 協定與 Internet 網路:第八章 TCP Socket 程式介面  上一頁           下一頁

 

翻轉工作室:粘添壽

 

8-8 Socket 多工方式

Socket 多工方式是表示一個 Socket 可否同時接受遠端多個通訊端點的連線要求,或是一個應用程式同時可以監視多個 Socket 通訊端點的連線。基本上,利用非同步傳輸模式的通訊鏈路,就可以達到此目的。但非同步傳輸的訊號(Signal)處理必須透過系統中斷程式,來告知使用者已發生事件(資料進入),如此牽涉到中斷系統,導致使用者編寫應用程式上較為困難。因此,我們可以在不變更系統環境之下,採用下列兩種方法達到 Socket 多工的目的:

Socket 被執行 listen() 系統呼叫(虛擬電路方式),而進入聆聽狀態之後,不要即時執行 read() 呼叫(或 recvfrom() 呼叫)。而先利用 select() 去詢問連線是否有資料進來,如果資料已進入,在執行 read() 呼叫來讀取該資料,如此執行 read() 呼叫就不會無窮的等待著。因此,一個主程式就可以監視多個 Socket 端點要求連線,或處理多個端點的傳輸資料。

另一種方法是 Socket 隨時監視是否有訊號進入,也就是直接執行 read() 系統呼叫等待接收訊號。但當收到遠端連接訊號後,立即利用 fork() 系統呼叫產生子程序(Child Process),由子程序負責連線處理的工作,而原來主程序(Main Process)再回去監視是否有其它遠端要求連線,如此,便可以達到一個 Socket 的通訊端點,可以接受多個使用者要求連線。一般 Internet 上的 Client/Server 架構上的伺服器都可同時接受多個使用者連線,下一節在詳加介紹其製作方法。

如將上述兩種方法可以混合使用,便可以達到 Socket 多工的主要功能:『一個主程式可以監視或處理多個 Socket 端點連線,並且一個 Socket 端點可以接受多個使用者的連線』,以下分別介紹這兩種多工方式:

(1) Socket 被執行 listen() 系統呼叫(虛擬電路方式),而進入聆聽狀態之後,不要即時執行 read() 呼叫(或 recvfrom() 呼叫)。而先利用 select() 去詢問連線是否有資料進來,如果資料已進入,在執行 read() 呼叫來讀取該資料,如此執行 read() 呼叫就不會無窮的等待著。因此,一個主程式就可以監視多個 Socket 端點要求連線,或處理多個端點的傳輸資料。

(2) 另一種方法是 Socket 隨時監視是否有訊號進入,也就是直接執行 read() 系統呼叫等待接收訊號。但當收到遠端連接訊號後,立即利用 fork() 系統呼叫產生子程序(Child Process),由子程序負責連線處理的工作,而原來主程序(Main Process)再回去監視是否有其它遠端要求連線,如此,便可以達到一個 Socket 的通訊端點,可以接受多個使用者要求連線。一般 Internet 上的 Client/Server 架構上的伺服器都可同時接受多個使用者連線,下一節在詳加介紹其製作方法。

        如將上述兩種方法可以混合使用,便可以達到 Socket 多工的主要功能:『一個主程式可以監視或處理多個 Socket 端點連線,並且一個 Socket 端點可以接受多個使用者的連線』,以下分別介紹這兩種多工方式:

8-8-1 Socket 多工輸入/輸出

        Socket 『多工輸入/輸出』(Multiple I/O是表示一個主程式可允許多個 Socket 端資料的進出。早期 Unix 系統大多利用 poll() 系統呼叫來監視多個檔案識別碼,首先將一串列的檔案識別碼加入 poll() 函數內。執行 poll() 後,在這些檔案識別碼之中有任一個事件發生,poll() 函數就會回應給主程式。目前大部分系統都採用 select() 系統呼叫來取代 poll(),而檔案識別碼也相當於建構 Socket 所產生的 Socket ID,它的工作模式如圖 8-7 所示。簡單的說,雖然每一應用程式都銜接到各自的 Socket 埠口上,但利用一個監督程式來監視若干個 Socket 埠口是否有連線要求,而並非每一應用都各自監督埠口。我們的做法是用一個集合變數 fd_set 來存放被監視的 Socket 端點的識別碼:fd_set = (socket_1, socket_2, socket_3, socket_4, socket_5),而其中任何一個 Socket 有事件發生(Read, Write, Exception),便會回應給主程式,再由主程式來處理該事件。

8-7 Socket 多工讀寫的運作

select() 系統呼叫的格式如下:

#include <sys/types.h>

#include <sys/time.h>

 

struct timeval *timeout;

int         numfds;

fd_set      *rfds;

fd_set      *wfds;

fd_set       *efds;

int          numsel;

 

numsel = select(numfds, rfds, wfds, efds, timeout);

各參數功能如下:

rfds此參數的作用在告訴系統有哪些 Socket 通訊端點準備作讀取的動作(Ready to Read)。參數 rfd 是以 fd_set 資料結構存放,使用者將已開啟而準備接收連線或資料的 Socket,存放在 rfd 上以執行 select() 來告訴系統。譬如 rfd = {32, 45, 12},表示準備接收 Socket 3245 12 的連線訊號或資料,這也是監視這幾個傳輸埠口的意思。當系統監視這幾個埠口有訊號進來,還是以 rfd 參數回應給使用者,使用者由 rfd 參數中知道有哪幾個 Socket 已有訊號進來,而必須去讀取(read())。

wfds告訴系統有哪幾個 Socket 端點準備傳送資料(Ready to Write),參數 wfd 也是 fd_set 的資料結構。譬如 wfd = {24, 12, 45},表示 Socket 2412 45 準備傳送資料。使用者利用 select() 系統呼叫告訴系統,系統就處理一些下層連線的動作。如果下層連線完成,便可以傳送資料時,但系統也是利用 wfd 參數回應給使用者,使用者便可以由回應的 wfd 中得知哪幾個 Socket 已可以發送資料,如此就不會被『阻斷』(Blocking

efds功能如同 efds rfds,但它是針對異常事件(Exception Event)的監視與回應。

timeout適用於設定執行 select() 等待的時間值。當執行 select() 呼叫時,有時候並無法即時回應所監視 Socket 的反應,timeout 表示可以最長的時間,簡單的說,表示 select() Blocking 的時間。timeout 是資料結構 timeval 的指標,其結構如下:

struct timeval {

long       tv_sec;    /* seconds       */

long       tv_usec   /* microseconds   */

};

其中 tv_sec 表示等待的秒數;而 tv_usec 為微秒數。

numfds表示此次執行 select() 中最大的 Socket 的識別值(Socket ID)加一。

numsel執行 select() 的回應值,表示發生幾個事件的總和,也就是 rfdswfds efds Socket ID 數目的總和。如 numsel = 1 表示執行錯誤。

select() 系統呼叫中的 timeout 值有下列三種執行方式:

timeout 是空值(Null)指標:select() 呼叫會無窮盡的等待,直到 rfdswfds efds 中有任何一個事件發生,才會返回。

timeout 不是空值指標,但 tv_sec tv_usec 皆為 0select() 會檢查所指定的 Socket 一次,不論是否有事件發生,都會立即返回。

timeout 不是空值指標,且 tv_sec tv_used 都有值: select() 檢查所指定的 Socket 後,如有事件發生會及時返回,否則會等待 Socket 的事件,直到所指定時間完了,才會返回。

        在執行 select() 後返回的 rfdswfds efds 內所儲存的 Socket ID,表示這些 Socket 都必須進一步的處理。執行前後這三個指標變數的內容含義不同,執行前表示必需偵測的 Socket ID;執行後表示有發生事件的 Socket ID,因此,執行程式之前必須將原有的指標內容保存,才不會被覆蓋。但 rfdswfds efds 都是 fd_set 的指標變數,在包含檔 sys/types.h 中描述。fd_set 的內容也是整數變數,利用整數中每一位元值表示相對應的 Socket ID,如果某一位元被設定為 1,則表示該 Socket 被設定。。另外,為了讓使用者方便設定和測試 rfdswfds efdsSocket Library 提供一系列的巨集函數來處理,以減低系統發展的複雜性,巨集函數如下:

#include <sys/types.h>

int fd;                /* Socket ID  */

fd_set *fdset;          /* fdset fd_set 的指標變數 */

FD_ZERO(fdset);      /* fdest 內所有位元清除為 0 */

FD_SET(fd, fdset);     /* fdset 中的 fd 位元設定為 1*/

FD_CLR(fd, fdset);     /* fdset fd 位元清除為 0 */

FD_ISSET(fd, fdset);    /* 測試 fdset fd 位元是否被設定為 1 */

為了說明這些巨集函數的使用方法,假設已有準備讀取的 Socket {2, 5, 7}、寫入為 {0, 2, 5}、異常事件為 {5, 8},其設定如下:

fd_set *rfds, *wfds;*efds;

FD_ZERO(rfds); FD_ZERO(wfds); FD_ZERO(efds);  /* clear to fd_set*/

FD_SET(2, rfds); FD_SET(5, rfds); FD_SET(7, rfds);  /* ready to read */

FD_SET(0, wfds); FD_SET(2, wfds); FD_SET(5, wfds); /* ready to write */

FD_SET(5, efds); FD_SET(8, efds);               /* ready to exception */

8-8-2 Socket 多工連線

        『多工連線』(Multiple Connection表示一個 Socket 通訊端點可同時接受多個連線,一般 Client/Server 架構的伺服器所提供的服務,就必須具有此功能,才能服務多個使用者。Socket 的多工連線的運作方式如圖 8-8 所示。圖中伺服器端有一個應用程式,隨時聆聽 Socket163.15.2.30:80)是否有連線要求。當客戶端(163.15.2.45:3452)有連線過來時,伺服器端接收到連線要求,便以 fork() 系統呼叫產生一個子程式(A 程式),並將系統環境複製(dup())到子程式,再由子程式負責和客戶端通訊。監督程式便關閉自己的連線端點,並回到繼續監督的狀況下,繼續等待是否有新的連線要求。

8-8 Socket 多工連線的運作方式

8-9 為虛擬電路(TCP 連線)的多工連線範例。但在這種情況之下,監督程式只能監視一個 Socket 通訊端點,一般都應用於使用較頻繁的伺服軟體上,譬如,網頁伺服器或郵件伺服器等等。但在一般網路上有許多使用率較低的伺服軟體,也必須隨時監視連線的要求,如果每個伺服器都自行監視,那會增加系統的複雜性,因此為了簡化系統的管理,可將多個 Socket 由一個監督程式來監視即可,但它接收到連線時,再產生子程式來處理它的連線動作,如此就能達成 Socket 的多工連線的功能。其實要達到此目的也不是困難,只要將圖 8-4 Server 端加入 fork() dup() 功能呼叫即可,加入後具有多工連線的功能如圖 8-9 所示,我們在下一節將以 xinetd 為範例來說明製作過程。

8-9 Socket 多工連線的功能圖

8-8-3 Socket 多工範例 xinetd

一般在 Unix/Linux 系統下隨時有許多服務程式(Daemon)在執行,譬如,TelnetFTPTFTP 等等,如果這些服務程式都有專門的監督程式在準備接受連線的話,那整個系統環境有就便得非常複雜,而且在管理上也較困難。因此我們可以將一些較少使用或較需要特殊處理的服務程式,由一個專門的監督程式來管理,這就是 inetd/xinetd 服務程式。有關 inetd xinetd 的功能及設定已在本書第九章(9-9 節)中介紹,這裡只針對 xinetd 的工作原理加以解說。

(A) xinetd 的多工原理

xientd 守護程式是真正符合 Socket 的多工要求:『一個主程式可以監視或處理多個 Socket 端點連線,並且每一個 Socket 端點可以接受多個使用者的連線』。也就是說,xientd 符合『多工輸入出』『多工連線』的要求,我們用圖 8-10 來說明它的多工原理。xientd 守護程式同時監督若干個應用程式(如 /etc/xinetd.d 目錄下),每一應用程式銜接到各自的傳輸埠口(如 /etc/services 檔案描述),譬如,telnet 連接到 23/tcptftp 連接到 69/udp 等等。當 xientd 監視到某一傳輸埠口(如 23/tcp)有連線要求時(圖中訊號 1),便呼叫該埠口的服務程式(如 in.telnet 服務程式),並將所有連線處理工作轉交給服務程式,再由服務程式(如 in.telnetd)直接和客戶端通訊(如圖中訊號 2)。xinetd 便釋放掉該處理動作,再回到監視埠口的工作,如此,xientd 便可達到多工輸入/輸出和多工連線的功能,我們也可以發現圖 8-10 是圖 8-7 8-8 的結合體。

8-10 xinetd 的多工原理

(B) xinetd 的運作程序

當系統啟動時(/etc/rc.d),xinetd 呼叫 /etc/xinted.d 下所有的服務程式,如果程式設定檔中 disable = no,表示該服務程式會被啟動,運作程序如圖 8-11 所示,說明如下:

8-11 xinetd 的運作程序

(1)  /etc/rc.d/init.d 啟動 xinetd 服務程式。

(2)  xinetd 服務程式呼叫啟動 /etc/xinetd.d 目錄下的服務程式(如 telnet)。如果該程式設定檔有設定啟動(disable = no),就依照 /etc/services 內所設定之服務程式的傳輸埠口。以下是 /etc/services 的部分檔案內容,可以看出 Telnet 服務的傳輸埠口為 23,而可採用 TCP UDP 傳輸。每一個服務程式呼叫的程序就如圖 8-11 中虛線所包圍的部分,如果在設定內規劃為 TCP 連線(socket_type = stream),才需要 listen() 系統呼叫。

ftp-data  20/tcp

ftp-data  20/udp

ftp      21/tcp

ftp      21/udp

ssh      22/tcp                 # SSH Remote Login Protocol

ssh      22/udp                 # SSH Remote Login Protocol

telnet    23/tcp

telnet    23/udp

(3) xinetd 將各個服務程式所產生的檔案描述子(File Descriptor)填入 select() 函數的 rfds 變數內,並執行 select() 監督所有開啟的 Socket 埠口。

(4) select() 接收到遠端連線要求,便執行 accept() 接受連線,而得到一個連線的檔案描述子(表示遠端連線埠口)。此時 xinetd fork() 呼叫產生一子程序,而由子程序負責連線的處理。子程序首先關閉除了此連線外,其它所有的檔案描述子,並以連線的檔案描述子呼叫 dup2() 產生檔案描述子 01 2,再關閉原來連線的檔案描述子。如果設定檔中所表示此程序所有人不是系統管理者(user = root),便再呼叫 setgid() setuid() 改變程序所有人。其它還有許多關於安全性的設定方式,我們不再另外敘述請參考 xinetd 的使用手冊。

(5) 子程式以 exec() 呼叫該連線的服務程式,譬如 Telnet 服務程式為 /usr/sbin/in.telnetd

(6) 如果是 TCP 連線(stream),則關閉原來連線(由子程式取代了),再繼續監督所有埠口(執行 select());否則直接再監督埠口是否有其連線進來。

        在服務程式的設定檔(如 /etc/xinetd.d/telnet)內設定參數 wait,是這表示主程式 xinetd 是否等待子程式執行完畢後再回到監督狀態下。一般 TCP 連線都將其設定在沒有等待(wait = no),表示呼叫子程式後,主程式直接回到監督狀態,這是因為連線已建立完成,爾後子程式只要依照連線傳輸資料就不會遺失。但如果使用 UDP 協定,它並沒有建立連線,在當主程式接收到第一筆資料以後,並不能保證還有沒有下一筆同一來源的資料到達。如果主程式直接回到監督狀態,他收到下一筆資料會再重新 fork() 一個子程式,造成重複的連線狀態,因此在 UDP 協定大多設定為有等待(wait = yes)。設定為有等待,有時候會因為每一連線的不正常,造成整個 xinetd 處理效率會不幅度降低。為了克服這種問題,目前在 Internet 網路上以 UDP 協定傳送訊息,儘可能限制在一個封包以內(512 Bytes),便可減少許多困擾。

 

 

<GOTOP>