Introduction
This article describe how to use WebSocket to do ServerPush in ZK MVVM, modified from previous article
ZK: Server Push with WebSocket (
http://ben-bai.blogspot.tw/2013/07/zk-server-push-with-websocket.html) with the desktop based implementation from
ZK AURequest with WebSocket (
http://ben-bai.blogspot.tw/2013/09/zk-aurequest-with-websocket.html).
It integrates WebSocket in ZK for ServerPush in more general way and has better performance (compared with previous component based version).
Environment: Tomcat 7.0.42, ZK 6.5.2
NOTE: This is just a POC with some customized components.
Pre-request
ZK AURequest with WebSocket
http://ben-bai.blogspot.tw/2013/09/zk-aurequest-with-websocket.html
ZK Create Helper Tag to Make Programmer Happier
http://ben-bai.blogspot.tw/2013/09/zkcreatehelpertag-to.html
ZK Basic MVVM Pattern
http://ben-bai.blogspot.tw/2012/12/zk-basic-mvvm-pattern.html
Result
View demo on line
http://screencast.com/t/zp1rcVhYa
Program
There are lots of source files, will only post code for some of them and post link for others.
index.zul
Entry page
https://github.com/benbai123/ZK_Practice/blob/master/Integrate/WebSocket/ServerPushWithWebSocket/WebContent/index.zul
push_to_all.zul
Push value to all desktop (i.e., Application scope) with button click.
<zk>
<!-- Tested with ZK 6.5.2 -->
<window apply="org.zkoss.bind.BindComposer"
viewModel="@id('vm') @init('test.TestPushToAllVM')">
Counter:
<!-- intbox, listen to positive integer in the begining -->
<intbox readonly="true" />
<contextBinding field="value" context="@load(vm.counter)" />
listen to: <label value="@load(vm.counter)" />
<div />
inverse Counter:
<!-- intbox, listen to negative integer in the begining -->
<intbox id="inverseCounter" readonly="true" />
<contextBinding field="value" context="@load(vm.inverseCounter)" />
listen to: <label value="@load(vm.inverseCounter)" />
<div />
<!-- update value of positive/negative integer -->
<button id="updateCounterBtn" label="updaet counter and negative counter"
onClick="@command('updateCounter')" />
<!-- switch listening context of the two intboxess above -->
<button label="switch context" id="switchBtn"
onClick="@command('switchCounter')" />
</window>
</zk>
test.TestPushToAllVM.java
VM for push_to_all.zul
package test;
import impl.serverpush.ServerPushUtil;
import java.util.concurrent.atomic.AtomicInteger;
import org.zkoss.bind.annotation.Command;
import org.zkoss.bind.annotation.NotifyChange;
import org.zkoss.zk.ui.Desktop;
import org.zkoss.zk.ui.Executions;
/** Tested with ZK 6.5.2
*
* @author benbai123
*
*/
public class TestPushToAllVM {
/** context listened by intboxes */
private String _counter = "positive";
private String _inverseCounter = "negative";
/** desktop of this VM */
private Desktop targetDesktop;
/** counter used to update positive/negative integer */
private static AtomicInteger _cntCounter = new AtomicInteger(0);
// getters
public String getCounter () {
return _counter;
}
public String getInverseCounter () {
return _inverseCounter;
}
/**
* update value to context 'positive' and 'negative' via
* WebSocket
*
* All components that listen to these context will be updated
*/
@Command
public void updateCounter () {
int val = _cntCounter.incrementAndGet();
push(val, "positive", false);
push(-1*val, "negative", false);
}
/** switch listening components of 'positive' and 'negative' context
* current desktop only
*
*/
@Command
@NotifyChange({"counter", "inverseCounter"})
public void switchCounter () {
int val = _cntCounter.get();
// push to opposite (for self desktop) before switch context
push(val, "negative", true);
push(-1*val, "positive", true);
String tmp = _counter;
_counter = _inverseCounter;
_inverseCounter = tmp;
}
/** push value
*
* @param val
* @param context
*/
private void push (Object val, String context, boolean desktopOnly) {
try {
if (targetDesktop == null) {
targetDesktop = Executions.getCurrent().getDesktop();
}
if (desktopOnly) {
// push to current desktop only
ServerPushUtil.pushVlaue(val, context, targetDesktop);
} else {
// push to all desktop
ServerPushUtil.pushVlaue(val, context);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
automatic_push.zul
Update value with java thread, Desktop scope.
<zk>
<!-- Tested with ZK 6.5.2 -->
<window apply="org.zkoss.bind.BindComposer"
viewModel="@id('vm') @init('test.TestAutoPushVM')">
Self:
<!-- intbox that will be updated -->
<intbox readonly="true" />
<contextBinding field="value" context="@load(vm.task)" />
<!-- start update -->
<button label="start" onClick="@command('start')" />
<!-- stop update -->
<button label="stop" onClick="@command('stop')" />
<!-- textbox that bind rows/cols to integer -->
<textbox />
<contextBinding id="cbd" field="@load(vm.prop)" context="@load(vm.task)" />
<button label="switch field (rows/cols)" onClick="@command('switchRowCol')" />
</window>
</zk>
test.TestAutoPushVM.java
VM for automatic_push.zul
package test;
import impl.serverpush.ServerPushUtil;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicInteger;
import org.zkoss.bind.annotation.Command;
import org.zkoss.bind.annotation.NotifyChange;
import org.zkoss.zk.ui.Desktop;
import org.zkoss.zk.ui.Executions;
/** Tested with ZK 6.5.2
*
* @author benbai123
*
*/
public class TestAutoPushVM {
/** Counter used to update integer */
private AtomicInteger _cnt = new AtomicInteger(0);
/** ServerPush task timer */
private Timer timer;
/** Desktop of this VM */
private Desktop targetDesktop;
/** Binded field of textbox */
private String _prop = "rows";
// getters
public String getTask () {
return "timerTask";
}
public String getProp () {
return _prop;
}
/**
* start server push with WebSocket for
* specific context "timerTask"
*
* current desktop only
*/
@Command
public void start () {
if (timer == null) {
// update once immediately when first time start
if (_cnt.get() == 0) {
push(_cnt.incrementAndGet(), "timerTask");
}
timer = new Timer();
timer.schedule(getTimerTask(), 1000, 1000);
}
}
/**
* stop server push with WebSocket for
* specific context "timerTask"
*
* current desktop only
*/
@Command
public void stop () {
if (timer != null) {
timer.cancel();
timer = null;
}
}
@Command
@NotifyChange("prop")
public void switchRowCol () {
if ("rows".equals(_prop)) {
_prop = "cols";
} else {
_prop = "rows";
}
}
// task to be scheduled to update context "timerTask" every second
private TimerTask getTimerTask () {
return new TimerTask() {
public void run () {
push(_cnt.incrementAndGet(), "timerTask");
}
};
}
/** push value to specific context
*
* @param val value to push
* @param context context to push
*/
private void push (Object val, String context) {
try {
if (targetDesktop == null) {
targetDesktop = Executions.getCurrent().getDesktop();
}
// push to current desktop only
ServerPushUtil.pushVlaue(val, context, targetDesktop);
} catch (Exception e) {
e.printStackTrace();
}
}
}
components.IWebSocketEnhancedComponent.java
Copied from AURequestWithWebSocket, completely the same.
https://github.com/benbai123/ZK_Practice/blob/master/Integrate/WebSocket/ServerPushWithWebSocket/src/components/IWebSocketEnhancedComponent.java
components.Intbox.java
Copied from AURequestWithWebSocket, change _useWebSocketAU default to true and override smartUpdate instead of setter.
https://github.com/benbai123/ZK_Practice/blob/master/Integrate/WebSocket/ServerPushWithWebSocket/src/components/Intbox.java
components.Textbox.java
Created for this POC, override smartUpdate instead of override setters separately.
Almost the same with Intbox (actually completely the same after serialVersionUID
https://github.com/benbai123/ZK_Practice/blob/master/Integrate/WebSocket/ServerPushWithWebSocket/src/components/Textbox.java
components.serverpush.IWebSocketServerPushEnhancedComponent.java
Define a "WebSocket Enhanced Component -- ServerPush type" for ServerPush pattern
package components.serverpush;
import impl.serverpush.Binding;
import components.IWebSocketEnhancedComponent;
/** Tested with ZK 6.5.2<br>
*
* Define a "WebSocket Enhanced Component -- ServerPush type"
* for ServerPush pattern
*
* @author benbai123
*
*/
public interface IWebSocketServerPushEnhancedComponent extends IWebSocketEnhancedComponent {
/** Add Binding for specific field/context pair
*
* The added Binding will be stored to a Binding List of a Component
*
* @param field field to bind
* @param context context to bind with field
* @return the Added Binding
*/
public Binding addSocketContextBinding (String field, String context);
/** Remove Binding for specific field/context pair
*
* Remove it from Binding List of a Component
*
* @param field field of field/context pair to remove
* @param context context of field/context pair to remove
*/
public void removeSocketContextBinding (String field, String context);
}
components.serverpush.Intbox.java
Created for this POC, implements IWebSocketServerPushEnhancedComponent to support WebSocket ServerPush Almost the same with Textbox (completely the same after serialVersionUID)
package components.serverpush;
import impl.serverpush.Binding;
import impl.serverpush.ServerPushUtil;
/** Tested with ZK 6.5.2<br>
*
* Created for this POC, implements IWebSocketServerPushEnhancedComponent to
* support WebSocket ServerPush
*
* Almost the same with Textbox (completely the same after serialVersionUID)
*
* @author benbai123
*
*/
public class Intbox extends components.Intbox implements IWebSocketServerPushEnhancedComponent {
private static final long serialVersionUID = -3277498174057967067L;
@Override
public Binding addSocketContextBinding(String field, String context) {
return ServerPushUtil.addContextBinding(this, field, context);
}
@Override
public void removeSocketContextBinding(String field, String context) {
ServerPushUtil.removeContextBinding(this, field, context);
}
}
components.serverpush.Textbox.java
Created for this POC, implements IWebSocketServerPushEnhancedComponent to support WebSocket ServerPush Almost the same with Intbox (completely the same after serialVersionUID)
package components.serverpush;
import impl.serverpush.Binding;
import impl.serverpush.ServerPushUtil;
/** Tested with ZK 6.5.2<br>
*
* Created for this POC, implements IWebSocketServerPushEnhancedComponent to
* support WebSocket ServerPush
*
* Almost the same with Intbox (completely the same after serialVersionUID)
*
* @author benbai123
*
*/
public class Textbox extends components.Textbox implements IWebSocketServerPushEnhancedComponent {
private static final long serialVersionUID = 2354643632056197764L;
@Override
public Binding addSocketContextBinding(String field, String context) {
return ServerPushUtil.addContextBinding(this, field, context);
}
@Override
public void removeSocketContextBinding(String field, String context) {
ServerPushUtil.removeContextBinding(this, field, context);
}
}
components.helper.ContextBinding.java
Helper component to help a component to bind field with ServerPush context
package components.helper;
import impl.serverpush.Binding;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.event.Event;
import org.zkoss.zk.ui.event.EventListener;
import org.zkoss.zk.ui.event.Events;
import org.zkoss.zul.Div;
import components.serverpush.IWebSocketServerPushEnhancedComponent;
/** Tested with ZK 6.5.2
*
* Helper component to help a component to bind field with ServerPush context
*
* @author benbai123
*
*/
public class ContextBinding extends Div {
private static final long serialVersionUID = -7156149643515776677L;
/** field to bind with context (required) */
private String _field = "";
/** context to bind with field (required) */
private String _context = "";
/** specified id of target component (optional) */
private String _target;
/** used to store current binding (relatively old) for changing field/context */
private Binding _oldBinding = null;
/** found target */
private IWebSocketServerPushEnhancedComponent _foundTarget;
@SuppressWarnings({ "unchecked", "rawtypes" })
public ContextBinding () {
// update target url while created
addEventListener(Events.ON_CREATE, new EventListener () {
public void onEvent (Event event) {
updateTargetBinding();
}
});
// do not output any html
setWidgetOverride ("redraw", "function (out) {}");
}
// setters
public void setTarget (String target) {
_target = target;
}
public void setField (String field) {
if (field == null) {
field = "";
}
if (!field.equals(_field)) {
_field = field;
updateTargetBinding();
}
}
public void setContext (String context) {
if (context == null) {
context = "";
}
if (!context.equals(_context)) {
_context = context;
updateTargetBinding();
}
}
/** Update Binding of target Component when field/context is changed
*
*/
private void updateTargetBinding () {
if (!_field.isEmpty() && !_context.isEmpty()) {
_foundTarget = findTarget();
if (_foundTarget != null) {
// remove old binding if exists
if (_oldBinding != null) {
if (_field.equals(_oldBinding.getField())
&& _context.equals(_oldBinding.getContext())) {
// already binded, do nothing
return;
}
_foundTarget.removeSocketContextBinding(_oldBinding.getField(), _oldBinding.getContext());
}
// add new binding and store it to _oldBinding
_oldBinding = _foundTarget.addSocketContextBinding(_field, _context);
}
}
}
/* package */ IWebSocketServerPushEnhancedComponent getFoundTarget () {
return _foundTarget;
}
/** Try to find target to update
* The order to try: <br>
* 1. Try to find fellow under the same space owner with specified "target" attribute.<br>
* 2. Try to find whether previous sibling is IWebSocketServerPushEnhancedComponent.<br>
* 3. Try to find whether previous sibling is ContextBinding and already found a target.<br>
* 4. Try to find whether parent is IWebSocketServerPushEnhancedComponent.<br>
*
* This way it can work without target attribute in most cases.
* @return IWebSocketServerPushEnhancedComponent if any
*/
/* package */ IWebSocketServerPushEnhancedComponent findTarget () {
Component comp;
Component previous = getPreviousSibling();
// Try to find fellow under the same space owner with specified "target" attribute.
if (_target != null && !_target.isEmpty()) {
comp = getSpaceOwner().getFellowIfAny(_target);
if (comp != null
&& (comp instanceof IWebSocketServerPushEnhancedComponent)) {
return (IWebSocketServerPushEnhancedComponent) comp;
}
}
// Try to find whether previous sibling is IWebSocketServerPushEnhancedComponent.
if (previous instanceof IWebSocketServerPushEnhancedComponent) {
return (IWebSocketServerPushEnhancedComponent) previous;
}
// Try to find whether previous sibling is ContextBinding and already found a target.
if (previous instanceof ContextBinding) {
IWebSocketServerPushEnhancedComponent previousTarget = ((ContextBinding) previous).getFoundTarget();
if (previousTarget != null) {
return previousTarget;
}
}
// Try to find whether parent is IWebSocketServerPushEnhancedComponent.
if (getParent() instanceof IWebSocketServerPushEnhancedComponent) {
return (IWebSocketServerPushEnhancedComponent)getParent();
}
return null;
}
}
impl.DesktopUtils.java
Modified from AURequestWithWebSocket, almost the same, add synchronized for ServerPush.
https://github.com/benbai123/ZK_Practice/blob/master/Integrate/WebSocket/ServerPushWithWebSocket/src/impl/DesktopUtils.java
impl.TestWebSocketServlet.java
Modified from AURequestWithWebSocket, almost the same, store channel at desktop so can send response with desktop itself.
https://github.com/benbai123/ZK_Practice/blob/master/Integrate/WebSocket/ServerPushWithWebSocket/src/impl/TestWebSocketServlet.java
impl.EventListener.java
Define an Event listener used to process event from Not used in this POC.
https://github.com/benbai123/ZK_Practice/blob/master/Integrate/WebSocket/ServerPushWithWebSocket/src/impl/EventListener.java
impl.RequestFromWebSocket.java
Copied from AURequestWithWebSocket, completely the same excepts this fragment. Not used in this POC
https://github.com/benbai123/ZK_Practice/blob/master/Integrate/WebSocket/ServerPushWithWebSocket/src/impl/RequestFromWebSocket.java
impl.serverpush.Binding.java
Binding that bind a field of a component to specific context for WebSocket ServerPush.
package impl.serverpush;
import java.io.Serializable;
/** Binding that bind a field of a component to specific context for WebSocket ServerPush
*
* @author benbai123
*
*/
public class Binding implements Serializable {
private static final long serialVersionUID = -1591783705902661276L;
/** field to bind of component */
private String _field;
/** context to bind with field */
private String _context;
// Constructor
public Binding (String field, String context) {
if (field == null) {
field = "";
}
if (context == null) {
context = "";
}
_field = field;
_context = context;
}
// getters, setters
public void setField (String field) {
if (field == null) {
field = "";
}
_field = field;
}
public String getField () {
return _field;
}
public void setContext (String context) {
if (context == null) {
context = "";
}
_context = context;
}
public String getContext () {
return _context;
}
// super
public int hashCode () {
return (_field + _context).hashCode() * 31;
}
public boolean equals (Object obj) {
if (obj != null
&& (obj instanceof Binding)) {
Binding b2 = (Binding) obj;
return (_field.equals(b2.getField())
&& _context.equals(b2.getContext()));
}
return false;
}
}
impl.serverpush.ServerPushUtil.java
Utilities for ServerPush with WebSocket, concept is similar to ServerPushWithWebSocket -- Component Oriented Version, rewritten it to Desktop Oriented Version.
package impl.serverpush;
import impl.TestWebSocketServlet;
import java.beans.Statement;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.Desktop;
/** Tested with ZK 6.5.2<br>
*
* Utilities for ServerPush with WebSocket, concept is similar to
* ServerPushWithWebSocket -- Component Oriented Version, rewritten it to
* Desktop Oriented Version.
*
* @author benbai123
*
*/
@SuppressWarnings("unchecked")
public class ServerPushUtil {
/** keep all binded Desktop */
private static List<Desktop> bindedDesktops = new ArrayList<Desktop>();
/** Lock for push to all desktops */
private static Integer LOCK_FOR_PUSH_TO_ALL = 0;
/** Lock for access bindedDesktops */
private static Integer LOCK_FOR_ACCESS_BINDED_DESKTOPS = 0;
/** attribute kay for binding map */
private static final String BINDING_MAP = "BINDING_MAP_FOR_WEBSOCKET_SERVERPUSH";
/** Add a Binding to a Component
*
* @param comp Component to bind member field with update context
* @param field field of component to bind
* @param context context to bind with field
* @return Binding the added Binding object
*/
public static Binding addContextBinding (Component comp, String field, String context) {
Binding binding = null;
synchronized (comp.getDesktop()) {
// get binding list of specified component
List<Binding> bindingList = getBindingList(comp);
// create a binding object with specified field/context
binding = new Binding(field, context);
// add binding into binding list if not exists
if (!bindingList.contains(binding)) {
bindingList.add(binding);
}
}
// return created binding
return binding;
}
/** Remove a Binding from a Component
*
* @param comp Component to remove Binding
* @param field field of the field-context pair to remove
* @param context context of the field-context pair to remove
*/
public static void removeContextBinding (Component comp, String field, String context) {
synchronized (comp.getDesktop()) {
Map<Component, List<Binding>> bindingMap = getBindingMap(comp.getDesktop());
// get binding list of specified component
List<Binding> bindingList = bindingMap.get(comp);
if (bindingList != null) {
// create a binding object with specified field/context
Binding target = new Binding (field, context);
// remove binding if exists
for (Binding b : bindingList) {
if (b.equals(target)) {
bindingList.remove(b);
break;
}
}
}
}
}
/** Push a value to a context for all desktops
*
* @param value value to push
* @param context context to push to
* @throws Exception whatever
*/
public static void pushVlaue (Object value, String context) throws Exception {
synchronized (LOCK_FOR_PUSH_TO_ALL) {
// used to store dead desktop
List<Desktop> deadDesktops = new ArrayList<Desktop>();
// make a copy to reduce lock
List<Desktop> desktopToPush = new ArrayList<Desktop>();
synchronized (LOCK_FOR_ACCESS_BINDED_DESKTOPS) {
desktopToPush.addAll(bindedDesktops);
}
// for each binded desktop
for (Desktop bindedDesktop : desktopToPush) {
if (bindedDesktop.isAlive()) {
// push value to context if alive
execPush(value, context, bindedDesktop);
} else {
// store it to deadDesktops otherwise
deadDesktops.add(bindedDesktop);
}
}
// remove all dead desktops
synchronized (LOCK_FOR_ACCESS_BINDED_DESKTOPS) {
bindedDesktops.removeAll(deadDesktops);
}
desktopToPush.removeAll(deadDesktops);
for (Desktop dt : desktopToPush) {
// send response via WebSocket
TestWebSocketServlet.sendResponse(dt);
}
}
}
/** Push a value to a context for specified desktop
*
* @param value value to push
* @param context context to push to
* @param desktop desktop to apply this push
* @throws Exception whatever
*/
public static void pushVlaue (Object value, String context, Desktop desktop) throws Exception {
if (desktop.isAlive()) {
// push value to context for specified desktop if alive
execPush(value, context, desktop);
} else {
synchronized (LOCK_FOR_ACCESS_BINDED_DESKTOPS) {
// remove specified desktop from bindedDesktops otherwise
bindedDesktops.remove(desktop);
}
}
// send response via WebSocket
TestWebSocketServlet.sendResponse(desktop);
}
/** Apply "push value to context" for specified desktop
*
* @param value value to push
* @param context context to push to
* @param desktop desktop to apply
* @throws Exception whatever
*/
public static void execPush (Object value, String context, Desktop desktop) throws Exception {
// execute push if WebSocket of desktop is ready
if (TestWebSocketServlet.isWebSocketReady(desktop)) {
synchronized (desktop) {
// get binding map
// (Map<String, List> where String is component ID and List is Binding objects)
Map<Component, List<Binding>> bindingMap = (Map<Component, List<Binding>>)desktop.getAttribute(BINDING_MAP);
if (bindingMap != null) {
for (Map.Entry<Component, List<Binding>> e : bindingMap.entrySet()) {
// get component by ID
Component comp = e.getKey();
// get binding list
List<Binding> bindings = e.getValue();
// for each binding
for (Binding binding : bindings) {
// push value to component if
// context of binding is equal to specified context
if (binding.getContext().equals(context)) {
// get field from binding
String field = binding.getField();
// build setter name
String method = "set" + field.substring(0, 1).toUpperCase() + field.substring(1);
// call setter to set value to component
Statement stat = new Statement(comp, method, new Object[]{value});
stat.execute();
}
}
}
}
}
}
}
/** Get Binding list of a Component
*
* @param comp Component to get Binding list
* @return Binding list for specified Component
*/
private static List<Binding> getBindingList (Component comp) {
// get binding map
Map<Component, List<Binding>> bindingMap = getBindingMap(comp.getDesktop());
// try to get binding list
List<Binding> bindingList = bindingMap.get(comp);
// create and add binding list if not exists
if (bindingList == null) {
bindingList = new ArrayList<Binding>();
bindingMap.put(comp, bindingList);
}
// return (created) binding list
return bindingList;
}
/** Get Binding map of a Desktop
*
* @param dt specified Desktop
* @return Map<Component, List<Binding>> Binding map of specified Desktop
*/
private static Map<Component, List<Binding>> getBindingMap (Desktop dt) {
// try to get binding map
Map<Component, List<Binding>> bindingMap = (Map<Component, List<Binding>>)dt.getAttribute(BINDING_MAP);
// create and add binding map if not exists
if (bindingMap == null) {
bindingMap = new Hashtable<Component, List<Binding>>();
dt.setAttribute(BINDING_MAP, bindingMap);
synchronized (LOCK_FOR_ACCESS_BINDED_DESKTOPS) {
bindedDesktops.add(dt);
}
}
// return (created) binding map
return bindingMap;
}
}
zk.xml
Modified from AURequestWithWebSocket, add config for lang-addon.xml, also change the way to get path for WebSocket (line 61~)
https://github.com/benbai123/ZK_Practice/blob/master/Integrate/WebSocket/ServerPushWithWebSocket/WebContent/WEB-INF/zk.xml
lang-addon.xml
Define components used in this POC.
<!-- Tested with ZK 6.5.2
Define components
-->
<language-addon>
<addon-name>param</addon-name>
<language-name>xul/html</language-name>
<!-- It specifies what language addons this addon
depends on. If specified, this addon will be
parsed after all the specified addons are parsed -->
<depends>zul</depends>
<!-- define param -->
<component>
<component-name>contextBinding</component-name>
<extends>div</extends>
<component-class>components.helper.ContextBinding</component-class>
</component>
<component>
<component-name>intbox</component-name>
<extends>intbox</extends>
<component-class>components.serverpush.Intbox</component-class>
</component>
<component>
<component-name>textbox</component-name>
<extends>textbox</extends>
<component-class>components.serverpush.Textbox</component-class>
</component>
</language-addon>
Download
Full project at github
https://github.com/benbai123/ZK_Practice/tree/master/Integrate/WebSocket/ServerPushWithWebSocket
Demo Flash
https://github.com/benbai123/ZK_Practice/blob/master/Integrate/WebSocket/ServerPushWithWebSocket/ServerPushWithWebSocket.swf