輕度rogue《柯南快快》3月1日發售 登陸主機和PC
WebSocket介紹
一、為什麼需要 WebSocket?
初次接觸 WebSocket 的人,都會問同樣的問題:我們已經有了 HTTP 協議,為什麼還需要另一個協議?它能帶來什麼好處?
答案很簡單,因為 HTTP 協議有一個缺陷:通訊只能由客戶端發起。
舉例來說,我們想了解今天的天氣,只能是客戶端向伺服器發出請求,伺服器返回查詢結果。HTTP 協議做不到伺服器主動向客戶端推送資訊。
這種單向請求的特點,註定瞭如果伺服器有連續的狀態變化,客戶端要獲知就非常麻煩。我們只能使用"輪詢":每隔一段時候,就發出一個詢問,瞭解伺服器有沒有新的資訊。最典型的場景就是聊天室。
輪詢的效率低,非常浪費資源(因為必須不停連線,或者 HTTP 連線始終開啟)。因此,工程師們一直在思考,有沒有更好的方法。WebSocket 就是這樣發明的。
二、簡介
WebSocket 協議在2008年誕生,2011年成為國際標準。所有瀏覽器都已經支援了。
它的最大特點就是,伺服器可以主動向客戶端推送資訊,客戶端也可以主動向伺服器傳送資訊,是真正的雙向平等對話,屬於伺服器推送技術的一種。
其他特點包括:
(1)建立在 TCP 協議之上,伺服器端的實現比較容易。
(2)與 HTTP 協議有著良好的相容性。預設埠也是80和443,並且握手階段採用 HTTP 協議,因此握手時不容易遮蔽,能通過各種 HTTP 代理伺服器。
(3)資料格式比較輕量,效能開銷小,通訊高效。
(4)可以傳送文字,也可以傳送二進位制資料。
(5)沒有同源限制,客戶端可以與任意伺服器通訊。
(6)協議識別符號是ws
(如果加密,則為wss
),伺服器網址就是 URL。
三.WebSocket 的作用
其實上面已經講了它的優點了,不過最近看知乎看到一段有關WebSocket挺有意義的,所以複製來。
在講Websocket之前,我就順帶著講下 long poll 和 ajax輪詢 的原理。首先是 ajax輪詢 ,ajax輪詢 的原理非常簡單,讓瀏覽器隔個幾秒就傳送一次請求,詢問伺服器是否有新資訊。
場景再現:
客戶端:啦啦啦,有沒有新資訊(Request)
服務端:沒有(Response)
客戶端:啦啦啦,有沒有新資訊(Request)
服務端:沒有。。(Response)
客戶端:啦啦啦,有沒有新資訊(Request)
服務端:你好煩啊,沒有啊。。(Response)
客戶端:啦啦啦,有沒有新訊息(Request)
服務端:好啦好啦,有啦給你。(Response)
客戶端:啦啦啦,有沒有新訊息(Request)
服務端:。。。。。沒。。。。沒。。。沒有(Response) ---- loop
long poll
long poll 其實原理跟 ajax輪詢 差不多,都是採用輪詢的方式,不過採取的是阻塞模型(一直打電話,沒收到就不掛電話),也就是說,客戶端發起連線後,如果沒訊息,就一直不返回Response給客戶端。直到有訊息才返回,返回完之後,客戶端再次建立連線,周而復始。
場景再現
客戶端:啦啦啦,有沒有新資訊,沒有的話就等有了才返回給我吧(Request)
服務端:額。。 等待到有訊息的時候。。來 給你(Response)
客戶端:啦啦啦,有沒有新資訊,沒有的話就等有了才返回給我吧(Request) -loop
從上面可以看出其實這兩種方式,都是在不斷地建立HTTP連線,然後等待服務端處理,可以體現HTTP協議的另外一個特點,被動性。
何為被動性呢,其實就是,服務端不能主動聯絡客戶端,只能有客戶端發起。
簡單地說就是,伺服器是一個很懶的冰箱(這是個梗)(不會、不能主動發起連線),但是上司有命令,如果有客戶來,不管多麼累都要好好接待。
說完這個,我們再來說一說上面的缺陷(原諒我廢話這麼多吧OAQ)
從上面很容易看出來,不管怎麼樣,上面這兩種都是非常消耗資源的。
ajax輪詢 需要伺服器有很快的處理速度和資源。(速度)
long poll 需要有很高的併發,也就是說同時接待客戶的能力。(場地大小)
通過上面這個例子,我們可以看出,這兩種方式都不是最好的方式,需要很多資源。
一種需要更快的速度,一種需要更多的'電話'。這兩種都會導致'電話'的需求越來越高。
哦對了,忘記說了HTTP還是一個無狀態協議。(感謝評論區的各位指出OAQ)
通俗的說就是,伺服器因為每天要接待太多客戶了,是個健忘鬼,你一掛電話,他就把你的東西全忘光了,把你的東西全丟掉了。你第二次還得再告訴伺服器一遍。
所以在這種情況下出現了,Websocket出現了。
他解決了HTTP的這幾個難題。
首先,被動性,當伺服器完成協議升級後(HTTP->Websocket),服務端就可以主動推送資訊給客戶端啦。
所以上面的情景可以做如下修改。
客戶端:啦啦啦,我要建立Websocket協議,需要的服務:chat,Websocket協議版本:17(HTTP Request)
服務端:ok,確認,已升級為Websocket協議(HTTP Protocols Switched)
客戶端:麻煩你有資訊的時候推送給我噢。。
服務端:ok,有的時候會告訴你的。
服務端:balabalabalabala
服務端:balabalabalabala
服務端:哈哈哈哈哈啊哈哈哈哈
服務端:笑死我了哈哈哈哈哈哈哈
就變成了這樣,只需要經過一次HTTP請求,就可以做到源源不斷的資訊傳送了。(在程式設計中,這種設計叫做回撥,即:你有資訊了再來通知我,而不是我傻乎乎的每次跑來問你)
這樣的協議解決了上面同步有延遲,而且還非常消耗資源的這種情況。
那麼為什麼他會解決伺服器上消耗資源的問題呢?
其實我們所用的程式是要經過兩層代理的,即HTTP協議在Nginx等伺服器的解析下,然後再傳送給相應的Handler(PHP等)來處理。
簡單地說,我們有一個非常快速的接線員(Nginx),他負責把問題轉交給相應的客服(Handler)。
本身接線員基本上速度是足夠的,但是每次都卡在客服(Handler)了,老有客服處理速度太慢。,導致客服不夠。
Websocket就解決了這樣一個難題,建立後,可以直接跟接線員建立持久連線,有資訊的時候客服想辦法通知接線員,然後接線員在統一轉交給客戶。
這樣就可以解決客服處理速度過慢的問題了。
雖然接線員很快速,但是每次都要聽這麼一堆,效率也會有所下降的,同時還得不斷把這些資訊轉交給客服,不但浪費客服的處理時間,而且還會在網路傳輸中消耗過多的流量/時間。
但是Websocket只需要一次HTTP握手,所以說整個通訊過程是建立在一次連線/狀態中,也就避免了HTTP的非狀態性,服務端會一直知道你的資訊,直到你關閉請求,這樣就解決了接線員要反覆解析HTTP協議,還要檢視identity info的資訊。
同時由客戶主動詢問,轉換為伺服器(推送)有資訊的時候就傳送(當然客戶端還是等主動傳送資訊過來的。。),沒有資訊的時候就交給接線員(Nginx),不需要佔用本身速度就慢的客服(Handler)了
實現遊戲公告功能
實現功能:遊戲管理裡釋出遊戲公告,其它遊戲玩家頁面能夠馬上接受到遊戲公告資訊。
下面直接上程式碼案例,這裡主要展示關鍵程式碼,底部有原始碼。
一、案例
1、pom.xml檔案
主要是新增springBoot和webSocket相關jar包,和一些輔助工具jar包(注意我採用的是springBoot2.1.0版本
pom.xml2、WebSocketConfig
這個是websocket配置中心,配置一些核心配置。
import org.springframework.context.annotation.Configuration; import org.springframework.messaging.simp.config.MessageBrokerRegistry; import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; import org.springframework.web.socket.config.annotation.StompEndpointRegistry; import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; @Configuration //註解用於開啟使用STOMP協議來傳輸基於代理(MessageBroker)的訊息,這時候控制器(controller)開始支援@MessageMapping,就像是使用@requestMapping一樣。 @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { /** * 註冊端點,釋出或者訂閱訊息的時候需要連線此端點 * setAllowedOrigins 非必須,*表示允許其他域進行連線 * withSockJS 表示開始sockejs支援 */ public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/endpoint-websocket").setAllowedOrigins("*").withSockJS(); } /** * 配置訊息代理(中介) * enableSimpleBroker 服務端推送給客戶端的路徑字首 * setApplicationDestinationPrefixes 客戶端傳送資料給伺服器端的一個字首 */ @Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.enableSimpleBroker("/topic"); registry.setApplicationDestinationPrefixes("/app"); } }
3、GameInfoController
管理員釋出公告訊息對應的介面
/* *模擬遊戲公告 */ @Controller public class GameInfoController { //@MessageMapping和@RequestMapping功能類似,用於設定URL對映地址,瀏覽器向伺服器發起請求,需要通過該地址。 //如果伺服器接受到了訊息,就會對訂閱了@SendTo括號中的地址傳送訊息。 @MessageMapping("/gonggao/chat") @SendTo("/topic/game_chat") public OutMessage gameInfo(InMessage message){ return new OutMessage(message.getContent()); } }
4、管理員頁面和使用者頁面
admin頁面和user頁面唯一的區別就是管理員多一個傳送公告的許可權,其它都一樣,user1和user2完全一樣。
(1)admin.html
admin.html(2)user1.html
user1.html(3)user2.html
user2.html5.app.js
這個是客戶端連線websocket的核心,通過html的點選事件來完成。
var stompClient = null; //這個方法僅僅是用來改變樣式,不是核心 function setConnected(connected) { $("#connect").prop("disabled", connected); $("#disconnect").prop("disabled", !connected); if (connected) { $("#conversation").show(); } else { $("#conversation").hide(); } $("#notice").html(""); } //1、建立連線(先連線服務端配置檔案中的基站,建立連線,然後訂閱伺服器目錄訊息 function connect() { //1、連線SockJS的endpoint是“endpoint-websocket”,與後臺程式碼中註冊的endpoint要一樣。 var socket = new SockJS('/endpoint-websocket'); //2、用stom進行包裝,規範協議 stompClient = Stomp.over(socket); stompClient.connect({}, function (frame) { //3、建立通訊 setConnected(true); console.log('Connected: ' + frame); //4、通過stompClient.subscribe()訂閱伺服器的目標是'/topic/game_chat'傳送過來的地址,與@SendTo中的地址對應。 stompClient.subscribe('/topic/game_chat', function (result) { console.info(result) showContent(JSON.parse(result.body)); }); }); } //2、關閉連線 function disconnect() { if (stompClient !== null) { stompClient.disconnect(); } setConnected(false); console.log("Disconnected"); } //3、遊戲管理員傳送公告資訊(這個也是遊戲使用者所沒有功能,其它都一樣) function sendName() { //1、通過stompClient.send 向/app/gonggao/chat 目標 傳送訊息,這個是在控制器的@messageMapping 中定義的。(/app為字首,配置裡配置) stompClient.send("/app/gonggao/chat", {}, JSON.stringify({'content': $("#content").val()})); } //4、訂閱的訊息顯示在客戶端指定位置 function showContent(body) { $("#notice").append("<tr><td>" + body.content + "</td> <td>"+new Date(body.time).toLocaleString()+"</td></tr>"); } $(function () { $("form").on('submit', function (e) { e.preventDefault(); }); $( "#connect" ).click(function() { connect(); }); $( "#disconnect" ).click(function() { disconnect(); }); $( "#send" ).click(function() { sendName(); }); });
6、檢視執行結果
7、小總結
首先很明顯看的出,websocket最大的優點,就是可以服務端主動向客戶端傳送訊息,而此前http只能是客戶端向服務端傳送請求。
gitHub原始碼:https://github.com/yudiandemingzi/spring-boot-websocket-study
實現一對一聊天功能
功能介紹:實現A和B單獨聊天功能,即A發訊息給B只能B接收,同樣B向A發訊息只能A接收。
本篇部落格是在上一遍基礎上搭建,上一篇部落格地址:【WebSocket】---實現遊戲公告功能。底部有原始碼。
先看演示效果:
一、案例解析
1、PTPContoller
/** * 功能描述:簡單版單人聊天 * 這裡沒有用到@SendTo("/topic/game_chat")來指定訂閱地址,而是通過SimpMessagingTemplate來指定 */ @Controller public class PTPContoller { @Autowired private WebSocketService ws; @MessageMapping("/ptp/single/chat") public void singleChat(InMessage message) { ws.sendChatMessage(message); } }
這裡和前面的公告訊息,最大的區別就是介面上沒有通過@SendTo("/topic/game_chat")來發送訊息。
(1)@SendTo和SimpMessagingTemplate區別
spring websocket基於註解的@SendTo和@SendToUser雖然方便,但是有侷限性,例如我這樣子的需求,我想手動的把訊息推送給某個人,或者特定一組人,怎麼辦,
@SendTo只能推送給所有人(它是一個具體地址,一點都不靈活),@SendToUser只能推送給請求訊息的那個人,這時,我們可以利用SimpMessagingTemplate這個類
SimpMessagingTemplate有倆個推送的方法
convertAndSend(destination, payload); //將訊息廣播到特定訂閱路徑中,類似@SendTo convertAndSendToUser(user, destination, payload);//將訊息推送到固定的使用者訂閱路徑中,類似@SendToUser
2、WebSocketService
import com.jincou.websocket.model.InMessage; import com.jincou.websocket.model.OutMessage; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.stereotype.Service; /** * 功能描述:簡單訊息模板,用來推送訊息 */ @Service public class WebSocketService { @Autowired private SimpMessagingTemplate template; /** * 簡單點對點聊天室 */ public void sendChatMessage(InMessage message) { //可以看出template最大的靈活就是我們可以獲取前端傳來的引數來指定訂閱地址 //前面引數是訂閱地址,後面引數是訊息資訊 template.convertAndSend("/chat/single/"+message.getTo(), new OutMessage(message.getFrom()+" 傳送:"+ message.getContent())); }
3、app.js
其它地方和公告的app.js一樣,只有下面兩點做了一點修改
function connect() { var from = $("#from").val(); var socket = new SockJS('/endpoint-websocket'); stompClient = Stomp.over(socket); stompClient.connect({}, function (frame) { setConnected(true); console.log('Connected: ' + frame); //1、通過+from就可以靈活的用當前使用者的某一資訊來指定該使用者訂閱地址。 stompClient.subscribe('/chat/single/'+from, function (result) { showContent(JSON.parse(result.body)); }); }); } function sendName() { //2、這裡出了傳送content資訊外,還發送了傳送者使用者資訊,和接受者的資訊 stompClient.send("/app/ptp/single/chat", {}, JSON.stringify({'content': $("#content").val(), 'to':$("#to").val(), 'from':$("#from").val()})); }
4、user.html
其它地方也和之前公告的一樣,下面是修改的地方
<div class="col-md-6"> <form class="form-inline"> <div class="form-group"> <input type="text" id="from" class="form-control" placeholder="我是"> <input type="text" id="to" class="form-control" placeholder="傳送給誰"> <input type="text" id="content" class="form-control" placeholder="請輸入..."> </div> <button id="send" class="btn btn-default" type="submit">傳送</button> </form> </div>
5、再把整個思路縷一縷
以 A 向 B 傳送訊息為例
(1)form輸入框輸入:“A”,to輸入框輸入 “B” 點選“Connect”建立websocket連線
(2)那麼的 A 使用者的訂閱地址就是'/chat/single/A'
(3)前端在“content”按鈕中輸入“你今天吃雞了嗎?”,再點選“傳送”按鈕
(4)後臺通過接受處理就成了:
template.convertAndSend("/chat/single/B",new OutMessage(" A 傳送:你今天吃雞了嗎?"));
那麼 B 向 A 傳送性質一模一樣。就可以實現一對一聊天。
實現定時推送比特幣交易資訊
實現功能:跟虛擬幣交易所一樣,時時更新當前比特幣的價格,最高價,最低價,買一價等等......
提示:(1)本篇部落格是在上一遍基礎上搭建,上一篇部落格地址:【WebSocket】---實現遊戲公告功能。
(2)底部有相關原始碼
先看效果演示
當前的資訊就是虛擬幣交易所最新BTC的資料資訊。
我們看到每隔1秒都會更新一次最新的比特幣當前資訊。(截止到我發這篇部落格時,比特幣當前價格:6473美元左右)
一、案例解析
1、如何呼叫虛擬幣的介面
你想獲得BTC最新的價格資訊,你首先的有它的相關介面,不然如何獲取資料,我是在阿里雲上購買的。
具體步驟:
(1)登陸阿里雲-->雲市場-->股票行情於匯率
(2)有很多企業都有相關介面有股票也有虛擬幣
(3)我選的一家名字叫:實時加密貨幣行情+推送
網址:https://market.aliyun.com/products/57000002/cmapi029361.html?spm=5176.730005.productlist.d_cmapi029361.xtd4I4
(4)對於介面都有相關說明,按照它的說明就可以獲取json資料。同時也可以線上除錯。
2、通過定時任務時時向客戶端傳送訊息
因為需要服務端隔一定時間向客戶端傳送訊息,所有服務端用定時任務再好不過了。
/** * //要啟動定時任務記得在啟動類上新增下面兩個註解 * @ComponentScan(basePackages="com.jincou.websocket") * @EnableScheduling * 功能描述:股票推送,這裡只需通過定時任務向客服端傳送訊息 */ @Component public class CoinSchedule { @Autowired private WebSocketService ws; //代表每一秒執行一次任務 @Scheduled(fixedRate=1000) public void coinInfo(){ ws.sendCoinInfo(); } }
3、WebSocketService類
訊息模版工具類,用來推送訊息用的。
/** * 功能描述:簡單訊息模板,用來推送訊息 */ @Service public class WebSocketService { @Autowired private SimpMessagingTemplate template; /** * 功能描述:Coin版本,虛擬幣資訊推送 */ public void sendCoinInfo() { //CoinService.getStockInfo()已經把json資料轉為實體物件 CoinResult coinResult = CoinService.getStockInfo(); String msgTpl = "虛擬幣名稱: %s ;程式碼: %s; 現價格: %s元 ;買一價: %s ; 買一量: %s ; 買二價: %s ; 賣二量: %s;"; CoinResult.Obj obj=coinResult.getObj(); if (null != obj) { //將 %s 替換成實際值 String msg = String.format(msgTpl, obj.getName(), obj.getSecurityCode(), obj.getNow(), obj.getBid1(), obj.getBid1Volume(), obj.getAsk1(), obj.getAsk1Volume()); //前面引數是訂閱地址,後面引數是訊息資訊(也就是比特幣時時訊息) template.convertAndSend("/topic/coin_info",new OutMessage(msg)); } } }
4、CoinService呼叫介面,並把json格式資料賦值給物件
這個是最關鍵的一步,主要做的事:去調遠端介面獲取資料後,將資料封裝到自己所寫的bean實體中。
import java.util.HashMap; import java.util.Map; import com.jincou.websocket.model.CoinResult; import com.jincou.websocket.utils.HttpUtils; import com.jincou.websocket.utils.JsonUtils; import org.apache.http.HttpResponse; import org.apache.http.util.EntityUtils; /** * 功能描述:介面服務,呼叫虛擬幣行情介面 */ public class CoinService { public static CoinResult getStockInfo(){ String host = "http://alirm-gbdc.konpn.com"; String path = "/query/gbdc"; String method = "GET"; String appcode = "你的AppCode"; Map<String, String> headers = new HashMap<String, String>(); //最後在header中的格式(中間是英文空格)為Authorization:APPCODE 83359fd73fe94948385f570e3c139105 headers.put("Authorization", "APPCODE " + appcode); Map<String, String> querys = new HashMap<String, String>(); //BTC代表返回比特幣相關資訊,如果這裡傳入ETH那就代表返回以太坊資訊 querys.put("symbol", "BTC"); try { //返回連線資訊,如果裡面帶有200,說明連線介面成功 HttpResponse response = HttpUtils.doGet(host, path, method, headers, querys); //將response的body資訊轉為字串 String responseText=EntityUtils.toString(response.getEntity()); //上面部分只要根據你購買的api介面說明操作就可以,下面才是你需要處理的 //將json格式的字串(根據一定規則)賦值給實體物件(JsonUtils是自己的一個工具類) CoinResult coinResult = JsonUtils.objectFromJson(responseText, CoinResult.class); System.out.println("控制檯列印虛擬幣當前資訊======================================="); System.out.println(coinResult.toString()); return coinResult; } catch (Exception e) { e.printStackTrace(); } return null; } }
5、json格式如何封裝到實體
這步主要講,將json格式字串通過工具類封裝到實體物件需要滿足的規則:
CoinResult coinResult = JsonUtils.objectFromJson(responseText, CoinResult.class); //看這步所需要滿足的規則
(1)先看介面的json格式
{"Code":0,"Msg":"", "Obj":{ "B1":271.100, --買一 "B1V":129, --買一量 "B2":0, --買二 "B2V":0, "B3":0, --買三 "B3V":0, "B4":0, --買四 "B4V":0, "B5":0, --買五 "B5V":0, "S1":271.150, --賣一 "S1V":20, --賣一量 "S2":0, --賣二 "S2V":0, "S3":0, --賣三 "S3V":0, "S4":0, --賣四 "S4V":0, "S5":0, --賣五 "S5V":0, "ZT":280.85, --漲停價 "DT":259.19, --跌停價 "O":270.39, --今開 "H":271.69, --最高 "L":270.14, --最低 "YC":270.55, --昨收 "A":35513202100.0, --交易額 "V":130972, --交易量 "P":271.14, --當前價 "Tick":1529911046, --標準時間戳 "N":"比特幣", --品種名 "M":"", --市場 "S":"BTC", --品種程式碼 "C":"" --編號 } }
(2)在看我的實體物件屬性
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; @JsonIgnoreProperties(ignoreUnknown = true) @Data public class CoinResult { //狀態碼,0代表成功 @JsonProperty("Code") private int Code; //具體資料(注意這裡json用{表示,所有代表物件 @JsonProperty("Obj") private Obj obj; @Data @JsonIgnoreProperties(ignoreUnknown = true) public static class Obj { //虛擬幣程式碼 @JsonProperty("S") private String securityCode; //虛擬幣名稱 @JsonProperty("N") private String name; //現在價格 @JsonProperty("P") private double now; //最高價格 @JsonProperty("H") private double high; //最低價格 @JsonProperty("L") private double low; //買一價 @JsonProperty("B1") private double bid1; //買一量 @JsonProperty("B1V") private int bid1Volume; //賣一價 @JsonProperty("S1") private double ask1; //賣一量 @JsonProperty("S1V") private double ask1Volume; //已成交價,這個介面沒有提供,只要記住{}代表是物件,【】代表是結合那就需要集合接受:如下 //private List<Transaction> transactions; } }
總結規則:
(1)json中的名字和實體中屬性名一定要一致才能賦值。
(2)如果只要有一個你名字一致而資料型別不一樣,那麼就會整體賦值失敗返回null。比如這裡B1價,它明明是double,如你你用int接收,那麼就會返回null。
(3)json格式中的資料如果是{},那麼可以用物件來接收,好比這的"Obj":{...},如果是{[],[]},那就需要List<物件>來接收
6、看前端
前端沒啥好說的只需要訂閱:/topic/coin_info 這個地址就可以接收服務端時時發來的訊息了。
gitHub原始碼:https://github.com/yudiandemingzi/spring-boot-websocket-study
多人聊天系統
功能說明:多人聊天系統,主要功能點:
1、當你登陸成功後,可以看到所有線上使用者(實際開發可以通過redis實現,我這邊僅僅用map集合)
2、實現群聊功能,我傳送訊息,大家都可以看到。
先看案例效果:
這裡面有關線上人數有個bug,就是線上使用者會被覆蓋,lisi登陸的話,zhangsan線上資訊就丟來,xiaoxiao登陸,lisi就丟來,這主要原因是因為我放的是普通集合,所以線上使用者資料是無法共享
所以只能顯示最後顯示的使用者,如果放到redis就不會有這個問題。
一、案例說明
1、UserChatController
@Controller public class UserChatController { @Autowired private WebSocketService ws; /** * 1、登陸時,模擬資料庫的使用者資訊 */ //模擬資料庫使用者的資料 public static Map<String, String> userMap = new HashMap<String, String>(); static{ userMap.put("zhangsan", "123"); userMap.put("lisi", "456"); userMap.put("wangwu", "789"); userMap.put("zhaoliu", "000"); userMap.put("xiaoxiao", "666"); } /** *2、 模擬使用者線上進行頁面跳轉的時候,判斷是否線上 * (這個實際開發中肯定存在redis或者session中,這樣資料才能共享) * 這裡只是簡單的做個模擬,所以暫且用普通map吧 */ public static Map<String, User> onlineUser = new HashMap<>(); static{ //key值一般是每個使用者的sessionID(這裡表示admin使用者一開始就線上) onlineUser.put("123",new User("admin","888")); } /** *3、 功能描述:使用者登入介面 */ @RequestMapping(value="login", method=RequestMethod.POST) public String userLogin( @RequestParam(value="username", required=true)String username, @RequestParam(value="pwd",required=true) String pwd, HttpSession session) { //判斷是否正確 String password = userMap.get(username); if (pwd.equals(password)) { User user = new User(username, pwd); String sessionId = session.getId(); //使用者登陸成功就把該使用者放到線上使用者中... onlineUser.put(sessionId, user); //跳到群聊頁面 return "redirect:/group/chat.html"; } else { return "redirect:/group/error.html"; } } /** *4、 功能描述:用於定時給客戶端推送線上使用者 */ @Scheduled(fixedRate = 2000) public void onlineUser() { ws.sendOnlineUser(onlineUser); } /** *5、 功能描述 群聊天介面 * message 訊息體 * headerAccessor 訊息頭訪問器,通過這個獲取sessionId */ @MessageMapping("/group/chat") public void topicChat(InMessage message, SimpMessageHeaderAccessor headerAccessor){ //這個sessionId是在HttpHandShakeIntecepter攔截器中放入的 String sessionId = headerAccessor.getSessionAttributes().get("sessionId").toString(); //通過sessionID獲得線上使用者資訊 User user = onlineUser.get(sessionId); message.setFrom(user.getUsername()); ws.sendTopicChat(message); } }
2、握手請求的攔截器
/** * WebSocket握手請求的攔截器. 檢查握手請求和響應, 對WebSocketHandler傳遞屬性 * 可以通過這個類的方法獲取resuest,和response */ public class HttpHandShakeIntecepter implements HandshakeInterceptor{ //在握手之前執行該方法, 繼續握手返回true, 中斷握手返回false. 通過attributes引數設定WebSocketSession的屬性 //這個專案只在WebSocketSession這裡存入sessionID @Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception { System.out.println("【握手攔截器】beforeHandshake"); if(request instanceof ServletServerHttpRequest) { ServletServerHttpRequest servletRequest = (ServletServerHttpRequest)request; HttpSession session = servletRequest.getServletRequest().getSession(); String sessionId = session.getId(); System.out.println("【握手攔截器】beforeHandshake sessionId="+sessionId); //這裡將sessionId放入SessionAttributes中, attributes.put("sessionId", sessionId); } return true; } //在握手之後執行該方法. 無論是否握手成功都指明瞭響應狀態碼和相應頭(這個專案沒有用到該方法) @Override public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) { System.out.println("【握手攔截器】afterHandshake"); if(request instanceof ServletServerHttpRequest) { ServletServerHttpRequest servletRequest = (ServletServerHttpRequest)request; HttpSession session = servletRequest.getServletRequest().getSession(); String sessionId = session.getId(); System.out.println("【握手攔截器】afterHandshake sessionId="+sessionId); } } }
3、頻道攔截器
/** * 功能描述:頻道攔截器 ,類似管道,可以獲取訊息的一些meta資料 */ public class SocketChannelIntecepter extends ChannelInterceptorAdapter{ /** * 在完成傳送之後進行呼叫,不管是否有異常發生,一般用於資源清理 */ @Override public void afterSendCompletion(Message<?> message, MessageChannel channel, boolean sent, Exception ex) { System.out.println("SocketChannelIntecepter->afterSendCompletion"); super.afterSendCompletion(message, channel, sent, ex); } /** * 在訊息被實際傳送到頻道之前呼叫 */ @Override public Message<?> preSend(Message<?> message, MessageChannel channel) { System.out.println("SocketChannelIntecepter->preSend"); return super.preSend(message, channel); } /** * 傳送訊息呼叫後立即呼叫 */ @Override public void postSend(Message<?> message, MessageChannel channel, boolean sent) { System.out.println("SocketChannelIntecepter->postSend"); StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(message);//訊息頭訪問器 if (headerAccessor.getCommand() == null ) return ;// 避免非stomp訊息型別,例如心跳檢測 String sessionId = headerAccessor.getSessionAttributes().get("sessionId").toString(); System.out.println("SocketChannelIntecepter -> sessionId = "+sessionId); switch (headerAccessor.getCommand()) { case CONNECT: connect(sessionId); break; case DISCONNECT: disconnect(sessionId); break; case SUBSCRIBE: break; case UNSUBSCRIBE: break; default: break; } } /** * 連線成功 */ private void connect(String sessionId){ System.out.println("connect sessionId="+sessionId); } /** * 斷開連線 */ private void disconnect(String sessionId){ System.out.println("disconnect sessionId="+sessionId); //使用者下線操作 UserChatController.onlineUser.remove(sessionId); } }
4、修改webSocket配置類
既然寫了兩個攔截器,那麼肯定需要在配置資訊裡去配置它們。
@EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { /** *配置基站 */ public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/endpoint-websocket").addInterceptors(new HttpHandShakeIntecepter()).setAllowedOrigins("*").withSockJS(); } /** * 配置訊息代理(中介) * enableSimpleBroker 服務端推送給客戶端的路徑字首 * setApplicationDestinationPrefixes 客戶端傳送資料給伺服器端的一個字首 */ @Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.enableSimpleBroker("/topic","/chat"); registry.setApplicationDestinationPrefixes("/app"); } @Override public void configureClientInboundChannel(ChannelRegistration registration) { registration.interceptors( new SocketChannelIntecepter()); } @Override public void configureClientOutboundChannel(ChannelRegistration registration) { registration.interceptors( new SocketChannelIntecepter()); } }
5、app.js
登陸頁面和群聊頁面就不細聊,貼上程式碼就好。
index.html
index.htmlchat.html
chat.htmlapp.js
var stompClient = null;
//一載入就會呼叫該方法 function connect() { var socket = new SockJS('/endpoint-websocket'); stompClient = Stomp.over(socket); stompClient.connect({}, function (frame) { setConnected(true); console.log('Connected: ' + frame); //訂閱群聊訊息 stompClient.subscribe('/topic/chat', function (result) { showContent(JSON.parse(result.body)); }); //訂閱線上使用者訊息 stompClient.subscribe('/topic/onlineuser', function (result) { showOnlieUser(JSON.parse(result.body)); }); }); } //斷開連線 function disconnect() { if (stompClient !== null) { stompClient.disconnect(); } setConnected(false); console.log("Disconnected"); } //傳送聊天記錄 function sendContent() { stompClient.send("/app/group/chat", {}, JSON.stringify({'content': $("#content").val()})); } //顯示聊天記錄 function showContent(body) { $("#record").append("<tr><td>" + body.content + "</td> <td>"+new Date(body.time).toLocaleTimeString()+"</td></tr>"); } //顯示實時線上使用者 function showOnlieUser(body) { $("#online").html("<tr><td>" + body.content + "</td> <td>"+new Date(body.time).toLocaleTimeString()+"</td></tr>"); } $(function () { connect();//自動上線 $("form").on('submit', function (e) { e.preventDefault(); }); $( "#disconnect" ).click(function() { disconnect(); }); $( "#send" ).click(function() { sendContent(); }); });
gitHub原始碼:https://github.com/yudiandemingzi/spring-boot-websocket-study