006-优化web请求二-应用缓存、异步调用【Future、ListenableFuture、CompletableFuture】、ETag、WebSocket【SockJS、Stomp】
四、应用缓存
使用spring应用缓存。使用方式:使用@EnableCache注解激活Spring的缓存功能,需要创建一个CacheManager来处理缓存。如使用一个内存缓存示例
package com.github.bjlhx15.gradle.demotest; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.EnableCaching; import org.springframework.cache.concurrent.ConcurrentMapCache; import org.springframework.cache.support.SimpleCacheManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.Arrays; @Configuration @EnableCaching public class CacheConfiguration { @Bean public CacheManager cacheManager(){ SimpleCacheManager simpleCacheManager=new SimpleCacheManager(); simpleCacheManager.setCaches(Arrays.asList(new ConcurrentMapCache("searches"))); return simpleCacheManager; } }
其他实现如:EhCacheManager、GuavaCacheManager等
主要标记:
@CacheEvict:将会从缓存中移除一个条目
@CachePut:会将方法结果放到缓存中,而不会影响到方法调用本身。
@Caching:将缓存注解重新分组
@CacheConfig:指向不同的缓存配置
更多spring 应用缓存:https://www.cnblogs.com/bjlhx/category/1233985.html
五、分布式缓存
推荐使用redis,系列文章:https://www.cnblogs.com/bjlhx/category/1066467.html
spring使用也比较方便:https://www.cnblogs.com/bjlhx/category/1233985.html
六、异步方法-EnableAsync
在程序执行时候还有一个瓶颈,串行执行,可以通过使用不同线程类快速提升应用的速度。
要启用Spring的异步功能,必须要使用@EnableAsync注解。这样将会透明地使用java.util.concurrent.Executor来执行所有带有@Async注解的方法。
@Async所修饰的函数不要定义为static类型,这样异步调用不会生效
针对调用的Async,如果不做Future特殊处理,执行完调用方法会立即返回结果,如异步邮件发送,不会真的等邮件发送完毕才响应客户,如需等待可以使用Future阻塞处理。
6.1、原始使用
1、main方法增加@EnableAsync注解
@SpringBootApplication @EnableAsync public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); System.out.println("ThreadId:"+Thread.currentThread().getId()); } }
2、在所需方法增加@Async注解
@Component public class Task { @Async public void doTaskOne() throws Exception { for (int i = 0; i < 3; i++) { Thread.sleep(200); System.out.println("ThreadId:"+Thread.currentThread().getId()+":doTaskOne"); } } @Async public void doTaskTwo() throws Exception { for (int i = 0; i < 3; i++) { Thread.sleep(200); System.out.println("ThreadId:"+Thread.currentThread().getId()+":doTaskTwo"); } } @Async public void doTaskThree() throws Exception { for (int i = 0; i < 3; i++) { Thread.sleep(200); System.out.println("ThreadId:"+Thread.currentThread().getId()+":doTaskThree"); } } }
3、查看调用
@RestController public class TestAsyns { @Autowired private Task task; @RequestMapping("/testAsync") public ResponseEntity testAsync() throws Exception { task.doTaskOne(); task.doTaskTwo(); task.doTaskThree(); return ResponseEntity.ok("ok"); } }
上述方法依次调用三个方法。
如果去除@EnableAsync注解,输出如下:【可见是串行执行】
ThreadId:33:doTaskOne ThreadId:33:doTaskOne ThreadId:33:doTaskOne ThreadId:33:doTaskTwo ThreadId:33:doTaskTwo ThreadId:33:doTaskTwo ThreadId:33:doTaskThree ThreadId:33:doTaskThree ThreadId:33:doTaskThree
如果增加@EnableAsync注解,输出如下:【可见是并行执行】
ThreadId:56:doTaskThree ThreadId:55:doTaskTwo ThreadId:54:doTaskOne ThreadId:54:doTaskOne ThreadId:55:doTaskTwo ThreadId:56:doTaskThree ThreadId:54:doTaskOne ThreadId:56:doTaskThree ThreadId:55:doTaskTwo
6.2、自定义执行器使用异步
1、配置类
方式一、注入Bean方式
import java.util.concurrent.Executor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; @Configuration public class ThreadConfig { // 执行需要依赖线程池,这里就来配置一个线程池 @Bean public Executor getExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(5); executor.setMaxPoolSize(10); executor.setQueueCapacity(25); executor.initialize(); return executor; } }
方式二、通过实现AsyncConfigurer接口,可以自定义默认的执行(executor)。新增如下配置类:
@Configuration public class AsyncConfiguration implements AsyncConfigurer { protected final Logger logger = LoggerFactory.getLogger(AsyncConfiguration.class); @Override public Executor getAsyncExecutor() { //做好不超过10个,这里写两个方便测试 return Executors.newFixedThreadPool(2); } @Override public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { return (ex,method,params)->logger.error("Uncaught async error",ex); } }
Executor的初始化配置,还有很多种,可以参看https://www.cnblogs.com/bjlhx/category/1086008.html
Spring 已经实现的异步线程池:
1. SimpleAsyncTaskExecutor:不是真的线程池,这个类不重用线程,每次调用都会创建一个新的线程。
2. SyncTaskExecutor:这个类没有实现异步调用,只是一个同步操作。只适用于不需要多线程的地方
3. ConcurrentTaskExecutor:Executor的适配类,不推荐使用。如果ThreadPoolTaskExecutor不满足要求时,才用考虑使用这个类
4. SimpleThreadPoolTaskExecutor:是Quartz的SimpleThreadPool的类。线程池同时被quartz和非quartz使用,才需要使用此类
5. ThreadPoolTaskExecutor :最常使用,推荐。 其实质是对java.util.concurrent.ThreadPoolExecutor的包装
使用上述配置,能够确保在应用中,用来处理异步任务的线程不会超过10个。这对于web应用很重要,因为每个客户端都会有一个专用的线程。你所使用的线程越多,阻塞时间越长那么能够处理的客户端就会越少。
如果设置成两个,程序中有3个异步线程,也会只有两个运行,如下
ThreadId:55:doTaskTwo ThreadId:54:doTaskOne ThreadId:55:doTaskTwo ThreadId:54:doTaskOne ThreadId:55:doTaskTwo ThreadId:54:doTaskOne ThreadId:55:doTaskThree ThreadId:55:doTaskThree ThreadId:55:doTaskThree
2、使用
同上述一致。
6.3、异步返回处理
方式一、使用Future【FutureTask是默认实现】处理+轮询处理【jdk1.5产物,没有提供Callback机制,只能主动轮询,通过get去获取结果】【不推荐】
修改异步执行的方法
@Component public class TaskFutureDemo { @Async public Future<String> doTaskOne() throws Exception { for (int i = 0; i < 3; i++) { Thread.sleep(1000); System.out.println("ThreadId:"+Thread.currentThread().getId()+":doTaskOne"); } return new AsyncResult<>("doTaskOne"); } @Async public Future<String> doTaskTwo() throws Exception { for (int i = 0; i < 3; i++) { Thread.sleep(1000); System.out.println("ThreadId:"+Thread.currentThread().getId()+":doTaskTwo"); } return new AsyncResult<>("doTaskTwo"); } @Async public Future<String> doTaskThree() throws Exception { for (int i = 0; i < 3; i++) { Thread.sleep(1000); System.out.println("ThreadId:"+Thread.currentThread().getId()+":doTaskThree"); } return new AsyncResult<>("doTaskThree"); } }
修改调用的方法
@RestController public class TestAsynsFutureController { @Autowired private TaskFutureDemo task; @RequestMapping("/testAsyncFuture") public ResponseEntity testAsyncFuture() throws Exception { Future<String> taskOne = task.doTaskOne(); Future<String> taskTwo = task.doTaskTwo(); Future<String> taskThree = task.doTaskThree(); while (true) { if (taskOne.isDone() && taskTwo.isDone() && taskThree.isDone()) { break; } } return ResponseEntity.ok("ok"); } }
方式二、Spring的ListenableFuture和CountDownLatch处理
Service实现类
@Service public class TaskListenableFutureService { private AsyncSearch asyncSearch; @Autowired public TaskListenableFutureService(AsyncSearch asyncSearch) { this.asyncSearch=asyncSearch; } public List<String> search(List<String> keywords){ CountDownLatch latch=new CountDownLatch(keywords.size()); List<String> allResult=Collections.synchronizedList(new ArrayList<>()); keywords.stream() .forEach(keyword->asyncFetch(latch,allResult,keyword)); await(latch); return allResult; } private void asyncFetch(CountDownLatch latch, List<String> result, String keyword){ asyncSearch.asyncFetch(keyword) .addCallback( key->onSuccess(result,latch,key), ex -> onError(latch,ex) ); } private void await(CountDownLatch latch){ try { latch.await(); } catch (InterruptedException e) { throw new IllegalStateException(e); } } private static void onSuccess(List<String> result,CountDownLatch latch,String keyword){ result.add(keyword); latch.countDown(); } private static void onError(CountDownLatch latch,Throwable ex){ ex.printStackTrace(); latch.countDown(); } @Component private static class AsyncSearch{ @Autowired public AsyncSearch() { } protected final Logger logger = LoggerFactory.getLogger(AsyncSearch.class); @Async public ListenableFuture<String> asyncFetch(String keyword){ logger.info(Thread.currentThread().getName()+"-"+keyword); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } return new AsyncResult<>("keyword="+keyword); } } }
调用方法
@RestController public class TestAsynsListenableFutureController { protected final Logger logger = LoggerFactory.getLogger(TestAsynsListenableFutureController.class); @Autowired private TaskListenableFutureService task; @RequestMapping("/testAsyncListenableFuture") public ResponseEntity testAsyncListenableFuture() throws Exception { List<String> list = task.search(Arrays.asList("java", "html", "spring")); list.stream().forEach(p-> logger.info(p)); return ResponseEntity.ok("ok"); } }
方式三、使用CompletableFuture【推荐】
User实体类
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; @JsonIgnoreProperties(ignoreUnknown=true) public class User { private String name; private String blog; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getBlog() { return blog; } public void setBlog(String blog) { this.blog = blog; } @Override public String toString() { return "User [name=" + name + ", blog=" + blog + "]"; } }
CompletableFuture的服务
@Service public class GitHubLookupService { private static final Logger logger = LoggerFactory.getLogger(GitHubLookupService.class); private final RestTemplate restTemplate; public GitHubLookupService(RestTemplateBuilder restTemplateBuilder) { this.restTemplate = restTemplateBuilder.build(); } @Async public CompletableFuture<User> findUser(String user) throws InterruptedException { logger.info("Looking up " + user); String url = String.format("https://api.github.com/users/%s", user); User results = restTemplate.getForObject(url, User.class); // Artificial delay of 1s for demonstration purposes Thread.sleep(1000L); return CompletableFuture.completedFuture(results); } }
调用方法
@RestController public class CompletableFutureController { private static final Logger logger = LoggerFactory.getLogger(CompletableFutureController.class); @Autowired private GitHubLookupService gitHubLookupService; @RequestMapping("/testCompletableFuture") public ResponseEntity testCompletableFuture() throws Exception { // Start the clock long start = System.currentTimeMillis(); // Kick of multiple, asynchronous lookups CompletableFuture<User> page1 = gitHubLookupService.findUser("PivotalSoftware"); CompletableFuture<User> page2 = gitHubLookupService.findUser("CloudFoundry"); CompletableFuture<User> page3 = gitHubLookupService.findUser("Spring-Projects"); // Wait until they are all done CompletableFuture.allOf(page1,page2,page3).join(); // Print results, including elapsed time logger.info("Elapsed time: " + (System.currentTimeMillis() - start)); logger.info("--> " + page1.get()); logger.info("--> " + page2.get()); logger.info("--> " + page3.get()); return ResponseEntity.ok("ok"); } }
七、ETag
7.1、什么是ETag?
ETag:是实体标签(Entity Tag)的缩写。ETag一般不以明文形式相应给客户端。在资源的各个生命周期中,它都具有不同的值,用于标识出资源的状态。当资源发生变更时,如果其头信息中一个或者多个发生变化,或者消息实体发生变化,那么ETag也随之发生变化。
ETag值的变更说明资源状态已经被修改。往往可以通过时间戳就可以便宜的得到ETag头信息。在服务端中如果发回给消费者的相应从一开始起就由ETag控制,那么可以确保更细粒度的ETag升级完全由服务来进行控制。服务计算ETag值,并在相应客户端请求时将它返回给客户端。
7.2、计算ETag值
在HTTP1.1协议中并没有规范如何计算ETag。ETag值可以是唯一标识资源的任何东西,如持久化存储中的某个资源关联的版本、一个或者多个文件属性,实体头信息和校验值、(CheckSum),也可以计算实体信息的散列值。有时候,为了计算一个ETag值可能有比较大的代价,此时可以采用生成唯一值等方式(如常见的GUID)。无论怎样,服务都应该尽可能的将ETag值返回给客户端。客户端不用关心ETag值如何产生,只要服务在资源状态发生变更的情况下将ETag值发送给它就行。
ETag值可以通过uuid、整数、长整形、字符串等四种类型。
计算ETag值时,需要考虑两个问题:计算与存储。如果一个ETag值只需要很小的代价以及占用很低的存储空间,那么我们可以在每次需要发送给客户端ETag值值的时候计算一遍就行行了。相反的,我们需要将之前就已经计算并存储好的ETag值发送给客户端。之前说:将时间戳作为字符串作为一种廉价的方式来获取ETag值。对于不是经常变化的消息,它是一种足够好的方案。注意:如果将时间戳做为ETag值,通常不应该用Last-Modified的值。由于HTTP机制中,所以当我们在通过服务校验资源状态时,客户端不需要进行相应的改动。计算ETag值开销最大的一般是计算采用哈希算法获取资源的表述值。可以只计算资源的哈希值,也可以将头信息和头信息的值也包含进去。如果包含头信息,那么注意不要包含计算机标识的头信息。同样也应该避免包含Expires、Cache-Control和Vary头信息。注意:在通过哈希算法。
7.3、ETag的类型以及他们之间的区别
ETag有两种类型:强ETag(strong ETag)与弱ETag(weak ETag)。
强ETag表示形式:"22FAA065-2664-4197-9C5E-C92EA03D0A16"。
弱ETag表现形式:w/"22FAA065-2664-4197-9C5E-C92EA03D0A16"。
强、弱ETag类型的出现与Apache服务器计算ETag的方式有关。Apache默认通过FileEtag中FileEtag INode Mtime Size的配置自动生成ETag(当然也可以通过用户自定义的方式)。假设服务端的资源频繁被修改(如1秒内修改了N次),此时如果有用户将Apache的配置改为MTime,由于MTime只能精确到秒,那么就可以避免强ETag在1秒内的ETag总是不同而频繁刷新Cache(如果资源在秒级经常被修改,也可以通过Last-Modified来解决)。
7.4、Etag - 作用
Etag 主要为了解决 Last-Modified 无法解决的一些问题。
1、 一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新GET;
2、某些文件修改非常频繁,比如在秒以下的时间内进行修改,(比方说1s内修改了N次),If-Modified-Since能检查到的粒度是s级的,这种修改无法判断(或者说UNIX记录MTIME只能精确到秒
3、某些服务器不能精确的得到文件的最后修改时间;
为此,HTTP/1.1 引入了 Etag(Entity Tags).Etag仅仅是一个和文件相关的标记,可以是一个版本标记,比如说v1.0.0或者说"2e681a-6-5d044840"这么一串看起来很神秘的编码。但是HTTP/1.1标准并没有规定Etag的内容是什么或者说要怎么实现,唯一规定的是Etag需要放在""内。
7.5、Etag - 工作原理
Etag由服务器端生成,客户端通过If-Match或者说If-None-Match这个条件判断请求来验证资源是否修改。常见的是使用If-None-Match.请求一个文件的流程可能如下:
====第一次请求===
1.客户端发起 HTTP GET 请求一个文件;
2.服务器处理请求,返回文件内容和一堆Header,当然包括Etag(例如"2e681a-6-5d044840")(假设服务器支持Etag生成和已经开启了Etag).状态码200
====第二次请求===
1.客户端发起 HTTP GET 请求一个文件,注意这个时候客户端同时发送一个If-None-Match头,这个头的内容就是第一次请求时服务器返回的Etag:2e681a-6-5d044840
2.服务器判断发送过来的Etag和计算出来的Etag匹配,因此If-None-Match为False,不返回200,返回304,客户端继续使用本地缓存;
流程很简单,问题是,如果服务器又设置了Cache-Control:max-age和Expires呢,怎么办?
答案是同时使用,也就是说在完全匹配If-Modified-Since和If-None-Match即检查完修改时间和Etag之后,服务器才能返回304.
7.6、在spring中实践
虽然对请求已经做了应用缓存等处理,但是持续请求一个restful接口请求还是会发送到服务端去读取缓存,即使结果没有发生改变,但结果本身还是会多次发送给用户,造成浪费带宽。
ETag是Web响应数据的一个散列(Hash),并且会在头信息中进行发送。客户端可以记住资源的ETag,并且通过If-None-Match头信息将最新的已知版本发送给服务器。如果在这段时间内请求没有发生变化的话,服务器就会返回304 Not Modified。
在Spring中有一个特殊的Servlet过滤器来处理ETag,名为ShallowEtagHeaderFilter。只需将此类注入即可:
@Bean public Filter etagFilter(){ return new ShallowEtagHeaderFilter(); }
只要响应头没有缓存控制头信息的话,系统就会为你的响应生成ETag。
示例
@GetMapping("/testNoChangeContent") public ResponseEntity testNoChangeContent(){ return ResponseEntity.ok("OK"); } @GetMapping("/testChangeContent") public ResponseEntity testChangeContent(){ return ResponseEntity.ok("OK:"+LocalDateTime.now()); }
接口一、testNoChangeContent,是测试内容没有改变的,第一次请求是200,以后请求是304
接口二、testChangeContent,是测试内容有改变的,第一次请求是200,以后请求均是200
八、WebSocket
在优化web请求时,这是一种优化方案,在服务器端有可用数据时,就立即将其发送到客户端。通过多线程方式获取搜索结果,所以数据会分为多个块。这时可以一点点地进行发送,而不必等待所有结果。
8.1、概述
1、WebSocket
Http连接为一次请求(request)一次响应(response),必须为同步调用方式。WebSocket 协议提供了通过一个套接字实现全双工通信的功能。一次连接以后,会建立tcp连接,后续客户端与服务器交互为全双工方式的交互方式,客户端可以发送消息到服务端,服务端也可将消息发送给客户端。
WebSocket 是发送和接收消息的底层API,WebSocket 协议提供了通过一个套接字实现全双工通信的功能。也能够实现 web 浏览器和 server 间的异步通信,全双工意味着 server 与浏览器间可以发送和接收消息。需要注意的是必须考虑浏览器是否支持。
2、SockJS
SockJS 是 WebSocket 技术的一种模拟。为了应对许多浏览器不支持WebSocket协议的问题,设计了备选SockJs。开启并使用SockJS后,它会优先选用Websocket协议作为传输协议,如果浏览器不支持Websocket协议,则会在其他方案中,选择一个较好的协议进行通讯。原来在不支持WebSocket的情况下,也可以很简单地实现WebSocket的功能的,方法就是使用 SockJS。它会优先选择WebSocket进行连接,但是当服务器或客户端不支持WebSocket时,会自动在 XHR流、XDR流、iFrame事件源、iFrame HTML文件、XHR轮询、XDR轮询、iFrame XHR轮询、JSONP轮询 这几个方案中择优进行连接。
3、Stomp
STOMP 中文为: 面向消息的简单文本协议。websocket定义了两种传输信息类型: 文本信息和二进制信息。类型虽然被确定,但是他们的传输体是没有规定的。所以,需要用一种简单的文本传输类型来规定传输内容,它可以作为通讯中的文本传输协议,即交互中的高级协议来定义交互信息。
STOMP本身可以支持流类型的网络传输协议: websocket协议和tcp协议。
Stomp还提供了一个stomp.js,用于浏览器客户端使用STOMP消息协议传输的js库。
STOMP的优点如下:
(1)不需要自建一套自定义的消息格式
(2)现有stomp.js客户端(浏览器中使用)可以直接使用
(3)能路由信息到指定消息地点
(4)可以直接使用成熟的STOMP代理进行广播 如:RabbitMQ, ActiveMQ
4、WebSocket、SockJs、STOMP三者关系
简而言之,WebSocket 是底层协议,SockJS 是WebSocket 的备选方案,也是 底层协议,而 STOMP 是基于 WebSocket(SockJS) 的上层协议
- 假设HTTP协议并不存在,只能使用TCP套接字来编写web应用,你可能认为这是一件疯狂的事情。
- 不过幸好,我们有HTTP协议,它解决了 web 浏览器发起请求以及 web 服务器响应请求的细节。
- 直接使用 WebSocket(SockJS) 就很类似于 使用 TCP 套接字来编写 web 应用;因为没有高层协议,因此就需要我们定义应用间所发送消息的语义,还需要确保 连接的两端都能遵循这些语义。
- 同HTTP在TCP套接字上添加请求-响应模型层一样,STOMP在 WebSocket之上提供了一个基于帧的线路格式层,用来定义消息语义。
8.2、不支持WebSocket的场景有:
浏览器不支持
Web容器不支持,如tomcat7以前的版本不支持WebSocket
防火墙不允许
Nginx没有开启WebSocket支持
当遇到不支持WebSocket的情况时,SockJS会尝试使用其他的方案来连接,刚开始打开的时候因为需要尝试各种方案,所以会阻塞一会儿,之后可以看到连接有异常,那就是尝试失败的情况。
为了测试,使用Nginx做反向代理,把www.test.com指到项目启动的端口上,然后本地配HOST来达到模拟真实场景的效果。因为Nginx默认是不支持WebSocket的,所以这里模拟出了服务器不支持WebSocket的场景。、
8.3、spring下的WebSocket使用【WebSocket→sockJs→stomp】
项目中使用的pom
compile 'org.springframework.boot:spring-boot-starter-websocket' compile 'org.springframework.boot:spring-messaging' compile group: 'org.webjars', name: 'sockjs-client', version: '1.1.2' compile group: 'org.webjars', name: 'stomp-websocket', version: '2.3.3' compile group: 'org.webjars', name: 'jquery', version: '3.3.1-1'
客户端JS
<script src="/webjars/jquery/3.3.1-1/jquery.js"></script> <script src="/webjars/sockjs-client/1.1.2/sockjs.min.js"></script> <script src="/webjars/stomp-websocket/2.3.3/stomp.min.js"></script> <script src="test.js"></script>
8.3.1、使用Spring的底层级WebSocket API
Spring为WebSocket提供了良好支持,WebSocket协议允许客户端维持与服务器的长连接。数据可以通过WebSocket在这两个端点之间进行双向传输,因此消费数据的一方能够实时获取数据。
按照其最简单的形式,WebSocket只是两个应用之间通信的通道。位于WebSocket一端的应用发送消息,另外一端处理消息。因为它是全双工的,所以每一端都可以发送和处理消息。如图18.1所示。
WebSocket通信可以应用于任何类型的应用中,但是WebSocket最常见的应用场景是实现服务器和基于浏览器的应用之间的通信。
实现步骤:
1、编写Handler消息处理器类
方法一:实现 WebSocketHandler 接口,WebSocketHandler 接口如下
public interface WebSocketHandler { void afterConnectionEstablished(WebSocketSession session) throws Exception; void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception; void handleTransportError(WebSocketSession session, Throwable exception) throws Exception; void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception; boolean supportsPartialMessages(); }
方法二:扩展 AbstractWebSocketHandler
@Service public class ChatHandler extends AbstractWebSocketHandler { @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { session.sendMessage(new TextMessage("hello world.")); } }
为了在Spring使用较低层级的API来处理消息,必须编写一个实现WebSocketHandler的类.WebSocketHandler需要我们实现五个方法。相比直接实现WebSocketHandler,更为简单的方法是扩展AbstractWebSocketHandler,这是WebSocketHandler的一个抽象实现。
除了重载WebSocketHandler中所定义的五个方法以外,我们还可以重载AbstractWebSocketHandler中所定义的三个方法:
- handleBinaryMessage()
- handlePongMessage()
- handleTextMessage()
这三个方法只是handleMessage()方法的具体化,每个方法对应于某一种特定类型的消息。
方案三、扩展TextWebSocketHandler或BinaryWebSocketHandler。
TextWebSocketHandler是AbstractWebSocketHandler的子类,它会拒绝处理二进制消息。它重载了handleBinaryMessage()方法,如果收到二进制消息的时候,将会关闭WebSocket连接。与之类似,BinaryWebSocketHandler也是AbstractWeb-SocketHandler的子类,它重载了handleTextMessage()方法,如果接收到文本消息的话,将会关闭连接。
2、增加websocket拦截器,管理用户
@Component public class WebSocketHandshakeInterceptor implements HandshakeInterceptor { @Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception { if (request instanceof ServletServerHttpRequest) { attributes.put("username","lhx"); } return true; } @Override public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) { } }
- beforeHandshake()方法,在调用 handler 前调用。常用来注册用户信息,绑定 WebSocketSession,在 handler 里根据用户信息获取WebSocketSession发送消息
3、WebSocketConfig配置
方式一、注解配置
@Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Autowired private ChatHandler chatHandler; @Autowired private WebSocketHandshakeInterceptor webSocketHandshakeInterceptor; @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(chatHandler,"/chat") .addInterceptors(webSocketHandshakeInterceptor); } @Bean public ServletServerContainerFactoryBean createWebSocketContainer() { ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean(); container.setMaxTextMessageBufferSize(8192*4); container.setMaxBinaryMessageBufferSize(8192*4); return container; } }
- 实现 WebSocketConfigurer 接口,重写 registerWebSocketHandlers 方法,这是一个核心实现方法,配置 websocket 入口,允许访问的域、注册 Handler、SockJs 支持和拦截器。
- registry.addHandler()注册和路由的功能,当客户端发起 websocket 连接,把 /path 交给对应的 handler 处理,而不实现具体的业务逻辑,可以理解为收集和任务分发中心。
- addInterceptors,顾名思义就是为 handler 添加拦截器,可以在调用 handler 前后加入我们自己的逻辑代码。
- ServletServerContainerFactoryBean可以添加对WebSocket的一些配置
方式二、xml配置
4、客户端配置
function contect() { var wsServer = 'ws://'+window.location.host+'/chat'; var websocket = new WebSocket(wsServer); websocket.onopen = function (evt) { onOpen(evt) }; websocket.onclose = function (evt) { onClose(evt) }; websocket.onmessage = function (evt) { onMessage(evt) }; websocket.onerror = function (evt) { onError(evt) }; function onOpen(evt) { console.log("Connected to WebSocket server."); websocket.send("test");//客户端向服务器发送消息 } function onClose(evt) { console.log("Disconnected"); } function onMessage(evt) { console.log('Retrieved data from server: ' + evt.data); } function onError(evt) { console.log('Error occured: ' + evt.data); } } contect();
8.3.2、SockJs针对WebSocket支持稍差的场景
为了应对许多浏览器不支持WebSocket协议的问题,设计了备选SockJs
。
SockJS 是 WebSocket 技术的一种模拟。SockJS 会 尽可能对应 WebSocket API,但如果 WebSocket 技术不可用的话,就会选择另外的通信方式协议。
1、服务端只需增加:.withSockJS()即可
@Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Autowired private ChatHandler chatHandler; @Autowired private WebSocketHandshakeInterceptor webSocketHandshakeInterceptor; @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { // withSockJS() 方法声明我们想要使用 SockJS 功能,如果WebSocket不可用的话,会使用 SockJS; registry.addHandler(chatHandler,"/chat") .addInterceptors(webSocketHandshakeInterceptor).withSockJS(); } }
或者xml配置
<websocket:sockjs />
2、客户端
只需对请求
// var server = 'ws://'+window.location.host+'/chat'; var server = 'http://'+window.location.host+'/chatsockjs'; var websocket = new SockJS(server);
- SockJS 所处理的 URL 是 “http://“ 或 “https://“ 模式,而不是 “ws://“ or “wss://“;
- 其他的函数如 onopen, onmessage, and onclose ,SockJS 客户端与 WebSocket 一样,
8.3.3、Stomp方式
STOMP帧由命令,一个或多个头信息以及负载所组成。
直接使用WebSocket(或SockJS)就很类似于使用TCP套接字来编写Web应用。因为没有高层级的线路协议(wire protocol),因此就需要我们定义应用之间所发送消息的语义,还需要确保连接的两端都能遵循这些语义。
不过,好消息是我们并非必须要使用原生的WebSocket连接。就像HTTP在TCP套接字之上添加了请求-响应模型层一样,STOMP在WebSocket之上提供了一个基于帧的线路格式(frame-based wire format)层,用来定义消息的语义。
乍看上去,STOMP的消息格式非常类似于HTTP请求的结构。与HTTP请求和响应类似,STOMP帧由命令、一个或多个头信息以及负载所组成。例如,如下就是发送数据的一个STOMP帧:
SEND destination:/app/room-message content-length:20 {\"message\":\"Hello!\"}
对以上代码分析:
- SEND:STOMP命令,表明会发送一些内容;
- destination:头信息,用来表示消息发送到哪里;
- content-length:头信息,用来表示 负载内容的 大小;
- 空行;
- 帧内容(负载)内容
8.3.3.1、基本用法
1、服务端Configuration配置
@Configuration @EnableWebSocketMessageBroker public class WebSocketConfiguration implements WebSocketMessageBrokerConfigurer { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { //添加一个/socket-server-point 连接端点,客户端就可以通过这个端点来进行连接;withSockJS作用是添加SockJS支持 registry.addEndpoint("/socket-server-point").withSockJS(); } @Override public void configureMessageBroker(MessageBrokerRegistry registry) { //定义了一个客户端订阅地址的前缀信息,也就是客户端接收服务端发送消息的前缀信息 registry.enableSimpleBroker("/topic"); //定义了服务端接收地址的前缀,也即客户端给服务端发消息的地址前缀 registry.setApplicationDestinationPrefixes("/ws"); } }
对以上代码分析:
- EnableWebSocketMessageBroker 注解表明: 这个配置类不仅配置了 WebSocket,还配置了基于代理的 STOMP 消息;
- 它复写了 registerStompEndpoints() 方法:添加一个服务端点,来接收客户端的连接。将 “/socket-server-point” 路径注册为 STOMP 端点。这个路径与之前发送和接收消息的目的路径有所不同, 这是一个端点,客户端在订阅或发布消息到目的地址前,要连接该端点,即用户发送请求 :url=’/127.0.0.1:8080/socket-server-point’ 与 STOMP server 进行连接,之后再转发到订阅url;
- 它复写了 configureMessageBroker() 方法:配置了一个 简单的消息代理,通俗一点讲就是设置消息连接请求的各种规范信息。
- 发送应用程序的消息将会带有 “/ws” 前缀。
Service服务处理
@Service public class HandlerService { private static final Logger logger = LoggerFactory.getLogger(HandlerService.class); @Async public CompletableFuture<String> handle(String key) throws Exception { Thread.sleep(new Random().nextInt(3000)); logger.info("Looking up " + key); key=key+":"+LocalDateTime.now(); return CompletableFuture.completedFuture(key); } }
Controller控制开发
@MessageMapping("/searchBase") public ResponseEntity searchBase() throws Exception { Consumer<List<String>> callback = p -> websocket.convertAndSend("/topic/searchResults", p); List<String> list = Arrays.asList("bba", "aaa", "ccc"); localSearch(list, callback); Map map = new HashMap(); map.put("list", list); map.put("date", LocalDateTime.now()); return ResponseEntity.ok(map); } public void localSearch(List<String> keys, Consumer<List<String>> callback) throws Exception { Thread.sleep(2000); List<String> list = new ArrayList<>(); for (String key : keys) { CompletableFuture<String> completableFuture = handlerService.handle(key); completableFuture.thenAcceptAsync(p -> { list.clear(); list.add(p); callback.accept(list); }); } }
8.3.3.2、消息流
8.3.3.3、启用STOMP代理中继
对于生产环境下的应用来说,你可能会希望使用真正支持STOMP的代理来支撑WebSocket消息,如RabbitMQ或ActiveMQ。这样的代理提供了可扩展性和健壮性更好的消息功能,当然它们也会完整支持STOMP命令。我们需要根据相关的文档来为STOMP搭建代理。搭建就绪之后,就可以使用STOMP代理来替换内存代理了,只需按照如下方式重载configureMessageBroker()方法即可:
@Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.enableStompBrokerRelay("/queue", "/topic"); //定义了一个客户端订阅地址的前缀信息,也就是客户端接收服务端发送消息的前缀信息 // registry.enableSimpleBroker("/topic"); //定义了服务端接收地址的前缀,也即客户端给服务端发消息的地址前缀 registry.setApplicationDestinationPrefixes("/ws"); }
-
上述configureMessageBroker()方法的第一行代码启用了STOMP代理中继(broker relay)功能,并将其目的地前缀设置为“/topic”和“/queue”。这样的话,Spring就能知道所有目的地前缀为“/topic”或“/queue”的消息都会发送到STOMP代理中。
-
在第二行的configureMessageBroker()方法中将应用的前缀设置为“/ws”。所有目的地以“/ws”打头的消息都将会路由到带有@MessageMapping注解的方法中,而不会发布到代理队列或主题中。
默认情况下,STOMP代理中继会假设代理监听localhost的61613端口,并且客户端的username和password均为“guest”。如果你的STOMP代理位于其他的服务器上,或者配置成了不同的客户端凭证,那么我们可以在启用STOMP代理中继的时候,需要配置这些细节信息:
@Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.enableStompBrokerRelay("/queue", "/topic") .setRelayHost("rabbit.someotherserver") .setRelayPort(62623) .setClientLogin("marcopolo") .setClientPasscode("letmein01") registry.setApplicationDestinationPrefixes("/app"); }
8.3.3.4、处理来自客户端的STOMP消息
1、应用消息MessageMapping
Spring 4.0引入了@MessageMapping注解,它用于STOMP消息的处理,类似于Spring MVC的@RequestMapping注解。当消息抵达某个特定的目的地时,带有@MessageMapping注解的方法能够处理这些消息。
/** * 处理来自客户端的STOMP消息 * @param incoming * @return */ @MessageMapping("/incoming") public Shout handleShout(Shout incoming) { logger.info("Received message: " + incoming.getMessage()); try { Thread.sleep(2000); } catch (InterruptedException e) {} Shout outgoing = new Shout(); outgoing.setMessage("incoming!"); return outgoing; }
消息接受类
public class Shout { private String message; public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } }
客户端
function contect() { var socket=new SockJS('/socket-server-point'); stompCliet=Stomp.over(socket); stompCliet.connect({},function (frame) { console.log('Connected :'+frame); stompCliet.send("/ws/incoming",{},"{\"message\":\"Hello!\"}"); }); } contect();
2、订阅模式SubscribeMapping
@SubscribeMapping的主要应用场景是实现请求-回应模式。在请求-回应模式中,客户端订阅某一个目的地,然后预期在这个目的地上获得一个一次性的响应。
例如,考虑如下@SubscribeMapping注解标注的方法:
@SubscribeMapping("/sub") public Shout handleSubscription(){ logger.info("Received message: " +"subscription"); Shout outgoing = new Shout(); outgoing.setMessage("subscription!"); return outgoing; }
当处理这个订阅时,handleSubscription()方法会产生一个输出的Shout对象并将其返回。然后,Shout对象会转换成一条消息,并且会按照客户端订阅时相同的目的地发送回客户端。
客户端
function contect() { var socket=new SockJS('/socket-server-point'); stompCliet=Stomp.over(socket); stompCliet.connect({},function (frame) { console.log('Connected :'+frame); stompCliet.subscribe('/ws/sub',function (result) { console.log("aaaa",JSON.parse(result.body)); }); stompCliet.send("/ws/sub",{},"{\"message\":\"Hello!\"}"); }); } contect();
这种请求-回应模式与HTTP GET的请求-响应模式并没有太大差别。但是,这里的关键区别在于HTTP GET请求是同步的,而订阅的请求-回应模式则是异步的,这样客户端能够在回应可用时再去处理,而不必等待。
8.3.3.5、发送消息到客户端
Spring提供了两种发送数据给客户端的方法:
- 作为处理消息或处理订阅的附带结果;
- 使用消息模板。
方式一、作为处理消息或处理订阅的附带结果、在处理消息之后,发送消息
@MessageMapping("/incoming") public Shout handleShout(Shout incoming) { logger.info("Received message: " + incoming.getMessage()); try { Thread.sleep(2000); } catch (InterruptedException e) {} Shout outgoing = new Shout(); outgoing.setMessage("incoming!"); return outgoing; }
当@MessageMapping注解标示的方法有返回值的时候,返回的对象将会进行转换(通过消息转换器)并放到STOMP帧的负载中,然后发送给消息代理。
默认情况下,帧所发往的目的地会与触发处理器方法的目的地相同,只不过会添加上“/topic”前缀。就本例而言,这意味着handleShout()方法所返回的Shout对象会写入到STOMP帧的负载中,并发布到“/topic/incoming”目的地。不过,我们可以通过为方法添加@SendTo注解,重载目的地:
@MessageMapping("/incoming") @SendTo("/topic/shout") public Shout handleShout(Shout incoming) { logger.info("Received message: " + incoming.getMessage()); try { Thread.sleep(2000); } catch (InterruptedException e) {} Shout outgoing = new Shout(); outgoing.setMessage("incoming!"); return outgoing; }
按照这个@SendTo注解,消息将会发布到“/topic/shout”。所有订阅这个主题的应用(如客户端)都会收到这条消息。
按照类似的方式,@SubscribeMapping注解标注的方式也能发送一条消息,作为订阅的回应。
@SubscribeMapping("/sub") public Shout handleSubscription(){ logger.info("Received message: " +"subscription"); Shout outgoing = new Shout(); outgoing.setMessage("subscription!"); return outgoing; }
@SubscribeMapping的区别在于这里的Shout消息将会直接发送给客户端,而不必经过消息代理。如果你为方法添加@SendTo注解的话,那么消息将会发送到指定的目的地,这样会经过代理。
对应客户端需要增加订阅即可
stompCliet.subscribe('/ws/sub',function (result) { console.log("aaaa",JSON.parse(result.body)); });
正如前面看到的那样,使用 @MessageMapping 或者 @SubscribeMapping 注解可以处理客户端发送过来的消息,并选择方法是否有返回值。
如果 @MessageMapping 注解的控制器方法有返回值的话,返回值会被发送到消息代理,只不过会添加上"/topic"前缀。可以使用@SendTo 重写消息目的地;
如果 @SubscribeMapping 注解的控制器方法有返回值的话,返回值会直接发送到客户端,不经过代理。如果加上@SendTo 注解的话,则要经过消息代理。
方式二、使用消息模板【在任意地方发送消息】
@MessageMapping和@SubscribeMapping提供了一种很简单的方式来发送消息,这是接收消息或处理订阅的附带结果。不过,Spring的SimpMessagingTemplate能够在应用的任何地方发送消息,甚至不必以首先接收一条消息作为前提。
我们不必要求用户刷新页面,而是让首页订阅一个STOMP主题.
function contect() { var socket=new SockJS('/socket-server-point'); stompCliet=Stomp.over(socket); stompCliet.connect({},function (frame) { console.log('Connected :'+frame); stompCliet.subscribe('/topic/sendDataToClient',function (result) { console.log("aaaa",JSON.parse(result.body)); }); // stompCliet.send("/ws/sub",{},"{\"message\":\"Hello!\"}"); }); } contect();
使用SimpMessagingTemplate能够在应用的任何地方发布消息
@Autowired(required = false) private SimpMessagingTemplate websocket; @GetMapping("/sendDataToClient") public ResponseEntity sendDataToClient() throws Exception { Map map=new HashMap(); map.put("aa","aaa"); map.put("bb","bbb"); websocket.convertAndSend("/topic/sendDataToClient",map); return ResponseEntity.ok("ok"); }
当然此处的SimpMessagingTemplate也可以使用父接口SimpMessageSendingOperations注入
在这个场景下,我们希望所有的客户端都能及时看到实时的/topic/sendDataToClient,这种做法是很好的。但有的时候,我们希望发送消息给指定的用户,而不是所有的客户端。
8.3.3.6、为目标用户发送消息
以上说明了如何广播消息,订阅目的地的所有用户都能收到消息。如果消息只想发送给特定的用户呢?spring-websocket 介绍了以下
在使用Spring和STOMP消息功能的时候,我们有两种方式利用认证用户:
1、@MessageMapping和@SubscribeMapping标注的方法基于@SendToUser注解和Principal参数来获取认证用户;@MessageMapping、@SubscribeMapping和@MessageException方法返回的值能够以消息的形式发送给认证用户;
2、SimpMessageSendingOperations接口或SimpMessagingTemplate的convertAndSendToUser方法能够发送消息给特定用户。
1、在控制器中处理用户的消息
在控制器的@MessageMapping或@SubscribeMapping方法中,处理消息时有两种方式了解用户信息。在处理器方法中,通过简单地添加一个Principal参数,这个方法就能知道用户是谁并利用该信息关注此用户相关的数据。除此之外,处理器方法还可以使用@SendToUser注解,表明它的返回值要以消息的形式发送给某个认证用户的客户端(只发送给该客户端)。
@SendToUser 表示要将消息发送给指定的用户,会自动在消息目的地前补上"/user"前缀。如下,最后消息会被发布在 /user/queue/notifications-username。但是问题来了,这个username是怎么来的呢?就是通过 principal 参数来获得的。那么,principal 参数又是怎么来的呢?需要在spring-websocket 的配置类中重写 configureClientInboundChannel 方法,添加上用户的认证。
服务端增加configuration
@Configuration @EnableWebSocketMessageBroker public class WebSocketConfiguration implements WebSocketMessageBrokerConfigurer { /** * 1、设置拦截器 * 2、首次连接的时候,获取其Header信息,利用Header里面的信息进行权限认证 * 3、通过认证的用户,使用 accessor.setUser(user); 方法,将登陆信息绑定在该 StompHeaderAccessor 上,在Controller方法上可以获取 StompHeaderAccessor 的相关信息 * @param registration */ @Override public void configureClientInboundChannel(ChannelRegistration registration) { registration.interceptors(new ChannelInterceptorAdapter() { @Override public Message<?> preSend(Message<?> message, MessageChannel channel) { StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); //1、判断是否首次连接 if (StompCommand.CONNECT.equals(accessor.getCommand())){ //2、判断用户名和密码 String username = accessor.getNativeHeader("username").get(0); String password = accessor.getNativeHeader("password").get(0); if ("admin".equals(username) && "admin".equals(password)){ Principal principal = new Principal() { @Override public String getName() { return username; } }; accessor.setUser(principal); return message; }else { return null; } } //不是首次连接,已经登陆成功 return message; } }); } }
服务端处理逻辑
@MessageMapping("/shout") @SendToUser("/queue/notifications") public Shout userStomp(Principal principal, Shout shout) { String name = principal.getName(); String message = shout.getMessage(); logger.info("认证的名字是:{},收到的消息是:{}", name, message); return shout; }
前端js代码
var headers={ username:'admin', password:'admin' }; function contect() { var socket=new SockJS('/socket-server-point'); stompCliet=Stomp.over(socket); stompCliet.connect(headers,function (frame) { console.log('Connected :'+frame); stompCliet.subscribe('/user/queue/notifications',function (result) { console.log("aaaa",JSON.parse(result.body)); }); stompCliet.send("/ws/shout",{},"{\"message\":\"Hello!\"}"); }); } contect();
2、convertAndSendToUser方法
除了convertAndSend()以外,SimpMessageSendingOperations 还提供了convertAndSendToUser()方法。按照名字就可以判断出来,convertAndSendToUser()方法能够让我们给特定用户发送消息。
@MessageMapping("/singleShout") public void singleUser(Shout shout, StompHeaderAccessor stompHeaderAccessor) { String message = shout.getMessage(); LOGGER.info("接收到消息:" + message); Principal user = stompHeaderAccessor.getUser(); simpMessageSendingOperations.convertAndSendToUser(user.getName(), "/queue/shouts", shout); }
如上,这里虽然我还是用了认证的信息得到用户名。但是,其实大可不必这样,因为 convertAndSendToUser 方法可以指定要发送给哪个用户。也就是说,完全可以把用户名的当作一个参数传递给控制器方法,从而绕过身份认证!convertAndSendToUser 方法最终会把消息发送到 /user/sername/queue/shouts 目的地上。
8.3.3.7、处理消息异常
在处理消息的时候,有可能会出错并抛出异常。因为STOMP消息异步的特点,发送者可能永远也不会知道出现了错误。@MessageExceptionHandler标注的方法能够处理消息方法中所抛出的异常。我们可以把错误发送给用户特定的目的地上,然后用户从该目的地上订阅消息,从而用户就能知道自己出现了什么错误
@MessageExceptionHandler(Exception.class) @SendToUser("/queue/errors") public Exception handleExceptions(Exception t){ t.printStackTrace(); return t; }
8.3.3.8、更多stomp配置
1、发起连接
其中headers表示客户端的认证信息:
若无需认证,直接使用空对象 “{}” 即可;
(1)connectCallback 表示连接成功时(服务器响应 CONNECTED 帧)的回调方法;
(2)errorCallback 表示连接失败时(服务器响应 ERROR 帧)的回调方法,非必须;
默认链接端点
//默认的和STOMP端点连接 /*stomp.connect("guest", "guest", function (franme) { });*/
有用户认证的
var headers={ username:'admin', password:'admin' }; stomp.connect(headers, function (frame) {
示例
// 建立连接对象(还未发起连接) var socket=new SockJS("/endpointChat"); // 获取 STOMP 子协议的客户端对象 var stompClient = Stomp.over(socket); // 向服务器发起websocket连接并发送CONNECT帧 stompClient.connect( {}, function connectCallback (frame) { // 连接成功时(服务器响应 CONNECTED 帧)的回调方法 console.log('已连接【' + frame + '】'); //订阅一个消息 stompClient.subscribe('/topic/getResponse', function (response) { showResponse(response.body); }); }, function errorCallBack (error) { // 连接失败时(服务器响应 ERROR 帧)的回调方法 console.log('连接失败【' + error + '】'); } );
2、断开连接
若要从客户端主动断开连接,可调用 disconnect() 方法:
client.disconnect( function () { alert("断开连接"); });
3、发送消息
连接成功后,客户端可使用 send() 方法向服务器发送信息:
client.send(destination url, headers, body);
其中:
(1)destination url 为服务器 controller中 @MessageMapping 中匹配的URL,字符串,必须参数;
(2)headers 为发送信息的header,JavaScript 对象,可选参数;
(3)body 为发送信息的 body,字符串,可选参数;
示例
client.send("/queue/test", {priority: 9}, "Hello, STOMP");
client.send("/queue/test", {}, "Hello, STOMP");
4、订阅、接收消息
STOMP 客户端要想接收来自服务器推送的消息,必须先订阅相应的URL,即发送一个 SUBSCRIBE 帧,然后才能不断接收来自服务器的推送消息。
订阅和接收消息通过 subscribe() 方法实现:
subscribe(destination url, callback, headers)
其中
(1)destination url 为服务器 @SendTo 匹配的 URL,字符串;
(2)callback 为每次收到服务器推送的消息时的回调方法,该方法包含参数 message;
(3)headers 为附加的headers,JavaScript 对象;该方法返回一个包含了id属性的 JavaScript 对象,可作为 unsubscribe() 方法的参数;默认情况下,如果没有在headers额外添加,这个库会默认构建一个独一无二的ID。在传递headers这个参数时,可以使用你自己id。
示例
var headers = { ack: 'client', //这个客户端指定了它会确认接收的信息,只接收符合这个selector : location = 'Europe'的消息。 'selector': "location = 'Europe'", //id:’myid’ }; var callback = function(message) { if (message.body) { alert("got message with body " +JSON.parse( message.body)) } else{ alert("got empty message"); } }); var subscription = client.subscribe("/queue/test", callback, headers); 如果想让客户端订阅多个目的地,你可以在接收所有信息的时候调用相同的回调函数: onmessage = function(message) { // called every time the client receives a message } var sub1 = client.subscribe("queue/test", onmessage); var sub2 = client.subscribe("queue/another", onmessage)
5、取消订阅
var subscription = client.subscribe(...); subscription.unsubscribe();
6、事务支持
可以在将消息的发送和确认接收放在一个事务中。
客户端调用自身的begin()方法就可以开始启动事务了,begin()有一个可选的参数transaction,一个唯一的可标识事务的字符串。如果没有传递这个参数,那么库会自动构建一个。
这个方法会返回一个object。这个对象有一个id属性对应这个事务的ID,还有两个方法:
commit()提交事务
abort()中止事务
在一个事务中,客户端可以在发送/接受消息时指定transaction id来设置transaction。
// start the transaction var tx = client.begin(); // send the message in a transaction client.send("/queue/test", {transaction: tx.id}, "message in a transaction"); // commit the transaction to effectively send the message tx.commit();
如果你在调用send()方法发送消息的时候忘记添加transction header,那么这不会称为事务的一部分,这个消息会直接发送,不会等到事务完成后才发送。
var txid = "unique_transaction_identifier"; // start the transaction var tx = client.begin(); // oops! send the message outside the transaction client.send("/queue/test", {}, "I thought I was in a transaction!"); tx.abort(); // Too late! the message has been sent
7、消息确认
默认情况,在消息发送给客户端之前,服务端会自动确认(acknowledged)。
客户端可以选择通过订阅一个目的地时设置一个ack header为client或client-individual来处理消息确认。
在下面这个例子,客户端必须调用message.ack()来通知客户端它已经接收了消息。
var subscription = client.subscribe("/queue/test", function(message) { // do something with the message ... // and acknowledge it message.ack(); }, {ack: 'client'} );
ack()接受headers参数用来附加确认消息。例如,将消息作为事务(transaction)的一部分,当要求接收消息时其实代理(broker)已经将ACK STOMP frame处理了。
var tx = client.begin(); message.ack({ transaction: tx.id, receipt: 'my-receipt' }); tx.commit();
ack()也可以用来通知STOMP 1.1.brokers(代理):客户端不能消费这个消息。与ack()方法的参数相同。
8、debug调试
有一些测试代码能有助于你知道库发送或接收的是什么,从而来调试程序。
客户端可以将其debug属性设置为一个函数,传递一个字符串参数去观察库所有的debug语句。
client.debug = function(str) { // append the debug log to a #debug div somewhere in the page using JQuery: $("#debug").append(str + "\n"); };
默认情况,debug消息会被记录在在浏览器的控制台。
9、心跳机制
如果STOMP broker(代理)接收STOMP 1.1版本的帧,heart-beating是默认启用的。heart-beating也就是频率,incoming是接收频率,outgoing是发送频率。
通过改变incoming和outgoing可以更改客户端的heart-beating(默认为10000ms):
client.heartbeat.outgoing = 20000; // client will send heartbeats every 20000ms client.heartbeat.incoming = 0; // client does not want to receive heartbeats // from the server
heart-beating是利用window.setInterval()去规律地发送heart-beats或者检查服务端的heart-beats。
更多
stomp.connect(headers, function (frame) { //发送消息 //第二个参数是一个头信息的Map,它会包含在STOMP的帧中 //事务支持 var tx = stomp.begin(); stomp.send("/app/marco", {transaction: tx.id}, strJson); tx.commit(); //订阅服务端消息 subscribe(destination url, callback[, headers]) stomp.subscribe("/topic/marco", function (message) { var content = message.body; var obj = JSON.parse(content); console.log("订阅的服务端消息:" + obj.message); }, {}); stomp.subscribe("/app/getShout", function (message) { var content = message.body; var obj = JSON.parse(content); console.log("订阅的服务端直接返回的消息:" + obj.message); }, {}); /*以下是针对特定用户的订阅*/ var adminJSON = JSON.stringify({'message': 'ADMIN'}); /*第一种*/ stomp.send("/app/singleShout", {}, adminJSON); stomp.subscribe("/user/queue/shouts",function (message) { var content = message.body; var obj = JSON.parse(content); console.log("admin用户特定的消息1:" + obj.message); }); /*第二种*/ stomp.send("/app/shout", {}, adminJSON); stomp.subscribe("/user/queue/notifications",function (message) { var content = message.body; var obj = JSON.parse(content); console.log("admin用户特定的消息2:" + obj.message); }); /*订阅异常消息*/ stomp.subscribe("/user/queue/errors", function (message) { console.log(message.body); }); //若使用STOMP 1.1 版本,默认开启了心跳检测机制(默认值都是10000ms) stomp.heartbeat.outgoing = 20000; stomp.heartbeat.incoming = 0; //客户端不从服务端接收心跳包 });
参看代码:https://github.com/JMCuixy/SpringWebSocket