Sonntag, 24. Februar 2013

K2.3 - HTML5 WebSocket Server mit Tomcat 7

Einleitung

In diesem Artikel werde ich zeigen, wie man die HTML5 WebSockets auf der Client-side und Server-side nutzt. Die WebSockets benötigen auf beiden Seiten spezielle APIs bzw. Implementierungen und gerade letztere gibt es in einigen Sprachen die im Web Umfeld relevant sind, z.B. Java, PHP, .NET, Ruby, Python, etc.

Heute werden wir auf der Client-side die neue HTML5 Javascript WebSocket API einsetzen und damit eine persistente Verbindung zum Webserver aufbauen. Außerdem werden wir eine Textnachricht zum Server senden und sogar eine Broadcast Nachricht vom Webserver empfangen. Der Client-side Code ist überwiegend von jcavallotti.blogspot.de kopiert - blogspot Blogger müssen schließlich zusammenhalten.

Auf der Server-side werden wir den Tomcat 7 nutzen mit seiner neuen Servlet API für WebSockets. Meine Hauptquelle für diesen Artikel war ein Tutorial auf tomcatexpert.com. Ich arbeite auf der Server-side bevorzugt mit Java oder PHP, daher kommen für mich Technologien wie Node.js oder Tornado nicht in Frage. Es gibt auch schon erste inoffizielle Module für den Apache http Server in C, die den Apache Webserver WebSocket-fähig machen. Was daraus wird bleibt abzuwarten.

Motivation

Mit einer persistenten Verbindung zum Server kann man eine Menge nützlicher Anwendungen umsetzen, allen voran sind das Applikationen mit Echtzeit Anforderungen. Mit Ajax ist lediglich ein s.g. Polling möglich, was einen extremen Overhead an unnötigen Verbindungsauf- und abbau bedeutet und somit die Anwendung einiges an Performance kostet.

Ein weiterer Vorteil von WebSockets ist, dass nun server-controlled Peer2Peer Verbindungen möglich sind. Das bedeutet, dass man zwar keine direkte Verbindung von Client zu Client hinbekommt, aber die Server-side Applikation die Verbindungen verwalten kann, und somit Daten direkt an Clients adressieren kann. Als bestes Beispiel dient hier ein Private Messaging Feature eines Chatsystems. Mit Ajax muss der Client zyklisch den Server fragen (polling), ob neue Private Nachrichten eingetroffen sind. Das verlangt außerdem einen Mehraufwand in der Programmierung bzgl. Sicherheit und Authentifizierung.
Mit den WebSockets benachrichtigt der Server den Client quasi in Echtzeit, wenn ein anderer Client eine Nachricht an diesen Client gesendet hat. Dieses Verfahren nennt man auch Event-basierte Kommunikation, weil der Client auf ein Ereignis reagiert, statt ständig nachzufragen, ob es ein Ereignis gibt.

WebSockets sind ein echtes Highlight im HTML5 Standard. Wer sich jetzt damit vertraut macht kann bestehende Anwendungen optimieren oder ganz neue state-of-the-art Applikationen bauen, wie z.B. tweetping.net.

Voraussetzungen

 Wir benötigen folgende Tools:
  • Eclipse
  • Maven
  • Tomcat 7
  • HTML5 kompatiblen Browser
Ich empfehle außerdem die Tomcat 7 Websocket API Dokumentation. Alle hier genannten Voraussetzungen werden in vorherigen Artikeln (K2.1 bis K2.2.1) ausführlich behandelt.

Umsetzung

Kurzübersicht aller Schritte

  1. Neue Verzeichnisstruktur inklusive Dateien im parentproject anlegen
  2. Verzeichnis als Maven Projekt importieren
  3. Eclipse Run Configuration einrichten
  4. Tomcat 7 Server starten
  5. Maven Cargo Plugin ausführen
  6. Mit HTML websocket Client verbinden
Den gesamten Code des aktuellen Standes meines Maven Projekts parentproject gibt es hier zum Download.

1. Verzeichnisstruktur anlegen

Wir wollen folgende Verzeichnisse und Dateien anlegen:


Die Quelltexte zu den 3 Dateien sehen folgendermaßen aus:

pom.xml

<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemalocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
   <modelversion>4.0.0</modelversion>
   <artifactid>websocket</artifactid>
   <version>1.0.0</version>
   <packaging>war</packaging>
  
   <parent>
    <groupid>de.blogspot.sw-technik</groupid>
     <artifactid>parentproject</artifactid>
     <version>0.0.1-SNAPSHOT</version>
   </parent>

 <properties>
  <tomcat .version="">7.0.29</tomcat>
  <cargo .version="">1.2.4</cargo>
 </properties>

 <build>
  <plugins>
   <plugin>
    <groupid>org.codehaus.cargo</groupid>
    <artifactid>cargo-maven2-plugin</artifactid>
    <version>${cargo.version}</version>
    <!-- Cargo Tomcat --> 
    <configuration>
     <container>
      <containerid>tomcat7x</containerid>
      <type>remote</type>
     </container>
     <configuration>
      <type>runtime</type>
      <properties>
       <!-- /text requires the tomcat-users.xml user/role config with manager-script -->
       <cargo .tomcat.manager.url="">http://localhost:8080/manager/text</cargo>
       <cargo .remote.username="">tomcat</cargo>
       <cargo .remote.password="">tomcat</cargo>
      </properties>
     </configuration>
    </configuration>
    <dependencies>
     <dependency>
        <groupid>org.codehaus.cargo</groupid>
        <artifactid>cargo-core-container-tomcat</artifactid>
        <version>${cargo.version}</version>
     </dependency>
    </dependencies>
   </plugin>
  </plugins>
 </build>
   
 <dependencies>

  <dependency>
   <groupid>org.apache.tomcat</groupid>
   <artifactid>tomcat-catalina</artifactid>
   <version>${tomcat.version}</version>
   <scope>provided</scope>
  </dependency>
  
  <dependency>
   <groupid>org.apache.tomcat</groupid>
   <artifactid>tomcat-coyote</artifactid>
   <version>${tomcat.version}</version>
   <scope>provided</scope>
  </dependency>
  
 </dependencies>
 
</project>


web.xml

<web-app>
   <display-name>Archetype Created Web Application</display-name>
 <servlet>
  <servlet-name>ws_servlet</servlet-name>
  <servlet-class>de.blogspot.swtechnik.servlet.WsServlet</servlet-class>
 </servlet>
 <servlet-mapping>
  <servlet-name>ws_servlet</servlet-name>
  <url-pattern>/websocket</url-pattern>
 </servlet-mapping>
</web-app>

de.blogspot.swtechnik.servlet.WsServlet.java

package de.blogspot.swtechnik.servlet;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;

import javax.servlet.http.HttpServletRequest;

import org.apache.catalina.websocket.MessageInbound;
import org.apache.catalina.websocket.StreamInbound;
import org.apache.catalina.websocket.WebSocketServlet;
import org.apache.catalina.websocket.WsOutbound;

public class WsServlet extends WebSocketServlet {

 private static final long serialVersionUID = 4541321228711151433L;

 /**
  * Connected clients
  */
 private static List clients = new ArrayList();
 
 /**
  * Send a message to all clients connected.
  * 
  * @param message
  */
 private void broadcast(String message) {
  StreamInbound someClient;
  ListIterator iter = clients.listIterator();
  while (iter.hasNext()) {
   someClient = (MessageInbound) iter.next();
   try {
    someClient.getWsOutbound().writeTextMessage(
      CharBuffer.wrap(message)
     );
   } catch (IOException e) {
   }
  }
 }
 
 @Override
 protected StreamInbound createWebSocketInbound(String string, HttpServletRequest hsr) {
  MessageInbound inbound = new MessageInbound() {
   
   @Override
   protected void onClose(int status) {
    System.out.println("onClose - status code: " + status);
    clients.remove(this);
   }
   
   @Override
   protected void onBinaryMessage(ByteBuffer bb) throws IOException {
    System.out.println("onBinaryMessage");
   }

   @Override
   protected void onTextMessage(CharBuffer cb) throws IOException {
    System.out.println("onTextMessage");
    
    CharBuffer msg = CharBuffer.wrap(cb);
    WsOutbound outbound = getWsOutbound();
    // Send message to client connected
    outbound.writeTextMessage(msg);
    // Send message to all clients connected
    broadcast("Broadcast");
   }
   
   @Override
   protected void onOpen(WsOutbound outbound) {
    int connSize = clients.size();
    System.out.println("onOpen - connections: " + connSize);
   }
  };
  
  // Collect clients connected
  clients.add(inbound);
  
  return inbound;
 }
}

2. Import als Maven Projekt

Öffnen Sie in Eclipse die Import View:
Nach Abschluss dieses Dialogs sollten Sie nun ein eigenständiges Projekt in Eclipse haben.


3. Eclipse Run Configuration einrichten

Für dieses Projekt nutzen wir das Maven Cargo Plugin, um unsere Web Anwendung per Klick auf dem Tomcat Server zu deployen.


4. Tomcat 7 starten

Für HTML5 websockets wird ein Server benötigt, der das ws und wss Protokoll implementiert. Der Apache http Webserver ist momentan noch nicht fähig Verbindungen mit diesem Protokoll zu verwalten. Aktuelle Server, die websockets unterstützen sind z.B.
Wir werden in diesem Tutorial den Tomcat 7 verwenden, da es mir lieber ist auf der Server-side mit Java, statt Javascript zu programmieren. Sicherlich sind die Java Lösungen nicht so performant, wie ein Node.js, aber für die meisten Anwendungen wird ein Tomcat 7 sicher mehr als ausreichend sein.

Tomcat 7 sollte in Eclipse bereits als Server eingerichtet sein. Nun starten Sie den Server:

5. Maven Cargo Plugin ausführen

In der pom.xml haben wir das Cargo Plugin integriert. Außerdme haben wir die Run Configuration eingerichtet und wenn wir nun das Projekt auf dem laufenden Tomcat Server deployen möchten müssen wir nur diese Konfiguration ausführen.
Sie sollten in der Eclipse Konsole sehen wie das Projekt gebaut wird und direkt nach dem erfolgreichen Build den Deployment Pfad in Tomcat:

6. Websocket Client

Es gibt für Websockets nicht nur auf der Server-side spezielle Voraussetzungen, sondern natürlich auch auf der Client-side. Wir werden eine einfache HTML Seite mit Javascript Code als Client verwenden. Es existieren zahlreiche Websocket Client APIs in verschiedenen Sprachen, z.B. Java, Javascript, PHP, Ruby, Python, etc.
HTML Beispiel:
<!DOCTYPE html>  
<html>  
<head>
<script src="http://code.jquery.com/jquery-1.9.1.min.js"></script>
<script src="http://code.jquery.com/jquery-migrate-1.1.1.min.js"></script>  
<script type="text/javascript">
var ws = new WebSocket("ws://localhost:8080/websocket-1.0.0/websocket");
ws.onopen = function() {
  console.log("Websocket Ready!!");
}
ws.onclose = function() {
  console.log("Websocket Closed!!");
}
ws.onerror = function() {
  console.log("Websocket Error!!");
}
ws.onmessage= function(data) {
  console.log(data.data);
  if ("Hi!" == data.data) {
    //ws.close();
  }
}
function sendMessage() {
  ws.send(2);
}

$( document ).ready( function() {
  $("a").click(function( event ) {
    sendMessage();
  });
});
</script>  
<title>WebSockets Client</title>  
</head>  
<body>
<a href="#">LINK</a>
</body></html>


Speichern Sie den obigen HTML Code in einer .html Datei auf ihrem Rechner. Rufen Sie diese Datei im Browser auf, z.B.
Öffnen Sie die Entwicklertools des Browsers. Sie sollten dann eine Meldung "Websocket Ready!!" sehen, und in der Eclipse Konsole (rechts) sehen Sie die Ausgabe aus der Methode onClose(int status) aus der Servlet Klasse.

Klicken Sie nun einmal auf "LINK" und beobachten Sie die Eclipse und Browser Konsole. Hier die Veränderungen:
Und ein weiterer Klick ergibt folgende Ausgabe:
Nun wollen wir uns mit einem zweiten Client verbinden. Dafür müssen wir nur ein neues Tab öffnen und die HTML websocket Client Datei aufrufen:
Jetzt wollen wir die Broadcast Methode unseres Servlets einmal testen. Ein Broadcast ist eine Nachricht, die an alle Clients versendet wird. In unserem Fall werden wir per Klick auf "LINK" im ersten Tab den Broadcast quasi auslösen. Die Nachricht wird dann auch im zweiten Tab zu sehen sein, weder dass wir dort eine Aktion ausgeführt haben, noch muss das Tab oder der Browser dabei im Vordergrund sein.
Außerdem sehen Sie in der Eclipse Konsole, dass nun tatsächlich 2 Verbindungen existieren.
An dieser Stelle endet das Tutorial zu HTML5 WebSockets, allerdings ist damit das Thema noch lange nicht ausgeschöpft. Sie haben aber jetzt das nötige Basiswissen, um zu erkennen, welches Potential in WebSockets steckt und wie Sie die neuen Möglichkeiten für Ihre Anwendungen nutzen können.

Zusammenfassung

Sie haben gelernt, wie man den Tomcat 7 als WebSocket Server einsetzen kann und wie Sie sich mit einem Javascript Client verbinden können. Die lokale Entwicklungsumgebung wurde mit Eclipse und dem Maven Cargo Plugin praxisorientiert aufgesetzt, sodass Sie nun alles haben, um professionell moderne HTML5 Anwendungen entwickeln zu können.

Weitere Schritte

Dieser Artikel hat das Thema WebSockets nur in einem minimalen Umfang angerissen. Es lohnt sich weiter mit den verschiedenen APIs, Frameworks und Anwendungsfällen zu beschäftigen. Am besten in der jeweils bevorzugten Technologie für Client und Server.

Weiterführende Links zu diesem Thema:

Keine Kommentare:

Kommentar veröffentlichen