een lange titel

Websocket met het Spark Framework

Met het Spark micro framework kan je ook Websocket servers maken.

In dit bericht wordt getoond hoe je met Spark framework een Websocket server kan maken. Het Spark framework ondersteunt Websockets en de implementatie steunt op die van Jetty. De volgende link is nuttig om de API te begrijpen.

Het voorbeeld is in Java geschreven en wordt gebouwd met Maven. Er is ook een Dockerfile voorzien zodat je ziet hoe je het voorbeeld ook voor Docker kan bouwen. Dit kan nuttig zijn als je van plan bent om de serverapp uit te baten bij een cloudprovider.

Het voorbeeld bestaat uit drie Java klassen:

  • Main
  • Spel
  • Speler

De Main klasse staat hieronder en is verantwoordelijk voor het opstarten van de server.

package be.tcdiepenbeek;

import static spark.Spark.*;

public class Main 
{
   public static void main(String[] args)
   {
      port(5001);
      webSocket("/echo", Spel.class);
      get("/hello", (req, res) -> "Hello World");
   }
}

Hierboven zie je dat er gewacht op clients die zich aanmelden bij poort 5001. Via het pad /echo kan een verbinding gemaakt worden. De Spel klasse zorgt voor de afhandeling van binnenkomende verbindingen. De get() regel toont dat je ook statische pagina’s kan afhandelen.

In de Spel klasse moet je met @WebSocket aangeven dat deze klasse een Websocketcontroller is.

package be.tcdiepenbeek;

import java.io.IOException;
import java.util.HashMap;

import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.annotations.*;

@WebSocket
public class Spel
{
   HashMap<Session, Speler> spelers;

   public Spel()
   {
      spelers = new HashMap<Session, Speler>();
   }

   @OnWebSocketConnect
   public void connected(Session session)
   {
      System.out.println("connected");
      
      int thisHash = System.identityHashCode(this);
      int sessionHash = System.identityHashCode(session);
      System.out.printf("   this:   %x\n", thisHash);
      System.out.printf("   session:%x\n ", sessionHash);
 
      spelers.put(session, new Speler(this, session));
      System.out.println("   #spelers " + spelers.size());
   }

   @OnWebSocketClose
   public void closed(Session session, int statusCode, String reason)
   {
      System.out.println("closed");
      int thisHash = System.identityHashCode(this);
      int sessionHash = System.identityHashCode(session);
      System.out.printf("   this:   %x\n", thisHash);
      System.out.printf("   session:%x\n ", sessionHash);

      spelers.remove(session);
      System.out.println("   #spelers " + spelers.size());
   }

   @OnWebSocketMessage
   public void message(Session session, String msg) throws IOException
   {
      System.out.println("message " + msg);   // Print message

      int thisHash = System.identityHashCode(this);
      int sessionHash = System.identityHashCode(session);
      System.out.printf("   this:   %x\n", thisHash);
      System.out.printf("   session:%x\n ", sessionHash);

      Speler speler = spelers.get(session);
      if (speler != null)
      {
         speler.message(msg);
      }
   }
}

Voor elke binnenkomende verbindig wordt de connected() methode gestart. Ook hier worden annotaties gebruikt om aan te geven welke methoden wanneer moeten gestart worden. De closed() methode wordt opgeroepen als een verbinding verbroken wordt en bij elk binnenkomend bericht door de client vertstuurd wordt message() gestart. Een belangrijke parameter bij al deze methoden is Session session. Hiermee wordt de verbinding ge├»dentificeerd. Al deze sessies worden bijgehouden in een HashMap. Met elke verbinding wordt een Speler object geassoci├źerd. De afhandeling van binnenkomende berichten wordt gedelegeerd naar de betrokken Speler. Dit patroon heeft als voordeel dat binnen de Speler dan datamembers kunnen bijgehouden worden die de toestand van de speler voorstellen.

Hierboben zie je dan ook dat Spel bij een binnenkomend bericht op zoek gaat naar de betrokken Speler en aan deze speler het bericht doorgeeft.

Dit is de broncod van de Speler:

package be.tcdiepenbeek;

import java.io.IOException;
import org.eclipse.jetty.websocket.api.Session;

public class Speler
{
   private Spel    spel;
   private Session session;

   public Speler(Spel sp, Session s)
   {
      spel    = sp;
      session = s;
   }

   public void message(String msg) throws IOException
   {
      System.out.println("Speler message " + msg);   // Print message
      session.getRemote().sendString("respons " + msg); // and send it back
   }
}

In de message() methode kan je de specifieke afhandeling van deze speler verder uitschrijven. Momenteel wordt het ontvangen bericht gewoon teruggestuurd.

Deze 3 klassen vormen een mooie skeletapplicatie waarop verder gebouwd kan worden. Als client kan je een webpagina in Javascript inzetten. Uiteraard is een Websocket client ook mogelijk in Android1.

Het voorbeeld wordt met Maven gebouwd en dit is de pom.xml die het project beschrijft.

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>be.tcdiepenbeek</groupId>
  <artifactId>webapp</artifactId>
  <packaging>jar</packaging>
  <version>1.0</version>
  <name>webapp</name>
  <url>http://maven.apache.org</url>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>com.sparkjava</groupId>
      <artifactId>spark-core</artifactId>
      <version>2.3</version>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-jar-plugin</artifactId>
        <version>2.4</version>
        <configuration>
          <finalName>webapp</finalName>
          <archive>
            <manifest>
              <addClasspath>true</addClasspath>
              <mainClass>be.tcdiepenbeek.Main</mainClass>
              <classpathPrefix>dependency-jars/</classpathPrefix>
            </manifest>
          </archive>
        </configuration>
      </plugin>

      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.3</version>
        <configuration>
          <source>1.8</source>
          <target>1.8</target>
        </configuration>
      </plugin>

      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-assembly-plugin</artifactId>
        <executions>
          <execution>
            <goals>
              <goal>attached</goal>
            </goals>
            <phase>package</phase>
            <configuration>
              <finalName>webapp</finalName>
              <descriptorRefs>
                <descriptorRef>jar-with-dependencies</descriptorRef>
              </descriptorRefs>
              <archive>
                <manifest>
                  <mainClass>be.tcdiepenbeek.Main</mainClass>
                </manifest>
              </archive>
            </configuration>
          </execution>
        </executions>
      </plugin>

    </plugins>
  </build>
</project>

Deze pom.xml is in staat om het .jar archief te bouwen. Gemakkelijkheidshalve is hier nog een Makefile die alle mvn commando’s bijhoudt.

package:
	mvn compile package

clean:
	mvn clean

run:
	java -jar target/webapp-jar-with-dependencies.jar


dockerbuild:
	docker build -t lrutten/sparkwebapp .


dockerrun:
	docker run -d -p 5001:5001 lrutten/sparkwebapp

Zoals je ziet, is er ook een Dockerfile voorzien zodat de applicatie in Docker kan getest worden. Deze Dockerfile vertrekt van een Java 8 image en voert de compilatie volledig uit.

FROM java:8 

# Install maven
RUN apt-get update
RUN apt-get install -y maven

# Prepare by downloading dependencies
ADD pom.xml /code/pom.xml
RUN ["mvn", "dependency:resolve"]
RUN ["mvn", "verify"]

# Adding source, compile and package into a fat jar
ADD src /code/src
RUN ["mvn", "package"]

EXPOSE 5001
CMD ["/usr/lib/jvm/java-8-openjdk-amd64/bin/java", "-jar", "target/webapp-jar-with-dependencies.jar"]

De applicatie is dan toegankelijk via poort 5001.


  1. Dit is al eens getest. [return]