java长轮询
pull,push的区别
pull或者push都是为了数据传输,只是发起方不同。下定义提供数据的是服务端S,j接受数据的为客户端C。
区别
发起方不同
pull发起方为客户端,服务端为被动接收,当服务端没有数据需要传输时,该次pull属于是无效的。
push发起方为服务端,服务端有数据需要推送时,遍历需要该数据的客户端,然后将数据发送给客户端。这里服务端需要保存客户端的链接。
时效性不同
pull通常是客户端在一定心跳间隔时间发起,在数据变化过后的下次心跳时间才能获取到最新的数据。时效性较差。
push在服务端有数据时就会发送给到客户端,基本数据变化了就能立马到客户端。时效性较强。
对于服务端和客户端的资源消耗
pull,如果不是经常变化的数据,pull的请求大部分都是无意义的,浪费了服务端和客户端的流量,服务端的cpu等资源。
push,服务端需要保存客户端的信息,或者客户端的链接。如果客户端较多,遍历客户端信息或者保存TCP链接对于服务端来说压力较大。
数据处理负责人
pull的负责人是客户端,如果有数据未被客户端处理,需要首先考虑是否是客户端的问题。比如是否需要新增客户端,检查客户端的代码配置。
push的负责人是服务端,如果有数据未被客户端处理,需要首先考虑是否是服务端的问题。例如是否是服务端数据未推送。
总结
看时间敏感,数据大小,数据处理难度,客户端服务端性能选择。还有一种处于pull和push之间的长轮询。
长轮询
长轮询本质上是pull,但是在客户端发起请求时,服务端不会马上返回,而是保持该链接。如果在一个时间间隔后没有数据,那么服务端会返回一个空的数据。如果在一个时间间隔内有数据返回,那么他就返回数据。
这样可以由服务端来控制客户端的请求频率,避免不必要的请求,还能及时的返回数据,保证数据时效性。可以避免在数据变化不多的情况下的资源消耗。
java代码
参考nacos。
Controller
首先定义controller,这里需要拿到HttpServletResponse。可以自定义返回的内容,header等。拿到HttpServletRequest ,获取请求头中的数据,例如本次是否长轮询,长轮询时间,源ip等信息。
@PostMapping("/listener")
@Secured(action = ActionTypes.READ, signType = SignType.CONFIG)
public void listener(HttpServletRequest request, HttpServletResponse response){
// do long-polling
inner.doPollingConfig(request, response, clientMd5Map, probeModify.length());
}
inner.doPollingConfig
/**
* long polling the config.
*/
public String doPollingConfig(HttpServletRequest request, HttpServletResponse response,
Map<String, String> clientMd5Map, int probeRequestSize) throws IOException {
// Long polling.
if (LongPollingService.isSupportLongPolling(request)) {
longPollingService.addLongPollingClient(request, response, clientMd5Map, probeRequestSize);
return HttpServletResponse.SC_OK + "";
}
//不支持的当成普通请求处理
}
longPollingService.addLongPollingClient
获取请求头中的一些参数,例如长轮询到期时间,是否本次需要长轮询,appName等信息。
/**
* Add LongPollingClient.
*
* @param req HttpServletRequest.
* @param rsp HttpServletResponse.
* @param clientMd5Map clientMd5Map.
* @param probeRequestSize probeRequestSize.
*/
public void addLongPollingClient(HttpServletRequest req, HttpServletResponse rsp, Map<String, String> clientMd5Map,
int probeRequestSize) {
String str = req.getHeader(LongPollingService.LONG_POLLING_HEADER);
String noHangUpFlag = req.getHeader(LongPollingService.LONG_POLLING_NO_HANG_UP_HEADER);
String appName = req.getHeader(RequestUtil.CLIENT_APPNAME_HEADER);
String tag = req.getHeader("Vipserver-Tag");
int delayTime = SwitchService.getSwitchInteger(SwitchService.FIXED_DELAY_TIME, 500);
// Add delay time for LoadBalance, and one response is returned 500 ms in advance to avoid client timeout.
long timeout = Math.max(10000, Long.parseLong(str) - delayTime);
//是否是固定时长的长轮询,如果是,那么会忽略请求头的长轮询时间
if (isFixedPolling()) {
timeout = Math.max(10000, getFixedPollingInterval());
// Do nothing but set fix polling timeout.
} else {
long start = System.currentTimeMillis();
//判断是否这次请求来的时候已经有变化了,如果有,返回成一个list
List<String> changedGroups = MD5Util.compareMd5(req, rsp, clientMd5Map);
if (changedGroups.size() > 0) {
//变化的list不为空就返回
generateResponse(req, rsp, changedGroups);
return;
} else if (noHangUpFlag != null && noHangUpFlag.equalsIgnoreCase(TRUE_STR)) {
//没有变化,并且本次不需要长轮询,直接返回,并且没有返回值。
return;
}
}
String ip = RequestUtil.getRemoteIp(req);
// Must be called by http thread, or send response.
final AsyncContext asyncContext = req.startAsync();
// AsyncContext.setTimeout() is incorrect, Control by oneself
asyncContext.setTimeout(0L);
//没有返回,需要长轮询,走这里
ConfigExecutor.executeLongPolling(
new ClientLongPolling(asyncContext, clientMd5Map, ip, probeRequestSize, timeout, appName, tag));
}
首先我们看到的是asyncContext,这个为了保存response,方便在线程池中返回数据。
final AsyncContext asyncContext = req.startAsync();
这里看到 ConfigExecutor.executeLongPolling(
new ClientLongPolling(asyncContext, clientMd5Map, ip, probeRequestSize, timeout, appName, tag));
ConfigExecutor.executeLongPolling是一个Executors.newScheduledThreadPool(1, threadFactory);周期性定时线程。
在创建该线程池的时候,指定了大小为1,并且在THREAD_POOL_MANAGER注册,方便在后期程序退出的时候销毁线程池。这里可以看一下线程池管理的讲解。
//ConfigExecutor.executeLongPolling
public static void executeLongPolling(Runnable runnable) {
LONG_POLLING_EXECUTOR.execute(runnable);
}
private static final ScheduledExecutorService LONG_POLLING_EXECUTOR = ExecutorFactory.Managed
.newSingleScheduledExecutorService(ClassUtils.getCanonicalName(Config.class),
new NameThreadFactory("com.alibaba.nacos.config.LongPolling"));
public static ScheduledExecutorService newSingleScheduledExecutorService(final String group,
final ThreadFactory threadFactory) {
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1, threadFactory);
//省略
return executorService;
}
ClientLongPolling
下面看ClientLongPolling,他是实现了runable接口,实现了run方法,在该实现方法中分为两个部分,首先是一个定时循环线程池,然后是报自己加到一个list集合中。
经过timeout后返回逻辑
在线程池到时后,先删除监听的客户端。删除成功后判断是否是定时的轮询,如果是,判断是否有变化,有变化就返回,没变化就返回为空。如果不是定时的轮询,直接返回。
这里不是定时轮询为什么直接返回,因为在其他地方有一个监听,如果有变化都在监听处会返回。
class ClientLongPolling implements Runnable {
@Override
public void run() {
asyncTimeoutFuture = ConfigExecutor.scheduleLongPolling(() -> {
try {
getRetainIps().put(ClientLongPolling.this.ip, System.currentTimeMillis());
// Delete subscriber's relations.
boolean removeFlag = allSubs.remove(ClientLongPolling.this);
if (removeFlag) {
if (isFixedPolling()) {
//这里判断是否需要返回数据,根据业务情况判断。
List<String> changedGroups = MD5Util
.compareMd5((HttpServletRequest) asyncContext.getRequest(),
(HttpServletResponse) asyncContext.getResponse(), clientMd5Map);
if (changedGroups.size() > 0) {
sendResponse(changedGroups);
} else {
sendResponse(null);
}
} else {
sendResponse(null);
}
} else {
LogUtil.DEFAULT_LOG.warn("client subsciber's relations delete fail.");
}
} catch (Throwable t) {
LogUtil.DEFAULT_LOG.error("long polling error:" + t.getMessage(), t.getCause());
}
}, timeoutTime, TimeUnit.MILLISECONDS);
allSubs.add(this);
}
void sendResponse(List<String> changedGroups) {
// Cancel time out task.
if (null != asyncTimeoutFuture) {
asyncTimeoutFuture.cancel(false);
}
generateResponse(changedGroups);
}
void generateResponse(List<String> changedGroups) {
if (null == changedGroups) {
// Tell web container to send http response.
asyncContext.complete();
return;
}
HttpServletResponse response = (HttpServletResponse) asyncContext.getResponse();
try {
final String respString = MD5Util.compareMd5ResultString(changedGroups);
// Disable cache.
response.setHeader("Pragma", "no-cache");
response.setDateHeader("Expires", 0);
response.setHeader("Cache-Control", "no-cache,no-store");
response.setStatus(HttpServletResponse.SC_OK);
response.getWriter().println(respString);
asyncContext.complete();
} catch (Exception ex) {
PULL_LOG.error(ex.toString(), ex);
asyncContext.complete();
}
}
ClientLongPolling(AsyncContext ac, Map<String, String> clientMd5Map, String ip, int probeRequestSize,
long timeoutTime, String appName, String tag) {
this.asyncContext = ac;
this.clientMd5Map = clientMd5Map;
this.probeRequestSize = probeRequestSize;
this.createTime = System.currentTimeMillis();
this.ip = ip;
this.timeoutTime = timeoutTime;
this.appName = appName;
this.tag = tag;
}
final AsyncContext asyncContext;
final Map<String, String> clientMd5Map;
final long createTime;
final String ip;
final String appName;
final String tag;
final int probeRequestSize;
final long timeoutTime;
Future<?> asyncTimeoutFuture;
}
void generateResponse(HttpServletRequest request, HttpServletResponse response, List<String> changedGroups) {
if (null == changedGroups) {
return;
}
try {
final String respString = MD5Util.compareMd5ResultString(changedGroups);
// Disable cache.
response.setHeader("Pragma", "no-cache");
response.setDateHeader("Expires", 0);
response.setHeader("Cache-Control", "no-cache,no-store");
response.setStatus(HttpServletResponse.SC_OK);
response.getWriter().println(respString);
} catch (Exception ex) {
PULL_LOG.error(ex.toString(), ex);
}
}
timeout未到,但是中途有变更通知
很直接的一个观察者模式。
在ClientLongPolling的run方法我们提到有一个
allSubs.add(this);
其中allSubs是一个final Queue allSubs;
我们回到longPollingService,这里看到它的构造函数。
@SuppressWarnings("PMD.ThreadPoolCreationRule")
public LongPollingService() {
allSubs = new ConcurrentLinkedQueue<>();
//定时更新一些指标信息,比如当前多少在长轮询客户端在监听。
ConfigExecutor.scheduleLongPolling(new StatTask(), 0L, 10L, TimeUnit.SECONDS);
//观察者模式来了
// Register LocalDataChangeEvent to NotifyCenter.
NotifyCenter.registerToPublisher(LocalDataChangeEvent.class, NotifyCenter.ringBufferSize);
// Register A Subscriber to subscribe LocalDataChangeEvent.
NotifyCenter.registerSubscriber(new Subscriber() {
@Override
public void onEvent(Event event) {
if (isFixedPolling()) {
//定时返回的,不用管,他会在固定timeout后做返回
// Ignore.
} else {
if (event instanceof LocalDataChangeEvent) {
//非固定时间返回,将变更的先返回
LocalDataChangeEvent evt = (LocalDataChangeEvent) event;
ConfigExecutor.executeLongPolling(new DataChangeTask(evt.groupKey, evt.isBeta, evt.betaIps));
}
}
}
@Override
public Class<? extends Event> subscribeType() {
return LocalDataChangeEvent.class;
}
});
}
在上面是一个经典的观察者模式,包含了注册,编写回调方法。下面是具体在回调过后的处理。DataChangeTask
class DataChangeTask implements Runnable {
@Override
public void run() {
try {
ConfigCacheService.getContentBetaMd5(groupKey);
for (Iterator<ClientLongPolling> iter = allSubs.iterator(); iter.hasNext(); ) {
ClientLongPolling clientSub = iter.next();
if (clientSub.clientMd5Map.containsKey(groupKey)) {
// If published tag is not in the beta list, then it skipped.
if (isBeta && !CollectionUtils.contains(betaIps, clientSub.ip)) {
continue;
}
// If published tag is not in the tag list, then it skipped.
if (StringUtils.isNotBlank(tag) && !tag.equals(clientSub.tag)) {
continue;
}
getRetainIps().put(clientSub.ip, System.currentTimeMillis());
//移除allSubs中该对象
iter.remove(); // Delete subscribers' relationships.
clientSub.sendResponse(Arrays.asList(groupKey));
}
}
} catch (Throwable t) {
}
}
DataChangeTask(String groupKey, boolean isBeta, List<String> betaIps) {
this(groupKey, isBeta, betaIps, null);
}
DataChangeTask(String groupKey, boolean isBeta, List<String> betaIps, String tag) {
this.groupKey = groupKey;
this.isBeta = isBeta;
this.betaIps = betaIps;
this.tag = tag;
}
final String groupKey;
final long changeTime = System.currentTimeMillis();
final boolean isBeta;
final List<String> betaIps;
final String tag;
}
在其他地方调用
NotifyCenter.publishEvent(new LocalDataChangeEvent(groupKey));
就可以触发事件。
扩展,观察者模式,门面模式
在观察者模式中,有事件,消息生产者——被观察者,消息消费者——观察者
其中事件Event可以保存被观察者传送给观察者的数据。当然也可以是一个简单的实现类。
被观察者需要保存对事件敢兴趣的观察者列表。例如用map。
观察者需要实现onEvent方法,当被观察者触发事件的时候,会调用该事件的观察者的onEvent方法。
下面简单的实现。
NotifyCenter 门面模式。
public class NotifyCenter {
private static final Logger LOGGER = LoggerFactory.getLogger(NotifyCenter.class);
public static int ringBufferSize;
public static int shareBufferSize;
private static final AtomicBoolean CLOSED = new AtomicBoolean(false);
//创建被观察者对象的factory
private static final EventPublisherFactory DEFAULT_PUBLISHER_FACTORY;
//本对象的单例
private static final NotifyCenter INSTANCE = new NotifyCenter();
//默认的被观察者
private DefaultSharePublisher sharePublisher;
private static Class<? extends EventPublisher> clazz;
/** 保存被生产者列表
* Publisher management container.
*/
private final Map<String, EventPublisher> publisherMap = new ConcurrentHashMap<>(16);
/**
* 保存消费者,subscriber.subscribeType获取关注的事件类型
* Register a Subscriber. If the Publisher concerned by the Subscriber does not exist, then PublihserMap will
* preempt a placeholder Publisher with default EventPublisherFactory first.
*
* @param consumer subscriber
*/
public static void registerSubscriber(final Subscriber consumer) {
//1、从publisherMap获取当前事件的publisher消息生产者
//2、将当前consumer保存到publisher的队列中
}
/**
* 注册消息生产者
* Register publisher with default factory.
*
* @param eventType class Instances type of the event type.
* @param queueMaxSize the publisher's queue max size.
*/
public static EventPublisher registerToPublisher(final Class<? extends Event> eventType, final int queueMaxSize) {
//1、从publisherMap获取当前事件的publisher消息生产者
//2、如果没有,就新建publisher并且保存到publisherMap
}
/**
* Request publisher publish event Publishers load lazily, calling publisher. Start () only when the event is
* actually published.
*
* @param event class Instances of the event.
*/
public static boolean publishEvent(final Event event) {
//1、获取当前事件的publisher
//2、publisher获取保存消费者的集合
//3、遍历调用onEvent
}