Java-WebSocket

文章目录

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();
    }
}
相关推荐
好好学习++9 分钟前
【HF设计模式】03-装饰者模式
java·c++·设计模式·装饰器模式
JWASX15 分钟前
定时/延时任务-Kafka时间轮源码分析
java·kafka·定时任务·时间轮
小林熬夜学编程19 分钟前
【Linux网络编程】第十弹---打造初级网络计算器:从协议设计到服务实现
linux·服务器·c语言·开发语言·前端·网络·c++
ThetaarSofVenice20 分钟前
【Java从入门到放弃 之 ArrayList】
java·开发语言
tester Jeffky21 分钟前
JavaScript 原型对象与原型链的魔法与艺术
开发语言·javascript·原型模式
hjxxlsx22 分钟前
Python 开源项目精彩荟萃
开发语言·python·开源
waves浪游25 分钟前
C/C++内存管理
c语言·开发语言·c++
yzhSWJ33 分钟前
处理idea+tomcat的中文乱码
java·tomcat·中文乱码·idea
IT猿手37 分钟前
基于RRT(Rapidly-exploring Random Tree)的无人机三维路径规划,MATLAB代码
开发语言·人工智能·深度学习·matlab·机器人·无人机·智能优化算法
nbsaas-boot42 分钟前
设计一个基础JWT的多开发语言分布式电商系统
开发语言·分布式