教程:用golang從零開始手寫一個bt下載客戶端(6)

背景

上篇文章我們介紹了消息的格式,並實現了消息的序列化和讀取,這篇文章將詳細介紹消息的類型和含義,並且通多代碼實現消息的發送和接收。

消息解釋

keep-alive:

keep-alive消息是一個長度為0的消息,它沒有message ID也沒有載荷。在一定週期內,peers如果沒有收到消息,有可能關閉連接,因此需要發送一個keep-alive消息維護長時間(通常為2分鐘)連接。

choke:

choke消息長度為1,沒有載荷,表示阻塞

unchoke:

unchoke消息長度為1,沒有載荷,表示解除阻塞

interested:

interested消息長度為1,沒有載荷,表示感興趣

not interested:

not interested消息長度為1,沒有載荷,表示不感興趣

have: <piece>

have消息長度為5,載荷是一個分片的從零開始的已經下載的索引值

bitfield: <bitfield>

bitfield消息只有在握手完成後立即發送,並且要在其他消息前發送。他的長度是可變的,載荷是一個位字段,表示已經成功下載了的分片,每個分片對應一位,高位代表0分片的索引,其中0表示還沒有這個分片,1表示有這個分片。


bitfield


request: <index><begin><length>/<begin>/<index>

request消息長度為13,用來請求一個塊,載荷包含如下信息:

index: integer 從0開始的分片索引begin: integer 從0開始的分片內的偏移length: integer 請求的長度(有爭議,因為沒有規定多長合適,太小了請求數量大,太長了容易斷)

piece: <index><begin><block>/<begin>/<index>

piece消息是長度可變的,x是塊的長度,載荷包含如下信息:

index: integer 從0開始的分片索引begin: integer 從0開始的分片內的偏移block: block of data 分片的子集

代碼實現

<code>package client

import (
\t"bytes"
\t"fmt"
\t"net"
\t"p2p/bitfield"
\t"p2p/handshake"
\t"p2p/message"
\t"p2p/peers"
\t"time"
)

type Client struct {
\tConn net.Conn
\tChoked bool
\tBitfield bitfield.Bitfield
\tpeer peers.Peer
\tinfoHash [20]byte
\tpeerID [20]byte
}

func completeHandshake(conn net.Conn, infohash, peerID [20]byte) (*handshake.Handshake, error) {
\tconn.SetDeadline(time.Now().Add(3 * time.Second))
\tdefer conn.SetDeadline(time.Time{})
\treq := handshake.New(infohash, peerID)
\t_, err := conn.Write(req.Serialize())
\tif err != nil {
\t\treturn nil, err
\t}
\tres, err := handshake.Read(conn)


\tif err != nil {
\t\treturn nil, err
\t}
\tif !bytes.Equal(res.InfoHash[:], infohash[:]) {
\t\treturn nil, fmt.Errorf("Expected infohash %x got %x", infohash, res.InfoHash)
\t}
\treturn res, nil
}

func recvBitfield(conn net.Conn) (bitfield.Bitfield, error) {
\tconn.SetDeadline(time.Now().Add(5 * time.Second))
\tdefer conn.SetDeadline(time.Time{})
\tmsg, err := message.Read(conn)
\tif err != nil {
\t\treturn nil, err
\t}
\tif msg.ID != message.MsgBitfield {
\t\terr := fmt.Errorf("Excepted bitfield but got %d", msg.ID)
\t\treturn nil, err
\t}
\treturn msg.Payload, nil
}

func New(peer peers.Peer, peerID [20]byte, infohash [20]byte) (*Client, error) {
\tconn, err := net.DialTimeout("tcp", peer.String(), 3*time.Second)
\tif err != nil {
\t\treturn nil, err
\t}
\t_, err = completeHandshake(conn, infohash, peerID)
\tif err != nil {
\t\tconn.Close()
\t\treturn nil, err
\t}
\tbf, err := recvBitfield(conn)
\tif err != nil {
\t\tconn.Close()
\t\treturn nil, err
\t}
\tclient := &Client{
\t\tConn: conn,
\t\tChoked: true,
\t\tBitfield: bf,
\t\tpeer: peer,
\t\tinfoHash: infohash,
\t\tpeerID: peerID,
\t}
\treturn client, nil
}

func (c *Client) SendUnchoke() error {

\tmsg := message.Message{ID: message.MsgUnchoke}
\t_, err := c.Conn.Write(msg.Serialize())
\treturn err
}

func (c *Client) SendInterested() error {
\tmsg := message.Message{ID: message.MsgInterested}
\t_, err := c.Conn.Write(msg.Serialize())
\treturn err
}

func (c *Client) SendHave(index int) error {
\tmsg := message.FormatHave(index)
\t_, err := c.Conn.Write(msg.Serialize())
\treturn err
}

func (c *Client) SendRequest(index int, begin int, length int) error {
\treq := message.FormatRequest(index, begin, length)
\t_, err := c.Conn.Write(req.Serialize())
\treturn err
}

func (c *Client) Read() (*message.Message, error) {
\tmsg, err := message.Read(c.Conn)
\treturn msg, err
}
/<code>

整個交換信息的流程是:

建立tcp連接->撥號發起握手->“你好”“你好”發送bitfield請求->“你都有哪些分片”“00100100”發送unchoke請求->“請問你準備好了嗎?”“沒有阻塞,可以”發送interested請求->“我感興趣”發起request請求->“請給我第n片數據”“給你第n片數據”校驗piece->“這個分片有沒有被篡改?”發起have請求->“我已經擁有這個分片了”更新已下載信息->“增加已下載的數量,更新自己的bitfield”

好,到目前為止,我們已經可以和peers通信了,下篇我們將介紹比較有趣的併發和狀態管理。敬請關注!