為什么需要WebSocket?
我們已經有了http協議,為什么還需要另外一個協議?有什么好處?
比如我想得到價格變化,只能是客戶端想服務端發起請求,服務器返回結果,HTTP協議做不到服務器主動向客戶端推送消息,
這種單向請求的特點,如果服務器有連續的狀態變化,客戶端要獲取指定只能輪詢,每隔一段時間,調一次接口,了解服務器有沒有新的價格信息
輪詢的效率低,且非常浪費資源,因此工程師們一直在思考,有沒有更好的方法。websocket就是這樣發明的。
是什么?
WebSocket 是 HTML5 開始提供的一種在單個 TCP 連接上進行全雙工通訊的協議。
WebSocket 使得客戶端和服務器之間的數據交換變得更加簡單,允許服務端主動向客戶端推送數據。在 WebSocket API 中,瀏覽器和服務器只需要完成一次握手,兩者之間就直接可以創建持久性的連接,並進行雙向數據傳輸。
websocket協議特點
1 推送功能
支持服務器端向客戶端推送功能。服務器可以直接發送數據而不用等待客戶端的請求。
2 減少通信量
只要建立起websocket連接,就一直保持連接,在此期間可以源源不斷的傳送消息,直到關閉請求。也就避免了HTTP的非狀態性。
和http相比,不但每次連接時的總開銷減少了,而且websocket的首部信息量也小 ,通信量也減少了。
3 減少資源消耗
那么為什么他會解決服務器上消耗資源的問題呢?
其實我們所用的程序是要經過兩層代理的,即HTTP協議在Nginx等服務器的解析下,然后再傳送給相應的Handler(PHP等)來處理
簡單地說,我們有一個非常快速的接線員(Nginx),他負責把問題轉交給相應的客服(Handler)。本身接線員基本上速度是足夠的,但是每次都卡在客服(Handler)了,老有客服處理速度太慢。導致客服不夠。Websocket就解決了這樣一個難題,建立后,可以直接跟接線員建立持久連接,有信息的時候客服想辦法通知接線員,然后接線員在統一轉交給客戶。這樣就可以解決客服處理速度過慢的問題了。
概念說完了 開整
第一步 導包
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>27.1-jre</version> </dependency>
其中guava包提供了一些集合Map的簡單創建等,可以不用導
第二步 配置類
@Configuration public class WebSocketConfig{ @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } }
第三步 控制層
@Slf4j @Controller public class SocketController { @RequestMapping("/websocket/{name}") public String webSocket(@PathVariable String name, Model model){ try{ log.info("跳轉到websocket的頁面上"); model.addAttribute("username",name); return "index"; } catch (Exception e){ log.info("跳轉到websocket的頁面上發生異常,異常信息是:"+e.getMessage()); return "error"; } } }
此處注意用的是@Controller而不是@RestController
因為RestController多了一個注解 @ResponseBody ,這注解個干啥我就不解釋了
第四步
@Slf4j @Component @ServerEndpoint("/websocket/{username}") public class WebSocket { /** * 在線人數 */ public static volatile AtomicInteger onlineNumber = new AtomicInteger(0); /** * 以用戶的姓名為key,WebSocket為對象保存起來 */ private static Map<String, WebSocket> clients = new ConcurrentHashMap<>(); /** * 會話 */ private Session session; /** * 用戶名稱 */ private String username; /** * 建立連接 * * @param session */ @OnOpen public void onOpen(@PathParam("username") String username, Session session) { System.out.println(this); onlineNumber.addAndGet(1); log.info("現在來連接的客戶id:"+session.getId()+"用戶名:"+username); this.username = username; this.session = session; log.info("有新連接加入! 當前在線人數" + onlineNumber); try { //messageType 1代表上線 2代表下線 3代表在線名單 4代表普通消息 //先給所有人發送通知,說我上線了 Map<String,Object> map1 = Maps.newHashMap(); map1.put("messageType",1); map1.put("username",username); sendMessageAll(JSON.toJSONString(map1),username); //把自己的信息加入到map當中去 clients.put(username, this); //給自己發一條消息:告訴自己現在都有誰在線 Map<String,Object> map2 = Maps.newHashMap(); map2.put("messageType",3); //移除掉自己 Set<String> set = clients.keySet(); map2.put("onlineUsers",set); sendMessageTo(JSON.toJSONString(map2),username); } catch (IOException e){ log.info(username+"上線的時候通知所有人發生了錯誤"); } } @OnError public void onError(Session session, Throwable error) { log.info("服務端發生了錯誤"+error.getMessage()); //error.printStackTrace(); } /** * 連接關閉 */ @OnClose public void onClose() { onlineNumber.addAndGet(-1); //webSockets.remove(this); clients.remove(username); try { //messageType 1代表上線 2代表下線 3代表在線名單 4代表普通消息 Map<String,Object> map1 = Maps.newHashMap(); map1.put("messageType",2); map1.put("onlineUsers",clients.keySet()); map1.put("username",username); sendMessageAll(JSON.toJSONString(map1),username); } catch (IOException e){ log.info(username+"下線的時候通知所有人發生了錯誤"); } log.info("有連接關閉! 當前在線人數" + onlineNumber); } /** * 收到客戶端的消息 * * @param message 消息 * @param session 會話 */ @OnMessage public void onMessage(String message, Session session) { try { log.info("來自客戶端消息:" + message+"客戶端的id是:"+session.getId()); JSONObject jsonObject = JSON.parseObject(message); String textMessage = jsonObject.getString("message"); String fromusername = jsonObject.getString("username"); String tousername = jsonObject.getString("to"); //messageType 1代表上線 2代表下線 3代表在線名單 4代表普通消息 Map<String,Object> map1 = Maps.newHashMap(); map1.put("messageType",4); map1.put("textMessage",textMessage); map1.put("fromusername",fromusername); if(tousername.equals("All")){ map1.put("tousername","所有人"); sendMessageAll(JSON.toJSONString(map1),fromusername); } else{ map1.put("tousername",tousername); sendMessageTo(JSON.toJSONString(map1),tousername); } } catch (Exception e){ log.info("發生了錯誤了"); } } public void sendMessageTo(String message, String ToUserName) throws IOException { WebSocket webSocket = clients.get(ToUserName); webSocket.session.getBasicRemote().sendText(message); // for (WebSocket item : clients.values()) { // if (item.username.equals(ToUserName) ) { // item.session.getAsyncRemote().sendText(message); // break; // } // } } public void sendMessageAll(String message,String FromUserName) throws IOException { for (WebSocket item : clients.values()) { item.session.getAsyncRemote().sendText(message); } } public static synchronized AtomicInteger getOnlineCount() { return onlineNumber; } }
看完代碼有沒有發現我在map中放的是this 為什么?
因為@ServerEndpoint不是單例模式
onlineNumber這個是線程安全的計數類 如果想深入了解就看看CAS吧
5 頁面目錄
放置resources/remplates下
6 頁面代碼
<!DOCTYPE html> <html> <head> <title>websocket</title> <script type="text/javascript" src="http://ajax.microsoft.com/ajax/jquery/jquery-1.4.min.js"></script> <script src="http://cdn.bootcss.com/stomp.js/2.3.3/stomp.min.js"></script> <script src="https://cdn.bootcss.com/sockjs-client/1.1.4/sockjs.min.js"></script> </head> <body> <div style="margin: auto;text-align: center"> <h1>Welcome to websocket</h1> </div> <br/> <div style="margin: auto;text-align: center"> <select id="onLineUser"> <option>--所有--</option> </select> <input id="text" type="text" /> <button onclick="send()">發送消息</button> </div> <br> <div style="margin-right: 10px;text-align: right"> <button onclick="closeWebSocket()">關閉連接</button> </div> <hr/> <div id="message" style="text-align: center;"></div> <input type="text" th:value="${username}" id="username" style="display: none" /> </body> <script type="text/javascript"> var webSocket; var commWebSocket; if ("WebSocket" in window) { webSocket = new WebSocket("ws://localhost:8080/websocket/"+document.getElementById('username').value); //連通之后的回調事件 webSocket.onopen = function() { //webSocket.send( document.getElementById('username').value+"已經上線了"); console.log("已經連通了websocket"); setMessageInnerHTML("已經連通了websocket"); }; //接收后台服務端的消息 webSocket.onmessage = function (evt) { var received_msg = evt.data; console.log("數據已接收:" +received_msg); var obj = JSON.parse(received_msg); console.log("可以解析成json:"+obj.messageType); //1代表上線 2代表下線 3代表在線名單 4代表普通消息 if(obj.messageType==1){ //把名稱放入到selection當中供選擇 var onlineName = obj.username; var option = "<option>"+onlineName+"</option>"; $("#onLineUser").append(option); setMessageInnerHTML(onlineName+"上線了"); } else if(obj.messageType==2){ $("#onLineUser").empty(); var onlineName = obj.onlineUsers; var offlineName = obj.username; var option = "<option>"+"--所有--"+"</option>"; for(var i=0;i<onlineName.length;i++){ if(!(onlineName[i]==document.getElementById('username').value)){ option+="<option>"+onlineName[i]+"</option>" } } $("#onLineUser").append(option); setMessageInnerHTML(offlineName+"下線了"); } else if(obj.messageType==3){ var onlineName = obj.onlineUsers; var option = null; for(var i=0;i<onlineName.length;i++){ if(!(onlineName[i]==document.getElementById('username').value)){ option+="<option>"+onlineName[i]+"</option>" } } $("#onLineUser").append(option); console.log("獲取了在線的名單"+onlineName.toString()); } else{ setMessageInnerHTML(obj.fromusername+"對"+obj.tousername+"說:"+obj.textMessage); } }; //連接關閉的回調事件 webSocket.onclose = function() { console.log("連接已關閉..."); setMessageInnerHTML("連接已經關閉...."); }; } else{ // 瀏覽器不支持 WebSocket alert("您的瀏覽器不支持 WebSocket!"); } //將消息顯示在網頁上 function setMessageInnerHTML(innerHTML) { document.getElementById('message').innerHTML += innerHTML + '<br/>'; } function closeWebSocket() { //直接關閉websocket的連接 webSocket.close(); } function send() { var selectText = $("#onLineUser").find("option:selected").text(); if(selectText=="--所有--"){ selectText = "All"; } else{ setMessageInnerHTML(document.getElementById('username').value+"對"+selectText+"說:"+ $("#text").val()); } var message = { "message":document.getElementById('text').value, "username":document.getElementById('username').value, "to":selectText }; webSocket.send(JSON.stringify(message)); $("#text").val(""); } </script> </html>
總結一下
此處注意用的是@Controller而不是@RestController
因為RestController多了一個注解 @ResponseBody
看完代碼有沒有發現我在map中放的是this 為什么?
因為@ServerEndpoint不是單例模式
onlineNumber這個是線程安全的計數類 如果想深入了解就看看CAS吧