整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          使用WebRTC搭建前端視頻聊天室-入門篇

          么是WebRTC?

          眾所周知,瀏覽器本身不支持相互之間直接建立信道進行通信,都是通過服務器進行中轉。比如現在有兩個客戶端,甲和乙,他們倆想要通信,首先需要甲和服務器、乙和服務器之間建立信道。甲給乙發送消息時,甲先將消息發送到服務器上,服務器對甲的消息進行中轉,發送到乙處,反過來也是一樣。這樣甲與乙之間的一次消息要通過兩段信道,通信的效率同時受制于這兩段信道的帶寬。同時這樣的信道并不適合數據流的傳輸,如何建立瀏覽器之間的點對點傳輸,一直困擾著開發者。WebRTC應運而生

          WebRTC是一個開源項目,旨在使得瀏覽器能為實時通信(RTC)提供簡單的JavaScript接口。說的簡單明了一點就是讓瀏覽器提供JS的即時通信接口。這個接口所創立的信道并不是像WebSocket一樣,打通一個瀏覽器與WebSocket服務器之間的通信,而是通過一系列的信令,建立一個瀏覽器與瀏覽器之間(peer-to-peer)的信道,這個信道可以發送任何數據,而不需要經過服務器。并且WebRTC通過實現MediaStream,通過瀏覽器調用設備的攝像頭、話筒,使得瀏覽器之間可以傳遞音頻和視頻

          WebRTC已經在我們的瀏覽器中

          這么好的功能,各大瀏覽器廠商自然不會置之不理。現在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(getUserMedia)

          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

          RTCPeerConnection

          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

          既然能建立點對點的信道來傳遞實時的視頻、音頻數據流,為什么不能用這個信道傳一點其他數據呢?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來進行文件傳輸

          一個綜合的Demo

          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的三大理由:

          • 1、采用全雙工通信,擺脫傳統HTTP輪詢的窘境。
          • 2、采用W3C國際標準,完美支持HTML5。
          • 3、簡單高效,容易上手。

          學習目標

          快速學會通過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服務端。

          核心思路:

          • ① 通過注解@ServerEndpoint來聲明實例化WebSocket服務端。
          • ② 通過注解@OnOpen、@OnMessage、@OnClose、@OnError 來聲明回調函數。

          • ③ 通過ConcurrentHashMap保存全部在線會話對象。
          @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();
           }
           });
           }
          }
          
          • ④ 通過會話對象 javax.websocket.Session 來發消息給客戶端。
          /**
           * 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 是什么?

          WebSocket 是一種網絡通信協議。RFC6455 定義了它的通信標準。WebSocket 是 HTML5 開始提供的一種在單個 TCP 連接上進行全雙工通訊的協議。

          為什么需要 WebSocket ?

          了解計算機網絡協議的人,應該都知道:HTTP 協議是一種無狀態的、無連接的、單向的應用層協議。它采用了請求/響應模型。通信請求只能由客戶端發起,服務端對請求做出應答處理。這種通信模型有一個弊端:HTTP 協議無法實現服務器主動向客戶端發起消息。這種單向請求的特點,注定了如果服務器有連續的狀態變化,客戶端要獲知就非常麻煩。大多數 Web 應用程序將通過頻繁的異步JavaScript和XML(AJAX)請求實現長輪詢。輪詢的效率低,非常浪費資源(因為必須不停連接,或者 HTTP 連接始終打開)。因此,工程師們一直在思考,有沒有更好的方法。WebSocket 就是這樣發明的。WebSocket 連接允許客戶端和服務器之間進行全雙工通信,以便任一方都可以通過建立的連接將數據推送到另一端。WebSocket 只需要建立一次連接,就可以一直保持連接狀態。這相比于輪詢方式的不停建立連接顯然效率要大大提高。

          WebSocket 如何工作?

          Web瀏覽器和服務器都必須實現 WebSockets 協議來建立和維護連接。由于 WebSockets 連接長期存在,與典型的HTTP連接不同,對服務器有重要的影響?;诙嗑€程或多進程的服務器無法適用于 WebSockets,因為它旨在打開連接,盡可能快地處理請求,然后關閉連接。任何實際的 WebSockets 服務器端實現都需要一個異步服務器。

          一.使用idea創建springboot項目


          創建項目


          如果不使用上述方法導入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>
          

          二.spring注入bean

          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();
           }
          }
          

          三.編寫websocket服務類

          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


          主站蜘蛛池模板: A国产一区二区免费入口| 国产午夜精品一区二区三区| 亚洲一区二区精品视频| 国产一区二区三区精品久久呦| 国产香蕉一区二区三区在线视频 | 国产精品丝袜一区二区三区| 久久精品一区二区三区日韩| 日韩伦理一区二区| 久久se精品一区精品二区| 国产一区在线观看免费| 人妻少妇AV无码一区二区| 成人免费视频一区| 日韩在线不卡免费视频一区| 国产在线视频一区二区三区| 精品无码一区二区三区爱欲| 欧美日韩一区二区成人午夜电影| 精品无码国产一区二区三区AV| 日韩久久精品一区二区三区 | 无码少妇丰满熟妇一区二区 | 日本欧洲视频一区| 无码精品蜜桃一区二区三区WW| 日韩一区二区三区四区不卡| 少妇一晚三次一区二区三区| 日韩毛片一区视频免费| 国产成人午夜精品一区二区三区| 国产精品视频一区二区三区经 | 日韩毛片基地一区二区三区| 中文日韩字幕一区在线观看| 国产亚洲福利精品一区| 亚洲AV无码一区东京热久久 | 中文字幕aⅴ人妻一区二区 | 末成年女A∨片一区二区| 人妻免费一区二区三区最新| 成人精品视频一区二区三区 | 波多野结衣一区二区三区88| 一区二区精品在线观看| 91麻豆精品国产自产在线观看一区 | 亚洲成av人片一区二区三区| 色老板在线视频一区二区| 久久精品一区二区三区日韩| 亚洲高清一区二区三区电影|