一般認(rèn)為Web服務(wù)器程序是一個長時間運(yùn)行的程序(即所謂的守護(hù)進(jìn)程,daemon )报辱,它只響應(yīng)來自網(wǎng)絡(luò)的請求時才發(fā)送網(wǎng)絡(luò)消息召噩。協(xié)議的另一端是Web客戶程序父虑,如某種瀏覽器,與服務(wù)器進(jìn)行通信總是由客戶進(jìn)程發(fā)起授药。在設(shè)計網(wǎng)絡(luò)應(yīng)用時士嚎,確定總是由客戶發(fā)起請求往往能夠簡化協(xié)議和程序本身。當(dāng)然一些較為復(fù)雜的的網(wǎng)絡(luò)應(yīng)用還需要異步回調(diào)通信悔叽,也就是由服務(wù)器向客戶端發(fā)起請求信息莱衩。
客戶與服務(wù)器之間是通過某個網(wǎng)絡(luò)協(xié)議通信的,但實(shí)際上娇澎,這樣的通信通常涉及多個網(wǎng)絡(luò)協(xié)議層笨蚁。這里聚焦的是:TCP/IP協(xié)議族,也稱為網(wǎng)絡(luò)協(xié)議族趟庄。舉例來說括细,Web客戶與服務(wù)器之間使用TCP通信,TCP又轉(zhuǎn)而使用IP通信戚啥,IP再通過某種形式的數(shù)據(jù)鏈路層通信奋单。
在圖中,客戶與服務(wù)器之間的信息流在其中一端是向下通過協(xié)議棧的猫十,跨越網(wǎng)絡(luò)后览濒,在另一端則是向上通過協(xié)議棧的呆盖。另外注意,TCP/IP協(xié)議是內(nèi)核中協(xié)議棧的一部分贷笛。(回憶:在LINUX的進(jìn)程中应又,LINUX系統(tǒng)是在內(nèi)核態(tài)的,內(nèi)核態(tài)被所有的進(jìn)程所共享乏苦,而TCP/IP協(xié)議屬于LINUX系統(tǒng)的網(wǎng)絡(luò)協(xié)議株扛,也就是內(nèi)核中的協(xié)議棧,具體的linux內(nèi)核相關(guān)的學(xué)習(xí)在linux 系統(tǒng)文集中)
同一網(wǎng)絡(luò)應(yīng)用的客戶和服務(wù)器當(dāng)處于不同局域網(wǎng)時邑贴,不同的局域網(wǎng)使用路由器連接到廣域網(wǎng):
根據(jù)原書的介紹席里,代碼里面所使用的大多數(shù)系統(tǒng)函數(shù),都定義了各自的包裹函數(shù)拢驾。而且可以使用這些包裹函數(shù)來檢查錯誤奖磁,輸出適當(dāng)?shù)南ⅲ约霸诔鲥e時終止程序的運(yùn)行繁疤。
下面講代碼了咖为,這個是客戶端顯示服務(wù)器的當(dāng)前時間和日期的簡單代碼(base1)
#include "unp.h" //該頭文件包含大部分網(wǎng)絡(luò)程序都需要的許多系統(tǒng)頭文件,并定義了所用到的各種常量值(如MAXLINE).
int main(int argc, char **argv) // main 函數(shù)的定義稠腊,形參是命令行參數(shù)
{
int sockfd, n;
char recvline[MAXLINE + 1];
struct sockaddr_in servaddr;
if (argc != 2) err_quit("usage: a.out <IPaddress>");
// 調(diào)用 socket 函數(shù)創(chuàng)建一個 ipv4 字節(jié)流套接字躁染,返回一個小整數(shù)描述符褪迟,以后的所有函數(shù)調(diào)用(如隨后的connect 和 read)就用該描述符來標(biāo)識這個套接字歼冰。
// 如果socket函數(shù)調(diào)用失敗,我們就調(diào)用自己的err_sys 函數(shù)放棄程序運(yùn)行曹动。
// 自定義的 **err_sys 函數(shù)** 輸出我們作為參數(shù)提供的出錯信息以及所發(fā)生的系統(tǒng)錯誤的描述叹放。后面會具體描述這些函數(shù)
if ( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) err_sys("socket error");
// 使用bzero把整個結(jié)構(gòu)清零后饰恕,置地址族為AF_INET,端口號為13.
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(13); /* daytime server */
if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr)<= 0) err_quit("inet_pton error for %s", argv[1]);
if (connect(sockfd, (SA *) &servaddr, sizeof(servaddr))< 0) err_sys("connect error");
// 兩個左括號間加一個空格井仰,提示比較運(yùn)算符的左側(cè)同時也是一個賦值運(yùn)算
while ( (n = read(sockfd, recvline, MAXLINE)) > 0) {
recvline[n] = 0; /* null terminate */
if (fputs(recvline, stdout) == EOF) err_sys("fputs error");
}
if (n < 0) err_sys("read error");
exit(0);
}
細(xì)節(jié)
socket模塊定義了一些常量參數(shù)埋嵌,用來指定 socket的的地址族、socket的類型俱恶、以及支持的TCP/IP協(xié)議雹嗦。
socket.socket([family[, type[, proto]]]):根據(jù)指定的 地址族 和 套接字類型、協(xié)議編號(默認(rèn)為0)來創(chuàng)建 套接字對象合是。AF_INET 對應(yīng)的 IPV4, AF_INET6 對應(yīng)的 IPV6了罪。具體參數(shù)見下表:
1、我們把服務(wù)器的IP地址和端口號填入一個網(wǎng)際套接字地址結(jié)構(gòu)(一個名為 servaddr 的 sockaddr_in 結(jié)構(gòu)變量)端仰。這里捶惜,端口號為13,是時間獲取服務(wù)器的眾所周知端口荔烧,支持該服務(wù)器的任何 TCP/IP 主機(jī)都使用這個端口號吱七。
2汽久、而網(wǎng)際套接字地址結(jié)構(gòu)中 IP地址 和 端口號 這兩個成員必須使用特定格式,為此我們調(diào)用庫 htons(“主機(jī)到網(wǎng)絡(luò)短整數(shù)”)去轉(zhuǎn)換二進(jìn)制端口號踊餐,又調(diào)用庫函數(shù) inet_pton(“呈現(xiàn)形式到整數(shù)”)去把 ASCII命令行參數(shù)(例如運(yùn)行本例子所用的206.168.112.96)轉(zhuǎn)換為合適的格式景醇。
3、bzero 不是一個ANSI C函數(shù)吝岭,幾乎所有支持套接字API的廠商都提供bzero三痰,如果沒有,那么可以使用unp.h頭文件中提供的該函數(shù)的宏定義窜管。inet_pton 函數(shù)是一個支持 IPV6 的新函數(shù)散劫,以前的代碼使用 inet_addr 函數(shù)來把ASCII點(diǎn)分十進(jìn)制數(shù)串變換為正確的格式,不過它有不少局限幕帆,而這些局限在inet_pton中都得以糾正获搏。
4、connect 函數(shù)應(yīng)用于一個TCP套接字時失乾,將與由它的第二個參數(shù)指向的套接字地址結(jié)構(gòu)指定的服務(wù)器建立一個TCP連接常熙。該套接字地址結(jié)構(gòu)的長度也必須作為該函數(shù)的第三個參數(shù)指定,對于網(wǎng)際套接字地址結(jié)構(gòu)碱茁,我們總是使用C語言的sizeof操作符由編譯器來計算這個長度裸卫。
5、在頭文件unp.h中纽竣,我們使用#define 把SA定義為struct sockaddr墓贿,通用套接字地址結(jié)構(gòu)。每當(dāng)一個套接字函數(shù)需要一個指向某個套接字地址結(jié)構(gòu)的指針時蜓氨,這個指針必須強(qiáng)制類型轉(zhuǎn)換成一個指向通用套接字地址結(jié)構(gòu)的指針募壕。
6、fputs()函數(shù)用于將指定的字符串寫入到文件流中语盈,其原型為:
int fputs(char * string, FILE * stream);【參數(shù)】string為將要寫入的字符串,stream為文件流指針缰泡〉痘模【返回值】成功返回非負(fù)數(shù),失敗返回EOF棘钞。fputs()從string的開頭往文件寫入字符串缠借,直到遇見結(jié)束符 '\0','\0' 不會被寫入到文件中宜猜。注意:fputs()可以指定輸出的文件流泼返,不會輸出多余的字符;puts()只能向 stdout 輸出字符串姨拥,而且會在最后自動增加換行符绅喉。
協(xié)議無關(guān)性
上面的程序是與 IPv4 協(xié)議相關(guān)的:我們分配并初始化一個 sockaddr_in 類型的結(jié)構(gòu)渠鸽,把該結(jié)構(gòu)的協(xié)議族成員設(shè)置為AF_INET, 并指定 socket 函數(shù)的第一個參數(shù)為 AF_INET.
為了讓圖1-5的程序能夠在IPv6上運(yùn)行,我們必須修改這段代碼柴罐。下面是一個能在IPV6上運(yùn)行的版本(base2):
#include "unp.h"
int main(int argc, char **argv)
{
int sockfd, n;
struct sockaddr_in6 servaddr;
char recvline[MAXLINE + 1];
if (argc != 2) err_quit("usage: a.out <IPaddress>");
if ( (sockfd = socket(AF_INET6, SOCK_STREAM, 0)) < 0) err_sys("socket error");
bzero(&servaddr, sizeof(servaddr));
servaddr.sin6_family = AF_INET6;
servaddr.sin6_port = htons(13); /* daytime server */
if (inet_pton(AF_INET6, argv[1], &servaddr.sin6_addr) <= 0) err_quit("inet_pton error for %s", argv[1]);
if (connect(sockfd, (SA *) &servaddr, sizeof(servaddr)) < 0) err_sys("connect error");
while ( (n = read(sockfd, recvline, MAXLINE)) > 0) {
recvline[n] = 0; /* null terminate */
if (fputs(recvline, stdout) == EOF) err_sys("fputs error");
}
if (n < 0) err_sys("read error");
exit(0);
}
不足之處:
- 這種寫法是與ipv6協(xié)議相關(guān)的寫法徽缚,更好的做法是編寫協(xié)議無關(guān)的程序。
- 這里的不足之處革屠,用戶必須以點(diǎn)分十進(jìn)制數(shù)格式給出服務(wù)器的IP地址(如適合于IPV4版本的206.168.112.219)凿试。而我們一般都習(xí)慣于用名字代替數(shù)字。
錯誤處理:包裹函數(shù)
任何現(xiàn)實(shí)世界的程序都必須檢查每個函數(shù)調(diào)用是否返回錯誤似芝。在上面的程序中那婉,我們檢查socket、inet_pton党瓮、connect详炬、read 和 fputs函數(shù)是否返回錯誤,當(dāng)發(fā)生錯誤時麻诀,就調(diào)用我們自己的err_quit 或 err_sys 函數(shù)輸出一個出錯信息并終止程序的運(yùn)行痕寓。但是個別情況下,當(dāng)這些函數(shù)返回錯誤時蝇闭,我們想做的事并非簡單地終止程序的運(yùn)行呻率。
于是,我們通過定義包裹函數(shù)來縮短程序呻引。每個包裹函數(shù)完成實(shí)際的函數(shù)調(diào)用礼仗,檢查返回值,并在發(fā)生錯誤時終止進(jìn)程逻悠。我們約定包裹函數(shù)名是實(shí)際函數(shù)名的首字母大寫形式元践。例如在語句:
sockfd = Soccket(AF_INET,SOCKET_STREAM,0); 中,函數(shù)Socket 是函數(shù) socket的包裹函數(shù)童谒,如圖:
/* include Socket */
int Socket(int family, int type, int protocol)
{
int n;
if ( (n = socket(family, type, protocol)) < 0)
err_sys("socket error");
return(n);
}
線程函數(shù)遇到錯誤時并不設(shè)置標(biāo)準(zhǔn)Unix的errno變量单旁,而是把errno的值作為函數(shù)返回值返回調(diào)用者。這意味著每次調(diào)用以pthread_開頭的某個函數(shù)時饥伊,我們必須分配一個變量來存放函數(shù)返回值象浑,以便在調(diào)用err_sys前把errno變量設(shè)置成該值。
/* include Pthread_mutex_lock */
void Pthread_mutex_lock(pthread_mutex_t *mptr)
{
int n;
if ( (n = pthread_mutex_lock(mptr)) == 0) return;
errno = n;
err_sys("pthread_mutex_lock error");
}
/* end Pthread_mutex_lock */
除非必須檢查某個確定的錯誤是否發(fā)生琅豆,并以不同于終止進(jìn)程的其他某種方式處理它愉豺,否則就使用這些包裹函數(shù)。
下面是匹配的時間獲取服務(wù)器程序(base3)
#include "unp.h"
#include <time.h>
int main(int argc, char **argv)
{
int listenfd, connfd;
struct sockaddr_in servaddr;
char buff[MAXLINE];
time_t ticks;
// 調(diào)用 socket 函數(shù)調(diào)用一個 ipv4 字節(jié)流套接字茫因,返回一個小整數(shù)描述符
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
// 填寫網(wǎng)際套接字地址結(jié)構(gòu)并調(diào)用bind函數(shù)
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET
// 我們指定IP地址為INADDR_ANY蚪拦,這樣要是服務(wù)器主機(jī)有多個網(wǎng)絡(luò)接口,服務(wù)器進(jìn)程就可以在任意
// 網(wǎng)絡(luò)接口上接受客戶連接。
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(13); /* daytime server */
Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));
// 調(diào)用 listen 函數(shù)把該套接字轉(zhuǎn)換成一個監(jiān)聽套接字驰贷,這樣來自客戶的外來鏈接就可在該套接字上由內(nèi)核接受盛嘿。
// 常值 LISTENNQ 在我們的 unp.h 頭文件中定義。它指定系統(tǒng)內(nèi)核允許在這個監(jiān)聽描述符上排隊的最大客戶饱苟。
Listen(listenfd, LISTENQ);
for ( ; ; ) {
connfd = Accept(listenfd, (SA *) NULL, NULL);
ticks = time(NULL);
// snprintf 函數(shù)在這個字符串末尾添加一個回車符和一個換行符孩擂,隨后write函數(shù)把結(jié)果字符串寫給用戶。
snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks));
Write(connfd, buff, strlen(buff));
Close(connfd);
}
}
細(xì)節(jié):
1箱熬、通常情況下类垦,服務(wù)器進(jìn)程在accept調(diào)用中被投入睡眠,等待某個客戶連接的到達(dá)并被內(nèi)核接受城须。TCP連接使用所謂的三次握手來建立連接蚤认。握手完畢時accept返回,其返回值是一個稱為已連接描述符的新描述符(本例中為connfd)糕伐。該描述符用于與新近連接的那個客戶通信砰琢。accept 為每個連接到本服務(wù)器的客戶返回一個新描述符。
2良瞧、調(diào)用sprintf無法檢查目的緩沖區(qū)是否溢出陪汽,相反,snprintf要求其第二個參數(shù)指定目的緩沖區(qū)的大小褥蚯,因此可確保該緩沖區(qū)不溢出挚冤。
3、值得注意的是:許多網(wǎng)絡(luò)入侵是由黑客通過發(fā)送數(shù)據(jù)赞庶,導(dǎo)致服務(wù)器對sprintf的調(diào)用使其緩沖區(qū)溢出而發(fā)生的训挡,必須小心使用的函數(shù)還有g(shù)ets、strcat和strcpy歧强,通常應(yīng)分別改為調(diào)用fgets澜薄、strncat和strncpy。更好的替代函數(shù)是后面才引入的strlcat 和 strlcpy摊册,它們確保結(jié)果是正確終止的字符串肤京。
4、本服務(wù)器一次只能處理一個客戶茅特。如果多個客戶連接差不多同時到達(dá)蟆沫,系統(tǒng)內(nèi)核在某個最大數(shù)目的限制下把它們排入隊列,然后每次返回一個給accept函數(shù)温治。本服務(wù)器只需調(diào)用time 和 ctime 這兩個庫函數(shù),運(yùn)行速度很快戒悠。同時能處理多個用戶的并發(fā)服務(wù)器有多種編程寫法熬荆,最簡單的技術(shù)是Unix的fork函數(shù)(多進(jìn)程編程),或在服務(wù)器啟動時預(yù)先fork一定數(shù)量的子進(jìn)程(進(jìn)程池)绸狐。
之后卤恳,全部用來描述網(wǎng)絡(luò)編程中使用的各種技術(shù)的兩個客戶/服務(wù)器程序示例如下:
- 時間獲取客戶/服務(wù)器程序(base1, base2, base3)累盗;
- 回射客戶/服務(wù)器程序(base4)
所有程序的擴(kuò)展,以及所有程序的完善突琳,都是與這三個程序息息相關(guān)若债。
描述一個網(wǎng)絡(luò)中各個協(xié)議層的常用方法是使用國際標(biāo)準(zhǔn)化組織的**計算機(jī)通信開放系統(tǒng)互聯(lián)(OSI)模型。這是一個七層模型拆融,如下圖所示蠢琳,圖中同時給出了它與網(wǎng)際協(xié)議族的近似映射。
這里镜豹,OSI 模型的底下兩層是隨系統(tǒng)提供的 設(shè)備驅(qū)動 和 網(wǎng)絡(luò)硬件傲须。通常情況下,除需知道數(shù)據(jù)鏈路的某些特性外趟脂,我們不必要知道這兩層的具體情況泰讽。
圖中,TCP和UDP之間留有空隙昔期,表明:網(wǎng)絡(luò)應(yīng)用繞過傳輸層直接使用 IPv4 或 IPv6 是可能的已卸。這就是所謂的原始套接字。
OSI模型的頂上三層被合并成一層硼一,稱為應(yīng)用層累澡。這就是:Web客戶(瀏覽器)、Telent客戶欠动、Web服務(wù)器永乌、FTP服務(wù)器和其他我們在使用的網(wǎng)絡(luò)應(yīng)用所在的層。而進(jìn)行網(wǎng)絡(luò)編程的套接字就是從應(yīng)用層進(jìn)入傳輸層的接口具伍。重點(diǎn)是翅雏,如何使用套接字編寫使用TCP或UDP的網(wǎng)絡(luò)應(yīng)用程序。 后面還會有如何通過原始套接字徹底繞過IP層直接讀取數(shù)據(jù)鏈路層的幀人芽。
問:之所以套接字提供的是從OSI模型的頂上三層進(jìn)入傳輸層的接口望几?
這樣設(shè)計有兩個理由,理由之一是:頂上三層處理具體網(wǎng)絡(luò)應(yīng)用(如FTP萤厅、Telnet或HTTP)的所有細(xì)節(jié)橄抹,卻對通信細(xì)節(jié)了解很少;底下四層對具體忘了應(yīng)用了解不多惕味,卻處理所有的通信細(xì)節(jié):** 發(fā)送數(shù)據(jù)楼誓,等待確認(rèn),給無序到達(dá)數(shù)據(jù)排序名挥,計算并驗證校驗和疟羹,等等。理由之二:頂上三層通常構(gòu)成所謂的用戶進(jìn)程,底下四層卻通常作為操作系統(tǒng)內(nèi)核的一部分提供榄融。Unix與其他現(xiàn)代操作系統(tǒng)都提供分隔用戶進(jìn)程與內(nèi)核的機(jī)制参淫。由此可見,OSI的第四層和第五層的接口是構(gòu)建API的自然位置愧杯。
網(wǎng)絡(luò)拓?fù)涞陌l(fā)現(xiàn)
大多數(shù)Unix系統(tǒng)都提供了可用于發(fā)現(xiàn)某些網(wǎng)絡(luò)細(xì)節(jié)的兩個基本命令:netstat 和 ifconfig涎才。而且,有些廠商把這些命令存放在諸如/sbin 或 /usr/sbin 這樣的管理目錄中力九,而不是通常的/usr/bin目錄耍铜,而這些管理目錄可能不在通常的shell搜索路徑中(由PATH環(huán)境變量指定)
(1)netstat -i 提供網(wǎng)絡(luò)接口的信息。我們還指定 -n標(biāo)志以輸出數(shù)值地址畏邢,而不是試圖把它們反向解析成名字业扒。下面的例子給出了接口及其名字好統(tǒng)計信息:
其中環(huán)回(loopback)接口稱為lo,以太網(wǎng)接口稱為eth0舒萎。下面的例子給出了支持IPV6的一個主機(jī)的類似信息:
(2)netstat -r展示路由表程储,也是另一種確定接口的方法。我們通常指定-n標(biāo)志以輸出數(shù)值地址臂寝。它還給出默認(rèn)路由器的IP地址章鲤。
(3)有了各個網(wǎng)絡(luò)接口的名字,執(zhí)行ifconfig就可獲得每個接口的詳細(xì)信息咆贬。
該命令給出了指定接口的IP地址败徊、子網(wǎng)掩碼和廣播地址。其中的MULTICAST標(biāo)志通常指明該接口所在主機(jī)支持多播掏缎。有些ifconfig的實(shí)現(xiàn)還提供-a標(biāo)志皱蹦,用于輸出所有已配置接口的信息。
(4)找出本地網(wǎng)絡(luò)中眾多主機(jī)的IP地址的方法之一是眷蜈,針對從上一步找到的本地接口的廣播地址執(zhí)行ping命令沪哺。
POSIX 的背景
POSIX(可移植操作系統(tǒng)接口)是由IEEE開發(fā)的一系列標(biāo)準(zhǔn)。第一個標(biāo)準(zhǔn)詳述了進(jìn)入類Unix內(nèi)核的C語言接口酌儒,涵蓋了下述領(lǐng)域:進(jìn)程原語(fork辜妓、exec、信號和定時器)忌怎、進(jìn)程環(huán)境(用戶ID和進(jìn)程組)籍滴、文件與目錄(所有I/O函數(shù))、終端I/O榴啸、系統(tǒng)數(shù)據(jù)庫(口令文件和用戶組文件)以及tar和cpio歸檔格式孽惰。POSIX.1增添了3章關(guān)于線程的內(nèi)容,并另有關(guān)于線程同步(互斥鎖和條件變量)鸥印、線程調(diào)度和同步調(diào)度的各節(jié)勋功。其中腥例,聲明 ISO/IEC 9945 由下面3個部分構(gòu)成:
- Part 1: System API(C language)—— 第一部分:系統(tǒng)API(C語言)。
- Part 2: Shell and utilities—— 第二部分:Shell 和實(shí)用程序酝润。
- Part 3: System administration—— 第三部分:系統(tǒng)管理(正在開發(fā)中) 。
最后一個版本璃弄,是聯(lián)網(wǎng)API標(biāo)準(zhǔn)要销,定義了兩個API,并稱它們?yōu)樵敱M網(wǎng)絡(luò)接口(DNI)
- DNI/Socket夏块,基于 4.4BSD 的套接字API
- DNI/XTI疏咐,基于 X/Open 的 XPG4規(guī)范
** 64位體系結(jié)構(gòu)**
選用64位軟件的體系結(jié)構(gòu),原因之一是在每個進(jìn)程內(nèi)部可以由此使用更長的編址長度(即64位指針)脐供,從而可以尋址很大的內(nèi)存空間(超過2^32字節(jié))』肴現(xiàn)有32位Unix系統(tǒng)上共同的編程模型稱為ILP32模型,表示整數(shù)(I)政己、長整數(shù)(L)和指針(P)都占用32位酌壕。64位Unix系統(tǒng)上變得最為流行的模型稱為LP64模型,表示只有長整數(shù)(L)和指針(P)占用64位歇由。下面對這兩種模型進(jìn)行了比較卵牍。
ANSI C創(chuàng)造了 size_t 數(shù)據(jù)類型,它用于作為malloc的唯一參數(shù)(待分配的字節(jié)數(shù))沦泌,或者作為read 和 write的第三個參數(shù)(待讀或?qū)懙淖止?jié)數(shù))糊昙。在32位系統(tǒng)中 size_t 是一個 32位值,但是在64位系統(tǒng)中它必須是一個64位值谢谦,以便發(fā)揮更大尋址模型的優(yōu)勢释牺。也就意味著64位系統(tǒng)中也許含有一個把size_t定義為unsigned long 的typedef指令。聯(lián)網(wǎng)API存在如下問題:POSIX.1g的某些草案規(guī)定回挽,存放套接字地址結(jié)構(gòu)大小的函數(shù)參數(shù)具有size_t數(shù)據(jù)類型(如bind和connect的第三個參數(shù))没咙。如果不修改這些規(guī)定,當(dāng)Unix系統(tǒng)從ILP32模型轉(zhuǎn)變?yōu)長P64模型時厅各,size_t和long都將從32位值變?yōu)?4位值镜撩。這兩個例子實(shí)際上并不需要使用64位的數(shù)據(jù)類型:套接字地址結(jié)構(gòu)的長度最多也就幾百個字節(jié)。處理這些情況的辦法是使用專門設(shè)計的數(shù)據(jù)類型队塘。套接字API對套接字地址結(jié)構(gòu)使用 socklen_t 數(shù)據(jù)類型袁梗。不把這些值由32位改為64位的理由是易于為那些已在32位系統(tǒng)中編譯的應(yīng)用程序提供在新的64位系統(tǒng)張的二進(jìn)制代碼兼容性。
(注明1:守護(hù)進(jìn)程不僅僅是一個長時間運(yùn)行的程序憔古,而是一個隨著計算機(jī)啟動遮怜,而自動運(yùn)行的后臺程序,而且能在后臺運(yùn)行且不跟任何終端關(guān)聯(lián)的進(jìn)程——運(yùn)行鸿市,一般用shell去寫一個自定義的守護(hù)進(jìn)程)
(注明2:異步回調(diào):回調(diào)就是該函數(shù)寫在高層锯梁,低層通過一個函數(shù)指針保存這個函數(shù)即碗,在某個事件的觸發(fā)下,低層通過該函數(shù)指針調(diào)用高層那個函數(shù)陌凳。異步區(qū)別于同步剥懒,在同步模式下,一段代碼調(diào)用另一段代碼時合敦,只能采用同步調(diào)用初橘,必須等待這段代碼執(zhí)行完返回結(jié)果后,調(diào)用方才能繼續(xù)往下執(zhí)行充岛,有了多線程的支持保檐,可以采用異步調(diào)用,調(diào)用方和被調(diào)方可以屬于兩個不同的線程崔梗,調(diào)用方啟動被調(diào)方線程后夜只,不等對方返回結(jié)果就繼續(xù)執(zhí)行后續(xù)代碼。被調(diào)方執(zhí)行完畢后蒜魄,通過某種手段通知調(diào)用方:結(jié)果已經(jīng)出來扔亥,請酌情處理。权悟,這里的某種手段主要是指的線程間的通信:管道砸王,socket)
(注明3:C語言中用#define偽命令定義的對象稱為常數(shù),用const限定詞定義并初始化的對象稱為常量)峦阁。常數(shù)的值在編譯時確定谦铃,常量的值則在運(yùn)行時初始化后確定(不過此后只能作為右值使用)。本書絕大多數(shù)恒定值是用#define 定義的常數(shù)榔昔。)
(注明4:Unix errno 值驹闰,只要一個Unix函數(shù)(例如某個套接字函數(shù))中有錯誤發(fā)生,全局變量errno就被置為一個指明該錯誤類型的正值撒会,函數(shù)本身則通常返回-1嘹朗。err_sys 查看errno變量的值并輸出相應(yīng)的出錯消息,例如當(dāng)errno值等于ETIMEOUT時诵肛,將輸出“Connection time out”(連接超時)屹培。errno的值只在函數(shù)發(fā)生錯誤時設(shè)置。如果函數(shù)不返回錯誤怔檩,errno的值就沒有定義褪秀。errno 的左右正數(shù)錯誤值都是常值,具有以“E”開頭的全大寫字母名字薛训,并通常在<sys/errno.n> 頭文件中定義媒吗。值0不表示任何錯誤。)
(注明5:對于內(nèi)核而言乙埃,所有打開的文件都通過文件描述符引用闸英。文件描述符是一個非負(fù)整數(shù)锯岖。當(dāng)打開一個現(xiàn)有文件或創(chuàng)建一個新文件時,內(nèi)核向進(jìn)程返回一個文件描述符甫何。當(dāng)讀或?qū)懸粋€文件時出吹,使用open或create返回的文件描述符表示該文件,將其作為參數(shù)傳給read或write函數(shù)辙喂。用size_t作為參數(shù)的幾個API函數(shù)如下:
- malloc 向系統(tǒng)申請分配指定size個字節(jié)的內(nèi)存空間趋箩。返回類型是 void* 類型。void* 表示未確定類型的指針加派。C,C++規(guī)定,void* 類型可以通過類型轉(zhuǎn)換強(qiáng)制轉(zhuǎn)換為任何其它類型的指針跳芳。
#include <stdlib.h>
#include <malloc.h>
extern void* malloc(unsigned int num_bytes); // 函數(shù)聲明:void *malloc(size_t size);
- read函數(shù)定義如下:
#include <unistd>
ssize_t read(int filedes, void *buf, size_t nbytes);
// 返回:若成功則返回讀到的字節(jié)數(shù)芍锦,若已到文件末尾則返回0,若出錯則返回-1
// filedes:文件描述符
// buf:讀取數(shù)據(jù)緩存區(qū)
// nbytes:要讀取的字節(jié)數(shù)
// 有幾種情況可使實(shí)際讀到的字節(jié)數(shù)少于要求讀的字節(jié)數(shù):
// 1)讀普通文件時飞盆,在讀到要求字節(jié)數(shù)之前就已經(jīng)達(dá)到了文件末端娄琉。例如,若在到達(dá)文件末端之前還有30個字節(jié)吓歇,而要求讀100個字節(jié)孽水,則read返回30,下一次再調(diào)用read時城看,它將返回0(文件末端)女气。
// 2)當(dāng)從終端設(shè)備讀時,通常一次最多讀一行测柠。
// 3)當(dāng)從網(wǎng)絡(luò)讀時炼鞠,網(wǎng)絡(luò)中的緩存機(jī)構(gòu)可能造成返回值小于所要求讀的字結(jié)束。
// 4)當(dāng)從管道或FIFO讀時轰胁,如若管道包含的字節(jié)少于所需的數(shù)量谒主,那么read將只返回實(shí)際可用的字節(jié)數(shù)。
// 5)當(dāng)從某些面向記錄的設(shè)備(例如磁帶)讀時赃阀,一次最多返回一個記錄霎肯。
// 6)當(dāng)某一個信號造成中斷,而已經(jīng)讀取了部分?jǐn)?shù)據(jù)榛斯。
case:
// 設(shè)置讀取的長度:
char msg[1024];
// 讀取用戶輸入:
int ret = read(fd, msg, sizeof(msg));
if( ret < 0 )
{
perror("read fail ");
exit(1);
}
- write函數(shù)定義如下:
#include <unistd>
ssize_t write(int filedes, void *buf, size_t nbytes);
// 返回:若成功則返回寫入的字節(jié)數(shù)观游,若出錯則返回-1
// filedes:文件描述符
// buf:待寫入數(shù)據(jù)緩存區(qū)
// nbytes:要寫入的字節(jié)數(shù)
case:
void TcpEventServer::ListenerEventCb(struct evconnlistener *listener, evutil_socket_t fd, struct sockaddr *sa, int socklen, void *user_data)
{
TcpEventServer *server = (TcpEventServer*)user_data;
//隨機(jī)選擇一個子線程,通過管道向其傳遞socket描述符
int num = rand() % server->m_ThreadCount;
int sendfd = server->m_Threads[num].notifySendFd;
write(sendfd, &fd, sizeof(evutil_socket_t));
}
)
(注明5:read/write的語義:為什么會阻塞肖抱?http://www.cnblogs.com/xiehongfeng100/p/4619451.html
首先备典,write成功返回,只是buf中的數(shù)據(jù)被復(fù)制到了kernel中的TCP發(fā)送緩沖區(qū)意述。至于數(shù)據(jù)什么時候被發(fā)往網(wǎng)絡(luò)提佣,什么時候被對方主機(jī)接收吮蛹,什么時候被對方進(jìn)程讀取,系統(tǒng)調(diào)用層面不會給予任何保證和通知拌屏。之所以會阻塞潮针,是當(dāng)kernel的該socket的發(fā)送緩沖區(qū)已滿時。對于每個socket倚喂,擁有自己的send buffer和receive buffer每篷。從Linux 2.6開始,兩個緩沖區(qū)大小都由系統(tǒng)自動調(diào)節(jié)端圈,但一般都在default和max之間浮動焦读。
# 獲取socket的發(fā)送/接受緩沖區(qū)的大小:(后面的值是在Linux 2.6.38 x86_64上測試的結(jié)果)
sysctl net.core.wmem_default #126976
sysctl net.core.wmem_max #131071
已經(jīng)發(fā)送到網(wǎng)絡(luò)的數(shù)據(jù)依然需要暫存在send buffer中舱权,只有收到對方的ack后矗晃,kernel才從buffer中清除這一部分?jǐn)?shù)據(jù),為后續(xù)發(fā)送數(shù)據(jù)騰出空間宴倍。接收端將收到的數(shù)據(jù)暫存在receive buffer中张症,自動進(jìn)行確認(rèn)。但如果socket所在的進(jìn)程不及時將數(shù)據(jù)從receive buffer中取出鸵贬,最終導(dǎo)致receive buffer填滿俗他,由于TCP的滑動窗口和擁塞控制,接收端會阻止發(fā)送端向其發(fā)送數(shù)據(jù)阔逼。這些控制皆發(fā)生在TCP/IP棧中兆衅,對應(yīng)用程序是透明的,應(yīng)用程序繼續(xù)發(fā)送數(shù)據(jù)嗜浮,最終導(dǎo)致send buffer填滿涯保,write調(diào)用阻塞。一般來說周伦,由于接收端進(jìn)程從socket讀數(shù)據(jù)的速度跟不上發(fā)送端進(jìn)程向socket寫數(shù)據(jù)的速度夕春,最終導(dǎo)致發(fā)送端write調(diào)用阻塞。而read調(diào)用的行為相對容易理解专挪,從socket的receive buffer中拷貝數(shù)據(jù)到應(yīng)用程序的buffer中及志。read調(diào)用阻塞,通常是發(fā)送端的數(shù)據(jù)沒有到達(dá)寨腔。)
(注明6:blocking(默認(rèn))和nonblock模式下read/write行為的區(qū)別
將socket fd設(shè)置為nonblock(非阻塞)是在服務(wù)器編程中常見的做法速侈,采用blocking IO并為每一個client創(chuàng)建一個線程的模式開銷巨大且可擴(kuò)展性不佳(帶來大量的切換開銷),更為通用的做法是采用線程池+Nonblock I/O+Multiplexing(select/poll迫卢,以及Linux上特有的epoll)倚搬。
// 設(shè)置一個文件描述符為nonblock
int set_nonblocking(int fd)
{
int flags;
if ((flags = fcntl(fd, F_GETFL, 0)) == -1)
flags = 0;
return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}
幾個重要的結(jié)論:
read總是在接收緩沖區(qū)有數(shù)據(jù)時立即返回,而不是等到給定的read buffer填滿時返回乾蛤。
只有當(dāng)receive buffer為空時每界,blocking模式才會等待捅僵,而nonblock模式下會立即返回-1(errno = EAGAIN或EWOULDBLOCK)注:阻塞模式下,當(dāng)對方socket關(guān)閉時眨层,read會返回0庙楚。blocking 的 write 只有在緩沖區(qū)足以放下整個 buffer 時才返回(與blocking read并不相同)nonblock write
則是返回能夠放下的字節(jié)數(shù),之后調(diào)用則返回-1(errno = EAGAIN或EWOULDBLOCK)對于blocking的write有個特例:當(dāng)write正阻塞等待時對面關(guān)閉了socket趴樱,則write則會立即將剩余緩沖區(qū)填滿并返回所寫的字節(jié)數(shù)馒闷,再次調(diào)用則write失敗(connection reset by peer)
)
(注明7:read/write對連接異常的反饋行為
對應(yīng)用程序來說叁征,與另一進(jìn)程的TCP通信其實(shí)是完全異步的過程:
- 我并不知道對面什么時候纳账、能否收到的數(shù)據(jù)
- 我不知道什么時候能夠收到對面的數(shù)據(jù)
- 我不知道什么時候通信結(jié)束(主動退出或是異常退出、機(jī)器故障捺疼、網(wǎng)絡(luò)故障等等)
對于1和2塞祈,采用write() -> read() -> write() -> read() ->...的序列,通過blocking read或者nonblock read+輪詢的方式帅涂,應(yīng)用程序基于可以保證正確的處理流程。
對于3尤蛮,kernel將這些事件的“通知”通過read/write的結(jié)果返回給應(yīng)用層媳友。
)