Python从头实现以太坊(六):Routing

Python从头实现以太坊系列索引:
一、Ping
二、Pinging引导节点
三、解码引导节点的响应
四、查找邻居节点
五、类-Kademlia协议
六、Routing

前几节讲到以太坊节点发现使用的是修改过的 Kademlia 会话协议。它的传输协议使用的是 UDP,UDP 跟 TCP 相比,因为不需要顺序发送和消息确认,所以可以做到低延迟,无阻塞,适合一些广播的场景,比如 DNS 或直播等。甚至 HTTP 未来也可能基于 UDP,HTTP-over-QUIC(QUIC 就是基于 UDP) 已经被 IETF 正式命名为 HTTP/3,UDP 的优点正在被越来越多的人发掘,未来可期。前面几节我们还讲到它的四种会话消息结构,分别是 Ping,Pong,FindNeighbor 和 Neighbors,这些消息在发送之前用 RLP(递归长度前缀)编码并用 ECDSA(椭圆曲线数字签名算法)进行签名,哈希算法使用的是 keccak256(即 sha3),这些代码也都实现了,现在还差路由部分,这就是我们这节课的主题。这节讲完,我们以太坊节点发现协议就完成了。

在开始之前,你可以到 https://github.com/HuangFJ/pyeth 查阅代码,或克隆到本地:

$ git clone https://github.com/HuangFJ/pyeth
$ 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 的 Ping,Pong,FindNeighbor 和 Neighbors 四种消息结构移到了 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)

主要变化是将原来的

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值