赞
踩
在HTTP协议中,所有的请求都是由客户端发起的,由服务端进行响应,服务端无法向客户端推送消息,但是在一些需要即时通信的应用中,又不可避免地需要服务端像客户端推送消息,传统的解决方案主要由如下几种:
轮询,就是客户端在固定的时间间隔下不停地向服务端发送请求,查看服务端是否由最新的数据,若服务端有最新的数据,则返回给客户端,若服务端没有,则返回一个空的JSON或者XML文档
。轮询对开发人员而言实现方便,但是弊端也是明显:客户端需要每次都要新建HTTP请求,服务端需要处理大量的无效请求,在高并发场景下会严重拖慢服务端的运行效率,同时服务端的资源被极大的浪费了。因此这种方式并不可取。在长轮询中,服务端不是每次都会立即响应客户端的请求,只有在服务端有最新数据的时候才会立即响应客户端的请求,否则服务端会持有这个请求而不返回,直到有服务端有最新数据时才返回
。这种方式可以在一定程度上节省网络资源和服务器资源,但是也存在一些问题,例如:
- 如果浏览器在服务器响应之前有新数据要发送,就只能创建一个新的并发请求,或者先尝试断掉当前请求,再创建新的请求。
- TCP和HTTP规范中都有连接超时一说,所以所谓的长轮询并不能一直持续,服务端和客户端的连接需要定期的连接和关闭再连接,这又增大开发人员的工作量,当然也有一些技术能够延长每次连接时间,但毕竟是非主流解决方案。
可以使用Applet和Flase来模拟比全双工通信,通过创建一个只有1个像素点大小的透明的Applet或者Flash,然后将之内嵌再网页中,再从Applet或者Flash的代码中创建一个Socket连接进行双向通信。这种连接方式消除了HTTP协议中的诸多限制,当服务器又消息发送到客户端的时候,开发者可以在Applet或者Flash中调用JavaScript函数将数据显示在页面上,当浏览器有数据要发送给服务器时也一样,通过Applet或者Flash来传递,这种方式真正地实现了全双工通信
,不过也有问题,说明如下:
- 浏览器必须能够运行Java或者Flash
2.无论是Applet还是Flash都存在安全问题
3.随着HTML5标准被各浏览器厂商广泛支持,Flash已经下架。
WebSocket
是一种在单个TCP连接上进行全双工通信的协议
,已被W3C定为标准。使用WebSocket可以使得客户端和服务器之间的数据交换变得更加简单,它允许服务器主动向客户端推送数据
。在WebSocket协议中,浏览器和服务器只需要完成一次握手,两者之间就可以直接创建持久性的连接,并进行双向数据传输
。
WebSocket
使用HTTP/1.1
的协议升级特性,一个WebSocket请求首先使用非正常的HTTP请求
以特定的模式访问一个URL,这个URL有两种模式,分别是ws和wss
,对应HTTP协议中的HTTP和HTTPS
,在请求头中有一个Connection:Upgrade
字段,表示客户端想要对应协议进行升级,另外还有一个Upgrade:websocket
字段,表示客户端想要将请求协议升级为WebSocket协议,这两个字段共同告诉服务器将连接升级为WebSocket这样一种全双工协议,如果服务端同意协议升级,那么在握手完成之后,文本消息或者其他二进制消息就可以同时在两个方向上进行发送,而不需要关闭和重建连接。此时的客户端和服务端关系是对等的,它们可以互相向对方主动发送消息。和传统的解决方案相比,WebSocket主要有如下特点:
WebSocket使用时需要先创建连接
,这使得WebSocket成为一种有状态的协议,在之后的通信过程中可以省略部分状态信息(例如身份认证等)。WebSocket连接在端口80(ws)或者443(wss)上创建
,与HTTP使用的端口相同,这样,基本上所有的防火墙都不会阻止WebSocket连接。WebSocket使用HTTP协议进行握手
,因此它可以自然而然地集成到网络浏览器和HTTP服务器中,而不需要额外的成本。心跳消息(ping 和 pong)
将被反复的发送,进而保持WebSocket连接一直处于活跃状态。Stomp
websocket使用socket实现双工异步通信能力
。但是如果直接使用websocket协议开发程序比较繁琐,我们可以使用它的子协议Stomp
SockJS
sockjs
是websocket协议的实现,增加了对浏览器不支持websocket的时候的兼容支持
SockJS的支持的传输的协议有3类: WebSocket, HTTP Streaming, and HTTP Long Polling。默认使用websocket,如果浏览器不支持websocket,则使用后两种的方式。
SockJS使用”Get /info”从服务端获取基本信息。然后客户端会决定使用哪种传输方式。如果浏览器使用websocket,则使用websocket。如果不能,则使用Http Streaming,如果还不行,则最后使用 HTTP Long Polling
STOMP作用:
提供消息体的格式,允许STOMP客户端(Endpoints)与任意STOMP消息代理(message broker)
进行交互,实现客户端之间进行异步消息传送
。
SpringBoot对WebSocket提供了非常友好的支持,可以方便开发者项目中快速集成WebSocket功能,实现单聊或者群聊。
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.6.3</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.cjw</groupId> <artifactId>websocket01</artifactId> <version>0.0.1-SNAPSHOT</version> <name>websocket01</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>webjars-locator-core</artifactId> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>sockjs-client</artifactId> <version>1.1.2</version> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>stomp-websocket</artifactId> <version>2.3.3</version> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>jquery</artifactId> <version>3.3.1</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
**spring-boot-starter-websocket
**依赖是WebSocket相关依赖,其他的都是前端库,使用jar包的形式对这些前端库进行统一管理,使用webjar添加到项目中的前端库
,在``SpringBoot项目中已经默认添加了静态资源过滤,因此可以直接使用。
STOMP支持
,STOMP是一个简单的可互操作的协议
,通常被用于通过中间服务器在客户端之间进行异步消息传递
。STOMP
即Simple (or Streaming) Text Orientated Messaging Protocol
,简单(流)文本定向消息协议,它提供了一个可互操作的连接格式,允许STOMP客户端与任意STOMP消息代理(Broker)进行交互。STOMP协议可以建立在WebSocket之上,也可以建立在其他应用层协议之上。public interface WebSocketMessageBrokerConfigurer { // 添加这个Endpoint,这样在网页中就可以通过websocket连接上服务,也就是我们配置websocket的服务地址,并且可以指定是否使用socketjs default void registerStompEndpoints(StompEndpointRegistry registry) { } // 配置发送与接收的消息参数,可以指定消息字节大小,缓存大小,发送超时时间 default void configureWebSocketTransport(WebSocketTransportRegistration registry) { } // 设置输入消息通道的线程数,默认线程为1,可以自己自定义线程数,最大线程数,线程存活时间 default void configureClientInboundChannel(ChannelRegistration registration) { } // 设置输出消息通道的线程数,默认线程为1,可以自己自定义线程数,最大线程数,线程存活时间 default void configureClientOutboundChannel(ChannelRegistration registration) { } // 自定义控制器方法的参数类型,有兴趣可以百度google HandlerMethodArgumentResolver这个的用法 default void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) { } // 自定义控制器方法返回值类型,有兴趣可以百度google HandlerMethodReturnValueHandler这个的用法 default void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> returnValueHandlers) { } // 添加自定义的消息转换器,spring 提供多种默认的消息转换器,返回false, //不会添加消息转换器,返回true,会添加默认的消息转换器, //当然也可以把自己写的消息转换器添加到转换链中 default boolean configureMessageConverters(List<MessageConverter> messageConverters) { return true; } // 配置消息代理,哪种路径的消息会进行代理处理 default void configureMessageBroker(MessageBrokerRegistry registry) { } }
服务端和浏览器的版本要求
WebSocket 服务端在各个主流应用服务器厂商中已基本获得符合 JEE JSR356 标准规范 API 的支持。当前支持websocket的版本:Tomcat 7.0.47+, Jetty 9.1+, GlassFish 4.1+, WebLogic 12.1.3+, and Undertow 1.0+ (and WildFly 8.0+).
浏览器的支持版本:
查看所有支持websocket浏览器的连接:
/** * 自定义类WebSocketConfig继承WebSocketMessageBrokerConfigurer * 进行WebSocket配置 * 通过@EnableWebSocketMessageBroker注解开启WebSocket消息代理 */ @Configuration //注解开启使用STOMP协议来传输基于代理(message broker)的消息 // 这时控制器支持使用@MessageMapping,就像使用@RequestMapping一样 @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { //配置消息代理(Message Broker) public void configureMessageBroker(MessageBrokerRegistry registry) { /* *设置消息代理前缀,即如果消息的前缀是“/topic”,就会将消息转发给消息代理(broker), *再由消息代理将消息广播给当前连接的客户端。(topic路径交给broker处理) * */ //点对点应配置一个/user消息代理,广播式应配置一个/topic消息代理 registry.enableSimpleBroker("/topic"); /*表示配置一个或者多个前缀,通过这些前缀过滤出需要被注解方法处理的消息 * 例如,前缀为“/app”的destination可以通过@MessageMapping注解的方法处理 * 而其他destination(例如“/topic” “/queue”)将被直接交给broker处理 * (app路径交给@MessageMapping注解的方法处理) * */ //点对点使用的订阅前缀(客户端订阅路径上会体现出来),不设置的话,默认也是/user/ registry.setApplicationDestinationPrefixes("/app"); } //注册STOMP协议的节点(endpoint),并映射指定的url public void registerStompEndpoints(StompEndpointRegistry registry) { /*表示定义一个前缀为"/chat"的endPoint,并开启sockjs支持 * sockjs可以解决浏览器对WebSocket的兼容性问题,客户端将通过 * 这里配置的URL建立WebSocket连接 * () * */ //注册一个STOMP的endpoint,并指定使用SockJS协议 registry.addEndpoint("/chat").withSockJS(); } }
@Controller public class GreetingController { /* * @MessageMapping("/hello")注解的方法将用来接收 * ”/app/hello“路径发送来的消息,在注解方法中对消息进行处理后, * 再将消息转发到@SendTo定义的路径上,而@SendTo路径 * 是一个前缀为“topic”的路径,因此该消息将被交给消息代理broker * 再由broker进行广播 * */ @MessageMapping("/hello") @SendTo("/topic/greetings") public Message greeting(Message message) { return message; } }
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>群聊</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> <script src="/app.js"></script> </head> <body> <div> <label for="name">请输入用户名:</label> <input id="name" placeholder="用户名"> </div> <div> <button id="connect" type="button">连接</button> <button id="disconnect" type="button" disabled>断开连接</button> </div> <div id="chat" style="display: none;"></div> <div> <label for="name">请输入聊天内容:</label> <input id="content" placeholder="聊天内容"> </div> <div id="greetings"> </div> <button id="send" type="button">发送</button> <div id="conversation" stype="display:none">聊天进行中。。。。</div> </body> </html>
var stompClient = null; function setConnected(connected) { $("#connect").prop("disabled", connected); $("#disconnect").prop("disabled", !connected); if (connected) { $("#conversation").show(); $("#chat").show(); } else { $("#conversation").hide(); $("#chat").hide(); } $("#greetings").html(""); } function connect() { console.log("connect-------------->start"); if (!$("#name").val()) { return ; } var socket = new SockJS('/chat'); stompClient = Stomp.over(socket); stompClient.connect({}, function (frame) { setConnected(true); stompClient.subscribe('/topic/greetings', function (greeting) { console.log("message========" + greeting); showGreeting(JSON.parse(greeting.body)); }); }); console.log("connect-------------->end"); } function disconnect() { console.log("disconnect-------------------> start"); if (stompClient !== null) { stompClient.disconnect(); } setConnected(false); console.log("disconnect-----------------> end"); } function sendName () { console.log("sendName--------------------->start"); stompClient.send("/app/hello", {}, JSON.stringify({'name': $("#name").val(), 'content':$("#content").val()})); console.log("sendName--------------------->end"); } function showGreeting(message) { $("#greetings").append( "<div>" + message.name + ":" + message.content + "</div>" ); } $(function () { $("#content").click(function () {connect();}); $("#disconnect").click(function () {disconnect()}); $("#send").click(function () {sendName();}); });
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
@Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Bean PasswordEncoder passwordEncoder() { BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(); String password = bCryptPasswordEncoder.encode("123"); System.out.println("password = " + password); return new BCryptPasswordEncoder(); } protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .anyRequest().authenticated() .and() .formLogin() .permitAll(); } protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("admin") .password("$2a$10$SUfihxUrF1gn4XoZuZygAuMb9VzT.n7n6Rh1kA1SlP5TXe4Kvxb8S") .roles("admin") .and() .withUser("sang") .password("$2a$10$SUfihxUrF1gn4XoZuZygAuMb9VzT.n7n6Rh1kA1SlP5TXe4Kvxb8S") .roles("user"); } }
/** * 自定义类WebSocketConfig继承WebSocketMessageBrokerConfigurer * 进行WebSocket配置 * 通过@EnableWebSocketMessageBroker注解开启WebSocket消息代理 */ @Configuration //注解开启使用STOMP协议来传输基于代理(message broker)的消息 // 这时控制器支持使用@MessageMapping,就像使用@RequestMapping一样 @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { //配置消息代理(Message Broker) public void configureMessageBroker(MessageBrokerRegistry registry) { /* *设置消息代理前缀,即如果消息的前缀是“/topic”,就会将消息转发给消息代理(broker), *再由消息代理将消息广播给当前连接的客户端。(topic路径交给broker处理) * */ //点对点应配置一个/user消息代理,广播式应配置一个/topic消息代理 /*为了方便群发消息和点对点发送消息添加了queue*/ registry.enableSimpleBroker("/topic1", "/queue"); /*表示配置一个或者多个前缀,通过这些前缀过滤出需要被注解方法处理的消息 * 例如,前缀为“/app”的destination可以通过@MessageMapping注解的方法处理 * 而其他destination(例如“/topic” “/queue”)将被直接交给broker处理 * (app路径交给@MessageMapping注解的方法处理) * */ //点对点使用的订阅前缀(客户端订阅路径上会体现出来),不设置的话,默认也是/user/ registry.setApplicationDestinationPrefixes("/app"); } //注册STOMP协议的节点(endpoint),并映射指定的url public void registerStompEndpoints(StompEndpointRegistry registry) { /*表示定义一个前缀为"/chat"的endPoint,并开启sockjs支持 * sockjs可以解决浏览器对WebSocket的兼容性问题,客户端将通过 * 这里配置的URL建立WebSocket连接 * () * */ //注册一个STOMP的endpoint,并指定使用SockJS协议 registry.addEndpoint("/chat").withSockJS(); } }
@Controller public class GreetingController { /* * SimpMessagingTemplate进行消息的发送,在SpringBoot中,已经配置好了 * 直接注入进来即可,使用SimpMessageTemplate, * 可以在任意地方发送消息到broker,也可以发送消息给某一个用户 * (点对点发送) * */ @Autowired SimpMessagingTemplate simpMessageingTemplate; /* * @MessageMapping("/hello")注解的方法将用来接收 * ”/app/hello“路径发送来的消息,在注解方法中对消息进行处理后, * 再将消息转发到@SendTo定义的路径上,而@SendTo路径 * 是一个前缀为“topic”的路径,因此该消息将被交给消息代理broker * 再由broker进行广播 * */ @MessageMapping("/hello") @SendTo("/topic1/greetings") public Message greeting(Message message) { return message; } /* * @MessageMapping("/chat")表示来自/app/chat的路径消息将被chat处理 * Principal获取登录信息 * Chat 客户端发送来的消息 * * */ @MessageMapping("/chat") public void chat (Principal principal, Chat chat) { /*获取当前用户信息*/ String from = principal.getName(); /*将消息存储进来*/ chat.setFrom(from); /*将消息发送出去*/ /* * * public void convertAndSendToUser(String user, String destination, Object payload, @Nullable Map<String, Object> headers, @Nullable MessagePostProcessor postProcessor) throws MessagingException { Assert.notNull(user, "User must not be null"); Assert.isTrue(!user.contains("%2F"), "Invalid sequence \"%2F\" in user name: " + user); user = StringUtils.replace(user, "/", "%2F"); destination = destination.startsWith("/") ? destination : "/" + destination; super.convertAndSend(this.destinationPrefix + user + destination, payload, headers, postProcessor); } * destinationPrefix:private String destinationPrefix = "/user/"; * * */ simpMessageingTemplate.convertAndSendToUser(chat.getTo(), "/queue/chat", chat); } public static class Chat { private String to; private String from; private String content; public String getTo() { return to; } public void setTo(String to) { this.to = to; } public String getFrom() { return from; } public void setFrom(String from) { this.from = from; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } } }
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>单聊</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> <script src="/chat.js"></script> </head> <body> <div id="chat"> <div id="chatsContent"> </div> <div> 请输入聊天内容: <input id="content" placeholder="聊天内容">目标用户: <input id="to" placeholder="目标用户"> <button id="send" type="button">发送</button> </div> </div> </body> </html>
var stompClient = null; function connect() { console.log("connetct--------------------->"); var socket = new SockJS('/chat'); stompClient = Stomp.over(socket); stompClient.connect({}, function (frame){ stompClient.subscribe('/user/queue/chat', function (chat) { console.log("chat---------------->"); console.log(chat); showGreeting(JSON.parse(chat.body)) }); }) } function sendMsg() { stompClient.send('/app/chat', {}, JSON.stringify({'content':$("#content").val(), 'to':$('#to').val()})); } function showGreeting(message) { console.log(message); $("#chatsContent").append("<div>" + message.from + ":" + message.content + "</div>"); } $(function() { connect(); $("#send").click(function () {sendMsg();}); })
经过SpringBoot自动化配置之后WebSocket使用起来还是非常方便的。通过@MessageMapping注解配置消息接口,通过@SendTo或者SimpMessagingTemplate进行消息转发,通过简单的配置,就能实现点对点、点对面的消息发送。一般在及时通信、通告发布等功能都会用到WebSocket。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。