#
這里提到的兩個(gè)設(shè)計(jì)模式都是用于高并發(fā)系統(tǒng)(例如一個(gè)高性能的網(wǎng)絡(luò)服務(wù)器)的。這里我只是簡(jiǎn)單地提一下:
1.半同步/半異步(half-sync/half-async):
在網(wǎng)上一份資料中引用了一本貌似很經(jīng)典的書(shū)里的比喻:
”
許多餐廳使用 半同步/半異步 模式的變體。例如,餐廳常常雇傭一個(gè)領(lǐng)班負(fù)責(zé)迎接顧客,并在餐廳繁忙時(shí)留意給顧客安排桌位,
為等待就餐的顧客按序排隊(duì)是必要的。領(lǐng)班由所有顧客“共享”,不能被任何特定顧客占用太多時(shí)間。當(dāng)顧客在一張桌子入坐后,
有一個(gè)侍應(yīng)生專(zhuān)門(mén)為這張桌子服務(wù)。
“
按照另一份似乎比較權(quán)威的文檔的描述,要實(shí)現(xiàn)半同步/半異步模式,需要實(shí)現(xiàn)三層:異步層、同步層、隊(duì)列層。因?yàn)楹芏嗖僮?br>采用異步方式會(huì)比較有效率(例如高效率的網(wǎng)絡(luò)模型似乎都采用異步IO),但是異步操作的復(fù)雜度比較高,不利于編程。而同步
操作相對(duì)之下編程要簡(jiǎn)單點(diǎn)。為了結(jié)合兩者的優(yōu)點(diǎn),就提出了這個(gè)模式。而為了讓異步層和同步層互相通信(模塊間的通信),系
統(tǒng)需要加入一個(gè)通信隊(duì)列。異步層將操作結(jié)果放入隊(duì)列,同步層從隊(duì)列里獲取操作結(jié)果。
回過(guò)頭來(lái)看看我之前寫(xiě)的那個(gè)select網(wǎng)絡(luò)模型代碼,個(gè)人認(rèn)為基本上算是一個(gè)半同步半異步模式的簡(jiǎn)單例子:Buffer相當(dāng)于通信
隊(duì)列,網(wǎng)絡(luò)底層將數(shù)據(jù)寫(xiě)入Buffer,上層再同步地從該隊(duì)列里獲取出數(shù)據(jù)。這樣看來(lái)似乎也沒(méi)什么難度。 = =
關(guān)于例子代碼,直接引用iunknown給的:
//這就是一個(gè)典型的循環(huán)隊(duì)列的定義,iget 是隊(duì)列頭,iput 是隊(duì)列尾</STRONG>
int clifd[MAXNCLI], iget, iput;
int main( int argc, char * argv[] )
{
......
int listenfd = Tcp_listen( NULL, argv[ 1 ], &addrlen );
......
iget = iput = 0;
for( int i = 0; i < nthreads; i++ ) {
pthread_create( &tptr[i].thread_tid, NULL, &thread_main, (void*)i );
for( ; ; ) {
connfd = accept( listenfd, cliaddr,, &clilen );
clifd[ iput ] = connfd; // 接受到的連接句柄放入隊(duì)列</STRONG>
if( ++iput == MAXNCLI ) iput = 0;
}
}
void * thread_main( void * arg )
{
for( ; ; ) {
while( iget == iput ) pthread_cond_wait( ...... );
connfd = clifd[ iget ]; // 從隊(duì)列中獲得連接句柄</STRONG>
if( ++iget == MAXNCLI ) iget = 0;
......
web_child( connfd );
close( connfd );
}
}
2.領(lǐng)導(dǎo)者/追隨者(Leader/Followers):
同樣,給出別人引用的比喻:
”
在日常生活中,領(lǐng)導(dǎo)者/追隨者模式用于管理許多飛機(jī)場(chǎng)出租車(chē)候車(chē)臺(tái)。在該用例中,出租車(chē)扮演“線程”角色,排在第一輛的出
租車(chē)成為領(lǐng)導(dǎo)者,剩下的出租車(chē)成為追隨者。同樣,到達(dá)出租車(chē)候車(chē)臺(tái)的乘客構(gòu)成了必須被多路分解給出租車(chē)的事件,一般以先進(jìn)
先出排序。一般來(lái)說(shuō),如果任何出租車(chē)可以為任何顧客服務(wù),該場(chǎng)景就主要相當(dāng)于非綁定句柄/線程關(guān)聯(lián)。然而,如果僅僅是某些
出租車(chē)可以為某些乘客服務(wù),該場(chǎng)景就相當(dāng)于綁定句柄/線程關(guān)聯(lián)。
“
其實(shí)這個(gè)更簡(jiǎn)單,我記得<unix網(wǎng)絡(luò)編程>中似乎提到過(guò)這個(gè)。總之有一種網(wǎng)絡(luò)模型(connection-per-thread?)里,一個(gè)線程用于
accept連接。當(dāng)接收到一個(gè)新的連接時(shí),這個(gè)線程就轉(zhuǎn)為connection thread,而這個(gè)線程后面的線程則上升為accept線程。這里,
accept線程就相當(dāng)于領(lǐng)導(dǎo)者線程,而其他線程則屬于追隨者線程。
iunknown 的例子代碼:
int listenfd;
int main( int argc, char * argv[] )
{
......
listenfd = Tcp_listen( NULL, argv[ 1 ], &addrlen );
......
for( int i = 0; i < nthreads; i++ ){
pthread_create( &tptr[i].thread_tid, NULL, &thread_main, (void*)i );
}
......
}
void * thread_main( void * arg )
{
for( ; ; ){
......
// 多個(gè)線程同時(shí)阻塞在這個(gè) accept 調(diào)用上,依靠操作系統(tǒng)的隊(duì)列</STRONG>
connfd = accept( listenfd, cliaddr, &clilen );
......
web_child( connfd );
close( connfd );
......
}
}
沒(méi)什么技術(shù)含量,將select模型做簡(jiǎn)單的封裝,同時(shí)提供服務(wù)器端和客戶端所用的接口。功能實(shí)現(xiàn)上對(duì)數(shù)據(jù)的發(fā)送和接收
都做了緩存,搞得跟異步IO一樣 = =#。
這個(gè)例子聊天服務(wù)器可以使用telnet登錄,服務(wù)器直接將telnet發(fā)來(lái)的字符串轉(zhuǎn)發(fā)給所有客戶端。我稍微寫(xiě)了一個(gè)小的網(wǎng)絡(luò)
模塊,可以用于以后寫(xiě)網(wǎng)絡(luò)程序的例子代碼,也算是練習(xí)下網(wǎng)絡(luò)庫(kù)的設(shè)計(jì)。
系統(tǒng)總體類(lèi)圖如下:
Address用于包裝sockaddr_in結(jié)構(gòu)體,目的就是讓系統(tǒng)用起來(lái)更方便。
Buffer用于封裝原始內(nèi)存,主要目的是拿來(lái)做發(fā)送、接收數(shù)據(jù)緩沖。
Fdset差不多和FD_SET一樣,只是這里自己寫(xiě)一個(gè)FD_SET,可以讓連接數(shù)不受FD_SETSIZE的限制。
Socket封裝了基本的SOCKET操作,包括創(chuàng)建、銷(xiāo)毀套接字。
Session比較有意思,按我的意思,就是代表一個(gè)網(wǎng)絡(luò)連接。對(duì)于服務(wù)器端,可能會(huì)有很多連接,每一個(gè)連接可以用一個(gè)
Session對(duì)象表示。而對(duì)于客戶端,只有一個(gè)連接,那么就是一個(gè)Session對(duì)象。對(duì)于Session對(duì)象來(lái)說(shuō),可以進(jìn)行數(shù)據(jù)的
發(fā)送和接收,因此這里Session有recv、send之類(lèi)的接口。為了緩沖數(shù)據(jù),所以Session對(duì)于讀寫(xiě)分別有一個(gè)Buffer對(duì)象。
Server代表一個(gè)服務(wù)器,直接提供創(chuàng)建服務(wù)器的接口。同時(shí)使用一個(gè)unsigned long作為每一個(gè)連接的ID號(hào)。
Client代表一個(gè)客戶端,可以直接用于連接服務(wù)器。
下載文件提供網(wǎng)絡(luò)模塊代碼,以及三個(gè)例子程序。點(diǎn)擊下載
Author : Kevin Lynx
從開(kāi)始接觸網(wǎng)絡(luò)編程這個(gè)東西開(kāi)始,我就不間斷地閱讀一些網(wǎng)絡(luò)庫(kù)(模塊)的源代碼,主要目的是為了獲取別
人在這方面的經(jīng)驗(yàn),編程這東西,還是要多實(shí)踐啊。
基本上,Etwork是一個(gè)很小巧的網(wǎng)絡(luò)庫(kù)。Etwork基于select模型,采用我之前說(shuō)的技巧,理論上可以處理很
多連接(先不說(shuō)效率)。
先看看下這個(gè)庫(kù)的結(jié)構(gòu):
如同很多網(wǎng)絡(luò)庫(kù)一樣,總會(huì)有一個(gè)類(lèi)似于ISocketManager的類(lèi),用于管理所有網(wǎng)絡(luò)連接(當(dāng)用戶服務(wù)器時(shí))。
而ISocket則用于代表一個(gè)網(wǎng)絡(luò)連接。在其他庫(kù)中,ISocketManager對(duì)應(yīng)的可能就是Server,而ISocket對(duì)應(yīng)
的則是Session。
在接口設(shè)計(jì)上,盡管Etwork寫(xiě)了很多接口類(lèi)(看看那些IClass),但是事實(shí)上它抽象得并不徹底。只是暴露給
客戶端的代碼很簡(jiǎn)潔,而庫(kù)本身依然臃腫。不知道為什么,現(xiàn)在我比較喜歡純C這種簡(jiǎn)潔的東西,對(duì)于OO以及
template,漸漸地有點(diǎn)心累。
在功能實(shí)現(xiàn)上,我以TCP服務(wù)器為例,CreateEtwork根據(jù)傳來(lái)的參數(shù)建立服務(wù)器,在SocketManager::open中
是很常規(guī)的socket, bind, listen。當(dāng)建立了服務(wù)器之后,需要在程序主循環(huán)里不斷地輪詢狀態(tài),這里主要
調(diào)用poll函數(shù)完成。
poll函數(shù)主體就是調(diào)用select。當(dāng)select成功返回活動(dòng)的套接字?jǐn)?shù)量后,Etwork依次輪詢讀、寫(xiě)、錯(cuò)誤fdset,
將保存的所有網(wǎng)絡(luò)連接(就是那些ISocket對(duì)象)對(duì)應(yīng)的套接字與fdset中當(dāng)前的套接字做比較。大致邏輯為:
fd_count = select( 0, readset, writeset, exceptset, &timeout );

for( each fd in readset )
if( fd is listening fd )
accept new connection
else
for( each socket in all connections )
if( fd == socket )
can read data on this socket

for( each fd in writeset )


for( each fd in exceptset )



沒(méi)什么特別讓人注意的地方(別覺(jué)得別人垃圾,耐心讀別人的代碼不是什么壞事)。每一次,當(dāng)Etwork檢測(cè)到
新的連接時(shí),會(huì)創(chuàng)建新的ISocket對(duì)象,并關(guān)聯(lián)對(duì)應(yīng)的套接字,然后保存此對(duì)象到一個(gè)列表中。當(dāng)poll結(jié)束
后,客戶端程序通常會(huì)調(diào)用accept函數(shù)(Etwork中提供的接口),該函數(shù)主要是將poll中保存的新的ISocket
對(duì)象全部拷貝出去。
在接收、發(fā)送網(wǎng)絡(luò)數(shù)據(jù)上,Etwork如同幾乎所有的網(wǎng)絡(luò)庫(kù)(模塊)一樣,采用了緩沖機(jī)制。這里所說(shuō)的緩沖機(jī)
制是,網(wǎng)絡(luò)模塊接收到網(wǎng)絡(luò)數(shù)據(jù)時(shí),將數(shù)據(jù)保存起來(lái),客戶端程序想獲取數(shù)據(jù)時(shí),實(shí)際上就是從這個(gè)緩沖中
直接取,而不是從網(wǎng)絡(luò)上獲取;同理,發(fā)送數(shù)據(jù)時(shí),客戶端程序?qū)?shù)據(jù)提供給網(wǎng)絡(luò)模塊,網(wǎng)絡(luò)模塊將數(shù)據(jù)保
存起來(lái),網(wǎng)絡(luò)模塊會(huì)在另一個(gè)時(shí)候發(fā)送這個(gè)緩沖中的數(shù)據(jù)(對(duì)于異步IO的處理畢竟不一樣)。
Etwork關(guān)于這個(gè)緩沖機(jī)制的相關(guān)代碼,主要集中在Buffer這個(gè)類(lèi)。與Buffer相關(guān)的是一個(gè)Message機(jī)制。Buffer
維護(hù)了一個(gè)Message的隊(duì)列(deque)。一個(gè)Message實(shí)際上是一個(gè)非常簡(jiǎn)單的結(jié)構(gòu)體:
struct Message


{
unsigned short offset_;
unsigned short size_;
};
這其實(shí)是消息頭,在消息頭后全部是數(shù)據(jù)。在創(chuàng)建消息時(shí)(new_message),Etwork根據(jù)客戶端提供的數(shù)據(jù)創(chuàng)建
足夠大的緩存保存:
Message * m = (Message *)::operator new( size + sizeof( Message ) );
這其實(shí)是一個(gè)很危險(xiǎn)的做法,但是從Etwokr的源碼可以看出來(lái),作者很喜歡玩弄這個(gè)技巧。與Buffer具體相
關(guān)的接口包括:get_data, put_data, get_message, put_message。Buffer內(nèi)部維護(hù)的數(shù)據(jù)都是以Message
的形式組織。但是,對(duì)于外部而言,卻依然是raw data,也就是諸如char*之類(lèi)的數(shù)據(jù)。幾個(gè)相關(guān)函數(shù)大致
上的操作為:獲取指定尺寸的消息(可能包含多個(gè)消息),將一段數(shù)據(jù)加入Buffer并以消息的形式組織(可能會(huì)
創(chuàng)建多個(gè)消息),將一個(gè)消息以raw data的形式輸出,將raw data以一個(gè)消息的形式加入到Buffer。
一般情況下,Etwork的poll操作,會(huì)將套接字上的數(shù)據(jù)接收并put_data到緩沖中;發(fā)送數(shù)據(jù)時(shí)則get_data。
客戶端要從緩沖中獲取數(shù)據(jù)時(shí),就調(diào)用get_message;發(fā)送數(shù)據(jù)時(shí)就put_message。
Etwork中還有一個(gè)比較有趣的東西:marshaller。這個(gè)東西主要就是提供將C++中各種數(shù)據(jù)類(lèi)型的變量進(jìn)行字
節(jié)編碼,也就是將int long struct之類(lèi)的東西轉(zhuǎn)換為unsigned char,從而方便直接往網(wǎng)絡(luò)上發(fā)送。
基本上,Buffer和marshaller可以說(shuō)是一個(gè)網(wǎng)絡(luò)庫(kù)(模塊)的必要部件,你可以在不同的網(wǎng)絡(luò)庫(kù)中看到類(lèi)似的
東西。
Etwork在網(wǎng)絡(luò)事件的處理上,除了上面的輪詢外,還支持回調(diào)機(jī)制。這主要是通過(guò)INotify,以及給各個(gè)ISocket
注冊(cè)Notify對(duì)象實(shí)現(xiàn)。沒(méi)什么難度,基本上就是observer模式的簡(jiǎn)單實(shí)現(xiàn)。
其他東西就沒(méi)什么好說(shuō)的了,縱觀一下,Etwork實(shí)現(xiàn)得還是比較典型的,可以作為開(kāi)發(fā)網(wǎng)絡(luò)庫(kù)的一個(gè)簡(jiǎn)單例子。
Author : Kevin Lynx
前言:
在很多比較各種網(wǎng)絡(luò)模型的文章中,但凡提到select模型時(shí),都會(huì)說(shuō)select受限于輪詢的套接字?jǐn)?shù)量,這個(gè)
數(shù)量也就是系統(tǒng)頭文件中定義的FD_SETSIZE值(例如64)。但事實(shí)上這個(gè)算不上真的限制。
C語(yǔ)言的偏方:
在C語(yǔ)言的世界里存在一個(gè)關(guān)于結(jié)構(gòu)體的偏門(mén)技巧,例如:
typedef struct _str_type


{
int _len;
char _s[1];
}str_type;
str_type用于保存字符串(我只是舉例,事實(shí)上這個(gè)結(jié)構(gòu)體沒(méi)什么用處),乍看上去str_type只能保存長(zhǎng)度為
1的字符串('\0')。但是,通過(guò)寫(xiě)下如下的代碼,你將突破這個(gè)限制:
int str_len = 5;
str_type *s = (str_type*) malloc( sizeof( str_type ) + str_len - 1 );
//
free( s );


這個(gè)技巧原理很簡(jiǎn)單,因?yàn)開(kāi)s恰好在結(jié)構(gòu)體尾部,所以可以為其分配一段連續(xù)的空間,只要注意指針的使用,
這個(gè)就算不上代碼上的罪惡。但是這個(gè)技巧有個(gè)限制,str_type定義的變量必須是被分配在堆上,否則會(huì)破
壞堆棧。另外,需要?jiǎng)討B(tài)增長(zhǎng)的成員需要位于結(jié)構(gòu)體的末尾。最后,一個(gè)忠告就是,這個(gè)是C語(yǔ)言里的技巧,
如果你的結(jié)構(gòu)體包含了C++的東西,這個(gè)技巧將不再安全(<Inside the C++ object model>)。
其實(shí)select也可以這樣做:
事實(shí)上,因?yàn)閟elect涉及到的fd_set是一個(gè)完全滿足上述要求的結(jié)構(gòu)體:
winsock2.h :


typedef struct fd_set
{

u_int fd_count; /**//* how many are SET? */

SOCKET fd_array[FD_SETSIZE]; /**//* an array of SOCKETs */
} fd_set;


但是,如果使用了以上技巧來(lái)增加fd_array的數(shù)量(也就是保存的套接字?jǐn)?shù)量),那么關(guān)于fd_set的那些宏可
能就無(wú)法使用了,例如FD_SET。
winsock2.h :

#define FD_SET(fd, set) do { \
u_int __i; \

for (__i = 0; __i < ((fd_set FAR *)(set))->fd_count; __i++)
{ \

if (((fd_set FAR *)(set))->fd_array[__i] == (fd))
{ \
break; \
} \
} \

if (__i == ((fd_set FAR *)(set))->fd_count)
{ \

if (((fd_set FAR *)(set))->fd_count < FD_SETSIZE)
{ \
((fd_set FAR *)(set))->fd_array[__i] = (fd); \
((fd_set FAR *)(set))->fd_count++; \
} \
} \
} while(0)


有點(diǎn)讓人眼花繚亂,我鼓勵(lì)你仔細(xì)看,其實(shí)很簡(jiǎn)單。這里有個(gè)小技巧,就是他把這些代碼放到一個(gè)do...while(0)
里,為什么要這樣做,我覺(jué)得應(yīng)該是防止名字污染,也就是防止那個(gè)__i變量與你的代碼相沖突。可以看出,
FD_SET會(huì)將fd_count與FD_SETSIZE相比較,這里主要是防止往fd_array的非法位置寫(xiě)數(shù)據(jù)。
因?yàn)檫@個(gè)宏原理不過(guò)如此,所以我們完全可以自己寫(xiě)一個(gè)新的版本。例如:
#define MY_FD_SET( fd, set, size ) do { \
unsigned int i = 0; \

for( i = 0; i < ((fd_set*) set)->fd_count; ++ i )
{ \

if( ((fd_set*)set)->fd_array[i] == (fd) )
{ \
break; \
} \
} \

if( i == ((fd_set*)set)->fd_count )
{ \

if( ((fd_set*)set)->fd_count < (size) )
{ \
((fd_set*)set)->fd_array[i] = (fd); \
((fd_set*)set)->fd_count ++; \
} \
} \
} while( 0 )


沒(méi)什么變化,只是為FD_SET加入一個(gè)fd_array的長(zhǎng)度參數(shù),宏體也只是將FD_SETSIZE換成這個(gè)長(zhǎng)度參數(shù)。
于是,現(xiàn)在你可以寫(xiě)下這樣的代碼:
unsigned int count = 100;
fd_set *read_set = (fd_set*) malloc( sizeof( fd_set ) + sizeof(SOCKET) * (count - FD_SETSIZE ) );
SOCKET s = socket( AF_INET, SOCK_STREAM, 0 );
//
MY_FD_SET( s, read_set, count );
//
free( read_set );
closesocket( s );


小提下select模型:
這里我不會(huì)具體講select模型,我只稍微提一下。一個(gè)典型的select輪詢模型為:
int r = select( 0, &read_set, 0, 0, &timeout );
if( r < 0 )


{
// select error
}

if( r > 0 )


{
for( each sockets )

{
if( FD_ISSET( now_socket, &read_set ) )

{
// this socket can read data
}
}
}


輪詢write時(shí)也差不多。在Etwork(一個(gè)超小型的基本用于練習(xí)網(wǎng)絡(luò)編程的網(wǎng)絡(luò)庫(kù),google yourself)中,作者
的輪詢方式則有所不同:
// read_set, write_set為采用了上文所述技巧的fd_set類(lèi)型的指針
int r = select( 0, read_set, write_set, 0, &timeout );
//
error handling
for( int i = 0; i < read_set->fd_count; ++ i )


{
// 輪詢所有socket,這里直接采用read_set->fd_array[i] == now_socket判斷,而不是FD_ISSET
}

for( int i = 0; i < write_set->fd_count; ++ i )


{
// 輪詢所有socket,檢查其whether can write,判斷方式同上
}


兩種方式的效率從代碼上看去似乎都差不多,關(guān)鍵在于,F(xiàn)D_ISSET干了什么?這個(gè)宏實(shí)際上使用了__WSAFDIsSet
函數(shù),而__WSAFDIsSet做了什么則不知道。也許它會(huì)依賴(lài)于FD_SETSIZE宏,那么這在我們這里將是不安全的,
所以相比之下,如果我們使用了這個(gè)突破FD_SETSIZE的偏方手段,那么也許第二種方式要好些。
相關(guān)下載(5.21.2008)
隨便寫(xiě)了一個(gè)改進(jìn)的select模型的echo服務(wù)器,放上源碼。
Author : Kevin Lynx
主要部分,四次握手:
斷開(kāi)連接其實(shí)從我的角度看不區(qū)分客戶端和服務(wù)器端,任何一方都可以調(diào)用close(or closesocket)之類(lèi)
的函數(shù)開(kāi)始主動(dòng)終止一個(gè)連接。這里先暫時(shí)說(shuō)正常情況。當(dāng)調(diào)用close函數(shù)斷開(kāi)一個(gè)連接時(shí),主動(dòng)斷開(kāi)的
一方發(fā)送FIN(finish報(bào)文給對(duì)方。有了之前的經(jīng)驗(yàn),我想你應(yīng)該明白我說(shuō)的FIN報(bào)文時(shí)什么東西。也就是
一個(gè)設(shè)置了FIN標(biāo)志位的報(bào)文段。FIN報(bào)文也可能附加用戶數(shù)據(jù),如果這一方還有數(shù)據(jù)要發(fā)送時(shí),將數(shù)據(jù)附
加到這個(gè)FIN報(bào)文時(shí)完全正常的。之后你會(huì)看到,這種附加報(bào)文還會(huì)有很多,例如ACK報(bào)文。我們所要把握
的原則是,TCP肯定會(huì)力所能及地達(dá)到最大效率,所以你能夠想到的優(yōu)化方法,我想TCP都會(huì)想到。
當(dāng)被動(dòng)關(guān)閉的一方收到FIN報(bào)文時(shí),它會(huì)發(fā)送ACK確認(rèn)報(bào)文(對(duì)于ACK這個(gè)東西你應(yīng)該很熟悉了)。這里有個(gè)
東西要注意,因?yàn)門(mén)CP是雙工的,也就是說(shuō),你可以想象一對(duì)TCP連接上有兩條數(shù)據(jù)通路。當(dāng)發(fā)送FIN報(bào)文
時(shí),意思是說(shuō),發(fā)送FIN的一端就不能發(fā)送數(shù)據(jù),也就是關(guān)閉了其中一條數(shù)據(jù)通路。被動(dòng)關(guān)閉的一端發(fā)送
了ACK后,應(yīng)用層通常就會(huì)檢測(cè)到這個(gè)連接即將斷開(kāi),然后被動(dòng)斷開(kāi)的應(yīng)用層調(diào)用close關(guān)閉連接。
我可以告訴你,一旦當(dāng)你調(diào)用close(or closesocket),這一端就會(huì)發(fā)送FIN報(bào)文。也就是說(shuō),現(xiàn)在被動(dòng)
關(guān)閉的一端也發(fā)送FIN給主動(dòng)關(guān)閉端。有時(shí)候,被動(dòng)關(guān)閉端會(huì)將ACK和FIN兩個(gè)報(bào)文合在一起發(fā)送。主動(dòng)
關(guān)閉端收到FIN后也發(fā)送ACK,然后整個(gè)連接關(guān)閉(事實(shí)上還沒(méi)完全關(guān)閉,只是關(guān)閉需要交換的報(bào)文發(fā)送
完畢),四次握手完成。如你所見(jiàn),因?yàn)楸粍?dòng)關(guān)閉端可能會(huì)將ACK和FIN合到一起發(fā)送,所以這也算不上
嚴(yán)格的四次握手---四個(gè)報(bào)文段。
在前面的文章中,我一直沒(méi)提TCP的狀態(tài)轉(zhuǎn)換。在這里我還是在猶豫是不是該將那張四處通用的圖拿出來(lái),
不過(guò),這里我只給出斷開(kāi)連接時(shí)的狀態(tài)轉(zhuǎn)換圖,摘自<The TCP/IP Guide>:
給出一個(gè)正常關(guān)閉時(shí)的windump信息:
14:00:38.819856 IP cd-zhangmin.1748 > 220.181.37.55.80: F 1:1(0) ack 1 win 65535
14:00:38.863989 IP 220.181.37.55.80 > cd-zhangmin.1748: F 1:1(0) ack 2 win 2920
14:00:38.864412 IP cd-zhangmin.1748 > 220.181.37.55.80: . ack 2 win 65535
補(bǔ)充細(xì)節(jié):
關(guān)于以上的四次握手,我補(bǔ)充下細(xì)節(jié):
1. 默認(rèn)情況下(不改變socket選項(xiàng)),當(dāng)你調(diào)用close( or closesocket,以下說(shuō)close不再重復(fù))時(shí),如果
發(fā)送緩沖中還有數(shù)據(jù),TCP會(huì)繼續(xù)把數(shù)據(jù)發(fā)送完。
2. 發(fā)送了FIN只是表示這端不能繼續(xù)發(fā)送數(shù)據(jù)(應(yīng)用層不能再調(diào)用send發(fā)送),但是還可以接收數(shù)據(jù)。
3. 應(yīng)用層如何知道對(duì)端關(guān)閉?通常,在最簡(jiǎn)單的阻塞模型中,當(dāng)你調(diào)用recv時(shí),如果返回0,則表示對(duì)端
關(guān)閉。在這個(gè)時(shí)候通常的做法就是也調(diào)用close,那么TCP層就發(fā)送FIN,繼續(xù)完成四次握手。如果你不調(diào)用
close,那么對(duì)端就會(huì)處于FIN_WAIT_2狀態(tài),而本端則會(huì)處于CLOSE_WAIT狀態(tài)。這個(gè)可以寫(xiě)代碼試試。
4. 在很多時(shí)候,TCP連接的斷開(kāi)都會(huì)由TCP層自動(dòng)進(jìn)行,例如你CTRL+C終止你的程序,TCP連接依然會(huì)正常關(guān)
閉,你可以寫(xiě)代碼試試。
特別的TIME_WAIT狀態(tài):
從以上TCP連接關(guān)閉的狀態(tài)轉(zhuǎn)換圖可以看出,主動(dòng)關(guān)閉的一方在發(fā)送完對(duì)對(duì)方FIN報(bào)文的確認(rèn)(ACK)報(bào)文后,
會(huì)進(jìn)入TIME_WAIT狀態(tài)。TIME_WAIT狀態(tài)也稱(chēng)為2MSL狀態(tài)。
什么是2MSL?MSL即Maximum Segment Lifetime,也就是報(bào)文最大生存時(shí)間,引用<TCP/IP詳解>中的話:“
它(MSL)是任何報(bào)文段被丟棄前在網(wǎng)絡(luò)內(nèi)的最長(zhǎng)時(shí)間。”那么,2MSL也就是這個(gè)時(shí)間的2倍。其實(shí)我覺(jué)得沒(méi)
必要把這個(gè)MSL的確切含義搞明白,你所需要明白的是,當(dāng)TCP連接完成四個(gè)報(bào)文段的交換時(shí),主動(dòng)關(guān)閉的
一方將繼續(xù)等待一定時(shí)間(2-4分鐘),即使兩端的應(yīng)用程序結(jié)束。你可以寫(xiě)代碼試試,然后用netstat查看下。
為什么需要2MSL?根據(jù)<TCP/IP詳解>和<The TCP/IP Guide>中的說(shuō)法,有兩個(gè)原因:
其一,保證發(fā)送的ACK會(huì)成功發(fā)送到對(duì)方,如何保證?我覺(jué)得可能是通過(guò)超時(shí)計(jì)時(shí)器發(fā)送。這個(gè)就很難用
代碼演示了。
其二,報(bào)文可能會(huì)被混淆,意思是說(shuō),其他時(shí)候的連接可能會(huì)被當(dāng)作本次的連接。直接引用<The TCP/IP Guide>
的說(shuō)法:The second is to provide a “buffering period” between the end of this connection
and any subsequent ones. If not for this period, it is possible that packets from different
connections could be mixed, creating confusion.
TIME_WAIT狀態(tài)所帶來(lái)的影響:
當(dāng)某個(gè)連接的一端處于TIME_WAIT狀態(tài)時(shí),該連接將不能再被使用。事實(shí)上,對(duì)于我們比較有現(xiàn)實(shí)意義的
是,這個(gè)端口將不能再被使用。某個(gè)端口處于TIME_WAIT狀態(tài)(其實(shí)應(yīng)該是這個(gè)連接)時(shí),這意味著這個(gè)TCP
連接并沒(méi)有斷開(kāi)(完全斷開(kāi)),那么,如果你bind這個(gè)端口,就會(huì)失敗。
對(duì)于服務(wù)器而言,如果服務(wù)器突然crash掉了,那么它將無(wú)法再2MSL內(nèi)重新啟動(dòng),因?yàn)閎ind會(huì)失敗。解決這
個(gè)問(wèn)題的一個(gè)方法就是設(shè)置socket的SO_REUSEADDR選項(xiàng)。這個(gè)選項(xiàng)意味著你可以重用一個(gè)地址。
對(duì)于TIME_WAIT的插曲:
當(dāng)建立一個(gè)TCP連接時(shí),服務(wù)器端會(huì)繼續(xù)用原有端口監(jiān)聽(tīng),同時(shí)用這個(gè)端口與客戶端通信。而客戶端默認(rèn)情況
下會(huì)使用一個(gè)隨機(jī)端口與服務(wù)器端的監(jiān)聽(tīng)端口通信。有時(shí)候,為了服務(wù)器端的安全性,我們需要對(duì)客戶端進(jìn)行
驗(yàn)證,即限定某個(gè)IP某個(gè)特定端口的客戶端。客戶端可以使用bind來(lái)使用特定的端口。
對(duì)于服務(wù)器端,當(dāng)設(shè)置了SO_REUSEADDR選項(xiàng)時(shí),它可以在2MSL內(nèi)啟動(dòng)并listen成功。但是對(duì)于客戶端,當(dāng)使
用bind并設(shè)置SO_REUSEADDR時(shí),如果在2MSL內(nèi)啟動(dòng),雖然bind會(huì)成功,但是在windows平臺(tái)上connect會(huì)失敗。
而在linux上則不存在這個(gè)問(wèn)題。(我的實(shí)驗(yàn)平臺(tái):winxp, ubuntu7.10)
要解決windows平臺(tái)的這個(gè)問(wèn)題,可以設(shè)置SO_LINGER選項(xiàng)。SO_LINGER選項(xiàng)決定調(diào)用close時(shí),TCP的行為。
SO_LINGER涉及到linger結(jié)構(gòu)體,如果設(shè)置結(jié)構(gòu)體中l(wèi)_onoff為非0,l_linger為0,那么調(diào)用close時(shí)TCP連接
會(huì)立刻斷開(kāi),TCP不會(huì)將發(fā)送緩沖中未發(fā)送的數(shù)據(jù)發(fā)送,而是立即發(fā)送一個(gè)RST報(bào)文給對(duì)方,這個(gè)時(shí)候TCP連
接就不會(huì)進(jìn)入TIME_WAIT狀態(tài)。
如你所見(jiàn),這樣做雖然解決了問(wèn)題,但是并不安全。通過(guò)以上方式設(shè)置SO_LINGER狀態(tài),等同于設(shè)置SO_DONTLINGER
狀態(tài)。
斷開(kāi)連接時(shí)的意外:
這個(gè)算不上斷開(kāi)連接時(shí)的意外,當(dāng)TCP連接發(fā)生一些物理上的意外情況時(shí),例如網(wǎng)線斷開(kāi),linux上的TCP實(shí)現(xiàn)
會(huì)依然認(rèn)為該連接有效,而windows則會(huì)在一定時(shí)間后返回錯(cuò)誤信息。
這似乎可以通過(guò)設(shè)置SO_KEEPALIVE選項(xiàng)來(lái)解決,不過(guò)不知道這個(gè)選項(xiàng)是否對(duì)于所有平臺(tái)都有效。
總結(jié):
個(gè)人感覺(jué),越寫(xiě)越爛。接下來(lái)會(huì)講到TCP的數(shù)據(jù)發(fā)送,這會(huì)涉及到滑動(dòng)窗口各種定時(shí)器之類(lèi)的東西。我真誠(chéng)
希望各位能夠多提意見(jiàn)。對(duì)于TCP連接的斷開(kāi),我們只要清楚:
1. 在默認(rèn)情況下,調(diào)用close時(shí)TCP會(huì)繼續(xù)將數(shù)據(jù)發(fā)送完畢;
2. TIME_WAIT狀態(tài)會(huì)導(dǎo)致的問(wèn)題;
3. 連接意外斷開(kāi)時(shí)可能會(huì)出現(xiàn)的問(wèn)題。
4. maybe more...
可能我這個(gè)人比較懷舊,對(duì)什么東西都想做個(gè)記錄,方便日后回憶。可能很多認(rèn)識(shí)我的朋友都是通過(guò)GameRes那個(gè)作品專(zhuān)
區(qū)。我對(duì)于當(dāng)年那種瘋狂編程的干勁很是自豪,現(xiàn)在差了很多,以前幫別人做小學(xué)生系列游戲外包的時(shí)候,可以12小時(shí)出
個(gè)弱智的小游戲,那些日子一度被我稱(chēng)為’12小時(shí)編程挑戰(zhàn)賽‘,只是自己跟自己比賽。
每一次發(fā)布在GameRes(排除早期的那些垃圾玩意),在寫(xiě)簡(jiǎn)介時(shí)我都要把自己開(kāi)發(fā)用的時(shí)間寫(xiě)上,可是脾氣好的sea_bug
每次都給我刪掉了。我自己匯總一下:
1. 最讓我自豪的一個(gè)游戲引擎,耗盡了我當(dāng)時(shí)所有的設(shè)計(jì)能力。我努力把它做得很具擴(kuò)展性,可是忽略了功能性。現(xiàn)在基本不維護(hù)了,可能是用戶群太少了。我想我還是沒(méi)做好吧:
edge2d google code page
托sea_bug的忙搞了個(gè)論壇,冷清得讓我心寒:http://bbs.gameres.com/showforum.asp?forumid=91
2. PacShooter3d:
http://data.gameres.com/showmessage.asp?TopicID=90655
不知道怎么的被人放到一個(gè)網(wǎng)站上了:http://noyes.cn/Software.Asp?id=9667
源代碼下載。
3. Space Demon demo
當(dāng)初看到dophi寫(xiě)的俄羅斯方塊營(yíng)造的那種感覺(jué)覺(jué)得很不錯(cuò),于是決定認(rèn)真地做個(gè)游戲出來(lái)。結(jié)果后來(lái)做的東西讓我很失望。這是一個(gè)在代碼上過(guò)度設(shè)計(jì)的東西。我雖然對(duì)這個(gè)游戲不滿意,但是我對(duì)代碼還基本滿意。后來(lái)這個(gè)游戲的代碼被我游戲?qū)W院的一個(gè)朋友拿給金山的一個(gè)主程(在他們學(xué)校教書(shū)?)看,還得到了表?yè)P(yáng)。;D
這個(gè)游戲我是直接開(kāi)源了的:http://www.gameres.com/showmessage.asp?TopicID=73123
4. Crazy Eggs Clone
<Crazy Eggs>是小林子他們工作室做的東西,屬于casual games,拿到國(guó)外去賣(mài)的。我當(dāng)時(shí)也覺(jué)得casual games市場(chǎng)不錯(cuò),還找了個(gè)美工,大談特談,吹噓了很多,最終在寫(xiě)策劃案的時(shí)候失敗了。我當(dāng)時(shí)心也懶了,最終失敗。
同樣是在GameRes上:http://www.gameres.com/showmessage.asp?TopicID=72351
源代碼下載。
后來(lái)我為了宣傳edge2d,特地把這個(gè)游戲移植到我的引擎上。我從來(lái)很自豪自己代碼的模塊性,所以移植起來(lái)很容易。除了edge2d版本,我還做了HGE版本,不過(guò)HGE版本是做給別人的外包:
edge2d版本下載
5. Brick Shooter Jr
這個(gè)游戲也是我翻版別人的,用的別人的美術(shù)+音樂(lè)資源,自己重寫(xiě)代碼。后來(lái)網(wǎng)上有個(gè)人又用我的資源翻作了個(gè),做的比我好。
http://data.gameres.com/showmessage.asp?TopicID=65654
源代碼下載
6. Feeding Frenzy
Popcap的經(jīng)典游戲,我做的垃圾東西,不提其他的了:
http://data.gameres.com/showmessage.asp?TopicID=62796
源代碼下載
7.是男人就下一百層
超級(jí)古老的東西,這個(gè)東西當(dāng)初還和上海一家廣告公司合作過(guò)。我簽署了長(zhǎng)這么大的第一份合同,結(jié)果后來(lái)一分錢(qián)沒(méi)撈到。他們公司現(xiàn)在也不做這個(gè)了。和我合作的產(chǎn)品經(jīng)理現(xiàn)在貌似在搞棋牌。
http://data.gameres.com/showmessage.asp?TopicID=54475
源代碼下載
8. 所謂的雷電,一個(gè)我最早做的東西,現(xiàn)在你開(kāi)baidu搜索 kevin lynx,出來(lái)最多的鏈接就是<雷電kevinlynx版>,別信那
些,全是流氓軟件。
http://data.gameres.com/showmessage.asp?TopicID=54474
源代碼下載
其他還給別人做了一些外包,在此特別感謝哆啦G夢(mèng)老大,給我找了很多工作。他這個(gè)人四處跳巢,還給我說(shuō)了幾次工作。
只是我還想暫時(shí)留在成都,所以都拒絕了。那些外包做的都比較垃圾,做到后來(lái)基本有個(gè)小游戲框架了。版權(quán)問(wèn)題可能不
能發(fā)布出來(lái)吧。
Author : Kevin Lynx
準(zhǔn)備:
在這里本文將遵循上一篇文章的風(fēng)格,只提TCP協(xié)議中的要點(diǎn),這樣我覺(jué)得可以更容易地掌握TCP。或者
根本談不上掌握,對(duì)于這種純理論的東西,即使你現(xiàn)在掌握了再多的細(xì)節(jié),一段時(shí)間后也會(huì)淡忘。
在以后各種細(xì)節(jié)中,因?yàn)槲覀儠?huì)涉及到分析一些TCP中的數(shù)據(jù)報(bào),因此一個(gè)協(xié)議包截獲工具必不可少。在
<TCP/IP詳解>中一直使用tcpdump。這里因?yàn)槲业南到y(tǒng)是windows,所以只好使用windows平臺(tái)的tcpdump,
也就是WinDump。在使用WinDump之前,你需要安裝該程序使用的庫(kù)WinpCap。
關(guān)于WinDump的具體用法你可以從網(wǎng)上其他地方獲取,這里我只稍微提一下。要讓W(xué)inDump開(kāi)始監(jiān)聽(tīng)數(shù)據(jù),
首先需要確定讓其監(jiān)聽(tīng)哪一個(gè)網(wǎng)絡(luò)設(shè)備(或者說(shuō)是網(wǎng)絡(luò)接口)。你可以:
windump -D
獲取當(dāng)前機(jī)器上的網(wǎng)絡(luò)接口。然后使用:
windump -i 2
開(kāi)始對(duì)網(wǎng)絡(luò)接口2的數(shù)據(jù)監(jiān)聽(tīng)。windump如同tcpdump(其實(shí)就是tcpdump)一樣支持過(guò)濾表達(dá)式,windump
將會(huì)根據(jù)你提供的過(guò)濾表達(dá)式過(guò)濾不需要的網(wǎng)絡(luò)數(shù)據(jù)包,例如:
windump -i 2 port 4000
那么windump只會(huì)顯示端口號(hào)為4000的網(wǎng)絡(luò)數(shù)據(jù)。
序號(hào)和確認(rèn)號(hào):
要講解TCP的建立過(guò)程,也就是那個(gè)所謂的三次握手,就會(huì)涉及到序號(hào)和確認(rèn)號(hào)這兩個(gè)東西。翻書(shū)到TCP
的報(bào)文頭,有兩個(gè)很重要的域(都是32位)就是序號(hào)域和確認(rèn)號(hào)域。可能有些同學(xué)會(huì)對(duì)TCP那個(gè)報(bào)文頭有所
疑惑(能看懂我在講什么的會(huì)產(chǎn)生這樣的疑惑么?),這里我可以告訴你,你可以假想TCP的報(bào)文頭就是個(gè)
C語(yǔ)言結(jié)構(gòu)體(假想而已,去翻翻bsd對(duì)TCP的實(shí)現(xiàn),肯定沒(méi)這么簡(jiǎn)單),那么大致上,所謂的TCP報(bào)文頭就是:
typedef struct _tcp_header


{

/**//// 16位源端口號(hào)
unsigned short src_port;

/**//// 16位目的端口號(hào)
unsigned short dst_port;

/**//// 32位序號(hào)
unsigned long seq_num;

/**//// 32位確認(rèn)號(hào)
unsigned long ack_num;

/**//// 16位標(biāo)志位[4位首部長(zhǎng)度,保留6位,ACK、SYN之類(lèi)的標(biāo)志位]
unsigned short flag;

/**//// 16位窗口大小
unsigned short win_size;

/**//// 16位校驗(yàn)和
short crc_sum;

/**//// 16位緊急指針
short ptr;

/**//// 可選選項(xiàng)
/// how to implement this ?
} tcp_header;

那么,這個(gè)序號(hào)和確認(rèn)號(hào)是什么?TCP報(bào)文為每一個(gè)字節(jié)都設(shè)置一個(gè)序號(hào),覺(jué)得很奇怪?這里并不是為每一
字節(jié)附加一個(gè)序號(hào)(那會(huì)是多么可笑的編程手法?),而是為一個(gè)TCP報(bào)文附加一個(gè)序號(hào),這個(gè)序號(hào)表示報(bào)文
中數(shù)據(jù)的第一個(gè)字節(jié)的序號(hào),而其他數(shù)據(jù)則是根據(jù)離第一個(gè)數(shù)據(jù)的偏移來(lái)決定序號(hào)的,例如,現(xiàn)在有數(shù)據(jù):
abcd。如果這段數(shù)據(jù)的序號(hào)為1200,那么a的序號(hào)就是1200,b的序號(hào)就是1201。而TCP發(fā)送的下一個(gè)數(shù)據(jù)包
的序號(hào)就會(huì)是上一個(gè)數(shù)據(jù)包最后一個(gè)字節(jié)的序號(hào)加一。例如efghi是abcd的下一個(gè)數(shù)據(jù)包,那么它的序號(hào)就
是1204。通過(guò)這種看似簡(jiǎn)單的方法,TCP就實(shí)現(xiàn)了為每一個(gè)字節(jié)設(shè)置序號(hào)的功能(終于明白為什么書(shū)上要告訴
我們‘為每一個(gè)字節(jié)設(shè)置一個(gè)序號(hào)’了吧?)。注意,設(shè)置序號(hào)是一種可以讓TCP成為’可靠協(xié)議‘的手段。
TCP中各種亂七八糟的東西都是有目的的,大部分目的還是為了’可靠‘兩個(gè)字。別把TCP看高深了,如果
讓你來(lái)設(shè)計(jì)一個(gè)網(wǎng)絡(luò)協(xié)議,目的需要告訴你是’可靠的‘,你就會(huì)明白為什么會(huì)產(chǎn)生那些亂七八糟的東西了。
接著看,確認(rèn)號(hào)是什么?因?yàn)門(mén)CP會(huì)對(duì)接收到的數(shù)據(jù)包進(jìn)行確認(rèn),發(fā)送確認(rèn)數(shù)據(jù)包時(shí),就會(huì)設(shè)置這個(gè)確認(rèn)號(hào),
確認(rèn)號(hào)通常表示接收方希望接收到的下一段報(bào)文的序號(hào)。例如某一次接收方收到序號(hào)為1200的4字節(jié)數(shù)舉報(bào),
那么它發(fā)送確認(rèn)報(bào)文給發(fā)送方時(shí),就會(huì)設(shè)置確認(rèn)號(hào)為1204。
大部分書(shū)上在講確認(rèn)號(hào)和序號(hào)時(shí),都會(huì)說(shuō)確認(rèn)號(hào)是序號(hào)加一。這其實(shí)有點(diǎn)誤解人,所以我才在這里廢話了
半天(高手寬容下:D)。
開(kāi)始三次握手:
如果你還不會(huì)簡(jiǎn)單的tcp socket編程,我建議你先去學(xué)學(xué),這就好比你不會(huì)C++基本語(yǔ)法,就別去研究vtable
之類(lèi)。
三次握手開(kāi)始于客戶端試圖連接服務(wù)器端。當(dāng)你調(diào)用諸如connect的函數(shù)時(shí),正常情況下就會(huì)開(kāi)始三次握手。
隨便在網(wǎng)上找張三次握手的圖:
如前文所述,三次握手也就是產(chǎn)生了三個(gè)數(shù)據(jù)包。客戶端主動(dòng)連接,發(fā)送SYN被設(shè)置了的報(bào)文(注意序號(hào)和
確認(rèn)號(hào),因?yàn)檫@里不包含用戶數(shù)據(jù),所以序號(hào)和確認(rèn)號(hào)就是加一減一的關(guān)系)。服務(wù)器端收到該報(bào)文時(shí),正
常情況下就發(fā)送SYN和ACK被設(shè)置了的報(bào)文作為確認(rèn),以及告訴客戶端:我想打開(kāi)我這邊的連接(雙工)。客戶
端于是再對(duì)服務(wù)器端的SYN進(jìn)行確認(rèn),于是再發(fā)送ACK報(bào)文。然后連接建立完畢。對(duì)于阻塞式socket而言,你
的connect可能就返回成功給你。
在進(jìn)行了鋪天蓋地的羅利巴索的基礎(chǔ)概念的講解后,看看這個(gè)連接建立的過(guò)程,是不是簡(jiǎn)單得幾近無(wú)聊?
我們來(lái)實(shí)際點(diǎn),寫(xiě)個(gè)最簡(jiǎn)單的客戶端代碼:
sockaddr_in addr;
memset( &addr, 0, sizeof( addr ) );
addr.sin_family = AF_INET;
addr.sin_port = htons( 80 );

/**//// 220.181.37.55
addr.sin_addr.s_addr = inet_addr( "220.181.37.55" );
printf( "%s : connecting to server.\n", _str_time() );
int err = connect( s, (sockaddr*) &addr, sizeof( addr ) );

主要就是connect。運(yùn)行程序前我們運(yùn)行windump:
windump -i 2 host 220.181.37.55
00:38:22.979229 IP noname.domain.4397 > 220.181.37.55.80: S 2523219966:2523219966(0) win 65535 <mss 1460,nop,nop,sackOK>
00:38:23.024254 IP 220.181.37.55.80 > noname.domain.4397: S 1277008647:1277008647(0) ack 2523219967 win 2920 <mss 1440,nop,nop,sackOK>
00:38:23.024338 IP noname.domain.4397 > 220.181.37.55.80: . ack 1 win 65535


如何分析windump的結(jié)果,建議參看<tcp/ip詳解>中對(duì)于tcpdump的描述。
建立連接的附加信息:
雖然SYN、ACK之類(lèi)的報(bào)文沒(méi)有用戶數(shù)據(jù),但是TCP還是附加了其他信息。最為重要的就是附加的MSS值。這個(gè)
可以被協(xié)商的MSS值基本上就只在建立連接時(shí)協(xié)商。如以上數(shù)據(jù)表示,MSS為1460字節(jié)。
連接的意外:
連接的意外我大致分為兩種情況(也許還有更多情況):目的主機(jī)不可達(dá)、目的主機(jī)并沒(méi)有在指定端口監(jiān)聽(tīng)。
當(dāng)目的主機(jī)不可達(dá)時(shí),也就是說(shuō),SYN報(bào)文段根本無(wú)法到達(dá)對(duì)方(如果你的機(jī)器根本沒(méi)插網(wǎng)線,你就不可達(dá)),
那么TCP收不到任何回復(fù)報(bào)文。這個(gè)時(shí)候,你會(huì)看到TCP中的定時(shí)器機(jī)制出現(xiàn)了。TCP對(duì)發(fā)出的SYN報(bào)文進(jìn)行
計(jì)時(shí),當(dāng)在指定時(shí)間內(nèi)沒(méi)有得到回復(fù)報(bào)文時(shí),TCP就會(huì)重傳剛才的SYN報(bào)文。通常,各種不同的TCP實(shí)現(xiàn)對(duì)于
這個(gè)超時(shí)值都不同,但是據(jù)我觀察,重傳次數(shù)基本上都是3次。例如,我連接一個(gè)不可達(dá)的主機(jī):
12:39:50.560690 IP cd-zhangmin.1573 > 220.181.37.55.1024: S 3117975575:3117975575(0) win 65535 <mss 1460,nop,nop,sackOK>
12:39:53.538734 IP cd-zhangmin.1573 > 220.181.37.55.1024: S 3117975575:3117975575(0) win 65535 <mss 1460,nop,nop,sackOK>
12:39:59.663726 IP cd-zhangmin.1573 > 220.181.37.55.1024: S 3117975575:3117975575(0) win 65535 <mss 1460,nop,nop,sackOK>
發(fā)出了三個(gè)序號(hào)一樣的SYN報(bào)文,但是沒(méi)有得到一個(gè)回復(fù)報(bào)文(廢話)。每一個(gè)SYN報(bào)文之間的間隔時(shí)間都是
有規(guī)律的,在windows上是3秒6秒9秒12秒。上面的數(shù)據(jù)你看不到12秒這個(gè)數(shù)據(jù),因?yàn)檫@是第三個(gè)報(bào)文發(fā)出的
時(shí)間和connect返回錯(cuò)誤信息時(shí)的時(shí)間之差。另一方面,如果連接同一個(gè)網(wǎng)絡(luò),這個(gè)間隔時(shí)間又不同。例如
直接連局域網(wǎng),間隔時(shí)間就差不多為500ms。
(我強(qiáng)烈建議你能運(yùn)行windump去試驗(yàn)這里提到的每一個(gè)現(xiàn)象,如果你在ubuntu下使用tcpdump,記住sudo :D)
出現(xiàn)意外的第二種情況是如果主機(jī)數(shù)據(jù)包可達(dá),但是試圖連接的端口根本沒(méi)有監(jiān)聽(tīng),那么發(fā)送SYN報(bào)文的這
方會(huì)收到RST被設(shè)置的報(bào)文(connect也會(huì)返回相應(yīng)的信息給你),例如:
13:37:22.202532 IP cd-zhangmin.1658 > 7AURORA-CCTEST.7100: S 2417354281:2417354281(0) win 65535 <mss 1460,nop,nop,sackOK>
13:37:22.202627 IP 7AURORA-CCTEST.7100 > cd-zhangmin.1658: R 0:0(0) ack 2417354282 win 0
13:37:22.711415 IP cd-zhangmin.1658 > 7AURORA-CCTEST.7100: S 2417354281:2417354281(0) win 65535 <mss 1460,nop,nop,sackOK>
13:37:22.711498 IP 7AURORA-CCTEST.7100 > cd-zhangmin.1658: R 0:0(0) ack 1 win 0
13:37:23.367733 IP cd-zhangmin.1658 > 7AURORA-CCTEST.7100: S 2417354281:2417354281(0) win 65535 <mss 1460,nop,nop,sackOK>
13:37:23.367826 IP 7AURORA-CCTEST.7100 > cd-zhangmin.1658: R 0:0(0) ack 1 win 0
可以看出,7AURORA-CCTEST.7100返回了RST報(bào)文給我,但是我這邊根本不在乎這個(gè)報(bào)文,繼續(xù)發(fā)送SYN報(bào)文。
三次過(guò)后connect就返回了。(數(shù)據(jù)反映的事實(shí)是這樣)
關(guān)于listen:
TCP服務(wù)器端會(huì)維護(hù)一個(gè)新連接的隊(duì)列。當(dāng)新連接上的客戶端三次握手完成時(shí),就會(huì)將其放入這個(gè)隊(duì)列。這個(gè)隊(duì)
列的大小是通過(guò)listen設(shè)置的。當(dāng)這個(gè)隊(duì)列滿時(shí),如果有新的客戶端試圖連接(發(fā)送SYN),服務(wù)器端丟棄報(bào)文,
同時(shí)不做任何回復(fù)。
總結(jié):
TCP連接的建立的相關(guān)要點(diǎn)就是這些(or more?)。正常情況下就是三次握手,非正常情況下就是SYN三次超時(shí),
以及收到RST報(bào)文卻被忽略。
Author : Kevin Lynx
TCP是TCP/IP協(xié)議簇中傳輸層上的一種網(wǎng)絡(luò)協(xié)議,它是一種面向連接的、可靠的協(xié)議。為了提供這種可靠性,
TCP實(shí)現(xiàn)了各種有效的機(jī)制、算法。為了從一種宏觀的角度去了解這個(gè)協(xié)議,這里先大致地提一下與之相關(guān)
的概念。
1. 什么是‘面向連接的’?
引用<TCP/IP協(xié)議詳解>中的概念:
面向連接意味著兩個(gè)使用TCP的應(yīng)用(通常是一個(gè)客戶和一個(gè)服務(wù)器)在彼此交換數(shù)據(jù)之前必須先建立
一個(gè)TCP連接。
2. 什么是‘三次握手’?
在建立TCP連接之前,兩個(gè)使用TCP的應(yīng)用需要交換三次網(wǎng)絡(luò)數(shù)據(jù)。這三個(gè)數(shù)據(jù)包的來(lái)往也就是所謂的‘
三次握手’。
3. 報(bào)文段segment
我們說(shuō)TCP是流式的網(wǎng)絡(luò)協(xié)議,那是因?yàn)椋瑧?yīng)用程序可以一直往TCP寫(xiě)數(shù)據(jù),無(wú)論你是逐byte,還是write
a chunk,TCP對(duì)應(yīng)用傳給它的數(shù)據(jù)進(jìn)行緩沖,直到緩沖數(shù)據(jù)達(dá)到一定尺寸才發(fā)送。可以看出,對(duì)于應(yīng)用
而言,TCP就像是stream的。但事實(shí)上,在TCP層,數(shù)據(jù)還是以塊為單位的。這個(gè)塊也就是所謂的報(bào)文段
segment。
4. 什么是MTU?
MTU即最大傳輸單元(Maximum Transmission Unit,MTU)是指一種通信協(xié)議的某一層上面所能通過(guò)的
大數(shù)據(jù)報(bào)大小(以字節(jié)為單位)。我個(gè)人目前的理解認(rèn)為,MTU是一個(gè)網(wǎng)絡(luò)在硬件層次上所允許的最大
數(shù)據(jù)包大小,例如以太網(wǎng)大概是1500字節(jié)。
5. 什么是MSS?
MSS即最大報(bào)文段大小(Maximum Segment Size),它是指TCP中一個(gè)報(bào)文段上附加的用戶數(shù)據(jù)的最大大小。
這里稍微說(shuō)下應(yīng)用層發(fā)送某個(gè)數(shù)據(jù)包時(shí)整個(gè)TCP/IP協(xié)議棧的操作過(guò)程:應(yīng)用層將自己的用戶數(shù)據(jù)傳給TCP
層(傳輸層),TCP在這些數(shù)據(jù)前添加自己的協(xié)議頭(簡(jiǎn)單地理解為附加一些數(shù)據(jù)),然后將數(shù)據(jù)交給
IP層(網(wǎng)絡(luò)層),IP層附加自己的協(xié)議頭,以此類(lèi)推。
雖然MSS意思是最大報(bào)文段大小,但事實(shí)上它是排除了協(xié)議頭的用戶數(shù)據(jù)。
6. MTU and MSS ?
可以簡(jiǎn)單地給你一個(gè)這樣的公示:mss = mtu - tcp_header_size - ip_header_size。
而通常,IP協(xié)議附加的協(xié)議頭大小和TCP的協(xié)議頭大小都是20字節(jié),所以通常的MSS為1460字節(jié)。
注意,這里說(shuō)的數(shù)字并不見(jiàn)得正確,因?yàn)镸SS是可以被協(xié)商的。各種協(xié)議頭也可能被添加附加數(shù)據(jù),但是
他們的關(guān)系是這樣的。
7. 什么是窗口大小?
找本TCP的書(shū)看下TCP數(shù)據(jù)包的包頭(本文多次使用數(shù)據(jù)包、報(bào)文的概念,我這里說(shuō)的都是一樣的),你會(huì)
發(fā)現(xiàn)那個(gè)16位的窗口大小。
窗口這個(gè)域?qū)τ谡麄€(gè)TCP協(xié)議都很重要。簡(jiǎn)單地說(shuō),窗口大小是指接收端的接收緩存的大小。上面說(shuō)了,應(yīng)用
在發(fā)數(shù)據(jù)的時(shí)候,TCP會(huì)緩存這些數(shù)據(jù),稍后發(fā)送。接收數(shù)據(jù)時(shí)也一樣,TCP接收數(shù)據(jù)并緩存起來(lái),直到應(yīng)用
調(diào)用recv之類(lèi)的函數(shù)取數(shù)據(jù)時(shí),TCP才將這些緩存數(shù)據(jù)清除。
TCP發(fā)送端會(huì)根據(jù)TCP接收端那個(gè)接收緩存大小決定發(fā)送多少數(shù)據(jù)(如何知道這個(gè)緩存大小?稍后給概念)。
這樣,TCP接收端的接收緩存才不至于緩沖溢出。
8. 提供可靠性的方法之一:ACK確認(rèn)?
這里還不敢提序號(hào)、確認(rèn)號(hào)、延時(shí)ACK等亂七八糟的東西。我只能告訴你,當(dāng)TCP發(fā)送某些數(shù)據(jù)給TCP接收方
時(shí),TCP接收方會(huì)發(fā)回一個(gè)確認(rèn)報(bào)文。TCP發(fā)送方收到這個(gè)確認(rèn)報(bào)文后,就可以確認(rèn)剛才發(fā)送的數(shù)據(jù)包成功到達(dá)。
為什么這個(gè)確認(rèn)報(bào)文叫ACK確認(rèn)(貌似是我臨時(shí)給的概念:D)?再翻到TCP包頭結(jié)構(gòu)那張圖,ACK是TCP包頭中
的1bit標(biāo)志位,如同SYN、PSH、RST之類(lèi)的標(biāo)志一樣,這些標(biāo)志都有一個(gè)專(zhuān)有的用途。當(dāng)ACK標(biāo)志位被設(shè)置為1
時(shí),我就稱(chēng)其為ACK確認(rèn)標(biāo)志,因?yàn)锳CK就是用于確認(rèn)報(bào)文段的。
在上面所說(shuō)的窗口大小中,我提到,發(fā)送方如何知道接收方的接收緩存大小呢?這也是通過(guò)確認(rèn)報(bào)文段實(shí)現(xiàn):
當(dāng)接收方接收到數(shù)據(jù)后,發(fā)送ACK確認(rèn)數(shù)據(jù)包給發(fā)送方,就設(shè)置包頭中的窗口域。
9. 提供可靠性的方法之二:各種定時(shí)器
TCP中會(huì)設(shè)置很多計(jì)時(shí)器,這些定時(shí)器大多用于超時(shí)重傳(老半天得不到回應(yīng),所以重傳數(shù)據(jù))。
10.什么是全雙工?
全雙工就是你可以同時(shí)在一個(gè)TCP連接上進(jìn)行數(shù)據(jù)的發(fā)送和接收。這種雙工特性也促使了關(guān)閉TCP連接時(shí)的四次
握手。
11.TODO : more concepts...
這里我盡量簡(jiǎn)單地介紹一些TCP中的概念,希望可以讓你有概括性的了解。預(yù)計(jì)下一節(jié)我會(huì)講講建立TCP連接的相關(guān)細(xì)節(jié)。
除了Stevens的<TCP/IP詳解>,我推薦<The TCP/IP Guide>,據(jù)說(shuō)是另一部TCP的權(quán)威之作。
預(yù)計(jì)新項(xiàng)目會(huì)選擇lua或python之一作為游戲的腳本語(yǔ)言。以前草草地接觸過(guò)這兩門(mén)語(yǔ)言,對(duì)于語(yǔ)法,以及嵌入進(jìn)C/C++程序都有點(diǎn)感性上的認(rèn)識(shí)。可能是受《UNIX編程藝術(shù)》中KISS原則的影響,現(xiàn)在總喜歡簡(jiǎn)潔的東西。所以我個(gè)人比較偏向于使用lua。
這兩天翻了下網(wǎng)絡(luò)上的資料,在lua的wiki上看到一篇比較lua和python的文章,草草地翻譯出要點(diǎn):
Python:
1. 擴(kuò)展庫(kù)很多,資料很多
2. 數(shù)值計(jì)算比較強(qiáng)大,支持多維數(shù)組,而lua沒(méi)有數(shù)組類(lèi)型
3. 本身帶的c類(lèi)型(?)支持處理動(dòng)態(tài)鏈接庫(kù),不需要進(jìn)行C封裝(C擴(kuò)展)
4. 遠(yuǎn)程調(diào)試器,似乎lua擴(kuò)展工具支持
5. 自然語(yǔ)言似的語(yǔ)法
6. 對(duì)于string和list的支持,lua可以通過(guò)擴(kuò)展庫(kù)實(shí)現(xiàn)
7. 對(duì)unicode的支持
8. 空格敏感(代碼不忽略空格),這其實(shí)可以使python的代碼風(fēng)格看起來(lái)更好一點(diǎn)
9. 內(nèi)建位操作,lua可以通過(guò)擴(kuò)展庫(kù)支持
10.語(yǔ)言本身對(duì)錯(cuò)誤的處理要好些,可以有效減少程序錯(cuò)誤
11.初級(jí)文檔比lua多
12.對(duì)面向?qū)ο笾С指?
Lua:
1. 比python小巧很多(包括編譯出來(lái)的運(yùn)行時(shí)庫(kù))
2. 占用更小的內(nèi)存
3. 解釋器速度更快
4. 比python更容易集成到C語(yǔ)言中
5. 對(duì)于對(duì)象不使用引用計(jì)數(shù)(引用計(jì)數(shù)會(huì)導(dǎo)致更多的問(wèn)題?)
6. lua早期定位于一種配置語(yǔ)言(作為配置文件),因此比起python來(lái)更容易配置數(shù)據(jù)
7. 語(yǔ)言更漂亮(nice)、簡(jiǎn)單(simple)、強(qiáng)大(powerful)。
8. lua支持多線程,每個(gè)線程可以配置獨(dú)立的解釋器,因此lua更適合于集成進(jìn)多線程程序
9. 對(duì)空格不敏感,不用擔(dān)心編輯器會(huì)將tab替換成空格
Useful Comments:
1. Everything is an object allocated on the heap in Python, including numbers. (So 123+456 creates a new heap object).
2. lua對(duì)于coroutine的支持更適用于嵌入進(jìn)游戲,雖然python也有,但是并沒(méi)有包含進(jìn)核心模塊
3.Python was a language better suited to Game AI
本來(lái)想去找點(diǎn)對(duì)于python的正面資料(嵌入進(jìn)游戲這方面),但是居然沒(méi)找到。客觀地說(shuō)如果單獨(dú)用python做應(yīng)用,python還是很有優(yōu)勢(shì)。現(xiàn)在心意已決,應(yīng)該向leader推薦lua。
ps,希望能補(bǔ)充以上兩種語(yǔ)言的特點(diǎn)。