教程:用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表示有這個分片。


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

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>

    整個交換信息的流程是:

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

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


    分享到:


    相關文章: