java 实现http长轮询_springmvc - 通过DeferredResult实现长轮询服务端推送消息

DeferredResult字面意思就是推迟结果,是在servlet3.0以后引入了异步请求之后,spring封装了一下提供了相应的支持,也是一个很老的特性了。DeferredResult可以允许容器线程快速释放以便可以接受更多的请求提升吞吐量,让真正的业务逻辑在其他的工作线程中去完成。

最近再看apollo配置中心的实现原理,apollo的发布配置推送变更消息就是用DeferredResult实现的,apollo客户端会像服务端发送长轮训http请求,超时时间60秒,当超时后返回客户端一个304 httpstatus,表明配置没有变更,客户端继续这个步骤重复发起请求,当有发布配置的时候,服务端会调用DeferredResult.setResult返回200状态码,然后轮训请求会立即返回(不会超时),客户端收到响应结果后,会发起请求获取变更后的配置信息。

下面我们自己写一个简单的demo来演示这个过程

@SpringBootApplication

public class DemoApplication implements WebMvcConfigurer {

public static void main(String[] args) {

SpringApplication.run(DemoApplication.class, args);

}

@Bean

public ThreadPoolTaskExecutor mvcTaskExecutor() {

ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

executor.setCorePoolSize(10);

executor.setQueueCapacity(100);

executor.setMaxPoolSize(25);

return executor;

}

//配置异步支持,设置了一个用来异步执行业务逻辑的工作线程池,设置了默认的超时时间是60秒

@Override

public void configureAsyncSupport(AsyncSupportConfigurer configurer) {

configurer.setTaskExecutor(mvcTaskExecutor());

configurer.setDefaultTimeout(60000L);

}

}

@RestController

public class ApolloController {

private final Logger logger = LoggerFactory.getLogger(this.getClass());

//guava中的Multimap,多值map,对map的增强,一个key可以保持多个value

private Multimap> watchRequests = Multimaps.synchronizedSetMultimap(HashMultimap.create());

//模拟长轮询

@RequestMapping(value = "/watch/{namespace}", method = RequestMethod.GET, produces = "text/html")

public DeferredResult watch(@PathVariable("namespace") String namespace) {

logger.info("Request received");

DeferredResult deferredResult = new DeferredResult<>();

//当deferredResult完成时(不论是超时还是异常还是正常完成),移除watchRequests中相应的watch key

deferredResult.onCompletion(new Runnable() {

@Override

public void run() {

System.out.println("remove key:" + namespace);

watchRequests.remove(namespace, deferredResult);

}

});

watchRequests.put(namespace, deferredResult);

logger.info("Servlet thread released");

return deferredResult;

}

//模拟发布namespace配置

@RequestMapping(value = "/publish/{namespace}", method = RequestMethod.GET, produces = "text/html")

public Object publishConfig(@PathVariable("namespace") String namespace) {

if (watchRequests.containsKey(namespace)) {

Collection> deferredResults = watchRequests.get(namespace);

Long time = System.currentTimeMillis();

//通知所有watch这个namespace变更的长轮训配置变更结果

for (DeferredResult deferredResult : deferredResults) {

deferredResult.setResult(namespace + " changed:" + time);

}

}

return "success";

}

}

当请求超时的时候会产生AsyncRequestTimeoutException, 我们需要在全局异常中捕获

然后我们通过postman工具发送请求 http://localhost:8080/watch/mynamespace,请求会挂起,60秒后,DeferredResult超时,客户端正常收到了304状态码,表明在这个期间配置没有变更过。

然后我们在模拟配置变更的情况,再次发起请求 http://localhost:8080/watch/mynamespace,等待个10秒钟(不要超过60秒),然后调用http://localhost:8080/publish/mynamespace , 发布配置变更。这时postman会立刻收到response响应结果,表明在轮训期间有配置变更过

mynamespace changed:1538880050147

这里我们用了一个MultiMap来存放所有轮训的请求,Key对应的是namespace,value对应的是所有watch这个namespace变更的异步请求DeferredResult,需要注意的是:在DeferredResult完成的时候记得移除MultiMap中相应的key,避免内存溢出请求。

采用这种长轮询的好处是,相比一直循环请求服务器,实例一多的话会对服务器产生很大的压力,http长轮询的方式会在服务器变更的时候主动推送给客户端,其他时间客户端是挂起请求的,这样同时满足了性能和实时性。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java实现轮询机制可以使用 Servlet 或者基于 Servlet 的框架,如 Spring MVC。下面是一个使用 Servlet 实现轮询的示例代码: ```java @WebServlet("/api/data") public class LongPollingServlet extends HttpServlet { private static final long serialVersionUID = 1L; protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 设置响应类型为 JSON response.setContentType("application/json"); // 模拟获取数据的过程 // 例如,可以查询数据库或者调用外部接口来获取数据 String data = getData(); if (data != null) { // 有新数据可用时,将数据作为响应发送给客户端 response.getWriter().write(data); response.getWriter().flush(); } else { // 没有新数据可用时,将请求挂起,并保持连接打开 synchronized (this) { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 接收到新数据时,唤醒挂起的请求 synchronized (this) { notifyAll(); } } private String getData() { // 返回获取到的数据,如果没有数据可用,返回 null // 例如,可以从数据库或其他资源中获取数据 return null; } } ``` 在上面的示例中,`/api/data` 路径对应的 Servlet 类 `LongPollingServlet` 用于接收轮询请求。在 `doGet` 方法中,首先设置响应类型为 JSON,然后模拟获取数据的过程。如果有新数据可用,将数据作为响应发送给客户端;如果没有新数据可用,将请求挂起,并保持连接打开,直到有新数据到达或超时。在 `doPost` 方法中,接收到新数据时,唤醒挂起的请求。 这只是一个简单的示例,实际情况可能需要根据具体需求对代码进行更多的处理和优化。另外,轮询机制也可以通过使用 WebSocket 或其他推送技术来实现,这些方法可以实现实时的双向通信,而不需要轮询

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值