
webSocket
1.為什么會(huì)有webSocket的出現(xiàn)?
默認(rèn)HTTP協(xié)議只支持請(qǐng)求響應(yīng)模式,也就是常說(shuō)的請(qǐng)求-響應(yīng)模式,這樣做可以簡(jiǎn)化Web服務(wù)器,減少服務(wù)器的負(fù)擔(dān),加快響應(yīng)速度。這種機(jī)制對(duì)于信息變化不是特別頻繁的應(yīng)用尚能相安無(wú)事,但是對(duì)于那些實(shí)時(shí)要求比較高的應(yīng)用來(lái)說(shuō),比如說(shuō)在線游戲、在線證券、設(shè)備監(jiān)控、新聞在線播報(bào)、RSS 訂閱推送等等,當(dāng)客戶端瀏覽器準(zhǔn)備呈現(xiàn)這些信息的時(shí)候,這些信息在服務(wù)器端可能已經(jīng)過(guò)時(shí)了。所以我們需要一種服務(wù)端可以主動(dòng)向客戶端推送消息的技術(shù),來(lái)保證消息的實(shí)時(shí)性。但是在webSocket出現(xiàn)之前,實(shí)時(shí)獲取消息也是有幾種解決方案的。
輪詢:
輪詢是比較常用并且簡(jiǎn)單的實(shí)現(xiàn)實(shí)時(shí)消息的方式,簡(jiǎn)單來(lái)說(shuō)就是客戶端不斷向服務(wù)端請(qǐng)求數(shù)據(jù),這個(gè)頻率可能是一分鐘甚至一秒一次,以此保持盡可能實(shí)時(shí)的獲取最新數(shù)據(jù)。
以Ajax+輪詢的方式,實(shí)現(xiàn)消息的獲取
setInterval('send()', 1000);//輪詢執(zhí)行,1s一次
function send() {
$.ajax({
url : '../case/quatrzLoad.ajax',
type : 'post',
dataType : 'JSON',
async : false,
cache : false,
data : {},
success : function(result) {
if (result.success) {
//右下角消息提醒
bottomRight();
//聲音提醒
playSound("../audio/remind.mp3");
}
?
}
});
}
輪詢的方式適用于用戶量比較少的應(yīng)用,而且實(shí)現(xiàn)簡(jiǎn)單。但是頻繁的請(qǐng)求會(huì)給服務(wù)器帶來(lái)很大的壓力。
長(zhǎng)連接:
在頁(yè)面里嵌入一個(gè)隱蔵iframe,將這個(gè)隱蔵iframe的src屬性設(shè)為對(duì)一個(gè)長(zhǎng)連接的請(qǐng)求或是采用xhr請(qǐng)求,服務(wù)器端就能源源不斷地往客戶端輸入數(shù)據(jù)。 優(yōu)點(diǎn):消息即時(shí)到達(dá),不發(fā)無(wú)用請(qǐng)求;管理起來(lái)也相對(duì)便。 缺點(diǎn):服務(wù)器維護(hù)一個(gè)長(zhǎng)連接會(huì)增加開(kāi)銷。
長(zhǎng)輪詢:
客戶端向服務(wù)器發(fā)送Ajax請(qǐng)求,服務(wù)器接到請(qǐng)求后hold住連接,直到有新消息才返回響應(yīng)信息并關(guān)閉連接,客戶端處理完響應(yīng)信息后再向服務(wù)器發(fā)送新的請(qǐng)求。 優(yōu)點(diǎn):在無(wú)消息的情況下不會(huì)頻繁的請(qǐng)求,耗費(fèi)資小。 缺點(diǎn):服務(wù)器hold連接會(huì)消耗資源,返回?cái)?shù)據(jù)順序無(wú)保證,難于管理維護(hù)。
2.webSocket是什么?
WebSocket同樣是HTML 5規(guī)范的組成部分之一。WebSocket 相較于上述幾種連接方式,實(shí)現(xiàn)原理較為復(fù)雜,用一句話概括就是:客戶端向 WebSocket 服務(wù)器通知(notify)一個(gè)帶有所有 接收者ID(recipients IDs)的事件(event),服務(wù)器接收后立即通知所有活躍的(active)客戶端,只有ID在接收者ID序列中的客戶端才會(huì)處理這個(gè)事件。由于 WebSocket 本身是基于TCP協(xié)議的,所以在服務(wù)器端我們可以采用構(gòu)建 TCP Socket 服務(wù)器的方式來(lái)構(gòu)建 WebSocket 服務(wù)器。
這個(gè) WebSocket 是一種全新的協(xié)議。它將 TCP 的 Socket(套接字)應(yīng)用在了web page上,從而使通信雙方建立起一個(gè)保持在活動(dòng)狀態(tài)連接通道,并且屬于全雙工(雙方同時(shí)進(jìn)行雙向通信)。
其實(shí)是這樣的,WebSocket 協(xié)議是借用 HTTP協(xié)議 的 101 switch protocol 來(lái)達(dá)到協(xié)議轉(zhuǎn)換的,從HTTP協(xié)議切換成WebSocket通信協(xié)議。
它的最大特點(diǎn)就是,服務(wù)器可以主動(dòng)向客戶端推送信息,客戶端也可以主動(dòng)向服務(wù)器發(fā)送信息,是真正的雙向平等對(duì)話,屬于服務(wù)器推送技術(shù)的一種。其他特點(diǎn)包括:
建立在 TCP 協(xié)議之上,服務(wù)器端的實(shí)現(xiàn)比較容易。
與 HTTP 協(xié)議有著良好的兼容性。默認(rèn)端口也是 80 和 443 ,并且握手階段采用 HTTP 協(xié)議,因此握手時(shí)不容易屏蔽,能通過(guò)各種 HTTP 代理服務(wù)器。
數(shù)據(jù)格式比較輕量,性能開(kāi)銷小,通信高效。
可以發(fā)送文本,也可以發(fā)送二進(jìn)制數(shù)據(jù)。
沒(méi)有同源限制,客戶端可以與任意服務(wù)器通信。
協(xié)議標(biāo)識(shí)符是ws(如果加密,則為wss),服務(wù)器網(wǎng)址就是 URL。
3.WebSocket如何創(chuàng)建?
WebSocket并不是全新的協(xié)議,而是利用了HTTP協(xié)議來(lái)建立連接。我們來(lái)看看WebSocket連接是如何創(chuàng)建的。
首先,WebSocket連接必須由瀏覽器發(fā)起,因?yàn)檎?qǐng)求協(xié)議是一個(gè)標(biāo)準(zhǔn)的HTTP請(qǐng)求,格式如下:
GET ws://localhost:3000/ws/chat HTTP/1.1
Host: localhost
Upgrade: websocket
Connection: Upgrade
Origin: http://localhost:3000
Sec-WebSocket-Key: client-random-string
Sec-WebSocket-Version: 13
該請(qǐng)求和普通的HTTP請(qǐng)求有幾點(diǎn)不同:
GET請(qǐng)求的地址不是類似
/path/,而是以ws://開(kāi)頭的地址;請(qǐng)求頭
Upgrade: websocket和Connection: Upgrade表示這個(gè)連接將要被轉(zhuǎn)換為WebSocket連接;Sec-WebSocket-Key是用于標(biāo)識(shí)這個(gè)連接,并非用于加密數(shù)據(jù);Sec-WebSocket-Version指定了WebSocket的協(xié)議版本。
隨后,服務(wù)器如果接受該請(qǐng)求,就會(huì)返回如下響應(yīng):
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: server-random-string
該響應(yīng)代碼101表示本次連接的HTTP協(xié)議即將被更改,更改后的協(xié)議就是Upgrade: websocket指定的WebSocket協(xié)議。
版本號(hào)和子協(xié)議規(guī)定了雙方能理解的數(shù)據(jù)格式,以及是否支持壓縮等等。如果僅使用WebSocket的API,就不需要關(guān)心這些。
現(xiàn)在,一個(gè)WebSocket連接就建立成功,瀏覽器和服務(wù)器就可以隨時(shí)主動(dòng)發(fā)送消息給對(duì)方。消息有兩種,一種是文本,一種是二進(jìn)制數(shù)據(jù)。通常,我們可以發(fā)送JSON格式的文本,這樣,在瀏覽器處理起來(lái)就十分容易。
為什么WebSocket連接可以實(shí)現(xiàn)全雙工通信而HTTP連接不行呢?實(shí)際上HTTP協(xié)議是建立在TCP協(xié)議之上的,TCP協(xié)議本身就實(shí)現(xiàn)了全雙工通信,但是HTTP協(xié)議的請(qǐng)求-應(yīng)答機(jī)制限制了全雙工通信。WebSocket連接建立以后,其實(shí)只是簡(jiǎn)單規(guī)定了一下:接下來(lái),咱們通信就不使用HTTP協(xié)議了,直接互相發(fā)數(shù)據(jù)吧。
安全的WebSocket連接機(jī)制和HTTPS類似。首先,瀏覽器用wss://xxx創(chuàng)建WebSocket連接時(shí),會(huì)先通過(guò)HTTPS創(chuàng)建安全的連接,然后,該HTTPS連接升級(jí)為WebSocket連接,底層通信走的仍然是安全的SSL/TLS協(xié)議。
4.webSocket 客戶端基本使用:
webSocket的api很簡(jiǎn)潔,甚至感覺(jué)粗暴。但是確實(shí)使用起來(lái)很方便:
?
$(function() {
var socket;
if(typeof(WebSocket) == "undefined") {
alert("您的瀏覽器不支持WebSocket");
return;
}
?
$("#btnConnection").click(function() {
//實(shí)現(xiàn)化WebSocket對(duì)象,指定要連接的服務(wù)器地址與端口
socket = new WebSocket("ws://192.16.20.39:8080/ops_console_war/ws/"+$("#cur_userId").val());
//打開(kāi)事件
socket.onopen = function() {
alert("Socket 已打開(kāi)");
//socket.send("這是來(lái)自客戶端的消息" + location.href + new Date());
};
//獲得消息事件
socket.onmessage = function(msg) {
console.log(msg);
if (msg.success) {
console.log("111");
}else{
console.log("222");
}
};
//關(guān)閉事件
socket.onclose = function() {
alert("Socket已關(guān)閉");
};
//發(fā)生了錯(cuò)誤事件
socket.onerror = function() {
alert("發(fā)生了錯(cuò)誤");
}
});
?
$("#btnSend").click(function() {
socket.send("這是來(lái)自客戶端的消息" + location.href + new Date());
});
?
$("#btnClose").click(function() {
socket.close();
});
?
?
$("#btnSendToAll").click(function(){
$.ajax({
url : '../user/sendMessage.ajax',
type : 'post',
dataType : 'JSON',
data : {
},
success : function(data) {
console.log("成功")
},error : function(msg) {
console.log("失敗")
}
});
});
});
5.webSocket服務(wù)端基本使用:
?
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
?
import javax.websocket.CloseReason;
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 java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
?
/**
* @ClassName WebSocketServer
* @Description webSocket服務(wù)
* @Author
* @Date 2019-10-22 下午 3:58
* @Version V1.0
*/
@ServerEndpoint("/ws/{userId}")
public class WebSocketServer {
?
private static WebSocketServer server = new WebSocketServer();
?
/***
* 日志
*/
Logger logger = LoggerFactory.getLogger(this.getClass());
?
/***
* 當(dāng)前在線用戶數(shù)量 包含同一用戶登錄多個(gè)瀏覽器的情況 不等于socketMap的大小
*/
private static AtomicInteger onlineUserCount = new AtomicInteger(0);
/***
* 存儲(chǔ)用戶id和session對(duì)應(yīng)關(guān)系的map
*/
private static Map<String,Session> socketMap = new ConcurrentHashMap<>(32);
?
?
/****
* 當(dāng)socket開(kāi)始連接時(shí)觸發(fā)
* @param userId
* @param session
*/
@OnOpen
public void onOpen(@PathParam("userId") String userId, Session session) {
?
if(StringUtils.isEmpty(userId)){
?
logger.error(" 客戶端開(kāi)始建立連接---但連接中缺失參數(shù)" );
?
}else{
?
socketMap.put(userId,session);
?
?
logger.info(" 用戶id為: "+userId+" 建立socket連接,當(dāng)前在線用戶數(shù)量: "+ socketMap.size());
?
}
?
}
?
?
/****
* 服務(wù)端收到客戶端消息時(shí)觸發(fā)
* @param message
* @param session
*/
@OnMessage
public void onMessage(String message, Session session) {
?
logger.info(" 客戶端向服務(wù)端發(fā)送消息 : {} ",message);
?
}
?
/****
* socket連接關(guān)閉時(shí)觸發(fā)
* @param session
* @param closeReason
*/
@OnClose
public void onClose(@PathParam("userId")String userId,Session session, CloseReason closeReason) {
//通過(guò)用戶id 將用戶session刪除
socketMap.remove(userId);
?
logger.info(" id 為 :"+userId+" 的用戶下線,當(dāng)前在線用戶數(shù)量 : "+socketMap.size());
}
?
/***
* 連接錯(cuò)誤時(shí)執(zhí)行
* @param t
*/
@OnError
public void onError(Throwable t) {
?
t.printStackTrace();
?
}
?
/***
*
* 通過(guò)用戶id向指定用戶推送消息
* @param message
* @param userId
* @throws IOException
*/
public void sendMessage(String message,String userId) throws IOException {
?
socketMap.get(userId).getBasicRemote().sendText(message);
?
}
?
?
/****
* 向所有用戶發(fā)送消息 群發(fā)消息
* @param message
* @throws IOException
*/
public void sendMessageToAllUser(String message) throws IOException{
?
long start = System.currentTimeMillis();
logger.info(" 服務(wù)端開(kāi)始群發(fā)消息 , 消息內(nèi)容: "+message);
?
for (String userId : socketMap.keySet()) {
?
socketMap.get(userId).getBasicRemote().sendText(message);
?
}
long end = System.currentTimeMillis();
logger.info(" 服務(wù)端群發(fā)消息結(jié)束 , 共耗時(shí) : "+(end-start)+" ms");
?
}
?
?
public static Map<String,Session> getSocketMap(){
return socketMap;
}
?
}
本人最近因?yàn)轫?xiàng)目原因,正在將以前的輪詢獲取消息的代碼進(jìn)行重構(gòu)。并且因?yàn)橛脩魯?shù)量并不是太多,所以沒(méi)有選用其他三方webSocket插件,因此接觸到webSocket。過(guò)幾天,會(huì)將完整的消息推送項(xiàng)目分享給大家。
