1.什么是WebSocket?
WebSocket 是 HTML5 的一种新协议,不同于http协议,WebSocket实现了客户端与服务器端的全双工通信,即允许服务器端主动推送消息给客户端。
2.为什么需要WebSocket?
因为Http协议做不到。 选取了我认为比较好的解释,摘自廖雪峰的官方网站,如下:
为什么传统的HTTP协议不能做到WebSocket实现的功能?
这是因为HTTP协议是一个请求-响应协议,请求必须先由浏览器发给服务器,服务器才能响应这个请求,再把数据发送给浏览器。换句话说,浏览器不主动请求,服务器是没法主动发数据给浏览器的。
这样一来,要在浏览器中搞一个实时聊天,或者在线多人游戏的话就没法实现了,只能借助Flash这些插件。
也有人说,HTTP协议其实也能实现啊,比如用轮询或者Comet。轮询是指浏览器通过JavaScript启动一个定时器,然后以固定的间隔给服务器发请求,询问服务器有没有新消息。这个机制的缺点一是实时性不够,二是频繁的请求会给服务器带来极大的压力。
Comet本质上也是轮询,但是在没有消息的情况下,服务器先拖一段时间,等到有消息了再回复。这个机制暂时地解决了实时性问题,但是它带来了新的问题:以多线程模式运行的服务器会让大部分线程大部分时间都处于挂起状态,极大地浪费服务器资源。另外,一个HTTP连接在长时间没有数据传输的情况下,链路上的任何一个网关都可能关闭这个连接,而网关是我们不可控的,这就要求Comet连接必须定期发一些ping数据表示连接“正常工作”。
以上两种机制都治标不治本,所以,HTML5推出了WebSocket标准,让浏览器和服务器之间可以建立无限制的全双工通信,任何一方都可以主动发消息给对方。
3.WebSocket原理简析
-
WebSocket协议与STOMP协议
根据Spring-framework-reference的介绍,WebSocket协议定义了两种类型的消息(text and binary),但是这两种消息的content并没有定义。该协议定义了一种机制,供客户端和服务器协商在WebSocket之上使用子协议(即更高级别的消息协议),在子协议中定义可以发送的消息类型,格式,内容等。子协议的使用是可选的,但无论哪种方式,客户端和服务器都需要在使用哪种协议来定义消息内容上达成共识。
最常用的子协议是STOMP(Simple Text Oriented Messageing Protocol),即面向文本的简单协议。在STOMP协议中,客户端可以使用
SEND
或SUBSCRIBE
命令发送或订阅消息,以及描述消息含义和接收者的Destination
请求头。这启用了一种简单的发布-订阅机制,开发者可以使用该机制,通过代理将消息发送到其它已连接到服务器的客户端,或者将消息发送到服务器来请求执行某些操作。destination请求头内容通常是类似路径的字符串,其中/ topic / …表示发布-订阅(一对多),而/ queue /表示点对点(一对一)消息交流。
-
Spring中的WebSocket支持
在使用Spring的STOMP支持时,Spring WebSocket应用程序将充当客户端的STOMP代理,客户端发送的消息会被路由到@Controller消息处理方法中,该代理跟踪订阅用户,在消息更新时,向订阅的用户广播消息。
-
Spring支持整合专用的STOMP代理
Spring还支持为消息广播配置专用的STOMP代理(RabbitMQ,ActiveMQ等),在这种情况下,Spring起到维护与代理的TCP连接,将消息中继到该代理,并将消息从该代理向下传递到已连接的WebSocket客户端的作用。
-
消息流
- 启用简单内置消息代理时,使用的组件图:
上图显示了三个消息通道:
clientInboundChannel
:用于传递从WebSocket客户端收到的消息。clientOutboundChannel
:用于向WebSocket客户端发送服务器消息。brokerChannel
:用于从服务器端应用程序代码内将消息发送到消息代理。- 将外部代理(例如RabbitMQ)配置为用于管理订阅和广播消息时使用的组件图准备另起一篇博客来写
4.SpringBoot整合WebSocket实现简单Web群聊
光说不练假把式,记录下自己写的一个小demo。
-
创建项目,添加Maven依赖
由于是想要通过demo快速熟悉Springboot整合WebSocket,并没有进行前后端分离,前端采用html + jquery + SockJS + StompJS,所有JS依赖采用WebJars的方式引入。pom文件依赖如下:
<dependencies> <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> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>jquery</artifactId> <version>3.4.1</version> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>stomp-websocket</artifactId> <version>2.3.3-1</version> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>sockjs-client</artifactId> <version>1.1.2</version> </dependency> <!--Webjars版本定位工具(前端),用于省略版本号访问静态资源--> <dependency> <groupId>org.webjars</groupId> <artifactId>webjars-locator-core</artifactId> </dependency> </dependencies>
-
创建WebSocket配置类(重中之重,具体解释看代码注释)
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; /** * WebSocket配置类,实现WebSocketMessageBrokerConfigurer接口 */ @Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void configureMessageBroker(MessageBrokerRegistry registry) { // 使用内置的消息代理进行订阅和广播,并将destination请求头以/topic或/queue开头的消息路由到代理 registry.enableSimpleBroker("/topic","queue"); // destination请求头以/app开头的STOMP消息将路由到@Controller类中的@MessageMapping方法。 registry.setApplicationDestinationPrefixes("/app"); } @Override public void registerStompEndpoints(StompEndpointRegistry registry) { // chat是WebSocket(或SockJS)客户端为了进行WebSocket握手而需要连接的HTTP URL。 registry.addEndpoint("/chat").withSockJS(); } }
-
创建消息实体类
public class Message { private String name; private String content; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } }
-
创建消息控制器,响应web请求
import com.hwj.websocketgroupchat.entity.Message; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.SendTo; import org.springframework.stereotype.Controller; @Controller public class MessageController { /** * 浏览器将消息发送至/hello, * 服务端将消息在方法中处理完后,发送至/topic/**代理 * @param message 客户端发送的消息 * @return */ @MessageMapping("/hello") @SendTo("/topic/greetings") public Message greet(Message message) { return message; } }
-
HTML页面
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>WebSocket群聊</title> <script src="/webjars/jquery/jquery.min.js"></script> <script src="/webjars/sockjs-client/sockjs.min.js"></script> <script src="/webjars/stomp-websocket/stomp.min.js"></script> </head> <body> <table> <tr> <td>请输入用户名:</td> <td><input type="text" id="name"></td> </tr> <tr> <td><input type="button" id="connect" value="连接"></td> <td><input type="button" id="disconnect" disabled="disabled" value="断开连接"></td> </tr> </table> <div id="chat" style="display: none"> <table> <tr> <td>请输入聊天内容</td> <td><input type="text" id="content"></td> <td><input type="button" id="send" value="发送消息"></td> </tr> </table> <div id="conversation">群聊进行中......</div> </div> <script> $(function () { $("#connect").click(function () { connect(); }) $("#disconnect").click(function () { if (stompClient != null) { stompClient.disconnect(); } setConnect(false) }) $("#send").click(function () { stompClient.send('/app/hello', {}, JSON.stringify({ 'name': $("#name").val(), 'content': $("#content").val() })) $("#content").val("") }) }) var stompClient = null; function connect() { if (!$("#name").val()) { return; } var socket = new SockJS('/chat'); stompClient = Stomp.over(socket); stompClient.connect({}, function (success) { setConnect(true); stompClient.subscribe('/topic/greetings', function (msg) { showGreeting(JSON.parse(msg.body)); }) }); } function showGreeting(msg) { $("#conversation").append('<div>' + msg.name + ':' + msg.content + '</div>'); } function setConnect(flag) { $("#connect").prop("disabled", flag); $("#disconnect").prop("disabled", !flag); if (flag) { $("#chat").show(); } else { $("#chat").hide(); } } </script> </body> </html>
-
项目包结构
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Tmr0yyfA-1582081701463)(https://cdn.heyhwj.cn/blog/20200218/EEjB8xp070bs.jpg)]
-
实现效果
5.总结
通过英文文档来学习一个新的技术,可以对自己的理解起到很好的加深作用。 demo只用到了WebSocket技术的冰山一角,重点需要掌握的是关于WebSocket配置类配置项的编写,以及控制器类中注解的写法。