Websockets for default ZK client-server communications

CE
PE

In addition to manual websocket use, ZK can be configured to use a websocket channel as a replacement for the default /zkau request and response cycles used to send events and updates between the client and the server.

In WEB-INF/zk.xml, add following lines to enable WebSocket connection:

<listener>
<listener-class>org.zkoss.zkmax.au.websocket.WebSocketWebAppInit</listener-class>
</listener>

To change the update URL, you could also add the following lines into WEB-INF/zk.xml. Optional: If not specified, “/zkwm” will be used by default.

<library-property>
<name>org.zkoss.zkmax.au.websocket.WebSocketEndPoint.urlPattern</name>
<value>/yourApp</value>
</library-property>

When a WebSocket connection is enabled, we’ll use WebSocketServerPush by default when server-push started. Note that we cannot guarantee the accessing of the information provided by http requests when WebSocket connection is enabled.

Manual use of Websockets: Employment/Purpose

since 8.0.0 ZK has supported a way to share the application data between a ZK application and a websocket application within the same session. Here we demonstrate how to use the org.zkoss.zk.ui.sys.Storage in a desktop scope to share the application data through the websocket channel.

Example

Websocket Server

@ServerEndpoint(value ="/echo/",
    configurator = ZKWebSocket.class)
public class EchoServer {
    @OnOpen
    public void onOpen(Session session, EndpointConfig config) {
        //since zk 8.6.4
        ZKWebSocket.initZkDesktop(session, config);
    }

    @OnMessage
    public void onMessage(String message, Session session){
        Storage<Integer> storage = ZKWebSocket.getDesktopStorage(session);
        if ("receive".equals(message)) {
            Integer count = storage.getItem("count");
            try {
                session.getBasicRemote().sendText("Received..." + count);
            } catch (Exception e) {
                e.printStackTrace();
            }
        } else {
            try {
                storage.setItem("count", Integer.parseInt(message));
                session.getBasicRemote().sendText("Sent..." + message);
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
    }
    @OnClose
    public void onClose(Session session){
    }

}
// Create a new instance of the websocket
var webSocket = new WebSocket("ws://localhost:8080/zkwebsocket/echo/?dtid=" + zk.$('$win').desktop.uuid);

ZK Application

MVVM Example

<window id="win" apply="org.zkoss.bind.BindComposer"
            viewModel="@id('vm') @init('org.zkoss.foo.ZKWebSocketViewModel')">
        <groupbox title="ZK">
            <hlayout>count: <label value="@load(vm.count)"/></hlayout>
            <button label="add" onClick="@command('cmd')"/>
        </groupbox>
</window>
@ToServerCommand("update")
public class ZKWebSocketViewModel {

    private Integer count;

    @Init
    public void init(@ContextParam(ContextType.DESKTOP) Desktop desktop) {
        count = 100;
        syncToStorage(desktop);
    }

    @Command
    @NotifyChange("count")
    public void cmd(@ContextParam(ContextType.DESKTOP) Desktop desktop) {
        count++;
        syncToStorage(desktop);
    }

    @Command("update")
    @NotifyChange("count")
    public void doUpdate(@ContextParam(ContextType.DESKTOP) Desktop desktop) {
        count = desktop.<Integer>getStorage().getItem("count");
    }

    private void syncToStorage(Desktop desktop) {
        Storage<Integer> desktopStorage = desktop.getStorage();
        desktopStorage.setItem("count", count);
    }
    public Integer getCount() {
        return count;
    }
}
  • Line 22 and 26: we can receive the data storage from the desktop object to share or update the application data into it, so that the websocket echo server can use or get the latest data from it or vice versa.

MVC Example

<window id="win" apply="org.zkoss.foo.ZKWebSocketComposer">
        <groupbox title="ZK">
            <hlayout>count: <label id="label" /></hlayout>
            <button id="btn" label="add"/>
        </groupbox>
</window>
public class ZKWebSocketComposer extends SelectorComposer<Window> {
    @Wire Label label;
    @Wire Button btn;
    private Integer count;

    @Override public void doAfterCompose(Window comp) throws Exception {
        super.doAfterCompose(comp);
        count = 100;
        label.setValue("100");
        syncToStorage();
    }

    @Listen("onClick = #btn")
    public void doClick() {
        count++;
        label.setValue(String.valueOf(count));
        syncToStorage();
    }

    private void syncToStorage() {
        getSelf().getDesktop().getStorage().setItem("count", count);
    }

    @Command // this annotation is under the package of org.zkoss.zk.ui.annotation
    public void update() {
        count = getSelf().getDesktop().<Integer>getStorage().getItem("count");
        label.setValue(String.valueOf(count));
    }
}
  • Line 21 and 26, we can receive the data storage from the desktop object to share or update the application data into it, so that the websocket echo server can use or get the latest data from it or vice versa.
  • Line 24: org.zkoss.zk.ui.annotation.Command annotation has been added since the release of ZK 8.0.0, and it is used to receive a notification from client to server. For more details, please take a look at the #Communication section.

Communication

From Websocket server to ZK application

MVVM Example

Here is the MVVM way to send a command from client to server.

// Create a new instance of the websocket
var webSocket = new WebSocket("ws://localhost:8080/zkwebsocket/echo/?dtid=" + zk.$('$win').desktop.uuid);

// receive a message from websocket, and notify ZK application to update the component data.
webSocket.onmessage = function(event) {
    zkbind.$('$win').command('update'); // the update command has already declared in ZKWebSocketViewModel.java
};

MVC Example

Here is the MVC way to send a command from client to server.

// Create a new instance of the websocket
var webSocket = new WebSocket("ws://localhost:8080/zkwebsocket/echo/?dtid=" + zk.$('$win').desktop.uuid);

// receive a message from websocket, and notify ZK application to update the component data.
webSocket.onmessage = function(event) {
    zkservice.$('$win').command('update'); // the update command has already declared in ZKWebSocketComposer.java
};

Command Parameter Converter

MVVM Example

When a user triggers a command with some data from client to server, the data should be in a Map (or says Object) type. For example,

    zkbind.$('$win').command('update', {foo: 'myfoo', bar: {title: 'myBarTitle'}});

In the Java code

    public static class Bar {
        private String title;
        public void setTitle(String title) { this.title = title; }
        public String getTitle() { return title; }
    }
    @Command("update")
    @NotifyChange("count")
    public void doUpdate(@ContextParam(ContextType.DESKTOP) Desktop desktop, @BindingParam("foo") String myfoo, @BindingParam("bar") Bar mybar) {
        count = desktop.<Integer>getStorage().getItem("count");
    }

As you can see above, the data will automatically be converted into a specific object type according to the method declaration.

Note: developer can implement a custom org.zkoss.bind.Converter and specify it into the ZK library properties.

MVC Example

When a user triggers a command with some data from client to server, the data should be in an array type in order. For example,

    zkservice.$('$win').command('update', [{foo: "myfoo"}, {bar: "mybar"}]); // the arguments should be in order within an array.

In the Java code

...
    public static class MyFoo {
        private String foo;
        public void setFoo(String foo) { this.foo = foo;}
        public String getFoo() { return this.foo;}
    }

    public static class MyBar {
        // omitted
    }

    @Command
    public void update(MyFoo foo, MyBar bar) {
    }
}

As you can see above, the data will automatically be converted into a specific object type according to the method declaration.

Note: developers can implement a custom org.zkoss.util.Converter and specify it into the ZK library properties.