Приветствую вас, уважаемые читатели. В предыдущей статье я рассказал, как сделать простую звонилку в браузере при помощи 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>