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

в 20:32, , рубрики: javascript, JS, peer connection, 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>    

Автор: stitakov

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js