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
- 接受两个参数
infoHash
和peerID
,它们都是20字节的数组。 - 返回一个指向
Handshake
结构体的指针,该结构体已经使用提供的信息散列和节点ID以及标准的Pstr
初始化。
方法 Serialize
- 属于
Handshake
类型,没有参数。 - 将
Handshake
实例序列化成一个字节切片。序列化的格式包括:Pstr
的长度,Pstr
字符串本身,8字节的保留空间(初始化为0),InfoHash
,最后是PeerID
。 - 返回序列化后的字节切片。
函数 Read
- 接受一个实现了
io.Reader
接口的参数r
,用于从中读取数据。 - 首先读取1个字节,表示
Pstr
的长度。如果这个长度为0,则返回错误,因为长度不能为0。 - 根据
Pstr
长度读取接下来的数据,包括Pstr
本身,8字节的保留空间,InfoHash
和PeerID
。 - 将读取的数据解析成
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
: 这些函数用于解析接收到的MsgPiece
和MsgHave
消息,从中提取出有用的信息。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
为每个对等节点启动一个下载工作器,负责下载分配给该节点的片段。calculateBoundsForPiece
和calculatePieceSize
计算给定片段的起始和结束位置以及大小,以便正确地请求和存储数据。Download
是主要的下载逻辑入口点,初始化工作队列和结果收集,启动对每个对等节点的下载工作器,然后组装下载的片段。
下载过程
Download
方法初始化工作队列,将所有需要下载的片段(pieceWork)加入队列。- 对于每个对等节点,
startDownloadWorker
方法作为一个单独的goroutine启动,尝试下载分配给它的片段。 - 工作器使用
attemptDownloadPiece
方法向对等节点发送数据块请求,处理响应,直到片段下载完毕。 - 下载的片段通过
checkIntegrity
方法检查数据完整性。如果通过,则将片段结果发送到结果通道。 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
}