目录
[1. 原生注解](#1. 原生注解)
[2. Spring封装](#2. Spring封装)
一、什么是Websocket
WebSocket 是一种在单个 TCP 连接上进行 全双工 通信的协议,它可以让客户端和服务器之间进行实时的双向通信。WebSocket 使用一个长连接,在客户端和服务器之间保持持久的连接,从而可以实时地发送和接收数据。
在 WebSocket 中,客户端和服务器之间可以互相发送消息,客户端可以使用 JavaScript 中的 WebSocket API 发送消息到服务器,也可以接收服务器发送的消息。
二、Websocket特点
简单来说,websocket 具有双向通信,实时性强,支持二进制,控制开销的特点。
- 协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。
- 实时通信,服务器可以随时主动给客户端下发数据。
- 保持连接状态,Websocket需要先创建连接,所以是一种有状态的协议,之后通信时就可以省略部分状态信息。
- 控制开销,连接创建后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对较小。在不包含扩展的情况下,对于服务器到客户端的内容,此头部大小只有2至10字节(和数据包长度有关);对于客户端到服务器的内容,头部还需要加上额外的4字节的掩码。
- 实现简单,建立在 TCP 协议之上,服务器端的实现比较容易,并且没有同源限制,客户端可以与任意服务器通信。
- 支持二进制传输,Websocket定义了二进制帧,可以发送文本,也可以发送二进制数据。
- 与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
- 支持扩展,用户可以扩展协议、实现部分自定义的子协议,如部分浏览器支持压缩等。
三、WebSocket与HTTP的区别
websocket和http都是基于TCP的应用层协议,使用的也是 80 端口(若运行在 TLS 之上时,默认使用 443 端口)。
其区别主要就在于连接的性质和通信方式。
WebSocket是一种双向通信的协议,通过一次握手即可建立持久性的连接,服务器和客户端可以随时发送和接收数据。而HTTP协议是一种请求-响应模式的协议,每次通信都需要发送一条请求并等待服务器的响应。
WebSocket的实时性更好,延迟更低,并且在服务器和客户端之间提供双向的即时通信能力,适用于需要实时数据传输的场景。
四、常见应用场景
- **实时聊天:**WebSocket能够提供双向、实时的通信机制,使得实时聊天应用能够快速、高效地发送和接收消息,实现即时通信。
- **实时协作:**用于实时协作工具,如协同编辑文档、白板绘画、团队任务管理等,团队成员可以实时地在同一页面上进行互动和实时更新。
- **实时数据推送:**用于实时数据推送场景,如股票行情、新闻快讯、实时天气信息等,服务器可以实时将数据推送给客户端,确保数据的及时性和准确性。
- **多人在线游戏:**实时的双向通信机制,适用于多人在线游戏应用,使得游戏服务器能够实时地将游戏状态和玩家行为传输给客户端,实现游戏的实时互动。
- **在线客服:**WebSocket可以用于在线客服和客户支持系统,实现实时的客户沟通和问题解决,提供更好的用户体验,减少等待时间。
五、SpringBoot集成WebSocket
引入依赖
XML
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
1. 原生注解
WebSocketConfig
java
package com.cjian.websocket.annotation;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
//开启WebSocket的支持,并把该类注入到spring容器中
@Configuration
@EnableWebSocket
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
说明:
这个配置类很简单,通过这个配置 spring boot 才能去扫描后面的关于 websocket 的注解
WsServerEndpoint
java
package com.cjian.websocket.annotation;
import cn.hutool.json.JSONUtil;
import jakarta.websocket.OnClose;
import jakarta.websocket.OnMessage;
import jakarta.websocket.OnOpen;
import jakarta.websocket.Session;
import jakarta.websocket.server.PathParam;
import jakarta.websocket.server.ServerEndpoint;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
@ServerEndpoint(value = "/websocket/{sessionId}")
@Component
public class WsServerEndpoint {
private static ConcurrentHashMap<String, WsServerEndpoint> webSocketMap = new ConcurrentHashMap<>();
//实例一个session,这个session是websocket的session
private Session session;
//新增一个方法用于主动向客户端发送消息
public static void sendMessage(String message, String sessionId) {
WsServerEndpoint webSocket = webSocketMap.get(sessionId);
if (webSocket != null) {
try {
webSocket.session.getBasicRemote().sendText(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 连接成功
*
* @param session
*/
@OnOpen
public void onOpen(Session session, @PathParam("sessionId") String sessionId) {
this.session = session;
webSocketMap.put(sessionId, this);
sendMessage("connect success", sessionId);
}
/**
* 连接关闭
*
* @param session
*/
@OnClose
public void onClose(Session session, @PathParam("sessionId") String sessionId) throws IOException {
webSocketMap.remove(sessionId);
session.close();
}
/**
* 接收到消息
*
* @param text
*/
@OnMessage
public void onMsg(String text, @PathParam("sessionId") String sessionId) {
sendMessage("receive msg from client:" + text, sessionId);
}
}
说明
这里有几个注解需要注意一下,首先是他们的包都在 **jakarta.websocket **下(我用的jdk22)。并不是 spring 提供的,而 jdk 自带的,下面是他们的具体作用。
- **@ServerEndpoint:**通过这个 spring boot 就可以知道你暴露出去的 ws 应用的路径,有点类似我们经常用的@RequestMapping。比如你的启动端口是8080,而这个注解的值是ws,那我们就可以通过 ws://127.0.0.1:8080/ws 来连接你的应用
- @OnOpen:当 websocket 建立连接成功后会触发这个注解修饰的方法,注意它有一个 Session 参数
- @OnClose:当 websocket 建立的连接断开后会触发这个注解修饰的方法,注意它有一个 Session 参数
- @OnMessage:当客户端发送消息到服务端时,会触发这个注解修改的方法,它有一个 String 入参表明客户端传入的值
- @OnError:当 websocket 建立连接时出现异常会触发这个注解修饰的方法,注意它有一个 Session 参数
另外一点就是服务端如何发送消息给客户端,服务端发送消息必须通过上面说的 Session 类,通常是在@OnOpen 方法中,当连接成功后把 session 存入 Map 的 value,key 是与 session 对应的用户标识,当要发送的时候通过 key 获得 session 再发送。
使用postman测试:
2. Spring封装
java
package com.cjian.websocket.spring;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
@Component
public class CustomWebsocketHandler extends TextWebSocketHandler {
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
String name = (String) session.getAttributes().get("name");
session.sendMessage(new TextMessage(name + " connection success"));
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
session.sendMessage(new TextMessage("receive msg:" + message.getPayload()));
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
session.close();
}
}
说明
通过继承 TextWebSocketHandler 类并覆盖相应方法,可以对 websocket 的事件进行处理,这里可以同原生注解的那几个注解连起来看
- afterConnectionEstablished 方法是在 socket 连接成功后被触发,同原生注解里的 @OnOpen 功能
- afterConnectionClosed 方法是在 socket 连接关闭后被触发,同原生注解里的 @OnClose 功能
- handleTextMessage 方法是在客户端发送信息时触发,同原生注解里的 @OnMessage 功能
CustomInterceptor
java
package com.cjian.websocket.spring;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;
import java.util.Map;
@Component
public class CustomInterceptor extends HttpSessionHandshakeInterceptor {
/**
* 握手前
*/
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
System.out.println("start hand shake");
ServletServerHttpRequest httpRequest = (ServletServerHttpRequest) request;
String name = httpRequest.getServletRequest().getParameter("name");
attributes.put("name", name);
return super.beforeHandshake(request, response, wsHandler, attributes);
}
/**
* 握手后
*/
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
System.out.println("hand shake end");
}
}
说明
通过实现 HandshakeInterceptor 接口来定义握手拦截器,注意这里与上面 Handler 的事件是不同的,这里是建立握手时的事件,分为握手前与握手后,而 Handler 的事件是在握手成功后的基础上建立 socket 的连接。所以在如果把认证放在这个步骤相对来说最节省服务器资源。它主要有两个方法 beforeHandshake 与 **afterHandshake **,顾名思义一个在握手前触发,一个在握手后触发。
CustomWebSocketConfig
java
package com.cjian.websocket.spring;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
@Configuration
@EnableWebSocket
public class CustomWebSocketConfig implements WebSocketConfigurer {
@Autowired
private CustomWebsocketHandler customWebsocketHandler;
@Autowired
private CustomInterceptor myInterceptor;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry
.addHandler(customWebsocketHandler, "myWS")
.addInterceptors(myInterceptor)
.setAllowedOrigins("*");
}
}
说明
通过实现 WebSocketConfigurer 类并覆盖相应的方法进行 websocket 的配置。我们主要覆盖 registerWebSocketHandlers 这个方法。通过向 WebSocketHandlerRegistry 设置不同参数来进行配置。其中 **addHandler 方法添加我们上面的写的 ws 的 handler 处理类,第二个参数是你暴露出的 ws 路径。 addInterceptors **添加我们写的握手过滤器。**setAllowedOrigins("*") **这个是关闭跨域校验,方便本地调试,线上推荐打开。
测试: