前言
之前一個(gè)項(xiàng)目中九風(fēng)開發(fā)app的用戶的消息部分妒蛇,由于項(xiàng)目比較緊机断,而且之前沒有接觸過WebSocket開發(fā),所以暫時(shí)先使用輪詢方式來開發(fā)消息模塊绣夺,最近準(zhǔn)備升級消息模塊吏奸,準(zhǔn)備使用tomcat的WebSocket來開發(fā)消息,寫此文章方便自己也方便大家陶耍。
如需馬上測試的scoket的請直接往下翻到代碼出奋蔚。
這篇文章中的代碼不能運(yùn)行在spring mvc模式下,如需在mvc模式下運(yùn)行烈钞,請參考這篇Sring MVC 模式下使用websocket泊碑。
特別說明
此文章中的后臺(tái)代碼不能直接用于Spring MVC中web層、service層直接調(diào)用毯欣,下篇文章準(zhǔn)備寫這個(gè)(還沒寫好馒过,九風(fēng)盡快), 文章中有需要改正的還請簡友指出酗钞。
消息推送
消息推送大家都不陌生腹忽,比如扣扣消息、某東某寶購物后的系統(tǒng)消息等等都是消息推送砚作,在H5出來之前窘奏,消息推送基本上都是使用HTTP請求的,但HTTP請求只能在客戶端發(fā)起請求后服務(wù)端返回消息葫录,而不能再客戶端未發(fā)起請求時(shí)服務(wù)端主動(dòng)推送消息給客戶端蔼夜,而對于HTTP的方式實(shí)現(xiàn)消息推送時(shí),有以下幾種方式:
輪詢方式:客戶端定時(shí)向服務(wù)端發(fā)送ajax請求压昼,服務(wù)器接收到請求后馬上返回消息并關(guān)閉連接求冷。
優(yōu)點(diǎn):后端程序編寫比較容易。
缺點(diǎn):TCP的建立和關(guān)閉操作浪費(fèi)時(shí)間和帶寬窍霞,請求中有大半是無用匠题,浪費(fèi)帶寬和服務(wù)器資源。
實(shí)例:適于小型應(yīng)用但金。
長輪詢:客戶端向服務(wù)器發(fā)送Ajax請求韭山,服務(wù)器接到請求后hold住連接,直到有新消息才返回響應(yīng)信息并關(guān)閉連接,客戶端處理完響應(yīng)信息后再向服務(wù)器發(fā)送新的請求钱磅。
優(yōu)點(diǎn):在無消息的情況下不會(huì)頻繁的請求梦裂,耗費(fèi)資源小。
缺點(diǎn):服務(wù)器hold連接會(huì)消耗資源盖淡,返回?cái)?shù)據(jù)順序無保證年柠,難于管理維護(hù)。
實(shí)例:WebQQ褪迟、Hi網(wǎng)頁版冗恨、Facebook IM。
長連接:在頁面里嵌入一個(gè)隱蔵iframe味赃,將這個(gè)隱蔵iframe的src屬性設(shè)為對一個(gè)長連接的請求或是采用xhr請求掀抹,服務(wù)器端就能源源不斷地往客戶端輸入數(shù)據(jù)。
優(yōu)點(diǎn):消息即時(shí)到達(dá)心俗,不發(fā)無用請求傲武;管理起來也相對方便。
缺點(diǎn):服務(wù)器維護(hù)一個(gè)長連接會(huì)增加開銷城榛,當(dāng)客戶端越來越多的時(shí)候谱轨,server壓力大!
實(shí)例:Gmail聊天
Flash Socket:在頁面中內(nèi)嵌入一個(gè)使用了Socket類的 Flash 程序JavaScript通過調(diào)用此Flash程序提供的Socket接口與服務(wù)器端的Socket接口進(jìn)行通信吠谢,JavaScript在收到服務(wù)器端傳送的信息后控制頁面的顯示土童。
優(yōu)點(diǎn):實(shí)現(xiàn)真正的即時(shí)通信,而不是偽即時(shí)工坊。
缺點(diǎn):客戶端必須安裝Flash插件献汗,移動(dòng)端支持不好,IOS系統(tǒng)中沒有flash的存在王污;非HTTP協(xié)議罢吃,無法自動(dòng)穿越防火墻。
實(shí)例:網(wǎng)絡(luò)互動(dòng)游戲昭齐。
webSocket:HTML5 WebSocket設(shè)計(jì)出來的目的就是取代輪詢和長連接尿招,使客戶端瀏覽器具備像C/S框架下桌面系統(tǒng)的即時(shí)通訊能力,實(shí)現(xiàn)了瀏覽器和服務(wù)器全雙工通信阱驾,建立在TCP之上就谜,雖然WebSocket和HTTP一樣通過TCP來傳輸數(shù)據(jù),但WebSocket可以主動(dòng)的向?qū)Ψ桨l(fā)送或接收數(shù)據(jù)里覆,就像Socket一樣丧荐;并且WebSocket需要類似TCP的客戶端和服務(wù)端通過握手連接,連接成功后才能互相通信喧枷。
優(yōu)點(diǎn):雙向通信虹统、事件驅(qū)動(dòng)弓坞、異步、使用ws或wss協(xié)議的客戶端能夠真正實(shí)現(xiàn)意義上的推送功能车荔。
缺點(diǎn):少部分瀏覽器不支持渡冻。
示例:社交聊天(微信、QQ)忧便、彈幕族吻、多玩家玩游戲、協(xié)同編輯茬腿、股票基金實(shí)時(shí)報(bào)價(jià)、體育實(shí)況更新宜雀、視頻會(huì)議/聊天切平、基于位置的應(yīng)用、在線教育辐董、智能家居等高實(shí)時(shí)性的場景悴品。
而websocket請求和服務(wù)器交互的如下圖所示:
對比前面的http的客戶端服務(wù)器的交互圖可以發(fā)現(xiàn)WebSocket方式減少了很多TCP打開和關(guān)閉連接的操作,WebSocket的資源利用率高简烘。
WebSocket規(guī)范
WebSocket一種在單個(gè) TCP 連接上進(jìn)行全雙工通訊的協(xié)議苔严。WebSocket通信協(xié)議于2011年被IETF定為標(biāo)準(zhǔn)RFC 6455,并被RFC7936所補(bǔ)充規(guī)范孤澎。WebSocket API也被W3C定為標(biāo)準(zhǔn)届氢。
WebSocket 使得客戶端和服務(wù)器之間的數(shù)據(jù)交換變得更加簡單,允許服務(wù)端主動(dòng)向客戶端推送數(shù)據(jù)覆旭。在 WebSocket API 中退子,瀏覽器和服務(wù)器只需要完成一次握手,兩者之間就直接可以創(chuàng)建持久性的連接型将,并進(jìn)行雙向數(shù)據(jù)傳輸寂祥。
WebSocket 協(xié)議本質(zhì)上是一個(gè)基于 TCP 的協(xié)議。為了建立一個(gè) WebSocket 連接七兜,客戶端瀏覽器首先要向服務(wù)器發(fā)起一個(gè) HTTP 請求丸凭,這個(gè)請求和通常的 HTTP 請求不同,包含了一些附加頭信息腕铸,附加信息如圖所示:
瀏覽器支持:所有的最新瀏覽器支持最新WebSocket規(guī)范(RFC 6455) 惜犀,從維基百科上介紹瀏覽器對WebSocket的支持如下表所示:
瀏覽器 | Chrome | Edge | Firfox | IE | Opera | Safari |
---|---|---|---|---|---|---|
最低版本 | 16 | 支持 | 11.0 | 10 | 12.10 | 6.0 |
移動(dòng)端支持:移動(dòng)端基本都支持websocket了,其實(shí)和瀏覽器版支持的版本一樣狠裹,具體支持如下所示:
最低 | android瀏覽器 | Chrome 移動(dòng)版 | Firfox 移動(dòng)版 | Opera 移動(dòng)版 | Safari IOS版 |
---|---|---|---|---|---|
最低版本 | 4.4 | 16 | 11.0 | 12.10 | 6.0 |
服務(wù)器支持:目前主流的web服務(wù)器都已經(jīng)支持向拆,具體版本如下表所示:
廠商 | 應(yīng)用服務(wù)器 | 備注 |
---|---|---|
IBM | WebSphere | WebSphere 8.0 以上版本支持,7.X 之前版本結(jié)合 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+支持 |
以下內(nèi)容將使用tomcat服務(wù)器來實(shí)現(xiàn)Websocket
java WebSocket實(shí)現(xiàn)
Oracle 發(fā)布的 java 的 WebSocket 的規(guī)范是 JSR356規(guī)范 ,Tomcat從7.0.27開始支持WebSocket刹缝,從7.0.47開始支持JSR-356。
websocket簡單實(shí)現(xiàn)分為以下幾個(gè)步驟:添加websocket庫颈将、編寫后臺(tái)代碼梢夯、編寫前端代碼。
添加websocket庫
在maven中添加websocket庫的代碼如下所示:
<dependency>
<groupId>javax.websocket</groupId>
<artifactId>javax.websocket-api</artifactId>
<version>1.1</version>
<scope>provided</scope>
</dependency>
九風(fēng)有次沒寫<scope>字段晴圾,前端后臺(tái)都會(huì)報(bào)錯(cuò)颂砸,大家記得加上就行。
前端錯(cuò)誤內(nèi)容:
WebSocket connection to 'ws://localhost:8080/{project-name}/websocket' failed: Error during WebSocket handshake: Unexpected response code: 404" 死姚。
后臺(tái)錯(cuò)誤內(nèi)容:
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
編寫后臺(tái)代碼
后臺(tái)實(shí)現(xiàn)websocket有兩種方式:使用繼承類人乓、使用注解;注解方式比較方便都毒,一下代碼中使用注解方式來進(jìn)行演示色罚。
聲明websocket地址類似Spring MVC中的@controller注解類似,websocket使用@ServerEndpoint來進(jìn)行聲明接口:@ServerEndpoint(value="/websocket/{paraName}")
; 其中 “ { } ”用來表示帶參數(shù)的連接账劲,如果需要獲取{}中的參數(shù)在參數(shù)列表中增加:@PathParam("paraName") Integer userId 戳护。則連接地址形如:ws://localhost:8080/project-name/websocket/8,其中每個(gè)連接可以設(shè)置不同的paraName的值瀑焦。
注解腌且、成員數(shù)據(jù)介紹:
1.@OnOpen
public void onOpen(Session session) throws IOException{ } -------有連接時(shí)的觸發(fā)函數(shù)。 我們可以在用戶連接時(shí)記錄用戶的連接帶的參數(shù)榛瓮,只需在參數(shù)列表中增加參數(shù):@PathParam("paraName") String paraName铺董。
2.@OnClose
public void onClose(){ } ------連接關(guān)閉時(shí)的調(diào)用方法。
3.@OnMessage
public void onMessage(String message, Session session) { } -------收到消息時(shí)調(diào)用的函數(shù)禀晓,其中Session是每個(gè)websocket特有的數(shù)據(jù)成員柄粹,詳情見4.
4.Session ----每個(gè)Session代表了兩個(gè)web socket斷點(diǎn)的會(huì)話;當(dāng)websocket握手成功后匆绣,websocket就會(huì)提供一個(gè)打開的Session驻右,可以通過這個(gè)Session來對另一個(gè)端點(diǎn)發(fā)送數(shù)據(jù);如果Session關(guān)閉后發(fā)送數(shù)據(jù)將會(huì)報(bào)錯(cuò)崎淳。
5.Session.getBasicRemote().sendText("message") -------向該Session連接的用戶發(fā)送字符串?dāng)?shù)據(jù)堪夭。
6.@OnError
public void onError(Session session, Throwable error) { } --------發(fā)生意外錯(cuò)誤時(shí)調(diào)用的函數(shù)。
后臺(tái)代碼:有以上基礎(chǔ)后就直接上代碼了.
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 九風(fēng)萍舟
*/
@ServerEndpoint(value="/websocketTest/{userId}")
public class Test {
private Logger logger = LoggerFactory.getLogger(Test.class);
private static String userId;
//連接時(shí)執(zhí)行
@OnOpen
public void onOpen(@PathParam("userId") String userId,Session session) throws IOException{
this.userId = userId;
logger.debug("新連接:{}",userId);
}
//關(guān)閉時(shí)執(zhí)行
@OnClose
public void onClose(){
logger.debug("連接:{} 關(guān)閉",this.userId);
}
//收到消息時(shí)執(zhí)行
@OnMessage
public void onMessage(String message, Session session) throws IOException {
logger.debug("收到用戶{}的消息{}",this.userId,message);
session.getBasicRemote().sendText("收到 "+this.userId+" 的消息 "); //回復(fù)用戶
}
//連接錯(cuò)誤時(shí)執(zhí)行
@OnError
public void onError(Session session, Throwable error){
logger.debug("用戶id為:{}的連接發(fā)送錯(cuò)誤",this.userId);
error.printStackTrace();
}
}
ServerEndpoint報(bào)錯(cuò): 原因是不能自動(dòng)檢測 ServerEndpoint 的包拣凹,解決方法:復(fù)制 import javax.websocket.server.ServerEndpoint;
到文件程序 import 區(qū)域即可森爽。
編寫前端代碼
后臺(tái)代碼編寫了那么前端代碼就幾乎不用講解了,相信大家一眼就能看得懂嚣镜。
<!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">
//判斷當(dāng)前瀏覽器是否支持WebSocket
if('WebSocket' in window){
websocket = new WebSocket("ws://localhost:8080/Demo/websocketTest/user000");
console.log("link success")
}else{
alert('Not support websocket')
}
//連接發(fā)生錯(cuò)誤的回調(diào)方法
websocket.onerror = function(){
setMessageInnerHTML("error");
};
//連接成功建立的回調(diào)方法
websocket.onopen = function(event){
setMessageInnerHTML("open");
}
console.log("-----")
//接收到消息的回調(diào)方法
websocket.onmessage = function(event){
setMessageInnerHTML(event.data);
}
//連接關(guān)閉的回調(diào)方法
websocket.onclose = function(){
setMessageInnerHTML("close");
}
//監(jiān)聽窗口關(guān)閉事件爬迟,當(dāng)窗口關(guān)閉時(shí),主動(dòng)去關(guān)閉websocket連接菊匿,防止連接還沒斷開就關(guān)閉窗口付呕,server端會(huì)拋異常计福。
window.onbeforeunload = function(){
websocket.close();
}
//將消息顯示在網(wǎng)頁上
function setMessageInnerHTML(innerHTML){
document.getElementById('message').innerHTML += innerHTML + '<br/>';
}
//關(guān)閉連接
function closeWebSocket(){
websocket.close();
}
//發(fā)送消息
function send(){
var message = document.getElementById('text').value;
websocket.send(message);
}
</script>
</body>
</html>
測試運(yùn)行
在Chrome上打開前端代碼后,馬上就建立了連接徽职,大家可以使用F12查看下建立連接的請求與響應(yīng)象颖,可以對比前面關(guān)于協(xié)議建立的部分進(jìn)行學(xué)習(xí)。
建立連接后姆钉,想后臺(tái)發(fā)送數(shù)據(jù)后说订,同時(shí)可以看到后臺(tái)返回的信息:
在后臺(tái)可以看到連接的建立和收到的數(shù)據(jù):
對于其他功能功能大家可以自己測測。
總結(jié)
websocket特別適合于需要實(shí)時(shí)數(shù)據(jù)傳送的場景潮瓶,比輪詢方式效率高很多陶冷。
參考
WebSocket與消息推送
Java后端WebSocket的Tomcat實(shí)現(xiàn)
WebSocket 實(shí)戰(zhàn)
使用 HTML5 WebSocket 構(gòu)建實(shí)時(shí) Web 應(yīng)用
混合移動(dòng)應(yīng)用的消息推送之 websocket
WebSocket 維基百科
WebSocket 接口文檔
RFC 6455 規(guī)范
JSR 356, Java API for WebSocket