以Kademlia为例实战DHT(二)

节点通信

重点分析:

  • krpc.go
krpc.go

在Mainline DHT中,一个节点包含一个IP和一个端口的组合,通过RPC协议进行通信。KRPC是一个简单的协议,包含节点发送信息(queries,replies and errors),其中包含用UDP Bencoded编码的字典。请求又分为四种:ping,find_node,get_peers,announce_peer。

  • ping:侦探对方是否在线。
  • find_node:查找某一个节点ID为Key的具体信息,信息包括ip,port,ID。
  • get_peers:查找某一个资源ID为Key的具体信息,信息里包含可提供下载资源的ip:port列表。
  • announce_peer:用来告别别人自己可提供某一资源的下载,让别人把这个消息保存起来。BT种子嗅探器是根据这个来得到消息的,不过得到消息后我们还需要进一步下载。

实现KPRC时,需要注意下面几点:

  • 每次收到请求或者回复你都需要根据情况更新你的Routing Table,或保存或丢掉。
  • 你需要实现transaction,transaction里边要包含你的请求信息以及被请求的ip及端口,只有这样当你收到回复消息时,你才能根据消息的transaction id做出正确的处理。
  • 一开始你是不在DHT网络中的,需要别人把你介绍进去,任何一个在DHT中的人都可以。一般我们可以向 router.bittorrent.com:6881、 dht.transmissionbt.com:6881 等发送find_node请求,然后我们的DHT就可以开始工作了。

KRPC消息是一个单一的字典,每个消息都是一个键值对,根据消息的类型,会有额外的键。每条消息都有一个表示带有表示事务ID的字符串值的键"t",这个事务ID由查询节点产生,并在响应中得到响应,因此响应可能与同一个节点的多个查询相关联。事务ID应该被编码为一个二进制数字的短字符串,通常2个octets已经足够覆盖2^16个未完成的查询。每一个KRPC消息中包含的另一个关键字是"y",它带有一个描述消息类型的字符串。“y”键的值是用于查询"q",响应"r",错误"e"。

  • Queries

Queries,或者KRPC消息字典中带有一个"q"的"y"值,会包含两个额外的键:"q"和"a"。其中Key "q"有一个字符串值,其中包含查询的方法名。键"a"有一个字典值,其中包含查询的命名参数。

  • Responses

Responses,或者KRPC消息字典中带有一个"r"的"y"值,会包含一个额外的键"r"。“r”的值是一本包含返回值的字典。响应消息是在成功完成查询之后发送的。

  • Errors

Errors,或者KRPC消息字典中带有一个"e"的"y"值,会包含一个额外的键"e"。“e”的值是一个列表,第一个元素是一个代表错误代码的整数,第二个元素是包含错误消息的字符串。当查询无法完成时,会发送错误。

// DHT 引擎所有的远程连接节点
type remoteNode struct {
    address net.UDPAddr
    addressBinaryFormat string
    id string
    lastQueryID int                         // lastQueryID需要在消费后自增。根据协议,应该是两个字母,但我使用0-255,虽然是当作字符串
    pendingQueries map[string]*queryType    // key: transaction ID
    pastQueries map[string]*queryType       // key: transaction ID
    reachable bool
    lastResponseTime time.Time
    lastSearchTime time.Time
    ActiveDownloads []string
}

type InfoHash string

type queryType struct {     //请求类型结构体
    Type string             //类型
    ih InfoHash             //发起请求的infohash
    srcNode string          //目标节点
}

type answerType struct {            //回复类型结构体
    Id string "id"                  //回复节点ID
    Target string "target"          //回复目标
    InfoHash InfoHash "info_hash"   //回复的infohash
    Port int "port"                 //端口
    Token string "token"            //token
}

type responseType struct {      //响应类型结构体
    T string "t"                //transaction的id
    Y string "y"                //y值
    Q string "q"                //q值
    R getPeersResponse "r"      //r值
    E []string "e"              //e值列表
    A answerType "a"            //answerType结构体A
}

type getPeersResponse struct {  //获取节点响应的结构体
    Values []string "values"    //响应值的列表
    Id string "id"              //事务id
    Nodes string "nodes"        //节点
    Nodes6 string "nodes6"      //节点
    Token string "token"        //token
}

type queryMessage struct {      //请求消息结构体
    T string "t"                //transaction的id
    Y string "y"                //y值
    Q string "q"                //q值
    A map[string]interface{} "a"//字典A包含查询的命名参数。
}

type replyMessage struct {      //回复消息结构体
    T string "t"                //transaction的id
    Y string "y"                //y值
    R map[string]interface{} "r"//字典R包含返回值的字典
}

type packetType struct{         //数据包类型结构体
    b []byte                    //类型
    raddr net.UDPAddr           //远程节点地址
}

下面是krpc.go脚本中包含的基本方法:

  • func parseNodesString(nodes string,proto string) (parsed map[string]string)
    • 将一个包含多个node的字符串解析成一个node的map返回。
    • 其中proto就是基本的两种,udp4和udp6。
    • 如果是udp4,那输入的nodes的长度必须是26的整数倍,如果是udp6,nodes的长度是38的整数倍。
    • 然后按照node的长度切割后遍历nodes,转成带点的IP:Port格式。
  • func (r *remoteNode) newQuery(transType string) (transId string)
    • 创建一个新的事务id并向r.pendingQueries添加一个条目。
    • 创建一个事务id,转成字符串。
    • 把这个事务id放在远程节点的pendingQueries map里,Key就是事务id,Value是传入的transType。本质是告诉远程节点,有一个待访问的请求来了。
  • func (r *remoteNode) wasContactedRecently(ih InfoHash) bool
    • 查看一个节点最近是否被联系过。
    • 如果远程节点的pendingQueries和pastQueries都为空,那肯定是没被访问过,返回false。
    • 如果远程节点的lastResponseTime为零,且其lastResponseTime大于searchRetryPeriod,也说明没被访问过,返回false。
    • 遍历pendingQueries,如果有value跟infohash相等,说明访问过,返回true。
    • 如果lastSearchTime为零,且其lastSearchTime过去时间大于searchRetryPeriod,说明没被访问过,返回false。
    • 遍历pastQueries,如果有value跟infohash相等,说明被访问过,返回true。
  • func bogusId(id string) bool
    • 判断id长度是否为20字节
  • func newRemoteNode(addr net.UDPAddr,id string) *remoteNode
    • 构建一个远程节点
    • 输入UDP地址,还有远程节点id
  • func newTransactionId() int
    • 创建一个事务id
  • func sendMsg(conn *net.UDPConn,raddr net.UDPAddr,query interface{})
    • 给远程节点发送消息
    • 参数,远程节点的连接conn,地址addr,query接口
    • 会用到bencode.Marshal方法将query转成bytes.Buffer
    • conn.WriteToUDP(b.Bytes(),&raddr)给远程节点发送消息
  • func readResponse(p packetType) (response responseType,err error)
    • 读取响应消息
    • 输入参数 消息包类型p packetType ,其包一个含字节数组和远程节点的地址
    • e2 := bencode.Unmarshal(bytes.NewBuffer(p.b),&response) 将p转成responseType
  • func listen(addr string,listenPort int,proto string) (socket *net.UDPConn,err error)
    • 监听远程节点,返回一个socket连接
  • func readFromSocket(socket *net.UDPConn,conChan chan packetType,bytesArena arena,stop chan bool)
    • 从UDP socket中读取,然后将字节切片写入packetType的chan
  • func DecodeInfoHash(x string) (b InfoHash,err error)
    • 将字符串转成InfoHash
  • func DecodePeerAddress(x string) string
    • 将字符串转成节点的地址

更多代码注解参考我的代码仓库代码:

https://github.com/jianhuaixie/dht

你可能感兴趣的:(以Kademlia为例实战DHT(二))