日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

WebSocket 实现 Web 端即时通信

發布時間:2025/3/20 编程问答 29 豆豆
生活随笔 收集整理的這篇文章主要介紹了 WebSocket 实现 Web 端即时通信 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

??點擊上方?好好學java?,選擇?星標?公眾號

重磅資訊、干貨,第一時間送達 今日推薦:牛人 20000 字的 Spring Cloud 總結,太硬核了~

前言

WebSocket 是HTML5開始提供的一種在瀏覽器和服務器間進行全雙工通信的協議。目前很多沒有使用WebSocket進行客戶端服務端實時通信的web應用,大多使用設置規則時間的輪詢,或者使用長輪詢較多來處理消息的實時推送。這樣勢必會較大程度浪費服務器和帶寬資源,而我們現在要講的WebSocket正是來解決該問題而出現,使得B/S架構的應用擁有C/S架構一樣的實時通信能力。

HTTP和WebSocket比較

HTTP

HTTP協議是半雙工協議,也就是說在同一時間點只能處理一個方向的數據傳輸,同時HTTP消息也是過于龐大,里面包含大量消息頭數據,真正在消息處理中很多數據不是必須的,這也是對資源的浪費。

  • 定時輪詢:定時輪詢就是客戶端定時去向服務器發送HTTP請求,看是否有數據,服務器接受到請求后,返回數據給客戶端,本次連接也會隨著關閉。該實現方案最簡單,但是會存在消息延遲和大量浪費服務器和帶寬資源。

  • 長輪詢:長輪詢與定時輪詢一樣,也是通過HTTP請求實現,但這里不是定時發送請求。客戶端發送請求給服務端,這時服務端會hold住該請求,當有數據過來或者超時時返回給請求的客戶端并開始下一輪的請求。

WebSocket

WebSocket在客戶端和服務端只需一次請求,就會在客戶端和服務端建立一條通信通道,可以實時相互傳輸數據,并且不會像HTTP那樣攜帶大量請求頭等信息。因為WebSocket是基于TCP雙向全雙工通信的協議,所以支持在同一時間點處理發送和接收消息,做到實時的消息處理。

  • 建立WebSocket連接:建立WebSocket連接,首先客戶端先要向服務端發送一個特殊的HTTP請求,使用的協議不是http或?https,而是使用過?ws或?wss(一個非安全的,一個安全的,類似前兩者之間的差別),請求頭里面要附加一個申請協議升級的信息?Upgrade:websocket,還有隨機生成一個?Sec-WebSocket-Key的值,及版本信息?Sec-WebSocket-Version等等。服務端收到客戶端的請求后,會解析該請求的信息,包括請求協議升級,版本校驗,以及將?Sec-WebSocket-Key的加密后以?sec-websocket-accept的值返回給客戶端,這樣客戶端和服務端的連接就建立了。

  • 關閉WebSocket連接:客戶端和服務端都可發送一個close控制幀,另一端主動關閉連接。

HTTP輪詢和WebSocket生命周期示意圖

服務端

這里服務端利用Netty的WebSocket開發。這里首先實現服務端啟動類,然后自定義處理器來處理WebSocket的消息。

package com.ytao.websocket; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.*; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.handler.codec.http.HttpObjectAggregator; import io.netty.handler.codec.http.HttpServerCodec; import io.netty.handler.logging.LogLevel; import io.netty.handler.logging.LoggingHandler; import io.netty.handler.stream.ChunkedWriteHandler; /*** Created by YANGTAO on 2019/11/17 0017.*/ public class WebSocketServer {public static String HOST = "127.0.0.1";public static int PORT = 8806;public static void startUp() throws Exception {// 監聽端口的線程組EventLoopGroup bossGroup = new NioEventLoopGroup();// 處理每一條連接的數據讀寫的線程組EventLoopGroup workerGroup = new NioEventLoopGroup();// 啟動的引導類ServerBootstrap serverBootstrap = new ServerBootstrap();try {serverBootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel ch) throws Exception{ChannelPipeline pipeline = ch.pipeline();pipeline.addLast("logger", new LoggingHandler(LogLevel.INFO));// 將請求和返回消息編碼或解碼成httppipeline.addLast("http-codec", new HttpServerCodec());// 使http的多個部分組合成一條完整的httppipeline.addLast("aggregator", new HttpObjectAggregator(65536));// 向客戶端發送h5文件,主要是來支持websocket通信pipeline.addLast("http-chunked", new ChunkedWriteHandler());// 服務端自定義處理器pipeline.addLast("handler", new WebSocketServerHandler());}})// 開啟心跳機制.childOption(ChannelOption.SO_KEEPALIVE, true).handler(new ChannelInitializer<NioServerSocketChannel>() {protected void initChannel(NioServerSocketChannel ch) {System.out.println("WebSocket服務端啟動中...");}});Channel ch = serverBootstrap.bind(HOST, PORT).sync().channel();System.out.println("WebSocket host: "+ch.localAddress().toString().replace("/",""));ch.closeFuture().sync();}catch (Exception e){e.printStackTrace();}finally {bossGroup.shutdownGracefully();workerGroup.shutdownGracefully();}}public static void main(String[] args) throws Exception {startUp();} }

上面啟動類和HTTP協議的類似,所以較好理解。啟動類啟動后,我們需要處理WebSocket請求,這里自定義 WebSocketServerHandler。我們在處理中設計的業務邏輯有,如果只有一個連接來發送信息聊天,那么我們就以服務器自動回復,如果存在一個以上,我們就將信息發送給其他人。

package com.ytao.websocket; import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPromise; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.websocketx.*; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.Date; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /*** Created by YANGTAO on 2019/11/17 0017.*/ public class WebSocketServerHandler extends SimpleChannelInboundHandler<Object> {private WebSocketServerHandshaker handshaker;private static Map<String, ChannelHandlerContext> channelHandlerContextConcurrentHashMap = new ConcurrentHashMap<>();private static final Map<String, String> replyMap = new ConcurrentHashMap<>();static {replyMap.put("博客", "https://ytao.top");replyMap.put("公眾號", "ytao公眾號");replyMap.put("在嗎", "在");replyMap.put("吃飯了嗎", "吃了");replyMap.put("你好", "你好");replyMap.put("誰", "ytao");replyMap.put("幾點", "現在本地時間:"+LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")));}@Overridepublic void messageReceived(ChannelHandlerContext channelHandlerContext, Object msg) throws Exception{channelHandlerContextConcurrentHashMap.put(channelHandlerContext.channel().toString(), channelHandlerContext);// httpif (msg instanceof FullHttpRequest){handleHttpRequest(channelHandlerContext, (FullHttpRequest) msg);}else if (msg instanceof WebSocketFrame){ // WebSockethandleWebSocketFrame(channelHandlerContext, (WebSocketFrame) msg);}}@Overridepublic void channelReadComplete(ChannelHandlerContext channelHandlerContext) throws Exception{if (channelHandlerContextConcurrentHashMap.size() > 1){for (String key : channelHandlerContextConcurrentHashMap.keySet()) {ChannelHandlerContext current = channelHandlerContextConcurrentHashMap.get(key);if (channelHandlerContext == current)continue;current.flush();}}else {// 單條處理channelHandlerContext.flush();}}private void handleHttpRequest(ChannelHandlerContext channelHandlerContext, FullHttpRequest request) throws Exception{// 驗證解碼是否異常if (!"websocket".equals(request.headers().get("Upgrade")) || request.decoderResult().isFailure()){// todo send response badSystem.err.println("解析http信息異常");return;}// 創建握手工廠類WebSocketServerHandshakerFactory factory = new WebSocketServerHandshakerFactory("ws:/".concat(channelHandlerContext.channel().localAddress().toString()),null,false);handshaker = factory.newHandshaker(request);if (handshaker == null)WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(channelHandlerContext.channel());else// 響應握手消息給客戶端handshaker.handshake(channelHandlerContext.channel(), request);}private void handleWebSocketFrame(ChannelHandlerContext channelHandlerContext, WebSocketFrame webSocketFrame){// 關閉鏈路if (webSocketFrame instanceof CloseWebSocketFrame){handshaker.close(channelHandlerContext.channel(), (CloseWebSocketFrame) webSocketFrame.retain());return;}// Ping消息if (webSocketFrame instanceof PingWebSocketFrame){channelHandlerContext.channel().write(new PongWebSocketFrame(webSocketFrame.content().retain()));return;}// Pong消息if (webSocketFrame instanceof PongWebSocketFrame){// todo Pong消息處理}// 二進制消息if (webSocketFrame instanceof BinaryWebSocketFrame){// todo 二進制消息處理}// 拆分數據if (webSocketFrame instanceof ContinuationWebSocketFrame){// todo 數據被拆分為多個websocketframe處理}// 文本信息處理if (webSocketFrame instanceof TextWebSocketFrame){// 推送過來的消息String msg = ((TextWebSocketFrame) webSocketFrame).text();System.out.println(String.format("%s 收到消息 : %s", new Date(), msg));String responseMsg = "";if (channelHandlerContextConcurrentHashMap.size() > 1){responseMsg = msg;for (String key : channelHandlerContextConcurrentHashMap.keySet()) {ChannelHandlerContext current = channelHandlerContextConcurrentHashMap.get(key);if (channelHandlerContext == current)continue;Channel channel = current.channel();channel.write(new TextWebSocketFrame(responseMsg));}}else {// 自動回復responseMsg = this.answer(msg);if(responseMsg == null)responseMsg = "暫時無法回答你的問題 ->_->";System.out.println("回復消息:"+responseMsg);Channel channel = channelHandlerContext.channel();channel.write(new TextWebSocketFrame("【服務端】" + responseMsg));}}}private String answer(String msg){for (String key : replyMap.keySet()) {if (msg.contains(key))return replyMap.get(key);}return null;}@Overridepublic void exceptionCaught(ChannelHandlerContext channelHandlerContext, Throwable throwable){throwable.printStackTrace();channelHandlerContext.close();}@Overridepublic void close(ChannelHandlerContext channelHandlerContext, ChannelPromise promise) throws Exception {channelHandlerContextConcurrentHashMap.remove(channelHandlerContext.channel().toString());channelHandlerContext.close(promise);} }

剛建立連接時,第一次握手有HTTP協議處理,所以 WebSocketServerHandler#messageReceived會判斷是HTTP還是WebSocket,如果是HTTP時,交由 WebSocketServerHandler#handleHttpRequest處理,里面會去驗證請求,并且處理握手后將消息返回給客戶端。如果不是HTTP協議,而是WebSocket協議時,處理交給 WebSocketServerHandler#handleWebSocketFrame處理,進入WebSocket處理后,這里面有判斷消息屬于哪種類型,里面包括 CloseWebSocketFrame, PingWebSocketFrame, PongWebSocketFrame, BinaryWebSocketFrame, ContinuationWebSocketFrame, TextWebSocketFrame,他們都是 WebSocketFrame的子類,并且 WebSocketFrame又繼承自 DefaultByteBufHolder。

channelHandlerContextConcurrentHashMap是緩存WebSocket已連接的信息,因為我們實現的需求要記錄連接數量,當有連接關閉時我們要刪除以緩存的連接,所以在 WebSocketServerHandler#close中要移除緩存。

最后的發送文本到客戶端,根據連接數量判斷。如果連接數量不大于1,那么,我們"價值一個億的AI核心代碼" WebSocketServerHandler#answer來回復客戶端消息。否則除了本次接收的連接,消息會發送給其他所有連接的客戶端。

客戶端

客戶端使用JS實現WebSocket的操作,目前主流的瀏覽器基本都支持WebSocket。支持情況如圖:

客戶端H5的代碼實現:

<!DOCTYPE html> <html lang="en"> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1" /><title>ytao-websocket</title><script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script><style type="text/css">#msgContent{line-height:200%;width: 500px;height: 300px;resize: none;border-color: #FF9900;}.clean{background-color: white;}.send{border-radius: 10%;background-color: #2BD56F;}@media screen and (max-width: 600px) {#msgContent{line-height:200%;width: 100%;height: 300px;}}</style> </head> <script>var socket;var URL = "ws://127.0.0.1:8806/ytao";connect();function connect() {$("#status").html("<span>連接中.....</span>");window.WebSocket = !window.WebSocket == true? window.MozWebSocket : window.WebSocket;if(window.WebSocket){socket = new WebSocket(URL);socket.onmessage = function(event){var msg = event.data + "\n";addMsgContent(msg);};socket.onopen = function(){$("#status").html("<span style='background-color: #44b549'>WebSocket已連接</span>");};socket.onclose = function(){$("#status").html("<span style='background-color: red'>WebSocket已斷開連接</span>");setTimeout("connect()", 3000);};}else{$("#status").html("<span style='background-color: red'>該瀏覽器不支持WebSocket協議!</span>");}}function addMsgContent(msg) {var contet = $("#msgContent").val() + msg;$("#msgContent").val(contet)}function clean() {$("#msgContent").val("");}function getUserName() {var n = $("input[name=userName]").val();if (n == "")n = "匿名";return n;}function send(){var message = $("input[name=message]").val();if(!window.WebSocket) return;if ($.trim(message) == ""){alert("不能發送空消息!");return;}if(socket.readyState == WebSocket.OPEN){var msg = "【我】" + message + "\n";this.addMsgContent(msg);socket.send("【"+getUserName()+"】"+message);$("input[name=message]").val("");}else{alert("無法建立WebSocket連接!");}}$(document).keyup(function(){if(event.keyCode ==13){send()}}); </script> <body><div style="text-align: center;"><div id="status"><span>連接中.....</span></div><div><h2>信息面板</h2><textarea id="msgContent" readonly="readonly"></textarea></div><div><input class="clean" type="button" value="清除聊天紀錄" onclick="clean()" /><input type="text" name="userName" value="" placeholder="用戶名"/></div><hr><div><form onsubmit="return false"><input type="text" name="message" value="" placeholder="請輸入消息"/><input class="send" type="button" name="msgBtn" value="send" onclick="send()"/></form></div><div><br><br><img src="http://yangtao.ytao.top/ytao%E5%85%AC%E4%BC%97%E5%8F%B7.jpg"></div></div> </body> </html>

JS這里實現相對較簡單,主要用到:

  • newWebSocket(URL)創建WebSocket對象

  • onopen()打開連接

  • onclose()關閉連接

  • onmessage接收消息

  • send()發送消息

當斷開連接后,客戶端這邊重新發起連接,直到連接成功為止。

啟動

客戶端和服務端連接后,我們從日志和請求中可以看到上面所提到的驗證信息。

客戶端:

服務端:

啟動服務端后,先實驗我們"價值一個億的AI",只有一個連接用戶時,發送信息結果如圖:

多個用戶連接,這里使用三個連接用戶群聊。

用戶一:

用戶二:

用戶三:

到目前為止,WebSocket已幫助我們實現即時通信的需求,相信大家也基本入門了WebSocket的基本使用。

總結

通過本文了解,可以幫助大家入門WebSocket并且解決當前可能存在的一些Web端的通信問題。我曾經在兩個項目中也有看到該類解決方案都是通過定時輪詢去做的,也或多或少對服務器資源造成一定的浪費。因為WebSocket本身是較復雜的,它提供的API也是比較多,所以在使用過程,要去真正使用好或去優化它,并不是一件很簡單的事,也是需要根據現實場景針對性的去做。

總結

以上是生活随笔為你收集整理的WebSocket 实现 Web 端即时通信的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。