websocket实践
服务端以jetty8做容器,参考
http://wiki.eclipse.org/Jetty/Feature/WebSockets
http://dev.w3.org/html5/websockets/
如果用maven需要jetty-webapp和jetty-websocket依赖:
<dependency> <groupId>org.eclipse.jetty</groupId> <artifactId>jetty-webapp</artifactId> <version>8.1.2.v20120308</version> </dependency> <dependency> <groupId>org.eclipse.jetty</groupId> <artifactId>jetty-websocket</artifactId> <version>8.1.2.v20120308</version> </dependency>
注意jetty8实现的是servlet-api-3.0,所以要排除对servlet-api-2.5的依赖。否则会报Java SecurityException : signer information does not match 的错误。
在此jetty版本下webservletsocket代码为:
package kzg.html5.websocket; import java.io.IOException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import javax.servlet.http.HttpServletRequest; import net.sf.json.JSONObject; import org.eclipse.jetty.websocket.WebSocket; import org.eclipse.jetty.websocket.WebSocketServlet; @SuppressWarnings("serial") public class DebugLogWsSocket extends WebSocketServlet { @Override public WebSocket doWebSocketConnect(HttpServletRequest request, String protocol) { LogService.debug("websocket request protocol:" + protocol); return new TestSocket(); } private static class TestSocket implements WebSocket.OnTextMessage { ExecutorService service; Connection conn; private boolean clientStop = false; @Override public void onClose(int code, String message) { LogService.debug("websocket close:" + code + ":" + message); setClientStop(true); if (null != service) { service.shutdown(); } } private void sendData(Object data) { JSONObject respj = new JSONObject(); respj.put("status", "ok"); respj.put("data", data); if (null == conn || !conn.isOpen()) { return; } try { conn.sendMessage(respj.toString()); } catch (IOException e) { LogService.error("websocket send data error:", e); } } @Override public void onOpen(final Connection aconn) { this.conn = aconn; LogService.debug("websocket open:" + conn.getProtocol()); service = Executors.newSingleThreadExecutor(); service.execute(new Runnable() { @Override public void run() { while (!isClientStop()) { sendData(System.currentTimeMillis()); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { return; } } } }); } @Override public void onMessage(final String message) { LogService.debug("websocket incomming message:" + message); sendData("server got message:" + message); } public synchronized boolean isClientStop() { return clientStop; } public synchronized void setClientStop(boolean clientStop) { this.clientStop = clientStop; } } }
大家可能看到与之前的api有些变化,最新的api用法还是要看http://wiki.eclipse.org/Jetty/Feature/WebSockets 这里。
web.xml配置:
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd" > <web-app> <servlet> <servlet-name>DebugLogServlet</servlet-name> <servlet-class>kzg.html5.websocket.DebugLogWsSocket</servlet-class> </servlet> <servlet-mapping> <servlet-name>DebugLogServlet</servlet-name> <url-pattern>/dlog/</url-pattern> </servlet-mapping> </web-app>
可看到与之前的servlet没什么区别。
客户端部分代码:
function strat_socket() { var error_count = 0; var url = 'ws://' + document.location.host + '/dlog/'; ws = new WebSocket(url); ws.onopen = function() { log('ws open'); }; ws.onerror = function(e) { log('ws error'); }; ws.onclose = function() { log('ws close'); }; ws.onmessage = function(msg) { log('ws message:'); log(msg); var d = $.parseJSON(msg.data); if (!d || 'error' == d.status) { error_count++; if (error_count > 3) { alert('poll error many times.'); return; } return; } process_log(d.data,true); }; }
开始我还对ws://这个url有些疑惑,实践中没有针对它的特殊配置。浏览器和web容器自会处理。
下面是我从firebug中查看到的onmessage事件中msg对象值:
data "{"status":"ok","data":1334634452781}" defaultPrevented false lastEventId "" origin "ws://localhost:20090" source null initMessageEvent initMessageEvent() stopImmediatePropagation stopImmediatePropagation() bubbles false cancelable false constructor MessageEvent {} currentTarget WebSocket { url="ws://localhost:20090/dlog/", readyState=3, bufferedAmount=0, more...} eventPhase 2 explicitOriginalTarget WebSocket { url="ws://localhost:20090/dlog/", readyState=3, bufferedAmount=0, more...} isTrusted true originalTarget WebSocket { url="ws://localhost:20090/dlog/", readyState=3, bufferedAmount=0, more...} target WebSocket { url="ws://localhost:20090/dlog/", readyState=3, bufferedAmount=0, more...} timeStamp 1334634452921000 type "message"
客户端关闭:
//1000 or 3000-4999 ws.close(3001,'my close');
依据文档如果code 不是1000或是3000-4999则会抛异常。第二个参数不能超过123字节,具体见http://dev.w3.org/html5/websockets/#dom-websocket-close
当然还有客户端发送:
ws.send('hello');
测试在chrome18和FF11,Safari5.0.5都运行正常。
说到Safari,又话来长,因为我发现tomcat7作为服务端的话就不能与Safari的websocket建立连接。
起因是这样的,我使用tomcat7建立的websocket工程在FF和chrome下都运行的好好的,但是在safari浏览器下建立不了连接,一开始就是个onclose事件。
目前最稳定的还是jetty8,见http://jfarcand.wordpress.com/2012/05/16/safaris-websocket-implementation-and-java-problematic/
WebServers | Version | Specification | Safari Stability |
Tomcat | 7.0.27 and up | hybi-13 and up | NOT SUPPORTED |
Jetty | 7.0 to 7.4.5 | Up to hybi-12 | UNSTABLE: Server suffer High CPU when Safari’s WebSocket connection get closed. |
Jetty | 7.5.x to 7.6.2 | Up to hybi-12 | UNSTABLE: Server suffer High CPU when Safari’s WebSocket connection get closed. |
Jetty | 7.5.x to 7.6.2 | Up to hybi-13 | UNSTABLE: Server suffer High CPU when Safari’s WebSocket connection get closed. |
Jetty | 8.x to 8.1.2 | Up to hybi-13 | UNSTABLE: Server suffer High CPU when Safari’s WebSocket connection get closed. |
Jetty | 7.6.3 | All hybi version | STABLE |
Jetty | 8.1.3 | All hybi version | STABLE |
GlassFish | 3.1.1 | All hybi version | UNSTABLE: Suffer many API bugs |
GlassFish | 3.1.2 | All hybi version | STABLE |
NettoSphere (based on Netty Framework) | 1.x | All hybi version | STABLE |