WebRTC - การส่งสัญญาณ
แอปพลิเคชัน WebRTC ส่วนใหญ่ไม่เพียงแค่สามารถสื่อสารผ่านภาพและเสียงเท่านั้น พวกเขาต้องการคุณสมบัติอื่น ๆ อีกมากมาย ในบทนี้เราจะสร้างเซิร์ฟเวอร์การส่งสัญญาณพื้นฐาน
การส่งสัญญาณและการเจรจา
ในการเชื่อมต่อกับผู้ใช้รายอื่นคุณควรทราบว่าเขาอยู่ที่ใดบนเว็บ ที่อยู่ IP ของอุปกรณ์ของคุณช่วยให้อุปกรณ์ที่ใช้อินเทอร์เน็ตสามารถส่งข้อมูลระหว่างกันได้โดยตรง RTCPeerConnectionวัตถุเป็นผู้รับผิดชอบสำหรับการนี้ ทันทีที่อุปกรณ์ต่างๆรู้วิธีค้นหาซึ่งกันและกันทางอินเทอร์เน็ตพวกเขาก็เริ่มแลกเปลี่ยนข้อมูลเกี่ยวกับโปรโตคอลและตัวแปลงสัญญาณที่แต่ละอุปกรณ์รองรับ
ในการสื่อสารกับผู้ใช้รายอื่นคุณต้องแลกเปลี่ยนข้อมูลการติดต่อส่วนที่เหลือจะดำเนินการโดย WebRTC กระบวนการเชื่อมต่อกับผู้ใช้รายอื่นเรียกอีกอย่างว่าการส่งสัญญาณและการเจรจา ประกอบด้วยสองสามขั้นตอน -
สร้างรายชื่อผู้ที่มีศักยภาพสำหรับการเชื่อมต่อแบบเพียร์
ผู้ใช้หรือแอปพลิเคชันเลือกผู้ใช้เพื่อทำการเชื่อมต่อ
เลเยอร์การส่งสัญญาณแจ้งผู้ใช้รายอื่นว่ามีคนต้องการเชื่อมต่อกับเขา เขาสามารถยอมรับหรือปฏิเสธได้
ผู้ใช้รายแรกจะได้รับแจ้งการยอมรับข้อเสนอ
ผู้ใช้รายแรกเริ่มต้นRTCPeerConnectionกับผู้ใช้รายอื่น
ผู้ใช้ทั้งสองแลกเปลี่ยนข้อมูลซอฟต์แวร์และฮาร์ดแวร์ผ่านเซิร์ฟเวอร์การส่งสัญญาณ
ผู้ใช้ทั้งสองแลกเปลี่ยนข้อมูลตำแหน่ง
การเชื่อมต่อสำเร็จหรือล้มเหลว
ข้อกำหนด WebRTC ไม่มีมาตรฐานใด ๆ เกี่ยวกับการแลกเปลี่ยนข้อมูล ดังนั้นโปรดทราบว่าข้างต้นเป็นเพียงตัวอย่างของการส่งสัญญาณที่อาจเกิดขึ้นได้ คุณสามารถใช้โปรโตคอลหรือเทคโนโลยีใดก็ได้ที่คุณต้องการ
การสร้างเซิร์ฟเวอร์
เซิร์ฟเวอร์ที่เรากำลังจะสร้างจะสามารถเชื่อมต่อผู้ใช้สองคนเข้าด้วยกันซึ่งไม่ได้อยู่บนคอมพิวเตอร์เครื่องเดียวกัน เราจะสร้างกลไกการส่งสัญญาณของเราเอง เซิร์ฟเวอร์การส่งสัญญาณของเราจะอนุญาตให้ผู้ใช้คนหนึ่งโทรหาอีกคนหนึ่ง เมื่อผู้ใช้โทรหาผู้อื่นเซิร์ฟเวอร์จะส่งข้อเสนอคำตอบผู้สมัคร ICE ระหว่างพวกเขาและตั้งค่าการเชื่อมต่อ WebRTC
แผนภาพด้านบนคือขั้นตอนการรับส่งข้อความระหว่างผู้ใช้เมื่อใช้เซิร์ฟเวอร์การส่งสัญญาณ ก่อนอื่นผู้ใช้แต่ละคนลงทะเบียนกับเซิร์ฟเวอร์ ในกรณีของเรานี่จะเป็นชื่อผู้ใช้สตริงธรรมดา ๆ เมื่อผู้ใช้ลงทะเบียนแล้วจะสามารถโทรหากันได้ ผู้ใช้ 1 ยื่นข้อเสนอด้วยตัวระบุผู้ใช้ที่เขาต้องการโทรหา ผู้ใช้รายอื่นควรตอบ ในที่สุดผู้สมัคร ICE จะถูกส่งระหว่างผู้ใช้จนกว่าจะสามารถเชื่อมต่อได้
ในการสร้างไคลเอนต์การเชื่อมต่อ WebRTC ต้องสามารถถ่ายโอนข้อความโดยไม่ต้องใช้การเชื่อมต่อแบบเพียร์ WebRTC นี่คือที่ที่เราจะใช้ HTML5 WebSockets - การเชื่อมต่อซ็อกเก็ตแบบสองทิศทางระหว่างจุดสิ้นสุดสองจุด - เว็บเซิร์ฟเวอร์และเว็บเบราว์เซอร์ ตอนนี้เริ่มใช้ไลบรารี WebSocket สร้างไฟล์server.jsและใส่รหัสต่อไปนี้ -
//require our websocket library
var WebSocketServer = require('ws').Server;
//creating a websocket server at port 9090
var wss = new WebSocketServer({port: 9090});
//when a user connects to our sever
wss.on('connection', function(connection) {
console.log("user connected");
//when server gets a message from a connected user
connection.on('message', function(message){
console.log("Got message from a user:", message);
});
connection.send("Hello from server");
});
บรรทัดแรกต้องการไลบรารี WebSocket ซึ่งเราได้ติดตั้งไว้แล้ว จากนั้นเราสร้างเซิร์ฟเวอร์ซ็อกเก็ตบนพอร์ต 9090 ต่อไปเราจะฟังเหตุการณ์การเชื่อมต่อ รหัสนี้จะดำเนินการเมื่อผู้ใช้ทำการเชื่อมต่อ WebSocket กับเซิร์ฟเวอร์ จากนั้นเราจะรับฟังข้อความที่ผู้ใช้ส่งมา ในที่สุดเราก็ตอบกลับไปยังผู้ใช้ที่เชื่อมต่อว่า“ สวัสดีจากเซิร์ฟเวอร์”
ตอนนี้รันเซิร์ฟเวอร์โหนดและเซิร์ฟเวอร์ควรเริ่มรับฟังการเชื่อมต่อซ็อกเก็ต
ในการทดสอบเซิร์ฟเวอร์ของเราเราจะใช้ยูทิลิตี้wscatซึ่งเราได้ติดตั้งไว้แล้ว เครื่องมือนี้ช่วยในการเชื่อมต่อโดยตรงกับเซิร์ฟเวอร์ WebSocket และทดสอบคำสั่ง เรียกใช้เซิร์ฟเวอร์ของเราในหน้าต่างเทอร์มินัลเดียวจากนั้นเปิดอีกหน้าต่างหนึ่งและรันคำสั่งwscat -c ws: // localhost: 9090 คุณควรเห็นสิ่งต่อไปนี้ในฝั่งไคลเอ็นต์ -
เซิร์ฟเวอร์ควรบันทึกผู้ใช้ที่เชื่อมต่อด้วย -
การลงทะเบียนผู้ใช้
ในเซิร์ฟเวอร์การส่งสัญญาณของเราเราจะใช้ชื่อผู้ใช้แบบสตริงสำหรับการเชื่อมต่อแต่ละครั้งเพื่อให้เราทราบว่าจะส่งข้อความไปที่ใด มาเปลี่ยนตัวจัดการการเชื่อมต่อของเราสักหน่อย -
connection.on('message', function(message) {
var data;
//accepting only JSON messages
try {
data = JSON.parse(message);
} catch (e) {
console.log("Invalid JSON");
data = {};
}
});
วิธีนี้เรายอมรับเฉพาะข้อความ JSON ต่อไปเราต้องจัดเก็บผู้ใช้ที่เชื่อมต่อทั้งหมดไว้ที่ใดที่หนึ่ง เราจะใช้วัตถุ Javascript ธรรมดาสำหรับมัน เปลี่ยนด้านบนของไฟล์ของเรา -
//require our websocket library
var WebSocketServer = require('ws').Server;
//creating a websocket server at port 9090
var wss = new WebSocketServer({port: 9090});
//all connected to the server users
var users = {};
เราจะเพิ่มฟิลด์ประเภทสำหรับทุกข้อความที่มาจากไคลเอนต์ ตัวอย่างเช่นหากผู้ใช้ต้องการเข้าสู่ระบบเขาจะส่งข้อความประเภทการเข้าสู่ระบบ มากำหนดกัน -
connection.on('message', function(message){
var data;
//accepting only JSON messages
try {
data = JSON.parse(message);
} catch (e) {
console.log("Invalid JSON");
data = {};
}
//switching type of the user message
switch (data.type) {
//when a user tries to login
case "login":
console.log("User logged:", data.name);
//if anyone is logged in with this username then refuse
if(users[data.name]) {
sendTo(connection, {
type: "login",
success: false
});
} else {
//save user connection on the server
users[data.name] = connection;
connection.name = data.name;
sendTo(connection, {
type: "login",
success: true
});
}
break;
default:
sendTo(connection, {
type: "error",
message: "Command no found: " + data.type
});
break;
}
});
หากผู้ใช้ส่งข้อความด้วยประเภทการเข้าสู่ระบบเรา -
ตรวจสอบว่ามีใครเข้าสู่ระบบด้วยชื่อผู้ใช้นี้แล้ว
หากเป็นเช่นนั้นให้แจ้งผู้ใช้ว่าเขาเข้าสู่ระบบไม่สำเร็จ
หากไม่มีใครใช้ชื่อผู้ใช้นี้เราจะเพิ่มชื่อผู้ใช้เป็นกุญแจสำคัญในออบเจ็กต์การเชื่อมต่อ
หากไม่รู้จักคำสั่งเราจะส่งข้อผิดพลาด
รหัสต่อไปนี้เป็นฟังก์ชันตัวช่วยสำหรับส่งข้อความไปยังการเชื่อมต่อ เพิ่มลงในไฟล์server.js -
function sendTo(connection, message) {
connection.send(JSON.stringify(message));
}
ฟังก์ชันข้างต้นช่วยให้มั่นใจได้ว่าข้อความทั้งหมดของเราจะถูกส่งในรูปแบบ JSON
เมื่อผู้ใช้ตัดการเชื่อมต่อเราควรล้างการเชื่อมต่อ เราสามารถลบผู้ใช้ได้เมื่อปิดเหตุการณ์ เพิ่มรหัสต่อไปนี้ในตัวจัดการการเชื่อมต่อ -
connection.on("close", function() {
if(connection.name) {
delete users[connection.name];
}
});
ตอนนี้เรามาทดสอบเซิร์ฟเวอร์ของเราด้วยคำสั่งเข้าสู่ระบบ โปรดทราบว่าข้อความทั้งหมดต้องเข้ารหัสในรูปแบบ JSON เรียกใช้เซิร์ฟเวอร์ของเราและพยายามเข้าสู่ระบบ คุณควรเห็นสิ่งนี้ -
การโทร
หลังจากเข้าสู่ระบบสำเร็จผู้ใช้ต้องการโทรหาคนอื่น เขาควรยื่นข้อเสนอให้กับผู้ใช้รายอื่นเพื่อให้บรรลุ เพิ่มตัวจัดการข้อเสนอ -
case "offer":
//for ex. UserA wants to call UserB
console.log("Sending offer to: ", data.name);
//if UserB exists then send him offer details
var conn = users[data.name];
if(conn != null){
//setting that UserA connected with UserB
connection.otherName = data.name;
sendTo(conn, {
type: "offer",
offer: data.offer,
name: connection.name
});
}
break;
ประการแรกเราได้รับการเชื่อมต่อของผู้ใช้ที่เราพยายามโทรหา หากมีอยู่เราจะส่งรายละเอียดข้อเสนอให้เขา เรายังเพิ่มotherNameให้กับวัตถุการเชื่อมต่อ สิ่งนี้สร้างขึ้นเพื่อความง่ายในการค้นหาในภายหลัง
กำลังตอบ
การตอบกลับมีรูปแบบที่คล้ายกันกับที่เราใช้ในตัวจัดการข้อเสนอ เซิร์ฟเวอร์ของเราส่งผ่านข้อความทั้งหมดเป็นคำตอบให้กับผู้ใช้รายอื่น เพิ่มรหัสต่อไปนี้หลังผู้ยื่นข้อเสนอ -
case "answer":
console.log("Sending answer to: ", data.name);
//for ex. UserB answers UserA
var conn = users[data.name];
if(conn != null) {
connection.otherName = data.name;
sendTo(conn, {
type: "answer",
answer: data.answer
});
}
break;
คุณสามารถดูว่าสิ่งนี้คล้ายกับตัวจัดการข้อเสนออย่างไร สังเกตว่าโค้ดนี้ตามด้วยฟังก์ชัน createOfferและcreateAnswerบนอ็อบเจ็กต์RTCPeerConnection
ตอนนี้เราสามารถทดสอบกลไกข้อเสนอ / คำตอบของเรา เชื่อมต่อลูกค้าสองรายในเวลาเดียวกันและพยายามเสนอและตอบคำถาม คุณควรเห็นสิ่งต่อไปนี้ -
ในตัวอย่างนี้ offer และ answer เป็นสตริงที่เรียบง่าย แต่ในแอปพลิเคชันจริงจะมีการกรอกข้อมูล SDP
ผู้สมัคร ICE
ส่วนสุดท้ายคือการจัดการผู้สมัคร ICE ระหว่างผู้ใช้ เราใช้เทคนิคเดียวกันในการส่งข้อความระหว่างผู้ใช้ ความแตกต่างที่สำคัญคือข้อความของผู้สมัครอาจเกิดขึ้นหลายครั้งต่อผู้ใช้ในลำดับใดก็ได้ เพิ่มตัวจัดการผู้สมัคร -
case "candidate":
console.log("Sending candidate to:",data.name);
var conn = users[data.name];
if(conn != null) {
sendTo(conn, {
type: "candidate",
candidate: data.candidate
});
}
break;
ควรทำงานคล้ายกับข้อเสนอและตัวจัดการคำตอบ
ออกจากการเชื่อมต่อ
เพื่อให้ผู้ใช้ของเราตัดการเชื่อมต่อกับผู้ใช้รายอื่นเราควรใช้ฟังก์ชันการวางสาย นอกจากนี้ยังบอกให้เซิร์ฟเวอร์ลบการอ้างอิงผู้ใช้ทั้งหมด เพิ่มไฟล์leave ตัวจัดการ -
case "leave":
console.log("Disconnecting from", data.name);
var conn = users[data.name];
conn.otherName = null;
//notify the other user so he can disconnect his peer connection
if(conn != null) {
sendTo(conn, {
type: "leave"
});
}
break;
นอกจากนี้ยังจะส่งเหตุการณ์การลาให้กับผู้ใช้รายอื่นเพื่อให้เขาสามารถยกเลิกการเชื่อมต่อกับเพียร์ได้ นอกจากนี้เราควรจัดการกรณีที่ผู้ใช้หลุดการเชื่อมต่อจากเซิร์ฟเวอร์การส่งสัญญาณ มาแก้ไขตัวจัดการระยะใกล้ของเรา-
connection.on("close", function() {
if(connection.name) {
delete users[connection.name];
if(connection.otherName) {
console.log("Disconnecting from ", connection.otherName);
var conn = users[connection.otherName];
conn.otherName = null;
if(conn != null) {
sendTo(conn, {
type: "leave"
});
}
}
}
});
ตอนนี้หากการเชื่อมต่อยุติผู้ใช้ของเราจะถูกตัดการเชื่อมต่อ ใกล้เหตุการณ์จะถูกไล่ออกเมื่อผู้ใช้ปิดหน้าต่างเบราว์เซอร์ของเขาในขณะที่เรายังคงอยู่ในข้อเสนอ , คำตอบหรือผู้สมัครรัฐ
เซิร์ฟเวอร์สัญญาณที่สมบูรณ์
นี่คือรหัสทั้งหมดของเซิร์ฟเวอร์การส่งสัญญาณของเรา -
//require our websocket library
var WebSocketServer = require('ws').Server;
//creating a websocket server at port 9090
var wss = new WebSocketServer({port: 9090});
//all connected to the server users
var users = {};
//when a user connects to our sever
wss.on('connection', function(connection) {
console.log("User connected");
//when server gets a message from a connected user
connection.on('message', function(message) {
var data;
//accepting only JSON messages
try {
data = JSON.parse(message);
} catch (e) {
console.log("Invalid JSON");
data = {};
}
//switching type of the user message
switch (data.type) {
//when a user tries to login
case "login":
console.log("User logged", data.name);
//if anyone is logged in with this username then refuse
if(users[data.name]) {
sendTo(connection, {
type: "login",
success: false
});
} else {
//save user connection on the server
users[data.name] = connection;
connection.name = data.name;
sendTo(connection, {
type: "login",
success: true
});
}
break;
case "offer":
//for ex. UserA wants to call UserB
console.log("Sending offer to: ", data.name);
//if UserB exists then send him offer details
var conn = users[data.name];
if(conn != null) {
//setting that UserA connected with UserB
connection.otherName = data.name;
sendTo(conn, {
type: "offer",
offer: data.offer,
name: connection.name
});
}
break;
case "answer":
console.log("Sending answer to: ", data.name);
//for ex. UserB answers UserA
var conn = users[data.name];
if(conn != null) {
connection.otherName = data.name;
sendTo(conn, {
type: "answer",
answer: data.answer
});
}
break;
case "candidate":
console.log("Sending candidate to:",data.name);
var conn = users[data.name];
if(conn != null) {
sendTo(conn, {
type: "candidate",
candidate: data.candidate
});
}
break;
case "leave":
console.log("Disconnecting from", data.name);
var conn = users[data.name];
conn.otherName = null;
//notify the other user so he can disconnect his peer connection
if(conn != null) {
sendTo(conn, {
type: "leave"
});
}
break;
default:
sendTo(connection, {
type: "error",
message: "Command not found: " + data.type
});
break;
}
});
//when user exits, for example closes a browser window
//this may help if we are still in "offer","answer" or "candidate" state
connection.on("close", function() {
if(connection.name) {
delete users[connection.name];
if(connection.otherName) {
console.log("Disconnecting from ", connection.otherName);
var conn = users[connection.otherName];
conn.otherName = null;
if(conn != null) {
sendTo(conn, {
type: "leave"
});
}
}
}
});
connection.send("Hello world");
});
function sendTo(connection, message) {
connection.send(JSON.stringify(message));
}
ดังนั้นงานจึงเสร็จสิ้นและเซิร์ฟเวอร์การส่งสัญญาณของเราก็พร้อมแล้ว โปรดจำไว้ว่าการทำสิ่งต่างๆไม่เป็นระเบียบเมื่อทำการเชื่อมต่อ WebRTC อาจทำให้เกิดปัญหาได้
สรุป
ในบทนี้เราได้สร้างเซิร์ฟเวอร์การส่งสัญญาณที่เรียบง่ายและตรงไปตรงมา เราดำเนินการตามขั้นตอนการส่งสัญญาณการลงทะเบียนผู้ใช้และกลไกข้อเสนอ / คำตอบ นอกจากนี้เรายังดำเนินการส่งผู้สมัครระหว่างผู้ใช้