Skip to content

Conversation

@decebals
Copy link
Member

@decebals decebals commented Jun 3, 2016

This is a functional POC/Draft for #275 .

I will start with a tiny demo (echo application using websocket).

First, in the Application class I added a new method with the signature:

public void addWebSocket(String path, WebSocketHandler webSocketHandler);

WebSocketHandler is an interface (functional) that contains one method

void onMessage(WebSocketContext webSocketContext, String message);

and some default methods.

To add an echo based on websocket I must write:

// add web socket
addWebSocket("/ws/echo", (webSocketContext, message) -> {
    try {
        webSocketContext.sendMessage(message);
    } catch (IOException e) {
        e.printStackTrace();
    }
});

If you need more control you can override other methods available in WebSocketHandler:

addWebSocket("/ws/echo", new WebSocketHandler() {

    @Override
    public void onMessage(WebSocketContext webSocketContext, String message) {
        System.out.println("TestWebSocket.onMessage");
        System.out.println("message = " + message);
        try {
            webSocketContext.sendMessage(message);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void onMessage(WebSocketContext webSocketContext, byte[] data, int offset, int length) {
        System.out.println("TestWebSocket.onMessage");
    }

    @Override
    public void onOpen(WebSocketContext webSocketContext) {
        System.out.println("TestWebSocket.onOpen");
    }

    @Override
    public void onClose(WebSocketContext webSocketContext, int closeCode, String message) {
        System.out.println("TestWebSocket.onClose");
    }

    @Override
    public void onTimeout(WebSocketContext webSocketContext) {
        System.out.println("TestWebSocket.onTimeout");
    }

    @Override
    public void onError(WebSocketContext webSocketContext, Throwable t) {
        System.out.println("TestWebSocket.onError");
    }

});

I use a standard Route to serve the index.html file that contains the script block with the websocket client side:

GET("/", routeContext -> {
    try {
        routeContext.send(IoUtils.toString(WebSocketApplication.class.getResourceAsStream("/index.html")));
    } catch (IOException e) {
        e.printStackTrace();
    }
});

// OR

DirectoryHandler directoryHandler = new DirectoryHandler("/", new File("src/main/resources"));
GET(directoryHandler.getUriPattern(), directoryHandler);

The content of index.html file is:

<!DOCTYPE html>
<html>
    <head>
        <title>Echo WebSocket</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width">
    </head>

    <body>
         <input id="message-box" autofocus onkeypress="send(event)"/>

        <!-- server responses get written here -->
        <div id="messages"></div>

        <!-- script to utilise the WebSocket -->
        <script type="text/javascript">

            var messages = document.getElementById("messages");

            // create a new instance of the webSocket
            var webSocket = new WebSocket("ws://localhost:8338/ws/echo");

            webSocket.onopen = function() {
                writeResponse("Connection opened!");
            };

            webSocket.onmessage = function(evt) {
                writeResponse(evt.data);
            };

            webSocket.onclose = function() {
                writeResponse("Connection closed!");
            };

            /**
             * Sends the value of the text input to the server
             */
            function send(evt) {
                // check for ENTER key pressed
                if (evt.keyCode != 13) {
                    return;
                }

                var messageBox = document.getElementById("message-box");

                // read the text from the message box and send it to server via websocket
                webSocket.send(messageBox.value);

                // clear the message box
                messageBox.value = "";
            }

            function writeResponse(text) {
                messages.innerHTML += "<br/>" + text;
            }

        </script>
    </body>
</html>

The last step is to "inject" the WebSocketFilter in the Pippo launcher:

 Pippo pippo = new Pippo() {

    /*
     * Change PippoFilter with a subclass that enables WebSocket
     */
    @Override
    protected PippoFilter createPippoFilter(Application application) {
        PippoFilter pippoFilter = new JettyWebSocketFilter();
        pippoFilter.setApplication(application);

        return pippoFilter;
    }

};

I must admit that I don't like this manual approach but for the moment is OK. Probably the correct approach is to detect if the application contains websocket routes and only in this situation we must change automatically the PippoFilter with WebSocketFilter.

This PR comes with:

  • the new websocket package in pippo-core (contains only three tiny interface and an abstract class)
  • the websocket support for JettyServer (pippo-jetty module)

Below I will add some implementation details.
First, I think it's easy to add support for Undertow and Tomcat. We must implement WebSocketFilter, WebSocketConnection and WebSocketProcessor for each server.

I added an useful AbstractWebSocketFilter that contains common logic for all websocket filters.
Were two option related to how to "inject" the websocket in Server:

  • create WebSocketFilter that extends PippoFilter
  • modify each WebServer implementation

I chose the first variant because I can use the new created filter in web.xml. With variant two, the websocket support is available only for applications that use Pippo with an embedded web server.

TODO:

  • inject WebSocketFilter automatically in Pippo launcher
  • use uriPattern as first parameter in Application.addWebSocket; now it's not possible to pass query parameters to websocket; the path parameter is static. My idea to resolve this task is to extract from DefaultRouter the part that read the parameters from a Request in a separate class - the DefaultRouter class s a little big so Single Responsability pattern for this class is welcome
  • add WebSocketContext.broadcastMessage(), for this we must register/unregister all WebSocketConnection(via onOpen and onClose methods) in a list

Any advice, question is welcome.

@coveralls
Copy link

Coverage Status

Coverage decreased (-0.3%) to 10.759% when pulling ce09af1 on web_socket into 04bc007 on master.

@coveralls
Copy link

Coverage Status

Coverage decreased (-0.3%) to 10.726% when pulling 76ace52 on web_socket into 04bc007 on master.

@gitblit
Copy link
Collaborator

gitblit commented Jun 7, 2016

I haven't looked at the code yet but your write up sounds exciting!

@decebals
Copy link
Member Author

decebals commented Jun 7, 2016

  • inject WebSocketFilter automatically in Pippo launcher

Here is a problem.
My idea was to add a new method in Application:

public boolean hasWebSocketHandlers() {
    return !webSocketHandlers.isEmpty();
} 

Then in Pippo we can modify the content on createPippoFilter with the new code:

protected PippoFilter createPippoFilter(Application application) {
   PippoFilter pippoFilter; 
   if (application.hasWebSocketHandlers()) {
        // pippoFilter = new ABCWebSocketFilter();
    } else {
        // pippoFilter = new PippoFilter();
    }
    pippoFilter.setApplication(application);

    return pippoFilter;
}

The problem is that the websocket is added on Application.onInit() method but the Filter is already created in that point, it is to late to change something.

I will try to find another solution.

@decebals
Copy link
Member Author

I will submit another PR, from another branch (web_socket_2) because the current branch is difficult to merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants