前言
之前一個項目中九風開發app的用戶的消息部分,由于項目比較緊,而且之前沒有接觸過WebSocket開發,所以暫時先使用輪詢方式來開發消息模塊,最近準備升級消息模塊,準備使用tomcat的WebSocket來開發消息,寫此文章方便自己也方便大家。
如需馬上測試的scoket的請直接往下翻到代碼出。
這篇文章中的代碼不能運行在spring mvc模式下,如需在mvc模式下運行,請參考這篇Sring MVC 模式下使用websocket。
特別說明
此文章中的后臺代碼不能直接用于Spring MVC中web層、service層直接調用,下篇文章準備寫這個(還沒寫好,九風盡快), 文章中有需要改正的還請簡友指出。
消息推送
消息推送大家都不陌生,比如扣扣消息、某東某寶購物后的系統消息等等都是消息推送,在H5出來之前,消息推送基本上都是使用HTTP請求的,但HTTP請求只能在客戶端發起請求后服務端返回消息,而不能再客戶端未發起請求時服務端主動推送消息給客戶端,而對于HTTP的方式實現消息推送時,有以下幾種方式:
輪詢方式:客戶端定時向服務端發送ajax請求,服務器接收到請求后馬上返回消息并關閉連接。
優點:后端程序編寫比較容易。
缺點:TCP的建立和關閉操作浪費時間和帶寬,請求中有大半是無用,浪費帶寬和服務器資源。
實例:適于小型應用。
長輪詢:客戶端向服務器發送Ajax請求,服務器接到請求后hold住連接,直到有新消息才返回響應信息并關閉連接,客戶端處理完響應信息后再向服務器發送新的請求。
優點:在無消息的情況下不會頻繁的請求,耗費資源小。
缺點:服務器hold連接會消耗資源,返回數據順序無保證,難于管理維護。
實例:WebQQ、Hi網頁版、Facebook IM。
長連接:在頁面里嵌入一個隱蔵iframe,將這個隱蔵iframe的src屬性設為對一個長連接的請求或是采用xhr請求,服務器端就能源源不斷地往客戶端輸入數據。
優點:消息即時到達,不發無用請求;管理起來也相對方便。
缺點:服務器維護一個長連接會增加開銷,當客戶端越來越多的時候,server壓力大!
實例:Gmail聊天
Flash Socket:在頁面中內嵌入一個使用了Socket類的 Flash 程序JavaScript通過調用此Flash程序提供的Socket接口與服務器端的Socket接口進行通信,JavaScript在收到服務器端傳送的信息后控制頁面的顯示。
優點:實現真正的即時通信,而不是偽即時。
缺點:客戶端必須安裝Flash插件,移動端支持不好,IOS系統中沒有flash的存在;非HTTP協議,無法自動穿越防火墻。
實例:網絡互動游戲。
webSocket:HTML5 WebSocket設計出來的目的就是取代輪詢和長連接,使客戶端瀏覽器具備像C/S框架下桌面系統的即時通訊能力,實現了瀏覽器和服務器全雙工通信,建立在TCP之上,雖然WebSocket和HTTP一樣通過TCP來傳輸數據,但WebSocket可以主動的向對方發送或接收數據,就像Socket一樣;并且WebSocket需要類似TCP的客戶端和服務端通過握手連接,連接成功后才能互相通信。
優點:雙向通信、事件驅動、異步、使用ws或wss協議的客戶端能夠真正實現意義上的推送功能。
缺點:少部分瀏覽器不支持。
示例:社交聊天(微信、QQ)、彈幕、多玩家玩游戲、協同編輯、股票基金實時報價、體育實況更新、視頻會議/聊天、基于位置的應用、在線教育、智能家居等高實時性的場景。
而websocket請求和服務器交互的如下圖所示:
對比前面的http的客戶端服務器的交互圖可以發現WebSocket方式減少了很多TCP打開和關閉連接的操作,WebSocket的資源利用率高。
WebSocket規范
WebSocket一種在單個 TCP 連接上進行全雙工通訊的協議。WebSocket通信協議于2011年被IETF定為標準RFC 6455,并被RFC7936所補充規范。WebSocket API也被W3C定為標準。
WebSocket 使得客戶端和服務器之間的數據交換變得更加簡單,允許服務端主動向客戶端推送數據。在 WebSocket API 中,瀏覽器和服務器只需要完成一次握手,兩者之間就直接可以創建持久性的連接,并進行雙向數據傳輸。
WebSocket 協議本質上是一個基于 TCP 的協議。為了建立一個 WebSocket 連接,客戶端瀏覽器首先要向服務器發起一個 HTTP 請求,這個請求和通常的 HTTP 請求不同,包含了一些附加頭信息,附加信息如圖所示:
瀏覽器支持:所有的最新瀏覽器支持最新WebSocket規范(RFC 6455) ,從維基百科上介紹瀏覽器對WebSocket的支持如下表所示:
瀏覽器 | Chrome | Edge | Firfox | IE | Opera | Safari |
---|---|---|---|---|---|---|
最低版本 | 16 | 支持 | 11.0 | 10 | 12.10 | 6.0 |
移動端支持:移動端基本都支持websocket了,其實和瀏覽器版支持的版本一樣,具體支持如下所示:
最低 | android瀏覽器 | Chrome 移動版 | Firfox 移動版 | Opera 移動版 | Safari IOS版 |
---|---|---|---|---|---|
最低版本 | 4.4 | 16 | 11.0 | 12.10 | 6.0 |
服務器支持:目前主流的web服務器都已經支持,具體版本如下表所示:
廠商 | 應用服務器 | 備注 |
---|---|---|
IBM | WebSphere | WebSphere 8.0 以上版本支持,7.X 之前版本結合 MQTT 支持類似的 HTTP 長連接 |
甲骨文 | WebLogic | WebLogic 12c 支持,11g 及 10g 版本通過 HTTP Publish 支持類似的 HTTP 長連接 |
微軟 | IIS | IIS 7.0+支持 |
Apache | Tomcat | Tomcat 7.0.5+支持,7.0.2X 及 7.0.3X 通過自定義 API 支持 |
Jetty | Jetty 7.0+支持 |
以下內容將使用tomcat服務器來實現Websocket
java WebSocket實現
Oracle 發布的 java 的 WebSocket 的規范是 JSR356規范 ,Tomcat從7.0.27開始支持WebSocket,從7.0.47開始支持JSR-356。
websocket簡單實現分為以下幾個步驟:添加websocket庫、編寫后臺代碼、編寫前端代碼。
添加websocket庫
在maven中添加websocket庫的代碼如下所示:
<dependency>
<groupId>javax.websocket</groupId>
<artifactId>javax.websocket-api</artifactId>
<version>1.1</version>
<scope>provided</scope>
</dependency>
九風有次沒寫<scope>字段,前端后臺都會報錯,大家記得加上就行。
前端錯誤內容:
WebSocket connection to 'ws://localhost:8080/{project-name}/websocket' failed: Error during WebSocket handshake: Unexpected response code: 404" 。
后臺錯誤內容:
Did not find handler method for [/websocket]
Matching patterns for request [/websocket] are [/**]
URI Template variables for request [/websocket] are {}
Mapping [/websocket] to HandlerExecutionChain with handler [org.springframework.web.servlet.resource.DefaultServletHttpRequestHandler@398f0b1f] and 1 interceptor
Last-Modified value for [/{project-name}/websocket] is: -1
編寫后臺代碼
后臺實現websocket有兩種方式:使用繼承類、使用注解;注解方式比較方便,一下代碼中使用注解方式來進行演示。
聲明websocket地址類似Spring MVC中的@controller注解類似,websocket使用@ServerEndpoint來進行聲明接口:@ServerEndpoint(value="/websocket/{paraName}")
; 其中 “ { } ”用來表示帶參數的連接,如果需要獲取{}中的參數在參數列表中增加:@PathParam("paraName") Integer userId 。則連接地址形如:ws://localhost:8080/project-name/websocket/8,其中每個連接可以設置不同的paraName的值。
注解、成員數據介紹:
1.@OnOpen
public void onOpen(Session session) throws IOException{ } -------有連接時的觸發函數。 我們可以在用戶連接時記錄用戶的連接帶的參數,只需在參數列表中增加參數:@PathParam("paraName") String paraName。
2.@OnClose
public void onClose(){ } ------連接關閉時的調用方法。
3.@OnMessage
public void onMessage(String message, Session session) { } -------收到消息時調用的函數,其中Session是每個websocket特有的數據成員,詳情見4.
4.Session ----每個Session代表了兩個web socket斷點的會話;當websocket握手成功后,websocket就會提供一個打開的Session,可以通過這個Session來對另一個端點發送數據;如果Session關閉后發送數據將會報錯。
5.Session.getBasicRemote().sendText("message") -------向該Session連接的用戶發送字符串數據。
6.@OnError
public void onError(Session session, Throwable error) { } --------發生意外錯誤時調用的函數。
后臺代碼:有以上基礎后就直接上代碼了.
import java.io.IOException;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @Class: Test
* @Description: 簡單websocket demo
* @author 九風萍舟
*/
@ServerEndpoint(value="/websocketTest/{userId}")
public class Test {
private Logger logger = LoggerFactory.getLogger(Test.class);
private static String userId;
//連接時執行
@OnOpen
public void onOpen(@PathParam("userId") String userId,Session session) throws IOException{
this.userId = userId;
logger.debug("新連接:{}",userId);
}
//關閉時執行
@OnClose
public void onClose(){
logger.debug("連接:{} 關閉",this.userId);
}
//收到消息時執行
@OnMessage
public void onMessage(String message, Session session) throws IOException {
logger.debug("收到用戶{}的消息{}",this.userId,message);
session.getBasicRemote().sendText("收到 "+this.userId+" 的消息 "); //回復用戶
}
//連接錯誤時執行
@OnError
public void onError(Session session, Throwable error){
logger.debug("用戶id為:{}的連接發送錯誤",this.userId);
error.printStackTrace();
}
}
ServerEndpoint報錯: 原因是不能自動檢測 ServerEndpoint 的包,解決方法:復制 import javax.websocket.server.ServerEndpoint;
到文件程序 import 區域即可。
編寫前端代碼
后臺代碼編寫了那么前端代碼就幾乎不用講解了,相信大家一眼就能看得懂。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
websocket Demo---- user000 <br />
<input id="text" type="text" />
<button onclick="send()"> Send </button>
<button onclick="closeWebSocket()"> Close </button>
<div id="message"> </div>
<script type="text/javascript">
//判斷當前瀏覽器是否支持WebSocket
if('WebSocket' in window){
websocket = new WebSocket("ws://localhost:8080/Demo/websocketTest/user000");
console.log("link success")
}else{
alert('Not support websocket')
}
//連接發生錯誤的回調方法
websocket.onerror = function(){
setMessageInnerHTML("error");
};
//連接成功建立的回調方法
websocket.onopen = function(event){
setMessageInnerHTML("open");
}
console.log("-----")
//接收到消息的回調方法
websocket.onmessage = function(event){
setMessageInnerHTML(event.data);
}
//連接關閉的回調方法
websocket.onclose = function(){
setMessageInnerHTML("close");
}
//監聽窗口關閉事件,當窗口關閉時,主動去關閉websocket連接,防止連接還沒斷開就關閉窗口,server端會拋異常。
window.onbeforeunload = function(){
websocket.close();
}
//將消息顯示在網頁上
function setMessageInnerHTML(innerHTML){
document.getElementById('message').innerHTML += innerHTML + '<br/>';
}
//關閉連接
function closeWebSocket(){
websocket.close();
}
//發送消息
function send(){
var message = document.getElementById('text').value;
websocket.send(message);
}
</script>
</body>
</html>
測試運行
在Chrome上打開前端代碼后,馬上就建立了連接,大家可以使用F12查看下建立連接的請求與響應,可以對比前面關于協議建立的部分進行學習。
建立連接后,想后臺發送數據后,同時可以看到后臺返回的信息:
在后臺可以看到連接的建立和收到的數據:
對于其他功能功能大家可以自己測測。
總結
websocket特別適合于需要實時數據傳送的場景,比輪詢方式效率高很多。
參考
WebSocket與消息推送
Java后端WebSocket的Tomcat實現
WebSocket 實戰
使用 HTML5 WebSocket 構建實時 Web 應用
混合移動應用的消息推送之 websocket
WebSocket 維基百科
WebSocket 接口文檔
RFC 6455 規范
JSR 356, Java API for WebSocket