Python從頭實現以太坊(六):Routing
$ git clone $ cd pyeth $ git checkout partfive
原始碼檔案包含:
├── app.py ├── priv_key ├── pyeth │ ├── __init__.py │ ├── constants.py │ ├── crypto.py │ ├── discovery.py │ ├── packets.py │ └── table.py ├── requirements.txt
跟上一次的程式碼版本(使用 git checkout fartfour
檢視)比較:
├── priv_key ├── pyeth │ ├── __init__.py │ ├── crypto.py │ └── discovery.py ├── requirements.txt ├── send_ping.py
可以看到我已經將 send_ping.py
改名為 app.py
,因為它的作用不再只是收發一次訊息的程式碼而是一個完整應用程式入口了。在 pyeth/
目錄中新增了 constants.py
、packets.py
和 table.py
三個原始檔。constants.py
是我們協議使用的一些常量,另外我們將原來 discovery.py
的 ,, 和 四種訊息結構移到了 packets.py
裡,最後在 table.py
實現路由表結構。
程式碼變化還是蠻大的,而且相對於前面的幾節,比較幹,考驗大家對 Python 這門語言是否熟練以及程式設計能力,至少會涉及以下一些知識:
gevent 協程
Actor 併發模型
巢狀函式
訊息佇列
非同步/回撥
請求和響應
如果是 TCP 的話,客戶端要先 connect
到服務端,服務端要 accept
之後才建立到客戶端的連線,之後雙方透過這個連線建立會話。但是 UDP 是沒有連線狀態的,收發訊息全部透過一個 socket,而且是非同步的,為了建立會話必須確定訊息來源並將 request 和 response 訊息對應起來,這樣在傳送 request 訊息之後,方可知道對端響應的確切 response。
因此,我用了 Actor 併發模型來實現這樣邏輯。你可以把 Actor 理解為一個具體物件,這個物件跟外界是隔離的,它與外界聯絡的唯一途徑就是透過訊息,它內部有一個訊息佇列,一旦接收到外部訊號,就會併發地執行過程並儲存狀態。
在 discovery.py
有一個 Pending
類,它相當於一個 Actor:
class Pending(Greenlet): def __init__(self, node, packet_type, callback, timeout=K_REQUEST_TIMEOUT): Greenlet.__init__(self) self._node = node self._packet_type = packet_type self._callback = callback self._timeout = timeout self._box = Queue() @property def is_alive(self): return self._box is not None @property def from_id(self): return self._node.node_id @property def packet_type(self): return self._packet_type @property def ep(self): return self._node.endpoint.address.exploded, self._node.endpoint.udpPort def emit(self, packet): self._box.put(packet) def _run(self): chunks = [] while self._box is not None: try: packet = self._box.get(timeout=self._timeout) chunks.append(packet) except Empty: # timeout self._box = None return None except: # die self._box = None raise try: if self._callback(chunks): # job done self._box = None return chunks except: # die self._box = None raise
Pending
類繼承自 gevent.Greenlet
協程類,有五個欄位,_node
是響應節點,_packet_type
是響應包型別,_callback
是回撥函式,_timeout
是超時時間,_box
是訊息佇列。它透過 emit
方法獲取外部訊號,併發執行 _run
方法內的過程。
我們將在傳送 Ping
和 FindNeighbors
請求的時候用到這個類。因為傳送 Ping
和 FindNeighbors
請求後,需要在 _timeout
時間內等待對端返回 Pong
和 Neighbors
響應並執行後續過程;但是如果超過這個時間沒有回應,我們認為請求超時無效。所以在請求的地方,我們用 Pending(node, self, node, packet_type, callback).start()
非同步啟動了一個Actor,當 UDP Socket 接收到相應的訊息的之後,我們就用 pending.emit(response)
把訊息傳給它處理,以響應之前的請求。
訊息處理完之後 Actor 是結束退出還是繼續等待是由回撥函式 _callback
的返回值決定的,這個函式在請求的時候定義,如果返回 True
表示這次請求成功,Actor 可以結束退出了;如果返回 False
說明還得繼續等待響應。之所以這樣做是因為 Neighbors
訊息大小有可能超過協議規定的最大包的大小限制,而必須拆成多個訊息返回。在傳送一個 FindNeighbors
請求之後可能會有得到多個 Neighbors
訊息做為回應,我們必須在請求建立的時候對這個流程加以控制。
節點 Key 和 ID
Node
節點類在 packets.py
檔案裡面:
class Node(object): def __init__(self, endpoint, node_key): self.endpoint = endpoint self.node_key = None self.node_id = None self.added_time = Node self.set_pubkey(node_key) def set_pubkey(self, pubkey): self.node_key = pubkey self.node_id = keccak256(self.node_key)
主要變化是將原來的 node
欄位名稱改成了 node_key
表示節點公鑰,從變數名就可以認出來。新增了 node_id
表示節點 ID,它由 node_key
進行 keccak256
雜湊運算得來,它是一個 256 bits 的大整數。node_key
和 node_id
都是 raw bytes 的形式。節點 ID 作為節點的指紋,一個地方用在計算節點的相近度,另一個地方用在訊息請求與響應的來源節點的對應關係上。
伺服器
discovery.py
的伺服器類 Server
做了很大的變動:
class Server(object): def __init__(self, boot_nodes): # hold all of pending self.pending_hold = [] # last pong received time of the special node id self.last_pong_received = {} # last ping received time of the special node id self.last_ping_received = {} # routing table self.table = RoutingTable(Node(self.endpoint, pubkey_format(self.priv_key.pubkey)[1:]), self) ... def add_table(self, node): self.table.add_node(node) def add_pending(self, pending): pending.start() self.pending_hold.append(pending) return pending def run(self): gevent.spawn(self.clean_pending) gevent.spawn(self.listen) # wait forever evt = Event() evt.wait() def clean_pending(self): while True: for pending in list(self.pending_hold): if not pending.is_alive: self.pending_hold.remove(pending) time.sleep(K_REQUEST_TIMEOUT) def listen(self): LOGGER.info("{:5} listening...".format('')) while True: ready = select([self.sock], [], [], 1.0) if ready[0]: data, addr = self.sock.recvfrom(2048) # non-block data reading gevent.spawn(self.receive, data, addr) def receive(self, data, addr):...
它新增了幾個欄位,pending_hold
是用來儲存請求時建立的 Pending
物件的列表,當伺服器接收到訊息後會從這個列表裡過濾相應的 Pending
物件。last_pong_received
記錄每個對端節點最後發來的 pong 訊息的時間。last_ping_received
記錄每個對端節點最後發來的 ping 訊息的時間。table
就是路由表 RoutingTable
物件。
原來的 listen_thread
方法改成了 run
,作為伺服器的啟動入口,它建立並執行 self.clean_pending
和 self.listen
協程,然後讓主程式陷入等待。self.clean_pending
定時將已經結束或超時的 Pending
物件從 pending_hold
列表裡面清除掉。self.listen
的變動是將訊息的接收後處理用 gevent.spawn
改成了並行。此外還新增了 add_table
和 add_pending
兩個方法,前者是在接收到 Neighbors
訊息的時候將返回的節點新增到路由表;後者是將請求後建立的 Pending
物件新增到 pending_hold
列表。
伺服器的四個訊息接收處理方法已經全部實現,他們會執行一個共同的過程 handle_reply
,這個方法就是用於從 pending_hold
裡過濾查詢相應的 Pending
物件的,並把響應的訊息傳給它讓原來的請求邏輯繼續執行。這裡特別需要強調的一個地方是打 LOGGER.warning
的那個地方,它反映一個問題,從 Neighbors
訊息裡面提取的一些節點,其自帶的 key 和同一個端點(IP 地址和埠一樣)返回的訊息簽名用的真實的 key 不一樣,這點一直讓我百思不得其解。
def handle_reply(self, addr, pubkey, packet_type, packet, match_callback=None): remote_id = keccak256(pubkey) is_match = False for pending in self.pending_hold: if pending.is_alive and packet_type == pending.packet_type: if remote_id == pending.from_id: is_match = True pending.emit(packet) match_callback and match_callback() elif pending.ep is not None and pending.ep == addr: LOGGER.warning('{:5} {}@{}:{} mismatch request {}'.format( '', binascii.hexlify(remote_id)[:8], addr[0], addr[1], binascii.hexlify(pending.from_id)[:8] ))
接收 Pong 響應
def receive_pong(self, addr, pubkey, pong): remote_id = keccak256(pubkey) # response to ping last_pong_received = self.last_pong_received def match_callback(): # solicited reply last_pong_received[remote_id] = time.time() self.handle_reply(addr, pubkey, Pong.packet_type, pong, match_callback)
接收到 Pong
訊息的時候,如果這個 Pong
有效(確實是我方節點請求的,且沒有超時),我們會更新對端節點最後 Pong
響應時間。因為 Python 只有 lambda 表示式,沒有匿名函式的概念,我們只能在這個函式里面定義一個巢狀函式 match_callback
當成回撥物件傳遞。
接收 Ping 請求
def receive_ping(self, addr, pubkey, ping, msg_hash): remote_id = keccak256(pubkey) endpoint_to = EndPoint(addr[0], ping.endpoint_from.udpPort, ping.endpoint_from.tcpPort) pong = Pong(endpoint_to, msg_hash, time.time() + K_EXPIRATION) node_to = Node(pong.to, pubkey) # sending Pong response self.send_sock(pong, node_to) self.handle_reply(addr, pubkey, PingNode.packet_type, ping) node = Node(endpoint_to, pubkey) if time.time() - self.last_pong_received.get(remote_id, 0) > K_BOND_EXPIRATION: self.ping(node, lambda: self.add_table(node)) else: self.add_table(node) self.last_ping_received[remote_id] = time.time()
接收到 Ping
訊息的時候,除了回應 Pong
訊息之外,還會判斷對端節點最後一次響應 Pong
回來的時間是否在 K_BOND_EXPIRATION
時間之前,是的話說明它要麼不在我方節點的路由表裡面要麼它無法連線了,此時我們需要重新傳送 Ping
跟它握手,如果 Ping
通的話,將它更新到我方節點的路由表;否則直接將它更新到我方節點的路由表。最後更新對端節點最後的響應 Ping
的時間。
接收 FindNeighbors 請求
def receive_find_neighbors(self, addr, pubkey, fn): remote_id = keccak256(pubkey) if time.time() - self.last_pong_received.get(remote_id, 0) > K_BOND_EXPIRATION: # lost origin or origin is off return target_id = keccak256(fn.target) closest = self.table.closest(target_id, BUCKET_SIZE) # sent neighbours in chunks ns = Neighbors([], time.time() + K_EXPIRATION) sent = False node_to = Node(EndPoint(addr[0], addr[1], addr[1]), pubkey) for c in closest: ns.nodes.append(c) if len(ns.nodes) == K_MAX_NEIGHBORS: self.send_sock(ns, node_to) ns.nodes = [] sent = True if len(ns.nodes) > 0 or not sent: self.send_sock(ns, node_to)
接收 FindNeighbors
訊息的時候,先判斷對端節點最後 Pong
響應的時間是否在 K_BOND_EXPIRATION
之前,是的話直接丟棄不理,因為避免攻擊,我方不能接受不在我方的節點路由表內的節點請求。否則呼叫 self.table.closest(target_id, BUCKET_SIZE)
方法獲取和 target_id
相近的 BUCKET_SIZE
個節點,分批返回給請求節點。
接收 Neighbors 響應
def receive_neighbors(self, addr, pubkey, neighbours): # response to find neighbours self.handle_reply(addr, pubkey, Neighbors.packet_type, neighbours)
接收 Neighbors
訊息的方法比較簡單,因為主要的邏輯在請求的方法 find_neighbors
裡面。
Ping 請求
def ping(self, node, callback=None): ping = PingNode(self.endpoint, node.endpoint, time.time() + K_EXPIRATION) message = self.wrap_packet(ping) msg_hash = message[:32] def reply_call(chunks): if chunks.pop().echo == msg_hash: if callback is not None: callback() return True ep = (node.endpoint.address.exploded, node.endpoint.udpPort) self.sock.sendto(message, ep) return self.add_pending(Pending(node, Pong.packet_type, reply_call))
ping
方法是非同步的。它建立了 PingNode
的訊息包併傳送出去之後,建立了一個 Pending
,把回撥函式 reply_call
傳給它,非同步等待響應。
FindNeighbors 請求
def find_neighbors(self, node, target_key): node_id = node.node_id if time.time() - self.last_ping_received.get(node_id, 0) > K_BOND_EXPIRATION: # send a ping and wait for a pong self.ping(node).join() # wait for a ping self.add_pending(Pending(node, PingNode.packet_type, lambda _: True)).join() fn = FindNeighbors(target_key, time.time() + K_EXPIRATION) def reply_call(chunks): num_received = 0 for neighbors in chunks: num_received += len(neighbors.nodes) if num_received >= BUCKET_SIZE: return True self.send_sock(fn, node) ep = (node.endpoint.address.exploded, node.endpoint.udpPort) # block to wait for neighbours ret = self.add_pending(Pending(node, Neighbors.packet_type, reply_call, timeout=3)).get() if ret: neighbor_nodes = [] for chunk in ret: for n in chunk.nodes: neighbor_nodes.append(n) return neighbor_nodes
而 find_neighbors
方法是同步的。它首先要做避免流量放大攻擊,判斷對端節點最後一次 Ping
請求的時間是否在 K_BOND_EXPIRATION
之前,是的話說明雙方節點可能已經互相不在對方的節點路由表裡面了,必須重新建立 ping-pong-ping
握手,避免訊息被雙方互相丟棄。這裡 self.ping(node).join()
可以看到,傳送 Ping
請求之後用 join()
阻塞等待這個 Pending
Greenlet 協程的結束。等待對端節點發來 Ping
請求的過程也是一樣。傳送 FindNeighbors
訊息之後等待 Neighbors
響應的過程也是同步,這裡用了 Greenlet 協程的 get()
阻塞等待協程結束並返回結果——和 target_key
相鄰的節點。回撥函式 reply_call
控制此次的 FindNeighbors
請求何時可以結束,這裡是收集到 BUCKET_SIZE
個節點後結束。
路由表
在 table.py
檔案裡面定義了兩個類——RoutingTable
和 Bucket
,RoutingTable
是路由表,Bucket
是儲存節點的 k-桶:
class Bucket(object): def __init__(self): self.nodes = [] self.replace_cache = []class RoutingTable(object): def __init__(self, self_node, server): self.buckets = [Bucket() for _ in range(BUCKET_NUMBER)] self.self_node = self_node self.server = server # add seed nodes for bn in self.server.boot_nodes: self.add_node(bn) gevent.spawn(self.re_validate) gevent.spawn(self.refresh) def lookup(self, target_key): target_id = keccak256(target_key) closest = [] while not closest: closest = self.closest(target_id, BUCKET_SIZE) if not closest: # add seed nodes for bn in self.server.boot_nodes: self.add_node(bn) asked = [self.self_node.node_id] pending_queries = 0 reply_queue = Queue() while True: for n in closest: if pending_queries >= KAD_ALPHA: break if n.node_id not in asked: asked.append(n.node_id) pending_queries += 1 gevent.spawn(self.find_neighbours, n, target_key, reply_queue) if pending_queries == 0: break ns = reply_queue.get() pending_queries -= 1 if ns: for node in ns: farther = find_farther_to_target_than(closest, target_id, node) if farther: closest.remove(farther) if len(closest) < BUCKET_SIZE: closest.append(node) def refresh(self): assert self.server.boot_nodes, "no boot nodes" while True: # self lookup to discover neighbours self.lookup(self.self_node.node_key) for i in range(3): random_int = random.randint(0, K_MAX_KEY_VALUE) node_key = int_to_big_endian(random_int).rjust(K_PUBKEY_SIZE / 8, b'x00') self.lookup(node_key) time.sleep(REFRESH_INTERVAL) def re_validate(self): while True: time.sleep(RE_VALIDATE_INTERVAL) # the last node in a random, non-empty bucket bi = 0 last = None idx_arr = [i for i in range(len(self.buckets))] random.shuffle(idx_arr) for bi in idx_arr: bucket = self.buckets[bi] if len(bucket.nodes) > 0: last = bucket.nodes.pop() break if last is not None: LOGGER.debug('{:5} revalidate {}'.format('', last)) # wait for a pong ret = self.server.ping(last).get() bucket = self.buckets[bi] if ret: # bump node bucket.nodes.insert(0, last) else: # pick a replacement if len(bucket.replace_cache) > 0: r = bucket.replace_cache.pop(random.randint(0, len(bucket.replace_cache) - 1)) if r: bucket.nodes.append(r) def add_node(self, node):... def get_bucket(self, node):... def closest(self, target_id, num):... def find_neighbours(self, node, target_key, reply_queue):...
從路由表的建構函式可以看出,它一開始就建立了 BUCKET_NUMBER
個的 k-桶,接著把啟動節點加進去,然後啟動 re_validate
和 refresh
協程。re_validate
做的是持續隨機挑選 k-桶,從 k-桶裡挑出最少互動的節點重新 Ping
,檢視節點是否線上。線上則將它重新更新到路由表,否則從 k-桶的 replace_cache
裡面挑出一個節點替換它,如果有的話。refresh
做的是不斷發現節點並填充路由表,它首先查詢跟自己相近的節點,然後查詢三個隨機節點的相鄰節點。
查詢跟某個 target_key
相近的節點用 lookup
方法,這個方法叫做遞迴查詢(Recursive Lookup)。它首先從路由表裡面獲取和 target_key
相鄰最近的 BUCKET_SIZE
個節點放在 closest
列表裡面,如果路由裡面一個節點都沒有,重新把啟動節點加進去,最後 closest
總會有一些節點。接著遍歷 closest
裡面的節點,並向每個節點索取其和 target_key
更相近的節點;將返回結果的節點新增到路由表,然後遍歷返回結果的節點,將它和 closest
裡面的節點做對比,看誰離 target_key
更近,更近的留在 closest
裡面。這個過程迴圈進行直到 closest
列表裡面的所有節點都被問過了。
路由表是不直接儲存節點的,它必須存在路由表相應的 k-桶裡面。如何選擇 k-桶?我們將某個節點的 ID 和當前伺服器的節點 ID 做一下異或距離運算,然後看這個距離有多少個“前導零”。最後將 k-桶個數減去“前導零”的個數的結果做為這個節點所在 k-桶的索引編號,如果結果小於 0
,取 0
。這個邏輯在 get_bucket
方法裡。
找到 k-桶後,如何將節點新增進去就按照 Kademlia 協議的規則:k-桶滿了,把它加到 k-桶的 replace_cache
;k-桶沒滿,但是k-桶已經包含此節點,把它調到最前面,否則把它直接插到最前面。
啟動伺服器
如果你執行 python app.py
可以看到的輸出:
2018-12-14 20:37:39.778 push (N 930cf49c) to bucket #142018-12-14 20:37:39.778 push (N 674085f6) to bucket #162018-12-14 20:37:39.778 push (N 009be51d) to bucket #162018-12-14 20:37:39.778 push (N 816ee7e3) to bucket #142018-12-14 20:37:39.778 push (N 3d1edcb0) to bucket #162018-12-14 20:37:39.778 push (N 29cca67d) to bucket #162018-12-14 20:37:39.786 listening...2018-12-14 20:37:39.795 ----> 816ee7e3@13.75.154.138:30303 (Ping)2018-12-14 20:37:39.796 ----> 930cf49c@52.16.188.185:30303 (Ping)2018-12-14 20:37:39.796 ----> 29cca67d@5.1.83.226:30303 (Ping)2018-12-14 20:37:40.142 <---- 816ee7e3@13.75.154.138:30303 (Pong)2018-12-14 20:37:40.798 <-//- 930cf49c@52.16.188.185:30303 (Pong) timeout2018-12-14 20:37:40.799 <-//- 29cca67d@5.1.83.226:30303 (Pong) timeout2018-12-14 20:37:41.143 <-//- 816ee7e3@13.75.154.138:30303 (Ping) timeout2018-12-14 20:37:41.145 ----> 816ee7e3@13.75.154.138:30303 (FN a3d334fa)2018-12-14 20:37:41.507 <---- 816ee7e3@13.75.154.138:30303 (Ns [(N a3ba0512), (N a35e82d2), (N a1ae51f6), (N a153f900), (N a14a9593), (N a7b31894), (N a5d7d971), (N a5268243), (N af477601), (N adda7a78), (N b6ed28e7), (N b62a7422)] 1544791081)2018-12-14 20:37:41.515 <---- 816ee7e3@13.75.154.138:30303 (Ns [(N b4114787), (N b8fb3e88), (N b83a8eb9), (N bf834266)] 1544791081)2018-12-14 20:37:41.515 push (N a3ba0512) to bucket #72018-12-14 20:37:41.515 push (N a35e82d2) to bucket #82018-12-14 20:37:41.515 push (N a1ae51f6) to bucket #102018-12-14 20:37:41.516 push (N a5268243) to bucket #112018-12-14 20:37:41.516 push (N af477601) to bucket #122018-12-14 20:37:41.516 push (N adda7a78) to bucket #122018-12-14 20:37:41.517 push (N bf834266) to bucket #132018-12-14 20:37:41.518 ----> a3ba0512@35.236.159.118:30303 (Ping)2018-12-14 20:37:41.607 <---- a3ba0512@35.236.159.118:30303 (Pong)2018-12-14 20:37:41.615 <---- a3ba0512@35.236.159.118:30303 (Ping)2018-12-14 20:37:41.615 ----> a3ba0512@35.236.159.118:30303 (Pong)2018-12-14 20:37:41.615 a3ba0512@35.236.159.118:30303 unsolicited response Ping2018-12-14 20:37:41.616 bump (N a3ba0512) in bucket #72018-12-14 20:37:41.800 <-//- 930cf49c@52.16.188.185:30303 (Ping) timeout2018-12-14 20:37:41.801 <-//- 29cca67d@5.1.83.226:30303 (Ping) timeout2018-12-14 20:37:41.801 ----> 930cf49c@52.16.188.185:30303 (FN a3d334fa)2018-12-14 20:37:41.802 ----> 29cca67d@5.1.83.226:30303 (FN a3d334fa)2018-12-14 20:37:42.617 <-//- a3ba0512@35.236.159.118:30303 (Ping) timeout2018-12-14 20:37:42.618 ----> a3ba0512@35.236.159.118:30303 (FN a3d334fa)2018-12-14 20:37:42.695 <---- a3ba0512@35.236.159.118:30303 (Ns [(N a3d1c129), (N a3d6aca8), (N a3c5cbb3), (N a3c42eff), (N a3cb0acd), (N a3ca35e8), (N a3c8dd80), (N a3cf1b5b), (N a3ceb7fb), (N a3f0ba70), (N a3f5b977), (N a3fbe63b)] 1544791082)2018-12-14 20:37:42.703 <---- a3ba0512@35.236.159.118:30303 (Ns [(N a3e3f5f9), (N a3e82fb3), (N a3e864b4), (N a3ed88d1)] 1544791082)2018-12-14 20:37:42.703 push (N a3d1c129) to bucket #22018-12-14 20:37:42.703 push (N a3d6aca8) to bucket #32018-12-14 20:37:42.704 push (N a3e864b4) to bucket #62018-12-14 20:37:42.705 push (N a3ed88d1) to bucket #62018-12-14 20:37:42.705 ----> a3d1c129@79.142.21.222:30303 (Ping)2018-12-14 20:37:43.144 <---- a3d1c129@79.142.21.222:30303 (Pong)2018-12-14 20:37:43.152 <---- a3d1c129@79.142.21.222:30303 (Ping)2018-12-14 20:37:43.153 ----> a3d1c129@79.142.21.222:30303 (Pong)2018-12-14 20:37:43.153 a3d1c129@79.142.21.222:30303 unsolicited response Ping2018-12-14 20:37:43.153 bump (N a3d1c129) in bucket #22018-12-14 20:37:44.155 <-//- a3d1c129@79.142.21.222:30303 (Ping) timeout2018-12-14 20:37:44.156 ----> a3d1c129@79.142.21.222:30303 (FN a3d334fa)2018-12-14 20:37:44.577 <---- a3d1c129@79.142.21.222:30303 (Ns [(N a3d334fa), (N a3d3d431), (N a3d6aca8), (N a3d681d2), (N a3d571d4), (N a3d91e85), (N a3c1bd3f), (N a3c1ad80), (N a3c5cbb3), (N a3c42eff), (N a3cb0acd), (N a3ca35e8)] 1544791084)2018-12-14 20:37:44.585 <---- a3d1c129@79.142.21.222:30303 (Ns [(N a3cae3f7), (N a3c80d3f), (N a3c8dd80), (N a3cf1b5b)] 1544791084)2018-12-14 20:37:44.585 push (N a3d3d431) to bucket #02018-12-14 20:37:44.585 bump (N a3d6aca8) in bucket #32018-12-14 20:37:44.586 push (N a3d681d2) to bucket #32018-12-14 20:37:44.586 push (N a3d571d4) to bucket #32018-12-14 20:37:44.586 push (N a3d91e85) to bucket #42018-12-14 20:37:44.587 bump (N a3cf1b5b) in bucket #52018-12-14 20:37:44.588 ----> a3d3d431@27.10.110.49:30303 (Ping)2018-12-14 20:37:44.803 <-//- 930cf49c@52.16.188.185:30303 (Ns) timeout2018-12-14 20:37:44.803 <-//- 29cca67d@5.1.83.226:30303 (Ns) timeout2018-12-14 20:37:44.805 ----> a3d6aca8@172.104.229.232:30303 (Ping)2018-12-14 20:37:44.805 ----> a3d681d2@172.104.180.194:30303 (Ping)2018-12-14 20:37:45.107 <---- a3d6aca8@172.104.229.232:30303 (Pong)2018-12-14 20:37:45.115 <---- a3d6aca8@172.104.229.232:30303 (Ping)2018-12-14 20:37:45.115 ----> a3d6aca8@172.104.229.232:30303 (Pong)2018-12-14 20:37:45.115 a3d6aca8@172.104.229.232:30303 unsolicited response Ping2018-12-14 20:37:45.115 bump (N a3d6aca8) in bucket #32018-12-14 20:37:45.590 <-//- a3d3d431@27.10.110.49:30303 (Pong) timeout2018-12-14 20:37:45.806 <-//- a3d681d2@172.104.180.194:30303 (Pong) timeout2018-12-14 20:37:46.117 <-//- a3d6aca8@172.104.229.232:30303 (Ping) timeout2018-12-14 20:37:46.118 ----> a3d6aca8@172.104.229.232:30303 (FN a3d334fa)2018-12-14 20:37:46.399 <---- a3d6aca8@172.104.229.232:30303 (Ns [(N a3d01efc), (N a3d05035), (N a3d08c94), (N a3d08bcf)] 1544791086)2018-12-14 20:37:46.407 <---- a3d6aca8@172.104.229.232:30303 (Ns [(N a3d33fe5), (N a3d3bdc2), (N a3d3bea3), (N a3d3a940), (N a3d39ef5), (N a3d3d431), (N a3d264f5), (N a3d2e0fd), (N a3d2e293), (N a3d2cf5f), (N a3d1b2a3), (N a3d1a970)] 1544791086)2018-12-14 20:37:46.407 push (N a3d01efc) to bucket #22018-12-14 20:37:46.408 push (N a3d05035) to bucket #2...
作者:JonHuang
連結:
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/132/viewspace-2818810/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- Python從頭實現以太坊(二):Pinging引導節點Python
- Python 如何實現以太坊虛擬機器Python虛擬機
- 從零實現Vue的元件庫(六)- Hover-Tip 實現Vue元件
- 分享Python以太坊虛擬機器實現Py-EVMPython虛擬機
- Python 轉義html中以"&#"開頭的字元PythonHTML字元
- 從頭開始實現神經網路:入門神經網路
- 從零開始實現放置遊戲(六):Excel批量匯入遊戲Excel
- python實現開啟筆記本攝像頭Python筆記
- 多頭注意力機制的python實現Python
- 從頭開始瞭解PyTorch的簡單實現PyTorch
- 以太坊代幣空投合約實現
- 以太坊連載(六):以太坊客戶端的選擇與安裝客戶端
- 如何實現一個簡單的以太坊?
- 用Golang實現以太坊代幣轉賬Golang
- Mandelbrot set 以parallel_for_實現Parallel
- WPF多表頭表格實現
- 從零開始實現放置遊戲(六)——實現掛機戰鬥(4)匯入Excel數值配置遊戲Excel
- 頭歌資料庫實驗六:儲存過程資料庫儲存過程
- Android 從零開始實現RecyclerView分組及粘性頭部效果AndroidView
- CART演算法解密:從原理到Python實現演算法解密Python
- 《Python從入門到實踐》第六章動手試一試Python
- Websocketd:以Unix方式實現的WebsocketWeb
- 從一個ConnectionPool的實現看design pattern的運用 (六) (轉)
- Python 爬蟲從入門到進階之路(六)Python爬蟲
- Docker最全教程——從理論到實戰(六)Docker
- 還在人工煉丹?自動提示工程指南來了,還帶從頭實現
- 【從頭到腳】前端實現多人視訊聊天— WebRTC 實戰(多人篇)| 掘金技術徵文前端Web
- 支援向量機(SVM)從原理到python程式碼實現Python
- ModbusTCP從站(服務端)掃描工具 python實現TCP服務端Python
- 使用Python從頭開始構建比特幣Python比特幣
- 第五課 以太坊開發框架Truffle從入門到實戰框架
- 六邊形架構 Java 實現架構Java
- 從一個ConnectionPool的實現看design pattern的運用 (續六) (轉)
- 從零開始寫 Docker(六)---實現 mydocker run -v 支援資料卷掛載Docker
- CRM實現以客戶為中心 (轉)
- 教你6步從頭寫機器學習演算法——以感知機演算法為例機器學習演算法
- synchronized的實現原理——物件頭解密synchronized物件解密
- Maui Blazor 使用攝像頭實現UIBlazor