引言
通过NIO+Continunation+HttpClient可以使Jetty具有异步长连接的功能,但有些应用场景确需要服务器“推”的功能,比如说:聊天室、实时消息提醒、股票行情等对于实时要求比较高的应用,能想到的实时推送的解决方案大致可以分为下面几种:
1、轮询:前台ajax轮询实时消息。
2、applet:已经OUT了不是~而且亦有安全方面的问题
3、长连接:在一次TCP的连接上发送多次数据,除非手动close,但需要在HTTP协议的基础上做协议的转换并应用在客户端和服务端,这些工作需要自己来实现。
我最初接触到websocket是设计一个资源远程加载的平台。设想你在本地开发web应用,你只需要告诉平台你的应用在本地的地址。第三方的人员(主管或者运营人员,亦或是一个项目组的同事)可以随时通过访问平台看到你工作的成果,因为是实时的,所以沟通会更加有效。
本文描述的websocket就是一个非Http的双向连接(其实也跟Http息息相关,下文有详解),有了它你不需要没事去轮询实现推的功能;有了它你可以对注册到平台上的计算机做一些事情(确实有安全隐患,不过都是开发环境也就无所谓了)。
一个简单的实例
为了研究websocket需要搭建一个功能环境,修改了网上的一段实例并调试无误,贴出来主要代码,需要完整工程的同学请留言。
实例流程如下:
A 客户端建立websocket连接后发送给服务端want消息告知服务端。
B 服务端接收到消息,判断如果是want命令的话,返回给所有客户端begin消息。
C 客户端接受消息,判断如果是begin命令的话,即读取本地文件发送给服务端。
D 服务端接收消息,判断如果是非want命令的话,将读取的内容加上后缀返回给客户端。
E 客户端接受消息,判断如果是非begin命令的话,将读取的内容显示在html页面上。
1、前台JS
下面三段js主要是实现服务端读取已注册的用户计算机上的文件,很邪恶的有木有。
//读取本地的文件 function read(file) { if(typeof window.ActiveXObject != 'undefined') { var content = ""; try { var fso = new ActiveXObject("Scripting.FileSystemObject"); var reader = fso.openTextFile(file, 1); while(!reader.AtEndofStream) { content += reader.readline(); content += "\n"; } // close the reader reader.close(); } catch (e) { alert("Internet Explore read local file error: \n" + e); } return content; } else if(document.implementation && document.implementation.createDocument) { var content = "" try { netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect'); var lf = Components.classes["@mozilla.org/file/local;1"].createInstance(Components.interfaces.nsILocalFile); lf.initWithPath(file); if (lf.exists() == false) { alert("File does not exist"); } var fis = Components.classes["@mozilla.org/network/file-input-stream;1"].createInstance(Components.interfaces.nsIFileInputStream); fis.init(lf, 0x01, 00004, null); var sis = Components.classes["@mozilla.org/scriptableinputstream;1"].createInstance(Components.interfaces.nsIScriptableInputStream); sis.init(fis); var converter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"].createInstance(Components.interfaces.nsIScriptableUnicodeConverter); var insis = sis.read(sis.available()); converter.charset = "GBK"; content = converter.ConvertToUnicode(insis); } catch (e) { alert("Mozilla Firefox read local file error: \n" + e); } return content; } } </script> <script type='text/javascript'> //判断当前浏览器是否支持websocket if (!window.WebSocket) alert("window.WebSocket unsuport!"); else alert("suport!"); function $() { return document.getElementById(arguments[0]); } function $F() { return document.getElementById(arguments[0]).value; } function getKeyCode(ev) { if (window.event) return window.event.keyCode; return ev.keyCode; } //websocket主要实现 var server = { connect : function() { var location ="ws://localhost:8888/petstore/servlet/a?key=123"; alert("before conn!"); this._ws =new WebSocket(location); alert("has conned!"); this._ws.onopen =this._onopen; this._ws.onmessage =this._onmessage; this._ws.onclose =this._onclose; server._send("want"); }, _onopen : function() { }, _send : function(message) { if (this._ws) this._ws.send(message); }, send : function(text) { if (text !=null&& text.length >0) server._send(text); }, _onmessage : function(m) { if (m.data) { if (m.data=="begin") { var res = read("/Users/apple/workspace/apache/chenshuai.html"); server._send(res); } else { var messageBox = $('messageBox'); var spanText = document.createElement('span'); spanText.className ='text'; spanText.innerHTML = m.data; var lineBreak = document.createElement('br'); messageBox.appendChild(spanText); messageBox.appendChild(lineBreak); messageBox.scrollTop = messageBox.scrollHeight - messageBox.clientHeight; } } }, _onclose : function(m) { this._ws =null; } }; </script> <script type='text/javascript'> //显式触发websocket的建立 $('connect').onclick =function(event) { alert("has clicked!"); server.connect(); return false; }; </script>
2、后台servlet
public class MyWebSocketServlet extends WebSocketServlet { private static final long serialVersionUID = -7289719281366784056L; public static String newLine = System.getProperty("line.separator"); private final Set<TailorSocket> _members = new CopyOnWriteArraySet<TailorSocket>(); private ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); public void init() throws ServletException { super.init(); } protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { getServletContext().getNamedDispatcher("default").forward(request, response); } public WebSocket doWebSocketConnect(HttpServletRequest request, String protocol) { String key = (String) request.getParameter("key"); return new TailorSocket(key); } class TailorSocket implements WebSocket.OnTextMessage { private Connection _connection; private String key; public String getKey() { return key; } public TailorSocket(String key) { this.key = key; } public void onClose(int closeCode, String message) { _members.remove(this); } public void sendMessage(String data) throws IOException { _connection.sendMessage(data); } public void onMessage(String data) { for(TailorSocket member : _members){ System.out.println("Trying to send to Member!"); if(member.isOpen()){ System.out.println("Sending!"); try { if (data.equals("want")) { member.sendMessage("begin"); } else { member.sendMessage(data+member.getKey()); } } catch (IOException e) { } } } System.out.println("Received: "+data); } public boolean isOpen() { return _connection.isOpen(); } public void onOpen(Connection connection) { _members.add(this); _connection = connection; try { connection.sendMessage("onOpen:Server received Web Socket upgrade and added it to Receiver List."); } catch (IOException e) { e.printStackTrace(); } } } }3、web.xml配置
<servlet> <servlet-name>WebSocket</servlet-name> <servlet-class>com.alibaba.myX3.dal.dataobject.MyWebSocketServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>WebSocket</servlet-name> <url-pattern>/servlet/*</url-pattern> </servlet-mapping>4、前台页面
<body> <div id='messageBox'></div> <div id='input'> <div> <input id='connect' class='button' type='submit' name='Connect' value='Connect' /> </div> </div> <script type='text/javascript'> $('connect').onclick =function(event) { alert("has clicked!"); server.connect(); return false; }; </script> <p> JAVA Jetty for WebSocket </p> </body>
5、运行结果
1)本机的文件
2)点击连接之后的结果
只要浏览器过关,任何人都可以看到你本地文件的内容了~推送功能算是完成了。
HTTP状态码101
给出状态码的预备知识,对于理解websocket挺有用的。
1xx:这一类型的状态码,代表请求已被接受,需要继续处理。这类响应是临时响应,只包含状态行和某些可选的响应头信息,并以空行结束。由于 HTTP/1.0 协议中没有定义任何 1xx 状态码,所以除非在某些试验条件下,服务器禁止向此类客户端发送 1xx 响应。
101:服务器已经理解了客户端的请求,并将通过Upgrade 消息头通知客户端采用不同的协议来完成这个请求。在发送完这个响应最后的空行后,服务器将会切换到在Upgrade 消息头中定义的那些协议。
WebSocket原理
客户端不在本文的研究范围,这里只分析Jetty是如何实现的,其实也大同小异,无非是新的协议罢了~
1、WebSocket模型
红色:请求的入口,它定义了WebSocket并持有WebSocketFactory,从而初始化WebSocket连接并设置连接的WebSocket值。
蓝色:相当于HTTP协议的HttpConnection,不需解释。
橙色:新的协议自然需要新的解析和生产规则了。
2、模拟一次连接的建立
A 简要流程:
B 详细流程:
1)客户端请求建立WebSocket连接
var location ="ws://localhost:8888/petstore/servlet/a?key=123"; alert("before conn!"); this._ws =new WebSocket(location);此时会向服务器发出:http://localhosts:8888/petstore/servlet/a?key=123,自然不是ws协议,服务器也得认识才行啊,所以ws协议的最初的建立也是依赖了http协议。
2)服务端servlet处理请求
//判断客户端是否要求更新协议为websocket if ("websocket".equalsIgnoreCase(request.getHeader("Upgrade"))) { String origin = request.getHeader("Origin"); if (origin==null) origin = request.getHeader("Sec-WebSocket-Origin"); if (!_acceptor.checkOrigin(request,origin)) { response.sendError(HttpServletResponse.SC_FORBIDDEN); return false; } // Try each requested protocol WebSocket websocket = null; @SuppressWarnings("unchecked") Enumeration<String> protocols = request.getHeaders("Sec- WebSocket-Protocol"); String protocol=null; while (protocol==null && protocols!=null && protocols.hasMoreElements()) { String candidate = protocols.nextElement(); for (String p : parseProtocols(candidate)) { websocket = _acceptor.doWebSocketConnect(request, p); if (websocket != null) { protocol = p; break; } } } // Did we get a websocket? if (websocket == null) { // 用servlet提供的webSocket实现,上面的实例有详细实现 websocket = _acceptor.doWebSocketConnect(request, null); if (websocket==null) { response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE); return false; } } // 告诉客户端,我已经准备好了切换协议了 upgrade(request, response, websocket, protocol); return true; } return false;
看下upgrade的实现:
AbstractHttpConnection http = AbstractHttpConnection.getCurrentConnection(); if (http instanceof BlockingHttpConnection) throw new IllegalStateException("Websockets not supported on blocking connectors"); //用着一样的信道,并木有重新建立socket连接 ConnectedEndPoint endp = (ConnectedEndPoint)http.getEndPoint(); connection = new WebSocketServletConnectionRFC6455(this, websocket, endp, _buffers, http.getTimeStamp(), _maxIdleTime, protocol, extensions, draft); // Set the defaults connection.getConnection().setMaxBinaryMessageSize(_maxBinaryMessageSize); connection.getConnection().setMaxTextMessageSize(_maxTextMessageSize); // 完成“握手”阶段,总要告诉点客户端什么,大致就是:我已经换好协议了,你那边可以发送新协议格式的数据了啊! connection.handshake(request, response, protocol); response.flushBuffer(); // Give the connection any unused data from the HTTP connection. connection.fillBuffersFrom(((HttpParser)http.getParser()).getHeaderBuffer()); connection.fillBuffersFrom(((HttpParser)http.getParser()).getBodyBuffer()); // 至此换了新的连接,新的协议解析器和生成器,总不至于还用外面的HttpConnection吧,那我就把新的协议放在request里面,把协议发生改变的标示放在reponse里面,后面jetty判断response如果有协议改变的话就会更新endpoint的connection了。 LOG.debug("Websocket upgrade {} {} {} {}",request.getRequestURI(),draft,protocol,connection); request.setAttribute("org.eclipse.jetty.io.Connection", connection);
3)Jetty替换原来的Http协议为最新的WebSocket协议
// look for a switched connection instance? if (_response.getStatus()==HttpStatus.SWITCHING_PROTOCOLS_101) { Connection switched= (Connection)_request.getAttribute("org.eclipse.jetty.io.Connection"); if (switched!=null) connection=switched; }
C 报文数据
URL:http://localhost:8888/petstore/servlet/a?key=123
状态码:101
报文头信息:
3、模拟请求的接受和发送
1)WebSocketParserRFC6455解析请求的参数
progress=true; _handler.onFrame(_flags, _opcode, data); _bytesNeeded=0; _state=State.START;2)接着看下ws协议框架处理器 WSFrameHandler是如何处理解析出的data的。
//示例中的websocket就是该类型的,因此由它来处理 if(_onTextMessage!=null) { if (_connection.getMaxTextMessageSize()<=0) { // No size limit, so handle only final frames if (lastFrame) //调用servlet中定义的websocket的回调接口 _onTextMessage.onMessage(buffer.toString(StringUtil.__UTF8)); else { LOG.warn("Frame discarded. Text aggregation disabled for {}",_endp); _connection.close(WebSocketConnectionD08.CLOSE_BADDATA,"Text frame aggregation disabled"); } }
3)看下servlet中定义的websocket:
class TailorSocket implements WebSocket.OnTextMessage { private Connection _connection; public void onClose(int closeCode, String message) { _members.remove(this); } public void sendMessage(String data) throws IOException { _connection.sendMessage(data); } public void onMessage(String data) { for(TailorSocket member : _members){ System.out.println("Trying to send to Member!"); if(member.isOpen()){ System.out.println("Sending!"); try { if (data.equals("want")) { member.sendMessage("begin"); } else { member.sendMessage(data+member.getKey()); } } catch (IOException e) { } } } System.out.println("Received: "+data); } public boolean isOpen() { return _connection.isOpen(); } public void onOpen(Connection connection) { _members.add(this); _connection = connection; try { connection.sendMessage("onOpen:Server received Web Socket upgrade and added it to Receiver List."); } catch (IOException e) { e.printStackTrace(); } } }
4)至于最后的发送数据,无非就是利用WebSocketGenerator生成协议格式的数据flush到信道中,不详述了。
总结
学习了WebSocket感觉对于HttpConnection和Http状态码的认识更加深刻了,以后可以基于Http定制好玩的协议,不过需要客户端的配合。
Jetty实现了服务器推的功能而无需轮询,缺陷就是支持的浏览器太少。大致流程是如此:
1、客户端首先发送一条要求切换协议格式的http请求要求建立websocket连接。
2、服务端的servlet处理请求时发现Http请求中要求切换协议,因此在原有的信道上创建了新的协议连接器,并返回给客户端101状态码,告诉客户端已经建立好了新的协议连接器了,此即上面注释中说的握手。
3、客户端收到101状态码后返回客户端的websocket实例,并开始发送和接受数据。