APR分析-網絡IO篇
“這個世界如果沒有了網絡就好比沒有了石油、沒有了電一樣,是多么的可怕呀。”相信世界上已經有很多很多的人能夠同意這種觀點了,通過這個觀點也可以看出網絡在現代人們心中的地位。而運行在網絡節點上的網絡應用程序則是在幕后默默地為人們提供著服務。Apache
Server就是其中一個典型的代表。而APR網絡I/O庫則像磐石一樣支撐著Apache
Server的運行。
APR網絡I/O的源代碼的位置在$(APR_HOME)/network_io目錄下,本篇blog著重分析unix子目錄下的各.c文件內容,其相應頭文件為$(APR_HOME)/include/apr_network_io.h。
以程序員的視角來看待網絡,這樣我們可以忽略一些網絡的基礎概念。下面將循序漸進地接觸網絡,并說明APR是如何支持這些網絡概念的。
一、IP地址--主機通信
我們熟知的并且每天工作于其上的因特網是一個世界范圍的主機的集合,這個主機集合被映射為一個32位(目前)或者64位(將來)IP地址;而IP地址又被映射為一組因特網域名;一個網絡中的主機上的進程能通過一個連接(connection)和任何其他網絡中的主機上的進程通信。
1、IP地址存儲
在如今的IPV4協議中我們一般使用一個unsignedint來存儲IP地址,在UNIX平臺下,使用如下結構來存儲一個IP地址的值:
/* Internet address structure */
struct in_addr {
unsigned int s_addr; /* network byte order (big-endian) */
};
這里值得一提的是APR關于IP地址存儲的做法,看如下代碼:
#if (!APR_HAVE_IN_ADDR)
/**
* We need to make sure we always have an in_addr type, soAPR will just
* define it ourselves, if the platform doesn't provide it.
*/
struct in_addr {
apr_uint32_t? s_addr;
};
#endif
APR保證了其所在平臺上in_addr的存在。還有一點兒需要注意的是在in_addr中,s_addr是以網絡字節序存儲的。如果你的IP地址不符合條件,可通過調用一些輔助接口來做轉換,這些接口包括:
htonl : host to network long ;
htons : host to network short ;
ntohl : network to host long ;
ntohs : network to host short.
2、IP地址表示
我們平時看到的IP地址都是類似“xxx.xxx.xxx.xxx”這樣的點分十進制的。上面說過IP地址使用的是一個unsignedint整形數來表示。這樣就存在著一個IP地址表示和IP地址存儲之間的一個轉換過程。APR提供這一轉換支持,我們用一個例子來說明:
#include
#include
#include "apr_network_io.h"
#include "apr_arch_networkio.h"
int main(int argc, const char * const * argv, const char * const*env)
{
apr_app_initialize(&argc, &argv, &env);
char???presentation[100];
int???? networkfmt;
memset(presentation, 0,sizeof(presentation));
apr_inet_pton(AF_INET,"255.255.255.255", &networkfmt);
printf("0x%x/n", networkfmt);
apr_inet_ntop(AF_INET,&networkfmt, presentation, sizeof(presentation));
printf("presentation is %s/n", presentation);
apr_terminate();
return 0;
}
APR提供apr_inet_pton將我們熟悉的點分十進制形式轉換成一個整型數存儲的IP地址;而apr_inet_ntop則將一個存整型數存儲的IP地址轉換為我們可讀的點分十進制形式。這兩個接口的功能類似于系統調用inet_pton和inet_ntop,至于使用哪個就看你的喜好了^_^。
二、SOCKET --進程通信
前面提到過通過一個連接(connection)可以連接兩個internet不同或相同主機上的不同進程,這個連接是點對點的。而從Unix內核角度來看,SOCKET則是連接的一個端點。每個SOCKET都有一個地址,其地址由主機IP地址和通訊端口號組成。一個連接有兩個端點,這樣一個連接就可以由一個SOCKET對唯一表示了。這個SOCKET對是這個樣子的(cliaddr:cliport,
servaddr:servport)。
那么在應用程序中我們如何獲取和使用這一互聯網上的進程通訊利器呢?每個平臺都為應用程序提供了一套SOCKET編程接口,APR又在不同平臺提供的接口之上進行了封裝,使代碼可以在不同平臺上編譯運行,而且易用性也有所提高。
1、SOCKET描述符
SOCKET屬于系統資源,我們必須通過系統調用來申請該資源。SOCKET資源的申請類似于FILE,在使用文件時我們通過調用open函數獲取文件描述符,類似我們也可通過調用下面的接口來獲取SOCKET描述符:
int socket(int domain, int type, int protocol);
從Unix程序的角度來看,SOCKET就是一個有相應描述符的打開的文件。在APR中我們可以通過調用apr_socket_create來創建一個APR自定義的SOCKET對象,該SOCKET結構如下:
/* apr_arch_networkio.h */
struct apr_socket_t {
apr_pool_t *cntxt;
int socketdes;
int type;
int protocol;
apr_sockaddr_t *local_addr;
apr_sockaddr_t *remote_addr;
apr_interval_time_t timeout;
#ifndef HAVE_POLL
int connected;
#endif
int local_port_unknown;
int local_interface_unknown;
int remote_addr_unknown;
apr_int32_t options;
apr_int32_t inherit;
sock_userdata_t *userdata;
#ifndef WAITIO_USES_POLL
/* if there is a timeout set, then this pollsetis used */
apr_pollset_t *pollset;
#endif
};
該結構中的socketdes字段其實是真正存儲由socket函數返回的SOCKET描述符的,其他字段都是為APR自己所使用的,這些字段在Bind、Connect等過程中使用。另外需要提及的就是要分清SOCKET描述符和SOCKET地址(IP地址,端口號),前者是系統資源,而后者用來描述一個連接的一個端點的地址。SOCKET描述符可以代表任意的SOCKET地址,也可以綁定到某個固定的SOCKET地址上(在后面有說明)。我們如果不顯式將SOCKET描述符綁定到某SOCKET地址上,系統內核就會自動為該SOCKET描述符分配一個SOCKET地址。
2、SOCKET屬性
還是與文件對比,在文件系統調用中有一個fcntl接口可以用來獲取或設置已分配的文件描述符的屬性,如是否Block、是否Buffer等。SOCKET也提供類似的接口調用setsockopt和getsockopt。在APR中等價于該功能的接口是apr_socket_opt_set和apr_socket_opt_get。APR在apr_network_io.h中提供如下SOCKET的參數屬性:
#define APR_SO_LINGER???????1??? /**< Linger */
#define APR_SO_KEEPALIVE????2??? /**< Keepalive */
#defineAPR_SO_DEBUG????????4??? /**< Debug */
#define APR_SO_NONBLOCK?????8??? /**< Non-blocking IO */
#define APR_SO_REUSEADDR???? 16??/**< Reuse addresses */
#define APR_SO_SNDBUF???????64?? /**< Send buffer */
#define APR_SO_RCVBUF???????128? /**< Receive buffer */
#define APR_SO_DISCONNECTED? 256? /**< Disconnected*/
... ...
另外從上面這些屬性值(都是2的n次方)可以看出SOCKET也是使用一個屬性控制字段中的“位”來控制SOCKET屬性的。
再有APR提供一個宏apr_is_option_set來判斷一個SOCKET是否擁有某個屬性。
3、Connect、Bind、Listen、Accept
--建立連接
這里不詳述C/S模型了,只是說說APR支持C/S模型的一些接口。
(1) apr_socket_connect
客戶端連接服務器端的唯一調用就是connect,connect試圖建立一個客戶端進程與服務器端進程的連接。apr_socket_connect的參數分別為客戶端已經打開的一個SOCKET以及指定的服務器端的SOCKET地址(IP
ADDR : PORT)。apr_socket_connect內部實現的流程大致如以下代碼:
apr_socket_connect
{
do {
rc =connect(sock->socketdes,
(const struct sockaddr *)&sa->sa.sin,
sa->salen);
} while (rc == -1 && errno ==EINTR);?? -------- (a)
if ((rc == -1) && (errno == EINPROGRESS || errno ==EALREADY)
&& (sock->timeout > 0)) {
rc =apr_wait_for_io_or_timeout(NULL, sock, 0); --------- (b)注[1]
if (rc != APR_SUCCESS){
return rc;
}
if (rc == -1 && errno != EISCONN) {
returnerrno;?? --------- (c)
}
初始化sock->remote_addr;
... ...
}
對上述代碼進行若干說明:
(a)執行系統調用connect連接服務器端,注意這里做了防止信號中斷的處理,這個技巧在以前的文章中提到過,這里不詳述;
(b)如果系統操作正在進行中,調用apr_wait_for_io_or_timeout進行超時等待;
(c)錯誤返回,前提errno不是表示已連接上。
一旦apr_socket_connect成功返回,我們就已經成功建立了一個SOCKET對,即一個連接。
(2) apr_socket_bind
Bind、Listen和Accept這三個過程是服務器端用于接收“連接”的必經之路。其中Bind就是告訴操作系統內核顯式地為該SOCKET描述符分配一個SOCKET地址,這個SOCKET地址就不能被其他SOCKET描述符占用了。在服務器編程中Bind幾乎成為了“必選”之調用,因為一般服務器程序都有自己的“名氣很大”的SOCKET地址,如TELNET服務端口號23等。apr_socket_bind也并未做太多的工作,只是簡單的調用了bind系統接口,并設置了apr_socket_t結構的幾個local_addr字段。
(3) apr_socket_listen
按照《Unix網絡編程Vol1》的說法,SOCKET描述符在初始分配時都處于“主動連接”狀態,Listen過程將該SOCKET描述符從“主動連接”轉換為“被動狀態”,并告訴內核接受該SOCKET描述符的連接請求。apr_socket_listen的背后直接就是listen接口調用。
(4) apr_socket_accept
Accept過程在“被動狀態”SOCKET描述符上接受一個客戶端的連接,這時系統內核會自動分配一個新的SOCKET描述符,內核為該描述符自動分配一個SOCKET地址,來代表這條連接的服務器端。注意在SOCKET編程接口中除了socket函數能分配新的SOCKET描述符之外,accept也是另外的一個也是唯一的一個能分配新的SOCKET描述符的系統調用了。apr_socket_accept首先在pool中分配一個新的apr_socket_t結構變量,然后調用accept,并設置新變量的各個字段。
4、Send/Recv --數據傳輸
網絡通信最重要的還是數據傳輸,在SOCKET編程接口中最常見的兩個接口就是recv和send。在APR中分別有apr_socket_recv和apr_socket_send與前面二者對應。下面逐一分析。
(1) apr_socket_recv
首先來看看apr_socket_recv的實現過程:
apr_socket_recv
{
if (上次調用apr_socket_recv沒有讀完所要求的字節數)
{ ----------(a)
設置sock->options;
goto do_select;
}
do {
rv =read(sock->socketdes, buf, (*len)); ------ (b)
} while (rv == -1 && errno ==EINTR);
if ((rv == -1) && (errno == EAGAIN || errno ==EWOULDBLOCK)
&& (sock->timeout > 0)) {
do_select:
arv =apr_wait_for_io_or_timeout(NULL, sock, 1);
if (arv !=APR_SUCCESS) {
*len = 0;
return arv;
}
else {
do {
rv = read(sock->socketdes, buf, (*len));
} while (rv == -1 && errno == EINTR);
}
}? ------------ (c)
設置(*len)和sock->options;
-------------(d)
... ...
}
針對上面代碼進行簡單說明:
(a)一次apr_socket_recv調用完全有可能沒有讀完所要求的字節數,這里做個判斷以決定是否繼續讀完剩下的數據;
(b)調用read讀取SOCKET緩沖區數據,注意這里做了防止信號中斷的處理,這個技巧在以前的文章中提到過,這里不詳述;
(c)如果SOCKET操作正在忙,我們調用apr_wait_for_io_or_timeout等待,直到SOCKET可用。這里我覺得好像有個問題,想象一下如果上一次SOCKET的狀態為APR_INCOMPLETE_READ,那么重新調用apr_socket_read后在SOCKET屬性中去掉APR_INCOMPLETE_READ,然后進入apr_wait_for_io_or_timeout過程,一旦apr_wait_for_io_or_timeout失敗,那么就直接返回了。而實際上SOCKET仍然應該處于APR_INCOMPLETE_READ狀態,而下次再調用apr_socket_read就直接進入一輪完整數據的讀取過程了,不知道這種情形是否能否發生。
(d)將(*len)設置為實際從SOCKET Buffer中讀取的字節數,并根據這一實際數據與要求數據作比較來設置sock->options。
(2) apr_socket_send
apr_socket_send負責發送數據到SOCKET Buffer,其實現的方式與apr_socket_recv大同小異,這里就不分析了。
三、小結
APR Network I/O中還有對Multicast的支持,由于平時不常接觸,這里不分析了。
注[1]:
/* in errno.h */
#define EISCONN????????133???? /* Socket is already connected */
#define EALREADY???????149???? /* operation already in progress */
#define EINPROGRESS????150???? /* operation now in progress */