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
