發(fā)起一個(gè)TCP連接,4元組是必須的,即源IP,源端口,目標IP,目標端口。目標IP和端口都是確定的,源IP根據路由選擇或者bind也可以確定,基本上最終的源IP都是本機的IP地址,然而通過(guò)IP_TRANSPARENT參數可以bind一個(gè)不屬于本機的IP地址。唯一麻煩的就是源端口的確定。
在繼續深入源端口選擇算法之前,必須要認識到一個(gè)大的前提,也算是源端口選擇算法的一個(gè)大的目標,那就是“必須保證TCP四元組的唯一性”!有了這個(gè)前提以及終極目標,TCP源端口的選擇算法就非常容易理解了。在以下的情況下需要算法來(lái)選擇一個(gè)源端口:
1.調用bind,但是bind的端口是0的時(shí)候;
2.沒(méi)有調用bind,直接調用connect的時(shí)候。這兩種情況使不同的,因為在第一種情況下,4元組中的目標IP和目標端口是不確定的,而在第二種情況下,除了源端口,其它的都是知道的。所以?xún)煞N情況的端口分配算法是不同的。
1.bind情形的列維搜索算法
對于bind的情形,由于缺失信息,需要采用非常嚴格的方式選擇源端口,即要做到:只要有可能四元組沖突,就不能分配。比如已經(jīng)有一個(gè)連接的四元組為:Tuple1(IPsrc,PORTsrc,IPdst,PORTdst),現在為一個(gè)新建立的套接字bind一個(gè)源端口,其不bind任何確定的IP地址,那么它就不能使用PORTsrc這個(gè)端口作為源端口,因為它可能和Tuple1沖突,雖然僅僅是可能而已!如下是算法的實(shí)現:
- #include <stdio.h>
- #include <stdlib.h>
- #include <pthread.h>
-
- #define LOW 10000
- #define HIGH 65535
-
- //端口分配函數
- //base:一個(gè)(源IP,目標IP,目標端口)三元組的hash值
- int get_local_port(int base )
- {
- unsigned int i,j,port, remaining;
- again:
- remaining = (HIGH - LOW) + 1;
- //采用隨機的方式更容易找到空閑端口
- port = LOW + random()%remaining;
- for (i = 1; i <= remaining; i++) {
- int port_ok = 0;
- //判斷該端口是否可用,由于四元組唯一性現在由于信息不全無(wú)法判斷,先檢查最容易匹配的:
- //端口沒(méi)有處在TW狀態(tài),非LISTEN狀態(tài),可用
- //此處要保證的是,聚集者要越往外越少。
- port_ok = 1;
- if (port_ok) {
- goto check_inner;
- }
- //如果不合適就以port為基準,遞增
- port ++;
- }
- check_inner:
- {
- //更深層次,但更耗時(shí)的判斷
- int port_inner_ok;
- port_inner_ok = 1;
- if (!port_inner_ok) {
- goto again;
- }
- }
- return port;
- }
-
- //分配端口函數
- void func()
- {
- while(1) {
- int port = get_local_port(0);
- printf(" %d \n", port);
- sleep(1);
- }
- }
-
- //main函數
- int main(int argc, char **argv)
- {
- func();
- return 0;
- }
可以看到,算法從一個(gè)隨機計算出的值為基準端口,然后通過(guò)一系列的判斷來(lái)得到該端口是否可用的信息,一共是兩層的判斷,如果外層簡(jiǎn)單判斷發(fā)現不可用,則遞增端口數值重新判斷,如果內層復雜判斷該端口不可用,則重新計算隨機基準端口重新開(kāi)始。使用這個(gè)算法可以很快定位到一個(gè)可用的端口。通過(guò)算法可以看得出,它符合
列維模型,即在更近的局部細致掃描,然后飛躍到一個(gè)更遠的地方繼續列維查找。
實(shí)際生活中,這種搜索是很高效的,深夜找賓館,到一個(gè)陌生的城市找工作,警察搜山...信天翁覓食...都是
列維搜索!
2.connect情形的精確判定模式
connect的時(shí)候,四元組中的三元組已經(jīng)確定,因此可以精確匹配了,和bind時(shí)的端口選擇相反,此時(shí)只要有一個(gè)元組不同即可成功,記住我們的目標,即保證TCP四元組的唯一性!
確定性的查找不需要列維搜索,而是大家都可以根據順序遞增加簡(jiǎn)單沖突判定的方式進(jìn)行端口選擇,最合常理的方式就是,每一個(gè)三元組(源IP,目標IP,目標端口)都可以有一個(gè)65534個(gè)端口可供選擇,每次遞增即可。但是這樣的話(huà)需要為每一個(gè)端口維護一個(gè)計數器,Linux使用了更加巧妙的方法,可以采用為每一個(gè)三元組用哈希計算一個(gè)確定的基準端口,全局維護一個(gè)遞增的計數器,根據這個(gè)計數器與基準端口之和和端口空間大小做模運算,這樣的一個(gè)取模操作可以確定一個(gè)offset,加上最小端口確定一個(gè)候選端口,這樣就保證了候選端口和三元組的線(xiàn)性關(guān)系,也就是說(shuō),每一個(gè)三元組獨立選擇端口。
這么做的好處在于,對每一個(gè)三元組而言,都是從基準端口開(kāi)始順序分配的,相同三元組的端口都集中在一起,因為我們正是要和相同三元組的那些已經(jīng)確定的端口來(lái)比較,以判斷有沒(méi)有沖突,通過(guò)這種方式,將相同三元組的已經(jīng)分配的端口集中在了一起,省去了維護鏈表的麻煩,只需要從計算出的候選端口開(kāi)始線(xiàn)性搜索整個(gè)端口空間即可,由于全局計數器是遞增的,所以除非使用bind占據了某個(gè)端口,一般都會(huì )很快找到可用端口號,最多搜索幾個(gè)就能找到。算法如下所示:
- #include <stdio.h>
- #include <stdlib.h>
- #include <pthread.h>
-
- #define LOW 10000
- #define HIGH 65535
-
- static unsigned int hint = 0;
-
- //端口分配函數
- //base:一個(gè)(源IP,目標IP,目標端口)三元組的hash值
- int get_local_port(int base )
- {
- unsigned int i,j,port, remaining;
- unsigned int offset = hint + base;
- remaining = (HIGH - LOW) + 1;
-
- for (i = 1; i <= remaining; i++) {
- int port_ok = 0;
- port = LOW + (i + offset) % remaining;
- //判斷該端口是否可用,由于僅四元組唯一即可接受,現在假設:
- //所有的端口均已經(jīng)安全釋放。
- port_ok = 1;
- if (port_ok) {
- break;
- }
- }
- //越過(guò)你排除的那幾個(gè)(那些!)
- hint += i;
- return port;
- }
-
- //hint遞增函數
- int inc_hint(int value)
- {
- hint += value;
- }
-
- //分配端口線(xiàn)程
- void *func(void *arg)
- {
- while(1) {
- int port = get_local_port(0);
- printf(" %d \n", port);
- sleep(1);
- }
- }
-
- //hint遞增線(xiàn)程
- void *func_others(void *arg)
- {
- while (1) {
- int rnd = random();
- //其它的線(xiàn)程選擇源端口的時(shí)候,由于使用不同的(源IP,目標IP,目標端口)
- //不會(huì )有任何沖突,因此只模擬遞增hint即可。
- inc_hint (1);
- sleep(1);
- }
- }
-
- //main函數
- int main(int argc, char **argv)
- {
-
- pthread_t id[20] = {0};
- int i = 0, ret = 0;
- //一個(gè)線(xiàn)程不斷分配端口
- ret = pthread_create(&id[0], NULL, (void*)func, NULL);
- if (ret) {
- printf("Create pthread error!/n");
- return 1;
- }
- //N個(gè)線(xiàn)程模擬其它的端口分配,僅僅遞增hint
- for (i = 1; i < 20; i++) {
- ret = pthread_create(&id[i], NULL, (void*)func_others, NULL);
- if (ret) {
- printf("Create pthread error!/n");
- return 1;
- }
- }
- for (i = 0; i < 20; i++) {
- pthread_join(id[i], NULL);
- }
- return 0;
- }
關(guān)鍵點(diǎn)在于,三元組已經(jīng)確定,剩下的就是根據不同的三元組獨立遞增端口,效果就是同樣三元組的端口都聚集在一起,在無(wú)需鏈表的情況下高效判斷!
附:關(guān)于列維搜索
列維模型其實(shí)是一種概率分布模型,和泊松分布大大不同!它更多的體現在一種“生長(cháng),聚集”效應上。通俗來(lái)講就是,首先隨機確定一個(gè)點(diǎn),然后在該點(diǎn)附近進(jìn)行遍歷搜索,成功后則加入,達到閥值后依然沒(méi)有找到則退出,再次隨機生成一個(gè)點(diǎn),在該點(diǎn)附近搜索,以此類(lèi)推。這種模型有很精確的數學(xué)證明。如果對數學(xué)望而卻步,可以從生活中體會(huì )。剛畢業(yè)不久的人,可能會(huì )留在一個(gè)城市工作,兩三年內換了N份工作,接下來(lái)突然到達一個(gè)陌生的城市,從新開(kāi)始...這就是列維模型!本質(zhì)上講,整個(gè)人類(lèi)文明都是遵循列維模型,一開(kāi)始原始人從來(lái)不知道哪里適合居住,也不知道自己要到哪里去,只是漂泊蕩漾,但是過(guò)了千百萬(wàn)年以后,我們發(fā)現人類(lèi)的分布并不是平均的,也就是說(shuō),有些地方發(fā)展成了大都市,有些地方依然沒(méi)有人煙,這難道說(shuō)發(fā)展成都市的地方自然條件優(yōu)于沒(méi)有人煙的地方嗎?非也!列維模型在起作用,它正如莫菲法則一樣是個(gè)真理!
列維模型一直都在主宰著(zhù)我們,并且工作的很好,由于列維模型,我們現在擁有了典型的幾個(gè)不錯的國際化大都市...列維模型正如磁石一樣在發(fā)揮著(zhù)作用。它本質(zhì)上就是要把同類(lèi)同質(zhì)的東西聚集在一起!Linux在bind的時(shí)候分配端口正是使用這種列維搜索方式。列維模型天生具備一個(gè)閥值,即,在由于列維模型聚集在一起的東西超過(guò)閥值后,將不再聚集,而是選擇“長(cháng)跳”,即隨機到達一個(gè)比較遠的地方重新開(kāi)始聚集!列維模型總結起來(lái)就是,局部搜索,達到范圍閥值后,到一個(gè)很遠但是隨機的地方重新開(kāi)始局部搜索!如下圖所示: