【WebSocket初探】
?????? 眾所周知,socket是編寫網絡通信應用的基本技術,網絡數據交換大多直接或間接通過socket進行。對于直接使用socket的client與服務端,一旦連接被建立則均可主動向對方傳送數據,而對于使用更上層的HTTP/HTTPS協議的應用,因為它們是非連接協議,所以通常僅僅能由client主動向服務端發送請求才干獲得服務端的響應并取得相關的數據。而當前越來越多的應用希望可以及時獲取服務端提供的數據,甚至希望可以達到接近實時的數據交換(比如非常多站點提供的在線客戶系統)。為達到此目的,通常採用的技術主要有輪詢、長輪詢、流等,而伴隨著HTML5的出現,相對更優異的WebSocket方案也應運而生。
一、????????????非WebSocket方案簡單介紹
1.??????輪詢
?????? 輪詢是由client定時向服務端發起查詢數據的請求的一種實現方式。早期的輪詢是通過不斷自己主動刷新頁面而實現的(在那個基本是IE統治瀏覽器的時代,那不斷刷新頁面產生的噪聲就難以讓人忍受),后來隨著技術的發展,特別是Ajax技術的出現,實現了無刷新更新數據。但本質上這些方式均是client定時輪詢服務端,這樣的方式的最顯著的缺點是假設client數量龐大而且定時輪詢間隔較短服務端將承受響應這些client海量請求的巨大的壓力。
2.??????長輪詢
?????? 在數據更新不夠頻繁的情況下,使用輪詢方法獲取數據時client常常會得到沒有數據的響應,顯然這樣的輪詢是一個浪費網絡資源的無效的輪詢。長輪詢則是針對普通輪詢的這樣的缺陷的一種改進方案,其詳細實現方式是假設當前請求沒有數據能夠返回,則繼續保持當前請求的網絡連接狀態,直到服務端有數據能夠返回或者連接超時。長輪詢通過這樣的方式降低了client與服務端交互的次數,避免了一些無謂的網絡連接。可是假設數據變更較為頻繁,則長輪詢方式與普通輪詢在性能上并無顯著差異。同一時候,添加連接的等待時間,往往意味著并發性能的下降。
3.??????流
????? 所謂流是指client在頁面之下向服務端發起一個長連接請求,服務端收到這個請求后響應它并不斷更新連接狀態,以確保這個連接在client與服務端之間一直有效。服務端能夠通過這個連接將數據主動推送到client。顯然,這樣的方案實現起來相對照較麻煩,并且可能被防火墻阻斷。
二、????????????WebSocket簡單介紹
1.??????WebSocket協議簡單介紹
?????? WebSocket是為解決client與服務端實時通信而產生的技術。其本質是先通過HTTP/HTTPS協議進行握手后創建一個用于交換數據的TCP連接,此后服務端與client通過此TCP連接進行實時通信。
WebSocket規范當前還沒有正式版本號,草案變化也較為迅速。Tomcat7(本文中的例程來自7.0.42)當前支持RFC 6455(http://tools.ietf.org/html/rfc6455)定義的WebSocket,而RFC 6455眼下還未凍結,將來可能會修復一些Bug,甚至協議本身也可能會產生一些變化。
??????? RFC6455定義的WebSocket協議由握手和傳輸數據兩個部分組成。
????來自client的握手信息類似例如以下:
GET /chat HTTP/1.1 Host: server.example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Origin: http://example.com Sec-WebSocket-Protocol: chat, superchat Sec-WebSocket-Version: 13
??????? 服務端的握手信息類似例如以下:
?
??????? 一旦client和服務端都發送了握手信息而且成功握手,則傳輸數據部分將開始。傳輸數據對client和服務端而言都是一個雙工通信通道,client和服務端來回傳遞的數據稱之為“消息”。
client通過WebSocket URI發起WebSocket連接,WebSocket URIs模式定義例如以下:
?
ws-URI = "ws:" "//" host [ ":" port ] path [ "?" query ] wss-URI = "wss:" "//" host [ ":" port ] path [ "?" query ]
?
??????? ws是普通的WebSocket通信協議,而wss是安全的WebSocket通信協議(就像HTTP與HTTPS之間的差異一樣)。在缺省情況下,ws的port是80而wss的port是443。
??????? 關于WebSocke協議規范的完整詳盡說明,請參考RFC 6455。
2.??????Tomcat7提供的WebSocket包簡單介紹
??????? Tomcat7提供的與WebSocket相關的類均位于包org.apache.catalina.websocket之中(包org.apache.catalina.websocket的實現包括于文件catalina.jar之中),它包括有類Constants、MessageInbound、StreamInbound、WebSocketServlet、WsFrame、WsHttpServletRequestWrapper、WsInputStream、WsOutbound。這些類的關系如圖 1所看到的。
?
???????????????????????????????????????????????????????????????? 圖1
??????? 包org.apache.catalina.websocket中的這些類為WebSocket開發服務端提供了支持,這些類的主要功能簡述例如以下:
??????? Constants:包org.apache.catalina.websocket中用到的常數定義在這個類中,它僅僅包括靜態常數定義,無不論什么邏輯實現。
??????? MessageInbound:基于消息的WebSocket實現類(帶內消息),應用程序應當擴展這個類并實現其抽象方法onBinaryMessage和onTextMessage。
?????? StreamInbound:基于流的WebSocket實現類(帶內流),應用程序應當擴展這個類并實現其抽象方法onBinaryData和onTextData。
?????? WebSocketServlet:提供遵循RFC6455的WebSocket連接的Servlet基本實現。client使用WebSocket連接服務端時,須要將WebSocketServlet的子類作為連接入口。同一時候,該子類應當實現WebSocketServlet的抽象方法createWebSocketInbound,以便創建一個inbound實例(MessageInbound或StreamInbound)。
?????? WsFrame:代表完整的WebSocket框架。
?????? WsHttpServletRequestWrapper:包裝過的HttpServletRequest對象。
?????? WsInputStream:基于WebSocket框架底層的socket的輸入流。
?????? WsOutbound:提供發送消息到client的功能。它提供的全部向client的寫方法都是同步的,能夠防止多線程同一時候向client寫入數據。
三、????????????基于Tomcat7的WebSocket例程
??????? 利用當前HTML5和Tomcat7為WebSocket提供的支持,基本僅僅須要編寫簡單的代碼對不同的事件做對應的邏輯處理就能夠實現利用WebSocket進行實時通信了。
??????? Tomcat7為WebSocket提供了3個例程(echo、chat及snake),下面就當中的echo和chat分別做一簡要解析。
1.??????echo例程
??????? echo例程主要演示下面功能:client連接服務端、client向服務端發送消息、服務端收到client發送的消息后將其原樣返回給client、client收到消息后將其顯示在網頁之上。
?????? 在client頁面選擇streams或messages作為“Connectusing”,然后點擊“Connect”button,能夠在右側窗體看到WebSocket連接打開的消息。隨后點擊“Echo message”button,client將向服務端發送一條消息,在右側窗體,能夠看到,消息發出的后的瞬間,client已經收到了服務端原樣返回的消息。
??????? client頁面及執行效果截圖如圖 2所看到的。
??????????????????????????????????????????????????????????????????????????????????????????????????? 圖2
?????? client實現上述功能的核心腳本例如以下,其關鍵點通過凝視的形式加以說明:
<script type="text/javascript">var ws = null;// 界面元素可用性控制function setConnected(connected) {document.getElementById('connect').disabled = connected;document.getElementById('disconnect').disabled = !connected;document.getElementById('echo').disabled = !connected;}function connect() {// 取得WebSocket連接入口(WebSocket URI)var target = document.getElementById('target').value;if (target == '') {alert('Please select server side connection implementation.');return;}// 創建WebSocketif ('WebSocket' in window) {ws = new WebSocket(target);} else if ('MozWebSocket' in window) {ws = new MozWebSocket(target);} else {alert('WebSocket is not supported by this browser.');return;}// 定義Open事件處理函數ws.onopen = function () {setConnected(true);log('Info: WebSocket connection opened.');};// 定義Message事件處理函數(收取服務端消息并處理)ws.onmessage = function (event) {log('Received: ' + event.data);};// 定義Close事件處理函數ws.onclose = function () {setConnected(false);log('Info: WebSocket connection closed.');};}// 關閉WebSocket連接function disconnect() {if (ws != null) {ws.close();ws = null;}setConnected(false);}function echo() {if (ws != null) {var message = document.getElementById('message').value;log('Sent: ' + message);// 向服務端發送消息ws.send(message);} else {alert('WebSocket connection not established, please connect.');}}// 生成WebSocket URI function updateTarget(target) {if (window.location.protocol == 'http:') {document.getElementById('target').value = 'ws://' + window.location.host + target;} else {document.getElementById('target').value = 'wss://' + window.location.host + target;}}// 在界面顯示log及消息function log(message) {var console = document.getElementById('console');var p = document.createElement('p');p.style.wordWrap = 'break-word';p.appendChild(document.createTextNode(message));console.appendChild(p);while (console.childNodes.length > 25) {console.removeChild(console.firstChild);}console.scrollTop = console.scrollHeight;}</script>
?????? 注:完整代碼參見apache-tomcat-7.0.42\webapps\examples\websocket\echo.html
?????? 以上client能夠依據“Connectusing”的不同選擇連接不同的服務端WebSocket。比如messages選項相應的服務端代碼例如以下,其核心邏輯是在收到client發來的消息后馬上將其發回client。
public class EchoMessage extends WebSocketServlet {private static final long serialVersionUID = 1L;private volatile int byteBufSize;private volatile int charBufSize;@Overridepublic void init() throws ServletException {super.init();byteBufSize = getInitParameterIntValue("byteBufferMaxSize", 2097152);charBufSize = getInitParameterIntValue("charBufferMaxSize", 2097152);}public int getInitParameterIntValue(String name, int defaultValue) {String val = this.getInitParameter(name);int result;if(null != val) {try {result = Integer.parseInt(val);}catch (Exception x) {result = defaultValue;}} else {result = defaultValue;}return result;}// 創建Inbound實例,WebSocketServlet子類必須實現的方法@Overrideprotected StreamInbound createWebSocketInbound(String subProtocol,HttpServletRequest request) {return new EchoMessageInbound(byteBufSize,charBufSize);}// MessageInbound子類,完畢收到WebSocket消息后的邏輯處理private static final class EchoMessageInbound extends MessageInbound {public EchoMessageInbound(int byteBufferMaxSize, int charBufferMaxSize) {super();setByteBufferMaxSize(byteBufferMaxSize);setCharBufferMaxSize(charBufferMaxSize);}// 二進制消息響應@Overrideprotected void onBinaryMessage(ByteBuffer message) throws IOException {getWsOutbound().writeBinaryMessage(message);}// 文本消息響應@Overrideprotected void onTextMessage(CharBuffer message) throws IOException {// 將收到的消息發回clientgetWsOutbound().writeTextMessage(message);}} }
??????? 注:完整代碼參見apache-tomcat-7.0.42\webapps\examples\WEB-INF\classes\websocket\echo\EchoMessage.java。
2.??????chat例程
??????? chat例程實現了通過網頁進行群聊的功能。每一個打開的聊天網頁都能夠收到全部在線者發出的消息,同一時候,每一個在線者也都能夠(也僅僅能夠)向其他全部人發送消息。也就是說,chat實例演示了怎樣通過WebSocket實現對全部在線client的廣播。
????????????????????????????????????????????????????????????????? 圖3
??????? chat例程client核心代碼例如以下,能夠看到事實上現方式與echo例程形式上稍有變化,本質依然是對WebSocket事件進行響應與處理。
<script type="text/javascript">var Chat = {};Chat.socket = null;Chat.connect = (function(host) {// 創建WebSocketif ('WebSocket' in window) {Chat.socket = new WebSocket(host);} else if ('MozWebSocket' in window) {Chat.socket = new MozWebSocket(host);} else {Console.log('Error: WebSocket is not supported by this browser.');return;}// 定義Open事件處理函數Chat.socket.onopen = function () {Console.log('Info: WebSocket connection opened.');document.getElementById('chat').onkeydown = function(event) {if (event.keyCode == 13) {Chat.sendMessage();}};};// 定義Close事件處理函數Chat.socket.onclose = function () {document.getElementById('chat').onkeydown = null;Console.log('Info: WebSocket closed.');};// 定義Message事件處理函數Chat.socket.onmessage = function (message) {Console.log(message.data);};});Chat.initialize = function() {if (window.location.protocol == 'http:') {Chat.connect('ws://' + window.location.host + '/examples/websocket/chat');} else {Chat.connect('wss://' + window.location.host + '/examples/websocket/chat');}};// 發送消息至服務端Chat.sendMessage = (function() {var message = document.getElementById('chat').value;if (message != '') {Chat.socket.send(message);document.getElementById('chat').value = '';}});var Console = {};Console.log = (function(message) {var console = document.getElementById('console');var p = document.createElement('p');p.style.wordWrap = 'break-word';p.innerHTML = message;console.appendChild(p);while (console.childNodes.length > 25) {console.removeChild(console.firstChild);}console.scrollTop = console.scrollHeight;});Chat.initialize();</script>??????? 注:完整代碼參見apache-tomcat-7.0.42\webapps\examples\websocket\chat.html ? ??????? chat例程服務端代碼例如以下: public class ChatWebSocketServlet extends WebSocketServlet {private static final long serialVersionUID = 1L;private static final String GUEST_PREFIX = "Guest";private final AtomicInteger connectionIds = new AtomicInteger(0);private final Set<ChatMessageInbound> connections =new CopyOnWriteArraySet<ChatMessageInbound>();// 創建Inbound實例,WebSocketServlet子類必須實現的方法@Overrideprotected StreamInbound createWebSocketInbound(String subProtocol,HttpServletRequest request) {return new ChatMessageInbound(connectionIds.incrementAndGet());}// MessageInbound子類,完畢收到WebSocket消息后的邏輯處理private final class ChatMessageInbound extends MessageInbound {private final String nickname;private ChatMessageInbound(int id) {this.nickname = GUEST_PREFIX + id;}// Open事件@Overrideprotected void onOpen(WsOutbound outbound) {connections.add(this);String message = String.format("* %s %s",nickname, "has joined.");broadcast(message);}// Close事件@Overrideprotected void onClose(int status) {connections.remove(this);String message = String.format("* %s %s",nickname, "has disconnected.");broadcast(message);}// 二進制消息事件@Overrideprotected void onBinaryMessage(ByteBuffer message) throws IOException {throw new UnsupportedOperationException("Binary message not supported.");}// 文本消息事件@Overrideprotected void onTextMessage(CharBuffer message) throws IOException {// Never trust the clientString filteredMessage = String.format("%s: %s",nickname, HTMLFilter.filter(message.toString()));broadcast(filteredMessage);}// 向全部已連接的客戶端發送文本消息(廣播)private void broadcast(String message) {for (ChatMessageInbound connection : connections) {try {CharBuffer buffer = CharBuffer.wrap(message);connection.getWsOutbound().writeTextMessage(buffer);} catch (IOException ignore) {// Ignore}}}}
??????? 注:完整代碼參見apache-tomcat-7.0.42\webapps\examples\WEB-INF\classes\websocket\echo\ChatWebSocketServlet.java。
通過上述例程能夠看到WebSocket廣播實際上是通過遍歷全部連接并通過每一個連接向對應的client發送消息實現的。
四、????????????WebSocket實戰
??????? 實時向在線用戶推送通知是一個WebSocket應用的簡單場景,后臺提交通知信息以后,所在在線用戶均應非常快收到這個通知。通過上述例程了解WebSocket后,能夠嘗試編寫一個實現這個需求的WebSocket應用。
首先編寫一個用戶的Sample頁面,該頁面沒有實質的內容,可是在收到后臺發出的通知時要在右下角通過彈窗顯示通知的內容。其代碼例如以下: <!DOCTYPE html> <html> <head><title>Receive Message</title><style type="text/css">#winpop { width:200px; height:0px; position:absolute; right:0; bottom:0; border:1px solid #999999; margin:0; padding:1px; overflow:hidden; display:none; background:#FFFFFF}#winpop .con { width:100%; height:80px; line-height:80px; font-weight:bold; font-size:12px; color:#FF0000; text-align:center}</style><script type="text/javascript">// 彈窗相關function tips_pop(){var MsgPop=document.getElementById("winpop");var popH=parseInt(MsgPop.style.height);if(isNaN(popH)) {popH = 0;}if (popH==0){MsgPop.style.display="block";show=setInterval("changeH('up')",100);}else {hide=setInterval("changeH('down')",100);}}function changeH(str) {var MsgPop=document.getElementById("winpop");var popH=parseInt(MsgPop.style.height);if(isNaN(popH)) {popH = 0;}if(str=="up"){ if (popH<=100){ MsgPop.style.height=(popH+4).toString()+"px";}else{ clearInterval(show);setTimeout("tips_pop()", 5000);}}if(str=="down"){ if (popH>=4){ MsgPop.style.height=(popH-4).toString()+"px";}else{ clearInterval(hide); MsgPop.style.display="none"; }}}// WebSocket相關var ws = null;function connect() {var target = 'ws://' + window.location.host + "/test/NotifyWebSocketServlet";if ('WebSocket' in window) {ws = new WebSocket(target);} else if ('MozWebSocket' in window) {ws = new MozWebSocket(target);} else {alert('WebSocket is not supported by this browser.');return;}ws.onopen = function () {document.getElementById('msg').innerHTML = "WebSocket has opened, Waiting message.......";};ws.onmessage = function (event) {document.getElementById('infomsg').innerHTML = event.data;tips_pop();};ws.onclose = function () {document.getElementById('msg').innerHTML = "WebSocket has closed";};}function disconnect() {if (ws != null) {ws.close();ws = null;}} connect(); </script> </head> <body><h1 align="center" id="msg">Try to connect websocket.</h1><div id="winpop"><div class="con" id="infomsg"></div></div> </body> </html>??????? 當用戶界面打開時,它會嘗試通過/test/NotifyWebSocketServlet建立與server的WebSocket連接,而NotifyWebSocketServlet的實現代碼則例如以下: package net.yanzhijun.example;import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest;import org.apache.catalina.websocket.StreamInbound; import org.apache.catalina.websocket.WebSocketServlet;public class NotifyWebSocketServlet extends WebSocketServlet {private static final long serialVersionUID = 1L; @Overrideprotected StreamInbound createWebSocketInbound(String subProtocol,HttpServletRequest request) {ServletContext application = this.getServletContext();return new NofityMessageInbound(application);} }
??????? 與Tomcat給出的演示樣例代碼不同的是,在NotifyWebSocketServlet中并未將繼承于MessageInbound的NofityMessageInbound作為一個內嵌類。前述演示樣例代碼中發送消息和接收消息都是在同一組client頁面和服務端響應Servlet間進行的,而當前須要實現是在一個頁面中提交通知,而在其他用戶的頁面上顯示通知信息,因此須要將全部client與服務端的連接存儲一個全局域中,故而NofityMessageInbound將不僅僅在當前Servlet中被使用,所以有必要將其獨立出來。
??????? NofityMessageInbound的完整代碼例如以下: package net.yanzhijun.example;import java.nio.CharBuffer; import java.nio.ByteBuffer; import java.io.IOException; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet;import javax.servlet.ServletContext;import org.apache.catalina.websocket.WsOutbound; import org.apache.catalina.websocket.MessageInbound;public class NofityMessageInbound extends MessageInbound {private ServletContext application;private Set<NofityMessageInbound> connections = null;public NofityMessageInbound(ServletContext application) {this.application = application;connections = (Set<NofityMessageInbound>)application.getAttribute("connections");if(connections == null) {connections =new CopyOnWriteArraySet<NofityMessageInbound>();}}@Overrideprotected void onOpen(WsOutbound outbound) {connections.add(this); application.setAttribute("connections", connections);}@Overrideprotected void onClose(int status) {connections.remove(this);application.setAttribute("connections", connections);}@Overrideprotected void onBinaryMessage(ByteBuffer message) throws IOException {throw new UnsupportedOperationException("message not supported.");}@Overrideprotected void onTextMessage(CharBuffer message) throws IOException {throw new UnsupportedOperationException("message not supported.");} }??????? 后臺發送通知的頁面實現的相當簡單,僅僅是一個表單提交一條通知信息。 <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html><head><title>PushMessage</title></head><body><h1 align="Center">Online Broadcast</h1><form method="post" action="PushMessageServlet"><p>Message:<br/><textarea name="message" rows="5" cols="30"></textarea></p><p><input type="submit" value="Send">??<input type="reset" value="Reset"></p></form></body> </html>
?????? 接收提交通知的Servlet是PushMessageServlet,它在收到后臺提交的通知后,就通過全部用戶的WebSocket連接將通知發送出去。 package net.yanzhijun.example;import java.io.PrintWriter; import java.nio.CharBuffer; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; import java.io.IOException;import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse;public class PushMessageServlet extends HttpServlet {private static final long serialVersionUID = 1L;@Overridepublic void doGet(HttpServletRequest request,HttpServletResponse response)throws IOException, ServletException {doPost(request, response);}@Overridepublic void doPost(HttpServletRequest request,HttpServletResponse response)throws IOException, ServletException {request.setCharacterEncoding("UTF-8");response.setContentType("text/html;charset=UTF-8");PrintWriter out = response.getWriter();String message = request.getParameter("message"); if(message == null || message.length() == 0) { out.println("The message is empty!");return;}// 廣播消息broadcast(message);out.println("Send success!"); }// 將參數中的消息發送至全部在線clientprivate void broadcast(String message) {ServletContext application=this.getServletContext(); Set<NofityMessageInbound> connections = (Set<NofityMessageInbound>)application.getAttribute("connections");if(connections == null){return;}for (NofityMessageInbound connection : connections) {try {CharBuffer buffer = CharBuffer.wrap(message);connection.getWsOutbound().writeTextMessage(buffer);} catch (IOException ignore) {// Ignore}}} }
??????? 編譯相關文件并完畢部署,嘗試在后臺發送消息,能夠看到用戶界面右下角出現的彈窗中顯示了后臺所提交的內容。 ????????????????????????????????????????????????? 圖4
五、????????????WebSocket總結
????通過以上例程和實例能夠看出,從開發角度使用WebSocket相當easy,基本僅僅須要創建WebSocket實例并對關心的事件進行處理就能夠了;從應用角度WebSocket提供了優異的性能,圖 5是來自websocket.org的性能測試圖表(http://www.websocket.org/quantum.html),能夠看到當并發和負載添加時輪詢與WebSocket的差異。 ?????????????????????????????????????????????????????????????????? 圖5 ???????? (以上例程client在IE10.0和Chrom28.0下測試通過。) ? ???????? 歡迎訪問夢斷酒醒的博客http://blog.csdn.net/ishallwn總結
以上是生活随笔為你收集整理的【WebSocket初探】的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: switch(封装)
- 下一篇: HashMap get不出对象时出错 解