пятница, 1 февраля 2013 г.

Создание websocket-приложения на Tomcat 7

Ранее я писал о своём опыте использования технологии websocket в связывании серверного java-приложения и android-клиента. Я предлагал использовать jetty 8 и делал обычное консольное java-приложение. Но если у нас websocket является частью web-приложения с сервлетами и т.п., то такой подход уже не годится. Да и зачем тянуть в свой проект jetty, если наш старый добрый Tomcat с версии 7.0.27 уже поддерживает websocket самостоятельно?
Сегодня мы сделаем websocket-приложение с помощью одного только Tomcat и javascript на клиенте.

WebSocketServlet
Ключевым механизмом реализации websocket в Tomcat является класс org.apache.catalina.websocket.WebSocketServlet. Он расширяет обычный javax.servlet.http.HttpServlet а значит мы можем "мапить" его на url как это делаем с остальными своими сервлетами. Собственно сама наша реализация крайне проста:

import org.apache.catalina.websocket.StreamInbound;
import org.apache.catalina.websocket.WebSocketServlet;
 
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServletRequest;
 
@WebServlet(name = "WsServlet", urlPatterns = {"/ws"})
public class WsServlet extends WebSocketServlet {
    @Override
    protected StreamInbound createWebSocketInbound(String s, HttpServletRequest httpServletRequest) {
        return new WsConnection();
    }
}

Мы тут реализуем метод, который возвращает объект, отвечающий за обслуживание websocket-соединения. Сам объект WsConnection выглядит так:

import org.apache.catalina.websocket.MessageInbound;
import org.apache.catalina.websocket.WsOutbound;
 
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.util.concurrent.ArrayBlockingQueue;
 
public class WsConnection extends MessageInbound {
 
    public static ArrayBlockingQueue<WsOutbound> connections = new ArrayBlockingQueue<WsOutbound>(100);
    private WsOutbound outbound;
 
    @Override
    protected void onBinaryMessage(ByteBuffer byteBuffer) throws IOException {
    }
 
    @Override
    protected void onTextMessage(CharBuffer charBuffer) throws IOException {
        broadcast(charBuffer.toString());
    }
 
    @Override
    protected void onOpen(WsOutbound outbound) {
        this.outbound = outbound;
        connections.add(outbound);
    }
 
    @Override
    protected void onClose(int status) {
        connections.remove(this.outbound);
    }
 
    private void broadcast(String message) {
        for (WsOutbound connection : connections) {
            try {
                CharBuffer buffer = CharBuffer.wrap(message);
                connection.writeTextMessage(buffer);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

Тут мы расширяем MessageInbound, что позволяет нам переопределяя соответствующие методы обрабатывать события установления и обрыва соединения, получения сообщения и т.п. При подключении нового клиента мы добавляем его в синхронизированную очередь, при отключении - удаляем. При получении сообщения просто рассылаем его всем подключенным клиентам. Такой вот чатик :)
На клиентской стороне можно сделать много красивостей, но мы обойдёмся минимальным количеством кода. Только чтобы убедиться в работоспособности сервера.

<html>
  <head>
    <title></title>
      <script type="text/javascript">
          var socket = new WebSocket("ws://mydomain.com:8080/ws");
 
          socket.onopen = function() {
              alert("Соединение установлено.");
          };
 
          socket.onclose = function(event) {
              if (event.wasClean) {
                  alert('Соединение закрыто');
              } else {
                  alert('Обрыв соединения');
              }
              alert('Код: ' + event.code + ' причина: ' + event.reason);
          };
          socket.onmessage = function(event) {
              var logarea = document.getElementById("log");
              logarea.value = event.data+"n"+logarea.value;
          };
          socket.onerror = function(error) {
              alert("Ошибка " + error.message);
          };
 
          function send() {
              var s = document.getElementById("in").value;
              socket.send(s);
          }
 
      </script>
  </head>
  <body>
    <input type="text" id="in" /><input type="button" onclick="send()" value="send" />
    <br/>
    <textarea id="log" rows="8" cols="20">