Простой чат через WebRTC DataChannel
WebRTC - (web real-time communications — коммуникации в реальном времени).
Данная технология позволяет обмениваться данными напрямую между пользователями, без сервера. Сервер участвует только на начальном этапе, для соединения. Через данную технологию можно передавать видео/аудио трансляцию, файлы, текстовую информацию. В данной статье рассмотрим создание простого текстового чата. Тестировать будем в Google Chrome. 
Демо чат 

Схема действия:
Создадим простую HTML страницу:
<button onclick="createChat();">New</button>
<div id="chats"></div>
<textarea id="text"></textarea>
<button id="send" onclick="post();" disabled>Send</button>
<div id="recs"></div>
<script>getChats();</script>
Кнопка для создания нового чата. В #chats, будет отображать список чатов. #text и #send, поле текста и кнопка отправки сообщения. В #recs отображаем список сообщений. В самом конце, вызовем функцию получения чатов.

Приступим к каналу данных, для начала создадим соединение и канал, и несколько переменных для нашего чата.
var 
     RTC = new RTCPeerConnection({iceServers: [{url: "stun:stun.l.google.com:19302"}]}),
     channel = RTC.createDataChannel('chat'),
     user = Math.random(),         // Случайное число для идентификации юзера
     chatId = null,                // Ид выбранного чата
     chats = [],                   // Список чатов
     timer = null;                 // Таймер для отслеживания собеседника
Мы будем работать в соседних вкладках браузера, по этому в RTCPeerConnection, можно ничего не передавать, но для соединения пользователей из разных сетей необходимо добавить iceServers. Мы воспользуемся бесплатным сервером от Google: stun:stun.l.google.com:19302

Подпишемся на обнаружения кандидатов, они могут изменяться при смене сети или оборудования, так что их сразу сохраняем на нашем сервере.
RTC.onicecandidate = function(candidate) {
     if (candidate.candidate) {
          GCF.AJ('server.php?act=addCandidate&user=' + user + '&chat=' + chatId, candidate.candidate.toJSON());
     }
}

RTC.ondatachannel = function(event) {
     GCF.Q('#send').disabled = false;
     event.channel.onmessage = function(event) {
          // Пришло сообщение, отобразим
          msg(event.data);
     };
};
Вторая подписка на событие готовности канала, в ней мы подпишемся на входящее сообщение. И вызовем нашу функцию msg, для отображения сообщения(о ней ниже). 

Получение чатов:
function getChats() {
     GCF.AJ('server.php?act=getChats&user=' + user, {}, function(data){
          if (data.chats) {
               var html = '';
               for (var i = 0; i < data.chats.length; i++) {
                    chats[data.chats[i].id] = data.chats[i];
                    html += '<div onclick="openChat(' + data.chats[i].id + ')">' + data.chats[i].id + '</div>';
               }

               GCF.Q('#chats').innerHTML = html;
          }
     }, true);
}
Пока все просто, с помощью нашей библиотеке GCF, выполним Ajax запрос на сервер(его опишу ниже) для получения чатов. Если данные пришли, сохраним их с id в качестве ключа в переменной chats и выведем HTML код в #chats.

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

Пойдем по порядку с создания.
function createChat() {
     chatId = user; // Установим текущий чат, как ID текущего пользователя
     RTC.createOffer(function(offer) { // Создадим offer
          // Установим данные инструкции в локальный канал
          RTC.setLocalDescription(offer);
          // Оправим на сервер
          GCF.AJ('server.php?act=createChat&user=' + user, offer, function() {
               // Будем ожидать подобных  инструкций от собеседника
               timer = setInterval(getAnswer, 1000);
          });
     }, err);
}
Создадим инструкции, установим их как локальные и отправим на сервер. После createOffer, начнут вызываться события onicecandidate, которые, благодаря обработчику выше, будут уходить на сервер. Далее этому пользователю остается только ждать собеседника. 

Данный чат теперь отображается в списке, теперь представим, что зашел другой пользователь и выбрал этот чат.
function openChat(id) {
     chatId = id; // Установим выбранный чат
     // Установим инструкции от 1 пользователя в удаленный канал
     RTC.setRemoteDescription(chats[id].offer);
     RTC.createAnswer(function(answer) { // Создадим answer
          // Установим полученные инструкции в локальный канал
          RTC.setLocalDescription(answer);
          // Отправим инструкции 2 пользователя на сервер
          GCF.AJ('server.php?act=setAnswer&user=' + user + '&chat=' + chatId, answer, function() {
               // Готовы получать кандидатов
               timer = setInterval(getCandidate, 2000);
          });
     }, err);
}
Тут мы установим инструкции от пользователя создавшего чат, как удаленные, получим свои инструкции, установим их, как локальные, и отправим на сервер. После установки локальных и удаленных инструкций, мы готовы получать и устанавливать кандидатов. Данный порядок важен. Сначала два вида инструкций, потом кандидаты.

Вернемся к пользователю создавшему чат. На сервере появился аnswer.
function getAnswer() {
     GCF.AJ('server.php?act=getAnswer&user=' + user + '&chat=' + chatId, {}, function(data){
          if (data.answer) {
               // Установим инструкции, пользователя открывшего чат, как удаленные
               RTC.setRemoteDescription(data.answer);
               clearInterval(timer);   // ответ есть таймер больше не нужен
               // Готовы получать кандидатов
               timer = setInterval(getCandidate, 2000);
          }
     }, true);
}
Аналогично, устанавливаем удаленные инструкции, готовы принимать кандидатов.

Кандидатов будем проверять на сервере, каждые 2 секунды, можно реже, вдруг что-то изменится.
function getCandidate() {
     GCF.AJ('server.php?act=getCandidate&user=' + user + '&chat=' + chatId, {}, function(data){
          if (data.candidates) {
               for (var i = 0; i < data.candidates.length; i++) {
                    RTC.addIceCandidate(new RTCIceCandidate(data.candidates[i]));
               }
          }
     }, true);
}
Как кандидаты будут установлены, вызовется ondatachannel, и можно будут отправлять сообщения.

И последний набор функций
function post() {
     // Отправляем сообщение по каналу
     channel.send(GCF.Q('#text').value);
     msg(GCF.Q('#text').value);
}
function msg(text) {
     GCF.Q('#recs').innerHTML = '<div>' + text + '</div>' + GCF.Q('#recs').innerHTML;
}
function err(e) {
     console.log(e);
}
Для createOffer и createAnswer обязательно нужен errorback, у нас это функция err.

Архитектура сервера по желанию, приведу решение, для данного примера.
<?
     session_start();

     // Проверим, что бы сессия с chats, была всегда
     if (!isset($_SESSION['chats'])) {
          $_SESSION['chats'] = array();
     }
     
     if ($_GET['act'] == 'createChat') { 
          // Создание чата, вызывается из createChat
          // Получим данные
          $data = file_get_contents('php://input');
          $data = json_decode($data, true);
          // Создад объект чата
          $_SESSION['chats'][$_GET['user']] = array(
               'id' => $_GET['user'],
               'offer' => $data,
               'answer' => null,
               'offerCandidates' => array(),
               'answerCandidates' => array()
          );
     } else if ($_GET['act'] == 'addCandidate') { 
          // Добавляем кандидата, вызывается из onicecandidate
          $data = file_get_contents('php://input');
          $data = json_decode($data, true);
          // Если ИД чата и юзера равны, значит это кандидаты создателя
          $candidates = $_GET['user'] == $_GET['chat'] ? 'offerCandidates' : 'answerCandidates';
          array_push($_SESSION['chats'][$_GET['chat']][$candidates], $data);
     } else if($_GET['act'] == 'getChats') {
          // Получаем чаты, вызывается из getChats
          $chats = array();
          foreach ($_SESSION['chats'] as $id => $chat) {
               // Будем отображать только с кандидатами создателя
               if (count($chat['offerCandidates'])) {
                    array_push($chats, $chat);
               }
          }
          echo '{"chats": ' . json_encode($chats) . '}';
     } else if ($_GET['act'] == 'setAnswer') {
          // Устанавливаем ответ, вызывается из openChat
          $data = file_get_contents('php://input');
          $data = json_decode($data, true);
          $_SESSION['chats'][$_GET['chat']]['answer'] = $data;
     } else if ($_GET['act'] == 'getAnswer') {
          // Получаем ответ, вызывается из getAnswer
          $answer = $_SESSION['chats'][$_GET['chat']]['answer'];
          if (!$answer) {
               $answer = null;
          }
          echo '{"answer": ' . json_encode($answer) . '}';
     } else if ($_GET['act'] == 'getCandidate') {
          // Получаем кандидата, вызывается из getCandidate
          // Если ИД чата и юзера равно, возвращаем кандидаты не создателя
          $candidates = $_GET['user'] == $_GET['chat'] ? 'answerCandidates' : 'offerCandidates';
          echo '{"candidates": ' . json_encode($_SESSION['chats'][$_GET['chat']][$candidates]) . '}';
          // Стерем их, т.к. уже будут установлены
          $_SESSION['chats'][$_GET['chat']][$candidates] = array();
     }
?>
Так как используются сессии, работать будет только в соседних вкладках браузера, но ничего не мешает сохранять в БД.

В ближайшее время рассмотрю пример видео трансляции. И возможно, оформлю данный код в виде функции библиотеки GCF.