Protocol Overview
The p2p-file-share system uses a JSON-based messaging protocol over TCP connections. All messages are UTF-8 encoded JSON objects delimited by newline characters (\n).
Messages are sent as JSON strings followed by a newline: JSON.stringify(message) + '\n'
Message Flow
Connection Establishment
A peer establishes a TCP connection to another peer’s listening port. const socket = net . connect ( port , host , () => {
socket . setEncoding ( 'utf8' );
this . _setupSocketHandlers ( socket );
this . _sendHandshake ( socket );
});
Handshake Exchange
Both peers exchange handshake messages containing file metadata and peer IDs. {
"type" : "handshake" ,
"id" : "ab12cd34ef567890" ,
"fileName" : "video.mkv" ,
"fileSize" : 157286400 ,
"fileHash" : "a9f8e7d6c5b4a3921807f6e5d4c3b2a1" ,
"pieceSize" : 65536 ,
"port" : 6881
}
Bitfield Exchange
After handshake validation, peers send bitfield messages listing available pieces. {
"type" : "bitfield" ,
"pieces" : [ 0 , 1 , 2 , 3 , 5 , 8 , 13 ]
}
Piece Requests
Leechers request specific pieces from peers that have them. {
"type" : "request" ,
"index" : 42
}
Piece Transfer
Seeders respond with piece data encoded in base64. {
"type" : "piece" ,
"index" : 42 ,
"data" : "SGVsbG8gV29ybGQh..."
}
Have Notifications
When a peer acquires a new piece, it notifies all connected peers. {
"type" : "have" ,
"index" : 42
}
Peer Exchange
Peers share information about other peers to enable swarm growth. {
"type" : "peers" ,
"peers" : [
{ "id" : "1a2b3c4d5e6f7890" , "host" : "192.168.1.10" , "port" : 6882 },
{ "id" : "9f8e7d6c5b4a3210" , "host" : "192.168.1.15" , "port" : 6883 }
]
}
Message Types
The protocol defines six message types, each serving a specific purpose:
handshake - Peer Identification and Metadata
bitfield - Available Pieces Advertisement
Purpose The bitfield message informs peers about which pieces the sender currently has. This is sent immediately after the handshake exchange. Structure {
"type" : "bitfield" ,
"pieces" : [ 0 , 1 , 2 , 3 , 5 , 8 , 13 , 21 , 34 ]
}
Fields
type : Always "bitfield"
pieces : Array of piece indices (0-based) that the sender possesses
Implementation _sendBitfield ( socket ) {
const piecesArray = Array . from ( this . havePieces );
const bitfieldMsg = {
type: 'bitfield' ,
pieces: piecesArray
};
socket . write ( JSON . stringify ( bitfieldMsg ) + ' \n ' );
}
Handling _handleBitfield ( socket , message ) {
const pieces = message . pieces ;
if ( ! socket . peerId || ! this . knownPeers . has ( socket . peerId )) return ;
const peerInfo = this . knownPeers . get ( socket . peerId );
peerInfo . availablePieces = new Set ( pieces );
console . log ( `Recibido mapa de piezas de peer ${ socket . peerId } : ${ pieces . length } piezas disponibles.` );
// Try to schedule piece requests now that we know peer's availability
this . _scheduleRequests ();
}
A seeder sends a complete bitfield with all piece indices. A new leecher sends an empty array or waits to acquire pieces before sending.
Purpose A leecher sends a request message to ask a peer for a specific piece it needs. Structure {
"type" : "request" ,
"index" : 42
}
Fields
type : Always "request"
index : Zero-based index of the requested piece
Sending Requests _sendRequest ( socket , index ) {
const requestMsg = {
type: 'request' ,
index: index
};
if ( socket ) {
this . pendingPieces . add ( index );
if ( socket . peerId && this . knownPeers . has ( socket . peerId )) {
this . knownPeers . get ( socket . peerId ). busy = true ;
}
socket . write ( JSON . stringify ( requestMsg ) + ' \n ' );
console . log ( `Solicitando pieza ${ index } al peer ${ socket . peerId } .` );
}
}
Handling Requests _handleRequest ( socket , message ) {
const index = message . index ;
if ( ! this . havePieces . has ( index )) {
console . warn ( `Peer ${ socket . peerId } solicitó pieza ${ index } que no tenemos.` );
return ;
}
// Read piece from file and send it
this . fileManager . readPiece ( index ). then ( buffer => {
const pieceMsg = {
type: 'piece' ,
index: index ,
data: buffer . toString ( 'base64' )
};
socket . write ( JSON . stringify ( pieceMsg ) + ' \n ' );
}). catch ( err => {
console . error ( `Error al leer pieza ${ index } para enviar:` , err );
});
}
Peers track pending requests to avoid requesting the same piece from multiple peers simultaneously. The busy flag prevents overwhelming a peer with parallel requests.
piece - Piece Data Transfer
Purpose A seeder responds to a request with the actual piece data encoded in base64. Structure {
"type" : "piece" ,
"index" : 42 ,
"data" : "SGVsbG8gV29ybGQhIFRoaXMgaXMgYSBiYXNlNjQgZW5jb2RlZCBwaWVjZS4uLg=="
}
Fields
type : Always "piece"
index : Zero-based index of this piece
data : Base64-encoded piece data (typically 64 KiB when encoded)
Handling Received Pieces async _handlePiece ( socket , message ) {
const index = message . index ;
const dataBase64 = message . data ;
// Decode base64 to buffer
const dataBuffer = Buffer . from ( dataBase64 , 'base64' );
// Write to file
try {
await this . fileManager . writePiece ( index , dataBuffer );
} catch ( err ) {
console . error ( `Error escribiendo la pieza ${ index } :` , err );
return ;
}
// Update tracking structures
this . havePieces . add ( index );
this . missingPieces . delete ( index );
this . pendingPieces . delete ( index );
// Mark peer as free for next request
if ( socket . peerId && this . knownPeers . has ( socket . peerId )) {
this . knownPeers . get ( socket . peerId ). busy = false ;
}
// Notify other peers we now have this piece
const haveMsg = { type: 'have' , index: index };
for ( let [ peerId , peer ] of this . knownPeers . entries ()) {
if ( peer . socket && peerId !== socket . peerId ) {
peer . socket . write ( JSON . stringify ( haveMsg ) + ' \n ' );
}
}
console . log ( `Pieza ${ index } recibida ( ${ dataBuffer . length } bytes). Piezas restantes: ${ this . missingPieces . size } .` );
// Check if download is complete
if ( this . missingPieces . size === 0 ) {
await this . _handleDownloadComplete ();
} else {
this . _scheduleRequests ();
}
}
Base64 encoding increases data size by approximately 33%. For a 64 KiB piece (65,536 bytes), the encoded string is about 87 KB.
have - New Piece Notification
Purpose When a leecher successfully receives and writes a piece, it broadcasts a ‘have’ message to all connected peers to update their view of piece availability. Structure {
"type" : "have" ,
"index" : 42
}
Fields
type : Always "have"
index : Zero-based index of the newly acquired piece
Sending Have Messages // After successfully writing a piece, notify all peers
const haveMsg = {
type: 'have' ,
index: index
};
for ( let [ peerId , peer ] of this . knownPeers . entries ()) {
if ( peer . socket && peerId !== socket . peerId ) {
peer . socket . write ( JSON . stringify ( haveMsg ) + ' \n ' );
}
}
Handling Have Messages _handleHave ( socket , message ) {
const index = message . index ;
if ( ! socket . peerId || ! this . knownPeers . has ( socket . peerId )) return ;
const peerInfo = this . knownPeers . get ( socket . peerId );
peerInfo . availablePieces . add ( index );
console . log ( `Peer ${ socket . peerId } ha obtenido la pieza ${ index } .` );
// If we need this piece and peer is available, request it
if ( this . missingPieces . has ( index ) && ! this . pendingPieces . has ( index ) && ! peerInfo . busy ) {
this . _scheduleRequests ();
}
}
Have messages enable real-time updates of piece distribution, allowing efficient scheduling of requests as the swarm evolves.
Purpose The ‘peers’ message enables peer discovery without central trackers. Peers share information about other peers they know, allowing the swarm to grow organically. Structure {
"type" : "peers" ,
"peers" : [
{
"id" : "1a2b3c4d5e6f7890" ,
"host" : "192.168.1.10" ,
"port" : 6882
},
{
"id" : "9f8e7d6c5b4a3210" ,
"host" : "192.168.1.15" ,
"port" : 6883
}
]
}
Fields
type : Always "peers"
peers : Array of peer objects, each containing:
id : 16-character hex peer identifier
host : IP address or hostname
port : TCP port number
Sending Peer Lists After handshake, share known peers with the new connection: const otherPeers = [];
for ( let [ pid , pinfo ] of this . knownPeers . entries ()) {
if ( pid !== remoteId && pinfo . socket ) {
otherPeers . push ({
id: pid ,
host: pinfo . host || pinfo . socket . remoteAddress ,
port: pinfo . port
});
}
}
if ( otherPeers . length > 0 ) {
const peersMsg = { type: 'peers' , peers: otherPeers };
socket . write ( JSON . stringify ( peersMsg ) + ' \n ' );
}
Handling Peer Discovery _handlePeers ( socket , message ) {
const peersList = message . peers ;
for ( let peer of peersList ) {
const { id : peerId , host , port } = peer ;
if ( peerId === this . id ) continue ; // Ignore our own ID
if ( ! this . knownPeers . has ( peerId )) {
this . knownPeers . set ( peerId , {
id: peerId , host , port ,
socket: null ,
availablePieces: new Set (),
busy: false
});
// Apply connection rule to prevent duplicates
if ( this . _shouldInitiateConnection ( peerId )) {
console . log ( `Descubierto peer ${ peerId } en ${ host } : ${ port } , iniciando conexión...` );
this . connectToPeer ( host , port );
} else {
console . log ( `Descubierto peer ${ peerId } . Esperando a que el peer inicie conexión.` );
}
}
}
}
See Peer Exchange (PEX) for more details on the connection rules.
TCP Connection Handling
Message Framing
Messages are delimited by newline characters. The receiver maintains a buffer and extracts complete messages:
socket . on ( 'data' , async ( data ) => {
// Concatenate incoming data to buffer
socket . buffer += data ;
let newlineIndex ;
// Process all complete messages in buffer
while (( newlineIndex = socket . buffer . indexOf ( ' \n ' )) !== - 1 ) {
const rawMessage = socket . buffer . slice ( 0 , newlineIndex );
socket . buffer = socket . buffer . slice ( newlineIndex + 1 );
if ( ! rawMessage ) continue ; // Skip empty lines
let message ;
try {
message = JSON . parse ( rawMessage );
} catch ( err ) {
console . error ( 'Mensaje JSON malformado recibido, descartando:' , rawMessage );
continue ;
}
await this . _handleMessage ( socket , message );
}
});
Connection Lifecycle
Setup
Configure socket encoding and event handlers: socket . setEncoding ( 'utf8' );
socket . peerId = null ;
socket . isOutgoing = true ; // or false for incoming
socket . buffer = '' ;
this . _setupSocketHandlers ( socket );
Active Communication
Exchange messages according to protocol flow.
Close Handling
Clean up state when connection closes: socket . on ( 'close' , () => {
if ( socket . peerId && this . knownPeers . has ( socket . peerId )) {
const peerInfo = this . knownPeers . get ( socket . peerId );
peerInfo . socket = null ;
peerInfo . busy = false ;
}
// Clear pending requests and reschedule
this . pendingPieces . clear ();
this . _scheduleRequests ();
});
Error Handling
socket . on ( 'error' , ( err ) => {
console . error ( `Error en la conexión con peer ${ socket . peerId || socket . remoteAddress } :` , err . message );
});
Protocol Features
JSON-Based Human-readable, easy to debug, and language-agnostic.
Newline Delimited Simple framing with buffering for partial message handling.
Stateful Connections Persistent TCP connections with bidirectional message flow.
Base64 Encoding Binary piece data safely transmitted as JSON strings.