眾所周知,瀏覽器本身不支持相互之間直接建立信道進行通信,都是通過服務器進行中轉。比如現在有兩個客戶端,甲和乙,他們倆想要通信,首先需要甲和服務器、乙和服務器之間建立信道。甲給乙發送消息時,甲先將消息發送到服務器上,服務器對甲的消息進行中轉,發送到乙處,反過來也是一樣。這樣甲與乙之間的一次消息要通過兩段信道,通信的效率同時受制于這兩段信道的帶寬。同時這樣的信道并不適合數據流的傳輸,如何建立瀏覽器之間的點對點傳輸,一直困擾著開發者。WebRTC應運而生
WebRTC是一個開源項目,旨在使得瀏覽器能為實時通信(RTC)提供簡單的JavaScript接口。說的簡單明了一點就是讓瀏覽器提供JS的即時通信接口。這個接口所創立的信道并不是像WebSocket一樣,打通一個瀏覽器與WebSocket服務器之間的通信,而是通過一系列的信令,建立一個瀏覽器與瀏覽器之間(peer-to-peer)的信道,這個信道可以發送任何數據,而不需要經過服務器。并且WebRTC通過實現MediaStream,通過瀏覽器調用設備的攝像頭、話筒,使得瀏覽器之間可以傳遞音頻和視頻
這么好的功能,各大瀏覽器廠商自然不會置之不理。現在WebRTC已經可以在較新版的Chrome、Opera和Firefox中使用了,著名的瀏覽器兼容性查詢網站caniuse上給出了一份詳盡的瀏覽器兼容情況
另外根據36Kr前段時間的新聞Google推出支持WebRTC及Web Audio的Android 版Chrome 29@36kr和Android版Opera開始支持WebRTC,允許用戶在沒有任何插件的情況下實現語音和視頻聊天,Android也開始支持WebRTC
WebRTC實現了三個API,分別是:
* MediaStream:通過MediaStream的API能夠通過設備的攝像頭及話筒獲得視頻、音頻的同步流
* RTCPeerConnection:RTCPeerConnection是WebRTC用于構建點對點之間穩定、高效的流傳輸的組件
* RTCDataChannel:RTCDataChannel使得瀏覽器之間(點對點)建立一個高吞吐量、低延時的信道,用于傳輸任意數據
這里大致上介紹一下這三個API
MediaStream API為WebRTC提供了從設備的攝像頭、話筒獲取視頻、音頻流數據的功能
W3C標準
W3C標準傳送門
如何調用
同門可以通過調用navigator.getUserMedia(),這個方法接受三個參數:
1. 一個約束對象(constraints object),這個后面會單獨講
2. 一個調用成功的回調函數,如果調用成功,傳遞給它一個流對象
3. 一個調用失敗的回調函數,如果調用失敗,傳遞給它一個錯誤對象
瀏覽器兼容性
由于瀏覽器實現不同,他們經常會在實現標準版本之前,在方法前面加上前綴,所以一個兼容版本就像這樣
var getUserMedia = (navigator.getUserMedia ||
navigator.webkitGetUserMedia ||
navigator.mozGetUserMedia ||
navigator.msGetUserMedia);
一個超級簡單的例子
這里寫一個超級簡單的例子,用來展現getUserMedia的效果:
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>GetUserMedia實例</title>
</head>
<body>
<video id="video" autoplay></video>
</body>
<script type="text/javascript">
var getUserMedia = (navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia);
getUserMedia.call(navigator, {
video: true,
audio: true
}, function(localMediaStream) {
var video = document.getElementById('video');
video.src = window.URL.createObjectURL(localMediaStream);
video.onloadedmetadata = function(e) {
console.log("Label: " + localMediaStream.label);
console.log("AudioTracks" , localMediaStream.getAudioTracks());
console.log("VideoTracks" , localMediaStream.getVideoTracks());
};
}, function(e) {
console.log('Reeeejected!', e);
});
</script>
</html>
將這段內容保存在一個HTML文件中,放在服務器上。用較新版本的Opera、Firefox、Chrome打開,在瀏覽器彈出詢問是否允許訪問攝像頭和話筒,選同意,瀏覽器上就會出現攝像頭所拍攝到的畫面了
注意,HTML文件要放在服務器上,否則會得到一個NavigatorUserMediaError的錯誤,顯示PermissionDeniedError,最簡單方法就是cd到HTML文件所在目錄下,然后python -m SimpleHTTPServer(裝了python的話),然后在瀏覽器中輸入http://localhost:8000/{文件名稱}.html
這里使用getUserMedia獲得流之后,需要將其輸出,一般是綁定到video標簽上輸出,需要使用window.URL.createObjectURL(localMediaStream)來創造能在video中使用src屬性播放的Blob URL,注意在video上加入autoplay屬性,否則只能捕獲到一張圖片
流創建完畢后可以通過label屬性來獲得其唯一的標識,還可以通過getAudioTracks()和getVideoTracks()方法來獲得流的追蹤對象數組(如果沒有開啟某種流,它的追蹤對象數組將是一個空數組)
約束對象(Constraints)
約束對象可以被設置在getUserMedia()和RTCPeerConnection的addStream方法中,這個約束對象是WebRTC用來指定接受什么樣的流的,其中可以定義如下屬性:
* video: 是否接受視頻流
* audio:是否接受音頻流
* MinWidth: 視頻流的最小寬度
* MaxWidth:視頻流的最大寬度
* MinHeight:視頻流的最小高度
* MaxHiehgt:視頻流的最大高度
* MinAspectRatio:視頻流的最小寬高比
* MaxAspectRatio:視頻流的最大寬高比
* MinFramerate:視頻流的最小幀速率
* MaxFramerate:視頻流的最大幀速率
詳情見Resolution Constraints in Web Real Time Communications draft-alvestrand-constraints-resolution-00
WebRTC使用RTCPeerConnection來在瀏覽器之間傳遞流數據,這個流數據通道是點對點的,不需要經過服務器進行中轉。但是這并不意味著我們能拋棄服務器,我們仍然需要它來為我們傳遞信令(signaling)來建立這個信道。WebRTC沒有定義用于建立信道的信令的協議:信令并不是RTCPeerConnection API的一部分
信令
既然沒有定義具體的信令的協議,我們就可以選擇任意方式(AJAX、WebSocket),采用任意的協議(SIP、XMPP)來傳遞信令,建立信道,比如我寫的demo,就是用的node的ws模塊,在WebSocket上傳遞信令
需要信令來交換的信息有三種:
* session的信息:用來初始化通信還有報錯
* 網絡配置:比如IP地址和端口啥的
* 媒體適配:發送方和接收方的瀏覽器能夠接受什么樣的編碼器和分辨率
這些信息的交換應該在點對點的流傳輸之前就全部完成,一個大致的架構圖如下:
通過服務器建立信道
這里再次重申,就算WebRTC提供瀏覽器之間的點對點信道進行數據傳輸,但是建立這個信道,必須有服務器的參與。WebRTC需要服務器對其進行四方面的功能支持:
1. 用戶發現以及通信
2. 信令傳輸
3. NAT/防火墻穿越
4. 如果點對點通信建立失敗,可以作為中轉服務器
NAT/防火墻穿越技術
建立點對點信道的一個常見問題,就是NAT穿越技術。在處于使用了NAT設備的私有TCP/IP網絡中的主機之間需要建立連接時需要使用NAT穿越技術。以往在VoIP領域經常會遇到這個問題。目前已經有很多NAT穿越技術,但沒有一項是完美的,因為NAT的行為是非標準化的。這些技術中大多使用了一個公共服務器,這個服務使用了一個從全球任何地方都能訪問得到的IP地址。在RTCPeeConnection中,使用ICE框架來保證RTCPeerConnection能實現NAT穿越
ICE,全名叫交互式連接建立(Interactive Connectivity Establishment),一種綜合性的NAT穿越技術,它是一種框架,可以整合各種NAT穿越技術如STUN、TURN(Traversal Using Relay NAT 中繼NAT實現的穿透)。ICE會先使用STUN,嘗試建立一個基于UDP的連接,如果失敗了,就會去TCP(先嘗試HTTP,然后嘗試HTTPS),如果依舊失敗ICE就會使用一個中繼的TURN服務器。
我們可以使用Google的STUN服務器:stun:stun.l.google.com:19302,于是乎,一個整合了ICE框架的架構應該長這個樣子
瀏覽器兼容
還是前綴不同的問題,采用和上面類似的方法:
var PeerConnection = (window.PeerConnection ||
window.webkitPeerConnection00 ||
window.webkitRTCPeerConnection ||
window.mozRTCPeerConnection);
創建和使用
//使用Google的stun服務器
var iceServer = {
"iceServers": [{
"url": "stun:stun.l.google.com:19302"
}]
};
//兼容瀏覽器的getUserMedia寫法
var getUserMedia = (navigator.getUserMedia ||
navigator.webkitGetUserMedia ||
navigator.mozGetUserMedia ||
navigator.msGetUserMedia);
//兼容瀏覽器的PeerConnection寫法
var PeerConnection = (window.PeerConnection ||
window.webkitPeerConnection00 ||
window.webkitRTCPeerConnection ||
window.mozRTCPeerConnection);
//與后臺服務器的WebSocket連接
var socket = __createWebSocketChannel();
//創建PeerConnection實例
var pc = new PeerConnection(iceServer);
//發送ICE候選到其他客戶端
pc.onicecandidate = function(event){
socket.send(JSON.stringify({
"event": "__ice_candidate",
"data": {
"candidate": event.candidate
}
}));
};
//如果檢測到媒體流連接到本地,將其綁定到一個video標簽上輸出
pc.onaddstream = function(event){
someVideoElement.src = URL.createObjectURL(event.stream);
};
//獲取本地的媒體流,并綁定到一個video標簽上輸出,并且發送這個媒體流給其他客戶端
getUserMedia.call(navigator, {
"audio": true,
"video": true
}, function(stream){
//發送offer和answer的函數,發送本地session描述
var sendOfferFn = function(desc){
pc.setLocalDescription(desc);
socket.send(JSON.stringify({
"event": "__offer",
"data": {
"sdp": desc
}
}));
},
sendAnswerFn = function(desc){
pc.setLocalDescription(desc);
socket.send(JSON.stringify({
"event": "__answer",
"data": {
"sdp": desc
}
}));
};
//綁定本地媒體流到video標簽用于輸出
myselfVideoElement.src = URL.createObjectURL(stream);
//向PeerConnection中加入需要發送的流
pc.addStream(stream);
//如果是發送方則發送一個offer信令,否則發送一個answer信令
if(isCaller){
pc.createOffer(sendOfferFn);
} else {
pc.createAnswer(sendAnswerFn);
}
}, function(error){
//處理媒體流創建失敗錯誤
});
//處理到來的信令
socket.onmessage = function(event){
var json = JSON.parse(event.data);
//如果是一個ICE的候選,則將其加入到PeerConnection中,否則設定對方的session描述為傳遞過來的描述
if( json.event === "__ice_candidate" ){
pc.addIceCandidate(new RTCIceCandidate(json.data.candidate));
} else {
pc.setRemoteDescription(new RTCSessionDescription(json.data.sdp));
}
};
實例
由于涉及較為復雜靈活的信令傳輸,故這里不做簡短的實例,可以直接移步到最后
既然能建立點對點的信道來傳遞實時的視頻、音頻數據流,為什么不能用這個信道傳一點其他數據呢?RTCDataChannel API就是用來干這個的,基于它我們可以在瀏覽器之間傳輸任意數據。DataChannel是建立在PeerConnection上的,不能單獨使用
使用DataChannel
我們可以使用channel = pc.createDataCHannel("someLabel");來在PeerConnection的實例上創建Data Channel,并給與它一個標簽
DataChannel使用方式幾乎和WebSocket一樣,有幾個事件:
* onopen
* onclose
* onmessage
* onerror
同時它有幾個狀態,可以通過readyState獲?。?/p>
* connecting: 瀏覽器之間正在試圖建立channel
* open:建立成功,可以使用send方法發送數據了
* closing:瀏覽器正在關閉channel
* closed:channel已經被關閉了
兩個暴露的方法:
* close(): 用于關閉channel
* send():用于通過channel向對方發送數據
通過Data Channel發送文件大致思路
JavaScript已經提供了File API從input[type='file']的元素中提取文件,并通過FileReader來將文件的轉換成DataURL,這也意味著我們可以將DataURL分成多個碎片來通過Channel來進行文件傳輸
SkyRTC-demo,這是我寫的一個Demo。建立一個視頻聊天室,并能夠廣播文件,當然也支持單對單文件傳輸,寫得還很粗糙,后期會繼續完善
使用方式
1.下載解壓并cd到目錄下
2.運行npm install安裝依賴的庫(express, ws, node-uuid)
3.運行node server.js,訪問localhost:3000,允許攝像頭訪問
4.打開另一臺電腦,在瀏覽器(Chrome和Opera,還未兼容Firefox)打開{server所在IP}:3000,允許攝像頭和話筒訪問
5.廣播文件:在左下角選定一個文件,點擊“發送文件”按鈕
6.廣播信息:左下角input框輸入信息,點擊發送
7.可能會出錯,注意F12對話框,一般F5能解決
功能
視頻音頻聊天(連接了攝像頭和話筒,至少要有攝像頭),廣播文件(可單獨傳播,提供API,廣播就是基于單獨傳播實現的,可同時傳播多個,小文件還好說,大文件坐等內存吃光),廣播聊天信息
WebRTC官方網站
W3C-GetUserMedia
W3C-WebRTC
Capturing Audio & Video in HTML5@html5rocks
Getting Started with WebRTC@html5rocks
caniuse
ICE交互式連接建立
文地址:https://dwz.cn/87Mq3Br1
作者:yizhiwazi
推薦WebSocket的三大理由:
快速學會通過WebSocket編寫簡單聊天功能。
溫馨提示:
1、WebSocket是HTML5開始提供的一種在單個 TCP 連接上進行全雙工通訊的協議。在WebSocket API中,瀏覽器和服務器只需要做一個握手的動作,然后,瀏覽器和服務器之間就形成了一條快速通道。兩者之間就直接可以數據互相傳送。
2、瀏覽器通過 JavaScript 向服務器發出建立 WebSocket 連接的請求,連接建立以后,客戶端和服務器端就可以通過 TCP 連接直接交換數據。
3、當你獲取 Web Socket 連接后,你可以通過 send() 方法來向服務器發送數據,并通過 onmessage 事件來接收服務器返回的數據。
一、打造 WebSocket 聊天客戶端
溫馨提示:得益于W3C國際標準的實現,我們在瀏覽器JS就能直接創建WebSocket對象,再通過簡單的回調函數就能完成WebSocket客戶端的編寫,非常簡單!接下來讓我們一探究竟。
使用說明:
使用步驟:1、獲取WebSocket客戶端對象。
例如: var webSocket = new WebSocket(url);
使用步驟:2、獲取WebSocket回調函數。
例如:webSocket.onmessage = function (event) {console.log('WebSocket收到消息:' + event.data);
使用步驟:3、發送消息給服務端
例如:webSokcet.send(jsonStr) 結合實際場景 本案例采用JSON字符串進行消息通信。
具體實現:
下面是本案例在線聊天的客戶端實現的JS代碼,附帶詳細注釋。
<script> /** * WebSocket客戶端 * * 使用說明: * 1、WebSocket客戶端通過回調函數來接收服務端消息。例如:webSocket.onmessage * 2、WebSocket客戶端通過send方法來發送消息給服務端。例如:webSocket.send(); */ function getWebSocket() { /** * WebSocket客戶端 PS:URL開頭表示WebSocket協議 中間是域名端口 結尾是服務端映射地址 */ var webSocket = new WebSocket('ws://localhost:8080/chat'); /** * 當服務端打開連接 */ webSocket.onopen = function (event) { console.log('WebSocket打開連接'); }; /** * 當服務端發來消息:1.廣播消息 2.更新在線人數 */ webSocket.onmessage = function (event) { console.log('WebSocket收到消息:%c' + event.data, 'color:green'); //獲取服務端消息 var message = JSON.parse(event.data) || {}; var $messageContainer = $('.message-container'); //喉嚨發炎 if (message.type === 'SPEAK') { $messageContainer.append( '<div class="mdui-card" style="margin: 10px 0;">' + '<div class="mdui-card-primary">' + '<div class="mdui-card-content message-content">' + message.username + ":" + message.msg + '</div>' + '</div></div>'); } $('.chat-num').text(message.onlineCount); //防止刷屏 var $cards = $messageContainer.children('.mdui-card:visible').toArray(); if ($cards.length > 5) { $cards.forEach(function (item, index) { index < $cards.length - 5 && $(item).slideUp('fast'); }); } }; /** * 關閉連接 */ webSocket.onclose = function (event) { console.log('WebSocket關閉連接'); }; /** * 通信失敗 */ webSocket.onerror = function (event) { console.log('WebSocket發生異常'); }; return webSocket; } var webSocket = getWebSocket(); /** * 通過WebSocket對象發送消息給服務端 */ function sendMsgToServer() { var $message = $('#msg'); if ($message.val()) { webSocket.send(JSON.stringify({username: $('#username').text(), msg: $message.val()})); $message.val(null); } } /** * 清屏 */ function clearMsg(){ $(".message-container").empty(); } /** * 使用ENTER發送消息 */ document.onkeydown = function (event) { var e = event || window.event || arguments.callee.caller.arguments[0]; e.keyCode === 13 && sendMsgToServer(); }; </script>
========================================================================
二、打造 WebSocket 聊天服務端
溫馨提示:得益于SpringBoot提供的自動配置,我們只需要通過簡單注解@ServerEndpoint就就能創建WebSocket服務端,再通過簡單的回調函數就能完成WebSocket服務端的編寫,比起客戶端的使用同樣非常簡單!
使用說明:
首先在POM文件引入spring-boot-starter-websocket 、thymeleaf 、FastJson等依賴。
使用步驟:1、開啟WebSocket服務端的自動注冊。
【這里需要特別提醒:ServerEndpointExporter 是由Spring官方提供的標準實現,用于掃描ServerEndpointConfig配置類和@ServerEndpoint注解實例。使用規則也很簡單:1.如果使用默認的嵌入式容器 比如Tomcat 則必須手工在上下文提供ServerEndpointExporter。2. 如果使用外部容器部署war包,則不要提供提供ServerEndpointExporter,因為此時SpringBoot默認將掃描服務端的行為交給外部容器處理。】
@Configuration public class WebSocketConfig { @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } }
使用步驟:2、創建WebSocket服務端。
核心思路:
@Component @ServerEndpoint("/chat")//標記此類為服務端 public class WebSocketChatServer { /** * 全部在線會話 PS: 基于場景考慮 這里使用線程安全的Map存儲會話對象。 */ private static Map<String, Session> onlineSessions = new ConcurrentHashMap<>(); /** * 當客戶端打開連接:1.添加會話對象 2.更新在線人數 */ @OnOpen public void onOpen(Session session) { onlineSessions.put(session.getId(), session); sendMessageToAll(Message.jsonStr(Message.ENTER, "", "", onlineSessions.size())); } /** * 當客戶端發送消息:1.獲取它的用戶名和消息 2.發送消息給所有人 * <p> * PS: 這里約定傳遞的消息為JSON字符串 方便傳遞更多參數! */ @OnMessage public void onMessage(Session session, String jsonStr) { Message message = JSON.parseObject(jsonStr, Message.class); sendMessageToAll(Message.jsonStr(Message.SPEAK, message.getUsername(), message.getMsg(), onlineSessions.size())); } /** * 當關閉連接:1.移除會話對象 2.更新在線人數 */ @OnClose public void onClose(Session session) { onlineSessions.remove(session.getId()); sendMessageToAll(Message.jsonStr(Message.QUIT, "", "下線了!", onlineSessions.size())); } /** * 當通信發生異常:打印錯誤日志 */ @OnError public void onError(Session session, Throwable error) { error.printStackTrace(); } /** * 公共方法:發送信息給所有人 */ private static void sendMessageToAll(String msg) { onlineSessions.forEach((id, session) -> { try { session.getBasicRemote().sendText(msg); } catch (IOException e) { e.printStackTrace(); } }); } }
/** * WebSocket 聊天消息類 */ package com.hehe.chat; import com.alibaba.fastjson.JSON; /** * WebSocket 聊天消息類 */ public class Message { public static final String ENTER = "ENTER"; public static final String SPEAK = "SPEAK"; public static final String QUIT = "QUIT"; private String type;//消息類型 private String username; //發送人 private String msg; //發送消息 private int onlineCount; //在線用戶數 public static String jsonStr(String type, String username, String msg, int onlineTotal) { return JSON.toJSONString(new Message(type, username, msg, onlineTotal)); } public Message(String type, String username, String msg, int onlineCount) { this.type = type; this.username = username; this.msg = msg; this.onlineCount = onlineCount; } //這里省略get/set方法 請自行補充 }
三、WebSocket在線聊天案例的視頻演示
1、源碼下載
至此,我們完成了客戶端和服務端的編碼,由于篇幅有限,本教程的頁面代碼并未完整貼上,想要完整的體驗效果請在Github下載源碼。
2、視頻演示
上面一頓操作猛如虎,實際到底是啥樣子呢,接下來由哈士奇童鞋為我們演示最終版的在線聊天案例:
四、全文總結
1、使用WebSocket用于實時雙向通訊的場景,常見的如聊天室、跨系統消息推送等。
2、創建WebSocket客戶端使用JS內置對象+回調函數+send方法發送消息。
3、創建WebSocket服務端使用注解聲明實例+使用注解聲明回調方法+使用Session發送消息。
們使用springboot和webSocket結合來編寫聊天室。
首先先了解下什么是WebSocket
WebSocket 是一種網絡通信協議。RFC6455 定義了它的通信標準。WebSocket 是 HTML5 開始提供的一種在單個 TCP 連接上進行全雙工通訊的協議。
了解計算機網絡協議的人,應該都知道:HTTP 協議是一種無狀態的、無連接的、單向的應用層協議。它采用了請求/響應模型。通信請求只能由客戶端發起,服務端對請求做出應答處理。這種通信模型有一個弊端:HTTP 協議無法實現服務器主動向客戶端發起消息。這種單向請求的特點,注定了如果服務器有連續的狀態變化,客戶端要獲知就非常麻煩。大多數 Web 應用程序將通過頻繁的異步JavaScript和XML(AJAX)請求實現長輪詢。輪詢的效率低,非常浪費資源(因為必須不停連接,或者 HTTP 連接始終打開)。因此,工程師們一直在思考,有沒有更好的方法。WebSocket 就是這樣發明的。WebSocket 連接允許客戶端和服務器之間進行全雙工通信,以便任一方都可以通過建立的連接將數據推送到另一端。WebSocket 只需要建立一次連接,就可以一直保持連接狀態。這相比于輪詢方式的不停建立連接顯然效率要大大提高。
Web瀏覽器和服務器都必須實現 WebSockets 協議來建立和維護連接。由于 WebSockets 連接長期存在,與典型的HTTP連接不同,對服務器有重要的影響?;诙嗑€程或多進程的服務器無法適用于 WebSockets,因為它旨在打開連接,盡可能快地處理請求,然后關閉連接。任何實際的 WebSockets 服務器端實現都需要一個異步服務器。
創建項目
如果不使用上述方法導入maven的 請使用
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency>
package com.example.websocket.demo; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.server.standard.ServerEndpointExporter; @Configuration public class WebSocketConfig { @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } }
package com.cloudkd.websocket; import java.io.IOException; import java.util.concurrent.CopyOnWriteArraySet; import javax.websocket.OnClose; import javax.websocket.OnError; import javax.websocket.OnMessage; import javax.websocket.OnOpen; import javax.websocket.Session; import javax.websocket.server.ServerEndpoint; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; //@ServerEndpoint("/websocket/{user}") @ServerEndpoint("/websocket") @Component public class WebSocketServer { private static final Logger log = LoggerFactory.getLogger(WebSocketServer.class); //靜態變量,用來記錄當前在線連接數。應該把它設計成線程安全的。 private static int onlineCount = 0; //concurrent包的線程安全Set,用來存放每個客戶端對應的MyWebSocket對象。 private static CopyOnWriteArraySet<WebSocketServer> webSocketSet = new CopyOnWriteArraySet<WebSocketServer>(); //與某個客戶端的連接會話,需要通過它來給客戶端發送數據 private Session session; /** * 連接建立成功調用的方法 */ @OnOpen public void onOpen(Session session) { this.session = session; //加入set中 webSocketSet.add(this); //在線數加1 addOnlineCount(); log.info("有新連接加入!當前在線人數為" + getOnlineCount()); try { sendMessage("連接成功"); } catch (IOException e) { log.error("websocket IO異常"); } } // //連接打開時執行 // @OnOpen // public void onOpen(@PathParam("user") String user, Session session) { // currentUser = user; // System.out.println("Connected ... " + session.getId()); // } /** * 連接關閉調用的方法 */ @OnClose public void onClose() { webSocketSet.remove(this); //從set中刪除 subOnlineCount(); //在線數減1 log.info("有一連接關閉!當前在線人數為" + getOnlineCount()); } /** * 收到客戶端消息后調用的方法 * * @param message 客戶端發送過來的消息 */ @OnMessage public void onMessage(String message, Session session) { log.info("來自客戶端的消息:" + message); //群發消息 for (WebSocketServer item : webSocketSet) { try { item.sendMessage(message); } catch (IOException e) { e.printStackTrace(); } } } /** * @param session * @param error */ @OnError public void onError(Session session, Throwable error) { log.error("發生錯誤"); error.printStackTrace(); } public void sendMessage(String message) throws IOException { this.session.getBasicRemote().sendText(message); } /** * 群發自定義消息 */ public static void sendInfo(String message) { log.info(message); for (WebSocketServer item : webSocketSet) { try { item.sendMessage(message); } catch (IOException ignored) { } } } private static synchronized int getOnlineCount() { return onlineCount; } private static synchronized void addOnlineCount() { WebSocketServer.onlineCount++; } private static synchronized void subOnlineCount() { WebSocketServer.onlineCount--; } }
圖中位置創建一個簡單的index.html頁面
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <script> var websocket = null; //判斷當前瀏覽器是否支持WebSocket if ('WebSocket' in window) { //這里ws://192.168.1.111:8080/websocket 寫自己的ip和端口號 websocket = new WebSocket("ws://192.168.1.111:8080/websocket"); } else { alert('Not support websocket') } //連接發生錯誤的回調方法 websocket.onerror = function () { setMessageInnerHTML("error"); }; //連接成功建立的回調方法 websocket.onopen = function (event) { setMessageInnerHTML("open"); } //接收到消息的回調方法 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> </head> <body> <h3>Welcome</h3><br/> <input id="text" type="text"/> <button onclick="send()">Send</button> <button onclick="closeWebSocket()">Close</button> <div id="message"></div> </body> </html>
點擊啟動項目
Ok啟動完成
訪問index.html測試一下
Ok成功啟動
后臺日志也有記錄
說個話試試
也可以多開幾個頁面測試下.
Ok可以了. 下面完來完成服務端向客戶端推消息
產生一個消息:產生消息場景有多種,http(s),定時任務,mq等,這里我用一個http請求的controller代碼完成
編寫一個pushWebController類
package com.example.websocket.demo; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.util.HashMap; import java.util.Map; @RestController public class PushWebController { private final Logger logger = LoggerFactory.getLogger(this.getClass()); @GetMapping(value = "/pushWeb") public Map<String, Object> pushVideoListToWeb(String message) { Map<String, Object> result = new HashMap<String, Object>(); try { WebSocketServer.sendInfo("有新客戶呼入,message:" + message); result.put("operationResult", true); } catch (Exception e) { result.put("operationResult", true); } return result; } }
重新啟動項目.測試下
成功了
到此位置. demo已經成功實現了. 自己感受下樂趣把.
如果你恰好也有可以用WebSocket實現的類似場景,希望對你有幫助。
Github地址:https://github.com/liangyisen/springboot_websocket
*請認真填寫需求信息,我們會在24小時內與您取得聯系。