SSE和WebSocket

本文深入探讨了SSE(服务器发送事件)和WebSocket两种实时通信技术。SSE是服务端向客户端单向推送消息的方式,示例展示了如何在Spring框架下实现SSE订阅和推送。WebSocket则支持双向通信,适用于即时通讯场景,文章通过服务端和前端代码展示了WebSocket的创建、连接、消息收发以及关闭过程。同时,提供了模拟接口用于服务端主动推送消息。
摘要由CSDN通过智能技术生成

SSE和WebSocket

SSE

SSE:server send event。服务端发送事件,指服务端主动给客户端推送消息(单向)

服务端代码:

package com.example.demo.sse;

import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.io.IOException;
import java.util.Collection;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;

@CrossOrigin
@Controller
@RequestMapping(path = "sse")
public class Sse {
    /**
     * key为用户Id,内层Map key为客户端ID(一个用户可能在多个客户端中登陆)
     */
    private static Map<String, ConcurrentHashMap<String, SseEmitter>> sseCache = new ConcurrentHashMap<>();

    /**
     * sse订阅方法,注意返回类型为:text/event-stream
     * @param userId 连接用户的ID
     * @param clientId 连接的客户端D
     * @return SseEmitter
     * @throws IOException
     */
    @ResponseBody
    @GetMapping(path = "subscribe", produces = {MediaType.TEXT_EVENT_STREAM_VALUE})
    public SseEmitter subscribe(String userId,String clientId) throws IOException {
        // 超时时间设置为30s,用于演示客户端自动重连
        SseEmitter sseEmitter = new SseEmitter(1000L * 30);
        // 设置前端的重试时间为1s
        sseEmitter.send(SseEmitter.event().reconnectTime(1_000L).data("连接成功"));
        sseCache.putIfAbsent(userId,new ConcurrentHashMap<>());
        sseCache.get(userId).put(clientId,sseEmitter);
        System.out.println("注册 userd:" + userId + "," + "clientId:" + clientId);
        sseEmitter.onTimeout(() -> {
            System.out.println(userId + "--" + clientId + " 超时");
            sseCache.getOrDefault(userId,new ConcurrentHashMap<>()).remove(clientId);
        });
        sseEmitter.onCompletion(() -> System.out.println("完成!!!"));
        return sseEmitter;
    }

    /**
     * 模拟服务端主动给客户端推送消息
     * @param userId 客户端的连接ID(需要接受消息的客户端ID)
     * @param content 发送消息内容
     * @return String
     * @throws IOException
     */
    @GetMapping(path = "push")
    @ResponseBody
    public String push(String userId, String clientId, String content) throws IOException {
        ConcurrentHashMap<String, SseEmitter> sseCacheOrDefault = sseCache.getOrDefault(userId, new ConcurrentHashMap<>());
        if(Objects.nonNull(clientId)){
            sendContent(content,sseCacheOrDefault.get(clientId));
        }else {
            Collection<SseEmitter> values = sseCacheOrDefault.values();
            for (SseEmitter sse: values){
                sendContent(content, sse);
            }
        }
        return "over";
    }

    /**
     * 发送消息
     * @param content 消息内容
     * @param sse SseEmitter
     * @throws IOException
     */
    private void sendContent(String content, SseEmitter sse) throws IOException {
        if(Objects.nonNull(sse)){
            sse.send(content);
        }
    }

    /**
     * 结束连接
     * @param userId 要结束的用户ID
     * @param clientId 要结束的客户端ID
     * @return String
     */
    @GetMapping(path = "over")
    @ResponseBody
    public String over(String userId,String clientId) {
        ConcurrentHashMap<String, SseEmitter> sseCacheOrDefault = sseCache.getOrDefault(userId, new ConcurrentHashMap<>());
        if(Objects.nonNull(clientId)){
            SseEmitter sseEmitter = sseCacheOrDefault.get(clientId);
            if (sseEmitter != null) {
                sseEmitter.complete();
            }
            sseCacheOrDefault.remove(clientId);
        }else {
            Collection<SseEmitter> values = sseCacheOrDefault.values();
            for (SseEmitter sseEmitter: values){
                if (sseEmitter != null) {
                    sseEmitter.complete();
                }
            }
            sseCacheOrDefault.remove(userId);
        }
        return "over";
    }
}

前端代码:

<!DOCTYPE HTML>
<html>
   <head>
   <meta charset="utf-8">
   <title>WebSocket</title>
      <script type="text/javascript">
      	var host = "127.0.0.1";
      	var port = '8080';
      	var userId = "test";
      	//获取浏览器信息
       var clientId =  window.btoa(window.navigator.userAgent);
       // sse方法
         function sseTest(){
         	var source = new EventSource('http://' + host + ':' + port + '/sse/subscribe?userId=' + userId + '&clientId=' + clientId);
		    source.onmessage = function (event) {
		    	console.log("SSE:"+ event.data)
		    };

		    <!-- 添加一个开启回调 -->
		    source.onopen = function (event) {
		        console.log("SSE:"+event);
		    };
         }
      </script>
        
   </head>
   <body>
   
      <div id="sse">
         <a href="javascript:sseTest()">运行 SSE</a>
      </div>
      
   </body>
</html>

WebSocket

WebSocket:客户端和服务端实现双工通信(双向),多用于即时通信

服务端代码:

pom.xml

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

注册ServerEndpointExporter

 /**
  * 会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint
  * 要注意,如果使用独立的servlet容器,
  * 而不是直接使用springboot的内置容器,
  * 就不要注入ServerEndpointExporter,因为它将由容器自己提供和管理。
  */
@Bean
@ConditionalOnMissingBean(ServerEndpointExporter.class)
public ServerEndpointExporter serverEndpointExporter() {
  return new ServerEndpointExporter();
}

WebSocket服务代码:(服务端核心逻辑)

package com.example.demo.websocket;

import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Collection;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

@Component
@ServerEndpoint(value = "/websocket/{userId}/{clientId}")
public class WebSocket {

    private static final AtomicInteger ONLINE_CLIENTS = new AtomicInteger(0);
    /**
     * key为用户Id,内层Map key为客户端ID(一个用户可能在多个客户端中登陆)
     */
    private static final Map<String, Map<String, Session>> CLIENTS = new ConcurrentHashMap<>();

    /**
     * 打开webSocket连接(注册)
     * @param userId 用户ID
     * @param clientId 客户端ID
     * @param session 连接session
     */
    @OnOpen
    public void onOpen(@PathParam("userId") String userId, @PathParam("clientId") String clientId ,Session session) {
        addOnlineCount();
        CLIENTS.putIfAbsent(userId, new ConcurrentHashMap<>());
        CLIENTS.get(userId).put(clientId,session);
        System.out.println("注册 userd:" + userId + "," + "clientId:" + clientId);
    }
    /**
     * 关闭webSocket连接(注销)
     * @param userId 用户ID
     * @param clientId 客户端ID
     * @param session 连接session
     * @throws IOException
     */
    @OnClose
    public void onClose(@PathParam("userId") String userId, @PathParam("clientId") String clientId ,Session session) throws IOException {
        System.out.println("websocket close userd:" + userId + "," + "clientId:" + clientId);
        session.close();
        CLIENTS.getOrDefault(userId,new ConcurrentHashMap<>()).remove(clientId);
        subOnlineCount();
    }


    /**
     * 接受前端信息
     * @param userId 用户ID
     * @param clientId 客户端ID
     * @param content 消息内容
     * @throws IOException
     */
    @OnMessage
    public void onMessage(@PathParam("userId") String userId, @PathParam("clientId") String clientId , String content) throws InterruptedException {
        System.out.println("接受到数据:" + content);
        sendContentToUser(userId,clientId,"已接受到数据:" + content);
        while (true){
            sendContentToUser(userId,clientId, LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
            Thread.sleep(5000L);
        }
    }

    /**
     * 接受前端信息
     * @param userId 用户ID
     * @param clientId 客户端ID
     * @param session 报错到sessin连接
     * @param error 抛出到异常
     */
    @OnError
    public void onError(@PathParam("userId") String userId, @PathParam("clientId") String clientId ,Session session, Throwable error) {
        System.out.println("websocket err userd:" + userId + "," + "clientId:" + clientId);
        error.printStackTrace();
    }


    /**
     * 发送消息给指定用户和客户端
     * @param userId 用户ID
     * @param clientId 客户端ID
     * @param content 消息内容
     */
    public static void sendContentToUser(String userId, String clientId, String content) {
        Map<String, Session> clients = CLIENTS.getOrDefault(userId, new ConcurrentHashMap<>());
        if(Objects.nonNull(clientId)){
            Session session = clients.get(clientId);
            sendContent(content, session);
        }else {
            Collection<Session> values = clients.values();
            for (Session session: values){
                sendContent(content,session);
            }
        }
    }

    /**
     * 用给指定session发送消息
     * @param content 消息内容
     * @param session 指定到session连接
     */
    private static void sendContent(String content, Session session) {
        if (Objects.nonNull(session)) {
            session.getAsyncRemote().sendText(content);
        }
    }


    /**
     * 发送消息给所有用户
     * @param content 消息内容
     */
    public static void sendContentAll(String content) {
        Collection<Map<String, Session>> values = CLIENTS.values();
        values.forEach(m -> m.values().forEach(s -> sendContent(content,s)));
    }



    private static int getOnlineCount() {
        return ONLINE_CLIENTS.get();
    }



    private static void addOnlineCount() {
        ONLINE_CLIENTS.addAndGet(1);
    }

    private static void subOnlineCount() {
        ONLINE_CLIENTS.decrementAndGet();
    }

}

模拟接口:(模拟服务端主动给客户端推送消息)

package com.example.demo.controller;

import com.example.demo.websocket.WebSocket;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class WebSocketController {
    /**
     * 模拟给指定用户和客户端发送消息
     * @param userId 用户Id
     * @param clientId 客户端ID
     * @param content 消息内容
     * @return String
     */
    @RequestMapping("/push")
    public String push(String userId,String clientId,String content){
        WebSocket.sendContentToUser(userId,clientId,content);
        return "OK";
    }

    /**
     * 模拟给所有客户端发消息
     * @param content 消息内容
     * @return String
     */
    @RequestMapping("/pushAll")
    public String pushAll(String content){
        WebSocket.sendContentAll(content);
        return "OK";
    }
}

前端代码:

<!DOCTYPE HTML>
<html>
   <head>
   <meta charset="utf-8">
   <title>WebSocket</title>
      <script type="text/javascript">
      	var host = "127.0.0.1";
      	var port = '8080';
      	var userId = "test";
      	//获取浏览器信息
       var clientId =  window.btoa(window.navigator.userAgent);
       // webSocket方法
         function WebSocketTest()
         {
            if ("WebSocket" in window)
            {
               alert("您的浏览器支持 WebSocket!");
               // 打开一个 web socket
               var ws = new WebSocket("ws://" + host+ ":" + port + "/websocket/" + clientId);
                
               ws.onopen = function()
               {
                  // Web Socket 已连接上,使用 send() 方法发送数据
                  ws.send("发送数据");
                  alert("数据发送中...");
               };
                
               ws.onmessage = function (evt) 
               { 
                  var received_msg = evt.data;
                  // alert("数据已接收..." + received_msg);
                  console.log("WebSocket:"+ received_msg);
               };
                
               ws.onclose = function()
               { 
                  // 关闭 websocket
                  alert("连接已关闭..."); 
               };
            }
            
            else
            {
               // 浏览器不支持 WebSocket
               alert("您的浏览器不支持 WebSocket!");
            }
         }
      </script> 
   </head>
   <body>
   
      <div id="websocket">
         <a href="javascript:WebSocketTest()">运行 WebSocket</a><br>
      </div>
      
   </body>
</html>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值