2016.7.4
今天晚上對(duì)項(xiàng)目頂層文件(daemon)進(jìn)行了分析官边,對(duì)其中的TCP連接進(jìn)行具體的代碼級(jí)分析拒逮。
1滩援、需求分析
首先得知道我們這里為什么要用TCP連接玩徊,我們的整個(gè)測(cè)試系統(tǒng)是由上位機(jī)作為客戶端恩袱,發(fā)送測(cè)試文件畔塔,測(cè)試命令給我們測(cè)試程序上,那么我們測(cè)試程序相當(dāng)于作為一個(gè)服務(wù)器端程序寄摆,一直阻塞等待數(shù)據(jù)的到來(lái)婶恼,如果弄清楚了這個(gè)就比較好理解后面的TCP端為什么用的服務(wù)器監(jiān)聽(tīng)函數(shù)勾邦。
對(duì)于整個(gè)測(cè)試系統(tǒng)下發(fā)的分為兩個(gè)部分:測(cè)試查詢命令和測(cè)試腳本參數(shù)文件
對(duì)于查詢命令數(shù)據(jù)量下眷篇,采用不可靠但簡(jiǎn)單的UDP服務(wù)
對(duì)于測(cè)試腳本參數(shù)文件铅歼,其中包含我們測(cè)試的各種參數(shù),需要可靠的傳輸厦幅,采用TCP服務(wù)
2确憨、TCP連接原理分析
TCP通信是通過(guò)網(wǎng)絡(luò)完成服務(wù)器與客戶端的通信休弃,支持可靠傳輸塔猾。TCP/IP協(xié)議存在于OS中丈甸,網(wǎng)絡(luò)服務(wù)通過(guò)OS提供睦擂,在OS中增加支持TCP/IP的系統(tǒng)調(diào)用——Berkeley套接字顿仇,如Socket,Connect囤采,Send斑唬,Recv等
TCP/IP協(xié)議族包括運(yùn)輸層恕刘、網(wǎng)絡(luò)層褐着、鏈路層含蓉,而socket所在位置如圖项郊,Socket是應(yīng)用層與TCP/IP協(xié)議族通信的中間軟件抽象層着降。
形象的比喻就是:socket就是一個(gè)口袋,一個(gè)洞蓄喇,用戶可以通過(guò)這個(gè)洞直接與網(wǎng)絡(luò)協(xié)議棧打交道妆偏,完成網(wǎng)絡(luò)通信钱骂,基于它就是因?yàn)槌橄蠼涌诜庋b罐柳,簡(jiǎn)單张吉。
簡(jiǎn)單介紹下socket的數(shù)據(jù)結(jié)構(gòu):
1肮蛹、 socket套接字
socket起源于Unix,而Unix/Linux基本哲學(xué)之一就是“一切皆文件”省核,都可以用“打開(kāi)open –> 讀寫(xiě)write/read –>關(guān)閉close”模式來(lái)操作气忠。Socket就是該模式的一個(gè)實(shí)現(xiàn)旧噪,socket即是一種特殊的文件淘钟,一些socket函數(shù)就是對(duì)其進(jìn)行的操作(讀/寫(xiě)IO米母、打開(kāi)铁瞒、關(guān)閉).
說(shuō)白了Socket是應(yīng)用層與TCP/IP協(xié)議族通信的中間軟件抽象層精拟,它是一組接口。在設(shè)計(jì)模式中笋鄙,Socket其實(shí)就是一個(gè)門(mén)面模式萧落,它把復(fù)雜的TCP/IP協(xié)議族隱藏在Socket接口后面找岖,對(duì)用戶來(lái)說(shuō)许布,一組簡(jiǎn)單的接口就是全部蜜唾,讓Socket去組織數(shù)據(jù),以符合指定的協(xié)議擎勘。
注意:其實(shí)socket也沒(méi)有層的概念棚饵,它只是一個(gè)facade設(shè)計(jì)模式的應(yīng)用噪漾,讓編程變的更簡(jiǎn)單怪与。是一個(gè)軟件抽象層分别。在網(wǎng)絡(luò)編程中耘斩,我們大量用的都是通過(guò)socket實(shí)現(xiàn)的括授。
2荚虚、套接字描述符
其實(shí)就是一個(gè)整數(shù)版述,我們最熟悉的句柄是0渴析、1俭茧、2三個(gè)母债,0是標(biāo)準(zhǔn)輸入场斑,1是標(biāo)準(zhǔn)輸出,2是標(biāo)準(zhǔn)錯(cuò)誤輸出喧半。0挺据、1扁耐、2是整數(shù)表示的婉称,對(duì)應(yīng)的FILE *結(jié)構(gòu)的表示就是stdin王暗、stdout俗壹、stderr
套接字API最初是作為UNIX操作系統(tǒng)的一部分而開(kāi)發(fā)的绷雏,所以套接字API與系統(tǒng)的其他I/O設(shè)備集成在一起涎显。特別是棺禾,當(dāng)應(yīng)用程序要為因特網(wǎng)通信而創(chuàng)建一個(gè)套接字(socket)時(shí),操作系統(tǒng)就返回一個(gè)小整數(shù)作為描述符(descriptor)來(lái)標(biāo)識(shí)這個(gè)套接字蛀醉。然后拯刁,應(yīng)用程序以該描述符作為傳遞參數(shù)垛玻,通過(guò)調(diào)用函數(shù)來(lái)完成某種操作(例如通過(guò)網(wǎng)絡(luò)傳送數(shù)據(jù)或接收輸入的數(shù)據(jù))帚桩。
在許多操作系統(tǒng)中账嚎,套接字描述符和其他I/O描述符是集成在一起的郭蕉,所以應(yīng)用程序可以對(duì)文件進(jìn)行套接字I/O或I/O讀/寫(xiě)操作召锈。
當(dāng)應(yīng)用程序要?jiǎng)?chuàng)建一個(gè)套接字時(shí)涨岁,操作系統(tǒng)就返回一個(gè)小整數(shù)作為描述符卵惦,應(yīng)用程序則使用這個(gè)描述符來(lái)引用該套接字需要I/O請(qǐng)求的應(yīng)用程序請(qǐng)求操作系統(tǒng)打開(kāi)一個(gè)文件沮尿。操作系統(tǒng)就創(chuàng)建一個(gè)文件描述符提供給應(yīng)用程序訪問(wèn)文件畜疾。從應(yīng)用程序的角度看啡捶,文件描述符是一個(gè)整數(shù)瞎暑,應(yīng)用程序可以用它來(lái)讀寫(xiě)文件了赌。下圖顯示勿她,操作系統(tǒng)如何把文件描述符實(shí)現(xiàn)為一個(gè)指針數(shù)組,這些指針指向內(nèi)部數(shù)據(jù)結(jié)構(gòu)郭卫。
對(duì)于每個(gè)程序系統(tǒng)都有一張單獨(dú)的表贰军。精確地講谓形,系統(tǒng)為每個(gè)運(yùn)行的進(jìn)程維護(hù)一張單獨(dú)的文件描述符表寒跳。當(dāng)進(jìn)程打開(kāi)一個(gè)文件時(shí)童太,系統(tǒng)把一個(gè)指向此文件內(nèi)部數(shù)據(jù)結(jié)構(gòu)的指針寫(xiě)入文件描述符表书释,并把該表的索引值返回給調(diào)用者 爆惧。應(yīng)用程序只需記住這個(gè)描述符扯再,并在以后操作該文件時(shí)使用它熄阻。操作系統(tǒng)把該描述符作為索引訪問(wèn)進(jìn)程描述符表秃殉,通過(guò)指針找到保存該文件所有的信息的數(shù)據(jù)結(jié)構(gòu)钾军。
針對(duì)套接字的系統(tǒng)數(shù)據(jù)結(jié)構(gòu):
1)巧颈、套接字API里有個(gè)函數(shù)socket,它就是用來(lái)創(chuàng)建一個(gè)套接字唇礁。套接字設(shè)計(jì)的總體思路是盏筐,單個(gè)系統(tǒng)調(diào)用就可以創(chuàng)建任何套接字琢融,因?yàn)樘捉幼质窍喈?dāng)籠統(tǒng)的漾抬。一旦套接字創(chuàng)建后纳令,應(yīng)用程序還需要調(diào)用其他函數(shù)來(lái)指定具體細(xì)節(jié)平绩。例如調(diào)用socket將創(chuàng)建一個(gè)新的描述符條目:
2)捏雌、雖然套接字的內(nèi)部數(shù)據(jù)結(jié)構(gòu)包含很多字段性湿,但是系統(tǒng)創(chuàng)建套接字后窘奏,大多數(shù)字字段沒(méi)有填寫(xiě)着裹。應(yīng)用程序創(chuàng)建套接字后在該套接字可以使用之前骇扇,必須調(diào)用其他的過(guò)程來(lái)填充這些字段少孝。
3稍走、文件描述符和文件指針的區(qū)別:
文件描述符:在linux系統(tǒng)中打開(kāi)文件就會(huì)獲得文件描述符婿脸,它是個(gè)很小的正整數(shù)狐树。每個(gè)進(jìn)程在PCB(Process Control Block)中保存著一份文件描述符表抑钟,文件描述符就是這個(gè)表的索引在塔,每個(gè)表項(xiàng)都有一個(gè)指向已打開(kāi)文件的指針心俗。
文件指針:C語(yǔ)言中使用文件指針做為I/O的句柄城榛。文件指針指向進(jìn)程用戶區(qū)中的一個(gè)被稱為FILE結(jié)構(gòu)的數(shù)據(jù)結(jié)構(gòu)狠持。FILE結(jié)構(gòu)包括一個(gè)緩沖區(qū)和一個(gè)文件描述符喘垂。而文件描述符是文件描述符表的一個(gè)索引正勒,因此從某種意義上說(shuō)文件指針就是句柄的句柄(在Windows系統(tǒng)上章贞,文件描述符被稱作文件句柄)鸭限。
下面講述下利用socket通信的服務(wù)器端和客戶端的基本結(jié)構(gòu)框圖:
服務(wù)器端先初始化Socket败京,然后與端口綁定(bind)赡麦,對(duì)端口進(jìn)行監(jiān)聽(tīng)(listen),調(diào)用accept阻塞渡冻,等待客戶端連接。在這時(shí)如果有個(gè)客戶端初始化一個(gè)Socket帽借,然后連接服務(wù)器(connect)砍艾,如果連接成功脆荷,這時(shí)客戶端與服務(wù)器端的連接就建立了蜓谋√一溃客戶端發(fā)送數(shù)據(jù)請(qǐng)求观堂,服務(wù)器端接收請(qǐng)求并處理請(qǐng)求师痕,然后把回應(yīng)數(shù)據(jù)發(fā)送給客戶端胰坟,客戶端讀取數(shù)據(jù)腕铸,最后關(guān)閉連接狠裹,一次交互結(jié)束涛菠。
3、TCP連接代碼分析(需要說(shuō)明的是:這里以項(xiàng)目的daemon.c代碼為例 )
首先給出定義的一些buf和一些文件數(shù)據(jù)變量:
按照上述紅色框框的程序流程圖牍颈,一步步分析煮岁。
(1)定義socket套接字
socket函數(shù)具體參數(shù)及返回值分析如下:
socket()函數(shù)
int socket(int protofamily, int type, int protocol);//返回sockfd
sockfd是描述符画机。
socket函數(shù)對(duì)應(yīng)于普通文件的打開(kāi)操作步氏。普通文件的打開(kāi)操作返回一個(gè)文件描述字荚醒,而socket()用于創(chuàng)建一個(gè)socket描述符(socket descriptor)界阁,它唯一標(biāo)識(shí)一個(gè)socket铺董。這個(gè)socket描述字跟文件描述字一樣精续,后續(xù)的操作都有用到它重付,把它作為參數(shù)确垫,通過(guò)它來(lái)進(jìn)行一些讀寫(xiě)操作删掀。
正如可以給fopen的傳入不同參數(shù)值披泪,以打開(kāi)不同的文件款票。創(chuàng)建socket的時(shí)候,也可以指定不同的參數(shù)創(chuàng)建不同的socket描述符卡乾,socket函數(shù)的三個(gè)參數(shù)分別為:
- protofamily:即協(xié)議域幔妨,又稱為協(xié)議族(family)陶冷。常用的協(xié)議族有维蒙,AF_INET(IPV4)、AF_INET6(IPV6)膊毁、AF_LOCAL(或稱AF_UNIX婚温,Unix域socket)栅螟、AF_ROUTE等等力图。協(xié)議族決定了socket的地址類(lèi)型吃媒,在通信中必須采用對(duì)應(yīng)的地址赘那,如AF_INET決定了要用ipv4地址(32位的)與端口號(hào)(16位的)的組合募舟、AF_UNIX決定了要用一個(gè)絕對(duì)路徑名作為地址胃珍。
- type:指定socket類(lèi)型。常用的socket類(lèi)型有钮热,SOCK_STREAM隧期、SOCK_DGRAM仆潮、SOCK_RAW性置、SOCK_PACKET鹏浅、SOCK_SEQPACKET等等(socket的類(lèi)型有哪些隐砸?)季希。
- protocol:故名思意式塌,就是指定協(xié)議友浸。常用的協(xié)議有尾菇,IPPROTO_TCP派诬、IPPTOTO_UDP默赂、IPPROTO_SCTP、IPPROTO_TIPC等疾捍,它們分別對(duì)應(yīng)TCP傳輸協(xié)議乱豆、UDP傳輸協(xié)議宛裕、STCP傳輸協(xié)議揩尸、TIPC傳輸協(xié)議(這個(gè)協(xié)議我將會(huì)單獨(dú)開(kāi)篇討論Q矣堋)朗恳。
注意:并不是上面的type和protocol可以隨意組合的,如SOCK_STREAM不可以跟IPPROTO_UDP組合崭庸。當(dāng)protocol為0時(shí)怕享,會(huì)自動(dòng)選擇type類(lèi)型對(duì)應(yīng)的默認(rèn)協(xié)議函筋。
當(dāng)我們調(diào)用socket創(chuàng)建一個(gè)socket時(shí)跌帐,返回的socket描述字它存在于協(xié)議族(address family谨敛,AF_XXX)空間中脸狸,但沒(méi)有一個(gè)具體的地址炊甲。如果想要給它賦值一個(gè)地址,就必須調(diào)用bind()函數(shù)吟吝,否則就當(dāng)調(diào)用connect()爸黄、listen()時(shí)系統(tǒng)會(huì)自動(dòng)隨機(jī)分配一個(gè)端口炕贵。
給出代碼中的實(shí)例
返回的即是套接字的名稱称开,套接字的數(shù)據(jù)結(jié)構(gòu)鳖轰,作為后面函數(shù)的參數(shù)蕴侣。
(2)bind()函數(shù)綁定定義socket的IP地址和端口號(hào)
bind函數(shù)具體參數(shù)及返回值分析如下:
bind()函數(shù)
正如上面所說(shuō)bind()函數(shù)把一個(gè)地址族中的特定地址賦給socket昆雀。例如對(duì)應(yīng)AF_INET狞膘、AF_INET6就是把一個(gè)ipv4或ipv6地址和端口號(hào)組合賦給socket挽封。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
函數(shù)的三個(gè)參數(shù)分別為:
- sockfd:即socket描述字臣镣,它是通過(guò)socket()函數(shù)創(chuàng)建了渠缕,唯一標(biāo)識(shí)一個(gè)socket亦鳞。bind()函數(shù)就是將給這個(gè)描述字綁定一個(gè)名字。
- addr:一個(gè)const struct sockaddr *指針遭笋,指向要綁定給sockfd的協(xié)議地址瓦呼。這個(gè)地址結(jié)構(gòu)根據(jù)地址創(chuàng)建socket時(shí)的地址協(xié)議族的不同而不同央串,如ipv4對(duì)應(yīng)的是:
struct sockaddr_in {
sa_family_t sin_family; /* address family: AF_INET */
in_port_t sin_port; /* port in network byte order */
struct in_addr sin_addr; /* internet address */
};
/* Internet address. */
struct in_addr {
uint32_t s_addr; /* address in network byte order */
};
ipv6對(duì)應(yīng)的是:
struct sockaddr_in6 {
sa_family_t sin6_family; /* AF_INET6 */
in_port_t sin6_port; /* port number */
uint32_t sin6_flowinfo; /* IPv6 flow information */
struct in6_addr sin6_addr; /* IPv6 address */
uint32_t sin6_scope_id; /* Scope ID (new in 2.4) */
};
struct in6_addr {
unsigned char s6_addr[16]; /* IPv6 address */
};
Unix域?qū)?yīng)的是:
#define UNIX_PATH_MAX 108
struct sockaddr_un {
sa_family_t sun_family; /* AF_UNIX */
char sun_path[UNIX_PATH_MAX]; /* pathname */
};
addrlen:對(duì)應(yīng)的是地址的長(zhǎng)度。
通常服務(wù)器在啟動(dòng)的時(shí)候都會(huì)綁定一個(gè)眾所周知的地址(如ip地址+端口號(hào))饲宿,用于提供服務(wù)瘫想,客戶就可以通過(guò)它來(lái)接連服務(wù)器昌讲;而客戶端就不用指定短绸,有系統(tǒng)自動(dòng)分配一個(gè)端口號(hào)和自身的ip地址組合鸠按。這就是為什么通常服務(wù)器端在listen之前會(huì)調(diào)用bind()目尖,而客戶端就不會(huì)調(diào)用瑟曲,而是在connect()時(shí)由系統(tǒng)隨機(jī)生成一個(gè)洞拨。
給出代碼中實(shí)例:
從代碼上分析可以知道烦衣,給三個(gè)socket通過(guò)bind綁定了不同的地址和端口號(hào)花吟,需要注意的是還有assert()函數(shù)衅澈,該函數(shù)是檢測(cè)異常今布,終止并打印相應(yīng)信息的函數(shù)部默,具體函數(shù)說(shuō)明如下:
assert宏的原型定義在<assert.h>中甩牺,其作用是如果它的條件返回錯(cuò)誤急但,則終止程序執(zhí)行搞乏,原型定義:
#include <assert.h>
void assert( int expression );
assert的作用是現(xiàn)計(jì)算表達(dá)式 expression 镐躲,如果其值為假(即為0)萤皂,那么它先向stderr打印一條出錯(cuò)信息裆熙,然后通過(guò)調(diào)用 abort 來(lái)終止程序運(yùn)行入录。請(qǐng)看下面的程序清單badptr.c:
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
int main( void )
{
FILE *fp;
fp = fopen( "test.txt", "w" );//以可寫(xiě)的方式打開(kāi)一個(gè)文件僚稿,如果不存在就創(chuàng)建一個(gè)同名文件
assert( fp ); //所以這里不會(huì)出錯(cuò)
fclose( fp );
fp = fopen( "noexitfile.txt", "r" );//以只讀的方式打開(kāi)一個(gè)文件蚀同,如果不存在就打開(kāi)文件失敗
assert( fp ); //所以這里出錯(cuò)
fclose( fp ); //程序永遠(yuǎn)都執(zhí)行不到這里來(lái)
return0;
}
[root@localhost error_process]# gcc badptr.c
[root@localhost error_process]# ./a.out
a.out: badptr.c:14: main: Assertion `fp' failed.
(3)listen監(jiān)聽(tīng)函數(shù),監(jiān)聽(tīng)端口是否有TCP握手請(qǐng)求
listen()拷恨、connect()函數(shù)
如果作為一個(gè)服務(wù)器,在調(diào)用socket()芦疏、bind()之后就會(huì)調(diào)用listen()來(lái)監(jiān)聽(tīng)這個(gè)socket,如果客戶端這時(shí)調(diào)用connect()發(fā)出連接請(qǐng)求分预,服務(wù)器端就會(huì)接收到這個(gè)請(qǐng)求薪捍。
int listen(int sockfd, int backlog);
int connect(int sockfd, conststruct sockaddr *addr, socklen_t addrlen);
listen函數(shù)的第一個(gè)參數(shù)即為要監(jiān)聽(tīng)的socket描述字笼痹,第二個(gè)參數(shù)為相應(yīng)socket可以排隊(duì)的最大連接個(gè)數(shù)。socket()函數(shù)創(chuàng)建的socket默認(rèn)是一個(gè)主動(dòng)類(lèi)型的酪穿,listen函數(shù)將socket變?yōu)楸粍?dòng)類(lèi)型的凳干,等待客戶的連接請(qǐng)求。
connect函數(shù)的第一個(gè)參數(shù)即為客戶端的socket描述字被济,第二參數(shù)為服務(wù)器的socket地址救赐,第三個(gè)參數(shù)為socket地址的長(zhǎng)度≈涣祝客戶端通過(guò)調(diào)用connect函數(shù)來(lái)建立與TCP服務(wù)器的連接。
listen函數(shù)較為簡(jiǎn)單轧叽,而且僅僅針對(duì)TCP連接的服務(wù)器端鞠绰,實(shí)例代碼如下:
從上面看出,MAXPENDING表示最大的連接數(shù)驴一,定義的是MAXPENDING=5。
(4)accpet接收函數(shù),接收客戶端的tcp連接
accept()函數(shù)
TCP服務(wù)器端依次調(diào)用socket()趣钱、bind()井联、listen()之后,就會(huì)監(jiān)聽(tīng)指定的socket地址了军掂。TCP客戶端依次調(diào)用socket()率触、connect()之后就向TCP服務(wù)器發(fā)送了一個(gè)連接請(qǐng)求。TCP服務(wù)器監(jiān)聽(tīng)到這個(gè)請(qǐng)求之后皂甘,就會(huì)調(diào)用accept()函數(shù)取接收請(qǐng)求,這樣連接就建立好了嗤锉。之后就可以開(kāi)始網(wǎng)絡(luò)I/O操作了苫幢,即類(lèi)同于普通文件的讀寫(xiě)I/O操作盐数。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); //返回連接connect_fd
參數(shù)sockfd
參數(shù)sockfd就是上面解釋中的監(jiān)聽(tīng)套接字漾峡,這個(gè)套接字用來(lái)監(jiān)聽(tīng)一個(gè)端口且预,當(dāng)有一個(gè)客戶與服務(wù)器連接時(shí),它使用這個(gè)一個(gè)端口號(hào),而此時(shí)這個(gè)端口號(hào)正與這個(gè)套接字關(guān)聯(lián)鼓择。當(dāng)然客戶不知道套接字這些細(xì)節(jié)催跪,它只知道一個(gè)地址和一個(gè)端口號(hào)。
參數(shù)addr
這是一個(gè)結(jié)果參數(shù),它用來(lái)接受一個(gè)返回值,這返回值指定客戶端的地址,當(dāng)然這個(gè)地址是通過(guò)某個(gè)地址結(jié)構(gòu)來(lái)描述的,用戶應(yīng)該知道這一個(gè)什么樣的地址結(jié)構(gòu)魄幕。如果對(duì)客戶的地址不感興趣,那么可以把這個(gè)值設(shè)置為NULL。
參數(shù)len
如同大家所認(rèn)為的,它也是結(jié)果的參數(shù),用來(lái)接受上述addr的結(jié)構(gòu)的大小的,它指明addr結(jié)構(gòu)所占有的字節(jié)個(gè)數(shù)尤勋。同樣的,它也可以被設(shè)置為NULL。
如果accept成功返回,則服務(wù)器與客戶已經(jīng)正確建立連接了,此時(shí)服務(wù)器通過(guò)accept返回的套接字來(lái)完成與客戶的通信。
注意:
accept默認(rèn)會(huì)阻塞進(jìn)程集乔,直到有一個(gè)客戶連接建立后返回汗唱,它返回的是一個(gè)新可用的套接字际插,這個(gè)套接字是連接套接字。此時(shí)我們需要區(qū)分兩種套接字
監(jiān)聽(tīng)套接字: 監(jiān)聽(tīng)套接字正如accept的參數(shù)sockfd僻焚,它是監(jiān)聽(tīng)套接字咐旧,在調(diào)用listen函數(shù)之后,是服務(wù)器開(kāi)始調(diào)用socket()函數(shù)生成的,稱為監(jiān)聽(tīng)socket描述字(監(jiān)聽(tīng)套接字)
連接套接字:一個(gè)套接字會(huì)從主動(dòng)連接的套接字變身為一個(gè)監(jiān)聽(tīng)套接字;而accept函數(shù)返回的是已連接socket描述字(一個(gè)連接套接字),它代表著一個(gè)網(wǎng)絡(luò)已經(jīng)存在的點(diǎn)點(diǎn)連接穿挨。
一個(gè)服務(wù)器通常通常僅僅只創(chuàng)建一個(gè)監(jiān)聽(tīng)socket描述字佛寿,它在該服務(wù)器的生命周期內(nèi)一直存在弹渔。內(nèi)核為每個(gè)由服務(wù)器進(jìn)程接受的客戶連接創(chuàng)建了一個(gè)已連接socket描述字椿胯,當(dāng)服務(wù)器完成了對(duì)某個(gè)客戶的服務(wù)廉油,相應(yīng)的已連接socket描述字就被關(guān)閉十兢。
自然要問(wèn)的是:為什么要有兩種套接字?原因很簡(jiǎn)單,如果使用一個(gè)描述字的話,那么它的功能太多,使得使用很不直觀,同時(shí)在內(nèi)核確實(shí)產(chǎn)生了一個(gè)這樣的新的描述字。
連接套接字socketfd_new 并沒(méi)有占用新的端口與客戶端通信只洒,依然使用的是與監(jiān)聽(tīng)套接字socketfd一樣的端口號(hào)
client_sd即使返回的新的套接字蔑穴,連接套接字纵朋,通過(guò)連接套接字進(jìn)行數(shù)據(jù)的傳輸聂薪。
但是上述這種方式存在i一個(gè)很大的問(wèn)題!!!!——即如果沒(méi)有tcp請(qǐng)求會(huì)一直阻塞剑刑,而且用戶無(wú)法得知素挽,效率極低。
為了解決上述的辦法,引進(jìn)了套接字監(jiān)控宏函數(shù)扇丛。
直接給出實(shí)例是如何調(diào)用的:
上述用的4個(gè)操作宏到底有什么用购啄,如何定義的呢几迄??、下面進(jìn)行詳細(xì)的介紹:
FD_ZERO盅惜,F(xiàn)D_ISSET這些都是套節(jié)字結(jié)合操作宏 ,看看MSDN上的select函數(shù), 這是在select io模型中的核心,用來(lái)管理套節(jié)字IO的,避免出現(xiàn)無(wú)辜鎖定.
int select(int nfds,fd_set FAR *readfds, fd_set FAR *writefds, fd_set FAR *exceptfds, const struct timeval FAR *timeout);
第一個(gè)參數(shù)在windows下可以忽略眠寿,但在linux下必須設(shè)為最大文件描述符加1宁舰;二是結(jié)構(gòu)fd_set在兩個(gè)系統(tǒng)里定義不一樣)低葫。
所以代碼中對(duì)這個(gè)位置也有特別的注意,為什么對(duì)三個(gè)套接字取了最大值:
max_sd表示正在被使用的socket類(lèi)
最后的是超時(shí)標(biāo)準(zhǔn),select是阻塞操作當(dāng)然要設(shè)置超時(shí)事件. 接著的三個(gè)類(lèi)型為fd_set的參數(shù)分別是用于檢查套節(jié)字的可讀性,可寫(xiě)性,和列外數(shù)據(jù)性質(zhì).
我舉個(gè)例子 :比如recv(), 在沒(méi)有數(shù)據(jù)到來(lái)調(diào)用它的時(shí)候,你的線程將被阻塞如果數(shù)據(jù)一直不來(lái),你的線程就要阻塞很久.這樣顯然不好.所以采用select來(lái)查看套節(jié)字是否可讀(也就是是否有數(shù)據(jù)讀了)
步驟如下
socket s;
.....
fd_set set;
while(1)
{
FD_ZERO(&set);//將你的套節(jié)字集合清空
FD_SET(s, &set);//加入你感興趣的套節(jié)字到集合,這里是一個(gè)讀數(shù)據(jù)的套節(jié)字s
select(0,&set,NULL,NULL,NULL);//檢查套節(jié)字是否可讀,
//很多情況下就是是否有數(shù)據(jù)(注意,只是說(shuō)很多情況)
//這里select是否出錯(cuò)沒(méi)有寫(xiě)
if(FD_ISSET(s, &set) //檢查s是否在這個(gè)集合里面,
{ //select將更新這個(gè)集合,把其中不可讀的套節(jié)字去掉
//只保留符合條件的套節(jié)字在這個(gè)集合里面
recv(s,...);
}
//do something here
}
也就是說(shuō),我們得檢測(cè)我們選定的套接字中此時(shí)可不可讀拾枣,如果不可讀我們就將套接字刷新掉俊啼,不比一直進(jìn)行阻塞等待,等中心建立新的套接字,在進(jìn)行下一輪霎苗,提高之前說(shuō)的阻塞效率昆淡。
(5)recv()函數(shù)接收客戶端傳送的數(shù)據(jù)
先看下recv函數(shù)的原型:
int recv( SOCKET s, char FAR *buf, int len, int flags);
不論是客戶還是服務(wù)器應(yīng)用程序都用recv函數(shù)從TCP連接的另一端接收數(shù)據(jù)鹏氧。該函數(shù)的第一個(gè)參數(shù)指定接收端套接字描述符;
第二個(gè)參數(shù)指明一個(gè)緩沖區(qū)腾窝,該緩沖區(qū)用來(lái)存放recv函數(shù)接收到的數(shù)據(jù)蔗草;
第三個(gè)參數(shù)指明buf的長(zhǎng)度;
第四個(gè)參數(shù)一般置0。
這里只描述同步Socket的recv函數(shù)的執(zhí)行流程昭抒。當(dāng)應(yīng)用程序調(diào)用recv函數(shù)時(shí),
(1)recv先等待s的發(fā)送緩沖中的數(shù)據(jù)被協(xié)議傳送完畢蚓聘,如果協(xié)議在傳送s的發(fā)送緩沖中的數(shù)據(jù)時(shí)出現(xiàn)網(wǎng)絡(luò)錯(cuò)誤急迂,那么recv函數(shù)返回SOCKET_ERROR,
(2)如果s的發(fā)送緩沖中沒(méi)有數(shù)據(jù)或者數(shù)據(jù)被協(xié)議成功發(fā)送完畢后腰吟,recv先檢查套接字s的接收緩沖區(qū),如果s接收緩沖區(qū)中沒(méi)有數(shù)據(jù)或者協(xié)議正在接收數(shù)據(jù)蒿赢,那么recv就一直等待,直到協(xié)議把數(shù)據(jù)接收完畢觉至。當(dāng)協(xié)議把數(shù)據(jù)接收完畢,recv函數(shù)就把s的接收緩沖中的數(shù)據(jù)copy到buf中(注意協(xié)議接收到的數(shù)據(jù)可能大于buf的長(zhǎng)度耿导,所以 在這種情況下要調(diào)用幾次recv函數(shù)才能把s的接收緩沖中的數(shù)據(jù)copy完柿冲。recv函數(shù)僅僅是copy數(shù)據(jù),真正的接收數(shù)據(jù)是協(xié)議來(lái)完成的),
recv函數(shù)返回其實(shí)際copy的字節(jié)數(shù)柠座。如果recv在copy時(shí)出錯(cuò)爆哑,那么它返回SOCKET_ERROR;如果recv函數(shù)在等待協(xié)議接收數(shù)據(jù)時(shí)網(wǎng)絡(luò)中斷了瘪吏,那么它返回0渺尘。
給出代碼中的實(shí)例:
上面說(shuō)了我們可能 一次copy沒(méi)有說(shuō)有的數(shù)據(jù),這時(shí)候我們需要做的就是多次調(diào)用recv函數(shù),很明顯争涌,這里我們不是簡(jiǎn)單的用這一句話完成继蜡,需要對(duì)數(shù)據(jù)流進(jìn)行控制輸出引颈,控制代碼如下:
代碼也不難,分析來(lái)看吉捶,就是一個(gè)while循環(huán),完成這個(gè)buffer(4KB)緩存池的寫(xiě)滿干旧,通過(guò)buffer_len和recv返回copy字節(jié)數(shù)ret進(jìn)行控制闹击,用buffer_len-ret當(dāng)?shù)扔?的時(shí)候也就是填滿了仲义,可以跳出循環(huán)了谣拣。
(6)close()函數(shù)關(guān)閉連接套接字
當(dāng)數(shù)據(jù)傳輸完成后例书,關(guān)閉對(duì)應(yīng)的連接套接字爱谁,則緩存中不會(huì)有其他的數(shù)據(jù)接收,避免下次調(diào)用的時(shí)候復(fù)寫(xiě)走搁。
close()函數(shù)
在服務(wù)器與客戶端建立連接之后躯肌,會(huì)進(jìn)行一些讀寫(xiě)操作拥刻,完成了讀寫(xiě)操作就要關(guān)閉相應(yīng)的socket描述字,好比操作完打開(kāi)的文件要調(diào)用fclose關(guān)閉打開(kāi)的文件徽惋。
#include <unistd.h>
int close(int fd);
close一個(gè)TCP socket的缺省行為時(shí)把該socket標(biāo)記為以關(guān)閉成黄,然后立即返回到調(diào)用進(jìn)程光绕。該描述字不能再由調(diào)用進(jìn)程使用驯绎,也就是說(shuō)不能再作為read或write的第一個(gè)參數(shù)鞭执。
注意:close操作只是使相應(yīng)socket描述字的引用計(jì)數(shù)-1,只有當(dāng)引用計(jì)數(shù)為0的時(shí)候葵诈,才會(huì)觸發(fā)TCP客戶端向服務(wù)器發(fā)送終止連接請(qǐng)求豌熄。
代碼實(shí)例調(diào)用如下:
很簡(jiǎn)單,沒(méi)有什么好分析的压鉴。
完成以上的6個(gè)動(dòng)作上鞠,最后即將客戶端發(fā)送的4KB數(shù)據(jù)放到buf中骗露,之后的程序就是根據(jù)這4KB的腳本數(shù)據(jù)波附,進(jìn)行后面的測(cè)試工具沾瓦。