文章目录
WebSocket概念
应用层协议,底层采用TCP,特点:持续连接,有状态,双向通信
当客户端想要与服务器建立WebSocket连接时,它会首先发送一个特殊的HTTP请求(WebSocket握手请求)给服务器,这个请求包含了升级到WebSocket协议的愿望。如果服务器同意,则会返回一个HTTP 101状态码表示切换协议,并完成握手过程。之后,原本的HTTP连接就变成了WebSocket连接
HTTP请求和响应示例
http
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Protocol: chat
Sec-WebSocket-Version: 13
Origin: http://example.com
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
Sec-WebSocket-Extensions: permessage-deflate; server_max_window_bits=15
Sec-WebSocket-Protocol (可选): 允许客户端指定子协议列表,以便服务器选择支持的协议
Sec-WebSocket-Extensions (可选): 如果有扩展机制被启用,则可以通过此字段告知服务器客户端支持的扩展类型。例如压缩算法等
在实际的应用场景中,除了上述的基本头部信息之外,还可以根据具体需求添加其他自定义头部信息,如认证令牌、用户身份验证信息等
WebSocket连接url示例
测试连接可用wscat命令行工具
ws://echo.websocket.org/ 端口同http 80
wss://api.example.com/socketserver 端口同https 443
SpringBoot实现一个WebSocket示例
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
java
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
WebSocket中的概念端点
涉及注解:
@ServerEndpoint
@OnOpen
@OnClose
@OnMessage
@OnError
java
@ServerEndpoint("/websocket/{userId}")
@Component
public class ChatWebSocketServer {
// 静态变量用于记录当前在线连接数,设计成线程安全的方式。
private static int onlineCount = 0;
// 存放每个客户端对应的ChatWebSocketServer对象,保证线程安全。
private static CopyOnWriteArraySet<ChatWebSocketServer> webSocketSet = new CopyOnWriteArraySet<>();
// 与某个客户端的连接会话,需要通过它给客户端发送数据。
private Session session;
// 用户ID,从路径参数获取
private String userId;
@OnOpen
public void onOpen(@PathParam("userId") String userId, Session session) {
this.session = session;
this.userId = userId;
webSocketSet.add(this); // 加入set中
addOnlineCount(); // 在线数加1
System.out.println("有新连接加入!当前在线人数为" + getOnlineCount());
try {
sendMessage("欢迎连接:" + userId);
} catch (IOException e) {
System.out.println("IO异常");
}
}
@OnClose
public void onClose() {
webSocketSet.remove(this); // 从set中删除
subOnlineCount(); // 在线数减1
System.out.println("有一连接关闭!当前在线人数为" + getOnlineCount());
}
@OnMessage
public void onMessage(String message, Session session) {
System.out.println("来自客户端的消息:" + message);
// 群发消息
for (ChatWebSocketServer item : webSocketSet) {
try {
item.sendMessage(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}
@OnError
public void onError(Session session, Throwable error) {
System.out.println("发生错误");
error.printStackTrace();
}
public void sendMessage(String message) throws IOException {
this.session.getBasicRemote().sendText(message);
}
public static synchronized int getOnlineCount() {
return onlineCount;
}
public static synchronized void addOnlineCount() {
MyWebSocketServer.onlineCount++;
}
public static synchronized void subOnlineCount() {
MyWebSocketServer.onlineCount--;
}
}
STOMP消息订阅和发布
Spring WebSocket 原生包含对 STOMP 消息传递的支持
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
对于WebSocket,URL应以ws://开始;对于SockJS,它会自动处理不同类型的传输
1.STOMP配置
在这里也可以不用STOMP ,也可以配置RabbitMQ等消息队列
java
package com.example.demo.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
// 启用简单的内存消息代理,用于广播消息到所有订阅者
config.enableSimpleBroker("/topic", "/queue");
// 设置应用程序全局目标前缀,确保所有目的地以"/app"开头的消息都会被路由到带有@MessageMapping注解的方法中。---就是个路由转发
config.setApplicationDestinationPrefixes("/app", "/chat");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// 注册STOMP端点,允许使用SockJS作为回退机制
registry.addEndpoint("/ws")
.setAllowedOriginPatterns("*") // 明确列出允许的源
.withSockJS();
}
}
2.创建消息控制器
java
package com.example.demo.controller;
import com.example.demo.message.Greeting;
import com.example.demo.message.HelloMessage;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;
import org.springframework.web.util.HtmlUtils;
@Controller
public class GreetingController {
@Autowired
private SimpMessagingTemplate simpMessagingTemplate;
@MessageMapping("/hello")
@SendTo("/topic/greetings")
public Greeting greeting(HelloMessage message) throws Exception {
Thread.sleep(1000); // simulated delay
return new Greeting("Hello, " + HtmlUtils.htmlEscape(message.getName()) + "!");
}
}
@MessageMapping
@SendTo
@EnableWebSocketMessageBroker 启动WebSocket消息代理Broker
/topic
前缀通常用于广播消息,意味着发送到此类目的地的消息会被分发给所有订阅了相同主题的客户端;而 /queue
前缀则常用来表示点对点的消息传递,即消息只会被发送给一个特定的订阅者,即使有多个订阅者存在也是如此
简单点说STOMP配置中 config.enableSimpleBroker("/topic", "/queue"); 开启了 /topic ,/queue为前缀的小型内存消息代理服务, 前端可以用SockJS订阅该路径下主题,当触发对应WebSocket 控制器路径方法时会发消息到指定目的地主题,广播给所有订阅者或者单个订阅者
后端主动发送消息
java
@Autowired
private SimpMessagingTemplate messagingTemplate;
@Autowired
private SimpUserRegistry userRegistry;
public void notifyOnlineUsersAboutEvent(String eventName) {
// 获取所有在线用户的ID
Set<String> onlineUserIds = userRegistry.getUsers().stream()
.map(user -> user.getName())
.collect(Collectors.toSet());
// 遍历所有在线用户并向他们发送通知
onlineUserIds.forEach(userId ->
messagingTemplate.convertAndSendToUser(userId, "/queue/events", eventName)
);
}
跨域
全局配置跨域
java
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*") // 注意这里使用了allowedOriginPatterns而不是allowedOrigins
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.exposedHeaders("Authorization", "Link")
.allowCredentials(true)
.maxAge(3600);
}
}
注意:在设置了 allowCredentials(true)
的情况下,不能同时设置 allowedOrigins("*")
,而应该使用 allowedOriginPatterns("*")
或者明确列出允许的源地址。这是因为浏览器的安全机制不允许携带凭证的同时接受来自任意来源的请求
针对WebSocket端点跨域配置
java
package com.example.demo.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
// 启用简单的内存消息代理,用于广播消息到所有订阅者
config.enableSimpleBroker("/topic", "/queue");
// 设置应用程序目的地前缀,以便区分来自应用的消息和其他类型的消息
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// 注册STOMP端点,允许使用SockJS作为回退机制
registry.addEndpoint("ws")
.setAllowedOriginPatterns("*") // 明确列出允许的源
.withSockJS();
}
}