写点什么

SpringBoot- 技术专题 -Websocket 消息推送和广播消息推送

发布于: 2020 年 11 月 04 日
SpringBoot-技术专题-Websocket消息推送和广播消息推送

maven 依赖

        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-websocket</artifactId>        </dependency>
复制代码

代码准备

//webSocket相关配置
//链接地址
public static String WEBSOCKETPATHPERFIX = "/ws-push";
public static String WEBSOCKETPATH = "/endpointWisely";
//消息代理路径
public static String WEBSOCKETBROADCASTPATH = "/topic";
//前端发送给服务端请求地址
public static final String FORETOSERVERPATH = "/welcome";
//服务端生产地址,客户端订阅此地址以接收服务端生产的消息
public static final String PRODUCERPATH = "/topic/getResponse";
//点对点消息推送地址前缀
public static final String P2PPUSHBASEPATH = "/user";
//点对点消息推送地址后缀,最后的地址为/user/用户识别码/msg
public static final String P2PPUSHPATH = "/msg";
复制代码

接收前端消息实体

public class WiselyMessage {    private String name;
public String getName() { return name; }
public void setName(String name) { this.name = name; }}
复制代码

后台发送消息实体

private String responseMessage;    public WiselyResponse(String responseMessage){        this.responseMessage = responseMessage;    }    public String getResponseMessage() {        return responseMessage;    }    public void setResponseMessage(String responseMessage) {        this.responseMessage = responseMessage;    }}
复制代码

配置 websocket

@Configuration// @EnableWebSocketMessageBroker注解用于开启使用STOMP协议来传输基于代理(MessageBroker)的消息,这时候控制器(controller)// 开始支持@MessageMapping,就像是使用@requestMapping一样。@EnableWebSocketMessageBrokerpublic class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {    @Override    public void registerStompEndpoints(StompEndpointRegistry stompEndpointRegistry) {        //注册一个Stomp的节点(endpoint),并指定使用SockJS协议。        stompEndpointRegistry.addEndpoint(Constant.WEBSOCKETPATH).withSockJS();    }    @Override    public void configureMessageBroker(MessageBrokerRegistry registry) {        //服务端发送消息给客户端的域,多个用逗号隔开        registry.enableSimpleBroker(Constant.WEBSOCKETBROADCASTPATH, Constant.P2PPUSHBASEPATH);        //定义一对一推送的时候前缀        registry.setUserDestinationPrefix(Constant.P2PPUSHBASEPATH);        //定义websoket前缀        registry.setApplicationDestinationPrefixes(Constant.WEBSOCKETPATHPERFIX);    }}
复制代码

service

@Servicepublic class WebSocketService {
@Autowired private SimpMessagingTemplate template;
/** * 广播 * 发给所有在线用户 * * @param msg */ public void sendMsg(WiselyResponse msg) { template.convertAndSend(Constant.PRODUCERPATH, msg); }
/** * 发送给指定用户 * @param users * @param msg */ public void send2Users(List<String> users, WiselyResponse msg) { users.forEach(userName -> { template.convertAndSendToUser(userName, Constant.P2PPUSHPATH, msg); }); }}
复制代码

控制器

@Controllerpublic class WsController {
@Resource WebSocketService webSocketService;
@MessageMapping(Constant.FORETOSERVERPATH) //@MessageMapping和@RequestMapping功能类似,用于设置URL映射地址, 浏览器向服务器发起请求,需要通过该地址。 @SendTo(Constant.PRODUCERPATH) //如果服务器接受到了消息,就会对订阅了@SendTo括号中的地址传送消息。 public WiselyResponse say(WiselyMessage message) throws Exception { List<String> users = Lists.newArrayList(); users.add("d892bf12bf7d11e793b69c5c8e6f60fb"); //此处写死只是为了方便测试,此值需要对应页面中订阅个人消息的userId webSocketService.send2Users(users, new WiselyResponse("admin hello")); return new WiselyResponse("Welcome, " + message.getName() + "!"); }}
复制代码

页面

<!DOCTYPE html><html xmlns:th="http://www.thymeleaf.org"><head>    <meta charset="UTF-8" />    <title>Spring Boot+WebSocket+广播式</title>
</head><body onload="disconnect()"><noscript><h2 style="color: #ff0000">貌似你的浏览器不支持websocket</h2></noscript><div> <div> <button id="connect" onclick="connect();">连接</button> <button id="disconnect" disabled="disabled" onclick="disconnect();">断开连接</button> </div> <div id="conversationDiv"> <label>输入你的名字</label><input type="text" id="name" /> <button id="sendName" onclick="sendName();">发送</button> <p id="response"></p> <p id="response1"></p> </div></div><!--<script th:src="@{sockjs.min.js}"></script><script th:src="@{stomp.min.js}"></script><script th:src="@{jquery.js}"></script>--><script src="https://cdn.bootcss.com/sockjs-client/1.1.4/sockjs.min.js"></script><script src="https://cdn.bootcss.com/stomp.js/2.3.3/stomp.min.js"></script><script src="https://cdn.bootcss.com/jquery/3.2.1/jquery.min.js"></script><script th:inline="javascript"> var stompClient = null; //此值有服务端传递给前端,实现方式没有要求 var userId = [[${userId}]]; function setConnected(connected) { document.getElementById('connect').disabled = connected; document.getElementById('disconnect').disabled = !connected; document.getElementById('conversationDiv').style.visibility = connected ? 'visible' : 'hidden'; $('#response').html(); } function connect() { var socket = new SockJS('/endpointWisely'); //1连接SockJS的endpoint是“endpointWisely”,与后台代码中注册的endpoint要一样。 stompClient = Stomp.over(socket);//2创建STOMP协议的webSocket客户端。 stompClient.connect({}, function(frame) {//3连接webSocket的服务端。 setConnected(true); console.log('开始进行连接Connected: ' + frame); //4通过stompClient.subscribe()订阅服务器的目标是'/topic/getResponse'发送过来的地址,与@SendTo中的地址对应。 stompClient.subscribe('/topic/getResponse', function(respnose){ showResponse(JSON.parse(respnose.body).responseMessage); }); //4通过stompClient.subscribe()订阅服务器的目标是'/user/' + userId + '/msg'接收一对一的推送消息,其中userId由服务端传递过来,用于表示唯一的用户,通过此值将消息精确推送给一个用户 stompClient.subscribe('/user/' + userId + '/msg', function(respnose){ console.log(respnose); showResponse1(JSON.parse(respnose.body).responseMessage); }); }); } function disconnect() { if (stompClient != null) { stompClient.disconnect(); } setConnected(false); console.log("Disconnected"); }
function sendName() { var name = $('#name').val(); //通过stompClient.send()向地址为"/welcome"的服务器地址发起请求,与@MessageMapping里的地址对应。因为我们配置了registry.setApplicationDestinationPrefixes(Constant.WEBSOCKETPATHPERFIX);所以需要增加前缀/ws-push/ stompClient.send("/ws-push/welcome", {}, JSON.stringify({ 'name': name })); }
function showResponse(message) { var response = $("#response"); response.html(message); } function showResponse1(message) { var response = $("#response1"); response.html(message); }</script></body></html>
复制代码
  • 测试

clipboard.png


点击连接控制台输出


clipboard.png



表示连接成功并且订阅了两个地址
复制代码


clipboard.png


此时在文本框内输入任意值结果如图所示则代表成功
复制代码


clipboard.png


clipboard.png


控制台中显示依次为,发送信息,目标长度内容
复制代码


clipboard.png



此时服务端控制器接收到请求
复制代码


clipboard.png


同时给指定用户发送了消息,所以控制台接收到消息
复制代码


clipboard.png


同时因为控制器有注解@SendTo所以会向@SendTo的地址广播消息,客户端订阅了广播地址所有控制台显示接收了消息
复制代码


clipboard.png



WebSocket 是目前比较成熟的技术了,WebSocket 协议为创建客户端和服务器端需要实时双向通讯的 webapp 提供了一个选择。其为 HTML5 的一部分,WebSocket 相较于原来开发这类 app 的方法来说,其能使开发更加地简单。大部分现在的浏览器都支持 WebSocket,比如 Firefox,IE,Chrome,Safari,Opera,并且越来越多的服务器框架现在也同样支持 WebSocket。

在实际的生产环境中,要求多个 WebSocket 服务器必须具有高性能和高可用,那么 WebSocket 协议就需要一个负载均衡层,NGINX 从 1.3 版本开始支持 WebSocket,其可以作为一个反向代理和为 WebSocket 程序做负载均衡。

WebSocket 协议与 HTTP 协议不同,但 WebSocket 握手与 HTTP 兼容,使用 HTTP 升级工具将连接从 HTTP 升级到 WebSocket。这允许 WebSocket 应用程序更容易地适应现有的基础架构。例如,WebSocket 应用程序可以使用标准 HTTP 端口 80 和 443,从而允许使用现有的防火墙规则。

WebSocket 应用程序可以在客户端和服务器之间保持长时间运行的连接,从而有助于开发实时应用程序。用于将连接从 HTTP 升级到 WebSocket 的 HTTP 升级机制使用 Upgrade 和 Connection 头。反向代理服务器在支持 WebSocket 时面临一些挑战。一个是 WebSocket 是一个逐跳协议,因此当代理服务器拦截客户端的升级请求时,需要向后端服务器发送自己的升级请求,包括相应的头文件。此外,由于 WebSocket 连接长期存在,与 HTTP 使用的典型短期连接相反,反向代理需要允许这些连接保持打开状态,而不是关闭它们,因为它们似乎处于空闲状态。

允许在客户机和后端服务器之间建立隧道,NGINX 支持 WebSocket。对于 NGINX 将升级请求从客户端发送到后台服务器,必须明确设置 Upgrade 和 Connection 标题。

Nginx 开启 websocket 代理功能的配置如下:

1)编辑nginx.conf,在http区域内一定要添加下面配置:map $http_upgrade $connection_upgrade {    default upgrade;    '' close;} map指令的作用:   该作用主要是根据客户端请求中$http_upgrade 的值,来构造改变$connection_upgrade的值,即根据变量$http_upgrade的值创建新的变量$connection_upgrade,创建的规则就是{}里面的东西。其中的规则没有做匹配,因此使用默认的,即 $connection_upgrade 的值会一直是 upgrade。然后如果 $http_upgrade为空字符串的话,那值会是 close。  2)编辑vhosts下虚拟主机的配置文件,在location匹配配置中添加如下内容:proxy_http_version 1.1;proxy_set_header Upgrade $http_upgrade;proxy_set_header Connection "Upgrade"; 示例如下:upstream socket.kevin.com {    hash $remote_addr consistent;    server 10.0.12.108:9000;    server 10.0.12.109:9000;}  location / {            proxy_pass http://socket.kevin.com/;            proxy_set_header Host $host:$server_port;            proxy_http_version 1.1;            proxy_set_header Upgrade $http_upgrade;            proxy_set_header Connection "upgrade";}
复制代码

WebSocket 机制

WebSocket 是 HTML5 下一种新的协议。它实现了浏览器与服务器全双工通信,能更好的节省服务器资源和带宽并达到实时通讯的目的。它与 HTTP 一样通过已建立的 TCP 连接来传输数据,但是它和 HTTP 最大不同是:

1) WebSocket 是一种双向通信协议。在建立连接后,WebSocket 服务器端和客户端都能主动向对方发送或接收数据,就像 Socket 一样;

2)WebSocket 需要像 TCP 一样,先建立连接,连接成功后才能相互通信。

传统 HTTP 客户端与服务器请求响应模式如下图所示:



WebSocket 模式客户端与服务器请求响应模式如下图:

上图对比可以看出,相对于传统 HTTP 每次请求-应答都需要客户端与服务端建立连接的模式,

WebSocket 是类似 Socket 的 TCP 长连接通讯模式。一旦 WebSocket 连接建立后,后续数据都以帧序列的形式传输。

在客户端断开 WebSocket 连接或 Server 端中断连接前,不需要客户端和服务端重新发起连接请求。

海量并发及客户端与服务器交互负载流量大的情况下,极大的节省了网络带宽资源的消耗,有明显的性能优势,且客户端发送和接受消息是在同一个持久连接上发起,实时性优势明显。

相比 HTTP 长连接,WebSocket 有以下特点:

1)是真正的全双工方式,建立连接后客户端与服务器端是完全平等的,可以互相主动请求。而 HTTP 长连接基于 HTTP,是传统的客户端对服务器发起请求的模式。

2)HTTP 长连接中,每次数据交换除了真正的数据部分外,服务器和客户端还要大量交换 HTTP header,信息交换效率很低。Websocket 协议通过第一个 request 建立了 TCP 连接之后,之后交换的数据都不需要发送 HTTP header 就能交换数据,这显然和原有的 HTTP 协议有区别所以它需要对服务器和客户端都进行升级才能实现(主流浏览器都已支持 HTML5)。

此外还有 multiplexing、不同的 URL 可以复用同一个 WebSocket 连接等功能。这些都是 HTTP 长连接不能做到的。

WebSocket 与 Http 相同点

-  都是一样基于 TCP 的,都是可靠性传输协议。

-  都是应用层协议。

WebSocket 与 Http 不同点

-  WebSocket 是双向通信协议,模拟 Socket 协议,可以双向发送或接受信息。HTTP 是单向的。

-  WebSocket 是需要浏览器和服务器握手进行建立连接的。而 http 是浏览器发起向服务器的连接,服务

器预先并不知道这个连接。

WebSocket 与 Http 联系

WebSocket 在建立握手时,数据是通过 HTTP 传输的。但是建立之后,在真正传输时候是不需要 HTTP 协议的。

在 WebSocket 中,只需要服务器和浏览器通过 HTTP 协议进行一个握手的动作,然后单独建立一条 TCP 的通信通道进行数据的传送。

WebSocket 连接的过程

1)客户端发起 http 请求,经过 3 次握手后,建立起 TCP 连接;http 请求里存放 WebSocket 支持的版本号等信息,如:Upgrade、Connection、WebSocket-Version 等;

2)服务器收到客户端的握手请求后,同样采用 HTTP 协议回馈数据;

3)客户端收到连接成功的消息后,开始借助于 TCP 传输信道进行全双工通信。


下面再通过客户端和服务端交互的报文对比 WebSocket 通讯与传统 HTTP 的不同点:

1)在客户端,new WebSocket 实例化一个新的 WebSocket 客户端对象,请求类似 ws://yourdomain:port/path 的服务端 WebSocket URL,客户端 WebSocket 对象会自动解析并识别为 WebSocket 请求,并连接服务端端口,执行双方握手过程,客户端发送数据格式类似:


GET /webfin/websocket/

HTTP/1.1

Host: localhost

Upgrade: websocket

Connection: Upgrade

Sec-WebSocket-Key: xqBt3ImNzJbYqRINxEFlkg==

Origin: http://localhost:8080Sec-WebSocket-Version: 13


可以看到,客户端发起的 WebSocket 连接报文类似传统 HTTP 报文,Upgrade:websocket 参数值表明这是 WebSocket 类型请求,Sec-WebSocket-Key WebSocket 客户端发送的一个 base64 编码的密文,要求服务端必须返回一个对应加密的 Sec-WebSocket-Accept 应答,否则客户端会抛出 Error during WebSocket handshake 错误,并关闭连接。

2)服务端收到报文后返回的数据格式类似:


HTTP/1.1 101 Switching Protocols

Upgrade: websocket

Connection: Upgrade

Sec-WebSocket-Accept: K7DJLdLooIwIG/MOpvWFB3y3FE8=


Sec-WebSocket-Accept的值是服务端采用与客户端一致的密钥计算出来后返回客户端的,HTTP/1.1 101 Switching Protocols表示服务端接受 WebSocket 协议的客户端连接,经过这样的请求-响应处理后,两端的 WebSocket 连接握手成功, 后续就可以进行 TCP 通讯了。

在开发方面,WebSocket API 也十分简单:只需要实例化 WebSocket,创建连接,然后服务端和客户端就可以相互发送和响应消息。在 WebSocket 实现及案例分析部分可以看到详细的 WebSocket API 及代码实现。

腾讯云公网有日租类型七层负载均衡转发部分支持 Websocket,目前包括英魂之刃、银汉游戏等多家企业已接入使用。当出现不兼容问题时,请修改 websocket 配置,websocket server 不校验下图中圈出的字段:

比如一个使用 WebSocket 应用于视频的业务思路如下:

1)使用心跳维护 websocket 链路,探测客户端端的网红/主播是否在线

2)设置负载均衡 7 层的 proxy_read_timeout 默认为 60s

3)设置心跳为 50s,即可长期保持 Websocket 不断开

Nginx 代理 webSocket 经常中断的解决方法(也就是如何保持长连接)

现象描述:用 nginx 反代代理某个业务,发现平均 1 分钟左右,就会出现 webSocket 连接中断,然后查看了一下,是 nginx 出现的问题。

产生原因:nginx 等待第一次通讯和第二次通讯的时间差,超过了它设定的最大等待时间,简单来说就是超时!

解决方法 1

其实只要配置 nginx.conf 的对应 localhost 里面的这几个参数就好

proxy_connect_timeout;

proxy_read_timeout;

proxy_send_timeout;

解决方法 2

发心跳包,原理就是在有效地再读时间内进行通讯,重新刷新再读时间

配置示例:

http {    server {        location / {            root   html;            index  index.html index.htm;            proxy_pass http://webscoket;            proxy_http_version 1.1;            proxy_connect_timeout 4s;                #配置点1            proxy_read_timeout 60s;                  #配置点2,如果没效,可以考虑这个时间配置长一点            proxy_send_timeout 12s;                  #配置点3            proxy_set_header Upgrade $http_upgrade;             proxy_set_header Connection "Upgrade";          }    }}
复制代码

关于上面配置 2 的解释

这个是服务器对你等待最大的时间,也就是说当你 webSocket 使用 nginx 转发的时候,用上面的配置 2 来说,如果 60 秒内没有通讯,依然是会断开的,所以,你可以按照你的需求来设定

比如说,我设置了 10 分钟,那么如果我 10 分钟内有通讯,或者 10 分钟内有做心跳的话,是可以保持连接不中断的,详细看个人需求。

WebSocket 与 Socket 的关系

Socket 其实并不是一个协议,而是为了方便使用 TCP 或 UDP 而抽象出来的一层,是位于应用层和传输控制层之间的一组接口。当两台主机通信时,必须通过 Socket 连接,Socket 则利用 TCP/IP 协议建立 TCP 连接。TCP 连接则更依靠于底层的 IP 协议,IP 协议的连接则依赖于链路层等更低层次。

WebSocket 就像 HTTP 一样,则是一个典型的应用层协议。

总的来说:Socket 是传输控制层接口,WebSocket 是应用层协议。


用户头像

我们始于迷惘,终于更高的迷惘。 2020.03.25 加入

一个酷爱计算机技术、健身运动、悬疑推理的极客狂人,大力推荐安利Java官方文档:https://docs.oracle.com/javase/specs/index.html

评论

发布
暂无评论
SpringBoot-技术专题-Websocket消息推送和广播消息推送