最近看到了WebSocket,不免想做些什麼小功能,然後就選擇了聊天室,首先當然先介紹什麼是WebSocket
1. WebSocket
WebSocket 是 HTML5 開始提供的可在單個 TCP 連接配接上進行全雙工通訊的協定,其允許服務端主動向用戶端推送資料,浏覽器和伺服器隻需要完成一次握手,兩者之間就直接可以建立持久性的連接配接,并進行雙向資料傳輸
注意:WebSocket 和 HTTP 的差別,WebSocket雖建立在HTTP上,但屬于新的獨立協定,隻是其建立連接配接的過程需要用到HTTP協定
為什麼需要WebSocket?
解決HTTP協定的某些缺陷 ---- 通信隻能由用戶端發起。很多網站為了實作推送技術,使用Ajax輪詢,這樣在沒有新消息的情況下用戶端也要發送請求,勢必造成伺服器的負擔,而WebSokcet可以主動向用戶端推送消息,是全雙工通訊,能更好的節省伺服器資源和帶寬
特點:
- 協定辨別符為ws:比如
ws://www.baidu.com
- 無同源政策限制
- 更好的二進制支援:可以發送字元串和二進制
- 握手階段用HTTP
- 資料格式輕量:WebSocket的服務端到用戶端的資料標頭隻有2到10位元組、HTTP每次都需要攜帶完整頭部,
連接配接過程:
一:客服端請求協定更新
GET / HTTP/1.1
Host: localhost:8080
Origin: http://127.0.0.1:8080
Connection: Upgrade // 表示要更新協定
Upgrade: websocket // 表示更新的協定是websocket
Sec-WebSocket-Version: 13 // websocket版本号
Sec-WebSocket-Key: w4v7O6xFTi36lqcgctw== // 随機生成,防止非故意的錯誤,連接配接錯了
二:伺服器響應
HTTP/1.1 101 Switching Protocols
Upgrade: websocket // 表示可以更新對應的協定
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUmm5OPpG2HaGWk= // 根據用戶端key用函數計算出來
三:此後開始使用WebSocket協定
補充:
ajax輪詢:讓浏覽器間隔幾秒就發送一次請求,來擷取最新的響應
long poll:保持長連接配接來阻塞輪詢。用戶端發起請求不會立刻響應,而是有資料才傳回然後關閉連接配接,然後用戶端再次發起long poll周而複始
2. 實作
這個代碼是極簡的,适合入門了解。WebSocket是一套已經規範好的标準的API,Tomcat、Spring等都實作了這套API,下面筆者用Springboot來操作
2.1 導入依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2.2 目錄結構
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiIn5GcuMDM2EzM1gjM50iM3kzM1kzMzITMyUDMwIDMy0yN4gzNzcTMvwVNwAjMwIzLcdDO4czM3EzLcd2bsJ2Lc12bj5ycn9Gbi52YuAjMwIzZtl2Lc9CX6MHc0RHaiojIsJye.png)
2.3 ServerConfig
@Configuration // 配置類,用來注冊服務
public class serverConfig {
@Bean // 傳回的bean會自動注冊進容器
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
2.4 MyServer
重點就在這裡,先說明一下:
- Endpoint為端點,可了解為伺服器接收端,WebSocket是端對端的通信
- Session為會話,表示兩個端點間的互動,要和cookie和session這個區分開來
- 方法上的注解:@OnOpen表示成功建立連接配接後調用的方法,其餘類推
@Component // 注解雖然單例,但還是會建立多例
@ServerEndpoint(value = "/wechat/{username}") // 聲明為伺服器端點
public class MyServer {
// 成員變量
private Session session;
private String username;
// 類變量
// 類變量涉及同步問題,用線程安全類
// 可以用<String room,<String username,MyServer> >來形成房間
private static AtomicInteger onlineCount = new AtomicInteger(0);
private static ConcurrentHashMap<String, MyServer> map = new ConcurrentHashMap<>();
// 連接配接
@OnOpen
public void onOpen(@PathParam("username") String username, Session session) throws IOException {
this.session = session;
this.username = username;
map.put(username, this);
addOnlineCount();
sendMessageAll(username + "加入了房間,目前線上人數:" + getOnlineCount());
}
// 關閉
@OnClose
public void onClose() throws IOException {
subOnlineCount();
map.remove(username);
sendMessageAll(username + "退出了房間,目前線上人數:" + getOnlineCount());
}
// 發送錯誤
@OnError
public void onError(Session session, Throwable error) {
error.printStackTrace();
}
// 預設群發
@OnMessage
public void onMessage(String message) throws IOException {
sendMessageAll(username + ":" + message);
}
// 群發
private void sendMessageAll(String message) throws IOException {
for (MyServer value : map.values()) {
value.session.getBasicRemote().sendText(message); // 阻塞式
// this.session.getAsyncRemote().sendText(message); // 非阻塞式
}
}
// 私發
private void sendMessageTo(String message, String to) throws IOException {
MyServer toUser = map.get(to);
toUser.session.getAsyncRemote().sendText(message);
}
public static synchronized int getOnlineCount() {
return onlineCount.get();
}
public static synchronized void addOnlineCount() {
MyServer.onlineCount.getAndIncrement();
}
public static synchronized void subOnlineCount() {
MyServer.onlineCount.getAndDecrement();
}
}
2.5 index.html
筆者寫的前端不太靠譜,知道什麼意思即可~
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>登入頁</title>
</head>
// 輸入名字,url傳參省事
<body>
<label for="username">Username:</label>
<input id="username" type="text" placeholder="請輸入昵稱">
<button id="submit" >ENTER</button>
</body>
<script>
var submit = document.getElementById('submit');
submit.addEventListener('click',function(){
window.location.href = 'homepage.html?username=' + document.getElementById('username').value;
})
</script>
</html>
2.6 homepage.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>房間</title>
</head>
<body>
<button onclick="wsClose()">退出房間</button>
<br/><br/>
<div id="showMessage"></div>
<br/><br/>
<input id="sendMessage" type="text"/>
<button onclick="sendMessage()">發送消息</button>
</body>
<script>
// 擷取url參數的昵稱
function getQueryVariable(variable) {
var query = window.location.search.substring(1);
var vars = query.split("&");
for (var i=0;i<vars.length;i++) {
var pair = vars[i].split("=");
if(pair[0] == variable){return pair[1];}
}
return(false);
}
var conn = "ws://localhost:8080/wechat/" + getQueryVariable("username");
// webSocket連接配接
var ws = new WebSocket(conn);
// 連接配接錯誤要做什麼呢?
ws.onerror = function () {
showMessageInnerHTML("發生未知錯誤錯誤");
}
// 用戶端連接配接需要幹什麼呢?
ws.onopen = function () {
showMessageInnerHTML("--------------------------");
}
// 用戶端關閉需要幹什麼呢?
ws.onclose = function () {
showMessageInnerHTML("退出了目前房間");
}
// 收到消息
ws.onmessage = function (even) {
showMessageInnerHTML(even.data);
}
// 關閉浏覽器時
window.onbeforeunload = function () {
ws.wsClose();
}
// 網頁上顯示消息
function showMessageInnerHTML(msg) {
document.getElementById('showMessage').innerHTML += msg + '<br/>';
}
// 發送消息
function sendMessage() {
var msg = document.getElementById('sendMessage').value;
ws.send(msg);
document.getElementById('sendMessage').value = '';
}
// 關閉連接配接
function wsClose() {
ws.close();
}
</script>
</html>
2.7 截圖
不想弄前端,湊合着看吧
參考
tomcat、Spring官網均有簡介及API的詳細介紹。推薦使用後者,後者符合spring規範而且更加優雅
http://tomcat.apache.org/tomcat-9.0-doc/websocketapi/index.html
https://spring.io/guides/gs/messaging-stomp-websocket/