HTTP是Web協議集中的重要協議,它是從客戶機/服務器模型發展起來的。客戶機/服務器是運行一對相互通訊的程序,客戶與服務器鏈接時,首先,向服務器提出請求,服務器根據客戶的請求,完成處理並給出響應。瀏覽器就是與Web服務器產生鏈接的客戶端程序,它的端口爲TCP的80端口。舉一個你們都很常見的例子,瀏覽器與Web服務器之間所遵循的協議就是HTTP。php
Web的應用層協議HTTP是Web的核心。HTTP在Web的客戶程序和服務器程序中得以實現。運行在不一樣端系統上的客戶程序和服務器程序經過交換HTTP消息彼此交流。HTTP定義這些消息的結構以及客戶和服務器如何交換這些消息。HTTP定義Web客戶(即瀏覽器)如何從web服務器請求Web頁面,以及服務器如何把Web頁面傳送給客戶。html
HTTP協議的優勢就是能夠解決傳輸數據的準確性、安全性較高、並且有處理網絡擁塞的能力。這主要的緣由就是HTTP協議傳輸數據的時候,是由HTTP都把TCP做爲底層的傳輸協議。HTTP客戶首先發起創建與服務器TCP鏈接。一旦創建鏈接,瀏覽器進程和服務器進程就能夠經過各自的套接字來訪問TCP。如前所述,客戶端套接字是客戶進程和TCP鏈接之間的「門」,服務器端套接字是服務器進程和同一TCP鏈接之間的門。客戶往本身的套接字發送HTTP請求消息,也從本身的套接字接收HTTP響應消息。相似的,服務器從本身的套接字接收HTTP請求消息,也往本身的套接字發送HTTP響應消息。客戶或服務器一旦把某個消息送入各自的套接字,這個消息就徹底落入TCP的控制之中。java
TCP給HTTP提供一個可靠的數據傳輸服務;這意味着由客戶發出的每一個HTTP請求消息最終將無損地到達服務器,由服務器發出的每一個HTTP響應消息最終也將無損地到達客戶。咱們可從中看到分層網絡體系結構的一個明顯優點——HTTP沒必要擔憂數據會丟失,也無需關心TCP如何從數據的丟失和錯序中恢復出來的細節。HTTP/1.1服務器端處理請求時按照收到的順序進行,這就保證了傳輸的正確性。固然,服務器端在發生鏈接中斷時,會自動的重傳請求,保證數據的完整性。node
值得注意的是HTTP協議的創建鏈接方式也很是特別,在早先的HTTP/1.0版本中使用的是非持久鏈接,而到了HTTP/1.1時非持久鏈接和持久鏈接都被支持,但默認使用持久鏈接。在此要說明非持久鏈接大概的過程是,舉一個瀏覽網頁的例子。首先客戶由與TCP鏈接相關聯的本地套接字發出—個HTTP請求消息;其次,服務器接受請求而且經由同一個套接字發送響應消息;而後HTTP服務器告知TCP關閉這個TCP鏈接(不過TCP要到客戶收到剛纔這個響應消息以後纔會真正終止這個鏈接);這時HTTP客戶經由同一個套接字接收這個響應消息後,TCP鏈接隨後終止。該消息明確標明所封裝的對象是一個什麼樣的文件。客戶從中取出這個文件,加以分析後發現其中的真正內容的引用。而後再對這些內容申請鏈接,最後瀏覽器就能夠顯示出網頁的內容了,假如網頁的內容是由5幅圖片組成的那麼完成對網頁的瀏覽就須要有6次TCP的鏈接,這是HTTP/1.0版本的作法。web
在HTTP/1.1中,在持久鏈接狀況下,服務器在發出響應後讓TCP鏈接繼續打開着。同一對客戶/服務器之間的後續請求和響應能夠經過這個鏈接發送。整個Web頁面(上例中爲包含一個基本HTML文件和5個圖像的頁面)能夠經過單個持久TCP鏈接發送,甚至存放在同一個服務器中的多個web頁面也能夠經過單個持久TCP鏈接發送。一般,HTTP服務器在某個鏈接閒置一段特定時間後關閉它,而這段時間一般是能夠配置的。持久鏈接分爲不帶流水線(without pipelining)和帶流水線(with pipelining)兩個版本。若是是不帶流水線的版本,那麼客戶只在收到前一個請求的響應後才發出新的請求。這種狀況下,web頁面所引用的每一個對象(上例中的5個圖像)都經歷1個RTT的延遲,用於請求和接收該對象。與非持久鏈接2個RTT的延遲相比,不帶流水線的持久鏈接已有所改善,不過帶流水線的持久鏈接還能進一步下降響應延遲。不帶流水線版本的另外一個缺點是,服務器送出一個對象後開始等待下一個請求,而這個新請求卻不能立刻到達。這段時間服務器資源便閒置了。HTTP/1.1的默認模式使用帶流水線的持久鏈接。這種狀況下,HTTP客戶每碰到一個引用就當即發出一個請求,於是HTTP客戶能夠一個接一個緊挨着發出各個引用對象的請求。服務器收到這些請求後,也能夠一個接一個緊挨着發出各個對象。若是全部的請求和響應都是緊挨着發送的,那麼全部引用到的對象一共只經歷1個RTT的延遲(而不是像不帶流水線的版本那樣,每一個引用到的對象都各有1個RTT的延遲)。另外,帶流水線的持久鏈接中服務器空等請求的時間比較少。與非持久鏈接相比,持久鏈接(不管是否帶流水線)除下降了1個RTT的響應延遲外,緩啓動延遲也比較小。其緣由在於既然各個對象使用同一個TCP鏈接,服務器發出第一個對象後就沒必要再以一開始的緩慢速率發送後續對象。相反,服務器能夠按照第一個對象發送完畢時的速率開始發送下一個對象。算法
從上面的例子不難看出,非持久鏈接中用戶的點擊致使瀏覽器發起創建一個與Web服務器的TCP鏈接,這裏涉及一個三次握手的過程:首先是客戶向服務器發送一個小的冗餘消息,接着是服務器向客戶確認並響應一個小的TCP消息,最後是客戶向服務器回確認。三次握手過程的前兩次結束時,流逝的時間爲1個RTT。此時客戶把HTTP請求消息發送到TCP鏈接中,客戶接着把三次握手過程最後一次中的確認捎帶,在包含這個消息的數據分節中發送出去。服務器收到來自TCP鏈接的請求消息後,把相應的HTML文件發送到TCP鏈接中,服務器接着把對早先收到的客戶請求的確認捎帶在包含該HTML文件的數據分節中發送出去。這個HTTP請求順應交互也花去1個RTT時間。很容易就能看出持久鏈接在時間損耗上的優點了。數據庫
另一個很是重要的地方就是HTTP協議的請求頭以及響應頭的格式。若是不清楚請求頭的格式就沒法向服務器端準確地發送下載請求;同時響應頭的得到信息,對下載工具下一步的處理也相當重要。下面列出HTTP請求消息:數組
GET/somedir/page.htmlHTTP/1.1瀏覽器
Host:www.hhit.edu.cn緩存
Connection:close
User-agent:Mozilla/4.0
Accept-language:zh-cn
(額外的回車符和換行符)
首先,這個消息是用普通的ASCII文本書寫的。其次,這個消息共有5行(每行以一個回車符和一個換行符結束),最後一行後面還有額外的一個回車和換行符。固然,一個請求消息能夠不止這麼多行,也能夠僅僅只有一行。該請求消息的第一行稱爲請求行(request line),後續各行都稱爲頭部行(header)。請求行有3個字段:方法字段、URL字段、HTTP版本字段。方法字段有若干個值可供選擇,包括GET、POST和HEAD。HTTP請求消息絕大多數使用GET方法,這是瀏覽器用來請求對象的方法,所請求的對象就在URL字段中標識。本例中瀏覽器實現的是HTTP/1.1版本。
上面例子中的請求消息有四行他們的意思分別是:
頭部行Host:www.hhit.edu.cn存放所請求對象的主機。
請求消息中包含頭部Connection:close是在告知服務器本瀏覽器不想使用持久鏈接;服務器發出所請求的對象後應關閉鏈接。儘管產生這個請求消息的瀏覽器實現的是HTTP/1.1版本,它仍是不想使用持久鏈接。
User-agent頭部行指定用戶代理,也就是產生當前請求的瀏覽器的類型。本例的用戶代理是Mozilla/4.0,它是Nelscape瀏覽器的一個版本。這個頭部行頗有用,由於服務器實際上能夠給不一樣類型的用戶代理髮送同一個對象的不一樣版本(這些不一樣版本位用同一個URL尋址)。
最後,Accept-languag:頭部行指出要是所請求對象有簡體中文版本,那麼用戶寧願接收這個版本;若是沒有這個語言版本,那麼服務器應該發送其默認版本。Accept-languag:僅僅是HTTP的衆多內容協商頭部之一。
HTTP協議請求消息的通常格式如圖:
圖 HTTP協議的請求信息格式
值得注意的是在請求消息的最後是附屬體,在咱們給出的例子中並無這一項。是由於,附屬體不在GET方法中使用,而是在POST方法中使用。POST方法適用於需由用戶填寫表單的場合,如往google搜索引擎中填入待搜索的詞。用戶提交表單後,瀏覽器就像用戶點擊了超連接那樣仍然從服務器請求一個Web頁面,不過該頁面的具體內容卻取決於用戶填寫在表單各個字段中的值。若是瀏覽器使用POST方法提出該請求,那麼請求消息附屬體中包含的是用戶填寫在表單各個字段中的值。
HTTP協議的響應消息和請求消息有所不一樣,下面是一個典型的響應消息:
HTTP/1.1 200 OK
Connection:close
Date:Thu,13Oct200503:17:33GMT
Server:Apache/2.0.54(Unix)
Last-Modified:Mon,22Jun199809;23;24GMT
Content-Length:682l
Content-Type:text/html
(數據數據數據數據數據…………)
這個響應消息分爲3部分:1個起始的狀態行(Status Line),6個頭部行、1個包含所請求對象自己的附屬體。狀態行有3個字段:協議版本字段、狀態碼字段、緣由短語字段。本例的狀態行代表,服務器使用HTTP/1.1版本,狀態碼(Status Codes)200表示響應過程徹底正常(也就是說服務器找到了所請求的對象,並正在發送)。
在上面的響應消息中頭部行的意思分別是:
服務器使用Connection:close頭部行告知客戶本身將在發送完本消息後關閉TCP鏈接。
Date頭部行:指出服務器建立併發送本響應消息的日期和時間。注意,這並非對象自己的建立時間或最後修改時間,而是服務器把該對象從其文件系統中取出,插入響應消息中發送出去的時間。
Server頭部行:指出本消息是由Apache服務器產生的;它與HTTP請求消息中的User-agent頭部行相似。
Last-Modified頭部行:指出對象自己的建立或最後修改日期或時間。Last-Modified頭部對於對象的高速緩存相當重要,且不論這種高速緩存是發生在本地客戶主機上仍是發生在網絡高速緩存服務器主機(也就是代理服務器主機)上。
Content-Length頭部行:指出所發送對象的字節數。
Content-Type頭部行:指出包含在附屬體中的對象是HTML文本。對象的類型是由Content—Type頭部而不是由文件擴展名正式指出的。
HTTP響應消息格式如圖:
圖 HTTP協議的響應信息格式
圖 完整的HTTP請求響應內容
在響應消息中的狀態行明顯多出了明顯多出了狀態碼(Status Codes)和緣由短語,是由於在請求的信息可能存在着不少狀況須要區分開,這樣也便於用戶可以清楚地知道請求的狀況以及出錯的緣由,響應消息中的狀態碼和緣由短語指示相應請求的處理結果。當服務器響應時,其狀態行的信息爲HTTP的版本號,狀態碼,及解釋狀態碼的簡單說明。現將5類狀態碼詳細列出:
1XX客戶方錯誤
100 繼續
101 交換協議
2XX成功
200 OK
201 已建立
202 接收
203 非認證信息
204 無內容
205 重置內容
206 部份內容
3XX重定向
300 多路選擇
301 永久轉移
302 暫時轉移
303 參見其它
304 未修改(Not Modified)
305 使用代理
4XX客戶方錯誤
400 錯誤請求(Bad Request)
401 未認證
402 須要付費
403 禁止(Forbidden)
404 未找到(Not Found)
405 方法不容許
406 不接受
407 須要代理認證
408 請求超時
409 衝突
410 失敗
411 須要長度
412 條件失敗
413 請求實體太大
414 請求URL太長
415 不支持媒體類型
5XX服務器錯誤
500 服務器內部錯誤
501 未實現(Not Implemented)
502 網關失敗
504 網關超時
505 HTTP版本不支持
要精確測量程序的運行時間很是困難,主要由於:
1. concurrent:OS併發運行多個process,在這些process間進行切換
2. 運行時間受到各類計算機環境的影響:如Cache的命中率
兩種測量機制:
1. 基於外部時鐘(Interval Counting)
計算機硬件中有一個external timer,其主要做用就是每一個必定時間(一般設置爲1-10 ms間某個數),向CPU發送中斷信號。CPU接收到該中斷信號後,當即執行相應的中斷處理程序。此時OS進入kernel mode,運行系統代碼,查看是否應該進行進程切換。利用這個external timer,OS能夠爲每一個進程記錄其所消耗的時間。
具體工做方式:
當timer中斷後,OS查看在中斷以前是哪一個process在運行,就把一次中斷間隔時間算在這個process的帳上。對每個process所花時間,實際上細分爲user time和system time,user time表示執行用戶程序所用時間,system time表示執行內核系統代碼所用時間,OS根據所在的mode(user mode和kernel mode)來區分。
如何使用Interval Counting來計時:
第一,Linux下的time命令
該命令能夠返回某個程序的運行時間的具體參數。
第二,C standard library <time.h>中clock函數
clock_t clock(void);
該函數返回當前進程到目前爲止所使用的clock數。
測量時間的方法:
準確性:這種方法最多能識別到interval 級別(10ms左右),並且因爲其實在中斷時將這個interval全記錄到前一刻的運行的那個process上,因此會造出偏差,但當運行時間較長時,這種偏差可相互抵消。所以,基於外部時鐘的方法,適用於運行時間 > 1s 的狀況,此時能夠測得比較準確的數據。
2. 基於CPU時鐘週期(Cycle Counters)
有些硬件CPU內部有一個寄存器專用於存放系統到目前爲止運行了多少個CPU時鐘週期。如interl 的IA32體系結構中有一個64位的寄存器用於該目的。使用該寄存器能夠測量程序運行的CPU時鐘週期數,進而算出運行時間。
問題:
和具體硬件相關,有可能一些硬件沒有這樣的寄存器,即便有,也要使用硬件相關的彙編語言來讀取該寄存器。
該寄存器並非和process相關,而是所有程序公用的,那麼進程切換怎麼辦?
所以,Cycle Counter方法比較適用於程序運行時間較短的狀況,理想狀況下 < 10ms最準確,由於此時進行進程切換的可能性較低。
如何使用:
第一,使用gettimeofday函數(POSIX標準),注意其底層究竟是使用什麼方式(Interval Counting仍是Cycle Counters)取決於具體的系統(硬件和OS)。
代碼參考Computer Systems ---- A programmer's perspective first edtion 官網下載的code中: code/perf/tod.c
第二,使用匯編語言直接訪問底層硬件,前提是硬件支持。
IA32平臺下代碼參考Computer Systems ---- A programmer's perspective first edtion 官網下載的code中: code/perf/clock.c
The K-Best Measurement Scheme:用於修正Cycle Counter方法中產生的偏差。具體參考Computer Systems ---- A programmer's perspective first edtion 9.4.3 節。
如何消除Cache的影響:按程序要運行的實際環境來模擬。若該程序在實際狀況下常常訪問新的數據,則能夠寫程序將Cache先清空;反之亦然。注意Cache有兩種,用於指令的cache和用於數據的cache。
總結:
1. 若是程序運行時間 大於1s,則採用interval counting就能夠取得比較精確數據。
2. 若是程序運行時間小於10ms,則採用cycle counters能夠獲取很是精確的數據,即便測試的時候系統負載很重(不少進程同時運行)。採用 K-Best Measurement Scheme,底層使用cycle counters。
3. 若是運行時間介於 10ms和1s之間,則想要獲取準確時間必須很是當心:採用 K-Best Measurement Scheme,底層使用cycle counters;同時測試的時候系統必須負載很輕(lightly loaded)。
This program is a code dump.
Code dumps are articles with little or no documentation or rearrangement of code. Please help to turn it into a literate program. Also make sure that the source of this code does consent to release it under the MIT or public domain license.
This is an implementation of a Bloom filter in C.
typedef unsigned int( * hashfunc_t ) (
const char* ) ;
typedef struct{ size_t asize ;
unsigned char* a ; size_t nfuncs ; hashfunc_t * funcs ; } BLOOM ; BLOOM * bloom_create ( size_t size
,size_t nfuncs
, ...) ;
intbloom_destroy ( BLOOM * bloom ) ;
intbloom_add ( BLOOM * bloom
, const char* s ) ;
intbloom_check ( BLOOM * bloom
, const char* s ) ; #endif
<<bloom.c>>= #include<limits.h> #include<stdarg.h> #include"bloom.h" #define SETBIT(a, n) (a[n/CHAR_BIT] |= (1<<(n%CHAR_BIT))) #define GETBIT(a, n) (a[n/CHAR_BIT] & (1<<(n%CHAR_BIT))) BLOOM * bloom_create ( size_t size
,size_t nfuncs
, ...) { BLOOM * bloom ; va_list l ;
intn ;
if( ! ( bloom = malloc (
sizeof( BLOOM ) ) ) )
returnNULL ;
if( ! ( bloom - > a = calloc ( ( size + CHAR_BIT - 1 )
/CHAR_BIT
, sizeof(
char) ) ) ) { free ( bloom ) ;
returnNULL ; }
if( ! ( bloom - > funcs = ( hashfunc_t * ) malloc ( nfuncs *
sizeof( hashfunc_t ) ) ) ) { free ( bloom - > a ) ; free ( bloom ) ;
returnNULL ; } va_start ( l
,nfuncs ) ;
for( n = 0 ; n < nfuncs ; + + n ) { bloom - > funcs [ n ] = va_arg ( l
,hashfunc_t ) ; } va_end ( l ) ; bloom - > nfuncs = nfuncs ; bloom - > asize = size ;
returnbloom ; }
intbloom_destroy ( BLOOM * bloom ) { free ( bloom - > a ) ; free ( bloom - > funcs ) ; free ( bloom ) ;
return0 ; }
intbloom_add ( BLOOM * bloom
, const char* s ) { size_t n ;
for( n = 0 ; n < bloom - > nfuncs ; + + n ) { SETBIT ( bloom - > a
,bloom - > funcs [ n ] ( s ) % bloom - > asize ) ; }
return0 ; }
intbloom_check ( BLOOM * bloom
, const char* s ) { size_t n ;
for( n = 0 ; n < bloom - > nfuncs ; + + n ) {
if( ! ( GETBIT ( bloom - > a
,bloom - > funcs [ n ] ( s ) % bloom - > asize ) ) )
return0 ; }
return1 ; }
<<test.c>>= #include<stdio.h> #include<string.h> #include"bloom.h"
unsigned intsax_hash (
const char* key ) {
unsigned inth = 0 ;
while( * key ) h
^= ( h
<<5 ) + ( h > > 2 ) + (
unsigned char) * key + + ;
returnh ; }
unsigned intsdbm_hash (
const char* key ) {
unsigned inth = 0 ;
while( * key ) h = (
unsigned char) * key + + + ( h < < 6 ) + ( h < < 16 ) - h ;
returnh ; }
intmain (
intargc
, char* argv [ ] ) { FILE * fp ;
charline [ 1024 ] ;
char* p ; BLOOM * bloom ;
if( argc < 2 ) { fprintf ( stderr
,"ERROR: No word file specified\n" ) ;
returnEXIT_FAILURE ; }
if( ! ( bloom = bloom_create ( 2500000
,2
,sax_hash
,sdbm_hash ) ) ) { fprintf ( stderr
,"ERROR: Could not create bloom filter\n" ) ;
returnEXIT_FAILURE ; }
if( ! ( fp = fopen ( argv [ 1 ]
,"r" ) ) ) { fprintf ( stderr
,"ERROR: Could not open file %s\n"
,argv [ 1 ] ) ;
returnEXIT_FAILURE ; }
while( fgets ( line
,1024
,fp ) ) {
if( ( p = strchr ( line
,'\r' ) ) ) * p = '\0' ;
if( ( p = strchr ( line
,'\n' ) ) ) * p = '\0' ; bloom_add ( bloom
,line ) ; } fclose ( fp ) ;
while( fgets ( line
,1024
,stdin ) ) {
if( ( p = strchr ( line
,'\r' ) ) ) * p = '\0' ;
if( ( p = strchr ( line
,'\n' ) ) ) * p = '\0' ; p = strtok ( line
," \t,.;:\r\n?!-/()" ) ;
while( p ) {
if( ! bloom_check ( bloom
,p ) ) { printf ( "No match for ford \"%s\"\n"
,p ) ; } p = strtok ( NULL
," \t,.;:\r\n?!-/()" ) ; } } bloom_destroy ( bloom ) ;
returnEXIT_SUCCESS ; }
<<Makefile>>= all:
bloombloom:
bloom.o test.o cc -o bloom -Wall -pedantic bloom.o test.obloom.o:
bloom.c bloom.h cc -o bloom.o -Wall -pedantic -ansi -c bloom.ctest.o:
test.c bloom.h cc -o test.o -Wall -pedantic -ansi -c test.c
BloomFilter——大規模數據處理利器
Bloom Filter是由Bloom在1970年提出的一種多哈希函數映射的快速查找算法。一般應用在一些須要快速判斷某個元素是否屬於集合,可是並不嚴格要求100%正確的場合。
一. 實例
爲了說明Bloom Filter存在的重要意義,舉一個實例:
假設要你寫一個網絡蜘蛛(web crawler)。因爲網絡間的連接錯綜複雜,蜘蛛在網絡間爬行極可能會造成「環」。爲了不造成「環」,就須要知道蜘蛛已經訪問過那些URL。給一個URL,怎樣知道蜘蛛是否已經訪問過呢?稍微想一想,就會有以下幾種方案:
1. 將訪問過的URL保存到數據庫。
2. 用HashSet將訪問過的URL保存起來。那隻需接近O(1)的代價就能夠查到一個URL是否被訪問過了。
3. URL通過MD5或SHA-1等單向哈希後再保存到HashSet或數據庫。
4. Bit-Map方法。創建一個BitSet,將每一個URL通過一個哈希函數映射到某一位。
方法1~3都是將訪問過的URL完整保存,方法4則只標記URL的一個映射位。
以上方法在數據量較小的狀況下都能完美解決問題,可是當數據量變得很是龐大時問題就來了。
方法1的缺點:數據量變得很是龐大後關係型數據庫查詢的效率會變得很低。並且每來一個URL就啓動一次數據庫查詢是否是過小題大作了?
方法2的缺點:太消耗內存。隨着URL的增多,佔用的內存會愈來愈多。就算只有1億個URL,每一個URL只算50個字符,就須要5GB內存。
方法3:因爲字符串通過MD5處理後的信息摘要長度只有128Bit,SHA-1處理後也只有160Bit,所以方法3比方法2節省了好幾倍的內存。
方法4消耗內存是相對較少的,但缺點是單一哈希函數發生衝突的機率過高。還記得數據結構課上學過的Hash表衝突的各類解決方法麼?若要下降衝突發生的機率到1%,就要將BitSet的長度設置爲URL個數的100倍。
實質上上面的算法都忽略了一個重要的隱含條件:容許小几率的出錯,不必定要100%準確!也就是說少許url實際上沒有沒網絡蜘蛛訪問,而將它們錯判爲已訪問的代價是很小的——大不了少抓幾個網頁唄。
二. Bloom Filter的算法
廢話說到這裏,下面引入本篇的主角——Bloom Filter。其實上面方法4的思想已經很接近Bloom Filter了。方法四的致命缺點是衝突機率高,爲了下降衝突的概念,Bloom Filter使用了多個哈希函數,而不是一個。
Bloom Filter算法以下:
建立一個m位BitSet,先將全部位初始化爲0,而後選擇k個不一樣的哈希函數。第i個哈希函數對字符串str哈希的結果記爲h(i,str),且h(i,str)的範圍是0到m-1 。
(1) 加入字符串過程
下面是每一個字符串處理的過程,首先是將字符串str「記錄」到BitSet中的過程:
對於字符串str,分別計算h(1,str),h(2,str)…… h(k,str)。而後將BitSet的第h(1,str)、h(2,str)…… h(k,str)位設爲1。
圖1.Bloom Filter加入字符串過程
很簡單吧?這樣就將字符串str映射到BitSet中的k個二進制位了。
(2) 檢查字符串是否存在的過程
下面是檢查字符串str是否被BitSet記錄過的過程:
對於字符串str,分別計算h(1,str),h(2,str)…… h(k,str)。而後檢查BitSet的第h(1,str)、h(2,str)…… h(k,str)位是否爲1,若其中任何一位不爲1則能夠斷定str必定沒有被記錄過。若所有位都是1,則「認爲」字符串str存在。
若一個字符串對應的Bit不全爲1,則能夠確定該字符串必定沒有被Bloom Filter記錄過。(這是顯然的,由於字符串被記錄過,其對應的二進制位確定所有被設爲1了)
可是若一個字符串對應的Bit全爲1,其實是不能100%的確定該字符串被Bloom Filter記錄過的。(由於有可能該字符串的全部位都恰好是被其餘字符串所對應)這種將該字符串劃分錯的狀況,稱爲false positive 。
(3) 刪除字符串過程
字符串加入了就被不能刪除了,由於刪除會影響到其餘字符串。實在須要刪除字符串的可使用Counting bloomfilter(CBF),這是一種基本Bloom Filter的變體,CBF將基本Bloom Filter每個Bit改成一個計數器,這樣就能夠實現刪除字符串的功能了。
Bloom Filter跟單哈希函數Bit-Map不一樣之處在於:Bloom Filter使用了k個哈希函數,每一個字符串跟k個bit對應。從而下降了衝突的機率。
三. Bloom Filter參數選擇
(1)哈希函數選擇
哈希函數的選擇對性能的影響應該是很大的,一個好的哈希函數要能近似等機率的將字符串映射到各個Bit。選擇k個不一樣的哈希函數比較麻煩,一種簡單的方法是選擇一個哈希函數,而後送入k個不一樣的參數。
(2)Bit數組大小選擇
哈希函數個數k、位數組大小m、加入的字符串數量n的關係能夠參考參考文獻1。該文獻證實了對於給定的m、n,當 k = ln(2)* m/n 時出錯的機率是最小的。
同時該文獻還給出特定的k,m,n的出錯機率。例如:根據參考文獻1,哈希函數個數k取10,位數組大小m設爲字符串個數n的20倍時,false positive發生的機率是0.0000889 ,這個機率基本能知足網絡爬蟲的需求了。
四. Bloom Filter實現代碼
下面給出一個簡單的Bloom Filter的Java實現代碼:
參考文獻:
[1]Pei Cao. Bloom Filters - the math.
http://pages.cs.wisc.edu/~cao/papers/summary-cache/node8.html
[2]Wikipedia. Bloom filter.
我這裏介紹一個極適合大量URL快速排重的方法 ,這個算法被稱爲Bloom filter,基本上,它也只適合這樣的場合。
這裏的大量是指有5000萬至1億的URL,更大的數據量可能也不合適了。
一開始我使用了一個最複雜的作法,是有一個單獨的daemon程序負責排重,數據和排重結果經過socket傳輸。
後來發現不行,僅僅幾百萬數據要作好幾個小時,5000萬不把人都急瘋了?至於daemon中具體用什麼算法就次要了,由於一涉及到網絡通信,速度再快也被拉下來(這裏針對的是發送一條記錄/返回一條結果的模式,一次傳送一批數據則與網絡情況有關了)
因此,把目標鎖定在單機排重,一開始,試驗了perl中的hash,很是簡單的代碼
從標準輸入或文件中每行一個URL讀入,插入到perl內置的hash表中,這就成了,須要輸出結果則預先判斷一下插入的key是否存在。
這個方法速度很快,惋惜的是,它佔用內存太大,假設1個URL平均50字節,5000萬個URL須要2.5G內存。
因而又想到一個方法,把部分數據放入硬盤空間,perl中也提供一個現成的模塊DB_File,把上面代碼中的註釋去掉,就可以使用DB_File了,用法與hash同樣,只是內部用數據庫實現的。
測試了一下,速度明顯降低了一個檔次,僅40萬的數據就要1分鐘,關鍵還在於隨着數據量的增長,速度降低加快,二者不呈線性關係。
數據量大的時候,有可能用MySQL的性能會比DB_File好,可是整體上應該是一丘之貉,我已經不抱指望了。
也許DB_File能夠優化一下,使用更多的內存和少許的硬盤空間,不過這個方案仍是太複雜,留給專家解決吧,通常來講,我認爲簡單的方法纔有可能作到高效。
下面咱們的重點對象隆重登場:Bloom filter。簡單的說是這樣一種方法:在內存中開闢一塊區域,對其中全部位置0,而後對數據作10種不一樣的hash,每一個hash值對內存bit數求模,求模獲得的數在內存對應的位上置1。置位以前會先判斷是否已經置位,每次插入一個URL,只有當所有10個位都已經置1了才認爲是重複的。
若是對上面這段話不太理解,能夠換個簡單的比喻:有10個桶,一個桶只能容納1個球,每次往這些桶中扔兩個球,若是兩個桶都已經有球,才認爲是重複,問問爲了避免重複總共能扔多少次球?
10次,是這個答案吧?每次扔1個球的話,也是10次。表面上看一次扔幾個球沒有區別,事實上一次兩個球的狀況下,重複機率比一次一個球要低。Bloom filter算法正式藉助這一點,僅僅用少許的空間就能夠進行大量URL的排重,而且使誤判率極低。
有人宣稱爲每一個URL分配兩個字節就能夠達到0衝突,我比較保守,爲每一個URL分配了4個字節,對於5000萬的數量級,它只佔用了100多M的空間,而且排重速度超快,一遍下來不到兩分鐘,極大得知足了個人慾望。
今天時間不夠,下一篇再貼代碼
接上篇,起初我爲了輸入輸出方便,是用perl去實現的,後來發現perl中求模速度太慢,就改用C了
常量定義:SPACE指你要分配多大的內存空間,我這裏是爲5000萬數據的每一條分配4字節
主程序:這裏循環讀入標準輸入的每一行,進行排重。
斷定函數:我沒有作Bloom filter算法中描述的10次hash,而是作了一個MD5,一個SHA1,而後摺合成9次hash。
gcc 編譯時添加 -lcrypto
主要算法就在這裏了,實際應用的話能夠採用循環監視磁盤文件的方法來讀入排重數據,那些代碼就與操做系統相關,不必在這寫了