消息推送技术(sse、websocket)

一、消息推送

消息推送:所谓信息推送,就是web广播,是通过一定的技术标准或协议,在互联网上通过定期传送用户需要的信息来减少信息过载的一项新技术。推送技术通过自动传送信息给用户,来减少用于网络上搜索的时间。

举个例子:服务器的消息推送给客户端,体现出一个主动,而不是你请求,我才给你数据。

二、常见的消息推送场景

大家在日常的上网过程中,大多数都是客户端发起请求,然后服务端响应。但是有的时候,确是需要服务端来做这件事。咱们其实能见很多消息推送场景的,例如:

  • B站视频的弹幕消息
  • 一些系统的实时消息提示
  • 付完款后页面会自动跳转到下单成功的页面
  • QQ、微信等实时聊天
  • 股价更新、新的博文、赛事结果
  • …………

三、消息推送的实现方式

那么他们是如何实现的呢?

  • AJAX长短轮循
  • 基于HTTP协议的SSE(Server-Sent Events)技术
  • WebSocket技术
  • MQTT通讯协议
  • ……

四、技术实现

1、基于HTTP的AJax轮循实现

1.1、Ajax短轮循

Ajax的短轮询本质上,还是客户端发起请求,服务端响应。但是为了能够实时,前端做了定时任务,使其能够实现实时刷新。后端只做一个showTime的接口即可。

<script type="text/javascript" src="/static/js/jquery-1.9.1.min.js"></script>
<script type="text/javascript">
    function showTime() {
        $.get("showTime", function (data) {
            console.log(data);
            $("#serverTime").html(data);
        })
    }
    //Http短轮循,定时 1秒执行一次
    setInterval(showTime, 1000);
</script>

1.2、Ajax长轮循

后端使用DeferredResult类作为返回类型。使用这个类后,每次请求过来后,如果请求不到,则会挂起。

<!--前端-->
<script type="text/javascript" src="static/js/jquery-1.9.1.min.js"></script>
<script type="text/javascript">
    longLoop();

    function longLoop() {
        $.get("httpLongPollingMethod",function (data) {
            console.log(data);
            $("#serverTime").text(data);
            //一定要等返回数据后才会请求。
            longLoop();
        })
    }
    //长轮循不能使用定时任务,否则就会出现:当前请求未返回挂起后,定时的时间到了,又会发起一个新的请求,又会挂起一个请求。。。。
    //然后挂起的请求多了,会浪费很大的资源
    // setInterval(longLoop, 1000);
</script>
@ResponseBody
@RequestMapping("/httpLongPollingMethod")
public DeferredResult<String> realtimeNews(HttpServletRequest request) {
    final DeferredResult<String> dr = new DeferredResult<String>();
    ExecutorService executorService = newFixedThreadPool(1);
    //业务开始.....
    //此时为了看到效果,咱们增加了线程
    executorService.submit(new Runnable() {
        public void run() {
            try {
                //咱们hold住程序3秒。看一下挂起的请求
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            int index = new Random().nextInt(10);
            //setResult 才是触发返回响应的地方。
            dr.setResult(Contanst.NEWS.get(index));
        }
    });
    //业务结束....
    return dr;
}

注意如果返回值类型不是DeferredResult,请求到了过期时间,会自动返回响应超时:java.net.SocketTimeoutException: Read timed out。但是如果使用了DeferredResult,如果服务端没有响应,那当前请求会挂起,一直等待响应。

注意:return是断开连接的标志,DeferredResult.setResult(T result) 是结束挂起标志,如果DeferredResult.result值为null,主线程会一直挂起,一直等待setResult方法调用。

执行完setResult方法,将真实的响应数据赋值到DeferedResult对象中异步线程会唤醒主线程,主线程会继续执行getResult方法,将真实数据响应到客户端。

在这里插入图片描述

关于DeferredResult的代码分析👉DeferredResult的源码分析

2、SSE

2.1、简介

Server-Sent Events(SSE)是一种用于实现服务器向客户端实时推送数据的Web技术。与传统的轮询和长轮询相比,SSE提供了更高效和实时的数据推送机制。

严格地说,HTTP 协议是无法做到服务器主动推送信息。但是,有一种变通方法,那就是SSE

SSE基于HTTP协议,允许服务器将数据以事件流(Event Stream)的形式发送给客户端。客户端通过建立持久的HTTP连接,并监听事件流,可以实时接收服务器推送的数据。

说白一点,SSE发送的不是一次性的数据包,而是一个数据流。只要客户端不关闭连接,会一直接受服务端发过来的数据流,视频播放就是这样的例子。本质上,这种通信就是以流信息的方式,完成一次用时很长的下载。

SSE就是利用这种机制,使用流信息向浏览器推送信息。它基于HTTP 协议,目前除了IE/Edge,其他浏览器都支持。

SSEWebSocket 作用相似,但是WebSocket更强大和灵活。因为WebSocket是全双工通道,可以双向通信;SSE 是单向通道,只能服务器向浏览器发送。

WebSocket相比SSE优点:

  • SSE 使用 HTTP 协议,现有的服务器软件都支持。WebSocket 是一个独立协议。
  • SSE 属于轻量级,使用简单;WebSocket协议相对复杂。
  • SSE 默认支持断线重连,WebSocket 需要自己实现。
  • SSE 一般只用来传送文本,二进制数据需要编码后传送,WebSocket 默认支持传送二进制数据。
  • SSE 支持自定义发送的消息类型。

2.2、SSE的(HTTP)协议介绍

SSE的通信协议是基于HTTP协议的,但是他是一个简单的纯文本传输协议。所以他仅支持UTF-8格式的编码
使用这个协议,响应报文Content-TypeMIME类型必须指定为text/event-stream。且他具有如下HTTP头信息:

Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

SSE的通信协议是一个事件流,他由不同的事件组成,不同事件间通过回车符\n来进行分隔,结束时使用双回车符\n\n

每个事件可以由多行构成,每行由类型和数据两部分组成。类型与数据通过冒号:进行分隔,冒号前的为类型,冒号后的为其对应的值。格式如下:

[field]:value\n

每个事件的field可以包含如下的值:

  • 空白:表示该行是注释,会在处理时被忽略。通常,服务器每隔一段时间就会向浏览器发送一个注释,保持连接不中断。
  • data:表示该行是事件所包含的数据。以 data 开头的行可以出现多次。所有这些行都是该事件的数据。
  • event:表示该行用来声明事件的类型。浏览器在收到数据时,会产生对应名称的事件。支持自定义事件。默认是message事件。浏览器可以用addEventListener(String:eventName,listener:function)监听该事件。
  • id:表示该行用来声明事件的标识符。
  • retry:表示该行用来声明浏览器在连接断开之后进行重连的等待时间。
//如下,冒号前表示的是该行的类型,冒号后面是对应的值。\n\n表示结束,需要空出两行才对,我给删除了。
//类型为空白,表示该行是注释,回在处理时被忽略
: this is a comment 
//本行表示用来声名事件类型,浏览器收到该事件时,会产生对应类型的事件。如果没有该字段,会产生默认类型message。支持自定义事件。
//名称为sendMessage的事件。数据发送到浏览器,会触发浏览器的sendMessage事件。如果没有该行,表示默认类型,触发message事件。
//即:浏览器在使用SEE的时候,会绑定事件,比如: onMessage方法,onopen方法,onerror方法。不加这一行,会默认触发onmessage方法。
//加了这一行会触发sendMessage事件,不过自定义的事件,需要通过addEventListener方法来增加事件的监听。
event: sendMessage
//id,用来声名事件标识符,即当前sendMessage事件id为002
id: 002
//发送的第一行数据
data: 欢迎来到弹幕园
//发送的第二行数据。允许使用自定类型的数据。不过传输的时候,他会给转成字符串
data: {"clientId":"2023514","data":"哈哈哈哈"}
//断点重连的时间,即该行用来声明浏览器在连接断开之后进行再次连接之前的等待时间。默认3秒重连
retry: 1000

2.3、SSE短轮循

<script type="text/javascript" src="static/js/jquery-1.9.1.min.js"></script>
<script type="text/javascript">
    //省略function showDate方法,内容就是一个div,显示数据哈,就不写了。。。。
    if (!!window.EventSource) {
        var source = new EventSource('/needPrice/getDate');
        source.onmessage = function (e) {
            showDate(e.data)
        };
        source.onopen = function (e) {
            console.log("Connecting server!");
        };
        source.onerror = function () {
            console.log("error");
        };
        //增加自定义事件sendMessage的监听
        source.addEventListener("sendMessage", function(e) {
            showDate(e.data)
        })
    } else {
        //兼容适配
        $("#hint").html("您的浏览器不支持SSE!");
    }
</script>

我翻查了,好多博客,发现了两种写法:其实他们本质上是一样的。

/**
 * 方法一直,直接返回数据。
 * 注意:需要指定respose报文的的content-type类型。使用produces属性指定。
 * @return
 */
@ResponseBody
@RequestMapping(value = "needPrice", produces = "text/event-stream;charset=UTF-8")
public String push() {
    Random r = new Random();
    //字符串拼接
    StringBuilder sb = new StringBuilder();
    sb.append("retry:2000\n")
        .append("data:")
        .append((r.nextInt(1000)))
        .append("\n\n");
    //sse连接就断开了,不是长连接。
    return sb.toString();
}
/**
 * 使用流来传输数据
 */
@ResponseBody
@RequestMapping(value = "/needPrice/getDate", produces = "text/event-stream;charset=UTF-8")
public void getDate(HttpServletResponse response) throws Exception {
    log.info("getDate event start");
    Random r = new Random();
    while (!response.getWriter().checkError()) {
      	//注意哈,这个是死循环。大家最好加一下次数限制
        StringBuilder sb = new StringBuilder();
        //自定义事件sendMessage
        sb.append("event:sendMessage\n")
            .append("data:")
            .append((r.nextInt(1000)))
            .append("\n\n");
        response.getWriter().write(sb.toString());
        response.getWriter().flush();
        Thread.sleep(1000);
    }
    //另外说一下哈,无论这行注不注释,他都是短链接。这个我测试过了哈。
    //流的关闭不关闭,跟他是不是短连接没关系哈!
    response.getWriter().close();
    log.info("getDate event end");
}

注意:这种方法简单,自己拼装数据。但他是短连接,因为客户端请求到数据后,链接接着就断开了。像第二种方法,不是因为咱们关了流,链接才断的,链接断不断,跟关不关流没关系哈。

另外,我尝试很多次让他变成长连接,但是都没成功。最后想起来,长连接之所以是长连接,是因为链接没断链,数据发送完了,连接就挂起了。咱们这些是没法通过组织数据,让链接挂起的。

链接什么时候断开,是由客户端来决定的,不是服务端来决定的。客户端想要手动关闭链接,需要使用EventSource.close()方法。

就算服务器端可以手动断开连接,然后客户端接着就会给你断点重连。。。。。。因为返回retry默认是3秒

在这里插入图片描述

可有有小伙伴疑问。你这不是一直在发送数据么?这是长连接啊!不是的啊,真的不是的长连接啊,因为我代码里面while是个死循环啊!response.getWriter().close();log.info("getDate event end"); 这两行根本就没执行到哈!当咱们return或则是close后,也就是响应后,连接还存在,才是长连接啊!

2.4、SSE长轮循

经过上面的代码洗礼,基本上就能明白,实现链接、轮序访问等功能,那就是前端的活。可是光做前端,做成的结果就是短连接。想要变成长连接,那需要后端来做。后端来做一件事,那就是响应后,将链接挂起,这样短连接才能变成长连接。

2.4.1、如何将短连接变成长连接?

那就需要一个类org.springframework.web.servlet.mvc.method.annotation.SseEmitter。类在下面这个包里。

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
</dependency>
2.4.2、SseEmitter介绍

在这里插入图片描述

  • 通过Structure图可以看出SseEmitter继承父类ResponseBodyEnitter
  • 内部有有两个内部类:SessEventBuilder及其实现类SseEventBuilderImpl
  • 有两个构造方法,无参构造和有参构造。有参构造方法,入参超时时间,或应该称为重建间隔时间。
  • 另外提供了方法。sendevent,专门用于构造SseEventBuilder
  • 另外集成了父类的很多方法:getTimeoutinitializeextendResponsesendsendsendInternalcompletecompleteWithErroronTimeoutonCompletion,最后都用来构筑DeferredResultServerHttpResponse

DeferredResult熟悉吧,就是Ajax长轮循的那个返回类型。剩下的不用说了吧!

2.4.3、代码演示

好了不说,那么多了!前端代码不变,就不在贴代码了。咱们就直接上后端代码:

@Controller
public class SseController {
    private static Logger logger = LoggerFactory.getLogger(SseController.class);
    private static Map<String,SseEmitter> sseEmitters = new ConcurrentHashMap<>();

    @ResponseBody
    @RequestMapping(value="/payMoney")
    public SseEmitter pay(String weCharId){
        SseEmitter emitter = new SseEmitter();
        sseEmitters.put(weCharId,emitter);
        //加线程是为了查看,主线程挂起的那一步。实际研发中不需要哈
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        executorService.submit(new Pay(weCharId) );
        return emitter;
    }

    /**
     * 第二个方法,再发送一遍数据。就会发现这个数据,是从第一个方法的连接里出现的。
     * 调用演示结果,如下图所示
     */
    @ResponseBody
    @RequestMapping(value="/payMoney/2")
    public SseEmitter pay2(String weCharId) throws IOException {
        SseEmitter sseEmitter = sseEmitters.get(weCharId);
        sseEmitter.send("不告诉你");
        return sseEmitter;
    }

    private static class Pay implements Runnable{
        private String weCharId;

        public Pay(String weCharId) {
            this.weCharId = weCharId;
        }
        @Override
        public void run() {
            SseEmitter sseEmitter = sseEmitters.get(weCharId);
            try {
                logger.info("联系支付服务,准备扣款");
                Thread.sleep(5000);
                sseEmitter.send("支付完成");
                logger.info("准备通知自动售货机");
                sseEmitter.send("已通知自动售货机C9出货,请勿走开!");
                logger.info("已通知自动售货机C9出货,请勿走开!");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

在这里插入图片描述

我的写的比较简陋,推荐一个写的不错的博客,它里面的demo能直接用👉Springboot之整合SSE实现消息推送

3、WebSocket

3.1、简介

WebSocket,一种在2011 年被互联网工程任务组(IETF)标准化的协议。

WebSocket解决了一个长期存在的问题:既然底层的协议(HTTP)是一个请求/响应模式的交互序列,那么如何实时地发布信息呢?AJAX提供了一定程度上的改善,但是数据流仍然是由客户端所发送的请求驱动的。还有其他的一些或多或少的取巧方式(Comet)

WebSocket规范以及它的实现代表了对一种更加有效的解决方案的尝试。简单地说,WebSocket提供了在一个单个的TCP连接上提供双向的通信……结合WebSocket API……它为网页和远程服务器之间的双向通信提供了一种替代HTTP轮询的方案。

但是最终它们仍然属于扩展性受限的变通之法。也就是说,WebSocket 在客户端和服务器之间提供了真正的双向数据交换。WebSocket 连接允许客户端和服务器之间进行全双工通信,以便任一方都可以通过建立的连接将数据推送到另一端。WebSocket 只需要建立一次连接,就可以一直保持连接状态。这相比于轮询方式的不停建立连接显然效率要大大提高。

Web浏览器和服务器都必须实现 WebSockets协议来建立和维护连接。

3.2、websocket的协议

3.2.1、websocket协议的主要特点
  • 建立在TCP上:WebSocket协议使用单个TCP连接进行全双工通信,避免了HTTP协议中的多次连接建立和断开操作,从而减少了网络延迟和带宽消耗。
  • 双向通信:WebSocket协议支持双向通信,即客户端和服务器可以同时向对方发送和接收数据,实现更加灵活和高效的数据交换。
  • 实时性:WebSocket协议可以实现实时通信,支持消息推送、实时聊天等功能,为Web应用程序带来更好的用户体验。
  • 协议标准化:WebSocket协议已经被标准化,并且被广泛支持,几乎所有的现代浏览器都支持WebSocket协议。

但是请注意,WebSocket客户端并不是所有浏览器都支持。

3.2.2、websocket协议

关于websocket协议,为了方面,咱们下面将其简写为 ws协议

ws协议是一个应用层协议,它必须依赖与HTTP 协议,在客户端与服务端建立连接时,会进行一次握手,握手成功后,数据就直接从TCP通道传输,之后就与 HTTP 无关了。
即:ws协议分为握手阶段和数据传输阶段,即进行了HTTP握手 + 双工的TCP连接。

如何查看ws协议的请求?

如下图所示:
在这里插入图片描述

因为ws协议基于HTTP,所以在建立链接的时候,他的报文类似于HTTP,但是与HTTP有区别。

下面是ws协议的报文

#请求报文
GET ws://localhost:8080/websocket/server/10 HTTP/1.1
Host: localhost:8080
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36
Upgrade: websocket
Origin: http://localhost:8080
Sec-WebSocket-Version: 13
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=D5F34862481F77BAC9536EC20700B8EF
Sec-WebSocket-Key: cGin/ll09jup00nyYzrlwA==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
#响应报文
HTTP/1.1 101
Upgrade: websocket
Connection: upgrade
Sec-WebSocket-Accept: VSXBz7oACTKyKyrwEQthOcELfpc=
Sec-WebSocket-Extensions: permessage-deflate;client_max_window_bits=15
Date: Wed, 18 Oct 2023 06:58:59 GMT

ws协议的请求/响应报文上面两项首标是固定值:

  • Connection 一定是Upgrade,表示客户端希望连接升级。
  • Upgrade 一定是Websocket,表示希望升级到ws协议。而不是mozillasocketlurnarsocket或者shitsocket

另外还有其他的:

  • Sec-WebSocket-Extensions:可能会在HTTP请求中出现多次,但是在HTTP响应中只出现一次。用于从客户端到服务器的初次握手,然后用于从服务器到客户端的响应。这个首标帮助客户端和服务器商定一组链接期间使用的协议级扩展。
  • Sec-WebSocket-Key:只能在HTTP请求中出现一次。用于从客户端到服务器的websocket初始握手,避免跨协议攻击。
  • Sec-WebSocket-Accept:只能在HTTP请求中出现一次。用于从客户端到服务器的websocket初始握手,确认服务器理解ws协议。即确认服务器支持ws协议。
  • Sec-WebSocket-Version:用于从客户端到服务器的websocket初始握手,表示版本的兼容性。RFC 6455要求的版本是13,之前草案的版本均弃用。服务器如果不支持客户端请求的协议版本,则用这个首标响应。在那种情况下服务器会列出自己支持的版本。

如何确认服务器支持ws协议?

Sec-WebSocket-Key是一个随机的字符串,由客户端生成,并发给服务器端,服务器端在此字符串的基础上,拼接上固定字符串258EAFA5-E914-47DA-95CA-C5AB0DC85B11,然后计算 SHA-1摘要,之后进行 BASE-64编码,将结果做为Sec-WebSocket-Accept的值,然后返回给客户端。该值与客户端计算出来的值是精准匹配,表示服务端支持ws协议。如此操作,可以尽量避免普通 HTTP 请求被误。

大体算法如下:

// 用Node.js加密API计算键值和后缀组合的SHA1散列值
var KEY_SUFFIX = "258EAFAS-E914-47DA-95CA-C5AB0DC85B11";
var hashWebSocketKey = function(key) {
	var sha1 = crypto.createHash("sha1");
	sha1.update(key + KEY_SUFFIX, "ascii");
	return sha1.digest("base64");
}
// KEY_SUFFIX是一个协议规范中包含的固定键值后缀,每个WebSocket服务器都必须知道。

对于其他协议的支持和扩展

ws协议支持更高级的协议和协议协商。当商定使用其他协议的时候,在网络层,这些协议用Sec-WebSocket-Protocol首标协商。

协议名称在客户端发送初始升级请求时用首标值表示:Sec-WebSocket-Protocol: com.kaazing.echo, example.protocol.name 上述首标表示客户端可以使用任一个协议(com.kaazing.echoexample.protocol.name),服务器可以选择适用的协议。

如果你在对ws://echo.websocket.org发送的升级请求中发送了这个首标,那么服务器响应中将包含如下首标:Sec-WebSocket-Protocol: com.kaazing.echo。这个响应表示,服务器选择适用com.kaazing.echo协议。这些协议在ws协议之上的层次,为框架和应用程序提供更高级的语义。

连接的客户端发送一个Sec-WebSocket-Extensions首标,包含所支持的扩展名称。例如,Chrome可以发送如下首标,表明它将接受一个试验性的压缩扩展:Sec-WebSocket-Extensions: x-webkit-deflate-frame 扩展可以为帧格式添加新的操作码和数据字段。

部署新扩展比新协议更困难,因为浏览器供应商必须明确地为这些扩展提供支持。

关于报文的长度啊,以及其他内容,可以看一下这个WebSocket协议入门:WebSocket 协议

3.3、代码演示

导入核心包: 其他的不重要的依赖包就不发了

注意:springMVC和springboot集成websocket的时候,包不是一个。另外,咱们用的是springMVC做的demo

<!-springMVC-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-websocket</artifactId>
</dependency>

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

后端:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

@Configuration
public class WebSocketConfig {
    /**
     * 注入ServerEndpointExporter,
     * 这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;

/**
 * 接口路径 ws://localhost:8087/webSocket/server/{userId};
 *
 * @author nimige
 */
@Component
@Slf4j
@ServerEndpoint("/websocket/server/{userId}")
public class WebSocketServer {

    /**
     * concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
     * 虽然@Component默认是单例模式的,但springboot还是会为每个websocket连接初始化一个bean,所以可以用一个静态set保存起来。
     * 注:底下WebSocket是当前类名
     */
    private static CopyOnWriteArraySet<WebSocketServer> webSocketControllers = new CopyOnWriteArraySet<>();
    /**
     * 用来存在线连接用户信息
     */
    private static ConcurrentHashMap<String, Session> sessionPool = new ConcurrentHashMap<String, Session>();
    /**
     * 与某个客户端的连接会话,需要通过它来给客户端发送数据
     */
    private Session session;
    /**
     * 用户ID
     */
    private String userId;

    /**
     * 链接成功调用的方法
     */
    @OnOpen
    public void onOpen(Session session, @PathParam(value = "userId") String userId) {
        try {
            this.session = session;
            this.userId = userId;
            webSocketControllers.add(this);
            sessionPool.put(userId, session);
            log.info("【websocket消息】有新的连接,总数为:" + webSocketControllers.size());
        } catch (Exception e) {
        }
    }

    /**
     * 链接关闭调用的方法
     */
    @OnClose
    public void onClose() {
        try {
            webSocketControllers.remove(this);
            sessionPool.remove(this.userId);
            log.info("【websocket消息】连接断开,总数为:" + webSocketControllers.size());
        } catch (Exception e) {
        }
    }

    /**
     * 收到客户端消息后调用的方法
     *
     * @param message
     */
    @OnMessage
    public void onMessage(String message) {
        log.info("【websocket消息】收到客户端消息:" + message);
    }

    /**
     * 发送错误时的处理
     *
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error) {

        log.error("用户错误,原因:" + error.getMessage());
        error.printStackTrace();
    }

    /**
     * 此为广播消息
     *
     * @param message
     */
    public void sendAllMessage(String message) {
        log.info("【websocket消息】广播消息:" + message);
        for (WebSocketServer webSocketController : webSocketControllers) {
            try {
                if (webSocketController.session.isOpen()) {
                    webSocketController.session.getAsyncRemote().sendText(message);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 此为单点消息
     *
     * @param userId
     * @param message
     */
    public void sendOneMessage(String userId, String message) {
        Session session = sessionPool.get(userId);
        if (session != null && session.isOpen()) {
            try {
                log.info("【websocket消息】 单点消息:" + message);
                session.getAsyncRemote().sendText(message);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 此为单点消息(多人)
     *
     * @param userIds
     * @param message
     */
    public void sendMoreMessage(String[] userIds, String message) {
        for (String userId : userIds) {
            Session session = sessionPool.get(userId);
            if (session != null && session.isOpen()) {
                try {
                    log.info("【websocket消息】 单点消息:" + message);
                    session.getAsyncRemote().sendText(message);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }

    }
}

import com.example.springbootsse.config.WebSocketServer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;

import java.io.IOException;

/**
 * WebSocketController
 *
 * @author nimige
 */
@RestController
public class WebSocketController {
    @Autowired
    private WebSocketServer webSocketServer;
	
    @RequestMapping("/websocket")
    public String index(){
        return "websocket";
    }

    @RequestMapping("/push/{toUserId}")
    public ResponseEntity<String> pushToWeb(String message, @PathVariable String toUserId) {
        webSocketServer.sendOneMessage(toUserId, message);
        return ResponseEntity.ok("消息发送完成");
    }
}

前端:

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>websocket通讯</title>
</head>
<body>
<div><label for="userId">【 userId 】:</label><input id="userId" name="userId" type="text" value="10"/></div>
<div><label for="toUserId">【toUserId】:</label><input id="toUserId" name="toUserId" type="text" value="20"/></div>
<div><label for="contentText">【toUserId】:</label><input id="contentText" name="contentText" type="text"
                                                        value="hello websocket"/></div>
<label for="contentText">【操作】:</label>
<div>
    <button type="button" onclick="openSocket()">开启socket</button>
    <button type="button" onclick="sendMessage()">发送消息</button>
</div>
<label for="contentText">【接收到的消息】:</label>
<textarea id="receiveMessage"></textarea>
</body>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>
<script>
    var socket;

    function openSocket() {
        if (!!window.WebSocket) {
            if (socket != null) {
                socket.close();
                socket = null;
            }
            //实现化WebSocket对象,指定要连接的服务器地址与端口,建立连接
            var socketUrl = "http://localhost:8080/websocket/server/" + $("#userId").val();
            //更换协议
            socketUrl = socketUrl.replace("https", "ws").replace("http", "ws");
            console.log(socketUrl);
            socket = new WebSocket(socketUrl);
            socket.onopen = function () {
                console.log("websocket已打开");
            };
            socket.onmessage = function (msg) {
                console.log(msg.data);
                $("#receiveMessage").append("\n"+msg.data);
            };
            socket.onclose = function () {
                console.log("websocket已关闭");
            };
            socket.onerror = function () {
                console.log("websocket发生了错误");
            }
        } else {
            console.log("该浏览器不支持WebSocket");
        }
    }

    function sendMessage() {
        if (!!window.WebSocket) {
            console.log('{"toUserId":"' + $("#toUserId").val() + '","contentText":"' + $("#contentText").val() + '"}');
            socket.send('{"toUserId":"' + $("#toUserId").val() + '","contentText":"' + $("#contentText").val() + '"}');
        }else {
            console.log("您的浏览器不支持WebSocket");
        }
    }
</script>
</html>

测试结果

开启socket链接–>日志有内容,network有访问。再使用工具模拟后端发送数据,前端能接收到。
在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

杀戮苍生

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值