BT網站-奧修磁力-Python開發爬蟲代替.NET,奧修-python
BT網站-奧修磁力-Python開發爬蟲代替.NET寫的爬蟲,主要示範訪問速度和在一千萬左右的HASH記錄中索引效率。
奧修磁力下載-http://www.oshoh.com 現在用的是Python +CENTOS 7 系統
奧修磁力下載(www.oshoh.com)經曆了多次點技術變更。開源版本使用了django網站架構重寫,之前是Flask,再早期是tornado。電影FM也是使用tornado,後來發現tornado並不適用於任何情境。以內容為王的網站還是django比較擅長,只是入門時間比其他架構都較長。早期資料庫採用了MongoDB,因為配合Python讀寫資料很方便,也不用關注資料結構,搜尋功能採用內建的關鍵詞搜尋,不過後來隨著資源數量增加,效能也明顯跟不上。今年換了WiredTiger引擎,內建的fulltext search還是不給力。另外Amazon的cloudsearch是個坑,土豪可以考慮,效能真的很不錯,就是比較貴。最後還是搭建一個SphinxSearch吧,資料庫也換成MySQL(MyISAM引擎),配合起來也很方便。Sphinx建立全文索引的速度很給力,官方的自評也很高,我自己測試1000w的資源(大概3GB),1分鐘左右就索引完畢。不信,大家可以自測一下。
原理如下:
BitTorrent DHT協議 BitTorrent使用一種分布的、寬鬆的雜湊表(DHT)為無法track的torrent儲存對等點聯絡資訊。這樣,每個peer都成為一個 tracker。這個協議基於Kademlia在UDP上實現。
請注意文中使用的術語,以免引起混淆。peer是一個監聽在TCP連接埠上,實現BitTorrent協議的用戶端/伺服器。節點(node)是一個監聽在UDP連接埠上,實現DHT協議的用戶端/伺服器。DHT網路由節點群組成,儲存peer的位置資訊。BitTorrent用戶端包含一個DHT 節點,並通過這個節點聯絡DHT中的其他節點,以擷取其它peer的位置,這樣就可以從它們那裡通過BitTorrent協議下載了。
綜述
每個節點都有一個全域唯一的標識符,稱為節點 ID。 節點 ID從160bit空間中隨機選取,與BitTorrent的infohash值的空間相同。距離度量用來比較兩個節點或者節點與infohash之間的遠近程度。節點必須維護一個含有少量其他節點聯絡資訊的路由表。ID越靠近自身ID時,路由表越詳細。節點知道很多離它很近的節點,只知道少量離它很遠地節點。
在Kademlia中,距離度量採用異或計算,結果解釋成一個不帶正負號的整數。
distance (A,B)=|A ? B|
值越小,距離越近。
當一個節點需要尋找一個torrent的peer時,它計算torrent的infohash和本地路由表中的節點 ID的距離。然後向與該torrent最近的一些節點請求該當前正在下載該torrent的peer資訊。如果某個節點有這些資訊,就直接回複。否則,它必須回複在它的路由表中離這個torrent更近的節點。如此不斷重複的搜尋更近的節點,直到找不到。當搜尋結束之後,peer將自己的聯絡資訊註冊到離 torrent最近的節點。
查詢peer請求的傳回值(包含一個不透明值),稱之為令牌。當一個節點通知其他節點它的peer正在下載一個torrent的時候,它必須使用最近向那個節點查詢peer請求時,獲得的令牌。當一個節點試圖公告一個torrent,它所請求過的節點根據這個節點的IP地址檢查它的令牌是否有效。這個機制可以防止惡意主機向其他主機註冊torrent。由於令牌僅僅由請求節點返回給它所接收到令牌的那個節點,所以並沒有規定具體實現。令牌應該在分發出去之後的一段合理時間內被接受。BitTorrent的實現是,用對方的IP地址和一個密碼(這個密碼每五分鐘更換一次),計算SHA1作為令牌,這個令牌的有效時間是十分鐘。
路由表
每個節點維護一個路由表,由它所知道的好節點群組成。路由表中的節點被用作在DHT中發送請求的起點。當其他節點查詢時,就返迴路由表中的節點。
並不是我們所知的每個節點都是一樣的。一些是好的,而另一些不是。很多使用DHT的節點都可以發送請求和接收應答,但是不能應答其他節點的請求。有一點很重要:每個節點路由表中的節點都應該是好節點。一個節點在過去的15分鐘內應答過本節點的的某個請求,它就是一個好節點;如果它曾經應答過本節點的某個請求,並且在過去的15分鐘內向本節點發送過請求,它也是一個好節點。如果一個節點15分鐘沒有活動,它就變成可疑節點。如果它連續多次未能應答請求,它就變成壞節點了。我們所知的好節點被賦予較未知狀態的節點更高的優先順序。
路由表覆蓋整個節點ID空間(從0到2160)。路由表被細分成“桶”,每個桶覆蓋一部分空間。一張空表只有一個桶,它覆蓋的空間是min=0,max=2160 。當一個ID是N的節點插入路由表中時,它被放進min<=N<max的桶中。一張空表只有一個桶,因此任何一個節點都可以放進去。每個桶在裝滿之前,最多隻能存放K個節點,目前是8個。當一個桶裝滿了好節點之後,就不能再往裡面加入別的節點,除非當前節點的ID落入桶的覆蓋範圍之內。這樣的話,這個桶將被兩個新桶替換掉,兩個新桶分別覆蓋原來一半的空間,並且原來桶裡面的節點重新分發到新桶之中。對一張只有一個桶的新表來說,滿的桶總是被分割成兩個分別覆蓋0-2159和2159-2160的桶。
當一個桶裝滿了好的節點,新的節點將被丟棄。如果桶中的某個節點變壞了,那麼它將被一個新節點替換。如果桶中一些可疑節點長達15分鐘沒有活動,最久不活躍節點將被ping。如果被ping節點響應了,那麼將依次ping下一個最久不活躍可疑節點,直到某一個未能響應,或者桶中的所有節點都是好的了。如果桶中的某個節點未能響應ping,建議在丟棄並用新的好節點替換它之前再試一次。這樣,路由表中將填滿穩定的長期活躍的節點。
每個桶都要維護一個最後變化屬性來標誌它的新舊程度。當桶中的一個節點被ping而且回複了,或者一個節點加入桶中,或者一個節點被另一個節點替換,桶的最後變化屬性將被更新。桶如果15分鐘沒有變化,就應該重新整理——從它覆蓋的ID空間中隨機播放一個ID,在上面執行一個 find_nodes搜尋。可以接收其他節點請求的節點通常不需要經常重新整理桶。不能接收其他節點請求的節點則需要定期重新整理所有桶,保證在DHT需要的時候路由表中的都是好節點。
啟動時,節點在它的路由表中插入第一個節點,然後應該嘗試尋找DHT中最近鄰的其他節點——向鄰近節點發送find_node命令,再向更近的節點發送該命令,直到不能找到更近鄰的節點。在用戶端軟體每次調用路由表時,應該儲存路由表。
BitTorrent協議擴充
BitTorrent協議被擴充了,以便讓tracker所告知的peer相互之間可以交換UDP連接埠號碼。這樣,用戶端就可以獲得常規torrent下載時自動產生的路由表。新安裝的用戶端第一次試圖下載一個無法track的torrent時,路由表中沒有任何節點,需要torrent中的聯絡資訊。
支援DHT的peer設定在BitTorrent協議握手交換的保留標誌位8位元組的最後一位。peer接收到遠程節點的握手訊息,如果標誌支援DHT,那麼應該回複一個PORT訊息。它以0x09開始,然後是兩位元組的UDP連接埠,採用網路位元組順序。peer接收到這個訊息應該試圖ping遠程peer上對應IP和連接埠的那個節點。如果收到ping的響應,這個節點應該嘗試按照通常規則,把這個新的聯絡資訊插入路由表中。
Torrent檔案擴充
一個無法track的torrent字典中不包括"announce"這個鍵,而是有一個"nodes"鍵。這個鍵應該設定成離產生torrent的節點路由表中最近的K個節點。或者,由產生torrent的人把這個鍵設定成已知的好節點。請不要把"router.bittorrent.com"自動加入 torrent檔案中,也不要把它加入到用戶端的路由表中。
DE<nodes = [["<host>", <port>], ["<host>", <port>], ...] nodes = [["127.0.0.1", 6881], ["your.router.node", 4804]]DE<
KRPC協議
KRPC協議是一個簡單的RPC機制,由在UDP上發送的bencode字典組成。發送一個請求包,回複一個響應包,沒有重試。有三種訊息類型: query, response, error。對於DHT協議,有四種query :ping, find_node, get_peers, announce_peer。
一個KRPC訊息是一個字典,包括兩個通用鍵和多個依訊息類型而定的其他鍵。每個訊息都有一個"t"鍵和一個字串值,代表transaction ID。這個transaction ID由請求節點產生,在回複的時候回顯。這樣回複可以關聯同一個節點的多個請求。KRPC中的另一個通用鍵是"y",也是一個字串作為值,表示訊息的類型。"y"的值有:"q"(query,請求),"r"(response,回複,響應),"e"(error,錯誤)。
聯絡編碼
peer的聯絡資訊編碼成一個6位元組的串,也叫"緊密的IP地址/連接埠資訊",4位元組的IP地址緊接2位元組連接埠號碼,都是採用網路位元組順序。
節點的聯絡資訊編碼成一個26位元組的串,也叫"緊密的節點資訊",20位元組的節點ID緊接緊密的IP地址/連接埠資訊,同樣採用網路位元組順序。
請求
請求,即"y"的值是"q"的KRPC訊息,包括兩個附加鍵"q"和"a"。"q"的值是請求的方法名,"a"的值是請求的參數。
響應
響應,即"y"的值是"r"的KRPC訊息,包括一個附加鍵"r"。"r"的值是請求的傳回值。當成功完成一個請求時,發送響應訊息。
錯誤
錯誤,即"y"的值是"e"的KRPC訊息,包括一個附加鍵"e"。"e"的值是一個列表,其中的第一個元素是一個整數,表示錯誤碼。第二個元素是一個錯誤訊息的字串。當請求無法完成時,發送錯誤。下表是可能出現的錯誤:
| 201 |
一般錯誤 |
| 202 |
伺服器錯誤 |
| 203 |
協議錯誤,比如異常訊息包,無效參數,無效令牌等 |
| 204 |
方法未知 |
錯誤包樣本
DE<generic error = {'t':0, 'y':'e', 'e':[201, "A Generic Error Ocurred"]} bencoded = d1:eli201e23:A Generic Error Ocurrede1:ti0e1:y1:eeDE<
DHT請求
所有的請求都有一個id的鍵,值是請求節點的ID。所有的響應都有一個id的鍵,值是響應節點的ID。
ping
最基本的請求是ping, "q"="ping"。 ping請求只有一個參數"id",值是寄件者的節點ID,20位元組的,網路位元組順序。相應的響應也只有一個id鍵,值是響應結點的ID 。
DE<arguments: {"id" : "<querying nodes id>"} response: {"id" : "<queried nodes id>"}DE<
樣本包
DE<ping Query = {"t":"0", "y":"q", "q":"ping", "a":{"id":"abcdefghij0123456789"}} bencoded = d1:ad2:id20:abcdefghij0123456789e1:q4:ping1:t1:01:y1:qeDE<
DE<Response = {"t":"0", "y":"r", "r": {"id":"mnopqrstuvwxyz123456"}} bencoded = d1:rd2:id20:mnopqrstuvwxyz123456e1:t1:01:y1:reDE<
find_node
find_node用來尋找一個給定ID的節點聯絡資訊,"q"=="find_node"。 find_node請求有兩個參數,"id"包含請求結點的ID;"target"包含請求節點要尋找的目標節點ID。當一個節點接收到一個 find_node請求後,它的響應應該包含一個"node"鍵,值是這個目標節點,或者它路由表中K(8)個離目標節點最近的好節點的緊密的節點資訊。
DE<arguments: {"id" : "<querying nodes id>", "target" : "<id of target node>"} response: {"id" : "<queried nodes id>", "nodes" : "<compact node info>"}DE<
樣本包
DE<find_node Query = {'t':0, 'y':'q', 'q':'find_node', 'a': {'id':'abcdefghij0123456789', 'target':'mnopqrstuvwxyz123456'}} bencoded = d1:ad2:id20:abcdefghij01234567896:target20:mnopqrstuvwxyz123456e1:q9:find_node1:ti0e1:y1:qeDE<
DE<Response = {'t':0, 'y':'r', 'r': {'id':'0123456789abcdefghij', 'nodes': 'def456...'}} bencoded = d1:rd2:id20:0123456789abcdefghij5:nodes9:def456...e1:ti0e1:y1:reDE<
get_peers
get_peers與一個torrent的infohash關聯,"q"="get_peers"。get_peers請求有兩個參數:"id"包含請求結點的ID;"info_hash"包含torrent的infohash。如果接收請求的節點知道infohash的peer,它把這些peer 的緊密的IP地址/連接埠資訊串連成一個串列表,以"value"作為鍵,回複給請求節點。如果接收請求的節點沒有infohash的 peer ,它回複路由表中離infohash最近的K個節點,以"nodes"作為鍵。任何一種情況,"token"鍵都包含在傳回值中。在將來發送 announce_peer請求的時候,token值也是必須的。
DE<arguments: {"id" : "<querying nodes id>", "info_hash" : "<20-byte infohash of target torrent>"} response: {"id" : "<queried nodes id>", "values" : ["<compact peer info string>"]} or: {"id" : "<queried nodes id>", "nodes" : "<compact node info>"}DE<
樣本包
DE<get_peers Query = {'t':0, 'y':'q', 'q':'get_peers', 'a': {'id':'abcdefghij0123456789', 'info_hash':'mnopqrstuvwxyz123456'}} bencoded = d1:ad2:id20:abcdefghij01234567899:info_hash20:mnopqrstuvwxyz123456e1:q9:get_peers1:ti0e1:y1:qeDE<
DE<Response with peers = {'t':0, 'y':'r', 'r': {'id':'abcdefghij0123456789', 'token':'aoeusnth', 'values': ['axje.uidhtnmbrl']}} bencoded = d1:rd2:id20:abcdefghij01234567895:token8:aoeusnth6:valuesl15:axje.uidhtnmbrlee1:ti0e1:y1:reDE<
DE<Response with closest nodes = {'t':0, 'y':'r', 'r': {'id':'abcdefghij0123456789', 'token':'aoeusnth', 'nodes': 'def456...'}} bencoded = d1:rd2:id20:abcdefghij01234567895:nodes9:def456...5:token8:aoeusnthe1:ti0e1:y1:reDE<
announce_peer
聲明請求節點的peer正在一個連接埠上下載一個torrent。announce_peer有四個參數:"id"是請求節點ID; "info_hash"是torrent的infohash;"port"是正在下載的連接埠號碼,整數;"token"是上次接收get_peers請求響應時獲得的。接收announce請求的節點必鬚根據IP地址檢查令牌(token),即上次它作為請求節點時發給它的令牌與現在提供的令牌相同。然後接收請求的節點應該儲存請求節點的IP地址和提供的與infohash關聯的連接埠到本地peer聯絡資訊儲存池中。
DE<arguments: {"id" : "<querying nodes id>", "info_hash" : "<20-byte infohash of target torrent>", "port" : <port number>, "token" : "<opaque token>"} response: {"id" : "<queried nodes id>"}DE<
樣本包
DE<announce_peers Query = {'t':0, 'y':'q', 'q':'announce_peers', 'a': {'id':'abcdefghij0123456789', 'info_hash':'mnopqrstuvwxyz123456', 'port' : 6881, 'token' : 'aoeusnth'}} bencoded = d1:ad2:id20:abcdefghij01234567899:info_hash20:
mnopqrstuvwxyz1234564:porti6881e5:token8:aoeusnthe1:q14:announce_peers1:ti0e1:y1:qeDE<
DE<Response = {"t":"0", "y":"r", "r": {"id":"mnopqrstuvwxyz123456"}} bencoded = d1:rd2:id20:mnopqrstuvwxyz123456e1:t1:01:y1:reDE<
腳註
原文地址:http://www.bittorrent.org/Draft_DHT_protocol.html
參考譯文:http://www.protocol.com.cn/archiver/tid-7852.html 研究DHT的群:375737269