java 实现长轮询(LongPolling)

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
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值