diff --git a/down/bt/client.go b/down/bt/client.go index 0f589fba..0d2d368a 100644 --- a/down/bt/client.go +++ b/down/bt/client.go @@ -2,8 +2,8 @@ package bt import ( "crypto/rand" - "gopeed/down/bt/metainfo" - "gopeed/down/bt/torrent" + "github.com/monkeyWie/gopeed/down/bt/metainfo" + "github.com/monkeyWie/gopeed/down/bt/torrent" "net/url" ) diff --git a/down/bt/metainfo/metainfo.go b/down/bt/metainfo/metainfo.go index af16b422..2efa7b1c 100644 --- a/down/bt/metainfo/metainfo.go +++ b/down/bt/metainfo/metainfo.go @@ -39,8 +39,8 @@ type File struct { } type ExtraInfo struct { - InfoHash [20]byte - FileSize uint64 + InfoHash [20]byte + TotalSize uint64 } func ParseFromFile(path string) (*MetaInfo, error) { @@ -75,10 +75,22 @@ func ParseFromFile(path string) (*MetaInfo, error) { } } } - metaInfo.FileSize = getTotalSize(&metaInfo) + metaInfo.TotalSize = getTotalSize(&metaInfo) return &metaInfo, nil } +// 获取某个分片的大小 +func (metaInfo *MetaInfo) GetPieceSize(index int) uint64 { + // 是否为最后一个分片 + if index == len(metaInfo.Info.Pieces)-1 { + size := metaInfo.TotalSize % metaInfo.Info.PieceLength + if size > 0 { + return size + } + } + return metaInfo.Info.PieceLength +} + func getTotalSize(metaInfo *MetaInfo) uint64 { if metaInfo.Info != nil { if len(metaInfo.Info.Files) > 0 { diff --git a/down/bt/peer/message.go b/down/bt/peer/message/message.go similarity index 51% rename from down/bt/peer/message.go rename to down/bt/peer/message/message.go index 80ec6621..874ac901 100644 --- a/down/bt/peer/message.go +++ b/down/bt/peer/message/message.go @@ -1,31 +1,52 @@ -package peer +package message import ( "encoding/binary" ) -type MessageID int +// message ID +type ID byte const ( - Keepalive MessageID = -1 - Choke MessageID = 0 - Unchoke MessageID = 1 - Interested MessageID = 2 - NotInterested MessageID = 3 - Have MessageID = 4 - Bitfield MessageID = 5 - Request MessageID = 6 - Piece MessageID = 7 - Cancel MessageID = 8 + IdChoke ID = 0 + IdUnchoke ID = 1 + IdInterested ID = 2 + IdNotinterested ID = 3 + IdHave ID = 4 + IdBitfield ID = 5 + IdRequest ID = 6 + IdPiece ID = 7 + IdCancel ID = 8 ) -type Message struct { +/*type Message struct { Length uint32 ID MessageID Payload []byte +}*/ +type Message struct { + Length uint32 + ID ID } -func NewMessage(id MessageID, payload []byte) *Message { +func (msg Message) Encode() []byte { + buf := make([]byte, 5) + binary.BigEndian.PutUint32(buf, msg.Length) + buf[4] = byte(msg.ID) + return buf +} + +func (msg Message) Decode(buf []byte) { + msg.Length = binary.BigEndian.Uint32(buf) + msg.ID = ID(buf[4]) +} + +type Serialize interface { + Encode() []byte + Decode(buf []byte) Serialize +} + +/*func NewMessage(id MessageID, payload []byte) *Message { message := &Message{ID: id, Payload: payload} if id == Keepalive { message.Length = 0 @@ -57,7 +78,7 @@ func (msg *Message) Decode(buf []byte) { msg.Payload = buf[5:] } } -} +}*/ // Message protocol // Length prefix| message ID | Payload @@ -73,39 +94,3 @@ func SplitMessage(data []byte, atEOF bool) (advance int, token []byte, err error } return } - -type MsgBitfield []byte - -// 某个分片是否下载完成 -func (mb MsgBitfield) IsComplete(i int) bool { - bts := []byte(mb) - index := i / 8 - if index >= len(bts) { - return false - } - return bts[index]&(1< 0 -} - -// 给定一组分片下载状态,计算出当前peer能提供下载的分片下标 -func (mb MsgBitfield) Have(pieces []bool) []int { - arr := make([]int, 0) - bts := []byte(mb) - length := len(pieces) / 8 - if len(pieces)%8 != 0 { - length++ - } - - for i := 0; i < length; i++ { - for j := 0; j < 8; j++ { - index := i*8 + j - // 如果此分片在本地还未下载,检查peer是否能提供该分片下载 - if index < len(pieces) && - !pieces[index] && - i < len(bts) && - uint(bts[i])&(1< 0 { - arr = append(arr, index) - } - } - } - return arr -} diff --git a/down/bt/peer/message/msg_bitfield.go b/down/bt/peer/message/msg_bitfield.go new file mode 100644 index 00000000..5d84c6fe --- /dev/null +++ b/down/bt/peer/message/msg_bitfield.go @@ -0,0 +1,59 @@ +package message + +// bitfield: +type Bitfield struct { + Message + payload []byte +} + +func NewBitfield(payload []byte) *Bitfield { + return &Bitfield{ + Message: Message{ + Length: uint32(len(payload) + 1), + ID: IdBitfield, + }, + payload: payload, + } +} + +func (b *Bitfield) Encode() []byte { + return append(b.Message.Encode(), b.payload...) +} + +func (b *Bitfield) Decode(buf []byte) Serialize { + b.Message.Decode(buf) + copy(b.payload, buf[5:]) + return b +} + +// 某个分片是否下载完成 +func (b *Bitfield) IsComplete(i int) bool { + index := i / 8 + if index >= len(b.payload) { + return false + } + return b.payload[index]&(1<<(7-i%8)) > 0 +} + +// 给定一组分片下载状态,计算出当前peer能提供下载的分片下标 +func (b *Bitfield) Have(pieces []bool) []int { + arr := make([]int, 0) + length := len(pieces) / 8 + if len(pieces)%8 != 0 { + length++ + } + + for i := 0; i < length; i++ { + for j := 0; j < 8; j++ { + index := i*8 + j + // 如果此分片在本地还未下载,检查peer是否能提供该分片下载 + if index < len(pieces) && + !pieces[index] && + i < len(b.payload) && + b.payload[i]&(1<<(7-j)) > 0 { + arr = append(arr, index) + } + } + } + return arr +} diff --git a/down/bt/peer/message/msg_choke.go b/down/bt/peer/message/msg_choke.go new file mode 100644 index 00000000..ed069aa1 --- /dev/null +++ b/down/bt/peer/message/msg_choke.go @@ -0,0 +1,24 @@ +package message + +// choke: +type Choke struct { + Message +} + +func NewChoke() *Choke { + return &Choke{ + Message{ + Length: 1, + ID: IdChoke, + }, + } +} + +func (c *Choke) Encode() []byte { + return c.Message.Encode() +} + +func (c *Choke) Decode(buf []byte) Serialize { + c.Message.Decode(buf) + return c +} diff --git a/down/bt/peer/message/msg_interested.go b/down/bt/peer/message/msg_interested.go new file mode 100644 index 00000000..e14109b0 --- /dev/null +++ b/down/bt/peer/message/msg_interested.go @@ -0,0 +1,24 @@ +package message + +// choke: +type Interested struct { + Message +} + +func NewInterested() *Interested { + return &Interested{ + Message{ + Length: 1, + ID: IdInterested, + }, + } +} + +func (c *Interested) Encode() []byte { + return c.Message.Encode() +} + +func (c *Interested) Decode(buf []byte) Serialize { + c.Message.Decode(buf) + return c +} diff --git a/down/bt/peer/message/msg_keepalive.go b/down/bt/peer/message/msg_keepalive.go new file mode 100644 index 00000000..31c8157d --- /dev/null +++ b/down/bt/peer/message/msg_keepalive.go @@ -0,0 +1,12 @@ +package message + +// keep-alive: +type Keepalive struct{} + +func (k *Keepalive) Encode() []byte { + return []byte{0, 0, 0, 0} +} + +func (k *Keepalive) Decode(buf []byte) Serialize { + return &Keepalive{} +} diff --git a/down/bt/peer/message/msg_not_interested.go b/down/bt/peer/message/msg_not_interested.go new file mode 100644 index 00000000..27d1182a --- /dev/null +++ b/down/bt/peer/message/msg_not_interested.go @@ -0,0 +1,24 @@ +package message + +// choke: +type NotInterested struct { + Message +} + +func NewNotInterested() *NotInterested { + return &NotInterested{ + Message{ + Length: 1, + ID: IdNotinterested, + }, + } +} + +func (ni *NotInterested) Encode() []byte { + return ni.Message.Encode() +} + +func (ni *NotInterested) Decode(buf []byte) Serialize { + ni.Message.Decode(buf) + return ni +} diff --git a/down/bt/peer/message/msg_piece.go b/down/bt/peer/message/msg_piece.go new file mode 100644 index 00000000..f0da5b35 --- /dev/null +++ b/down/bt/peer/message/msg_piece.go @@ -0,0 +1,23 @@ +package message + +import "io" + +// piece: +type Piece struct { + Message + Index uint32 + Begin uint32 + Block io.Reader +} + +func newPiece(index uint32, begin uint32, reader io.Reader, length uint32) *Piece { + return &Piece{ + Message: Message{ + Length: 9 + length, + ID: IdPiece, + }, + Index: index, + Begin: begin, + Block: io.LimitReader(reader, int64(length)), + } +} diff --git a/down/bt/peer/message/msg_request.go b/down/bt/peer/message/msg_request.go new file mode 100644 index 00000000..4b17cde5 --- /dev/null +++ b/down/bt/peer/message/msg_request.go @@ -0,0 +1,41 @@ +package message + +import ( + "encoding/binary" +) + +// request: +type Request struct { + Message + Index uint32 + Begin uint32 + Length uint32 +} + +func NewRequest(index uint32, begin uint32, length uint32) *Request { + return &Request{ + Message: Message{ + Length: 13, + ID: IdRequest, + }, + Index: index, + Begin: begin, + Length: length, + } +} + +func (r *Request) Encode() []byte { + buf := make([]byte, 12) + binary.BigEndian.PutUint32(buf[:4], r.Index) + binary.BigEndian.PutUint32(buf[4:8], r.Begin) + binary.BigEndian.PutUint32(buf[8:], r.Length) + return append(r.Message.Encode(), buf...) +} + +func (r *Request) Decode(buf []byte) Serialize { + r.Message.Decode(buf) + r.Length = binary.BigEndian.Uint32(buf[5:9]) + r.Begin = binary.BigEndian.Uint32(buf[9:13]) + r.Index = binary.BigEndian.Uint32(buf[13:17]) + return r +} diff --git a/down/bt/peer/message/msg_unchoke.go b/down/bt/peer/message/msg_unchoke.go new file mode 100644 index 00000000..cfc0d41a --- /dev/null +++ b/down/bt/peer/message/msg_unchoke.go @@ -0,0 +1,24 @@ +package message + +// unchoke: +type Unchoke struct { + Message +} + +func NewUnchoke() *Unchoke { + return &Unchoke{ + Message{ + Length: 1, + ID: IdUnchoke, + }, + } +} + +func (u *Unchoke) Encode() []byte { + return u.Message.Encode() +} + +func (u *Unchoke) Decode(buf []byte) Serialize { + u.Message.Decode(buf) + return u +} diff --git a/down/bt/peer/message_test.go b/down/bt/peer/message_test.go deleted file mode 100644 index 0dd64576..00000000 --- a/down/bt/peer/message_test.go +++ /dev/null @@ -1,138 +0,0 @@ -package peer - -import ( - "fmt" - "reflect" - "strconv" - "testing" -) - -func TestMsgBitfield_IsComplete(t *testing.T) { - type args struct { - i int - } - tests := []struct { - name string - mb MsgBitfield - args args - want bool - }{ - { - "case-0", - MsgBitfield([]byte{convertByte("10000000")}), - args{ - 0, - }, - true, - }, - { - "case-1", - MsgBitfield([]byte{convertByte("01000000")}), - args{ - 1, - }, - true, - }, - { - "case-2", - MsgBitfield([]byte{convertByte("00000000")}), - args{ - 2, - }, - false, - }, - { - "case-10", - MsgBitfield([]byte{convertByte("00000000"), convertByte("00100000")}), - args{ - 10, - }, - true, - }, - { - "case-11", - MsgBitfield([]byte{convertByte("00000000"), convertByte("00100000")}), - args{ - 11, - }, - false, - }, - { - "case-20", - MsgBitfield([]byte{convertByte("00000000"), convertByte("00100000")}), - args{ - 20, - }, - false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := tt.mb.IsComplete(tt.args.i); got != tt.want { - t.Errorf("MsgBitfield.get() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestMsgBitfield_Have(t *testing.T) { - type args struct { - pieces []bool - } - tests := []struct { - name string - mb MsgBitfield - args args - want []int - }{ - { - "case-1", - MsgBitfield([]byte{convertByte("11111111"), convertByte("00000000")}), - args{ - []bool{false, false, false, false, false, false, false, false}, - }, - []int{0, 1, 2, 3, 4, 5, 6, 7}, - }, - { - "case-2", - MsgBitfield([]byte{convertByte("01010101"), convertByte("01000000")}), - args{ - []bool{false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false}, - }, - []int{1, 3, 5, 7, 9}, - }, - { - "case-3", - MsgBitfield([]byte{convertByte("11010101"), convertByte("01000000")}), - args{ - []bool{false, true, false}, - }, - []int{0}, - }, - { - "case-4", - MsgBitfield([]byte{convertByte("11010101")}), - args{ - []bool{false, true, false, false, true, false, false, true, false}, - }, - []int{0, 3, 5}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := tt.mb.Have(tt.args.pieces); !reflect.DeepEqual(got, tt.want) { - t.Errorf("MsgBitfield.Have() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestMsg(t *testing.T) { - fmt.Println(Choke) - fmt.Println(Bitfield) -} - -func convertByte(bit string) byte { - i, _ := strconv.ParseUint(bit, 2, 8) - return byte(i) -} diff --git a/down/bt/torrent/peer_state.go b/down/bt/torrent/peer_state.go index e2d8a97b..314762d0 100644 --- a/down/bt/torrent/peer_state.go +++ b/down/bt/torrent/peer_state.go @@ -5,8 +5,10 @@ import ( "encoding/binary" "fmt" "github.com/cenkalti/mse" - "gopeed/down/bt/peer" + "github.com/monkeyWie/gopeed/down/bt/peer" + "github.com/monkeyWie/gopeed/down/bt/peer/message" "io" + "math" "net" ) @@ -23,7 +25,8 @@ type peerState struct { // peer is interested in this client peerInterested bool - bitfield peer.MsgBitfield + writeMsgCh chan []byte + bitfield *message.Bitfield } // 使用MSE加密来避免运营商对bt流量的封锁,基本上现在市面上BT客户端都默认开启了,不用MSE的话很多Peer拒绝连接 @@ -81,38 +84,50 @@ func (ps *peerState) handshake() (*peer.Handshake, error) { } func (ps *peerState) download() error { + // 异步写,防止读取阻塞 + ps.writeMsgCh = make(chan []byte, 64) + go func() { + for buf := range ps.writeMsgCh { + _, err := ps.conn.Write(buf) + if err != nil { + fmt.Println(err) + ps.conn.Close() + } + } + }() scanner := bufio.NewScanner(ps.conn) - scanner.Split(peer.SplitMessage) + scanner.Split(message.SplitMessage) for scanner.Scan() { buf := scanner.Bytes() - message := &peer.Message{} - message.Decode(buf) - switch message.ID { - case peer.Keepalive: - break - case peer.Choke: - ps.peerChoking = true - break - case peer.Unchoke: - ps.handleUnchoke() - break - case peer.Interested: - ps.peerInterested = true - break - case peer.NotInterested: - ps.peerInterested = false - break - case peer.Have: - break - case peer.Bitfield: - ps.handleBitfield(message.Payload) - break - case peer.Request: - break - case peer.Piece: - break - case peer.Cancel: - break + length := binary.BigEndian.Uint32(buf[:4]) + if length == 0 { + // keepalive + } else { + messageID := message.ID(buf[4]) + payload := buf[5:] + switch messageID { + case message.IdChoke: + break + case message.IdUnchoke: + ps.handleUnchoke() + break + case message.IdInterested: + break + case message.IdNotinterested: + break + case message.IdHave: + break + case message.IdBitfield: + ps.handleBitfield(payload) + break + case message.IdRequest: + break + case message.IdPiece: + ps.handlePiece() + break + case message.IdCancel: + break + } } } return nil @@ -124,16 +139,19 @@ func (ps *peerState) handleUnchoke() { if ps.amInterested { have := ps.getHavePieces(ps.bitfield) if len(have) > 0 { - for i := range have { - buf := make([]byte, 12) - binary.BigEndian.PutUint32(buf[:4], uint32(have[i])) - binary.BigEndian.PutUint32(buf[4:8], 0) - binary.BigEndian.PutUint32(buf[8:], 2<<13) - _, err := ps.conn.Write(peer.NewMessage(peer.Request, buf).Encode()) - if err != nil { - ps.conn.Close() - return + // 获取分片的长度 + for _, index := range have { + pieceLength := ps.torrent.MetaInfo.GetPieceSize(index) + lastOffset := uint64(0) + // 按块下载分片 + for lastOffset < pieceLength { + blockLength := math.Min(2<<13, float64(pieceLength-lastOffset)) + ps.write(message.NewRequest(uint32(index), uint32(lastOffset), uint32(blockLength)).Encode()) + lastOffset += uint64(blockLength) + fmt.Println("request piece") } + // TODO 测试代码 + break } return } @@ -142,32 +160,50 @@ func (ps *peerState) handleUnchoke() { } func (ps *peerState) handleBitfield(payload []byte) { - ps.bitfield = payload + ps.bitfield = message.NewBitfield(payload) have := ps.getHavePieces(ps.bitfield) if len(have) > 0 { // 表示对该peer感兴趣,并且不choked该peer - _, err := ps.conn.Write(peer.NewMessage(peer.Interested, []byte{}).Encode()) - if err != nil { - ps.conn.Close() - return - } + ps.write(message.NewInterested().Encode()) ps.amInterested = true - _, err = ps.conn.Write(peer.NewMessage(peer.Unchoke, []byte{}).Encode()) - if err != nil { - ps.conn.Close() - return - } + + ps.write(message.NewUnchoke().Encode()) ps.amChoking = false } else { ps.conn.Close() } } +// 处理分片下载响应 +func (ps *peerState) handlePiece() { + fmt.Println("one piece") +} + // 获取peer能提供需要下载的文件分片 -func (ps *peerState) getHavePieces(bitfield peer.MsgBitfield) []int { +func (ps *peerState) getHavePieces(bitfield *message.Bitfield) []int { states := make([]bool, len(ps.torrent.PieceState)) for i := range states { states[i] = ps.torrent.PieceState[i].complete } return bitfield.Have(states) } + +// 异步写入数据 +func (ps *peerState) write(buf []byte) { + ps.writeMsgCh <- buf +} + +// 获取要写入到的文件 +/*func (ps *peerState) getWriteFile(request *message.Request) string { + info := ps.torrent.MetaInfo.Info + // 单文件 + if len(info.Files) == 0 { + return info.Name + } else { + request.Index * info.PieceLength + be + for i := 0; i < len(info.Files); i++ { + + } + } +} +*/ diff --git a/down/bt/torrent/peer_state_test.go b/down/bt/torrent/peer_state_test.go index f23792ec..9b117486 100644 --- a/down/bt/torrent/peer_state_test.go +++ b/down/bt/torrent/peer_state_test.go @@ -2,9 +2,9 @@ package torrent import ( "fmt" - "gopeed/down/bt/metainfo" - "gopeed/down/bt/peer" - "gopeed/down/bt/tracker" + "github.com/monkeyWie/gopeed/down/bt/metainfo" + "github.com/monkeyWie/gopeed/down/bt/peer" + "github.com/monkeyWie/gopeed/down/bt/tracker" "testing" ) @@ -31,6 +31,8 @@ func Test_peerState_download(t *testing.T) { if err != nil { panic(err) } + fmt.Println("handshake end") + // 开始下载 err = ps.download() @@ -77,3 +79,7 @@ func peerTest(torrent *Torrent, peer *peer.Peer, ch chan *peerState) { } ch <- ps } + +func TestSome(t *testing.T) { + fmt.Println(11) +} diff --git a/down/bt/torrent/torrent.go b/down/bt/torrent/torrent.go index c03e2917..3ee15edf 100644 --- a/down/bt/torrent/torrent.go +++ b/down/bt/torrent/torrent.go @@ -1,7 +1,7 @@ package torrent import ( - "gopeed/down/bt/metainfo" + "github.com/monkeyWie/gopeed/down/bt/metainfo" ) type Torrent struct { diff --git a/down/bt/tracker/tracker.go b/down/bt/tracker/tracker.go index 2a8ef562..bece5ff6 100644 --- a/down/bt/tracker/tracker.go +++ b/down/bt/tracker/tracker.go @@ -4,8 +4,8 @@ import ( "encoding/binary" "errors" "fmt" - "gopeed/down/bt/metainfo" - "gopeed/down/bt/peer" + "github.com/monkeyWie/gopeed/down/bt/metainfo" + "github.com/monkeyWie/gopeed/down/bt/peer" "io" "io/ioutil" "math" @@ -208,7 +208,7 @@ func (tracker *Tracker) announce(conn *net.UDPConn, timeout int64, connectionId request.peerID = tracker.PeerID request.downloaded = 0 request.uploaded = 0 - request.left = tracker.MetaInfo.FileSize - request.downloaded + request.left = tracker.MetaInfo.TotalSize - request.downloaded request.event = 0 request.ip = 0 request.key = 0 @@ -287,7 +287,7 @@ func (tracker *Tracker) httpTracker(url *url.URL) (peers []peer.Peer, err error) query.Add("port", "6882") query.Add("uploaded", "0") query.Add("downloaded", "0") - query.Add("left", strconv.FormatInt(int64(metaInfo.FileSize), 10)) + query.Add("left", strconv.FormatInt(int64(metaInfo.TotalSize), 10)) url.RawQuery = query.Encode() request, err := http.NewRequest("GET", url.String(), nil) if err != nil { diff --git a/down/bt/tracker/tracker_test.go b/down/bt/tracker/tracker_test.go index 5b8b5b08..906de0b7 100644 --- a/down/bt/tracker/tracker_test.go +++ b/down/bt/tracker/tracker_test.go @@ -2,7 +2,7 @@ package tracker import ( "fmt" - "gopeed/down/bt/metainfo" + "github.com/monkeyWie/gopeed/down/bt/metainfo" "testing" ) diff --git a/go.mod b/go.mod index 3e0cbf02..f1f60be4 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ -module gopeed +module github.com/monkeyWie/gopeed -go 1.12 +go 1.13 require ( github.com/cenkalti/mse v0.0.0-20140930130441-6ef65f170972 diff --git a/main.go b/main.go index 3c93d228..69342c01 100644 --- a/main.go +++ b/main.go @@ -2,7 +2,11 @@ package main import ( "fmt" - "gopeed/down/http" + "github.com/monkeyWie/gopeed/down/bt/metainfo" + "github.com/monkeyWie/gopeed/down/bt/peer" + "github.com/monkeyWie/gopeed/down/bt/torrent" + "github.com/monkeyWie/gopeed/down/bt/tracker" + "github.com/monkeyWie/gopeed/down/http" ) func main() { @@ -20,5 +24,26 @@ func main() { } got, _ := http.Resolve(request) fmt.Println(got) + getUsablePeer() // webview.Open("Minimal webview example", "https://www.baidu.com", 800, 600, true) } + +func buildTorrent() *torrent.Torrent { + metaInfo, err := metainfo.ParseFromFile("../testdata/Game.of.Thrones.S08E05.720p.WEB.H264-MEMENTO.torrent") + if err != nil { + panic(err) + } + metaInfo.Announce = "udp://tracker.opentrackr.org:1337/announce" + metaInfo.AnnounceList = [][]string{} + return torrent.NewTorrent(peer.GenPeerID(), metaInfo) +} + +// 获取一个能连接上的peer +func getUsablePeer() { + torrent := buildTorrent() + tracker := &tracker.Tracker{ + PeerID: torrent.PeerID, + MetaInfo: torrent.MetaInfo, + } + tracker.Tracker() +}