Spring Web Socket

目录

1.介绍

1.1 什么是Web socket

1.2 与HTTP协议区别

1.2.1 通信方式

1.2.2 数据传输与开销

1.2.3 应用场景

2. Java原生使用

2.1 导入所需依赖

2.2 使用

2.2.1 java后端

2.2.2 Html页面

3.原生1.0实现在线聊天室

3.1.编写登录页面

3.2 编写登录servlet

3.3 编写聊天页面

3.4 编写握手之前类

3.4 编写聊天服务

4.原生1.0订阅与发布

4.1.发布页面

4.2.发布管理后台

4.3.订阅页面

4.4.订阅管理

4.5.事件总线

5. spring 2.0在线聊天室

5.1 导入相关依赖

5.2 编写MVC配置类

5.3替代传统的 web.xml 文件

5.4 聊天服务

5.5 spring中 web socket配置

6.STOMP协议

6.1介绍

6.1.1 区别:

7. spring 2.0 实现主题订阅与发布

7.1 编写Web socket 服务类

7.2 发布后端

7.3 发布前端页面

7.4 订阅页面


1.介绍

1.1 什么是Web socket

Web socket是一种网络协议,本质还是基于TCP协议实现,他可以实现双通,往常的HTTP协议是只能一次请求一次相应,服务端是不会主动像客户端发请求的,只有客户端向服务端发送请求,在 Web Socket 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输,客户端和服务器之间的数据交换变得更加简单。

1.2 与HTTP协议区别

1.2.1 通信方式

Web Socket

  • 是一种在单个TCP连接上进行全双工通信的协议。

  • 允许客户端和服务器之间进行实时的双向通信,即服务器可以随时主动给客户端下发数据,客户端也可以随时向服务器发送数据。

HTTP

  • 超文本传输协议,是一个简单的请求-响应协议。

  • 通常是单向通信,即客户端发起请求,服务器返回响应。虽然可以通过Ajax、长轮询等方式模拟双向通信,但本质上还是基于多次HTTP请求的短连接。

1.2.2 数据传输与开销

Web Socket

  • 使用长连接,因此在数据传输过程中可以减少连接建立和断开的开销。

  • 头部信息相对较小,减少了数据传输的开销。

HTTP

  • 每次通信都需要发送完整的HTTP请求和响应头,因此数据传输的开销相对较大。

  • 头部信息包含了大量的元数据(如请求方法、URL、响应状态码、响应头等),这些元数据在数据传输过程中会占用一定的带宽。

1.2.3 应用场景

Web Socket

  • 适用于需要实时数据传输的场景,如实时聊天、实时协作、实时数据推送、多人在线游戏等。

  • 与HTTP协议有着良好的兼容性,默认端口也是80和443,并且握手阶段采用HTTP协议,因此握手时不容易被屏蔽,能通过各种HTTP代理服务器。

HTTP

  • 适用于传统的Web浏览、文件传输等场景。

  • 作为Web领域的基础协议,具有广泛的兼容性和应用基础。

2. Java原生使用

2.1 导入所需依赖

        
       
            jakarta.servlet
            jakarta.servlet-api
            6.0.0
            provided
        
        
        
            jakarta.websocket
            jakarta.websocket-api
            2.1.1
            provided
        
        
        
            jakarta.websocket
            jakarta.websocket-client-api
            2.1.1
            provided
        
        
        
            ch.qos.logback
            logback-classic
            1.5.8
        

2.2 使用

2.2.1 java后端
/**
 *
 * websocket的服务端
 * ServerEndpoint注解是JSR的标准注解,
 * 用于定义websocket服务端的连接地址
 */
@Slf4j
@ServerEndpoint("/server")
public class WebSocketServer {
​
    /**
     * 当客户端连接服务器后执行的方法(执行一次)
     * session参数表示一个客户端的连接会话
     */
    @OnOpen
    public void onOpen(Session session) {
        log.info("客户端已连接...");
    }
​
    /**
     * 接收客户端发送的消息
     * @param message
     */
    @OnMessage
    public void onMessage(String message, Session session) throws IOException {
        // 打印客户端信息
        log.info(message);
        //发送一个消息给客户端
        session.getBasicRemote().sendText("Hello Client");
    }
​
    /**
     * 当客户端关闭连接后执行此方法
     */
    @OnClose
    public void onClose(Session session) {
        log.info("客户端已断开连接");
    }
}

@ServerEndpoint("/server")注解表示需要连接的端点,此时的端点为/server,客户端只需要和这个端点建立通信即可。

@OnOpen 当客户端连接服务器成功后执行的方法。

@OnMessage 接收前端客户端页面发送的信息的方法。

@OnClose 当客户端关闭连接后执行此方法。

2.2.2 Html页面



    
    Title


websocket示例

3.原生1.0实现在线聊天室

目标:多个用户可以登录,登录成功才能进入聊天页面,多个人可以互相发送信息。页面要显示发送人,发生时间,发送内容。

引入如下依赖


            jakarta.servlet
            jakarta.servlet-api
            6.0.0
            provided
        
        
            jakarta.websocket
            jakarta.websocket-api
            2.1.1
            provided
        
        
            jakarta.websocket
            jakarta.websocket-client-api
            2.1.1
            provided
        
        
            ch.qos.logback
            logback-classic
            1.5.8
        
        
            org.projectlombok
            lombok
            1.18.34
        
        
            com.google.code.gson
            gson
            2.10.1
        

3.1.编写登录页面




    
    Title


用户登陆

  账号:    

3.2 编写登录servlet

用户通过页面输入用户名进入,将用户名存入session

@WebServlet("/login")
public class LoginServlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String username = req.getParameter("username");
        req.getSession().setAttribute("username",username);
        //转发到聊天页面
        req.getRequestDispatcher("/WEB-INF/index.html").forward(req, resp);
    }
}

3.3 编写聊天页面




    
    Title
    


聊天室

       

3.4 编写握手之前类

web socket协议无法之间获取HTTP请求协议的数据,但是web socket协议是通过HTTP协议进行升级的,所有我们可以在升级之前获取session,在给到聊天服务

public class WebSocketHandshake extends Configurator {
​
    @Override
    public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
        // 获取请求的session
        HttpSession httpSession = (HttpSession) request.getHttpSession();
        String name = (String)httpSession.getAttribute("username");
        sec.getUserProperties().put("username", name);
    }
}

3.4 编写聊天服务

通过ServerEndpoint注解中configurator属性放进来,目的就是为了拿到HTTP中的session,两个集合必须标注static,实现共享,如果没有static,那么每一次请求都是新的实例。

发送信息的方式:

同步:session.getBasicRemote().sendText(message); 适合同步、简单的消息发送需求。

异步:session.getAsyncRemote().sendText(message);适合需要高并发或异步消息发送的场景。

@ServerEndpoint(value = "/server", configurator = WebSocketHandshake.class)
public class ChatServer {
​
    /**
     * 在线用户列表,key保存用户名, value存放所有客户端连接的Session
     */
    private static Map onlineUsers = new HashMap<>();
​
    /**
     * 离线消息缓存,key保存用户名,value是消息集合
     */
    private static Map> offlineCache = new HashMap<>();
​
    /**
     * 从数据库中获取所有用户并初始化缓存
     */
    static {
        offlineCache.put("wangl", new ArrayList<>());
        offlineCache.put("zing", new ArrayList<>());
        offlineCache.put("cj", new ArrayList<>());
    }
​
    /**
     * 当客户端连接到服务器时,将session对象保存到在线列表中
     * ,同时读取离线消息
     * @param session
     */
    @OnOpen
    public void onOpen(Session session) throws IOException {
        //获取用户名
        User user = (User) session.getUserProperties().get("user");
        onlineUsers.put(user.getUserName(), session);
        //读取离线消息
        List messages = offlineCache.get(user.getUserName());
        for(String message : messages) {
            session.getBasicRemote().sendText(message);
        }
        //读取完消息清空消息缓存
        messages.clear();
    }
​
    /**
     * 接收到某个客户端发送的消息时,群发给所有人
     * 注意:发行时要获取发送人的信息,
     * 通常发送人的信息是登陆后保存在HttpSession中
     * 因此可以在websocket握手的阶段获取HttpSession(握手时会先发起一次http请求)
     * @param session
     * @param message
     */
    @OnMessage
    public void onMessage(Session session, String message) throws IOException {
        //如果是心跳检测则直接返回
        if(message.trim().isEmpty()) {
            return;
        }
        //获取发送人
        User user = (User) session.getUserProperties().get("user");
        //构建消息对象
        MessageVO vo = new MessageVO();
        vo.setSendUser(user.getUserName());
        vo.setSendDate(new SimpleDateFormat("HH:m:ss").format(new Date()));
        vo.setContent(message);
        //将vo序列为json字符串
        String json = new Gson().toJson(vo);
        /**
         * 遍历离线消息缓存的所有用户
         */
        for(String userName : offlineCache.keySet()) {
            //判断用户名是否是在线用户
            if(onlineUsers.containsKey(userName)) {
                Session onlineSession = onlineUsers.get(userName);
                onlineSession.getBasicRemote().sendText(json);
            } else {
                //将消息保存到离线缓存中
                offlineCache.get(userName).add(json);
            }
        }
    }
​
    /**
     * 当用户关闭连接时,从在线列表中移除该用户
     * @param session
     */
    @OnClose
    public void onClose(Session session) throws IOException {
        //获取用户名
        User user = (User) session.getUserProperties().get("user");
        //从用户列表中移除
        onlineUsers.remove(user.getUserName());
        //关闭会话
        session.close();
    }
}

4.原生1.0订阅与发布

在上面聊天的界面中,是点对点的形式,而订阅发布则是多对多的形式。常见的案例就是公众号,只要订阅了,那么服务端有更新就好给你推送信息。下面通过java语言简单实现一个订阅与发布。

4.1.发布页面




    
    Title
    


后台

  消息:    

4.2.发布管理后台

@WebServlet("/publish")
public class AdminServlet extends HttpServlet {

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //获取页面提交的消息内容
        String message = req.getParameter("message");
        //通知所有的订阅者
        EventBus.notify(message);
    }
}

4.3.订阅页面




    
    Title
    


消息订阅

4.4.订阅管理

@ServerEndpoint("/server")
public class WebSocketServer {
​
    @OnOpen
    public void onOpen(Session session) {
        //连接时将所有的客户端注册到事件总线上
        EventBus.subscribe(session);
    }
​
    /**
     * 离线时取消订阅
     * @param session
     */
    @OnClose
    public void onClose(Session session) throws IOException {
        EventBus.unsubscribe(session);
        session.close();
    }
}

4.5.事件总线

/**
 * 事件总线,用于提供订阅者(客户端)的注册,
 * 同时还具备消息通知的功能(通知所有的订阅者)
 */
public class EventBus {
    /**
     * 订阅者集合
     */
    private static List sessions = new ArrayList();

    /**
     * 订阅方法
     * @param session 订阅者(客户端)
     */
    public static void subscribe(Session session) {
        sessions.add(session);
    }

    /**
     * 取消订阅
     * @param session
     */
    public static void unsubscribe(Session session) {
        sessions.remove(session);
    }

    /**
     * 通知所有的订阅者
     * @param message
     */
    public static void notify(String message) throws IOException {
        for(Session session : sessions) {
            session.getBasicRemote().sendText(message);
        }
    }
}

注意:订阅与发布是不能存session的,因为存session是无法获取的,所有必须得来个中间层(消息队列),目前已经实现了简单的订阅与发布,但是存在一个问题,那就是没有分类,如果我想看体育相关,不想新闻相关,那么就实现不了,那么接下来,spring提供了很好的工具与配置,帮助我们来实现此功能。

5. spring 2.0在线聊天室

与1.0版本相比,我们可以不用编写握手之前的类(WebSocketHandshake),spring框架给我们做了非常好的处理。页面可以用之前的,类我们重新编写,这里我们就不做离线功能。

5.1 导入相关依赖

        
            org.springframework
            spring-webmvc
            6.1.12
        
        
            org.springframework
            spring-websocket
            6.1.12
        
        
            com.fasterxml.jackson.core
            jackson-databind
            2.17.2
        

5.2 编写MVC配置类

// 声明一个配置类
@Configuration
// 启用mvc注解支持
@EnableWebMvc
public class MvcConfigure implements WebMvcConfigurer {
​
    // 配置默认servlet
    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }
​
    /**
    *用于设置视图解析器(InternalResourceViewResolver),它定义了在返回视图名称时如何找到对应的视图文件。
    */
    @Bean
    public InternalResourceViewResolver ViewResolver() {
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
        viewResolver.setPrefix("/WEB-INF/page/");
        viewResolver.setSuffix(".html");
        return viewResolver;
    }
}

5.3替代传统的 web.xml 文件

目的实现自动注册DispatcherServlet。

public class WebInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    //不需要配置根应用上下文
    @Override
    protected Class[] getRootConfigClasses() {
        return new Class[0];
    }

    //只需一个AppConfigure即可
    @Override
    protected Class[] getServletConfigClasses() {
        return new Class[]{AppConfigure.class};
    }

    // 所有路径都经过DispatcherServlet
    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }
}

下面文件与上方功能一致


    dispatcher
    org.springframework.web.servlet.DispatcherServlet
    
        contextConfigLocation
        /WEB-INF/spring-servlet.xml
    


    dispatcher
    /

5.4 聊天服务

public class ChatServer extends TextWebSocketHandler {
​
    /**
     * 在线用户列表
     */
    private static Map onlineUsers = new HashMap<>();
​
    /**
     * 类似于onOpen
     * @param session
     * @throws Exception
     */
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        //获取登录用户
        String user = (String) session.getAttributes().get("user");
        onlineUsers.put(user, session);
    }
​
    /**
     * 类似于onMessage
     * @param session
     * @param message
     * @throws Exception
     */
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        String user = (String) session.getAttributes().get("user");
        //封装消息对象
        MessageVO vo = new MessageVO();
        vo.setSendUser(user);
        vo.setSendDate(new SimpleDateFormat("HH:mm:ss").format(new Date()));
        vo.setContent(message.getPayload());
        String json = new ObjectMapper().writeValueAsString(vo);
        //注意:WebSocketSession发送的消息是TextMessage对象
        message = new TextMessage(json);
        //群发
        for(String name : onlineUsers.keySet()) {
            WebSocketSession s = onlineUsers.get(name);
            s.sendMessage(message);
        }
    }
​
    /**
     * 类似于onClose
     * @param session
     * @param status
     * @throws Exception
     */
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        //获取登录用户
        String user = (String) session.getAttributes().get("user");
        onlineUsers.remove(user);
        session.close();
    }
}

可以发现,他已经对session进行了封装,可以直接获取httpSession里面的值。变得非常好。

5.5 spring中 web socket配置

@Configuration
@EnableWebSocket
public class WebSocketConfigure implements WebSocketConfigurer {

    /**
     * 装配WebSocket服务端
     * @return
     */
    @Bean
    public ChatServer chatServer() {
        return new ChatServer();
    }

    /**
     * 设置服务端的连接端点
     * @param registry
     */
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(chatServer(), "/server")
                //注册握手拦截器
                .addInterceptors(handshakeInterceptor());
    }

    /**
     * 装配HttpSession的握手拦截器
     * @return
     */
    @Bean
    public HttpSessionHandshakeInterceptor handshakeInterceptor() {
        return new HttpSessionHandshakeInterceptor();
    }
}

6.STOMP协议

6.1介绍

STOMP允许消息客户端(生产者、消费者)与任意消息代理(Broker)之间进行异步消息传输的简单文本定向消息协议。但STOMP并不是为WebSocket而设计的,它是属于消息队列的一种协议(AMQP、JMS等都属于消息队列协议)。许多消息队列都支持STOMP协议(例如:RabbitMQ、ActiveMQ)。由于它的简单性,因此可以用于定义web socket的消息体格式。我们先建立了webscoket连接, 接下来我只需要在web scoket连接的基础上建立stomp连接,因此STOMP协议格式的消息就会写入到websocket的payload中。

6.1.1 区别:

WebSocket WebSocket 是一种网络协议,工作在 TCP/IP 协议的应用层。它是为实现双向、全双工通信而设计的协议,提供了浏览器与服务器之间的低延迟通信能力。WebSocket 本身非常轻量,仅提供基本的消息传递机制,并不定义消息的内容格式或语义。可以发送任意二进制数据或文本数据。开发者需要自行定义通信协议和格式,例如使用 JSON、Protobuf 或其他自定义格式。WebSocket 的设计目标是让浏览器或其他客户端与服务器之间实现低延迟、双向通信。它是一个通用的通信协议,不限定消息的语义或用途。开发者需要自己定义消息格式和协议逻辑。它本身不支持高级功能,如消息队列、主题订阅等。

STOMP STOMP 是一种 消息协议,设计用于消息代理系统(如 ActiveMQ、RabbitMQ)。它是一种面向文本的协议,定义了消息格式和通信语义(如订阅、发送、确认等)。STOMP 可以运行在多个底层协议之上,包括 TCP 和 WebSocket。换句话说,WebSocket 可以用作 STOMP 的底层传输协议。STOMP 的设计目标是为消息代理系统提供一种轻量的、简单的消息协议。它支持订阅/发布(pub/sub)模式、点对点消息、消息确认等功能,适合构建基于消息队列的应用。

STOMP 提供了高层次的抽象,比如:

1.客户端订阅某个主题或队列,接收消息。

2.客户端发送消息到某个主题或队列。

3。确认或拒绝接收到的消息。 STOMP 适合复杂的消息传递系统,比如分布式系统、企业集成等。

使用场景

  • WebSocket

    • 游戏中的实时交互

    • 实时聊天应用

    • 股票交易和行情推送

    • 实时协作编辑

  • STOMP

    • 使用消息代理的企业系统(如 ActiveMQ、RabbitMQ)

    • 需要可靠消息传递的系统

    • 分布式系统中的任务调度与事件通知

特性 WebSocket STOMP
层次 应用层协议 消息协议(基于传输协议)
设计目的 通用实时通信 消息队列通信
消息格式 自定义(文本/二进制) 规范化(类似 HTTP)
高级功能 不支持,需要自定义 支持订阅、确认等消息特性
使用场景 即时双向通信应用 消息代理与队列系统

如果需要低层次的实时通信能力,Web Socket 是理想选择;如果需要在消息代理系统中构建复杂的消息通信应用,STOMP 更加合适。

7. spring 2.0 实现主题订阅与发布

可以继续使用上面5.1-5.3的代码。下方则是继续编写。

7.1 编写Web socket 服务类

@Configuration
/**
 * 当使用消息代理时启用EnableWebSocketMessageBroker注解
 * ,同时需要实现WebSocketMessageBrokerConfigurer
 */
@EnableWebSocketMessageBroker
public class WebSocketConfigure implements WebSocketMessageBrokerConfigurer {
​
    /**
     * 注册一个基于STOMP协议的服务端点
     * @param registry
     */
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("message-broker");
    }
​
    /**
     * 配置消息代理,这里使用spring内部提供的消息代理中间件,
     * 也可继承外部强大的消息队列中间件(如:rabbitmq)
     * @param registry
     */
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        //设置一个主题请阅的前缀, 当发布或者订阅消息时都要指定这个前缀
        registry.enableSimpleBroker("/topic");
    }
}

7.2 发布后端

@RestController
@RequiredArgsConstructor
public class AdminController {
​
    /**
     * 注入模版消息,用于发布内容到消息代理
     */
    private final SimpMessagingTemplate template;
​
    @PostMapping("/publish")
    public String publish(@RequestParam("message") String message) {
        //将消息发布到指定的主题上
        template.convertAndSend("/topic/sports", message);
        return "success";
    }
}

7.3 发布前端页面




    
    Title
    


发布消息

       
  消息:    

7.4 订阅页面




    
    新闻
  
  


订阅新闻

目前已经实现了主题订阅与发布,发布可以发布多个主题,比如新闻与体育,此页面只订阅新闻,体育将不会看到。

你可能感兴趣的:(前端,java,spring,笔记,学习)