Java servlet通过事件驱动进行高性能长轮询详解
作者:hi wei
servlet3.0的异步原理
servlet基础就不做介绍了,这里就介绍servlet3.0的一个重要的新特性:异步。
servlet3.0原理图:
- tomcat接收到客户端的请求后会将请求AsyncContext交给业务线程,这样tomcat工作线程就能释放出来处理其它请求的连接。
- 业务线程池接收到AsyncContext后,就可以处理请求业务,完成业务逻辑后,根据AsyncContext获取response,返回响应结果。
- AsyncListener会监听AsyncContext的行为,我们可以根据具体的行为做出对应的业务处理。
servlet3.0将tomcat工作线程和业务线程分隔开来,这样tomcat工作线程就能处理更多的连接请求。业务线程主要处理业务逻辑。在这种模式下,可以更好的分配业务线程的数量,也能根据不同的业务,设置不同的线程数量,更加灵活。
注意:tomcat的NIO和servlet3.0的异步没有关系。tomcat NIO模式,是对于http连接的处理使用,目的是用更少的线程处理更多的连接。servlet3.0是在tomcat工作线程的处理逻辑上实现异步处理功能。
使用servlet3.0实现长轮询
什么是长轮询:
- 长轮询是指客户端会一直向服务端发起请求,适用与服务端向客户端推送数据使用。长轮询要满足以下几点: 客户端发起请求后,当服务端业务没有数据时,不会立即返回空值,而是hold住连接,等待数据生成后立即返回。
- 请求在服务端有超时时间,不会一直hold住。当超时后,服务端会返回超时信息,客户端收到返回后会再次发起请求。
- 每次请求结束后,客户端会再次发起请求。
短轮询、长轮询和长连接比较:
- 短轮询:客户端定时向服务器发送Ajax请求,服务器接到请求后马上返回响应信息并关闭连接。
优点:后端程序编写比较容易,适于小型应用。。
缺点:请求中有大半是无用,浪费带宽和服务器资源。
- 长轮询:客户端向服务器发送Ajax请求,服务器接到请求后hold住连接,直到有新消息才返回响应信息并关闭连接,客户端处理完响应信息后再向服务器发送新的请求。
优点:在无消息的情况下不会频繁的请求。
缺点:服务器hold连接会消耗资源。
- 长连接:客户端与服务端建立长连接socket
优点:可靠性高,实时性高。
缺点:实现复杂,要维护心跳,服务器维持连接消耗资源。
长轮询实现
原理图:
- 请求过来之后,生成事件,加入对应的事件集合。请求设置30s超时时间,并添加监听。tomcat工作线程释放。
- 当服务端数据准备好之后,触发对应事件,从容器获取订阅事件进行执行。完成后返回response。
- 请求超时,listener触发,返回超时信息。
下面看下具体实现:
事件定义,这里只是定义一个简单的事件:
package com.hiwe.demo.event; import javax.servlet.AsyncContext; public class HttpEvent { /** * 可以是业务数据主键,这里用请求名称做个简单demo */ private String requestName; private AsyncContext asyncContext; public HttpEvent(String requestName,AsyncContext asyncContext){ this.requestName = requestName; this.asyncContext = asyncContext; } public String getRequestName() { return requestName; } public AsyncContext getAsyncContext() { return asyncContext; } }
事件管理器:
package com.hiwe.demo.event; import javax.servlet.AsyncContext; import java.io.IOException; import java.io.PrintWriter; import java.util.HashMap; import java.util.Map; public class EventManager { private final static Map<String,HttpEvent> subHttpEvents = new HashMap<>(); /** * 新增事件订阅 * @param event */ public static void addHttpEvent(HttpEvent event){ subHttpEvents.put(event.getRequestName(),event); } /** * 触发事件 * @param requestName */ public static void onEvent(String requestName){ HttpEvent httpEvent = subHttpEvents.get(requestName); if(httpEvent==null){ return; } AsyncContext asyncContext = httpEvent.getAsyncContext(); try { PrintWriter writer = asyncContext.getResponse().getWriter(); writer.print(requestName+" request success!"); writer.flush(); asyncContext.complete(); subHttpEvents.remove(requestName); } catch (IOException e) { e.printStackTrace(); } } }
异步请求监听器:
package com.hiwe.demo.listener; import javax.servlet.AsyncContext; import javax.servlet.AsyncEvent; import javax.servlet.AsyncListener; import javax.servlet.ServletResponse; import javax.servlet.annotation.WebListener; import java.io.IOException; import java.io.PrintWriter; @WebListener public class AppAsyncListener implements AsyncListener { @Override public void onComplete(AsyncEvent asyncEvent) throws IOException { System.out.println("AppAsyncListener onComplete"); // we can do resource cleanup activity here } @Override public void onError(AsyncEvent asyncEvent) throws IOException { System.out.println("AppAsyncListener onError"); //we can return error response to client } @Override public void onStartAsync(AsyncEvent asyncEvent) throws IOException { System.out.println("AppAsyncListener onStartAsync"); //we can log the event here } /** * 超时触发 * @param asyncEvent * @throws IOException */ @Override public void onTimeout(AsyncEvent asyncEvent) throws IOException { AsyncContext asyncContext = asyncEvent.getAsyncContext(); ServletResponse response = asyncEvent.getAsyncContext().getResponse(); PrintWriter out = response.getWriter(); //返回code码,以便前端识别,并重建请求 out.write(201+" longPolling timeout"); out.flush(); asyncContext.complete(); } }
长轮询接口:
package com.hiwe.demo.controller; import com.hiwe.demo.listener.AppAsyncListener; import com.hiwe.demo.event.EventManager; import com.hiwe.demo.event.HttpEvent; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import javax.servlet.AsyncContext; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @RestController @RequestMapping("/app") public class AsyncController { /** * 长轮询接口 * @param requestName * @param request * @param response */ @GetMapping("/asyncGet") public void getDemo(@RequestParam(value = "requestName") String requestName, HttpServletRequest request, HttpServletResponse response){ //开启异步支持 request.setAttribute("org.apache.catalina.ASYNC_SUPPORTED", true); AsyncContext asyncContext = request.startAsync(); //添加监听器 asyncContext.addListener(new AppAsyncListener()); //设置超时时间 asyncContext.setTimeout(30000); //添加到事件集合中去 HttpEvent httpEvent = new HttpEvent(requestName, asyncContext); EventManager.addHttpEvent(httpEvent); } /** * 触发事件使用 * @param requestName */ @GetMapping("/trigger") public void triggerDemo(@RequestParam(value = "requestName") String requestName){ EventManager.onEvent(requestName); } }
以上一个简单的长轮询就实现了,我们可以进行一下测试:
启动应用后访问:http://localhost:8080/app/asyncGet?requestName=123
服务端因为数据未准备就绪,所以会hold住请求。当等待30s后会返回超时信息:
我们在30s内触发event:http://localhost:8080/app/trigger?requestName=123
返回:
以上整个长轮询实现完成了,如果有错误,欢迎指正!
到此这篇关于Java servlet通过事件驱动进行高性能长轮询详解的文章就介绍到这了,更多相关Java 高性能长轮询内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!