一、WebSocket是什么?
WebSocket是在单个TCP连接上进行全双工通信的协议,可以在服务器和客户端之间建立双向通信通道。
WebSocket 首先与服务器建立常规 HTTP 连接,然后通过发送Upgrade标头将其升级为双向 WebSocket 连接。
WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
二、WebSocket的优点
1.较少的控制开销。在连接创建后,服务器和客户端之间交换数据时,用于控制协议的数据包头部较小。在不包含扩展的情况下,对于服务器到客户端的内容,此头部大小只有2至10字节(和数据包长度有关);对于客户端到服务器的内容,此头部还需要加上额外的4字节的掩码。相对于HTTP请求每次都要携带完整的头部,此项开销显著减少了。
2.更强的实时性。由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。相对于HTTP请求需要等待客户端发起请求服务端才能响应,延迟明显更少;即使是和Comet等类似的长轮询比较,其也能在短时间内更多次地传递数据。
3.保持连接状态。与HTTP不同的是,WebSocket需要先创建连接,这就使得其成为一种有状态的协议,之后通信时可以省略部分状态信息,而HTTP请求可能需要在每个请求都携带状态信息,比如身份认证等。
4.更好的二进制支持。Websocket定义了二进制帧,相对HTTP,可以更轻松地处理二进制内容。
5.可以支持扩展。Websocket定义了扩展,用户可以扩展协议、实现部分自定义的子协议。如部分浏览器支持压缩等。
6.更好的压缩效果。相对于HTTP压缩,Websocket在适当的扩展支持下,可以沿用之前内容的上下文,在传递类似的数据时,可以显著地提高压缩率。
三、项目实战
1.引入依赖
<!--websocket-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
完整的pom文件如下所示:
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.14</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>WebSocketChatDemo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>WebSocketChatDemo</name>
<description>WebSocketChatDemo</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--websocket-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!--lombok插件依赖-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2.创建SystemWebSocketHandler类
package com.example.websocketchatdemo.websocket;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* @author qx
* @date 2023/8/22
* @des WebSocket处理类
*/
@Slf4j
@Component
public class SystemWebSocketHandler implements WebSocketHandler {
// 存储所有客户端会话
private static final List<WebSocketSession> sessionList = new ArrayList<>();
/**
* 与服务器连接成功
*/
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
log.info("客户端成功建立连接:{}", session.getId());
sessionList.add(session);
}
/**
* 接受客户端的消息
*/
@Override
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
// 获取客户端的消息
String msg = message.getPayload().toString();
log.info("接收到客户端的消息:{}", msg);
sendMsg(msg);
}
/**
* 给所有客户端发送消息
*
* @param msg 消息内容
* @throws IOException
*/
private void sendMsg(String msg) throws IOException {
for (WebSocketSession session : sessionList) {
if (session.isOpen()) {
session.sendMessage(new TextMessage(msg));
}
}
}
/**
* 通讯异常
*/
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
log.error("通讯出现异常");
}
/**
* 连接关闭
*/
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
log.info("连接关闭");
}
/**
* 是否允许分段发送
*/
@Override
public boolean supportsPartialMessages() {
// 一次性发送消息
return false;
}
}
3.创建WebSocket配置类
这个配置类主要进行跨域的配置。
package com.example.websocketchatdemo.websocket;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
/**
* @author qx
* @date 2023/8/22
* @des WebSocket配置类
*/
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebMvcConfigurer, WebSocketConfigurer {
@Autowired
private SystemWebSocketHandler handler;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
// 设置WebSocket服务器地址 ws://localhost:8080/SpringBootWebSocket
registry.addHandler(handler, "/SpringBootWebSocket").setAllowedOrigins("*");
}
}
4.前台页面编写
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>聊天室</title>
</head>
<body>
聊天消息内容:
<br/>
<textarea id="chat_content" readonly style="height: 400px;width: 600px"></textarea>
<br/>输入框:
<br/>
<div>
<textarea id="in_content" placeholder="请输入内容" style="height: 100px;width: 500px"></textarea>
</div>
<button type="button" id="btn_send">发送消息</button>
<br/><br/>
<label>用户:</label>
<div>
<input type="text" id="in_name" placeholder="请输入姓名"/>
</div>
<br/>
<button type="button" id="btn_join">进入聊天室</button>
<button type="button" id="btn_quit">离开聊天室</button>
<script src="http://apps.bdimg.com/libs/jquery/2.1.1/jquery.min.js"></script>
<script>
$(function () {
var socketUrl = "ws://localhost:8080/SpringBootWebSocket";
var ws = null;
$("#btn_join").click(function () {
if (ws != null) {
alert("用户[" + $("#in_name").val() + "]已加入连接");
return;
}
// 判断当前浏览器是否支持WebSocket
if ('WebSocket' in window) {
ws = new WebSocket(socketUrl)
} else {
alert("当前浏览器不支持WebSocket");
}
// 建立连接
ws.onopen = function (event) {
console.log('与服务器建立连接');
ws.send("您的好友[" + $("#in_name").val() + "]上线了");
}
// 接收服务端返回给前端的消息
ws.onmessage = function (event) {
$("#chat_content").append(event.data + "\n");
}
// 连接关闭
ws.onclose = function () {
console.log("与服务器断开连接")
$("#chat_content").append("用户[" + $("#in_name").val() + "]离开聊天室" + "\n");
$("#in_name").val("");
}
});
//发送消息
$("#btn_send").click(function () {
if (ws == null) {
alert("该用户不在线");
return;
}
var msg = $("#in_content").val();
ws.send("用户[" + $("#in_name").val() + "]:" + msg);
})
// 离开聊天室
$("#btn_quit").click(function () {
ws.send("用户[" + $("#in_name").val() + "]离开聊天室!");
$("#in_content").val("");
ws.close();
})
})
</script>
</body>
</html>
5.控制器编写
主要编写一个跳转到聊天室的请求
package com.example.websocketchatdemo.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* @author qx
* @date 2023/8/22
* @des 测试
*/
@Controller
@RequestMapping("/chat")
public class IndexController {
/**
* 跳转到聊天室
*/
@GetMapping("/index")
public String toChat() {
return "index";
}
}
6.测试
我们启动项目,打开两个聊天页面。
方便设置两个用户加入聊天室
然后在一个用户中发送消息,我们可以看到两个聊天窗口的消息同步了。
当一个用户退出聊天室时,会提示用户退出聊天室。
这个时候另一个用户发送消息只能自己看到了。
如果想测试多个用户,再从新打开一个页面,进入聊天室就可以了。