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” 前缀。
2、控制器以及逻辑开发
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
006-优化web请求二-应用缓存、异步调用【Future、ListenableFuture、CompletableFuture】、ETag、WebSocket【SockJS、Stomp】的更多相关文章
- Dubbo消费者异步调用Future使用
Dubbo的四大组件工作原理图,其中消费者调用提供者采用的是同步调用方式.消费者对于提供者的调用,也可以采用异步方式进行调用.异步调用一般应用于提供者提供的是耗时性IO服务 一.Future异步执行原 ...
- 如何构建日均千万PV Web站点(二) 之~缓存为王~
随着网站业务的不断发展,用户的规模越来越大:介于中国无比蹩脚复杂的网路环境:南电信:北联通:中间竟然只用一条链路进行互联通信!有研究表明,网站访问延迟和用户流失率正相关,网站访问速度越慢,用户越容易失 ...
- Java异步调用Future对象
Future类存在于JDK的concurrent包中,主要用途是接收Java的异步线程计算返回的结果. 个人理解的使用场景大概如下: 有两个任务A和B,A任务中仅仅需要使用B任务计算成果,有两种方法实 ...
- Springboot + Websocket + Sockjs + Stomp + Vue + Iview 实现java后端日志显示在前端web页面上
话不多说,看代码. 一.pom.xml 引入spring boot websocket依赖 <dependency> <groupId>org.springframework. ...
- WEB页面异步调用场景测试
在我们测试异步调用前,我们首先弄清楚异步调用到底是什么? 异步调用的定义:一个可以无需等待被调用函数的返回值就让操作继续进行的方法, 举一个形象的例子就是:领导给A分配了一个任务, 然后领导就干其他事 ...
- 限时购校验小工具&dubbo异步调用实现限
本文来自网易云社区 作者:张伟 背景 限时购是网易考拉目前比较常用的促销形式,但是前期创建一个限时购活动时需要各个BU按照指定的Excel格式进行选品提报,为了保证提报数据准确,运营需要人肉校验很多信 ...
- 从Nginx的Web请求处理机制中剖析多进程、多线程、异步IO
Nginx服务器web请求处理机制 从设计架构来说,Nginx服务器是与众不同的.不同之处一方面体现在它的模块化设计,另一方面,也是最重要的一方面,体现在它对客户端请求的处理机制上. Web服务器和客 ...
- 005-优化web请求一-gzip压缩、http缓存控制和缓存校验[Pragma、Expires、Cache-Control、max-age、Last-Modified、用户刷新访问、避免过度304]
优化Web应用的典型技术:缓存控制头信息.Gzip.应用缓存.ETag.反应型技术[异步方法调用和WebSocket] 一.模板缓存 spring.thymeleaf.cache=true sprin ...
- iOS网络模块优化(失败重发、缓存请求有网发送)
iOS开发中,一般都是通过AFN搭建一个简易的网络模块来进行与服务器的通信,这一模块要优化好没那么简单,需要花费很多时间与精力,仅仅根据这几年来的填坑经验,总结下这一块的需要注意的地方,也是给自己梳理 ...
随机推荐
- 网络编程 -- RPC实现原理 -- RPC -- 迭代版本V1 -- 本地方法调用
网络编程 -- RPC实现原理 -- 目录 啦啦啦 V1——RPC -- 本地方法调用:不通过网络 入门 1. RPCObjectProxy rpcObjectProxy = new RPCObjec ...
- Spring task定时任务执行一段时间后莫名其妙停止的问题
前因: 我写了一个小项目,主要功能是用Spring task定时任务每天定时给用户发送邮件.执行了几个月一直没有问题,前几天,莫名其妙的突然不再发送邮件了. 只好花费一些时间来查看到底是什么原因造成的 ...
- H5 (webApi) 接口帮助文档
只有你想不到,没有找不到的,强大的 webApis!!! https://developer.mozilla.org/en-US/docs/Web/API
- PHP利用ImageMagick把PDF转成PNG
http://blog.csdn.net/longaction2012/article/details/12257867
- zeromq使用模式实验总结
zeromq:官网 安装 demo及各语言绑定 golang绑定 实验环境:win10 x64/centos6 x86 zeromq4.0.6 zmq三种模式:push/pull.pub/sub.r ...
- Linux(Ubuntu)使用 sudo apt-get install 命令安装软件的目录在哪?(已解决)
Linux(Ubuntu)使用 sudo apt-get install 命令安装软件的目录在哪? bin文件路径: /usr/bin 库文件路径: /usr/lib/ 其它的图标啊什么的路径 ...
- Don't forget to Cherish
Watch the video Cherish time, Cherish memories, Cherish tears, Cherish smiles, Cherish friends, Cher ...
- easyui Dialog 去边框
使用Url页面显示 panel-body 的内容, 因页面高度大于iframe高度,右侧则会出现边框, 如果页面显示的内容高度真实大于iframe高度,那出现边框是很正常. 我的问题就是有内部页面 的 ...
- PL-SLAM
双目 1.PL-SLAM: a Stereo SLAM System through the Combination of Points and Line Segments ubuntu14.04配置 ...
- [No0000181]改善C#程序的建议9:使用Task代替ThreadPool和Thread
一:Task的优势 ThreadPool相比Thread来说具备了很多优势,但是ThreadPool却又存在一些使用上的不方便.比如: 1: ThreadPool不支持线程的取消.完成.失败通知等交互 ...