Отправка сообщений peer-to-peer при помощи PeerJS

    Приветствую вас, уважаемые читатели. В предыдущей статье я рассказал, как сделать простую звонилку в браузере при помощи PeerJS. А сегодня планирую рассмотреть, как обмениваться сообщениями между двумя пользователями напрямую без задержек.

    Кому это интересно? Если Вы разрабатываете онлайн игру, в которой необходим быстрый обмен данными между игроками, тогда прямой обмен сообщениями это пожалуй то, что вам нужно.

    Разметка и инициализация


    Я покажу, как работает технология, на примере простого чата между двумя пользователями, а также расскажу как адаптировать код для обмена игровыми данными.

    Начнем с первичной разметки и инициализации объекта peer

    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="utf-8" />
        <title>PeerJS Обмен сообщениями</title>
         <script src="https://unpkg.com/peerjs@1.0.0/dist/peerjs.min.js"></script>
    </head>
    <body>
    	<h3>Мой ID: <span id=myid ></span></h3>
    	<input id=otherPeerId type=text placeholder="otherPeerId" ><button onclick="connectToNode(document.getElementById('otherPeerId').value)">Соединиться</button>
    	<div id=messages style="width:400px;height:60vh; background:#ADD8E6;margin:5px;">
    	</div><br>
    	<textarea id=mess style="width:400px;height:15vh" ></textarea><br>
    	<button onclick="sendMess(document.getElementById('mess'))">Отправить</button>
    	<script>
    		var messList=[];		
    		function addMess(mess) {
    			messList.push(mess);
    			document.getElementById('messages').innerHTML=messList.join("");
    		}
    		var peer=new Peer(); //инициализация peer		
    		var conn; //переменная, хранящая соединение
    		peer.on('open', function(peerID) {
    			document.getElementById('myid').innerHTML=peerID;			
    		});		
    	</script>
    </body>    
    

    В заголовке (head) мы подключаем PeerJS. Какую роль играют элементы с индексами myid и otherPeerId смотрите в статье о звонке

    Массив messList будет хранить ленту сообщений. Функция addMess будет добавлять элементы в этот массив и выводить его содержимое в контейнер переписки.

    Далее идет инициализация объекта peer, которая также описана в прошлой статье.

    Теперь немного о соединениях. Чтобы установить соединение необходимо, чтоб один участник, зная peerID другого, начал соединение с ним, а второй — получил это соединение.

    Установка соединения


    peer.on('connection', function(c) { //входящее соединение...
    	conn=c;
    	initConn();
    });
    function connectToNode(partnerPeer) { //исходящее соединение...
    	conn = peer.connect(partnerPeer);
            conn.partnerPeer=partnerPeer;
    	initConn();
    }
    

    Событие 'connection' для объекта peer происходит при входящем соединении. А функция connect объекта peer устанавливает такое соединение. В обоих случаях будем сохранять объект соединение в переменную conn. Поскольку дальнейшие действия с соединением для текущего учебного примера будут идентичны (хотя в боевом проекте разница может присутствовать), я вынес в отдельную функцию initConn.

    function initConn() {
    	conn.on ('open', function () { //открыто соединение
    		  addMess("<div><h4>Соединение установлено</h4></div>");
    		  conn.on ('data', function (data) { //прилетело сообщение
    			 addMess("<div><b>Партнер: </b>"+data+"</div>");
    		  });
    	});
    	conn.on('close',function() {addMess('-----------Соединение разорвано-------------');});
    }
    

    Здесь вешаем 2 обработчика: на открытие и на закрытие соединения. В обработчике на открытие соединения довешываем обработчик на прием данных, который будет добавлять в контейнер диалога прилетевшее сообщение.

    Остается только реализовать функцию, которая будет отправлять сообщение по нажатию кнопки Отправить, которая:

    1. добавляет сообщение в свою ленту
    2. отправляет сообщение партнеру (метод send у объекта соединение)
    3. очищает поле ввода сообщения

      function sendMess(elem) {
      	addMess("<div><b>Я: </b>"+elem.value+"</div>");
      	conn.send(elem.value);
      	elem.value="";
      }
      

    Адаптация к пересылке игровых данных


    Что необходимо сделать, чтоб посылать таким же методом не обычный текст, а данные, которыми нужно обмениваться в процессе игр? На самом деле ничего особенного. В JS есть методы JSON.stringify и JSON.parse которые преобразуют объект в строку и обратно. Просто заверните ваши данные объект, преобразуйте объект в строку (JSON.stringify) перед отправкой и превратите полученные данные в объект (JSON.parse) при получении

    //отправка
    gameObject={x:2,y:5,...}
    conn.send(JSON.stringify(gameObject));
    
    //получение
     conn.on ('data', function (data) { //прилетело сообщение
           gameObject=JSON.parse(data);
     });
    

    Обычно для пересылки игровых объектов и текстовых сообщений не нужны большие объемы данных. Но если вы собираетесь переслать содержимое целого контейнера на странице (куча HTML кода) имейте в виду, что большое соединение может не дойти в неизменном виде.

    Из личного опыта скажу: не стоит пересылать таким способом сообщения больше 10 КБ (~10 000 символов). Лучше такое сообщение записать во временный файл и послать партнеру команду на чтение кода из этого файла (думаю смысл вы уловили).

    На этом можно было бы остановиться, если бы не…

    Обрыв соединения


    Да, такое происходит. Виной тому бывает нестабильный интернет. Бывало ли так, что вы уже почти выиграли, но обрывается соединение и вы теряете весь свой прогресс? Чтобы такого избежать, давайте допишем код, который будет поднимать упавшее соединение. Будем для этого обрабатывать событие 'close'. Это событие возникает если:

    1. соединение было закрыто намеренно
    2. соединение пропало из-за плохого интернета или партнер попросту закрыл вкладку

      conn.on('close',function() {
          setTimeout(function() { 
              if(conn.partnerPeer) {
                    var pp=conn.partnerPeer;
                    conn = peer.connect(conn.partnerPeer);
                    conn.partnerPeer=pp;
                   initConn();
               }
      	else conn=null;
          }
          ,2000);
          addMess('-----------Соединение разорвано-------------');
      });
      

    Здесь мы с задержкой в 2 секунды после обрыва соединения просто пытаемся установить новое.

    partnerPeer у объекта conn присутствует только у установившего в первый раз соединение партнера, а значит только одна из 2-х сторон соединения начнет его восстанавливать при обрыве.

    И теперь весь код целиком:

    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="utf-8" />
        <title>PeerJS Обмен сообщениями</title>
         <script src="https://unpkg.com/peerjs@1.0.0/dist/peerjs.min.js"></script>
    </head>
    <body>
    	<h3>Мой ID: <span id=myid ></span></h3>
    	<input id=otherPeerId type=text placeholder="otherPeerId" ><button onclick="connectToNode(document.getElementById('otherPeerId').value)">Соединиться</button>
    	<div id=messages style="width:400px;height:60vh; background:#ADD8E6;margin:5px;">
    	</div><br>
    	<textarea id=mess style="width:400px;height:15vh" ></textarea><br>
    	<button onclick="sendMess(document.getElementById('mess'))">Отправить</button>
    	<script>
    		var messList=[];		
    		function addMess(mess) {
    			messList.push(mess);
    			document.getElementById('messages').innerHTML=messList.join("");
    		}
    		var peer=new Peer(); 		
    		var conn; //переменная, хранящая соединение
    		peer.on('open', function(peerID) {
    			document.getElementById('myid').innerHTML=peerID;			
    		});
    		peer.on('connection', function(c) { //входящее соединение...
    			conn=c;
    			initConn();
    		});
    		function connectToNode(partnerPeer) { //исходящее соединение...
    			conn = peer.connect(partnerPeer);
    			initConn();
    		}
    		function initConn() {
    			conn.on ('open', function () { //открыто соединение
    				  addMess("<div><h4>Соединение установлено</h4></div>");
    				  conn.on ('data', function (data) { //прилетело сообщение
    					 addMess("<div><b>Партнер: </b>"+data+"</div>");
    				  });
    			});
    			conn.on('close',function() {addMess('-----------Соединение разорвано-------------');});
    		}
    		function sendMess(elem) {
    			addMess("<div><b>Я: </b>"+elem.value+"</div>");
    			conn.send(elem.value);
    			elem.value="";
    		}
    	</script>
    </body>    
    

    Комментарии 12

      0

      На iOS не работает же Peerjs?

        0
        для iOS не работает. Хотя есть умельцы, которые вроде бы как-то обходят эту проблему. В общем, в Сафари однозначно работать не будет. А для Google Chrome на iOS я как-то натыкался на решение, но сейчас уже подзабыл
          0

          Chrome использует safari для отображения на iOS. Но сейчас зашёл проверил на iOS 13 ввели в экспериментальном варианте поддержку webrtc.

        0
        Как происходит подключение и происходит ли вообще если сетевая маршрутизация не позволяет п2п конкретным двум устройствам?
          0
          Для обмена сообщениями всё работает в чистом виде: так, как вы видите в статье. По крайней мере при тестировании с различными конфигурациями никаких проблем обнаружено не было. Если у вас для какого-то случая не заработает — пишите.
          А вот для звонка сетевая конфигурация действительно важна. Об этом подробнее здесь и здесь
          0
          С сафари все также без изменений?
            0
            в PeerJS да. Используйте чистый WebRTC :)
              0
              PeerJS как-то подустарел чтоли. Проект с запашком каким-то.
                0
                C запашком скорее Safari, чем PeerJS :)))
                  0
                  simple-peer выбрал. Peerjs не обновлялся кучу лет и не собирается.
                0
                А это тогда что?)
                github.com/peers/peerjs/issues/725#issuecomment-687819237?
                В описании либы пишут про сафари 12+ теперь
                  0
                  все таки как-то вы странно намекаете, что значит чистый?)

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое