Sitemap

How I built a BitTorrent client called GoTorrent for my college final-year project.

--

Press enter or click to view image in full size

This blog is divided into 7 parts:

  1. Why This Project
  2. Understanding BitTorrent
  3. GoTorrent Architecture
  4. System Workflow: How GoTorrent Operates
  5. The P2P Engine (Under the Hood)
  6. Key Features and Big Challenges
  7. Conclusion and Learnings

1. Why This Project

Before diving into the technical details, here is why I chose this project.

When I was around 12–13 years old, I used to download games and movies using applications like uTorrent. Those early experiences made me curious about how files could be shared among many users without relying on a single central server.

That curiosity eventually led me to build GoTorrent as my final-year project, both to understand the BitTorrent protocol deeply and to challenge myself with a real-world networking system.

2. Understanding BitTorrent

Before explaining GoTorrent, it helps to understand how BitTorrent works.

BitTorrent is a peer-to-peer (P2P) protocol where each user (peer) can both download and upload. Instead of downloading an entire file from one server, the file is split into pieces and shared across peers.

2.1 Torrent File and Tracker

A .torrent file contains metadata such as:

  • File name and size
  • Piece length and SHA-1 hashes
  • Tracker URLs

When a client loads this file, it contacts a tracker to get a list of peers sharing that torrent.

2.2 Peer-to-Peer Communication

After peers are discovered, they connect directly over TCP and exchange protocol messages such as:

  • handshake for connection setup
  • interested / not interested
  • choke / unchoke
  • request, piece, have

2.3 Piece Verification

Each piece is verified using SHA-1 hashes from torrent metadata. If a hash check fails, that piece is downloaded again.

2.4 Seeding and Swarming

Once a peer completes the file, it becomes a seeder and uploads pieces to other peers. This cooperative model is what makes BitTorrent scalable.

In short, BitTorrent is a distributed exchange of pieces among peers, not a one-way client-server download.

3. GoTorrent Architecture

GoTorrent follows a daemon-based architecture built in Go.

3.1 Daemon (gotorrentd): The Engine

The daemon handles core responsibilities:

  • pkg/daemon: orchestration, lifecycle control, and coordination
  • pkg/torrent: .torrent parsing and tracker communication (HTTP/UDP)
  • DHT integration in daemon for additional peer discovery
  • pkg/p2p, pkg/client, pkg/message: peer protocol engine and piece transfer
  • pkg/daemon/store.go + SQLite: persistence of state, progress, and bitfield

Note: Magnet link support is planned, but current implementation primarily handles .torrent input.

3.2 Client Layer

Users interact with the daemon through:

  • CLI client via RPC
  • REST endpoints for programmatic control

3.3 Why Go

Go was a strong fit because of:

  • Goroutines and channels for concurrency
  • Reliable networking primitives for TCP/UDP/HTTP
  • Good runtime performance for high-throughput transfers

This architecture keeps core download logic isolated and long-running, while control surfaces remain lightweight.

4. System Workflow: How GoTorrent Operates

4.1 Parsing The .torrent file via bencode-go parser

Press enter or click to view image in full size
Bencode parser parses the torrent file into json format with all the information which exists in the .torrent file
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"`
AnnounceList [][]string `bencode:"announce-list"`
Info bencodeInfo `bencode:"info"`
}
func (bto *bencodeTorrent) toTorrentFile() (TorrentFile, error) {}

This Function parses the data and splits the hashes and fill up the trackers array with the trackers.

4.2 Contacting the Tracker and Peer Discovery

After parsing, we get a TorrentFile model:

type TorrentFile struct {
Announce string
InfoHash [20]byte
PieceHashes [][20]byte
AnnounceList []string
PieceLength int
Length int
Name string
}

The tracker request starts in pkg/torrent/tracker.go. GoTorrent builds a standard tracker URL with query params like:

  • info_hash
  • peer_id
  • port
  • uploaded, downloaded, left
  • compact=1

Core builder:

func buildTrackerURL(tf *TorrentFile, peerID [20]byte, port uint16) (string, error)

The main entry point is:

func RequestPeers(tf *TorrentFile, peerID [20]byte) ([]peers.Peer, error)

This function checks tracker scheme and routes to:

  • HTTP/HTTPS tracker flow: requestPeersHTTP(...)
  • UDP tracker flow: RequestPeersUDPWithFallback(...)

HTTP tracker response model

type bencodeTrackerResp struct {
Interval int `bencode:"interval"`
Peers string `bencode:"peers"`
FailureReason string `bencode:"failure reason,omitempty"`
WarningMessage string `bencode:"warning message,omitempty"`
Complete int `bencode:"complete,omitempty"`
Incomplete int `bencode:"incomplete,omitempty"`
}

The response is decoded and compact peers are converted using:

func UnmarshalPeers(peersBin []byte) ([]Peer, error)

where each peer is parsed from 6 bytes (4 for IP + 2 for port).

UDP tracker flow and resilience

pkg/torrent/udp.go implements the UDP tracker protocol in two phases:

  1. Connect (actionConnect)
  2. Announce (actionAnnounce)

Key functions:

func requestPeersUDP(u *url.URL, tf *TorrentFile, peerID [20]byte) ([]peers.Peer, error)
func buildConnectRequest(transactionID uint32) []byte
func parseConnectResponse(data []byte, expectedTransactionID uint32) (uint64, error)
func buildAnnounceRequest(connectionID uint64, transactionID uint32, tf *TorrentFile, peerID [20]byte, port uint16) []byte
func parseAnnounceResponse(data []byte, expectedTransactionID uint32) ([]peers.Peer, error)

To improve reliability, sendAndReceive(...) uses retry + exponential backoff, and RequestPeersUDPWithFallback(...) tries known fallback UDP trackers if the primary one fails.

4.3 Hybrid Peer Discovery Inside the Daemon

Tracker contact alone is not enough for all torrents, so GoTorrent adds DHT discovery in the daemon (pkg/daemon/server.go).

Get Yash’s stories in your inbox

Join Medium for free to get updates from this writer.

In Server.AddTorrent(...), peer discovery happens in this order:

  1. Iterate tracker URLs from AnnounceList
  2. Query trackers in order via torrent.RequestPeers(...) until one returns peers
  3. If DHT is enabled, query DHT using s.dht.Announce(tf.InfoHash, 0, true)
  4. Merge and deduplicate peers using IP:Port keys

This gives a hybrid discovery strategy:

  • Trackers provide fast bootstrap peers
  • DHT improves availability when trackers are down or limited

4.4 End-to-End Workflow (Execution Pipeline)

Once peers are found, the daemon creates the P2P job:

torrentJob := &p2p.Torrent{
Peers: initialPeers,
PeerID: ourPeerID,
InfoHash: tf.InfoHash,
PieceHashes: tf.PieceHashes,
PieceLength: tf.PieceLength,
Length: tf.Length,
Name: tf.Name,
Announce: tf.Announce,
DownloadPath: s.downloadPath,
}

Then the workflow is:

  1. Persist torrent metadata in SQLite (store.AddTorrent)
  2. Add job to in-memory map (s.torrents)
  3. Start async download worker (go torrentJob.DownloadTheFile(s.store))
  4. Periodically update progress (store.UpdateTorrentProgress)
  5. Serve status through RPC + REST APIs

So the full data path is:

.torrent -> metadata parse -> peer discovery (Tracker + DHT) -> worker swarm -> piece validation -> disk write -> DB update -> status APIs

5. The P2P Engine (Under the Hood)

This is the core download logic in pkg/p2p/p2p.go and related protocol packages.

5.1 Torrent Runtime Model

type Torrent struct {
Peers []peers.Peer
PeerID [20]byte
InfoHash [20]byte
PieceHashes [][20]byte
PieceLength int
Length int
Name string
Announce string
BytesDownloaded int64
Status string
ActivePeers int
State uint8
OurBitfield bitfield.Bitfield
DownloadPath string
}

This struct tracks both static metadata (hashes, piece size, total length) and mutable runtime state (status, speed, active peers, bitfield).

5.2 Peer Connection, Handshake, and Bitfield

Each worker connects through client.New(...):

  • TCP connect to peer IP:Port
  • Perform BitTorrent handshake
  • Receive initial bitfield

Handshake package (pkg/handshake/handshake.go) defines:

type Handshake struct {
Pstr string
InfoHash [20]byte
PeerID [20]byte
}

completeHandshake(...) verifies returned InfoHash matches the target torrent, preventing cross-torrent contamination.

5.3 Piece Scheduling and Parallel Download

The engine uses a producer-consumer model:

  • workQueue contains pending pieces (pieceWork)
  • Each peer goroutine runs startDownloadWorker(...)
  • Successful pieces are sent through results

Per-piece download uses pipelining with backlog:

const MaxBlockSize = 16384
const MaxBacklog = 5

attemptDownloadPiece(...) keeps requesting blocks while unchoked and reads protocol messages (Unchoke, Have, Piece, etc.) via readMessage().

5.4 Integrity and Safe Write Path

Before writing piece data, GoTorrent validates SHA-1:

func checkIntegrity(pw *pieceWork, buf []byte) error

If hash check fails, piece is re-queued. If it passes, piece is written at exact file offset with WriteAt(...), and local bitfield is updated.

5.5 Pause, Resume, Stop, and Speed Tracking

Torrent control uses a state channel:

func (t *Torrent) Pause()
func (t *Torrent) Resume()
func (t *Torrent) Stop()

Download speed is calculated every 2 seconds from byte deltas. Progress is persisted periodically so restarts can continue from saved bitfield state.

6. Key Features and Big Challenges

6.1 Key Features Implemented (Go Backend)

  1. Hybrid peer discovery
  2. HTTP and UDP tracker support
  3. Multi-peer concurrent piece download using goroutines
  4. Piece-level SHA-1 integrity validation
  5. Persistent progress with SQLite (including bitfield)
  6. Pause/resume/remove lifecycle management
  7. Daemon architecture with RPC and REST interfaces
  8. Real-time status broadcasting pipeline

6.2 Big Technical Challenges

  1. Unreliable tracker ecosystem Many trackers fail, timeout, or return empty peer lists. I solved this using multi-tracker attempts, UDP retries, and fallback tracker logic.
  2. Peer instability and choking behavior Not all peers cooperate. Some choke, disconnect, or have partial data. I handled this using per-worker timeouts, piece re-queueing, and concurrent workers.
  3. Correct piece boundary math Last piece is often smaller than normal piece length. I added calculateBoundsForPiece(...) and calculatePieceSize(...) to avoid overflow and corruption.
  4. Data integrity under concurrency Concurrent downloads can cause races if state updates are unsafe. I used mutexes for shared state (BytesDownloaded, bitfield, status) and only mark piece complete after successful hash + disk write.
  5. Crash/restart resilience Download clients should not lose progress after restart. I persisted status + bitfield in SQLite and load managed torrents from DB during daemon startup.
  6. Lifecycle transitions are tricky Pausing, resuming, stopping, and restarting workers can create invalid states. I made explicit state transitions (StatePaused, StateDownloading, StateSeeding, etc.) and guarded resume behavior based on current status.

7. Conclusion and Learnings

Building GoTorrent as my final year project taught me far more than just “how torrents work”. It gave me hands-on experience with distributed systems, binary protocols, concurrency, fault tolerance, and long-running backend service design in Go.

The biggest learning was this: A BitTorrent client is not just about downloading files. It is a coordination problem across unreliable networks, inconsistent peers, and asynchronous state updates.

From parsing bencoded metadata to tracker communication, DHT lookup, handshake negotiation, piece scheduling, integrity verification, and persistence, every stage has failure modes that must be handled explicitly.

Go’s goroutines, channels, and standard library made it practical to build this as a real production-style system, not just a classroom prototype.

You can explore the complete source code and implementation details here: https://github.com/Yashh56/GoTorrent

If you found the project interesting or helpful, feel free to leave a star on the repository ⭐

--

--

A Developer Who Loves to Read and Write.

See more from me