Web 應用的交互過程通常是客戶端通過瀏覽器發出一個請求,服務器端接收請求后進行處理並返回結果給客戶端,客端瀏覽器將信息呈現。但是對於實時性要求較高、海量並發的應用,比如金融證券的實時信息,web導航應用中地理位置獲取,社交網絡的實時消息推送等。
解決方案
方案一:客戶端用js代碼每隔一定時間向服務器發送請求,這樣會造成資源浪費,在高並發的情況下還可能造成服務器奔潰。
方案二:基於Flash、AdobeFlash,通過socket實現數據信息交互,再利用Flash暴露的接口供js調用,但是Flash在移動互聯網上的支持不好,IOS和Android都不支持Flash了。
方案三:WebSocket,2014年開始,各大應用服務器和瀏覽器廠商逐步統一,J2EE7也實現了WebSocket協議,無論客戶端還是服務器都提供了對其的支持。
WebSocket介紹與原理
WebSocket 是 HTML5 一種新的協議。它實現了瀏覽器與服務器全雙工通信,能更好的節省服務器資源和帶寬並達到實時通訊,它建立在 TCP 之上,同 HTTP 一樣通過 TCP 來傳輸數據,但是它和HTTP 最大不同是:
WebSocket 是一種雙向通信協議,在基於http建立連接后,WebSocket 服務器和 browser都能主動向對方發送或接收數據,就像 Socket 一樣;WebSocket 需要類似 TCP 的客戶端和服務器端通過握手連接,連接成功后才能相互通信,實現長連接。
WebSocket 客戶端連接報文
GET /webfin/websocket/ HTTP/1.1
Host: localhost
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: xqBt3ImNzJbYqRINxEFlkg==
Origin: http://localhost:8080
Sec-WebSocket-Version: 13
可以看到,客戶端發起的 WebSocket 連接報文類似傳統 HTTP 報文,”Upgrade:websocket”參數值表明這是 WebSocket 類型請求,“Sec-WebSocket-Key”是 WebSocket 客戶端發送的一個 base64 編碼的密文,要求服務端必須返回一個對應加密的“Sec-WebSocket-Accept”應答,否則客戶端會拋出“Error during WebSocket handshake”錯誤,並關閉連接。
WebSocket 服務端響應報文
HTTP/1.1 101 Switching Protocols
“Sec-WebSocket-Accept”的值是服務端采用與客戶端一致的密鑰計算出來后返回客戶端的,“HTTP/1.1 101 Switching Protocols”表示服務端接受 WebSocket 協議的客戶端連接,經過這樣的請求-響應處理后,客戶端服務端的 WebSocket 連接握手成功, 后續就可以進行 TCP 通訊了。
下載javax.websocket.jar,使用注解方式實現了一個簡單的多房間聊天demo,demo只有一個服務端類和一個前端chat.html頁面,打開多個chat.html頁面,輸入相同的房間名,進入房間后可以相互通信,不同房間不能互相通信,不同用戶我用websocket的session自己分配的id來區分,因為一個用戶連接到webSocket服務器就對應一個session,實際開發可以用http的session中登錄的用戶名來區分,連接到服務器的url中,roomName是一個路徑參數,即在chat.html中獲取到房間名。多房間的原理其實就是把多個用(session)放在roomName對應的set集合中,每次廣播信息只在房間名對應的set集合中廣播,實現房間聊天信息的隔離。
代碼如下:
package cn.com.taiji.controller; import cn.com.taiji.util.StringUtil; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import javax.websocket.*; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; import java.io.IOException; import java.net.URLDecoder; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; /** * @ServerEndpoint 注解是一個類層次的注解,它的功能主要是將目前的類定義成一個websocket服務器端, * 注解的值將被用於監聽用戶連接的終端訪問URL地址,客戶端可以通過這個URL來連接到WebSocket服務器端 */ @ServerEndpoint(value = "/websocket/{userJson}") @Component public class MyWebSocket { static Log log= LogFactory.getLog(MyWebSocket.class); //靜態變量,用來記錄當前在線連接數。應該把它設計成線程安全的。 private static int onlineCount = 0; // 使用map來收集session,key為roomName,value為同一個房間的用戶集合 // concurrentMap的key不存在時報錯,不是返回null //以案號為key作為一個房間 private static Map<String, ConcurrentHashMap<String,MyWebSocket>> rooms = new ConcurrentHashMap(); //concurrent包的線程安全Set,用來存放每個客戶端對應的MyWebSocket對象。若要實現服務端與單一客戶端通信的話,可以使用Map來存放,其中Key可以為用戶標識 // private static CopyOnWriteArraySet<MyWebSocket> webSocketSet = new CopyOnWriteArraySet<MyWebSocket>(); //記錄每個房間的 // private static ConcurrentHashMap<String,MyWebSocket> webSocketMap = new ConcurrentHashMap<>(); /** * 與某個客戶端的連接對話,需要通過它來給客戶端發送消息 */ private Session session; /** * 標識當前連接客戶端的用戶名 */ private String userName; /** * 標識當前連接客戶端的房間名稱 */ private String roomName; /** * 連接建立成功調用的方法 * @param session 可選的參數。session為與某個客戶端的連接會話,需要通過它來給客戶端發送數據 */ @OnOpen public void onOpen(Session session, @PathParam("userJson") String userJson){ try{ //解析發送的報文 String[] split = userJson.split(","); this.roomName = split[0].trim(); if(split.length>=1){ this.userName = split[1].trim(); } this.session = session; // 將session按照房間名來存儲,將各個房間的用戶隔離 if (!rooms.containsKey(roomName)) { // 對應房間不存在時,創建房間 ConcurrentHashMap<String,MyWebSocket> room = new ConcurrentHashMap<>(); // 添加用戶 if (StringUtil.isEmpty(this.userName)){ room.put(session.getId(),this); }else { room.put(userName,this); } rooms.put(roomName, room); } else { // 房間已存在,直接添加用戶到相應的房間 rooms.get(roomName).put(userName,this); } addOnlineCount();//在線數加1 log.info("有新連接加入!當前在線人數為" + getOnlineCount()); }catch (Exception e){ e.printStackTrace(); } } /** * 收到客戶端消息后調用的方法 * @param msg 客戶端發送過來的消息 * @param session 可選的參數 */ @OnMessage public void receiveMsg(String msg, Session session) throws Exception { // 此處應該有html過濾 // 接收到信息后進行廣播 String roomName = this.roomName; broadcast(roomName, msg); } // 按照房間名進行廣播 public static void broadcast(String roomName, String msg) throws Exception { ConcurrentHashMap<String, MyWebSocket> map = rooms.get(roomName); for(String key:map.keySet()){//keySet獲取map集合key的集合 然后在遍歷key即可 try{ MyWebSocket myWebSocket = map.get(key); myWebSocket.sendMessage(msg);// }catch (Exception e){ e.printStackTrace(); } } } /** * 連接關閉調用的方法 */ @OnClose public void onClose(@PathParam("roomName") String roomName, Session session){ String userName = this.userName; if (StringUtil.isEmpty(userName)){ rooms.get(roomName).remove(session.getId()); }else { rooms.get(roomName).remove(userName); } subOnlineCount(); log.info("用戶退出:"+userName+",當前在線人數為:" + getOnlineCount()); } /** * 發生錯誤時調用 * @param session * @param error */ @OnError public void onError(Session session, Throwable error){ log.error("發生錯誤"); error.printStackTrace(); } /** * 這個方法與上面幾個方法不一樣。沒有用注解,是根據自己需要添加的方法。 * @param message * @throws IOException */ public void sendMessage(String message) throws IOException{ this.session.getBasicRemote().sendText(message); //this.session.getAsyncRemote().sendText(message); } public static synchronized int getOnlineCount() { return onlineCount; } public static synchronized void addOnlineCount() { MyWebSocket.onlineCount++; } public static synchronized void subOnlineCount() { MyWebSocket.onlineCount--; } private boolean isjson(String str) { try { JSONObject jsonStr = JSONObject.parseObject(str); return true; }catch (Exception e) { return false; } } }
頁面代碼
<%@ page language="java" pageEncoding="UTF-8" %> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>網絡聊天室</title> </head> <style type="text/css"> .msg_board { width: 322px; height: 100px; border: solid 1px darkcyan; padding: 5px; overflow-y: scroll; // 文字長度大於div寬度時換行顯示 word-break: break-all; } /*set srcoll start*/ ::-webkit-scrollbar { width: 10px; height: 10px; background-color: #D6F2FD; } ::-webkit-scrollbar-track { -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); /*border-radius: 5px;*/ background-color: #D6F2FD; } ::-webkit-scrollbar-thumb { height: 20px; /*border-radius: 10px;*/ -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3); background-color: #89D7F7; } /*set srcoll end*/ </style> <body> <label>房間名</label> <input id="userName" type="hidden" name="userName" value="${userName}"> <input id="dsrdw" type="hidden" name="dsrdw" value="${dsrdw}"> <input id="input_roomName" type="text" value="${caseNum}"> <button onclick="initWebSocket()">進入聊天室</button> <button onclick="closeWs()">退出聊天室</button> <div class="msg_board"></div> <input id="input_msg" size="43" maxlength="40"> <button onclick="send_msg()">發送</button> </body> <script type="text/javascript"> var webSocket = null; function send_msg() { var t = ""; if (webSocket != null) { var input_msg = document.getElementById("input_msg").value.trim(); if (input_msg == "") { return; } webSocket.send(input_msg); // 清除input框里的信息 document.getElementById("input_msg").value = ""; // var msg_board = document.getElementsByClassName("msg_board")[0]; // var received_msg = input_msg; // var old_msg = msg_board.innerHTML; // msg_board.innerHTML = old_msg + received_msg + "<br>"; // // 讓滾動塊往下移動 // msg_board.scrollTop = msg_board.scrollTop + 40; } else { alert("您已掉線,請重新進入聊天室..."); } }; function closeWs() { webSocket.close(); }; function initWebSocket() { var roomName = document.getElementById("input_roomName").value; var userName = document.getElementById("userName").value; // 房間名不能為空 if (roomName == null || roomName == "") { alert("請輸入房間名"); return; } var userJson = roomName+","+userName; if ("WebSocket" in window) { if (webSocket == null) { var url = "ws://127.0.0.1:8080/websocket/" + userJson; // 打開一個 web socket webSocket = new WebSocket(url); } else { alert("您已進入聊天室..."); } webSocket.onopen = function () { alert("已進入聊天室,暢聊吧..."); }; webSocket.onmessage = function (evt) { var msg_board = document.getElementsByClassName("msg_board")[0]; var received_msg = evt.data; var old_msg = msg_board.innerHTML; msg_board.innerHTML = old_msg + received_msg + "<br>"; // 讓滾動塊往下移動 msg_board.scrollTop = msg_board.scrollTop + 40; }; webSocket.onclose = function () { // 關閉 websocket,清空信息板 alert("連接已關閉..."); webSocket = null; document.getElementsByClassName("msg_board")[0].innerHTML = ""; }; } else { // 瀏覽器不支持 WebSocket alert("您的瀏覽器不支持 WebSocket!"); } } </script> </html>
原文鏈接:https://blog.csdn.net/zhengholien/java/article/details/76696509