WebSocket技术讲解
一、简介
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连接异常")
}
}