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