[Topic] source code analysis - Ali Center Best Open Source Nacos configure long polling implementation principle

Introduction: reading the manual carefully and you will learn web end real-time communication technologies commonly used types and application scenarios, you will learn several different long polling, and their differences finally see the Internet giant Nacos long polling technology to push the message in the face of polling technology requires a long scene scene / chat / distribution center, etc. later, you can write code elegant and bursting performance, the text content looks longer, in fact, most of the the length of the code is implemented may choose to skip or simply look inside the code is run directly on, the IDE may wish to copy and paste operation to see the effect. Further extension of reading some of the proposals have spare capacity throughout the re-read after reading , otherwise it will break ideas.


table of Contents

1.Web end instant messaging technology

Web 1.1 implementation of the common end real-time communication technologies

1.1.1 short poll

1.1.2 Long Polling

1.1.3 long connection

1.1.4websocket

2. Detailed long polling

Benefits 2.1 long polling

Principle 2.2 long polling

2.3 long polling achieved

2.3.1 achieve a: while infinite loop

2.3.2 achieve two: Lock notify + future.get (timeout)

2.3.3 achieve three: schedule + AsyncContext embodiment (Nacos center disposed implementation)

3. Summary


1.Web end instant messaging technology

End Web server that is instant messaging technology can instantly change information transmitted to the client, this scenario is very common in development, such as push notifications, such as online chat, it is a more practical technology, in many scenarios to enhance the user experience the wonders.

Web 1.1 implementation of the common end real-time communication technologies

1.1.1 short poll

Every so often a client sends a request to the server, the server receives a request that is responding to client requests, the easiest way to implement this, but also more practical, but the disadvantage is obvious, is not high real-time performance, and frequent user requests the server will cause a lot of pressure when excessive.

1.1.2 Long Polling

Not respond directly to the server receives a request sent by the client, but the request to hold live for some time, during this period if there are changes in the data, the server will respond if there is no change at some time after arrival before returning request, the client Js after processing in response to the retransmission request ... will continue in this way can significantly reduce the number of requests, reduce server stress, while being able to increase real-time response, and to do good is essentially immediate response of.

1.1.3 long connection

SSE is a long connection H5's new features, full name Server-Sent Events, it may allow the service to push data to the client, the client need not send SSE request to the server, the server when the data changes, the client sends an , can ensure real-time, significantly reduce server stress.

1.1.4websocket

websocket is provided a new protocol H5, full duplex communication can be achieved, and the client service client and server can freely transmit data to each other, the difference between the request and response does not exist.

Above implementations have advantages and disadvantages, without judgment, each have their own application scenarios, do not tangle which technology is better, only more suitable.

In addition Benpian only do a detailed description of long-polling, since recently studied maker of long polling technique, I feel very powerful, admire knees are presented, so share.

2. Detailed long polling

Benefits 2.1 long polling

Having a long polling relatively simple, efficient, low pressure side service, lightweight, fast response, etc., it is widely used in a variety of middleware, distribution center, online chat (e.g., Web qq) other scenes.

Principle 2.2 long polling

The client sends a request to the server, the server does not respond directly after receiving the request, but the request to hold for some time, at this time if the server detects that data changes, interrupted hold, then immediately respond to the client, otherwise, what is not done until it reaches a preset timeout, and then returns a response. in this time requesting to hold live, in fact, is a listener or observer mode, but the focus is how to hold live request?

(Further reading: [Design Mode] - difference and connection mode listener and observer mode https://blog.csdn.net/lovexiaotaozi/article/details/102579360 )

2.3 long polling achieved

Way to achieve long polling server there are many, this introduction are three not in a hurry to write code, haircut ideas:

① If there is no change in the observed object, the server is not on what to do, innocently waiting to get away.

② observed object change occurs once, immediately hold the end state, in response to the request.

③hold data did not occur until the preset timeout change, returns a response (timeout information may contain or may not contain, anyway, the client still have to re-send request ...)

2.3.1 achieve a: while infinite loop

I've posted the complete code, and can be copied directly into your IDE, run:

@RestController
@RequestMapping("/loop")
public class LoopLongPollingController {
    @Autowired
    LoopLongPollingService loopLongPollingService;

    /**
     * 从服务端拉取被变更的数据
     * @return
     */
    @GetMapping("/pull")
    public Result pull() {
        String result = loopLongPollingService.pull();
        return ResultUtil.success(result);
    }

    /**
     * 向服务端推送变更的数据
     * @param data
     * @return
     */
    @GetMapping("/push")
    public Result push(@RequestParam("data") String data) {
        String result = loopLongPollingService.push(data);
        return ResultUtil.success(result);
    }
}
@Data
public class Result<T> {
    private T data;
    private Integer code;
    private Boolean success;
}
public class ResultUtil {
    public static Result success() {
        Result result = new Result();
        result.setCode(200);
        result.setSuccess(true);
        return result;
    }

    public static Result success(Object data) {
        Result result = new Result();
        result.setSuccess(true);
        result.setCode(200);
        result.setData(data);
        return result;
    }
}
@Configuration
public class ThreadPoolConfig {
    @Bean
    public ScheduledExecutorService getScheduledExecutorService() {
        AtomicInteger poolNum = new AtomicInteger(0);
        ScheduledExecutorService scheduler = new ScheduledThreadPoolExecutor(2, r -> {
            Thread t = new Thread(r);
            t.setName("LoopLongPollingThread-" + poolNum.incrementAndGet());
            return t;
        });
        return scheduler;
    }
}
public interface LoopLongPollingService {
    String pull();

    String push(String data);
}
@Service
public class LoopLongPollingServiceImpl implements LoopLongPollingService {
    @Autowired
    ScheduledExecutorService scheduler;
    private LoopPullTask loopPullTask;

    @Override
    public String pull() {
        loopPullTask = new LoopPullTask();
        Future<String> result = scheduler.schedule(loopPullTask, 0L, TimeUnit.MILLISECONDS);
        try {
            return result.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        return "ex";
    }

    @Override
    public String push(String data) {
        Future<String> future = scheduler.schedule(new LoopPushTask(loopPullTask, data), 0L, TimeUnit.MILLISECONDS);
        try {
            return future.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        return "ex";
    }
}
@Slf4j
public class LoopPullTask implements Callable<String> {
    @Getter
    @Setter
    public volatile String data;
    private Long TIME_OUT_MILLIS = 10000L;

    @Override
    public String call() throws Exception {
        Long startTime = System.currentTimeMillis();
        while (true) {
            if (!StringUtils.isEmpty(data)) {
                return data;
            }
            if (isTimeOut(startTime)) {
                log.info("获取数据请求超时" + new Date());
                data = "请求超时";
                return data;
            }
            //减轻CPU压力
            Thread.sleep(200);
        }
    }

    private boolean isTimeOut(Long startTime) {
        Long nowTime = System.currentTimeMillis();
        return nowTime - startTime > TIME_OUT_MILLIS;
    }
}
public class LoopPushTask implements Callable<String> {
    private LoopPullTask loopPullTask;
    private String data;

    public LoopPushTask(LoopPullTask loopPullTask, String data) {
        this.loopPullTask = loopPullTask;
        this.data = data;
    }

    @Override
    public String call() throws Exception {
        loopPullTask.setData(data);
        return "changed";
    }
}

Then I turn the browser access:

http://localhost:8080/loop/pull

http://localhost:8080/loop/push?data=aa

effect:

time out:

If you can show dynamic enough, so the result is not intuitive look, the original data change effect is to pull the pages in the loading process, when the data change page has been accessed, the page loads instantly receive a return, returned content is data after the change, are interested can self-presentation.


Thinking:

Doing so does achieve the desired results, but there is a very serious performance problems when requesting to obtain data has been in a while (true) death cycle, if the process does not have any data changes, CPU resources are wasted in concurrent high scenario, all threads are competing for CPU resources, then while (true) loop, not only valuable CPU resources are wasted, but also easy to overload the server to crash. The possibility of introducing what it means, so that the CPU resources to be used only when the change occurs in the data, the rest is let out to do something else, the answer is yes.

2.3.2 achieve two: Lock notify + future.get (timeout)

Ideas: by Object.wait () blocking pull task thread when changes occur until the data, and then wake it up, so you do not waste CPU resources like the image in front of the while loop, but also promptly inform enough!

In order to distinguish, in place Lock Loop used here, consistent with common code and the remaining above:

@RestController
@RequestMapping("/lock")
public class LockLongPollingController {
    @Autowired
    private LockLongPollingService lockLongPollingService;

    @RequestMapping("/pull")
    public Result pull() {
        String result = lockLongPollingService.pull();
        return ResultUtil.success(result);
    }

    @RequestMapping("/push")
    public Result push(@RequestParam("data") String data) {
        String result = lockLongPollingService.push(data);
        return ResultUtil.success(result);
    }
}
public interface LockLongPollingService {
    String pull();

    String push(String data);
}

 

@Service
public class LockLongPollingServiceImpl implements LockLongPollingService {
    @Autowired
    ScheduledExecutorService scheduler;
    private LockPullTask lockPullTask;
    private Object lock;

    @PostConstruct
    public void post() {
        lock = new Object();
    }

    @Override
    public String pull() {
        lockPullTask = new LockPullTask(lock);
        Future<String> future = scheduler.submit(lockPullTask);
        try {
            return future.get(10000, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            return dealTimeOut();
        }
        return "ex";
    }

    private String dealTimeOut() {
        synchronized (lock) {
            lock.notifyAll();
            lockPullTask.setData("timeout");
        }
        return "timeout";
    }

    @Override
    public String push(String data) {
        Future<String> future = scheduler.schedule(new LockPushTask(lockPullTask, data, lock), 0L,
            TimeUnit.MILLISECONDS);
        try {
            return future.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        return "ex";
    }
}
@Slf4j
public class LockPullTask implements Callable<String> {
    @Getter
    @Setter
    public volatile String data;
    private Object lock;

    public LockPullTask(Object lock) {
        this.lock = lock;
    }

    @Override
    public String call() throws Exception {
        log.info("长轮询任务开启:" + new Date());
        while (StringUtils.isEmpty(data)) {
            synchronized (lock) {
                lock.wait();
            }
        }
        log.info("长轮询任务结束:" + new Date());
        return data;
    }

}
@Slf4j
public class LockPushTask implements Callable<String> {
    private LockPullTask lockPullTask;
    private String data;
    private Object lock;

    public LockPushTask(LockPullTask lockPullTask, String data, Object lock) {
        this.lockPullTask = lockPullTask;
        this.data = data;
        this.lock = lock;
    }

    @Override
    public String call() throws Exception {
        log.info("数据发生变更:" + new Date());
        synchronized (lock) {
            lockPullTask.setData(data);
            lock.notifyAll();
            log.info("数据变更为:" + data);
        }
        return "changed";
    }
}

Test results:

time out:

Thinking:

Doing so has greatly improved in terms of performance, but also solve the problem of the timeliness of notice, but a closer look still some problems, such as: "Ali Baba java Development Manual" in reference to the abnormal not used for process control, Yet here do exception handling process control in overtime, of course, write understandable ...

Is there a way to make the code more elegant?  

2.3.3 achieve three: schedule + AsyncContext embodiment (Nacos center disposed implementation)

Nacos on design considerations quite a lot, in addition to elegant code in question I mentioned earlier, and also need to consider high performance and multi-user subscriptions, and many other issues, for all kinds of details Benpian not discussed, only the core of the long drawn polling part for demonstration.

The basic idea is by asynchronous processing capabilities provided Servlet3.0 after, task request is added to the queue, when the changed data, fetches the corresponding request from the queue, then the response to the request, the interface is responsible for pulling data through delay timeout processing task is completed, if the wait for a timeout time setting data has not changed, take the initiative to push information to complete the timeout response, let's look at the code to achieve:

@RestController
@GetMapping("/nacos")
public class NacosLongPollingController extends HttpServlet {
    @Autowired
    private NacosLongPollingService nacosLongPollingService;

    @RequestMapping("/pull")
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        String dataId = req.getParameter("dataId");
        if (StringUtils.isEmpty(dataId)) {
            throw new IllegalArgumentException("请求参数异常,dataId能为空");
        }
        nacosLongPollingService.doGet(dataId, req, resp);
    }
    //为了在浏览器中演示,我这里先用Get请求,dataId可以区分不同应用的请求
    @GetMapping("/push")
    public Result push(@RequestParam("dataId") String dataId, @RequestParam("data") String data) {
        if (StringUtils.isEmpty(dataId) || StringUtils.isEmpty(data)) {
            throw new IllegalArgumentException("请求参数异常,dataId和data均不能为空");
        }
        nacosLongPollingService.push(dataId, data);
        return ResultUtil.success();
    }
}
public interface NacosLongPollingService {
    void doGet(String dataId, HttpServletRequest req, HttpServletResponse resp);

    void push(String dataId, String data);
}

Service layer Note asyncContext start must start from the currently executing thread doGet method, you can not start an asynchronous thread, otherwise the response will be returned immediately, you can not play the effect of hold. 

@Service
public class NacosLongPollingServiceImpl implements NacosLongPollingService {
    final ScheduledExecutorService scheduler;
    final Queue<NacosPullTask> nacosPullTasks;

    public NacosLongPollingServiceImpl() {
        scheduler = new ScheduledThreadPoolExecutor(1, r -> {
            Thread t = new Thread(r);
            t.setName("NacosLongPollingTask");
            t.setDaemon(true);
            return t;
        });
        nacosPullTasks = new ConcurrentLinkedQueue<>();
        scheduler.scheduleAtFixedRate(() -> System.out.println("线程存活状态:" + new Date()), 0L, 60, TimeUnit.SECONDS);
    }

    @Override
    public void doGet(String dataId, HttpServletRequest req, HttpServletResponse resp) {
        // 一定要由当前HTTP线程调用,如果放在task线程容器会立即发送响应
        final AsyncContext asyncContext = req.startAsync();
        scheduler.execute(new NacosPullTask(nacosPullTasks, scheduler, asyncContext, dataId, req, resp));
    }

    @Override
    public void push(String dataId, String data) {
        scheduler.schedule(new NacosPushTask(dataId, data, nacosPullTasks), 0L, TimeUnit.MILLISECONDS);
    }
}

NacosPullTask ​​responsible for pulling content changes, pay attention to this point in the inner class inside the class itself, rather than a reference to the object of anonymous inner classes.

@Slf4j
public class NacosPullTask implements Runnable {
    Queue<NacosPullTask> nacosPullTasks;
    ScheduledExecutorService scheduler;
    AsyncContext asyncContext;
    String dataId;
    HttpServletRequest req;
    HttpServletResponse resp;

    Future<?> asyncTimeoutFuture;

    public NacosPullTask(Queue<NacosPullTask> nacosPullTasks, ScheduledExecutorService scheduler,
        AsyncContext asyncContext, String dataId, HttpServletRequest req, HttpServletResponse resp) {
        this.nacosPullTasks = nacosPullTasks;
        this.scheduler = scheduler;
        this.asyncContext = asyncContext;
        this.dataId = dataId;
        this.req = req;
        this.resp = resp;
    }

    @Override
    public void run() {
        asyncTimeoutFuture = scheduler.schedule(() -> {
            log.info("10秒后开始执行长轮询任务:" + new Date());
            //这里如果remove this会失败,内部类中的this指向的并非当前对象,而是匿名内部类对象
            nacosPullTasks.remove(NacosPullTask.this);
            //sendResponse(null);
        }, 10, TimeUnit.SECONDS);
        nacosPullTasks.add(this);
    }

    /**
     * 发送响应
     *
     * @param result
     */
    public void sendResponse(String result) {
        System.out.println("发送响应:" + new Date());
        //取消等待执行的任务,避免已经响完了,还有资源被占用
        if (asyncTimeoutFuture != null) {
            //设置为true会立即中断执行中的任务,false对执行中的任务无影响,但会取消等待执行的任务
            asyncTimeoutFuture.cancel(false);
        }

        //设置页码编码
        resp.setContentType("application/json; charset=utf-8");
        resp.setCharacterEncoding("utf-8");

        //禁用缓存
        resp.setHeader("Pragma", "no-cache");
        resp.setHeader("Cache-Control", "no-cache,no-store");
        resp.setDateHeader("Expires", 0);
        resp.setStatus(HttpServletResponse.SC_OK);
        //输出Json流
        sendJsonResult(result);
    }

    /**
     * 发送响应流
     *
     * @param result
     */
    private void sendJsonResult(String result) {
        Result<String> pojoResult = new Result<>();
        pojoResult.setCode(200);
        pojoResult.setSuccess(!StringUtils.isEmpty(result));
        pojoResult.setData(result);
        PrintWriter writer = null;
        try {
            writer = asyncContext.getResponse().getWriter();
            writer.write(pojoResult.toString());
            writer.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            asyncContext.complete();
            if (null != writer) {
                writer.close();
            }
        }
    }
}

NacosPushTask perform data changes: 

public class NacosPushTask implements Runnable {
    private String dataId;
    private String data;
    private Queue<NacosPullTask> nacosPullTasks;

    public NacosPushTask(String dataId, String data,
        Queue<NacosPullTask> nacosPullTasks) {
        this.dataId = dataId;
        this.data = data;
        this.nacosPullTasks = nacosPullTasks;
    }

    @Override
    public void run() {
        Iterator<NacosPullTask> iterator = nacosPullTasks.iterator();
        while (iterator.hasNext()) {
            NacosPullTask nacosPullTask = iterator.next();
            if (dataId.equals(nacosPullTask.dataId)) {
                //可根据内容的MD5判断数据是否发生改变,这里为了演示简单就不写了
                //移除队列中的任务,确保下次请求时响应的task不是上次请求留在队列中的task
                iterator.remove();
                //执行数据变更,发送响应
                nacosPullTask.sendResponse(data);
                break;
            }
        }
    }
}

effect:

Timeout scene:

 

3. Summary

From the viewpoint of difficulty to achieve: to achieve a <achieve two <realize three

From a performance point of view: to achieve a <achieve two <realize three

To achieve three fundamental different from the first two implementations that send a response method to achieve the three controls by themselves, while the front is two ways to springboot control. Control yourself avoiding the kinds of restrictions, more freedom and flexibility. If further design, consider adding a cache of dataId and data encryption to ensure information security, data change MD5 checksum, heartbeat monitoring, high availability and so on ... with a UI interface, you can initially provided a comparable middleware Ali .

If the text is not correct at the welcome message treatise, have questions, you can also leave a message, I see we will respond promptly.

If you think reading this article harvest, welcome attention, and I will continue to share with you and to learn and grow.

 

 

Published 89 original articles · won praise 69 · views 40000 +

Guess you like

Origin blog.csdn.net/lovexiaotaozi/article/details/102775350