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 อาจทำให้เกิดปัญหาได้

สรุป

ในบทนี้เราได้สร้างเซิร์ฟเวอร์การส่งสัญญาณที่เรียบง่ายและตรงไปตรงมา เราดำเนินการตามขั้นตอนการส่งสัญญาณการลงทะเบียนผู้ใช้และกลไกข้อเสนอ / คำตอบ นอกจากนี้เรายังดำเนินการส่งผู้สมัครระหว่างผู้ใช้