Decentralized Local Sharing: Cách WebRTC vận hành LAN-Drop
"Xây dựng giải pháp thay thế AirDrop không cần cài đặt app cho mạng nội bộ sử dụng kênh truyền dữ liệu WebRTC và máy chủ Go."
Chia sẻ tệp hoặc văn bản giữa máy tính và điện thoại của bạn đáng lẽ phải diễn ra ngay lập tức. Đáng tiếc là quy trình thông thường thường yêu cầu tải tệp lên lưu trữ đám mây, tự gửi tin nhắn cho chính mình trên các ứng dụng chat hoặc cài đặt các phần mềm đa nền tảng nặng nề.
Chúng ta có thể giải quyết triệt để vấn đề này bằng cách sử dụng WebRTC (Web Real-Time Communication) để thiết lập các kênh truyền dữ liệu trực tiếp, ngang hàng (P2P) giữa các trình duyệt trong cùng mạng nội bộ, được điều phối bởi một máy chủ tín hiệu (signaling server) siêu nhẹ viết bằng Go.
Dưới đây là phân tích cách LAN-Drop thiết lập kết nối ngang hàng trực tiếp ngay trong trình duyệt web tiêu chuẩn.
Vòng đời kết nối ngang hàng WebRTC
Để bắt đầu truyền dữ liệu trực tiếp, hai thiết bị phải trao đổi siêu dữ liệu (Session Description Protocol - SDP) và các tuyến mạng khả dụng (ICE Candidates). Vì ban đầu chúng không biết địa chỉ IP nội bộ của nhau, một Signaling Server trung gian sẽ làm nhiệm vụ kết nối bước bắt tay này.
[Thiết bị A: Browser] <── WebSocket (SDP/ICE) ──> [Go Signaling Server] <── WebSocket (SDP/ICE) ──> [Thiết bị B: Browser]
│ │
└────────────────────────── Kênh P2P đã mã hóa (WebRTC) ──────────────────────────────────────────┘
Sau khi bước bắt tay SDP hoàn tất, các trình duyệt sẽ tự động kết nối trực tiếp với nhau trong mạng LAN, và máy chủ tín hiệu sẽ đi vào trạng thái rảnh.
Thiết kế Signaling Server bằng Go
Máy chủ điều phối tín hiệu cần hoạt động cực kỳ nhẹ và nhanh. Trong Go, chúng ta có thể viết trình xử lý WebSocket chuyển tiếp bằng thư viện gorilla/websocket:
package main
import (
"log"
"net/http"
"sync"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true },
}
type SignalServer struct {
mu sync.Mutex
clients map[string]*websocket.Conn
}
func (s *SignalServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println("Lỗi kết nối WebSocket:", err)
return
}
defer conn.Close()
clientId := r.URL.Query().Get("id")
s.mu.Lock()
s.clients[clientId] = conn
s.mu.Unlock()
log.Printf("Thiết bị %s đã kết nối vào luồng tín hiệu\n", clientId)
for {
// Đọc thông điệp từ client (như offer, answer hoặc ICE candidate)
_, msg, err := conn.ReadMessage()
if err != nil {
break
}
// Chuyển tiếp tín hiệu sang thiết bị đích...
s.relayMessage(clientId, msg)
}
s.mu.Lock()
delete(s.clients, clientId)
s.mu.Unlock()
}
Truyền tệp qua WebRTC Data Channel
Trong trình duyệt, chúng ta đọc file dưới dạng ArrayBuffer và truyền qua kênh RTCDataChannel theo từng khối nhỏ (thường từ 16KB đến 64KB) để tránh tràn bộ nhớ trình duyệt:
const peerConnection = new RTCPeerConnection({
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
});
const dataChannel = peerConnection.createDataChannel("fileTransfer");
dataChannel.onopen = () => {
console.log("Kênh truyền P2P trực tiếp đã mở!");
};
// Truyền file theo từng mảnh (chunk)
function sendFile(file) {
const chunkSize = 16384; // 16KB
const reader = new FileReader();
let offset = 0;
reader.onload = (e) => {
dataChannel.send(e.target.result);
offset += e.target.result.byteLength;
if (offset < file.size) {
readNextChunk();
} else {
console.log("Đã gửi file thành công!");
}
};
const readNextChunk = () => {
const slice = file.slice(offset, offset + chunkSize);
reader.readAsArrayBuffer(slice);
};
readNextChunk();
}