WebSocket学习与使用
1、WebSocket是什么
WebSocket是一種在單個(gè)TCP連接上進(jìn)行全雙工通信的協(xié)議,其目的是在瀏覽器和服務(wù)器之間建立一個(gè)不受限的雙向通信的通道,使得服務(wù)器可以主動(dòng)發(fā)送消息給瀏覽器。在HTML5中包含了WebSocket API規(guī)范。
WebSocket 協(xié)議在2008年誕生,2011年成為國(guó)際標(biāo)準(zhǔn)。目前所有瀏覽器都已經(jīng)支持。
根據(jù)安全與否,與HTTP/HTTPS類似,WebSocket有ws/wss兩種協(xié)議。?
?(需要注意的是,目前瀏覽器對(duì)WebSocket未做同源策略限制,因此采用WebSocket的應(yīng)用需要注意防范跨站請(qǐng)求偽造)
?
2、Web雙向通信方案
所謂雙向通信,關(guān)鍵在于服務(wù)端”主動(dòng)“能發(fā)信息給客戶端。
傳統(tǒng)的HTTP協(xié)議是一個(gè)請(qǐng)求-響應(yīng)協(xié)議,是無(wú)狀態(tài)的:請(qǐng)求必須先由瀏覽器發(fā)給服務(wù)器,服務(wù)器才能響應(yīng)這個(gè)請(qǐng)求,再把數(shù)據(jù)發(fā)送給瀏覽器。即服務(wù)端處于被動(dòng)狀態(tài),只有在收到客戶端請(qǐng)求后才能響應(yīng)發(fā)數(shù)據(jù)給客戶端,且請(qǐng)求與響應(yīng)一一對(duì)應(yīng)。
在WebSocket之前,在Web要實(shí)現(xiàn)類似雙向通信功能,只能通過(guò) ajax poll (輪詢)或 long poll (長(zhǎng)輪詢) 等。
1、ajax poll (輪詢):客戶端每隔幾秒發(fā)送請(qǐng)求詢問(wèn)服務(wù)端是否有新消息,服務(wù)器接收到請(qǐng)求后馬上返回并關(guān)閉連接。
優(yōu)點(diǎn):后臺(tái)實(shí)現(xiàn)簡(jiǎn)單。
缺點(diǎn):實(shí)時(shí)性不夠;TCP建立和關(guān)閉操作浪費(fèi)時(shí)間和帶寬,頻繁請(qǐng)求造成大訪問(wèn)壓力,且有很多是無(wú)用請(qǐng)求,浪費(fèi)帶寬和服務(wù)器資源。
實(shí)例:小型應(yīng)用
2、long poll (長(zhǎng)輪詢):本質(zhì)也是輪詢,不同的是客戶端發(fā)起請(qǐng)求后,服務(wù)端若沒(méi)有新消息則hold阻塞,直到有消息才返回并關(guān)閉連接。
優(yōu)點(diǎn):在無(wú)消息的情況下不會(huì)頻繁請(qǐng)求,耗費(fèi)資源小。
缺點(diǎn):以多線程模式運(yùn)行的服務(wù)器會(huì)讓大部分線程大部分時(shí)間都處于掛起狀態(tài),極大浪費(fèi)服務(wù)器資源;HTTP長(zhǎng)時(shí)間沒(méi)傳數(shù)據(jù),該連接可能被網(wǎng)關(guān)關(guān)閉,不可控,故需要發(fā)”心跳“。
實(shí)例:WebQQ、Hi網(wǎng)頁(yè)版、Facebook IM
3、HTTP長(zhǎng)連接:一個(gè)TCP連接可以發(fā)送多次HTTP請(qǐng)求,而不是傳統(tǒng)那樣每個(gè)請(qǐng)求都重新建立一個(gè)連接。
在頁(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ì)增加開銷,當(dāng)客戶端越來(lái)越多的時(shí)候,server壓力大。
4、Flash Socket:在頁(yè)面中內(nèi)嵌入一個(gè)使用了Socket類的 Flash 程序,JavaScript通過(guò)調(diào)用此Flash程序提供的Socket接口與服務(wù)器端的Socket接口進(jìn)行通信,JavaScript在收到服務(wù)器端傳送的信息后控制頁(yè)面的顯示。
優(yōu)點(diǎn):實(shí)現(xiàn)真正的即時(shí)通信,而不是偽即時(shí)。
缺點(diǎn):客戶端必須安裝Flash插件,移動(dòng)端支持不好,IOS系統(tǒng)中沒(méi)有flash的存在;非HTTP協(xié)議,無(wú)法自動(dòng)穿越防火墻。
實(shí)例:網(wǎng)絡(luò)互動(dòng)游戲。?
?
3、WebSocket的特點(diǎn)
WebSocket的出現(xiàn)可以取代輪詢和長(zhǎng)連接,客戶端不用定期輪詢(網(wǎng)關(guān)問(wèn)題仍存在,故WebSocket內(nèi)部也定期發(fā)送”心跳“),其特點(diǎn)有:
1、建立在 TCP 協(xié)議之上,服務(wù)器端的實(shí)現(xiàn)比較容易。(基于TCP,所以可以支持全雙工通信)
2、與 HTTP 協(xié)議有著良好的兼容性。默認(rèn)端口也是80和443,并且握手階段采用 HTTP 協(xié)議,因此握手時(shí)不容易屏蔽,能通過(guò)各種 HTTP 代理服務(wù)器。
3、數(shù)據(jù)格式比較輕量,性能開銷小,通信高效。
4、可以發(fā)送文本,也可以發(fā)送二進(jìn)制數(shù)據(jù)。(通常用JSON,方便處理)
5、沒(méi)有同源限制,客戶端可以與任意服務(wù)器通信。
相對(duì)于傳統(tǒng)HTTP每次請(qǐng)求-應(yīng)答都需要客戶端與服務(wù)端建立連接的模式,WebSocket是類似Socket的TCP長(zhǎng)連接通訊模式。一旦WebSocket連接建立后,后續(xù)數(shù)據(jù)都以幀序列的形式傳輸。在客戶端斷開WebSocket連接或Server端中斷連接前,不需要客戶端和服務(wù)端重新發(fā)起連接請(qǐng)求。在海量并發(fā)及客戶端與服務(wù)器交互負(fù)載流量大的情況下,極大的節(jié)省了網(wǎng)絡(luò)帶寬資源的消耗,有明顯的性能優(yōu)勢(shì),且客戶端發(fā)送和接受消息是在同一個(gè)持久連接上發(fā)起,實(shí)時(shí)性優(yōu)勢(shì)明顯。?
?
4、WebSocket協(xié)議原理
可以視為兩個(gè)階段,先借助HTTP進(jìn)行握手,握手完成后就建立了連接以后直接雙向通信。
1、握手階段利用HTTP協(xié)議發(fā)送一次握手請(qǐng)求,協(xié)商升級(jí)到WebSocket協(xié)議(101 Switching Protocols)。握手請(qǐng)求是一個(gè)標(biāo)準(zhǔn)HTTP請(qǐng)求,格式如下:
請(qǐng)求格式示例: GET /chat HTTP/1.1 Host: server.example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw== Sec-WebSocket-Protocol: chat, superchat Sec-WebSocket-Version: 13 Origin: http://example.com 響應(yīng)格式示例: HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk= Sec-WebSocket-Protocol: chat View Code2、握完手后與HTTP協(xié)議無(wú)關(guān)了,客戶端和服務(wù)端直接建立了連接,可以直接雙向主動(dòng)發(fā)送數(shù)據(jù)
WebSocket 是獨(dú)立創(chuàng)建在TCP上的協(xié)議,HTTP協(xié)議中的那些概念都和WebSocket 沒(méi)有關(guān)聯(lián),唯一關(guān)聯(lián)的是使用HTTP協(xié)議的 101 狀態(tài)碼進(jìn)行協(xié)議切換。
?
5、WebSocket實(shí)踐(SocketIO)
關(guān)于WebSocket的API,前端有WebSocket API已經(jīng)成為HTML5標(biāo)準(zhǔn)的一部分,后端有很多框架,Java中也有很多。這里以SocketIO為例.
5.1、SocketIO
SocketIO是基于WebSocket實(shí)現(xiàn)的一個(gè)跨平臺(tái)的實(shí)時(shí)通信庫(kù),基于engine.io實(shí)現(xiàn)。engine.io 使用了 WebSocket 和 XMLHttprequest或JSONP封裝了一套自己的 Socket 協(xié)議(暫時(shí)叫 EIO Socket),在低版本瀏覽器里面使用長(zhǎng)輪詢替代 WebSocket。一個(gè)完整的 EIO Socket 包括多個(gè) XHR 和 WebSocket 連接。
SocketIO不僅支持WebSocket,為了兼容有些瀏覽器不支持WebSocket的問(wèn)題還提供了降級(jí)功能:
Websocket
Adobe? Flash? Socket
AJAX long polling
AJAX multipart streaming
Forever Iframe
JSONP Polling
這些降級(jí)功能對(duì)用戶來(lái)說(shuō)是透明的,SocketIO會(huì)根據(jù)瀏覽器支持情況進(jìn)行自動(dòng)選擇。
此外,SocketIO還提供了命名空間、自動(dòng)重連等功能。
關(guān)于SocketIO的庫(kù)很多:
SocketIO前端庫(kù)(官方)
SocketIO后端庫(kù)(Java、C++等)
5.2、SocketIO示例
Java服務(wù)端:
依賴:(netty-socketio、socket-io-client)
<dependency><groupId>io.socket</groupId><artifactId>socket.io-client</artifactId><version>1.0.0</version></dependency><dependency><groupId>com.corundumstudio.socketio</groupId><artifactId>netty-socketio</artifactId><version>1.7.12</version></dependency> View CodeJava服務(wù)端代碼示例:
public class SocketServer {private final Logger logger = LoggerFactory.getLogger(this.getClass());private static SocketIOServer server = initServer();/*** 初始化服務(wù)端* * @return*/private static SocketIOServer initServer() {Configuration config = new Configuration();config.setHostname("localhost");config.setPort(9090);config.setContext("/wsapi");// 前端連接時(shí)通過(guò)指定path與此對(duì)應(yīng)config.setAuthorizationListener(new AuthorizationListener() {// 授權(quán) @Overridepublic boolean isAuthorized(HandshakeData data) {String token = data.getSingleHeader("X-Authorization");// cannot// get,always// nullreturn true;}});server = new SocketIOServer(config);return server;}/*** 啟動(dòng)服務(wù)端*/public void startServer() {// 添加連接監(jiān)聽(tīng)server.addConnectListener(new ConnectListener() {@Overridepublic void onConnect(SocketIOClient socketIOClient) {String acid = socketIOClient.getHandshakeData().getSingleUrlParam("acid");// 前端連接時(shí)帶上的參數(shù)String clientId = socketIOClient.getSessionId().toString();logger.info("server 服務(wù)端啟動(dòng)成功");}});// 添加斷開連接監(jiān)聽(tīng)server.addDisconnectListener(new DisconnectListener() {@Overridepublic void onDisconnect(SocketIOClient socketIOClient) {logger.info("server 服務(wù)端斷開連接");}});// 添加事件監(jiān)聽(tīng)server.addEventListener("join", String.class, new DataListener<String>() {@Overridepublic void onData(SocketIOClient socketIOClient, String str, AckRequest ackRequest) throws Exception {logger.info("收到客戶端加入消息:" + str);server.getBroadcastOperations().sendEvent("joinSuccess", "join success");}});// 啟動(dòng)服務(wù)端 server.start();}/*** 停止服務(wù)端*/public void stopServer() {server.stop();} } View Code前端代碼示例:
<!DOCTYPE html> <html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta http-equiv="X-UA-Compatible" content="ie=edge"><script src="https://cdn.bootcss.com/socket.io/2.1.1/socket.io.dev.js"></script><title>socketio-client</title> </head><body><br><div style="border-style:solid"><button id="preBtn">預(yù)請(qǐng)求</button><p id="engineInfoContainer"></p></div><br><div style="border-style:solid"><button id="engineBtn">引擎響應(yīng)</button><p id="engineCallInfoContainr"></p></div><script>var wsEventKey4Req = "expRequest";var wsEventKey4Res = "expResponse";var token = "Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ6aGFuZ3NhbiIsInNjb3BlcyI6WyJST0xFX1NUVURFTlQiXSwidXNlcklkIjoiMTdmNDIyYmQtNzQzOC00NTA0LWJhZTItZGU0ZmU1ZDg4MmUyIiwiaXNzIjoiemh1eWFuYm8iLCJpYXQiOjE1MzIzMTE5MjcsImV4cCI6MTUzNTkxMTkyN30._BCXb_ukWmpummKGXwcYTvJVHjPjlPyDC59C3-anLehnmWL-PBSJOrKBU9Vsa3Wom3ARCji3vOI32LlPYj3SVg";var socket = io.connect('http://localhost:8090?X-Authorization=' + token,{"path":"/wsapi"});socket.on('connect', function () {console.log("connected to server") ;socket.emit("zsmtest","hello there, I'm client.");});socket.on('disconnect', function () { console.log("server disconnected") });console.log(socket);//front call below//namespace, room// socketio.of('/private').in('chat').send("send to all the clients in the chat room which belong to namespace(private)");// socketio.of('/private').send("send to all the clients which belong to namespace(priavte)");// socketio.send("send to the clients which belong to default namespace(/)");// socket.broadcast.in('chat').emit('message', "send to the clients which belong to namespace(socket belong to) except sender");// socket.broadcast.emit('message', "send to the clients which belong to namespace(socket belong to) except sender"); document.getElementById("preBtn").onclick = function () {var data = { mykey: 'clientdata' };socket.emit(wsEventKey4Req, data);console.log("send data: " + JSON.stringify(data));}socket.on(wsEventKey4Req, function (data) {console.log(data);document.getElementById("engineInfoContainer").textContent = JSON.stringify(data);});//engine call belowdocument.getElementById("engineBtn").onclick = function () {var data = { engineHost: 'sensetime', enginePort:8888 };data={clientId:"c883779f-1bfb-4101-b679-0805ea1d84ee", data:data};socket.emit(wsEventKey4Res, data);}socket.on(wsEventKey4Res, function (data) {console.log(data);document.getElementById("engineCallInfoContainr").textContent = JSON.stringify(data);});</script> </body></html> View Code?
socket.io 提供了三種默認(rèn)的事件(客戶端和服務(wù)器都有):connect 、message 、disconnect 。當(dāng)與對(duì)方建立連接后自動(dòng)觸發(fā) connect 事件,當(dāng)收到對(duì)方發(fā)來(lái)的數(shù)據(jù)后觸發(fā) message 事件(通常為 socket.send() 觸發(fā)),當(dāng)對(duì)方關(guān)閉連接后觸發(fā) disconnect 事件。
此外,socket.io 還支持自定義事件,畢竟以上三種事件應(yīng)用范圍有限,正是通過(guò)這些自定義的事件才實(shí)現(xiàn)了豐富多彩的通信。
最后,需要注意的是,在服務(wù)器端區(qū)分以下三種情況:
socket.emit() :向建立該連接的客戶端廣播
socket.broadcast.emit() :向除去建立該連接的客戶端的所有客戶端廣播
io.sockets.emit() :向所有客戶端廣播,等同于上面兩個(gè)的和
遇到的坑:
- 關(guān)于協(xié)議支持。上述的Java SocketIO? client 版本(1.0.0)不支持WebSocket,只支持polling(服務(wù)端則兩者都支持),故client 需要指定Transports:?options.transports = new String[] { "polling" };?,否則需要使用者自己code配合server實(shí)現(xiàn)升級(jí)到WebSocket協(xié)議。前面說(shuō)法錯(cuò)誤,完全支持websocket,且比polling穩(wěn)定,示例(注意該Client庫(kù)所用的HTTP工具為okhttp,后者默認(rèn)支持并發(fā)數(shù)為5): options.forceNew = true;options.transports = new String[] { "websocket" };ConnectionPool connectionPool = new ConnectionPool(200, 10, TimeUnit.SECONDS);OkHttpClient okHttpClient = new OkHttpClient.Builder().connectionPool(connectionPool).build();options.webSocketFactory = okHttpClient;options.callFactory = okHttpClient;
- 關(guān)于連接復(fù)用。SocketIo client默認(rèn)會(huì)復(fù)用連接,導(dǎo)致server對(duì)不同請(qǐng)求拿到的sessionId一樣。因此若在server基于該sessionId來(lái)維護(hù)與相應(yīng)SocketIoClient的映射則可能會(huì)出問(wèn)題(在本人實(shí)踐中Socket server等待其他服務(wù)發(fā)來(lái)的消息,根據(jù)消息里的sessionId字段轉(zhuǎn)發(fā)給相應(yīng)SocketIoClient,發(fā)完后移除對(duì)應(yīng)SocketIoClient,由于sessionId有重,導(dǎo)致后續(xù)相同sessionId的消息找不到相應(yīng)client從而造成某些client收不到消息)。解決:調(diào)用者(客戶端)設(shè)置參數(shù)以禁用連接復(fù)用:?options.forceNew = true;?(不管是用polling還是websocket均如是)
- 關(guān)于ACK。對(duì)于addEventListener添加的事件(onConnect等預(yù)定義事件不屬于此),server收到client發(fā)過(guò)來(lái)的數(shù)據(jù)后觸發(fā)addEventListener中的onData方法,方法最后默認(rèn)會(huì)調(diào)用AckRequest.sendData以回發(fā)收到消息確認(rèn)信息(當(dāng)然亦可手動(dòng)調(diào)用)。
這里的AckRequest是client所傳的ack對(duì)象,若client在emit未傳該對(duì)象則client在發(fā)送數(shù)據(jù)后會(huì)一直等待直到超時(shí)(不傳ack對(duì)象的話,在并發(fā)壓測(cè)時(shí)服務(wù)端表現(xiàn)為不能立馬收到所有client發(fā)送的數(shù)據(jù),而是5個(gè)一批超時(shí)后再下一批,why??)。
故client emit時(shí)須帶上ack對(duì)象,若不帶,則server在收到消息時(shí)往任意eventKey上回發(fā)任意數(shù)據(jù)此時(shí)client也會(huì)當(dāng)成已確認(rèn)收到(why??)。示例: server.addEventListener(wsEvent4FrontExpReq, Map.class, new DataListener<Map>() {@Overridepublic void onData(SocketIOClient socketIOClient, Map reqMap, AckRequest ackRequest) throws Exception {if (!ackRequest.isAckRequested()) {socketIOClient.sendEvent(UUID.randomUUID().toString(), "got message");// ackRequest.sendAckData("got message"); }// else// {// ackRequest.sendAckData("got message");// 會(huì)在本方法結(jié)束后自動(dòng)被調(diào)用// }//business code... }} View Code?
?
?
?
?
?
其他相關(guān):?
如何帶認(rèn)證參數(shù):官方文檔中說(shuō)通過(guò)extraHeaders不過(guò)只在polling模式下生效故不可取,可以在連接url中作為參數(shù)傳輸不過(guò)url可以直接看到故不安全。
設(shè)置context path:如代碼中所示,前端通過(guò)連接時(shí)的path參數(shù)設(shè)置,不設(shè)置則默認(rèn)為 /socket.io
?更多啟動(dòng)選項(xiàng)設(shè)置參考:https://github.com/socketio/engine.io-client#methods
?如:transportOptions (Object): hash of options, indexed by transport name, overriding the common options for the given transport
?
6、相關(guān)資料
websocket入門簡(jiǎn)介-阮一峰
websocket原理-知乎
https://socket.io/get-started/chat/?
SocketIO原理-知乎
?
轉(zhuǎn)載于:https://www.cnblogs.com/z-sm/p/9385317.html
《新程序員》:云原生和全面數(shù)字化實(shí)踐50位技術(shù)專家共同創(chuàng)作,文字、視頻、音頻交互閱讀總結(jié)
以上是生活随笔為你收集整理的WebSocket学习与使用的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Netty实践
- 下一篇: 还珠格格里的尔泰,重现娱乐圈