NodeJS + ReactJS | มาทำ P2P ด้วย Peerjs เบื้องต้นกัน

Created on Jun 24, 2020
P2P หรือ Peer-to-peer คือโครงสร้างเน็ตเวิค ที่ client เชื่อมต่อกันเอง แทนที่ client-server ที่ client จะเชื่อมกับ server โดยตรง

ก่อนอื่นเลย ทำไมเราถึงสนใจ P2P?

P2P นั้นมีมานานมากแล้ว หลักๆเราจะใช้ในพวก bit torrent ที่ client มาช่วยกันแชร์ไฟล์กันเอง ไม่ต้องพึ่งพา server กลางมาเก็บข้อมูลเหล่านั้น แต่ประโยชน์นอกเหนือจากนั้นหละ
เราสามารถใช้หลักการนี้ในการส่งข้อมูลที่มีความเป็นส่วนตัว ไม่อยากผ่านเซิฟเวอร์กลางก็ได้ หรือเราทำแอปที่มีวิธีการสื่อสารข้อมูลที่เราจะประหยัดทรัพยากรเซิฟเวอร์ได้ แทนที่เราต้องใช้เซิฟเวอร์เป็นตัวกลาง รับ-ส่ง ข้อมูลทุกอย่าง ซึ่งนอกจากจะเปลืองทรัพยากร CPU-Memory แล้ว มันยังเปลือง network bandwidth อีก ดังนั้นในบางกรณี เราอาจจะออกแบบให้ client ส่งข้อมูลโดยตรงกันเลย เป็นอีกทางเลือกในการออกแอปของเรา

แล้วหลักการทำงานจริงๆของมันละ เป็นยังไง

จู่ๆ จะให้ client ไปเชื่อมต่อกันเองนั้น เป็นไปได้ยาก โดยเฉพาะเมื่อเราอยู่ในโลกของ behind the NAT เห็นได้ชัดมากในประเทศเรา ที่ผู้ให้บริการอินเตอร์เน็ต จะจ่าย private IP ให้เรา แต่จู่ๆถ้าเราอยากเชื่อมต่อกับอินเตอร์เน็ตโลกภายนอก เราจำเป็นต้องมี public IP แล้วถ้าเราไม่มีละ เราจะทำยังไง
ในส่วนนี้ เราจะเอา STUN และ/หรือ TURN servers เข้ามาช่วย เซิฟเวอร์เหล่านี้จะมาช่วยเป็นตัวกลางในช่วงแรก ที่จะทำหน้าที่เปิดช่อง connection ให้ก่อน แล้วส่งข้อมูลจำเป็นให้กับเหล่า clients หลังจากนั้น client ก็สามารถสื่อสารกันโดยตรงได้แล้ว
หลังจาก client เชื่อมต่อกันแล้ว ต่อให้ เซิฟเวอร์ล่ม client ก็ยังสามารถสื่อสารกันต่อได้

มาลองลงมือทำกัน

โดยรวมเราจะใช้ module ของ PeerJS ซึ่งจะหุ้ม WebRTC เพื่อให้ง่ายกับการทำ P2P สำหรับการส่งข้อมูลตั้งแต่ข้อความ ยัน ภาพและเสียง วีดีโอ

สิ่งที่จะมาทำกันในบทความนี้

  1. Server : เป็น STUN server แบบง่ายๆ ใช้สำหรับแค่ให้ client หากัน
  2. Client : เป็นเว็ปแอปแบบส่งข้อความแบบเรียบง่ายสุดๆ
ทั้งหมดนี้เพื่อให้เราได้ลองทำความรู้จักกับ P2P เบื้องต้น
โครงสร้างโฟลเดอร์โปรเจคคร่าวๆ เราจะประมาณนี้
|-- simple-p2p-peerjs-example
    |-- client
    |   |-- package.json
    |   |-- src
    |       |-- App.css
    |       |-- App.js
    |       |-- index.css
    |       |-- index.js
    |-- server
        |-- index.js

มาเริ่มกับ server กันก่อน

cd
ไปที่ โฟลเดอร์
server
cd server
npm init -y
npm i peer
สร้างไฟล์
index.js
ในไฟล์นี้จะมีโค๊ดของเซิฟเวอร์เราทั้งหมด เพราะมันสั้นมากกก
const { PeerServer } = require('peer');
const port = 9000;
const path = '/myapp';
const peerServer = PeerServer({ port: port, path: path });
peerServer.on('connection', (client) => {
console.log(\`id: \${client.id} | token: \${client.token}\`)
});
console.log(\`peer server running on localhost:\${port}\${path}\`);
view raw index.js hosted with ❤ by GitHub
เพียงเท่านี้ STUN server เราก็เสร็จเรียบร้อยแล้ว จะรัน
node index.js
รอทิ้งไว้เลยก็ได้

แทรกอธิบายโค๊ดหน่อย

เราเพียงแค่
require(‘peer’)
เข้ามา แล้วเรียกใช้
PeerServer()
ตัวเซิฟเวอร์ของเราก็รันได้แล้ว
นอกจากนั้น ถ้าเราอยากดักข้อมูลขณะที่เกิดเช่น connection กับ disconnect ก็ทำได้ตามโค๊ดด้านล่าง
peerServer.on("connection", (client) => {
  // ดักข้อมูล client ตรงนี้
});
ตัว Peer เองยังสามารถพ่วงกับ expressjs เดิมของเราได้เลยด้วย สามารถไปอ่านเพิ่มได้จาก docs ต้นทางเลย

มาทำ client กันบ้าง

เราจะเขียนเว็ปโดยใช้ reactjs และจัดการ state ด้วย react hook มาเริ่มกันเลย
กลับมาที่โฟลเดอร์ต้นทางกันก่อน
/simple-p2p-peerjs-example
แล้วสร้าง react app ด้วย
npx create-react-app client
cd client
เพื่อลดความซับซ้อนในการอธิบาย เราก็จะแก้ที่ไฟล์เดียว ที่
/source/App.js
ไม่เน้นหน้าตา 😅
ขณะรันจะออกมาแบบนี้
โค๊ดตามด้านล่างเลย
import React, { useState, useEffect } from 'react';
import './App.css';
import Peer from 'peerjs';
var peer = new Peer({ host: 'localhost', port: 9000, path: '/myapp', secure: false });
// ค่า default สำหรับ conn
var conn = peer.connect();
function App() {
const [id, setId] = useState(0);
const [dest, setDest] = useState('dest-peer-id');
const [connect, setConnect] = useState(false);
const [sendMessage, setSendMessage] = useState('send something');
const [receiveMessage, setReceiveMessage] = useState('');
useEffect(() => {
// เชื่อมต่อกับ server เพื่อรับ ข้อมูล id ของตัวเอง
peer.on('open', function (id) {
console.log('My peer ID is: ' + id);
setId(id);
});
// for Client Receive Connection
// เรียกเมื่อ ได้รับแจ้งจากเซิฟเวอร์ว่า มีการเชื่อมต่อเข้ามา
peer.on('connection', function (newConn) {
setConnect(true);
// สำหรับปุ่ม send จะได้กดส่งได้
conn = newConn;
setDest(newConn.peer);
newConn.on('open', function () {
// Send messages when open connection
newConn.send('Hello!');
});
// Receive messages
newConn.on('data', function (data) {
setReceiveMessage(data);
});
});
});
function startConnection() {
conn = peer.connect(dest);
//for Client Establish Connection
conn.on('open', function () {
// Receive messages
setConnect(true);
conn.on('data', function (data) {
setReceiveMessage(data);
});
});
}
function send() {
// Send messages
conn.send(sendMessage);
}
return (
<div className="App">
<header className="App-header">
<h3>Simple P2P Web App with PeerJS</h3>
<h4>Peer ID: {id}</h4>
{
connect ?
<h6 style={{ color: "green" }}>Connected</h6>
:
<h6 style={{ color: "red" }}>Not Connected</h6>
}
<input type="text" placeholder={dest} name="dest" onChange={e => setDest(e.target.value)} />
<button type="submit" onClick={startConnection}>Connect</button>
<br />
<input type="text" placeholder={sendMessage} name="sendMessage" onChange={e => setSendMessage(e.target.value)} />
<button type="submit" onClick={send}>Send Message</button>
<h5>Receive Message: {receiveMessage}</h5>
</header>
</div>
);
}
export default App;
view raw App.js hosted with ❤ by GitHub
ซึ่งออกมาเหมือนทำแอปแชททั่วไปเลย แต่ความพิเศษอยู่ที่ ข้อความไม่ได้ผ่านเซิฟเวอร์ และเมื่อเชื่อมต่อเรียบร้อยแล้วต่อให้ เซิฟเวอร์ ล่ม ก็ยังคุยกันต่อได้

แทรกอธิบายโค๊ดหน่อย

เราจะพูดถึงหลักๆ 2 ตัวแปล
peer
กับ
conn
(สำหรับในโค๊ดที่แนบข้างต้น ชื่อตัวแปรจริงๆไม่ได้ตายตัวนะ)
var peer = new Peer({
  host: "localhost",
  port: 9000,
  path: "/myapp",
  secure: false,
});
peer
ที่เรารับ
new Peer()
จะทำหน้าที่เชื่อมกับ Peer Server เพื่อรับ Peer ID ใช้เป็นที่อยู่สำหรับเชื่อมต่อกับ client ตัวอื่น
ตอนเชื่อมเราก็จะเรียก
conn = peer.connect(dest);
ซึ่ง ตัวแปร
conn
ก็จะได้รับข้อมูลของ client ปลายทางแล้ว
จะส่งข้อมูล ก็เรียก
conn.send(sendMessage);
ส่วนจะรับข้อมูล เราก็จะทำการดักรอ หรือที่เรียกว่า listen ด้วยคำสั่ง
conn.on(…)
conn.on("open", function () {
  // Receive messages
  setConnect(true);
  conn.on("data", function (data) {
    setReceiveMessage(data);
  });
});
เมื่อกี้เราพูดถึงฝั่งคนเปิดช่องทางติดต่อ ต่อมาเรามาพูดถึงฝั่งปลายทางบ้าง
จะสังเกตว่า peer ก็มี
peer.on()
peer.on("connection", function (newConn) {
  setConnect(true);
  // สำหรับปุ่ม send จะได้กดส่งได้
  conn = newConn;
  setDest(newConn.peer);
  newConn.on("open", function () {
    // Send messages when open connection
    newConn.send("Hello!");
  });
  // Receive messages
  newConn.on("data", function (data) {
    setReceiveMessage(data);
  });
});
ซึ่งตัวนี้แหละ จะเป็นการรับแจ้งจากฝั่งเซิฟเวอร์ว่ามีคนอยากติดต่อเข้ามา เราก็จะได้รับข้อมูลของคนที่เปิดการเชื่อมต่อเก็บในตัวแปร
newconn
ซึ่งตัวแปรนี้ก็ทำหน้าเช่นเดียวกันกับ
conn
เลย เราสามารถดักข้อมูลได้เช่นกันด้วย
newconn.on(…)
โค๊ดสำหรับ P2P ส่งความแบบง่ายๆก็มีเพียงเท่านี้
ปล. ภาพตัวอย่างการรันด้านบน ทดสอบบนวงเน็ตเวิคเดียวกันก็จริง แต่จริงๆ สามารถใช้งานบนโลกอินเตอร์เน็ตได้ client ไม่จำเป็นต้องมี public IP แต่ยังไง server ยังต้องมี public IP นะ
ใครอยากต่อยอดเอาไปทำเป็นแอปแชทก็ลองกันดูได้ หรือใช้สำหรับการสื่อสารประเภทอื่นก็ลองดู ตัว PeerJS เองยังลองรับการทำพวก video call ในตัวด้วย ลองไปอ่าน docs ต้นทางได้ แต่วันนี้ต้องขอลากันไปก่อนแล้ว ขอบคุณที่เข้ามาอ่านกันนะครับ
เช่นเคย แจกโค๊ดไว้ทิ้งท้าย เอาไป git clone เล่นกันได้ตามสบาย