从零开始编写一个BitTorrent下载器 (7)

info_hash:用以标识需要下载的文件,其值就是之前由info对应值计算出的infohash。trackers服务器基于这个值返回能够为下载提供资源的peers主机。

peer_id:20字节长的数据,用于向peers主机和trackers服务器标识自己的身份。具体实现仅仅是产生随机的20个字节。真实的BitTorrent客户端ID形如-TR2940-k8hj0wgej6ch,标出了客户端软件及其版本(TR2940表示Transmission Client 2.94)。

处理trackers服务器的响应(~/peers/peers.go)

服务器响应也是采用Bencode编码的:

d 8:interval i900e 5:peers 252:[很长的二进制块] e

interval表示本地应当在多长的时间间隔后再次向tracker服务器请求以刷新peers主机列表,900的单位是秒。peers包含了每个peer主机的IP地址,以二进制表示,由若干个6字节元组成,前4个字节表示主机IP,后2个字节表示端口号(大端存储的16位无符号整型,uint16)。大端存储,即big-endian,是网络中所采用的存储方式(相对于小端存储),故被称为network order。运算时可以直接将一组字节从左至右拼接以形成所要表达的整数,如0x1A和0xE1能拼接成0x1AE1,即十进制的6881。

type Peer struct { IP net.IP Port uint16 } // Unmarshal函数从缓冲区解析IP及其端口 func Unmarshal(peerBin []byte)([]Peer, error) { const peerSize = 6 numPeers := len(peerBin) / peerSize if len(peerBin) % peerSize != 0 { err := fmt.Errorf("received malformed peers") return nil, err } peers := make([]Peer, numPeers) for i := 0; i < numPeers ; i++ { offset := i * peerSize peers[i].IP = net.IP(peerBin[offset : offset+4]) peers[i].Port = binary.BigEndian.Uint16(peersBin[offset+4 : offset+6]) } return peers, nil } 下载

在取得peers主机的地址后,就可以进行下载了。对每台peer主机的连接,有如下的几个步骤:

与目标peer建立TCP连接;

完成BitTorrent握手;

交换信息(告知对方本地需要的资源)。

TCP连接(~/client/client.go)

设定一个超时检测机制,防止消耗过多网络资源。

conn, err := net.DialTimeout("tcp", peer.String(), 3*time.Second) if err != nil { return nil, err } 握手(~/handshake/handshake.go)

通过达成握手,以确定某peer主机具有期望的功能:

能够使用BT协议通信;

能够理解本机发出的信息,并作出响应;

持有本机需要的文件资源,或者持有文件资源在网络中位置的索引。

BitTorrent握手行为需要传输的信息由5个部分构成:

协议标识(表明这是BitTorrent协议)的长度,即19,十六进制表示为0x13;

协议标识,被称为pstr,即BitTorrent protocol;

8个保留字节,默认全为0,如果客户端支持BT协议的某些扩展,则需要将其中一些设置为1;

infohash,基于种子中info对应的全部信息计算得出的哈希值,用于标明本机需要的文件;

PEER ID,用于标明本机身份。

这些信息组合起来,就是达成握手需要的序列:

\x13BitTorrent protocol\x00\x00\x00\x00\x00\x00\x00\x00\x86\xd4\xc8\x00\x24\xa4\x69\xbe\x4c\x50\xbc\x5a\x10\x2c\xf7\x17\x80\x31\x00\x74-TR2940-k8hj0wgej6ch

本机发出这些信息后,peers主机应当以相同形式响应,且返回的infohash应当与本机持有的一致。

使用一个结构体表示握手包,并添加一些序列化、读取函数。

// 握手包结构体 type Handshake struct { Pstr string InfoHash [20]byte PeerID [20]byte } //Serialize函数用于序列化握手信息 func (h *Handshake) Serialize() []byte { buf := make([]byte, len(h.Pstr)+49) buf[0] = byte(len(h.Pstr)) curr := 1 curr += copy(buf[curr:], h.Pstr) curr += copy(buf[curr:], make([]byte, 8)) //即8个保留字节 curr += copy(buf[curr:], h.InfoHash[:]) curr += copy(buf[curr:], h.PeerID[:]) return buf } func Read(r io.Reader) (* Handshake, error) { // ... } 信息

完成握手后就将开始正式的收发信息。如果远端的peers主机未能做好收发的准备,本机仍旧无法发送信息,此时本机会被远端认定为阻塞的(choked)。在peers主机完成准备后,会向本机发送解除阻塞(unchoke)信息。代码设计中,默认需要杰出阻塞才能进行下载。

解析(~/message/message.go)

信息包含三个部分:长度、ID、payload。

长度为32位整型,是大端存储形式的4个字节。ID用以表示信息类型,这在代码中进行了详细定义。

type messageID uint8 const ( // MsgChoke表示阻塞 MsgChoke messageID = 0 // MsgUnchoke表示解除阻塞 MsgUnchoke messageID = 1 // MsgInterested表示信息相关 MsgInterested messageID = 2 // MsgNotInterested表示信息不相关 MsgNotInterested messageID = 3 // MsgHave表示提醒接收者,发送者拥有资源 MsgHave messageID = 4 // MsgBitfield表示发送者拥有资源的哪些部分 MsgBitfield messageID = 5 // MsgRequest表示向接收方请求数据 MsgRequest messageID = 6 // MsgPiece表示发送数据以完成请求 MsgPiece messageID = 7 // MsgCancel表示取消一个请求 MsgCancel messageID = 8 ) //Message结构体储存ID和包含信息的payload type Message struct { ID messageID Payload []byte } // Serialize函数用于执行序列化 // 信息依次为前缀、信息的ID、payload // 需要将`nil`解释为`keep-alive` func (m *Message) Serialize() []byte { if m == nil { return make([]byte, 4) } length := uint32(len(m.Payload) + 1) buf := make([]byte, 4+length) binary.BigEndian.PutUint32(buf[0:4], length) buf[4] = byte(m.ID) copy(buf[5:], m.Payload) return buf }

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zgfffd.html