Gangmax Blog

自由之思想,独立之精神

WebSocket

| Comments

WebSocket is a computer communications protocol, providing full-duplex communication channels over a single TCP connection. It is designed to be implemented in web browsers and web servers, but it can be used by any client or server application.

Protocol

To establish a WebSocket connection, the client sends a WebSocket handshake request, for which the server returns a WebSocket handshake response, as shown in the example below.

1
2
3
4
5
6
7
8
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com

Server response:

1
2
3
4
5
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat

Notes:

  1. The ”upgrade header(the “Upgrade”/”Connection” items) is used in the request/response to set up a connection;

  2. The HTTP status code “101 Switching Protocols” is used in the response;

  3. The client sends a “Sec-WebSocket-Key” header containing base64-encoded random bytes, and the server replies with a hash of the key in the “Sec-WebSocket-Accept” header. The hashing function appends the fixed string ”258EAFA5-E914-47DA-95CA-C5AB0DC85B11” (a GUID) to the value from “Sec-WebSocket-Key” header (which is not decoded from base64), applies the SHA-1 hashing function, and encodes the result using base64(from here). This logic can be verified by the following commands with the example values in the above request/response:

1
2
3
# The "-n" argument of "echo" removes the trailing newline:
~> echo -n "x3JJHMbDL1EzLkh9GBhXDw==258EAFA5-E914-47DA-95CA-C5AB0DC85B11" | openssl dgst -binary -sha1 | openssl base64
HSmrc0sMlYUkAGmm5OPpG2HaGWk=

Server Side

In the Java world, you can use ”Tyrus” to implement the WebSocket server side. The offical document has detail description about how to do it.

If you are adding WebSocket feature in your WAR project, add the following dependencies in your “pom.xml”:

pom.xml
1
2
3
4
5
<dependency>
    <groupId>javax.websocket</groupId>
    <artifactId>javax.websocket-api</artifactId>
    <version>1.1</version>
</dependency>   

This is used to import the annotations below to create the WebSocket server side code. Since “JSR 356, Java API for WebSocket” is part of Java EE 7, any application server supports Java EE 7 should already have this dependency. For web containers, Tomcat supports the Java WebSocket 1.1 API defined by JSR-356 from version 7.0. So I speculate in a war deployed in such environment, marking the dependency as “<scope>provided</scope>” is enough.

Then you need to create a Java class with the annotations which aims to handle the WebSocket request as the WebSocket server side(from here):

EchoServer.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import java.io.IOException;

import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

/** 
 * @ServerEndpoint gives the relative name for the end point
 * This will be accessed via ws://localhost:8080/EchoChamber/echo
 * Where "localhost" is the address of the host,
 * "EchoChamber" is the name of the package
 * and "echo" is the address to access this class from the server
 */
@ServerEndpoint("/echo")
public class EchoServer {
    /**
     * @OnOpen allows us to intercept the creation of a new session.
     * The session class allows us to send data to the user.
     * In the method onOpen, we'll let the user know that the handshake was 
     * successful.
     */
    @OnOpen
    public void onOpen(Session session){
        System.out.println(session.getId() + " has opened a connection");
        try {
            session.getBasicRemote().sendText("Connection Established");
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }

    /**
     * When a user sends a message to the server, this method will intercept the message
     * and allow us to react to it. For now the message is read as a String.
     */
    @OnMessage
    public void onMessage(String message, Session session){
        System.out.println("Message from " + session.getId() + ": " + message);
        try {
            session.getBasicRemote().sendText(message);
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }

    /**
     * The user closes the connection.
     * 
     * Note: you can't send messages to the client from this method
     */
    @OnClose
    public void onClose(Session session){
        System.out.println("Session " +session.getId()+" has ended");
    }
}

Note the “javax.websocket.*” annotations which are used to mark the Java class as WebSocket server side implementation.

Besides the annotation implementation, another “MessageHandlers” programmatic endpoint implementation can by used:

TestEndPoint.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import javax.websocket.Endpoint;
import javax.websocket.EndpointConfig;
import javax.websocket.MessageHandler;
import javax.websocket.Session;
import javax.websocket.CloseReason;

public class TestEndPoint extends Endpoint {

    @Override
    public void onOpen(Session session, EndpointConfig EndpointConfig) {
        session.addMessageHandler(new MessageHandler.Whole<String>() {
            @Override
            public void onMessage(String message) {
                System.out.println("Received message: "+message);
            }
        });
    }

    @Override
    public void onClose(Session session, CloseReason closeReason) {
        super.onClose(session, closeReason);
    }

    @Override
    public void onError(Session session, Throwable thr) {
        super.onError(session, thr);
    }
}

From here: Deploying WebSocket endpoints can be done in two ways. Either deploying via putting the endpoint in the WAR file, or using the ServerContainer methods to deploy the programmatic endpoint in the deployment phase.

  1. For deploying Endpoints as a WAR file:

    The classes that are scanned for in WAR are the following ones:

    1. Classes that implement the javax.websocket.ServerApplicationConfig.

    2. Classes annotated with javax.websocket.server.ServerEndpoint.

    3. Classes that extend javax.websocket.Endpoint.

    More details can be found from the official document. This way is easy and I think it’s the way that most developers do.

  2. Deploying endpoints via “javax.websocket.server.ServerContainer”

Endpoints may be deployed using javax.websocket.server.ServerContainer during the application initialization phase. For websocket enabled web containers, developers may obtain a reference to the ServerContainer instance by retrieving it as an attribute named javax.websocket.server.ServerContainer on the ServletContext, see the following example for annotated endpoint:

MyServletContextListenerAnnotated.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
import javax.websocket.DeploymentException;
import javax.websocket.OnMessage;
import javax.websocket.server.ServerContainer;
import javax.websocket.server.ServerEndpoint;

@WebListener
@ServerEndpoint("/annotated")
public class MyServletContextListenerAnnotated implements ServletContextListener {

    @Override
    public void contextInitialized(ServletContextEvent servletContextEvent) {
        final ServerContainer serverContainer = (ServerContainer) servletContextEvent.getServletContext()
                                                    .getAttribute("javax.websocket.server.ServerContainer");

        try {
            serverContainer.addEndpoint(MyServletContextListenerAnnotated.class);
        } catch (DeploymentException e) {
            e.printStackTrace();
        }
    }

    @OnMessage
    public String onMessage(String message) {
        return message;
    }

    @Override
    public void contextDestroyed(ServletContextEvent servletContextEvent) {
    }
}

Client Side

It seems there are many ways to implement the client side such as using ”Angular Websocket”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
  <script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.3.15/angular.min.js"></script>
  <script src="bower_components/angular-websocket/angular-websocket.js"></script>
  <section ng-controller="SomeController">
    <ul ng-repeat="data in MyData.collection track by $index" >
      <li>  </li>
    </ul>
  </section>
  <script>
    angular.module('YOUR_APP', [
      'ngWebSocket' // you may also use 'angular-websocket' if you prefer
    ])
    //                          WebSocket works as well
    .factory('MyData', function($websocket) {
      // Open a WebSocket connection
      var dataStream = $websocket('ws://website.com/data');

      var collection = [];

      dataStream.onMessage(function(message) {
        collection.push(JSON.parse(message.data));
      });

      var methods = {
        collection: collection,
        get: function() {
          dataStream.send(JSON.stringify({ action: 'get' }));
        }
      };

      return methods;
    })
    .controller('SomeController', function ($scope, MyData) {
      $scope.MyData = MyData;
    });
  </script>

Finally, here is a very good article about this topic: ”Java EE 7 and WebSocket API for Java (JSR 356) with AngularJS on WildFly”.

Comments