WEBRTC PEER-TO-PEER FILE-SHARING REAL-TIME WEB-DEVELOPMENT JAVASCRIPT BROWSER-API P2P DATA-CHANNEL FRONTEND NETWORKING SECURITY

Membangun Aplikasi Peer-to-Peer File Sharing dengan WebRTC Data Channel

⏱️ 17 menit baca
👨‍💻

Membangun Aplikasi Peer-to-Peer File Sharing dengan WebRTC Data Channel

1. Pendahuluan

Pernahkah Anda ingin berbagi file besar dengan teman atau rekan kerja tanpa harus mengunggahnya ke layanan cloud, menunggu proses upload yang lama, atau khawatir tentang privasi data di server pihak ketiga? Atau mungkin Anda membayangkan aplikasi kolaborasi real-time di mana data dapat langsung bertukar antar pengguna?

Di era digital ini, kebutuhan akan transfer data yang cepat, efisien, dan aman semakin meningkat. Tradisionalnya, ini seringkali melibatkan server sebagai perantara: Anda upload, lalu orang lain download. Pendekatan ini bekerja, tetapi memiliki beberapa kelemahan:

Untungnya, ada solusi yang elegan untuk mengatasi tantangan ini: WebRTC Data Channel.

WebRTC (Web Real-Time Communication) adalah teknologi yang memungkinkan komunikasi real-time, termasuk video, audio, dan data generik, langsung antar browser (peer-to-peer) tanpa perlu server perantara untuk transfer data itu sendiri. Jika Anda sudah familiar dengan bagaimana WebRTC memungkinkan video call langsung, Data Channel adalah sisi lain dari koin yang memungkinkan transfer data arbitrer — dan inilah yang akan kita manfaatkan untuk membangun aplikasi berbagi file P2P.

Dalam artikel ini, kita akan menyelami WebRTC Data Channel dan bagaimana kita bisa memanfaatkannya untuk membangun aplikasi berbagi file peer-to-peer yang efisien dan aman. Siap untuk mengurangi ketergantungan pada server dan meningkatkan privasi data Anda? Mari kita mulai!

2. Mengapa WebRTC Data Channel untuk Berbagi File?

Sebelum kita masuk ke teknis, mari kita pahami mengapa Data Channel menjadi pilihan menarik untuk skenario berbagi file:

✅ Keunggulan WebRTC Data Channel

  1. Transfer Langsung (Peer-to-Peer): Setelah koneksi awal terjalin, data mengalir langsung antar browser. Ini menghilangkan bottleneck server dan dapat menghasilkan kecepatan transfer yang jauh lebih tinggi, terutama untuk file besar.
  2. Keamanan Bawaan: Semua komunikasi WebRTC, termasuk Data Channel, dienkripsi secara default menggunakan DTLS (Datagram Transport Layer Security). Ini memastikan data Anda aman selama perjalanan antar peer.
  3. Privasi Lebih Baik: Karena tidak ada server perantara yang menyimpan file Anda, risiko data disadap atau disalahgunakan oleh pihak ketiga berkurang drastis.
  4. Fleksibilitas Data: Berbeda dengan media stream (audio/video), Data Channel dirancang untuk mengirim data arbitrer apapun yang Anda inginkan: teks, JSON, ArrayBuffer (untuk file biner), dan lainnya.
  5. Biaya Lebih Rendah: Mengurangi atau bahkan menghilangkan kebutuhan akan server yang menampung file Anda dapat menghemat biaya infrastruktur yang signifikan.

📌 Perbedaan dengan Media Stream

WebRTC dikenal luas untuk video dan audio call. Data Channel adalah bagian dari WebRTC yang memungkinkan pengiriman data non-media. Bayangkan jika media stream adalah “jalur telepon” untuk suara dan gambar, maka Data Channel adalah “jalur data” yang memungkinkan Anda mengirim paket surat atau email secara langsung.

3. Sekilas tentang WebRTC dan Mekanisme Signaling

Meskipun transfer data itu sendiri terjadi secara peer-to-peer, WebRTC tetap memerlukan server perantara di awal untuk proses yang disebut signaling.

💡 Analogi Sederhana: Bayangkan Anda ingin menelepon teman Anda (peer-to-peer). Anda tidak langsung “terhubung” begitu saja. Anda perlu nomor telepon teman Anda, dan teman Anda perlu nomor telepon Anda. Pertukaran nomor telepon ini adalah proses signaling. Setelah Anda berdua memiliki nomor telepon masing-masing (yaitu, Anda telah bertukar informasi koneksi), Anda bisa langsung menelepon tanpa perantara operator.

Dalam konteks WebRTC:

Proses signaling melibatkan pertukaran SDP offer/answer dan ICE candidates antar kedua peer melalui server perantara (seringkali menggunakan WebSocket). Setelah informasi ini berhasil dipertukarkan dan koneksi ICE terbentuk, barulah Data Channel dapat dibuka dan data dapat mengalir secara langsung.

⚠️ Penting: Artikel ini akan fokus pada implementasi Data Channel itu sendiri. Untuk detail lebih lanjut tentang signaling dan NAT traversal, Anda bisa membaca artikel kami yang lain:

4. Mempersiapkan Proyek Sederhana

Mari kita siapkan struktur dasar HTML dan JavaScript untuk aplikasi berbagi file kita. Kita akan membuat dua “jendela” browser yang mensimulasikan dua peer.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WebRTC P2P File Sharing</title>
    <style>
        body { font-family: sans-serif; display: flex; justify-content: space-around; padding: 20px; }
        .peer-box {
            border: 1px solid #ccc;
            padding: 20px;
            margin: 10px;
            width: 45%;
            box-shadow: 2px 2px 8px rgba(0,0,0,0.1);
        }
        h2 { text-align: center; }
        input[type="file"] { margin-bottom: 10px; }
        button { padding: 8px 15px; background-color: #007bff; color: white; border: none; cursor: pointer; }
        button:disabled { background-color: #cccccc; cursor: not-allowed; }
        .log {
            border: 1px solid #eee;
            padding: 10px;
            height: 150px;
            overflow-y: scroll;
            margin-top: 15px;
            background-color: #f9f9f9;
        }
        .progress-bar {
            width: 0%;
            height: 20px;
            background-color: #28a745;
            text-align: center;
            line-height: 20px;
            color: white;
            font-size: 0.8em;
            margin-top: 5px;
        }
        .progress-container {
            width: 100%;
            background-color: #e0e0e0;
            margin-top: 10px;
        }
    </style>
</head>
<body>
    <div class="peer-box">
        <h2>Peer A (Pengirim)</h2>
        <input type="file" id="fileInputA">
        <button id="sendBtnA" disabled>Kirim File</button>
        <div class="log" id="logA"></div>
        <div class="progress-container"><div class="progress-bar" id="progressBarA">0%</div></div>
        <p>
            <textarea id="sdpOffer" placeholder="Paste SDP Offer dari Peer B di sini" rows="5" style="width: 100%; margin-top: 10px;"></textarea>
            <button id="createOfferBtn">Buat Offer & Copy</button>
            <button id="setRemoteAnswerBtn">Set Remote Answer</button>
        </p>
    </div>

    <div class="peer-box">
        <h2>Peer B (Penerima)</h2>
        <input type="file" id="fileInputB" disabled>
        <button id="sendBtnB" disabled>Kirim File</button>
        <div class="log" id="logB"></div>
        <div class="progress-container"><div class="progress-bar" id="progressBarB">0%</div></div>
        <p>
            <textarea id="sdpAnswer" placeholder="Paste SDP Offer dari Peer A di sini" rows="5" style="width: 100%; margin-top: 10px;"></textarea>
            <button id="createAnswerBtn">Buat Answer & Copy</button>
            <button id="setRemoteOfferBtn">Set Remote Offer</button>
        </p>
    </div>

    <script src="script.js"></script>
</body>
</html>

Dan file script.js kita akan kita isi secara bertahap.

5. Membuat Peer Connection dan Data Channel

Pertama, kita inisialisasi RTCPeerConnection untuk kedua peer dan menyiapkan Data Channel.

// script.js
const config = {
    iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
};

let peerA, peerB;
let dataChannelA, dataChannelB;

const fileInputA = document.getElementById('fileInputA');
const sendBtnA = document.getElementById('sendBtnA');
const logA = document.getElementById('logA');
const progressBarA = document.getElementById('progressBarA');
const sdpOffer = document.getElementById('sdpOffer');
const createOfferBtn = document.getElementById('createOfferBtn');
const setRemoteAnswerBtn = document.getElementById('setRemoteAnswerBtn');

const fileInputB = document.getElementById('fileInputB');
const sendBtnB = document.getElementById('sendBtnB');
const logB = document.getElementById('logB');
const progressBarB = document.getElementById('progressBarB');
const sdpAnswer = document.getElementById('sdpAnswer');
const createAnswerBtn = document.getElementById('createAnswerBtn');
const setRemoteOfferBtn = document.getElementById('setRemoteOfferBtn');

function log(peerId, message) {
    const logElement = peerId === 'A' ? logA : logB;
    logElement.innerHTML += `<p>${message}</p>`;
    logElement.scrollTop = logElement.scrollHeight;
}

function updateProgressBar(peerId, percentage) {
    const progressBar = peerId === 'A' ? progressBarA : progressBarB;
    progressBar.style.width = `${percentage}%`;
    progressBar.textContent = `${Math.round(percentage)}%`;
}

// --- Peer A Setup ---
peerA = new RTCPeerConnection(config);
log('A', 'RTCPeerConnection A dibuat.');

// Buat Data Channel di Peer A (Pengirim)
dataChannelA = peerA.createDataChannel('fileTransfer');
log('A', 'Data Channel A dibuat.');

dataChannelA.onopen = () => {
    log('A', 'Data Channel A: Terhubung!');
    sendBtnA.disabled = false;
    fileInputA.disabled = false;
};
dataChannelA.onclose = () => {
    log('A', 'Data Channel A: Terputus.');
    sendBtnA.disabled = true;
    fileInputA.disabled = true;
};
dataChannelA.onerror = (e) => log('A', `Data Channel A Error: ${e.error.message}`);
// Kita akan menambahkan onmessage nanti

// --- Peer B Setup ---
peerB = new RTCPeerConnection(config);
log('B', 'RTCPeerConnection B dibuat.');

// Tangani saat Data Channel dibuat oleh Peer A
peerB.ondatachannel = (event) => {
    dataChannelB = event.channel;
    log('B', 'Data Channel B diterima.');

    dataChannelB.onopen = () => {
        log('B', 'Data Channel B: Terhubung!');
        sendBtnB.disabled = false; // Jika Peer B juga bisa mengirim
        fileInputB.disabled = false; // Jika Peer B juga bisa mengirim
    };
    dataChannelB.onclose = () => {
        log('B', 'Data Channel B: Terputus.');
        sendBtnB.disabled = true;
        fileInputB.disabled = true;
    };
    dataChannelB.onerror = (e) => log('B', `Data Channel B Error: ${e.error.message}`);
    // Kita akan menambahkan onmessage nanti
};

// --- ICE Candidate Handling (untuk signaling) ---
peerA.onicecandidate = (event) => {
    if (event.candidate) {
        log('A', 'ICE Candidate A ditemukan.');
        // Dalam aplikasi nyata, kirim candidate ini ke Peer B melalui server signaling
        // Untuk demo, kita abaikan dan hanya fokus pada SDP
    }
};

peerB.onicecandidate = (event) => {
    if (event.candidate) {
        log('B', 'ICE Candidate B ditemukan.');
        // Dalam aplikasi nyata, kirim candidate ini ke Peer A melalui server signaling
        // Untuk demo, kita abaikan dan hanya fokus pada SDP
    }
};

// --- Signaling Sederhana (Manual untuk Demo) ---
createOfferBtn.onclick = async () => {
    const offer = await peerA.createOffer();
    await peerA.setLocalDescription(offer);
    sdpOffer.value = JSON.stringify(peerA.localDescription);
    log('A', 'Offer dibuat. Salin ke Peer B.');
};

setRemoteOfferBtn.onclick = async () => {
    const offer = JSON.parse(sdpAnswer.value); // sdpAnswer di Peer B akan menerima offer dari Peer A
    await peerB.setRemoteDescription(new RTCSessionDescription(offer));
    log('B', 'Remote Offer disetel.');

    const answer = await peerB.createAnswer();
    await peerB.setLocalDescription(answer);
    sdpAnswer.value = JSON.stringify(peerB.localDescription);
    log('B', 'Answer dibuat. Salin ke Peer A.');
};

setRemoteAnswerBtn.onclick = async () => {
    const answer = JSON.parse(sdpOffer.value); // sdpOffer di Peer A akan menerima answer dari Peer B
    await peerA.setRemoteDescription(new RTCSessionDescription(answer));
    log('A', 'Remote Answer disetel. Koneksi P2P seharusnya terhubung!');
};

Dalam kode di atas:

Untuk menguji bagian ini:

  1. Buka index.html di dua tab browser yang berbeda. Satu akan menjadi Peer A, satu lagi Peer B.
  2. Di tab Peer A, klik “Buat Offer & Copy”. Salin teks di textarea Peer A.
  3. Di tab Peer B, paste teks yang disalin ke textarea Peer B, lalu klik “Set Remote Offer”.
  4. Di tab Peer B, klik “Buat Answer & Copy”. Salin teks di textarea Peer B.
  5. Di tab Peer A, paste teks yang disalin ke textarea Peer A, lalu klik “Set Remote Answer”.
  6. Jika berhasil, Anda akan melihat pesan “Data Channel A: Terhubung!” dan “Data Channel B: Terhubung!” di log masing-masing.

6. Mengirim dan Menerima File melalui Data Channel

Sekarang kita akan mengimplementasikan logika untuk membaca file, memecahnya menjadi chunks, mengirimnya, dan menggabungkannya kembali di sisi penerima.

// Lanjutkan di script.js

// --- Logika Pengiriman File (Peer A) ---
const chunkSize = 16 * 1024; // 16KB per chunk

sendBtnA.onclick = () => {
    const file = fileInputA.files[0];
    if (!file) {
        log('A', '⚠️ Pilih file terlebih dahulu!');
        return;
    }

    log('A', `Mulai mengirim file: ${file.name} (${(file.size / (1024 * 1024)).toFixed(2)} MB)`);
    sendBtnA.disabled = true;

    // Kirim metadata file terlebih dahulu
    const metadata = {
        fileName: file.name,
        fileSize: file.size,
        type: 'metadata'
    };
    dataChannelA.send(JSON.stringify(metadata));

    let offset = 0;
    const reader = new FileReader();

    reader.onload = (e) => {
        // Kirim chunk data
        dataChannelA.send(e.target.result);
        offset += e.target.result.byteLength;
        updateProgressBar('A', (offset / file.size) * 100);

        if (offset < file.size) {
            readNextChunk();
        } else {
            log('A', '✅ File berhasil dikirim!');
            sendBtnA.disabled = false;
        }
    };

    reader.onerror = (e) => {
        log('A', `❌ Error membaca file: ${e.target.error}`);
        sendBtnA.disabled = false;
    };

    function readNextChunk() {
        const slice = file.slice(offset, offset + chunkSize);
        reader.readAsArrayBuffer(slice);
    }

    readNextChunk();
};

// --- Logika Penerimaan File (Peer B) ---
let receivedFileBuffer = [];
let receivedFileSize = 0;
let receivedFileName = '';
let currentFileOffset = 0;

dataChannelB.onmessage = (event) => {
    if (typeof event.data === 'string') {
        // Ini mungkin metadata atau pesan non-file lainnya
        const message = JSON.parse(event.data);
        if (message.type === 'metadata') {
            receivedFileName = message.fileName;
            receivedFileSize = message.fileSize;
            receivedFileBuffer = [];
            currentFileOffset = 0;
            updateProgressBar('B', 0);
            log('B', `Menerima metadata file: ${receivedFileName} (${(receivedFileSize / (1024 * 1024)).toFixed(2)} MB)`);
        } else {
            log('B', `Pesan dari Peer A: ${event.data}`);
        }
    } else if (event.data instanceof ArrayBuffer) {
        // Ini adalah chunk file
        receivedFileBuffer.push(event.data);
        currentFileOffset += event.data.byteLength;
        updateProgressBar('B', (currentFileOffset / receivedFileSize) * 100);

        if (currentFileOffset === receivedFileSize) {
            const receivedBlob = new Blob(receivedFileBuffer);
            const downloadUrl = URL.createObjectURL(receivedBlob);

            const a = document.createElement('a');
            a.href = downloadUrl;
            a.download = receivedFileName;
            a.textContent = `Download ${receivedFileName}`;
            a.style.display = 'block';
            log('B', '✅ File berhasil diterima! ' + a.outerHTML);

            URL.revokeObjectURL(downloadUrl); // Bebaskan memori setelah didownload
            receivedFileBuffer = [];
            receivedFileSize = 0;
            receivedFileName = '';
            currentFileOffset = 0;
            updateProgressBar('B', 0);
        }
    }
};

// --- Logika Pengiriman File (Peer B, opsional) ---
// Jika Anda ingin Peer B juga bisa mengirim, Anda bisa menduplikasi logika sendBtnA
// dan mengganti dataChannelA dengan dataChannelB.
// Namun, untuk demo ini, kita fokus pada A mengirim ke B.
sendBtnB.onclick = () => {
    log('B', 'Fitur pengiriman dari Peer B tidak diimplementasikan di demo ini.');
};

🎯 Penjelasan Kode Transfer File

  1. Chunking File: File besar tidak bisa dikirim sekaligus melalui Data Channel. Kita harus memecahnya menjadi bagian-bagian kecil (chunks). chunkSize 16KB adalah ukuran yang umum.
  2. FileReader: Digunakan untuk membaca file yang dipilih oleh pengguna sebagai ArrayBuffer.
  3. Metadata: Sebelum mengirim data file biner, kita mengirim metadata (nama file, ukuran) sebagai string JSON. Ini penting agar penerima tahu apa yang diharapkan.
  4. dataChannel.send(): Metode ini digunakan untuk mengirim data. Bisa menerima string atau ArrayBuffer.
  5. dataChannel.onmessage: Di sisi penerima, event ini akan dipicu setiap kali data diterima. Kita memeriksa apakah data adalah string (metadata) atau ArrayBuffer (chunk file).
  6. Menggabungkan Chunks: Chunks yang diterima disimpan dalam array (receivedFileBuffer). Setelah semua chunks diterima (`currentFileOffset ===