BitTorrent go语言实现

BitTorrent 是一种用于在 Internet 上下载和分发文件的协议。与传统的客户端/服务器关系相比,在传统的客户端/服务器关系中,下载器连接到中央服务器(例如:在Netflix上观看电影,或加载您现在正在阅读的网页),BitTorrent网络中的参与者(称为对等)相互下载文件片段 - 这就是使其成为点对点协议的原因。我们将研究这是如何工作的,并构建我们自己的客户端,该客户端可以找到对等节点并在它们之间交换数据。

以下是该项目的原github地址:

GitHub - veggiedefender/torrent-client: Tiny BitTorrent client written in Go

client.go

实现BitTorrent客户端的片段,旨在与Torrent网络中的对等方(peers)建立TCP连接并进行交互。它包含几个主要的部分:定义客户端结构体、完成握手、接收比特场(Bitfield)、以及与对等方进行消息的发送和接收。

客户端结构体(Client

type Client struct {
	Conn     net.Conn
	Choked   bool
	Bitfield bitfield.Bitfield
	peer     peers.Peer
	infoHash [20]byte
	peerID   [20]byte
}
  • Conn: 与对等方建立的TCP连接。
  • Choked: 表示客户端当前是否被对等方“阻塞”(即暂时不发送数据)。
  • Bitfield: 一个比特场,表示客户端拥有哪些文件分片。
  • peer: 对等方的信息。
  • infoHash: Torrent的信息哈希,用于识别Torrent资源。
  • peerID: 客户端的标识符。

完成握手(completeHandshake

func completeHandshake(conn net.Conn, infohash, peerID [20]byte) (*handshake.Handshake, error) {...}

完成与对等方的握手过程。首先发送一个握手请求,然后等待并验证对等方的握手响应。这个过程中检查响应中的infoHash是否与期望值匹配,以验证连接的对等方是否拥有客户端想要的文件。

接收比特场(recvBitfield

func recvBitfield(conn net.Conn) (bitfield.Bitfield, error) {...}

接收来自对等方的比特场消息,该消息在握手完成后发送,描述了对等方拥有哪些文件分片。

新建客户端连接(New

func New(peer peers.Peer, peerID, infoHash [20]byte) (*Client, error) {...}

这个函数尝试与一个指定的对等方建立连接,并完成握手和比特场的接收。如果一切顺利,它将返回一个初始化好的Client实例。

读取消息(Read

func (c *Client) Read() (*message.Message, error) {...}

从连接中读取并返回一个消息。这可以是任何种类的消息,例如请求分片、发送分片、心跳保持等。

发送消息

以下是一系列发送特定消息的方法,它们包括向对等方发送“感兴趣”(SendInterested)、“不感兴趣”(SendNotInterested)、“解除阻塞”(SendUnchoke)、以及“拥有”(SendHave)等消息。这些方法通过TCP连接向对等方发送相应的消息:

  • SendInterested: 告诉对等方客户端对某些分片感兴趣。
  • SendNotInterested: 告诉对等方客户端对当前的分片不感兴趣。
  • SendUnchoke: 告诉对等方客户端允许对方下载分片。
  • SendHave: 告诉对等方客户端获取了某个分片。

这些交互遵循BitTorrent协议的规则,以协调文件的分片交换,从而实现点对点文件共享。

完整代码实现:

package client

import (
	"bytes"
	"fmt"
	"net"
	"time"

	"github.com/veggiedefender/torrent-client/bitfield"
	"github.com/veggiedefender/torrent-client/peers"

	"github.com/veggiedefender/torrent-client/message"

	"github.com/veggiedefender/torrent-client/handshake"
)

// A Client is a TCP connection with a peer
type Client struct {
	Conn     net.Conn
	Choked   bool
	Bitfield bitfield.Bitfield
	peer     peers.Peer
	infoHash [20]byte
	peerID   [20]byte
}

func completeHandshake(conn net.Conn, infohash, peerID [20]byte) (*handshake.Handshake, error) {
	conn.SetDeadline(time.Now().Add(3 * time.Second))
	defer conn.SetDeadline(time.Time{}) // Disable the deadline

	req := handshake.New(infohash, peerID)
	_, err := conn.Write(req.Serialize())
	if err != nil {
		return nil, err
	}

	res, err := handshake.Read(conn)
	if err != nil {
		return nil, err
	}
	if !bytes.Equal(res.InfoHash[:], infohash[:]) {
		return nil, fmt.Errorf("Expected infohash %x but got %x", res.InfoHash, infohash)
	}
	return res, nil
}

func recvBitfield(conn net.Conn) (bitfield.Bitfield, error) {
	conn.SetDeadline(time.Now().Add(5 * time.Second))
	defer conn.SetDeadline(time.Time{}) // Disable the deadline

	msg, err := message.Read(conn)
	if err != nil {
		return nil, err
	}
	if msg == nil {
		err := fmt.Errorf("Expected bitfield but got %s", msg)
		return nil, err
	}
	if msg.ID != message.MsgBitfield {
		err := fmt.Errorf("Expected bitfield but got ID %d", msg.ID)
		return nil, err
	}

	return msg.Payload, nil
}

// New connects with a peer, completes a handshake, and receives a handshake
// returns an err if any of those fail.
func New(peer peers.Peer, peerID, infoHash [20]byte) (*Client, error) {
	conn, err := net.DialTimeout("tcp", peer.String(), 3*time.Second)
	if err != nil {
		return nil, err
	}

	_, err = completeHandshake(conn, infoHash, peerID)
	if err != nil {
		conn.Close()
		return nil, err
	}

	bf, err := recvBitfield(conn)
	if err != nil {
		conn.Close()
		return nil, err
	}

	return &Client{
		Conn:     conn,
		Choked:   true,
		Bitfield: bf,
		peer:     peer,
		infoHash: infoHash,
		peerID:   peerID,
	}, nil
}

// Read reads and consumes a message from the connection
func (c *Client) Read() (*message.Message, error) {
	msg, err := message.Read(c.Conn)
	return msg, err
}

// SendRequest sends a Request message to the peer
func (c *Client) SendRequest(index, begin, length int) error {
	req := message.FormatRequest(index, begin, length)
	_, err := c.Conn.Write(req.Serialize())
	return err
}

// SendInterested sends an Interested message to the peer
func (c *Client) SendInterested() error {
	msg := message.Message{ID: message.MsgInterested}
	_, err := c.Conn.Write(msg.Serialize())
	return err
}

// SendNotInterested sends a NotInterested message to the peer
func (c *Client) SendNotInterested() error {
	msg := message.Message{ID: message.MsgNotInterested}
	_, err := c.Conn.Write(msg.Serialize())
	return err
}

// SendUnchoke sends an Unchoke message to the peer
func (c *Client) SendUnchoke() error {
	msg := message.Message{ID: message.MsgUnchoke}
	_, err := c.Conn.Write(msg.Serialize())
	return err
}

// SendHave sends a Have message to the peer
func (c *Client) SendHave(index int) error {
	msg := message.FormatHave(index)
	_, err := c.Conn.Write(msg.Serialize())
	return err
}

handshake.go

这个Go包名为handshake,用于实现BitTorrent协议中的握手过程。握手是P2P网络中节点之间进行相互识别的特殊消息。该包提供了创建、序列化以及从流中解析握手消息的功能。下面详细解释这个包的组成部分:

结构体 Handshake

  • Pstr: 代表协议标识符字符串,对于BitTorrent协议,这个字符串一般是"BitTorrent protocol"。
  • InfoHash: 一个20字节的数组,包含了torrent文件的信息散列。这用于识别正在下载或分享的特定文件。
  • PeerID: 一个20字节的数组,包含了节点(peer)的唯一标识。这用于标识每个参与网络的节点。

函数 New

  • 接受两个参数infoHashpeerID,它们都是20字节的数组。
  • 返回一个指向Handshake结构体的指针,该结构体已经使用提供的信息散列和节点ID以及标准的Pstr初始化。

方法 Serialize

  • 属于Handshake类型,没有参数。
  • Handshake实例序列化成一个字节切片。序列化的格式包括:Pstr的长度,Pstr字符串本身,8字节的保留空间(初始化为0),InfoHash,最后是PeerID
  • 返回序列化后的字节切片。

函数 Read

  • 接受一个实现了io.Reader接口的参数r,用于从中读取数据。
  • 首先读取1个字节,表示Pstr的长度。如果这个长度为0,则返回错误,因为长度不能为0。
  • 根据Pstr长度读取接下来的数据,包括Pstr本身,8字节的保留空间,InfoHashPeerID
  • 将读取的数据解析成Handshake结构体并返回。

这个包的主要作用是在BitTorrent网络中实现节点之间初步的通信协议。通过Handshake消息,每个节点可以验证其他节点是否参与同一个文件的分发,同时也能互相标识自身,为后续的更复杂交互打下基础。

package handshake

import (
	"fmt"
	"io"
)

// A Handshake is a special message that a peer uses to identify itself
type Handshake struct {
	Pstr     string
	InfoHash [20]byte
	PeerID   [20]byte
}

// New creates a new handshake with the standard pstr
func New(infoHash, peerID [20]byte) *Handshake {
	return &Handshake{
		Pstr:     "BitTorrent protocol",
		InfoHash: infoHash,
		PeerID:   peerID,
	}
}

// Serialize serializes the handshake to a buffer
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 reserved bytes
	curr += copy(buf[curr:], h.InfoHash[:])
	curr += copy(buf[curr:], h.PeerID[:])
	return buf
}

// Read parses a handshake from a stream
func Read(r io.Reader) (*Handshake, error) {
	lengthBuf := make([]byte, 1)
	_, err := io.ReadFull(r, lengthBuf)
	if err != nil {
		return nil, err
	}
	pstrlen := int(lengthBuf[0])

	if pstrlen == 0 {
		err := fmt.Errorf("pstrlen cannot be 0")
		return nil, err
	}

	handshakeBuf := make([]byte, 48+pstrlen)
	_, err = io.ReadFull(r, handshakeBuf)
	if err != nil {
		return nil, err
	}

	var infoHash, peerID [20]byte

	copy(infoHash[:], handshakeBuf[pstrlen+8:pstrlen+8+20])
	copy(peerID[:], handshakeBuf[pstrlen+8+20:])

	h := Handshake{
		Pstr:     string(handshakeBuf[0:pstrlen]),
		InfoHash: infoHash,
		PeerID:   peerID,
	}

	return &h, nil
}

message.go

这个Go语言包定义了用于BitTorrent协议中的消息格式和处理函数。在BitTorrent协议中,消息用于节点间的通信,例如请求文件的某个部分或者通知其他节点自己拥有的文件部分。下面是对这个代码包中主要组成部分的详细解释:

常量和messageID类型

messageID是一个uint8类型的别名,表示消息的ID。每种消息类型都有一个对应的ID,例如:

  • MsgChoke (ID = 0): 通知对方停止发送请求。
  • MsgUnchoke (ID = 1): 允许对方发送请求。
  • MsgInterested (ID = 2): 表示对对方发送的数据感兴趣。
  • MsgNotInterested (ID = 3): 表示对对方发送的数据不感兴趣。
  • MsgHave (ID = 4): 通知对方已经获取了某个文件片段。
  • 其他相应的ID用于其他类型的消息。

Message结构体

Message结构体包含了消息的ID (ID)和负载(Payload)。Payload是一个字节切片,其内容根据不同的消息类型而不同。

函数和方法

  • FormatRequest: 创建一个请求消息(MsgRequest),用于请求文件的一个特定的数据块。它接受三个参数:index(文件片段的索引)、begin(数据块在片段中的开始偏移量)、length(数据块的长度),然后生成对应的消息。
  • FormatHave: 创建一个通知消息(MsgHave),用于告知其他节点自己已经下载了一个文件片段。它接受一个参数:index(文件片段的索引)。
  • ParsePiece 和 ParseHave: 这些函数用于解析接收到的MsgPieceMsgHave消息,从中提取出有用的信息。
  • Serialize: 将一个Message对象序列化为一个字节切片,以便能够通过网络发送。如果消息对象是nil,则代表一个保持连接活跃的空消息。
  • Read: 从一个输入流中读取并解析出一个消息对象。首先读取消息长度,如果长度为0,则表示是一个保持连接的空消息;否则,读取相应长度的数据作为消息的内容。

辅助方法

  • name(): 返回消息类型的字符串表示,主要用于调试和日志记录。
  • String(): 返回消息的字符串表示形式,包括消息类型和负载的长度。如果消息为空,则返回"KeepAlive"。

总的来说,这个包定义了BitTorrent通信协议中的消息结构和一些基本的操作函数,用于创建、解析和序列化不同类型的消息,以支持BitTorrent客户端和节点间的通信。

package message

import (
	"encoding/binary"
	"fmt"
	"io"
)

type messageID uint8

const (
	// MsgChoke chokes the receiver
	MsgChoke messageID = 0
	// MsgUnchoke unchokes the receiver
	MsgUnchoke messageID = 1
	// MsgInterested expresses interest in receiving data
	MsgInterested messageID = 2
	// MsgNotInterested expresses disinterest in receiving data
	MsgNotInterested messageID = 3
	// MsgHave alerts the receiver that the sender has downloaded a piece
	MsgHave messageID = 4
	// MsgBitfield encodes which pieces that the sender has downloaded
	MsgBitfield messageID = 5
	// MsgRequest requests a block of data from the receiver
	MsgRequest messageID = 6
	// MsgPiece delivers a block of data to fulfill a request
	MsgPiece messageID = 7
	// MsgCancel cancels a request
	MsgCancel messageID = 8
)

// Message stores ID and payload of a message
type Message struct {
	ID      messageID
	Payload []byte
}

// FormatRequest creates a REQUEST message
func FormatRequest(index, begin, length int) *Message {
	payload := make([]byte, 12)
	binary.BigEndian.PutUint32(payload[0:4], uint32(index))
	binary.BigEndian.PutUint32(payload[4:8], uint32(begin))
	binary.BigEndian.PutUint32(payload[8:12], uint32(length))
	return &Message{ID: MsgRequest, Payload: payload}
}

// FormatHave creates a HAVE message
func FormatHave(index int) *Message {
	payload := make([]byte, 4)
	binary.BigEndian.PutUint32(payload, uint32(index))
	return &Message{ID: MsgHave, Payload: payload}
}

// ParsePiece parses a PIECE message and copies its payload into a buffer
func ParsePiece(index int, buf []byte, msg *Message) (int, error) {
	if msg.ID != MsgPiece {
		return 0, fmt.Errorf("Expected PIECE (ID %d), got ID %d", MsgPiece, msg.ID)
	}
	if len(msg.Payload) < 8 {
		return 0, fmt.Errorf("Payload too short. %d < 8", len(msg.Payload))
	}
	parsedIndex := int(binary.BigEndian.Uint32(msg.Payload[0:4]))
	if parsedIndex != index {
		return 0, fmt.Errorf("Expected index %d, got %d", index, parsedIndex)
	}
	begin := int(binary.BigEndian.Uint32(msg.Payload[4:8]))
	if begin >= len(buf) {
		return 0, fmt.Errorf("Begin offset too high. %d >= %d", begin, len(buf))
	}
	data := msg.Payload[8:]
	if begin+len(data) > len(buf) {
		return 0, fmt.Errorf("Data too long [%d] for offset %d with length %d", len(data), begin, len(buf))
	}
	copy(buf[begin:], data)
	return len(data), nil
}

// ParseHave parses a HAVE message
func ParseHave(msg *Message) (int, error) {
	if msg.ID != MsgHave {
		return 0, fmt.Errorf("Expected HAVE (ID %d), got ID %d", MsgHave, msg.ID)
	}
	if len(msg.Payload) != 4 {
		return 0, fmt.Errorf("Expected payload length 4, got length %d", len(msg.Payload))
	}
	index := int(binary.BigEndian.Uint32(msg.Payload))
	return index, nil
}

// Serialize serializes a message into a buffer of the form
// <length prefix><message ID><payload>
// Interprets `nil` as a keep-alive message
func (m *Message) Serialize() []byte {
	if m == nil {
		return make([]byte, 4)
	}
	length := uint32(len(m.Payload) + 1) // +1 for id
	buf := make([]byte, 4+length)
	binary.BigEndian.PutUint32(buf[0:4], length)
	buf[4] = byte(m.ID)
	copy(buf[5:], m.Payload)
	return buf
}

// Read parses a message from a stream. Returns `nil` on keep-alive message
func Read(r io.Reader) (*Message, error) {
	lengthBuf := make([]byte, 4)
	_, err := io.ReadFull(r, lengthBuf)
	if err != nil {
		return nil, err
	}
	length := binary.BigEndian.Uint32(lengthBuf)

	// keep-alive message
	if length == 0 {
		return nil, nil
	}

	messageBuf := make([]byte, length)
	_, err = io.ReadFull(r, messageBuf)
	if err != nil {
		return nil, err
	}

	m := Message{
		ID:      messageID(messageBuf[0]),
		Payload: messageBuf[1:],
	}

	return &m, nil
}

func (m *Message) name() string {
	if m == nil {
		return "KeepAlive"
	}
	switch m.ID {
	case MsgChoke:
		return "Choke"
	case MsgUnchoke:
		return "Unchoke"
	case MsgInterested:
		return "Interested"
	case MsgNotInterested:
		return "NotInterested"
	case MsgHave:
		return "Have"
	case MsgBitfield:
		return "Bitfield"
	case MsgRequest:
		return "Request"
	case MsgPiece:
		return "Piece"
	case MsgCancel:
		return "Cancel"
	default:
		return fmt.Sprintf("Unknown#%d", m.ID)
	}
}

func (m *Message) String() string {
	if m == nil {
		return m.name()
	}
	return fmt.Sprintf("%s [%d]", m.name(), len(m.Payload))
}

p2p.go

这段Go语言代码属于一个P2P(Peer-to-Peer)种子(Torrent)客户端的一部分,其主要功能是下载和组装来自多个对等节点(peers)的文件碎片(pieces)。代码通过实现BitTorrent协议的核心逻辑来实现这一功能。我将逐部分解读代码功能和逻辑。

包和导入

代码定义了一个名为p2p的包,并导入了一些必要的包,用于实现网络通信、日志记录、时间处理等功能。

常量定义

  • MaxBlockSize定义了可以从对等节点请求的最大数据块大小(字节)。
  • MaxBacklog定义了客户端可以同时保持未完成请求的最大数量。

数据结构

  • Torrent结构体包含了下载一个种子所需的所有信息,包括对等节点列表、PeerID、信息哈希、片段哈希、片段长度、文件总长度和文件名。
  • pieceWork表示一个待下载片段的工作单位,包含索引、哈希和长度。
  • pieceResult表示下载完成的片段结果,包含索引和数据缓冲区。
  • pieceProgress追踪特定片段的下载进度,包括下载量、请求量和未完成请求数。

主要功能和方法

  • readMessage方法处理与对等节点交换的消息,根据消息类型更新下载状态或处理下载的数据块。
  • attemptDownloadPiece尝试下载一个片段,通过发送请求并处理来自对等节点的响应。
  • checkIntegrity确认下载的片段数据与预期的哈希值匹配,以验证数据完整性。
  • startDownloadWorker为每个对等节点启动一个下载工作器,负责下载分配给该节点的片段。
  • calculateBoundsForPiececalculatePieceSize计算给定片段的起始和结束位置以及大小,以便正确地请求和存储数据。
  • Download是主要的下载逻辑入口点,初始化工作队列和结果收集,启动对每个对等节点的下载工作器,然后组装下载的片段。

下载过程

  1. Download方法初始化工作队列,将所有需要下载的片段(pieceWork)加入队列。
  2. 对于每个对等节点,startDownloadWorker方法作为一个单独的goroutine启动,尝试下载分配给它的片段。
  3. 工作器使用attemptDownloadPiece方法向对等节点发送数据块请求,处理响应,直到片段下载完毕。
  4. 下载的片段通过checkIntegrity方法检查数据完整性。如果通过,则将片段结果发送到结果通道。
  5. Download方法从结果通道收集完整的片段,按顺序组装它们以重建完整文件。

总结

这段代码是一个BitTorrent客户端的关键部分,它展示了如何从多个对等节点并发下载文件碎片,并通过校验碎片的完整性来确保数据的准确性。这个过程利用了Go语言的并发特性,通过goroutines和通道来实现高效的数据下载和处理。

package p2p

import (
	"bytes"
	"crypto/sha1"
	"fmt"
	"log"
	"runtime"
	"time"

	"github.com/veggiedefender/torrent-client/client"
	"github.com/veggiedefender/torrent-client/message"
	"github.com/veggiedefender/torrent-client/peers"
)

// MaxBlockSize is the largest number of bytes a request can ask for
const MaxBlockSize = 16384

// MaxBacklog is the number of unfulfilled requests a client can have in its pipeline
const MaxBacklog = 5

// Torrent holds data required to download a torrent from a list of peers
type Torrent struct {
	Peers       []peers.Peer
	PeerID      [20]byte
	InfoHash    [20]byte
	PieceHashes [][20]byte
	PieceLength int
	Length      int
	Name        string
}

type pieceWork struct {
	index  int
	hash   [20]byte
	length int
}

type pieceResult struct {
	index int
	buf   []byte
}

type pieceProgress struct {
	index      int
	client     *client.Client
	buf        []byte
	downloaded int
	requested  int
	backlog    int
}

func (state *pieceProgress) readMessage() error {
	msg, err := state.client.Read() // this call blocks
	if err != nil {
		return err
	}

	if msg == nil { // keep-alive
		return nil
	}

	switch msg.ID {
	case message.MsgUnchoke:
		state.client.Choked = false
	case message.MsgChoke:
		state.client.Choked = true
	case message.MsgHave:
		index, err := message.ParseHave(msg)
		if err != nil {
			return err
		}
		state.client.Bitfield.SetPiece(index)
	case message.MsgPiece:
		n, err := message.ParsePiece(state.index, state.buf, msg)
		if err != nil {
			return err
		}
		state.downloaded += n
		state.backlog--
	}
	return nil
}

func attemptDownloadPiece(c *client.Client, pw *pieceWork) ([]byte, error) {
	state := pieceProgress{
		index:  pw.index,
		client: c,
		buf:    make([]byte, pw.length),
	}

	// Setting a deadline helps get unresponsive peers unstuck.
	// 30 seconds is more than enough time to download a 262 KB piece
	c.Conn.SetDeadline(time.Now().Add(30 * time.Second))
	defer c.Conn.SetDeadline(time.Time{}) // Disable the deadline

	for state.downloaded < pw.length {
		// If unchoked, send requests until we have enough unfulfilled requests
		if !state.client.Choked {
			for state.backlog < MaxBacklog && state.requested < pw.length {
				blockSize := MaxBlockSize
				// Last block might be shorter than the typical block
				if pw.length-state.requested < blockSize {
					blockSize = pw.length - state.requested
				}

				err := c.SendRequest(pw.index, state.requested, blockSize)
				if err != nil {
					return nil, err
				}
				state.backlog++
				state.requested += blockSize
			}
		}

		err := state.readMessage()
		if err != nil {
			return nil, err
		}
	}

	return state.buf, nil
}

func checkIntegrity(pw *pieceWork, buf []byte) error {
	hash := sha1.Sum(buf)
	if !bytes.Equal(hash[:], pw.hash[:]) {
		return fmt.Errorf("Index %d failed integrity check", pw.index)
	}
	return nil
}

func (t *Torrent) startDownloadWorker(peer peers.Peer, workQueue chan *pieceWork, results chan *pieceResult) {
	c, err := client.New(peer, t.PeerID, t.InfoHash)
	if err != nil {
		log.Printf("Could not handshake with %s. Disconnecting\n", peer.IP)
		return
	}
	defer c.Conn.Close()
	log.Printf("Completed handshake with %s\n", peer.IP)

	c.SendUnchoke()
	c.SendInterested()

	for pw := range workQueue {
		if !c.Bitfield.HasPiece(pw.index) {
			workQueue <- pw // Put piece back on the queue
			continue
		}

		// Download the piece
		buf, err := attemptDownloadPiece(c, pw)
		if err != nil {
			log.Println("Exiting", err)
			workQueue <- pw // Put piece back on the queue
			return
		}

		err = checkIntegrity(pw, buf)
		if err != nil {
			log.Printf("Piece #%d failed integrity check\n", pw.index)
			workQueue <- pw // Put piece back on the queue
			continue
		}

		c.SendHave(pw.index)
		results <- &pieceResult{pw.index, buf}
	}
}

func (t *Torrent) calculateBoundsForPiece(index int) (begin int, end int) {
	begin = index * t.PieceLength
	end = begin + t.PieceLength
	if end > t.Length {
		end = t.Length
	}
	return begin, end
}

func (t *Torrent) calculatePieceSize(index int) int {
	begin, end := t.calculateBoundsForPiece(index)
	return end - begin
}

// Download downloads the torrent. This stores the entire file in memory.
func (t *Torrent) Download() ([]byte, error) {
	log.Println("Starting download for", t.Name)
	// Init queues for workers to retrieve work and send results
	workQueue := make(chan *pieceWork, len(t.PieceHashes))
	results := make(chan *pieceResult)
	for index, hash := range t.PieceHashes {
		length := t.calculatePieceSize(index)
		workQueue <- &pieceWork{index, hash, length}
	}

	// Start workers
	for _, peer := range t.Peers {
		go t.startDownloadWorker(peer, workQueue, results)
	}

	// Collect results into a buffer until full
	buf := make([]byte, t.Length)
	donePieces := 0
	for donePieces < len(t.PieceHashes) {
		res := <-results
		begin, end := t.calculateBoundsForPiece(res.index)
		copy(buf[begin:end], res.buf)
		donePieces++

		percent := float64(donePieces) / float64(len(t.PieceHashes)) * 100
		numWorkers := runtime.NumGoroutine() - 1 // subtract 1 for main thread
		log.Printf("(%0.2f%%) Downloaded piece #%d from %d peers\n", percent, res.index, numWorkers)
	}
	close(workQueue)

	return buf, nil
}

bitfield.go

package bitfield

// Bitfield 是一个表示peer拥有哪些数据片段的位图。
type Bitfield []byte

// HasPiece 检查Bitfield是否包含特定索引处的片段。
// 它返回true如果该位已被设置,表示拥有该数据片段。
func (bf Bitfield) HasPiece(index int) bool {
    // 计算索引在哪个字节以及在该字节中的偏移位
    byteIndex := index / 8 // 由于一个字节有8位,所以通过整除获取字节索引
    offset := index % 8 // 取余数得到在字节内的偏移位

    // 如果计算出的字节索引超出范围,则返回false
    if byteIndex < 0 || byteIndex >= len(bf) {
        return false
    }

    // 通过右移操作和与操作检查特定位是否被设置
    // 如果该位为1,表示拥有该数据片段,返回true
    return bf[byteIndex]>>uint(7-offset)&1 != 0
}

// SetPiece 在Bitfield中设置一个位,表示现在拥有了特定索引处的数据片段。
func (bf Bitfield) SetPiece(index int) {
    // 同样,计算索引对应的字节位置和偏移量
    byteIndex := index / 8
    offset := index % 8

    // 如果计算出的字节索引无效,则静默返回,不执行任何操作
    if byteIndex < 0 || byteIndex >= len(bf) {
        return
    }

    // 通过左移操作将1移至正确位置,并使用按位或操作来设置该位
    // 这样就在Bitfield中标记了拥有特定索引处的数据片段
    bf[byteIndex] |= 1 << uint(7 - offset)
}

peer.go

这个Go包名为peers,用于解析和表示BitTorrent网络中的同伴(peer)信息。每个同伴由其IP地址和端口号组成,这样可以在网络中进行相互连接。该包提供了从原始二进制数据中解析同伴信息的功能以及对同伴信息的简单表述。以下是详细说明:

结构体 Peer

Peer 结构体用来编码一个同伴的连接信息。

  • IP: 使用net.IP类型来存储同伴的IP地址。net.IP是一个字节切片,能够处理IPv4和IPv6地址。
  • Port: 存储同伴端口号的无符号16位整数。

函数 Unmarshal

Unmarshal 函数用于从一段二进制数据中解析出同伴的IP地址和端口号,这些信息通常由tracker服务器返回。

  • peersBin: 一个字节切片,包含了连续的同伴信息。每个同伴的信息由6个字节组成,前4个字节为IP地址,后2个字节为端口号。
  • 函数首先计算得到同伴数量,即二进制数据长度除以每个同伴信息的字节大小(6)。
  • 如果peersBin的长度不是6的倍数,则说明数据格式有误,返回错误。
  • 然后,函数通过迭代每个同伴信息块,解析出IP地址和端口号,并将这些信息填充到Peer结构体的切片中。
  • IP地址直接根据偏移量从peersBin中切片得到。
  • 端口号使用binary.BigEndian.Uint16函数从对应的二进制数据中解析得到,注意这里假设网络字节顺序为大端序。
  • 最终,函数返回一个填充好的Peer结构体切片。

方法 String

String 方法为Peer结构体实现了Stringer接口,这使得同伴信息可以以易于阅读的形式输出。

  • 方法使用net.JoinHostPort函数将IP字段和Port字段合并为一个字符串,格式为"IP:Port"Port字段首先被转换成int类型,然后转换为字符串。
  • 这种格式便于打印和记录同伴信息。

此包为处理和展示BitTorrent同伴信息提供了基本工具,使得从原始的tracker响应中提取有用信息并以人类可读的形式展示变得简单。

package peers

import (
	"encoding/binary"
	"fmt"
	"net"
	"strconv"
)

// Peer encodes connection information for a peer
type Peer struct {
	IP   net.IP
	Port uint16
}

// Unmarshal parses peer IP addresses and ports from a buffer
func Unmarshal(peersBin []byte) ([]Peer, error) {
	const peerSize = 6 // 4 for IP, 2 for port
	numPeers := len(peersBin) / peerSize
	if len(peersBin)%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(peersBin[offset : offset+4])
		peers[i].Port = binary.BigEndian.Uint16([]byte(peersBin[offset+4 : offset+6]))
	}
	return peers, nil
}

func (p Peer) String() string {
	return net.JoinHostPort(p.IP.String(), strconv.Itoa(int(p.Port)))
}

 torrentfile.go

这个Go语言的包定义了与处理 .torrent 文件相关的数据结构和函数,并实现了从 .torrent 文件中读取数据以及用这些数据启动下载任务的功能。以下是包中每个部分的详细解释:

常数和类型定义

  • const Port uint16 = 6881: 这行代码定义了一个常数 Port,其值设定为6881,这是BitTorrent客户端监听的默认端口之一。

TorrentFile 类型

  • TorrentFile 结构体用于存储 .torrent 文件中编码的元数据。这些元数据包括:
    • Announce: Tracker的URL,客户端通过它找到其他共享文件的peers。
    • InfoHash: Torrent文件信息部分的SHA1哈希值,用于唯一标识一个torrent。
    • PieceHashes: 一个20字节长哈希数组,每个哈希对应文件的一个片段。
    • PieceLength: 每个文件片段的长度(字节数)。
    • Length: 整个文件的长度。
    • Name: 文件名。

bencodeInfo 和 bencodeTorrent 类型

  • bencodeInfo 结构体用于解析bencode编码的 .torrent 文件中的 info 字典。它包含上面提到的几个字段。
  • bencodeTorrent 结构体表示整个bencode编码的 .torrent 文件,包括 announce 字段和 info 字典。

DownloadToFile 方法

  • DownloadToFile 方法是 TorrentFile 结构体的一个接收者方法,它实现了下载功能,并将下载的内容保存到文件系统中的一个文件中。它做了以下几个步骤:
    • 生成一个随机的peer ID。
    • 通过 requestPeers 方法请求peers(这个方法没有在你提供的代码中定义,可能在其他文件中)。
    • 使用获取到的peers等信息构建 p2p.Torrent 对象。
    • 调用 Download 方法开始下载。
    • 将下载的内容写入到指定的文件路径中。

Open 函数

  • Open 函数是一个用来解析 .torrent 文件并返回 TorrentFile 实例的函数。它打开文件,解析bencode编码的内容,计算info哈希,并分割piece哈希。

hash 和 splitPieceHashes 方法

  • hash 方法用于计算 bencodeInfo 结构体的SHA1哈希值,这是torrent文件中的 InfoHash
  • splitPieceHashes 方法用于将 bencodeInfo 中的 Pieces 字符串分割成一个20字节哈希数组,每个哈希对应于文件的一个片段。

toTorrentFile 方法

  • toTorrentFile 方法将 bencodeTorrent 结构体转换为 TorrentFile 结构体。这个过程涉及调用 hash 和 splitPieceHashes 方法来填充 TorrentFile 的 InfoHash 和 PieceHashes 字段。

这个包提供了处理 .torrent 文件、开始下载任务并将数据保存到文件中的基本功能。不过,代码示例中缺少了一些方法的实现,例如 requestPeers 方法,以及实际的下载逻辑,这些可能定义在包的其他部分。

package torrentfile

import (
	"bytes"
	"crypto/rand"
	"crypto/sha1"
	"fmt"
	"os"

	"github.com/jackpal/bencode-go"
	"github.com/veggiedefender/torrent-client/p2p"
)

// Port to listen on
const Port uint16 = 6881

// TorrentFile encodes the metadata from a .torrent file
type TorrentFile struct {
	Announce    string
	InfoHash    [20]byte
	PieceHashes [][20]byte
	PieceLength int
	Length      int
	Name        string
}

type bencodeInfo struct {
	Pieces      string `bencode:"pieces"`
	PieceLength int    `bencode:"piece length"`
	Length      int    `bencode:"length"`
	Name        string `bencode:"name"`
}

type bencodeTorrent struct {
	Announce string      `bencode:"announce"`
	Info     bencodeInfo `bencode:"info"`
}

// DownloadToFile downloads a torrent and writes it to a file
func (t *TorrentFile) DownloadToFile(path string) error {
	var peerID [20]byte
	_, err := rand.Read(peerID[:])
	if err != nil {
		return err
	}

	peers, err := t.requestPeers(peerID, Port)
	if err != nil {
		return err
	}

	torrent := p2p.Torrent{
		Peers:       peers,
		PeerID:      peerID,
		InfoHash:    t.InfoHash,
		PieceHashes: t.PieceHashes,
		PieceLength: t.PieceLength,
		Length:      t.Length,
		Name:        t.Name,
	}
	buf, err := torrent.Download()
	if err != nil {
		return err
	}

	outFile, err := os.Create(path)
	if err != nil {
		return err
	}
	defer outFile.Close()
	_, err = outFile.Write(buf)
	if err != nil {
		return err
	}
	return nil
}

// Open parses a torrent file
func Open(path string) (TorrentFile, error) {
	file, err := os.Open(path)
	if err != nil {
		return TorrentFile{}, err
	}
	defer file.Close()

	bto := bencodeTorrent{}
	err = bencode.Unmarshal(file, &bto)
	if err != nil {
		return TorrentFile{}, err
	}
	return bto.toTorrentFile()
}

func (i *bencodeInfo) hash() ([20]byte, error) {
	var buf bytes.Buffer
	err := bencode.Marshal(&buf, *i)
	if err != nil {
		return [20]byte{}, err
	}
	h := sha1.Sum(buf.Bytes())
	return h, nil
}

func (i *bencodeInfo) splitPieceHashes() ([][20]byte, error) {
	hashLen := 20 // Length of SHA-1 hash
	buf := []byte(i.Pieces)
	if len(buf)%hashLen != 0 {
		err := fmt.Errorf("Received malformed pieces of length %d", len(buf))
		return nil, err
	}
	numHashes := len(buf) / hashLen
	hashes := make([][20]byte, numHashes)

	for i := 0; i < numHashes; i++ {
		copy(hashes[i][:], buf[i*hashLen:(i+1)*hashLen])
	}
	return hashes, nil
}

func (bto *bencodeTorrent) toTorrentFile() (TorrentFile, error) {
	infoHash, err := bto.Info.hash()
	if err != nil {
		return TorrentFile{}, err
	}
	pieceHashes, err := bto.Info.splitPieceHashes()
	if err != nil {
		return TorrentFile{}, err
	}
	t := TorrentFile{
		Announce:    bto.Announce,
		InfoHash:    infoHash,
		PieceHashes: pieceHashes,
		PieceLength: bto.Info.PieceLength,
		Length:      bto.Info.Length,
		Name:        bto.Info.Name,
	}
	return t, nil
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值