一、簡介
1. 什麼是websocket?
WebSocket協定是基于TCP的一種新的網絡協定。實作了浏覽器和伺服器的全雙工(full-duplex)通信。允許伺服器主動發送資訊給用戶端。
WebSocket使得用戶端和伺服器之間的資料交換變得更加簡單,允許服務端主動向用戶端推送資料。在WebSocket API中,浏覽器和伺服器隻需要完成一次握手,兩者之間就直接可以建立持久性的連接配接,并進行雙向資料傳輸。
注意:Websocket 通過HTTP/1.1 協定的101狀态碼進行握手。
2. 為什麼需要websocket?
- 全雙工 服務端可以主動向用戶端推送資訊,用戶端也可以主動向服務端發送資訊
- 與HTTP共享端口,基于HTTP完成握手
- 資料傳輸基于幀,支援發送文本類型資料和二進制資料
- 沒有同源限制,用戶端可以與任意服務端通信
- 支援協定辨別(ws或wss)與尋址,通過URL指定服務
3.适用場景有哪些?
- 社交訂閱
- 多玩家遊戲
- 協同編輯/程式設計
- 收集點選流資料
- 股票基金報價
- 體育實況更新
- 聊天室
- 基于位置的應用
- 線上教育
- 論壇的消息廣播
- 視訊彈幕
二、原理講解
首先,WebSocket 是一個持久化的協定,是相對于 HTTP 這種非持久的協定來說的。HTTP 的生命周期通過 Request 來界定,也就是一個 Request 一個 Response ,那麼在 HTTP1.0 中,這次 HTTP 請求就結束了。
在 HTTP1.1 中進行了改進,使得有一個 keep-alive,也就是說,在一個 HTTP 連接配接中,可以發送多個 Request,接收多個 Response。但是請記住 Request = Response, 在 HTTP 中永遠是這樣,也就是說一個 Request 隻能有一個 Response。而且這個 Response 也是被動的,不能主動發起。
三、前端websocket類的介紹(目前浏覽器基本上都已經内置該類)
1. websocket的屬性:
- webSocket.onopen:用于指定連接配接成功後的回調函數。
- webSocket.onmessage:用于從伺服器接收到資訊時的回調函數。
- webSocket.onerror:用于指定連接配接失敗後的回調函數。
- webSocket.onclose:用于指定連接配接關閉後的回調函數。
- webSocket.binaryType: 使用二進制的資料類型連接配接。
- webSocket.url :WebSocket 的絕對路徑(隻讀)
- webSocket.protocol:伺服器選擇的下屬協定(隻讀)
- webSocket.bufferedAmount: 未發送至伺服器的位元組數。(隻讀)
- webSocket.readyState : 執行個體對象的目前狀态, 共有 4 種(隻讀)
2. websocket 方法:
- webSocket.close([code[, reason]]) :關閉目前連結,
- code: 可選,一個數字狀态碼,它解釋了連接配接關閉的原因。如果沒有傳這個參數,預設使用 1005,抛出異常:INVALID_ACCESS_ERR,無效的 code.
- reason 可選,一個人類可讀的字元串,它解釋了連接配接關閉的原因。這個 UTF-8 編碼的字元串不能超過 123 個位元組,抛出異常:SYNTAX_ERR,超出 123個位元組。
- webSocket.send(data) :發送資料到伺服器。
3. websocket 事件:
- open:連接配接成功時觸發。 也可以通過 onopen 屬性來設定。
- message:收到資料時觸發。 也可以通過 onmessage 屬性來設定。
- error:連接配接因錯誤而關閉時觸發,例如無法發送資料時。 也可以通過 onerror 屬性來設定。
- close:連接配接被關閉時觸發。 也可以通過 onclose 屬性來設定。
四、基礎部分
本項目基于SpringBoot架構實作,模闆引擎采用freemarker
1.maven依賴
<!-- webSocket架構 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2.服務端的代碼
2.1 WebSocketConfig類
該配置類,隻有在SpringBoot環境下需要配置。将該類放在config包下。
package org.example.ssm.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* websocket配置類
* 配置類,讓用戶端能連接配接上,隻是springboot環境下才需要
*/
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
2.2 MyWebSocket類
該類是websocket服務端的核心類,主要是對websocket的onopen,onmessage,onclose,onerror事件進行重寫處理。建議放在ws包下。
package com.chinasoft.websocket.ws;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.util.concurrent.CopyOnWriteArrayList;
@ServerEndpoint("/websocket")
@Component
public class MyWebSocket {
//記錄目前線上連接配接數
public static int onlineCount=0;
// concurrent包的線程安全Set,用來存放每個用戶端對應的MyWebSocket對象。
public static CopyOnWriteArrayList<MyWebSocket> user=new CopyOnWriteArrayList<MyWebSocket>();
// 與某個用戶端的連接配接會話,需要通過它來給用戶端發送資料,這是websocket專屬的session不是HttpSession
public Session session;
//打開連接配接
@OnOpen
public void onOpen(Session session) throws Exception{
this.session=session;
user.add(this);
String id=session.getId();
System.out.println("使用者名:"+id+"打開了連接配接~~~~");
session.getBasicRemote().sendText("data:"+id+"使用者連接配接");
}
//關閉連接配接
@OnClose
public void onClose(Session session) throws Exception{
user.remove(this);
System.out.println("使用者名:"+session.getId()+"關閉了連接配接~~~~");
}
//收到用戶端消息後調用的方法(接受方法)
@OnMessage
public void onMessage(String message,Session session) throws Exception{
System.out.println("sessionMessage:"+session);
System.out.println("接受前端發送回來的消息"+message);
for(int i=0;i<user.size();i++){
MyWebSocket d=user.get(i);
d.session.getBasicRemote().sendText(session.getId());
}
System.out.println("我群發了消息:hello");
for(int i=0;i<user.size();i++){
MyWebSocket d=user.get(i);
d.session.getBasicRemote().sendText("datas:"+session.getId()+":"+message);
}
}
@OnError
public void onError(Session session,Throwable error){
System.out.println("發生error");
error.printStackTrace();
}
//給用戶端傳遞消息
public void sendMessage(String message){
try{
this.session.getBasicRemote().sendText("message");
}catch (Exception e){
e.printStackTrace();
}
}
}
2.3 WebSocketController類
該類主要是處理index相關的請求
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class WebSocketController {
@GetMapping("index")
public String index(){
return "index";
}
}
3.前端代碼
index.ftl
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script>
let ws;//建立webSocker執行個體
let lockReconnect=false;//避免重複連接配接
let wsUrl="ws://localhost:8888/ssm/websocket";
function getConn(){
createWebSocket(wsUrl);
}
//建立WebSocket對象
function createWebSocket(url){
//給個辨別
console.log('建立WebSocket對象');
try{
ws=new WebSocket(url);
initEventHandle();//初始化事件的方法
}catch(e){
console.log('建立WebSocket對象出錯了');
reconnect(url);
console.log('WebSocket重新連接配接');
//給個錯誤辨別
//然後重連
}
}
//關閉連接配接
function closeWebSocket(){
//給個辨別關閉連接配接
console.log('關閉WebSocket對象出錯了');
ws.close();
}
//初始化WebSocket的事件
function initEventHandle(){
ws.onclose=function(){
console.log("連接配接關閉--重連");
reconnect(wsUrl);
}
ws.onerror=function(){
console.log("連接配接異常--重連");
reconnect(wsUrl);
}
ws.onopen=function(){
console.log("開啟連接配接");
heartCheck.reset().start();
}
ws.onmessage=function(event){
console.log("事件:"+event.data);
document.getElementById("msg").innerHTML = event.data;
heartCheck.reset().start();
}
}
function reconnect(url){
console.log('正在重連');
if(lockReconnect)return;
lockReconnect=true;
//沒連接配接上會一直重連,設定延遲避免請求過多
setTimeout(function(){
createWebSocket(url);
lockReconnect=false;
},2000);
}
let heartCheck={
timeout: 6000,//6秒
timeoutObj: null,
reset: function(){
console.log('接收到消息,檢測正常');
clearTimeout(this.timeoutObj);
return this;
},
start: function(){
this.timeoutObj=setTimeout(function(){
// ws.send('HeartBeat');
// console.log('發送一個心跳');
},this.timeout);
}
}
function sendInfo(){
ws.send(document.getElementById("message").value.trim());
}
</script>
</head>
<body>
<p>第一步:取得連接配接:<input type="button" value="點選我建立連接配接" onclick="getConn();"></p>
<p>第二布:輸入内容:<input type="text" id="message"><input type="button" value="群發" onclick="sendInfo();"></p>
<p id="msg"></p>
</body>
</html>
五、進階部分
視訊彈幕項目的簡單實作
1.maven依賴
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.chinasoft</groupId>
<artifactId>websocket</artifactId>
<version>1.0-SNAPSHOT</version>
<!--所有的springboot項目都需要繼承父類-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.6.RELEASE</version>
</parent>
<dependencies>
<!--web項目的啟動器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--freemarker前端頁面-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<!--引入WebSocket pom依賴-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
</dependencies>
</project>
2.服務端的代碼
2.1 WebSocketConfig類
該配置類,隻有在SpringBoot環境下需要配置。将該類放在config包下。
package org.example.ssm.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* websocket配置類
* 配置類,讓用戶端能連接配接上,隻是springboot環境下才需要
*/
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
2.2 WebMvcConfig類
該類是SpringMVC的配置類,主要是對資源檔案進行配置。采用外部資源導入項目中,外部資源存放位置根據個人情況修改。将該類放在config包下
package org.example.ssm.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
/**
* 外部資源配置類
*/
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/css/**").addResourceLocations("file:E:/static/css/");
registry.addResourceHandler("/js/**").addResourceLocations("file:E:/static/js/");
}
}
2.3 WebSocketController類
該類主要是處理index相關的請求
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class WebSocketController {
@GetMapping("index")
public String index(){
return "index";
}
}
2.4 ChatWebSocket類
該類是websocket服務端的核心類,主要是對websocket的onopen,onmessage,onclose,onerror事件進行重寫處理。建議放在ws包下
import com.alibaba.fastjson.JSONObject;
import org.example.ssm.entity.VO.MessageVO;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* 這是伺服器端的websocket,接收來自用戶端的websocket請求
* 相當于一個controller
* 聊天室的實作
* 單人聊天業務實作:
* 1.擷取到加入聊天的使用者資訊
* 2.會話内容采用浏覽器本地存儲,存在sessionStrage裡面
*/
@ServerEndpoint("/chat")
@Component
public class ChatWebSocket {
// 靜态變量,用來記錄目前線上連接配接數。應該把它設計成線程安全的。
private static int onlineCount = 0;
// concurrent包的線程安全Set,用來存放每個用戶端對應的MyWebSocket對象。
private static CopyOnWriteArraySet<ChatWebSocket> webSocketSet = new CopyOnWriteArraySet<ChatWebSocket>();
// 與某個用戶端的連接配接會話,需要通過它來給用戶端發送資料,這是websocket專屬的session不是HttpSession
private Session session;
/**
* 連接配接建立成功調用的方法
*/
@OnOpen
public void onOpen(Session session) {
this.session = session;
webSocketSet.add(this); //加入set中
addOnlineCount(); //線上數加1
System.out.println("有新連接配接加入!目前線上人數為" + getOnlineCount());
try {
// 連接配接預處理
} catch (Exception e) {
e.getCause();
}
}
/**
* 連接配接關閉調用的方法
*/
@OnClose
public void onClose() {
webSocketSet.remove(this); //從set中删除
subOnlineCount(); //線上數減1
System.out.println("有一連接配接關閉!目前線上人數為" + getOnlineCount());
}
/**
* 收到用戶端消息後調用的方法
* @param message 用戶端發送過來的消息
*/
@OnMessage
public void onMessage(String message,Session session) {
JSONObject jsonObject = JSONObject.parseObject(message);
// 群發消息
for (ChatWebSocket item : webSocketSet) {
try {
item.sendMessage(jsonObject.toJSONString());
} catch (IOException e) {
e.printStackTrace();
}
}
}
@OnError
public void onError(Throwable error) {
System.out.println("發生錯誤");
error.printStackTrace();
}
public void sendMessage(String message) throws IOException {
this.session.getBasicRemote().sendText(message);
}
public static void sendInfo(String message){
for (ChatWebSocket item : webSocketSet) {
try {
item.sendMessage(message);
} catch (IOException e) {
continue;
}
}
}
public static synchronized int getOnlineCount() {
return onlineCount;
}
public static synchronized void addOnlineCount() {
ChatWebSocket.onlineCount++;
}
public static synchronized void subOnlineCount() {
ChatWebSocket.onlineCount--;
}
}
3.前端代碼
3.1 index.ftl
視訊的路徑自行選擇本地視訊
注意:需要自行下載下傳bootstrap4前端架構。官網:Bootstrap中文網 (bootcss.com)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>視訊彈幕</title>
<link href="css/bootstrap.css" rel="stylesheet">
<script src="js/jquery-3.6.0.min.js"></script>
<style>
.bullet {
width: 800px;
height: 550px;
padding: 15px;
margin: 50px auto;
border-radius: 10px;
box-shadow: 3px 4px 5px 6px grey;
}
.input-group{
margin-top: 10px;
}
</style>
</head>
<body>
<div class="container">
<div class="row">
<div class="col">
<#-- 視訊彈幕,固定擷取視訊 -->
<div class="bullet">
<div class="video">
<video src="../video/test.mp4" width="100%" height="90%" controls></video>
</div>
<div class="input-group">
<input type="text" class="form-control" placeholder="你也來發個彈幕呗!!!" id="canve">
<div class="input-group-append">
<button class="btn btn-outline-success" type="button" onclick="sendBullet($('#canve').val());">點選我發送</button>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="js/websocket.js"></script>
<script>
var wsUrl = "ws://localhost:8888/ssm/bullet"
var ws = createConnect(wsUrl)
// 開始連接配接,擷取已有的彈幕,進行播放
ws.onopen = function () {
console.log("擷取已有的彈幕")
}
// 接收消息事件
ws.onmessage = function (e) {
var str = JSON.parse(e.data)
console.log(str)
createEle(str)
}
// 發送彈幕
function sendBullet(message) {
if (message == null || message == "" || message == undefined) {
alert("請輸入内容" + message)
} else {
ws.send(JSON.stringify(message))
}
}
//1.擷取元素
//擷取.box元素
var bullet = document.querySelector(".bullet")
//擷取box的寬度
var width = bullet.offsetWidth
//擷取box的高度
var heigth = bullet.offsetHeight * 0.8
// 建立彈幕元素
function createEle(txt) {
//動态生成span标簽
//建立标簽
var span = document.createElement('span')
//接收參數txt并且生成替換内容
span.innerHTML = txt
//初始化生成位置x
//動态生成span标簽
span.style.left = width + 'px'
span.style.position = "absolute"
//把标簽塞到oBox裡面
bullet.appendChild(span)
roll.call(span, {
// call改變函數内部this的指向
timing: ['linear'][~~(Math.random() * 2)],
color: '#' + (~~(Math.random() * (1 << 24))).toString(16),
top: random(10, heigth),
fontSize: random(10, 24)
})
}
function roll(opt) {
//彈幕滾動
//如果對象中不存在timing 初始化
opt.timing = opt.timing || 'linear'
opt.color = opt.color || '#fff'
opt.top = opt.top || 0
opt.fontSize = opt.fontSize || 16
this._left = parseInt(this.offsetLeft) //擷取目前left的值
this.style.color = opt.color //初始化顔色
this.style.top = opt.top + 'px'
this.style.fontSize = opt.fontSize + 'px'
this.timer = setInterval(function () {
if (this._left <= 100) {
clearInterval(this.timer) //終止定時器
this.parentNode.removeChild(this)
return //終止函數
}
switch (opt.timing) {
case 'linear': //如果勻速
this._left += -2
break
// case 'ease-out': //
// this._left += (0 - this._left) * .01;
// break;
}
this.style.left = this._left + 'px'
}.bind(this), 1000 / 60)
}
function random(start, end) {
//随機數封裝
return start + ~~(Math.random() * (end - start))
}
</script>
</body>
</html>
3.2 websocket.js封裝代碼
// 定義websocket心跳,避免長時間連接配接加重伺服器的負擔
var heartCheck = {
// 心跳間隔1分鐘
timeout: 60 * 1000,
// 心跳對象
timeoutObj: null,
reset: function () {
console.log('接收到消息,檢測正常')
clearTimeout(this.timeoutObj)
return this
},
start: function () {
console.log("開始心跳")
this.timeoutObj = setTimeout(function () {
}, this.timeout)
}
}
// 建立websocket連接配接
function createConnect(wsUrl) {
// 判斷浏覽器是否支援websocket協定
if (typeof WebSocket != 'undefined') {
console.log("您的浏覽器支援Websocket通信協定")
// 建立websocket的執行個體,建立該執行個體後會立即執行websocket請求
var webSocket = new WebSocket(wsUrl)
// 初始化websocket事件
initEvent(webSocket)
return webSocket
} else {
alert("您的浏覽器不支援Websocket通信協定,請使用Chrome或者Firefox浏覽器!")
return null
}
}
// 初始化websocket事件
function initEvent(ws) {
// 開始連接配接事件
ws.onopen = function () {
console.log("建立websocket連接配接")
// 開始心跳,判斷使用者是否線上
heartCheck.reset().start()
}
// 接收來自伺服器的消息,傳入event參數。
// 擷取websocket響應消息,此處業務邏輯需要根據實際情況進行書寫
ws.onmessage = function (event) {
console.log("收到來自伺服器的消息:" + event.data)
// 重置心跳并開始心跳
heartCheck.reset().start()
}
// 斷開websocket連接配接前的操作
ws.onclose = function () {
console.log("websocket連接配接已斷開")
}
// websocket連接配接錯誤事件
ws.onerror = function () {
console.log("websocket連接配接異常")
}
}
好了,關于websocket技術的講解就到這裡了。
感謝您!花費這麼多寶貴的時間觀看!