更新時間:2018年12月07日13時14分 來源:傳智播客 瀏覽次數(shù):
# WebSocket分析及實踐
## WebSocket產(chǎn)生背景:實時Web應(yīng)用的窘境
Web 應(yīng)用的信息交互過程通常是客戶端通過瀏覽器發(fā)出一個請求,服務(wù)器端接收和審核完請求后進行處理并返回結(jié)果給客戶端,然后客戶端瀏覽器將信息呈現(xiàn)出來,這種機制對于信息變化不是特別頻繁的應(yīng)用尚能相安無事,但是對于那些實時要求比較高的應(yīng)用來說,比如說實時報表統(tǒng)計、在線游戲、在線證券、設(shè)備監(jiān)控、新聞在線播報、RSS 訂閱推送等等,當(dāng)客戶端瀏覽器準(zhǔn)備呈現(xiàn)這些信息的時候,這些信息在服務(wù)器端可能已經(jīng)過時了。所以保持客戶端和服務(wù)器端的信息同步是實時 Web 應(yīng)用的關(guān)鍵要素,對 Web 開發(fā)人員來說也是一個難題。
? 在 WebSocket 出來之前,開發(fā)人員想實現(xiàn)這些實時的 Web 應(yīng)用,不得不采用一些折衷的方案,其中最常用的就是輪詢 (Polling) 和 Comet 技術(shù),而 Comet 技術(shù)實際上是輪詢技術(shù)的改進,又可細(xì)分為兩種實現(xiàn)方式,一種是長輪詢機制,一種稱為流技術(shù)。下面簡單介紹一下這幾種技術(shù):
- **輪詢**
這是最早的一種實現(xiàn)實時 Web 應(yīng)用的方案??蛻舳艘砸欢ǖ臅r間間隔向服務(wù)端發(fā)出請求,以頻繁請求的方式來保持客戶端和服務(wù)器端的同步。這種同步方案的最大問題是:當(dāng)客戶端以固定頻率向服務(wù)器發(fā)起請求的時候,服務(wù)器端的數(shù)據(jù)可能并沒有更新,這樣會帶來很多無謂的網(wǎng)絡(luò)傳輸,所以這是一種非常低效的實時方案。
- **長輪詢:**
長輪詢是對定時輪詢的改進和提高,目地是為了降低無效的網(wǎng)絡(luò)傳輸。當(dāng)服務(wù)器端沒有數(shù)據(jù)更新的時候,連接會保持一段時間周期直到數(shù)據(jù)或狀態(tài)改變或者時間過期,通過這種機制來減少無效的客戶端和服務(wù)器間的交互。當(dāng)然,如果服務(wù)端的數(shù)據(jù)變更非常頻繁的話,這種機制和定時輪詢比較起來沒有本質(zhì)上的性能的提高。
- **流:**
流技術(shù)方案通常就是在客戶端的頁面使用一個隱藏的窗口向服務(wù)端發(fā)出一個長連接的請求。服務(wù)器端接到這個請求后作出回應(yīng)并不斷更新連接狀態(tài)以保證客戶端和服務(wù)器端的連接不過期。通過這種機制可以將服務(wù)器端的信息源源不斷地推向客戶端。這種機制在用戶體驗上有一點問題,需要針對不同的瀏覽器設(shè)計不同的方案來改進用戶體驗,同時這種機制在并發(fā)比較大的情況下,對服務(wù)器端的資源是一個極大的考驗。
綜合這幾種方案,您會發(fā)現(xiàn)這些目前我們所使用的所謂的實時技術(shù)并不是真正的實時技術(shù),它們只是在用 Ajax 方式來模擬實時的效果,在每次客戶端和服務(wù)器端交互的時候都是一次 HTTP 的請求和應(yīng)答的過程,而每一次的 HTTP 請求和應(yīng)答都帶有完整的 HTTP 頭信息,這就增加了每次傳輸?shù)臄?shù)據(jù)量,而且這些方案中客戶端和服務(wù)器端的編程實現(xiàn)都比較復(fù)雜,在實際的應(yīng)用中,為了模擬比較真實的實時效果,開發(fā)人員往往需要構(gòu)造兩個 HTTP 連接來模擬客戶端和服務(wù)器之間的雙向通訊,一個連接用來處理客戶端到服務(wù)器端的數(shù)據(jù)傳輸,一個連接用來處理服務(wù)器端到客戶端的數(shù)據(jù)傳輸,這不可避免地增加了編程實現(xiàn)的復(fù)雜度,也增加了服務(wù)器端的負(fù)載,制約了應(yīng)用系統(tǒng)的擴展性。
##什么是WebSocket?
WebSocket是HTML5的新特性之一,其設(shè)計出來的目的就是要取代輪詢和 Comet 技術(shù),使客戶端瀏覽器具備像 C/S 架構(gòu)下桌面系統(tǒng)的實時通訊能力。
那WebSocket究竟是什么?首先我們需要清楚,WebSocket本質(zhì)上就是一種計算機網(wǎng)絡(luò)應(yīng)用層的協(xié)議(HTTP就是一種網(wǎng)絡(luò)應(yīng)用層協(xié)議),用來彌補HTTP協(xié)議在持久通信能力上的不足。我們知道HTTP協(xié)議本身是無狀態(tài)協(xié)議,每一個新的HTTP請求,只能通過客戶端主動發(fā)起,通過建立連接-->傳輸數(shù)據(jù)-->斷開連接的方式來傳輸數(shù)據(jù),傳送完連接就斷開了,也就是此次HTTP請求已經(jīng)完全結(jié)束了(雖然HTTP1.1增加了keep-alive請求頭可以通過一條通道請求多次,但本質(zhì)上還是一樣的)。并且服務(wù)器是不能主動給客戶端發(fā)送數(shù)據(jù)的(因為之前的請求得到響應(yīng)后連接就斷開了,之后服務(wù)器根本不知道誰請求過),客戶端也不會知道之前請求的任何信息,所以HTTP協(xié)議本身是沒有持久通信能力的,正因為這樣,也就出現(xiàn)了上述實時Web應(yīng)用的窘境。
WebSocket協(xié)議實現(xiàn)了瀏覽器與服務(wù)器的全雙工通信(指在通信的任意時刻,線路上存在A到B和B到A的雙向信號傳輸,簡單說就如同打電話一樣,瀏覽器和服務(wù)器任何一方隨時都能夠主動給對方說話)。并且在HTML5標(biāo)準(zhǔn)中增加了有關(guān)WebSocket協(xié)議的相關(guān)API,所以只要實現(xiàn)了HTML5標(biāo)準(zhǔn)的客戶端,就可以與支持WebSocket協(xié)議的服務(wù)器進行全雙工的持久通信了。
與HTTP協(xié)議一樣,WebSocket協(xié)議也需要通過已建立的TCP連接來傳輸數(shù)據(jù)。具體實現(xiàn)上是通過HTTP協(xié)議建立通道,然后在此基礎(chǔ)上用真正的WebSocket協(xié)議進行通信,所以WebSocket協(xié)議和Http協(xié)議是有一定的交叉關(guān)系的。Websocket是應(yīng)用層第七層上的一個應(yīng)用層協(xié)議,它必須依賴 HTTP 協(xié)議進行一次握手 ,握手成功后,數(shù)據(jù)就直接從TCP通道傳輸,與HTTP無關(guān)了。
## 為什么需要WebSocket?
瀏覽器通過 JavaScript 向服務(wù)器發(fā)出建立 WebSocket 連接的請求,連接建立以后,客戶端和服務(wù)器端就可以通過 TCP 連接直接交換數(shù)據(jù)。因為 WebSocket 連接本質(zhì)上就是一個 TCP 連接,所以在數(shù)據(jù)傳輸?shù)姆€(wěn)定性和數(shù)據(jù)傳輸量的大小方面,和輪詢以及 Comet 技術(shù)比較,具有很大的性能優(yōu)勢。Websocket.org 網(wǎng)站對傳統(tǒng)的輪詢方式和 WebSocket 調(diào)用方式作了一個詳細(xì)的測試和比較,將一個簡單的 Web 應(yīng)用分別用輪詢方式和 WebSocket 方式來實現(xiàn),在這里引用一下他們的測試結(jié)果圖:
通過這張圖可以清楚的看出,在流量和負(fù)載增大的情況下,WebSocket 方案相比傳統(tǒng)的 Ajax 輪詢方案有極大的性能優(yōu)勢。這也是為什么我們認(rèn)為 WebSocket 是未來實時 Web 應(yīng)用的首選方案的原因。
## WebSocket使用場景
什么時候使用WebSocket,你需要考慮如下兩個因素
- 你的應(yīng)用是否提供多個用戶之間的相互交流?
- 你的應(yīng)用是展示服務(wù)器端經(jīng)常變動的數(shù)據(jù)嗎?
如果上述兩個問題你的回答是肯定的,那么請考慮使用WebSocket。如果你還不確定,那有一些經(jīng)典場景,你可以參考并激發(fā)一下自己的靈感。
- **實時統(tǒng)計(圖表)**
你需要你的統(tǒng)計數(shù)據(jù)(或者圖表)實時更新,類似于淘寶雙11大屏那樣的效果
- **系統(tǒng)即時提醒**
- **實時地圖位置**
- **彈幕**
- **社交訂閱**
社交類的應(yīng)用的一個裨益之處就是能夠即時的知道你的朋友正在做什么。雖然聽起來有點可怕,但是我們都喜歡這樣做。你不會想要在數(shù)分鐘之后才知道微信朋友圈朋友發(fā)布的更新動態(tài)。你是在線的,所以你的訂閱的更新應(yīng)該是實時的。
- **股票基金報價**
金融界瞬息萬變——幾乎是每毫秒都在變化。我們?nèi)祟惖拇竽X不能持續(xù)以那樣的速度處理那么多的數(shù)據(jù),所以我們寫了一些算法來幫我們處理這些事情。雖然你不一定是在處理高頻的交易,但是,過時的信息也只能導(dǎo)致?lián)p失。當(dāng)你有一個顯示盤來跟蹤你感興趣的公司時,你肯定想要隨時知道他們的價值,而不是10秒前的數(shù)據(jù)。使用WebSocket可以流式更新這些數(shù)據(jù)變化而不需要等待。
- **體育實況更新**
如果你在你的網(wǎng)站應(yīng)用中包含了體育新聞,WebSocket能夠助力你的用戶獲得實時的更新。
- **基于位置的應(yīng)用**
越來越多的開發(fā)者借用移動設(shè)備的GPS功能來實現(xiàn)他們基于位置的應(yīng)用。如果你收集到了用戶的位置數(shù)據(jù)(比如記錄運動軌跡)。如果你想實時的更新網(wǎng)絡(luò)數(shù)據(jù)儀表盤(可以說是一個監(jiān)視運動員的教練),HTTP協(xié)議顯得有些笨拙。借用WebSocket TCP鏈接可以讓數(shù)據(jù)飛起來。
- **在線教育**
上學(xué)花費越來越貴了,但互聯(lián)網(wǎng)變得更快和更便宜。在線教育是一種不錯的學(xué)習(xí)方式,尤其是你可以和老師以及其他同學(xué)一起交流。此時,用WebSocket來實現(xiàn)是個不錯的選擇,可以多媒體聊天、文字聊天以及其它優(yōu)勢如與別人合作一起在公共數(shù)字黑板上畫畫等。
## 如何使用WebSocket?
使用WebSocket需要客戶端和服務(wù)器端,客戶端通常就是瀏覽器(基于瀏覽器進行的JavaScript調(diào)用),服務(wù)器端通常就是支持WebSocket的中間件。
- **客戶端瀏覽器支持(以下是主流瀏覽器對HTML5 WebSocket的支持情況)**
- **支持 WebSocket 的服務(wù)器**
服務(wù)器端的實現(xiàn)不受平臺和開發(fā)語言的限制,只需要遵從 WebSocket 規(guī)范即可,目前已經(jīng)出現(xiàn)了一些比較成熟的 WebSocket 服務(wù)器端實現(xiàn),比如Kaazing WebSocket Gateway(一個 Java 實現(xiàn)的 WebSocket Server)、mod_pywebsocket(一個 Python 實現(xiàn)的 WebSocket Server)、Netty(一個 Java 實現(xiàn)的網(wǎng)絡(luò)框架其中包括了對 WebSocket 的支持)、Node.js(一個 Server 端的 JavaScript 框架提供了對 WebSocket 的支持),當(dāng)然也可以使用Tomcat(需要為Tomcat7.0.47以上,且Tomcat7.x和Tomcat8.x的使用方式還不一樣)。
- **WebSocket JavaScript API接口**
針對 Web 開發(fā)人員的 WebSocket JavaScript 客戶端接口是非常簡單的,以下是 WebSocket JavaScript 接口的定義:
其中 URL 屬性代表 WebSocket 服務(wù)器的網(wǎng)絡(luò)地址,協(xié)議通常是”ws”,send 方法就是發(fā)送數(shù)據(jù)到服務(wù)器端,close 方法就是關(guān)閉連接。除了這些方法,還有一些很重要的事件:onopen,onmessage,onerror 以及 onclose。
##WebSocket實戰(zhàn)—系統(tǒng)即時提醒
在上一節(jié)中我們說到使用WebSocket需要客戶端和服務(wù)器端,客戶端通常就是瀏覽器(基于瀏覽器進行的JavaScript調(diào)用),服務(wù)器端通常就是支持WebSocket的中間件。本節(jié)中我們將進行一個WebSocket實戰(zhàn)—系統(tǒng)即時提醒(使用Chrome + Tomcat7.0.70,在此基礎(chǔ)上進行前后端代碼開發(fā)),后端模擬業(yè)務(wù)變化,向前端實時推送提醒消息,前端頁面進行消息提醒的實時展示。
- WebSocket客戶端代碼(基于Jsp)
```jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
```
- WebSocket服務(wù)器端代碼(基于Java)
```java
package com.itheima.ssm.controller;
import java.io.IOException;
import java.util.concurrent.CopyOnWriteArraySet;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
/**
* @ServerEndpoint 注解是一個類層次的注解,它的功能主要是將目前的類定義成一個websocket服務(wù)器端,
* 注解的值將被用于監(jiān)聽用戶連接的終端訪問URL地址,客戶端可以通過這個URL來連接到WebSocket服務(wù)器端
*/
@ServerEndpoint("/websocket")
public class WebSocketTest {
//靜態(tài)變量,用來記錄當(dāng)前在線連接數(shù)。應(yīng)該把它設(shè)計成線程安全的。
private static int onlineCount = 0;
//concurrent包的線程安全Set,用來存放每個客戶端對應(yīng)的MyWebSocket對象。若要實現(xiàn)服務(wù)端與單一客戶端通信的話,可以使用Map來存放,其中Key可以為用戶標(biāo)識
public static CopyOnWriteArraySet webSocketSet = new CopyOnWriteArraySet();
//與某個客戶端的連接會話,需要通過它來給客戶端發(fā)送數(shù)據(jù)
private Session session;
/**
* 連接建立成功調(diào)用的方法
* @param session 可選的參數(shù)。session為與某個客戶端的連接會話,需要通過它來給客戶端發(fā)送數(shù)據(jù)
*/
@OnOpen
public void onOpen(Session session){
this.session = session;
webSocketSet.add(this); //加入set中
addOnlineCount(); //在線客戶端數(shù)加1
System.out.println("有新連接加入!當(dāng)前在線客戶端數(shù)為" + getOnlineCount());
}
/**
* 連接關(guān)閉調(diào)用的方法
*/
@OnClose
public void onClose(){
webSocketSet.remove(this); //從set中刪除
subOnlineCount(); //在線客戶端數(shù)減1
System.out.println("有一連接關(guān)閉!當(dāng)前在線客戶端數(shù)為" + getOnlineCount());
}
/**
* 收到客戶端消息后調(diào)用的方法
* @param message 客戶端發(fā)送過來的消息
* @param session 可選的參數(shù)
*/
@OnMessage
public void onMessage(String message, Session session) {
System.out.println("來自客戶端的消息:" + message);
}
/**
* 發(fā)生錯誤時調(diào)用
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error){
System.out.println("發(fā)生錯誤");
error.printStackTrace();
}
/**
* 這個方法與上面幾個方法不一樣。沒有用注解,是根據(jù)自己需要添加的方法。
* @param message
* @throws IOException
*/
public void sendMessage(String message) throws IOException{
this.session.getBasicRemote().sendText(message);
}
public static synchronized int getOnlineCount() {
return onlineCount;
}
public static synchronized void addOnlineCount() {
WebSocketTest.onlineCount++;
}
public static synchronized void subOnlineCount() {
WebSocketTest.onlineCount--;
}
}
```
- 服務(wù)器端測試代碼,模擬服務(wù)器主動向前端發(fā)送消息
```java
/**
* 觸發(fā)后模擬服務(wù)器主動向前端發(fā)送系統(tǒng)即時提醒
*/
@RequestMapping("sendMsg")
public void sendMsg() {
SimpleDateFormat sf = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");
// 模擬向已連接的WebSocket客戶端發(fā)送系統(tǒng)提醒
for(WebSocketTest item: WebSocketTest.webSocketSet){
try {
item.sendMessage("系統(tǒng)提醒:當(dāng)前時間," + sf.format(new Date()) + ",請盡快完成任務(wù)!");
} catch (IOException e) {
e.printStackTrace();
continue;
}
}
}
```
- 服務(wù)器啟動后,通過http://localhost:8080/WebSocket/alert.action跳轉(zhuǎn)到了前端Jsp頁面,頁面創(chuàng)建了WebSocket連接,然后通過調(diào)用http://localhost:8080/WebSocket/sendMsg.action模擬服務(wù)器主動向前端發(fā)送系統(tǒng)提醒信息,前端進行即時的展示,效果如下:
## 注意
通過上面的講述,WebSocket 的優(yōu)勢已經(jīng)很明顯了,但是作為一個正在演變中的 Web 規(guī)范,我們也要看到目前用 Websocket 構(gòu)建應(yīng)用程序的一些風(fēng)險。首先,WebSocket 規(guī)范目前還處于草案階段,也就是它的規(guī)范和 API 還是有變動的可能,另外的一個風(fēng)險就是微軟的 IE 作為占市場份額最大的瀏覽器,和其他的主流瀏覽器相比,對 HTML5 的支持是比較差的,這是我們在構(gòu)建企業(yè)級的 Web 應(yīng)用的時候必須要考慮的一個問題。