Статьи

Websocket Chat

Протокол веб-сокетов рекламировался как большой шаг вперед для двунаправленных веб-приложений, таких как чат, обещая новую эру простых кометных приложений. К сожалению, не существует такой вещи, как «серебряная пуля», и этот блог будет проходить через простую чат-комнату, чтобы увидеть, что делает websocket и не помогает с кометными приложениями. В мире websocket существует еще большая потребность в таких фреймворках, как cometd .

Простой чат

Чат — это приложение «helloworld» в web-2.0, а в Jetty -7 включена простая чат-комната для веб-сокетов, которая теперь поддерживает веб-сокеты . Источник простого чата можно увидеть в svn для клиентской и серверной части . Ключевой частью на стороне клиента является установление соединения WebSocket:

        join: function(name) {
          this._username=name;
          var location = document.location.toString().replace('http:','ws:');
          this._ws=new WebSocket(location);
          this._ws.onopen=this._onopen;
          this._ws.onmessage=this._onmessage;
          this._ws.onclose=this._onclose;
        },

Затем клиент может отправить сообщение чата на сервер:

        _send: function(user,message){
          user=user.replace(':','_');
          if (this._ws)
            this._ws.send(user+':'+message);
        },

и получить сообщение чата с сервера и отобразить его:

        _onmessage: function(m) {
          if (m.data){
            var c=m.data.indexOf(':');
            var from=m.data.substring(0,c).replace('<','<').replace('>','>');
            var text=m.data.substring(c+1).replace('<','<').replace('>','>');
            
            var chat=$('chat');
            var spanFrom = document.createElement('span');
            spanFrom.className='from';
            spanFrom.innerHTML=from+': ';
            var spanText = document.createElement('span');
            spanText.className='text';
            spanText.innerHTML=text;
            var lineBreak = document.createElement('br');
            chat.appendChild(spanFrom);
            chat.appendChild(spanText);
            chat.appendChild(lineBreak);
            chat.scrollTop = chat.scrollHeight - chat.clientHeight;   
          }
        },

Для серверной части мы просто принимаем входящие соединения в качестве участников:

        public void onConnect(Outbound outbound)
        {
            _outbound=outbound;
            _members.add(this);
        }

а затем для всех полученных сообщений мы отправляем их всем участникам:

        public void onMessage(byte frame, String data)
        {
            for (ChatWebSocket member : _members)
            {
                try
                {
                    member._outbound.sendMessage(frame,data);
                }
                catch(IOException e)
                {
                    Log.warn(e);
                }
            }
        }

Так что мы сделали правильно? У нас есть работающий чат — давайте развернем его, и мы станем следующим гуглом gchat !! К сожалению, реальность не так проста, и в этой комнате чата далеко не хватает тех функций, которые ожидают от комнаты чата — даже самой простой.

Не так просто чат

На закрытии?

Для чата стандартным вариантом использования является то, что как только вы определяете свое присутствие в комнате, оно остается до тех пор, пока вы явно не покинете комнату. В контексте веб-чата это означает, что вы можете отправлять получать сообщения чата, пока вы не закроете браузер или не уйдете со страницы. К сожалению, простой пример чата не реализует эту семантику, потому что протокол websocket допускает время простоя соединения. Поэтому, если в течение некоторого времени в чате не будет ничего сказано, то соединение с веб-сокетом будет закрыто клиентом, сервером или даже посредником. Приложение будет уведомлено об этом событии вызываемым методом onClose .

Так как же чат должен обрабатывать onClose ? Очевидная вещь для клиента — просто снова вызвать join и открыть новое соединение с сервером:

        _onclose: function() {
          this._ws=null;
          this.join(this.username);
        }

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

Держать в живых

Чтобы поддерживать присутствие, приложение чата может отправлять сообщения поддержки активности на веб-сокет, чтобы предотвратить его закрытие из-за простоя. Тем не менее, приложение вообще не имеет представления о том, что такое тайм-аут простоя, поэтому ему нужно будет выбрать какой-то произвольный частый период (например, 30 с) для отправки keep-alive и надеяться, что он меньше, чем любой тайм-аут простоя на пути (больше или меньше, чем длительный опрос сейчас). В идеале будущая версия websocket будет поддерживать обнаружение тайм-аута, поэтому она может либо сообщать приложению период для сообщений проверки активности, либо даже отправлять сообщения активности от имени приложения.

К сожалению, keep-alive не избавляет onClose от необходимости инициировать новые веб-сокеты, потому что Интернет не идеальное место, и особенно с Wi-Fi и мобильными клиентами, иногда соединения просто сбрасываются. Это стандартная часть HTTP, которая, если соединение закрывается во время использования, запросы GET повторяются для новых соединений, поэтому пользователи в основном защищены от временных сбоев соединения. Комната чата websocket должна работать с тем же предположением и даже с keep-alives, она должна быть готова к повторному открытию соединения при вызове onClose .

Очереди

With keep alives, the websocket chat connection should be mostly be a long lived entity, with only the occasional reconnect due to transient network problems or server restarts. Occasional loss of presence might not been seen to be a problem, unless you’re the dude that just typed a long chat message on the tiny keyboard of your vodafone360 app or instead of chat you are playing on chess.com and you don’t want to abandon a game due to transient network issues.  So for any reasonable level of quality of service, the application is going to need to «pave over» any small gaps in connectivity by providing some kind of message queue in both client and server.  If a message is sent during the period of time that there is no websocket connection, it needs to be queued until such time as the new connection is established. 

Timeouts

Unfortunately, some failures are not transient and sometimes a new connection will not be established. We can’t allow queues to grow for ever and to pretend that a user is present long after their connection is gone. Thus both ends of the chat application will also need timeouts and user will not be seen to have left the chat room until they have no connection for the period of the timeout or until an explicit leaving message is received.

Ideally a future version of websocket will support an orderly close message so the application can distinguish between a network failure (and keep the user’s presence for a time)  and an orderly close as the user leaves the page (and remove the user’s present).

Message Retries

Even with message queues, there is a race condition that makes it difficult to completely close the gaps between connections. If the onClose method is called very soon after a message is sent, then the application has no way to know if that close happened before or after the message was delivered. If quality of service is important, then the application currently has not option but to have some kind of per message or periodic acknowledgement of message delivery. Ideally a future version of websocket will support orderly close, so that delivery can be known for non failed connections and a complication of acknowledgements can be avoided unless the highest quality of service is required.

Backoff

With onClose handling, keep-alives, message queues, timeouts and retries, we finally will have a chat room that can maintain a users presence while they remain on the web page.  But unfortunately the chat room is still not complete, because it needs to handle errors and non transient failures. Some of the circumstances that need to be avoided include:

  • If the chat server is shut down, the client application is notified of this simply by a call to onClose rather than an onOpen call. In this case, onClose should not just reopen the connection as a 100% CPU busy loop with result.  Instead the chat application has to infer that there was a connection problem and to at least pause a short while before trying again — potentially with a retry backoff algorithm to reduce retries over time. Ideally a future version of websocket will allow more access to connection errors, as the handling of no-route-to-host may be entirely different to handling of a 401 unauthorized response from the server.
  • If the user types a large chat message, then the websocket frame sent may exceed some resource level on the client, server or intermediary. Currently the websocket response to such resource issues is to simply close the connection.  Unfortunately for the chat application, this may look like a transient network failure (coming after a successful onOpen call), so it may just reopen the connection and naively retry sending the message, which will again exceed the max message size and we can lather, rinse and repeat!  Again it is important that any automatic retries performed by the application will be limited by a backoff timeout  and/or max retries.   Ideally a future version of websocket will be able to send an error status as something distinct from a network failure or idle timeout, so the application will know not to retry errors.

Does it have to be so hard?

The above scenario is not the only way that a robust chat room could be developed. With some compromises on quality of service and some good user interface design, it would certainly be possible to build a chat room with less complex usage of a WebSocket.  However, the design decisions represented by the above scenario are not unreasonable even for chat and certainly are applicable to applications needing a better QoS that most chat rooms.

What this blog illustrates is that there is no silver bullet and that WebSocket will not solve many of the complexities that need to be addressed when developing robust comet web applications. Hopefully some features such as keep alives, timeout negotiation, orderly close and error notification can be build into a future version of websocket, but it is not the role of websocket to provide the more advanced handling of queues, timeouts, reconnections, retries and backoffs.   If you wish to have a high quality of service, then either your application or the framework that it uses will need to deal with these features.

Cometd with Websocket

Cometd version 2 will soon be released with support for websocket as an alternative transport to the currently supported JSON long polling and JSONP callback polling. Cometd supports all the  features discussed in this blog and makes them available transparently to browsers with or without websocket support.  We are hopeful that websocket usage will be able to give us even better throughput and latency for cometd than the already impressive results achieved with long polling.

From http://blogs.webtide.com