Java servlet通过事件驱动进行高性能长轮询详解

网友投稿 293 2022-07-23


目录servlet3.0的异步原理使用servlet3.0实现长轮询长轮询实现

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住。当超时后,服务端会返回超时信息,客户端收到返回后会再次发起请求。每次请求结束OBgXx后,客户端会再次发起请求。

短轮询、长轮询和长连接比较:

短轮询:客户端定时向服务器发送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 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();

http://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(OBgXxhttpEvent);

}

/**

* 触发事件使用

* @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

返回:

以上整个长轮询实现完成了,如果有错误,欢迎指正!


版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:全局记录Feign的请求和响应日志方式
下一篇:springboot项目启动后执行方法的三种方式
相关文章

 发表评论

暂时没有评论,来抢沙发吧~