先回顾一下客户端和服务端交互的过程
服务端
入口
直接看长轮询的接口
ConfigController.listener
@PostMapping("/listener")
@Secured(action = ActionTypes.READ, parser = ConfigResourceParser.class)
public void listener(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
request.setAttribute("org.apache.catalina.ASYNC_SUPPORTED", true);
String probeModify = request.getParameter("Listening-Configs");
if (StringUtils.isBlank(probeModify)) {
throw new IllegalArgumentException("invalid probeModify");
}
log.info("listen config id:" + probeModify);
probeModify = URLDecoder.decode(probeModify, Constants.ENCODE);
Map<String, String> clientMd5Map;
try {
// 处理数据,将拼接的字符串 转成 key-content
clientMd5Map = MD5Util.getClientMd5Map(probeModify);
} catch (Throwable e) {
throw new IllegalArgumentException("invalid probeModify");
}
log.info("listen config id 2:" + probeModify);
// do long-polling
inner.doPollingConfig(request, response, clientMd5Map, probeModify.length());
}
ConfigServletInner.doPollingConfig()
public String doPollingConfig(HttpServletRequest request, HttpServletResponse response,
Map<String, String> clientMd5Map, int probeRequestSize)
throws IOException {
// 长轮询
// 长轮询判断,根据请求头的 Long-Pulling-Timeout
if (LongPollingService.isSupportLongPolling(request)) {
longPollingService.addLongPollingClient(request, response, clientMd5Map, probeRequestSize);
return HttpServletResponse.SC_OK + "";
}
// else 兼容短轮询逻辑
// 如果是短轮询 走下面的请求, 下面的请求就是把客户端传过来的数据和服务端的数据逐项进行比较,保存到changeGroup中。
List<String> changedGroups = MD5Util.compareMd5(request, response, clientMd5Map);
// 兼容短轮询result
String oldResult = MD5Util.compareMd5OldResult(changedGroups);
String newResult = MD5Util.compareMd5ResultString(changedGroups);
String version = request.getHeader(Constants.CLIENT_VERSION_HEADER);
if (version == null) {
version = "2.0.0";
}
int versionNum = Protocol.getVersionNumber(version);
/**
* 2.0.4版本以前, 返回值放入header中
*/
if (versionNum < START_LONGPOLLING_VERSION_NUM) {
response.addHeader(Constants.PROBE_MODIFY_RESPONSE, oldResult);
response.addHeader(Constants.PROBE_MODIFY_RESPONSE_NEW, newResult);
} else {
request.setAttribute("content", newResult);
}
Loggers.AUTH.info("new content:" + newResult);
// 禁用缓存
response.setHeader("Pragma", "no-cache");
response.setDateHeader("Expires", 0);
response.setHeader("Cache-Control", "no-cache,no-store");
response.setStatus(HttpServletResponse.SC_OK);
return HttpServletResponse.SC_OK + "";
}
上面的代码主要是分两种情况:
1、长轮询请求
2、短轮询请求:直接进行比较,保存到changeGroups中
短轮询比较简单,下面来分析下长轮询都做了哪些事情:
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);
/**
* 提前500ms返回响应,为避免客户端超时 @qiaoyi.dingqy 2013.10.22改动 add delay time for LoadBalance
*/
long timeout = Math.max(10000, Long.parseLong(str) - delayTime);
// 判断,如果为true,定时任务将会在30s后开始执行,否则在 29.5s开始执行
if (isFixedPolling()) {
timeout = Math.max(10000, getFixedPollingInterval());
// do nothing but set fix polling timeout
} else {
long start = System.currentTimeMillis();
// 和服务端的数据进行 MD5 比较,如果发现变化则直接返回
List<String> changedGroups = MD5Util.compareMd5(req, rsp, clientMd5Map);
if (changedGroups.size() > 0) {
generateResponse(req, rsp, changedGroups);
LogUtil.clientLog.info("{}|{}|{}|{}|{}|{}|{}",
System.currentTimeMillis() - start, "instant", RequestUtil.getRemoteIp(req), "polling",
clientMd5Map.size(), probeRequestSize, changedGroups.size());
return;
} else if (noHangUpFlag != null && noHangUpFlag.equalsIgnoreCase(TRUE_STR)) {
LogUtil.clientLog.info("{}|{}|{}|{}|{}|{}|{}", System.currentTimeMillis() - start, "nohangup",
RequestUtil.getRemoteIp(req), "polling", clientMd5Map.size(), probeRequestSize,
changedGroups.size());
return;
}
}
String ip = RequestUtil.getRemoteIp(req);
// 一定要由HTTP线程调用,否则离开后容器会立即发送响应
// 把当前请求转化为一个异步请求,意味着此时tomcat线程被释放,
// 也就是客户端的请求,需要通过 asyncContext 来手动触发返回, 否则一直挂起
final AsyncContext asyncContext = req.startAsync();
// AsyncContext.setTimeout()的超时时间不准,所以只能自己控制
asyncContext.setTimeout(0L);
// 执行长轮询请求
scheduler.execute(
new ClientLongPolling(asyncContext, clientMd5Map, ip, probeRequestSize, timeout, appName, tag));
}
1、获取客户端请求的超时时间,减去500ms后赋值给timeout变量.
2、判断isFixedPolling,如果为true,定时任务将会在30s后开始执行,否则在29.5s后执行
3、如果不是isFixedPolling,则会先和服务端的数据进行MD5对比,如果发生变化直接返回
4、如果没有变化,就会把请求转化为异步请求挂起,然后延迟执行ClientLongPolling线程
长轮询的延迟任务
因为ClientLongPolling是一个runnable线程,可以看下他的run方法
public void run() {
// 延迟执行任务
asyncTimeoutFuture = scheduler.schedule(new Runnable() {
@Override
public void run() {
try {
getRetainIps().put(ClientLongPolling.this.ip, System.currentTimeMillis());
/**
* 删除订阅关系
*/
allSubs.remove(ClientLongPolling.this);
if (isFixedPolling()) {
LogUtil.clientLog.info("{}|{}|{}|{}|{}|{}",
(System.currentTimeMillis() - createTime),
"fix", RequestUtil.getRemoteIp((HttpServletRequest)asyncContext.getRequest()),
"polling",
clientMd5Map.size(), probeRequestSize);
// 通过MD5比较客户端请求的groupKeys是否发生变更,并将变更结果通过response返回给客户端
List<String> changedGroups = MD5Util.compareMd5(
(HttpServletRequest)asyncContext.getRequest(),
(HttpServletResponse)asyncContext.getResponse(), clientMd5Map);
if (changedGroups.size() > 0) {
// 返回给客户端
sendResponse(changedGroups);
} else {
sendResponse(null);
}
} else {
LogUtil.clientLog.info("{}|{}|{}|{}|{}|{}",
(System.currentTimeMillis() - createTime),
"timeout", RequestUtil.getRemoteIp((HttpServletRequest)asyncContext.getRequest()),
"polling",
clientMd5Map.size(), probeRequestSize);
sendResponse(null);
}
} catch (Throwable t) {
LogUtil.defaultLog.error("long polling error:" + t.getMessage(), t.getCause());
}
}
}, timeoutTime, TimeUnit.MILLISECONDS);
// 添加到订阅队列中
allSubs.add(this);
}
ClientLongPolling.run方法立有一个延迟任务,执行的时候,通过比较MD5判断客户端请求的groupKeys是否发生变更,并将变更结果通过response返回给客户端.
数据变更事件
上面讲到有一个30s的周期,如果这段时间内配置变了,那不就不能得到及时更新了吗?
当然不是了,注意上面的一个变量:allSubs
从上面的逻辑不难看出,在延迟任务执行前的这30s内,ClientLongPolling是一直存在于队列中,因为执行完后就被添加到了队列中,执行时才会移除,所有我们需要看看这个是干什么的
LongPollingService
继承了AbstractEventListener
可以看下LongPollingService的构造器
public LongPollingService() {
// 初始化 allSubs
allSubs = new ConcurrentLinkedQueue<ClientLongPolling>();
scheduler = Executors.newScheduledThreadPool(1, new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setDaemon(true);
t.setName("com.alibaba.nacos.LongPolling");
return t;
}
});
scheduler.scheduleWithFixedDelay(new StatTask(), 0L, 10L, TimeUnit.SECONDS);
}
就会调用AbstractEventListener的构造器
public AbstractEventListener() {
/**
* automatic register
*/
// 添加了监听器
EventDispatcher.addEventListener(this);
}
监听的事件
public void onEvent(Event event) {
if (isFixedPolling()) {
// ignore
} else {
if (event instanceof LocalDataChangeEvent) {
// 监听 LocalDataChangeEvent 事件
LocalDataChangeEvent evt = (LocalDataChangeEvent)event;
// 创建 DataChangeTask
scheduler.execute(new DataChangeTask(evt.groupKey, evt.isBeta, evt.betaIps));
}
}
}
在LongPollingService
生成的时候,订阅了一个LocalDataChangeEvent
事件,触发这个事件的时候会执行一个DataChangeTask
异步任务
下面看下DataChangeTask里的run方法
public void run() {
try {
ConfigService.getContentBetaMd5(groupKey);
// 遍历队列中的所有的ClientLongPolling
for (Iterator<ClientLongPolling> iter = allSubs.iterator(); iter.hasNext(); ) {
ClientLongPolling clientSub = iter.next();
// 判断当前的 ClientLongPolling中,请求的key是否包含当前修改的groupKey
if (clientSub.clientMd5Map.containsKey(groupKey)) {
// 如果beta发布且不在beta列表直接跳过
if (isBeta && !betaIps.contains(clientSub.ip)) {
continue;
}
// 如果tag发布且不在tag列表直接跳过
if (StringUtils.isNotBlank(tag) && !tag.equals(clientSub.tag)) {
continue;
}
getRetainIps().put(clientSub.ip, System.currentTimeMillis());
iter.remove(); // 删除订阅关系
LogUtil.clientLog.info("{}|{}|{}|{}|{}|{}|{}",
(System.currentTimeMillis() - changeTime),
"in-advance",
RequestUtil.getRemoteIp((HttpServletRequest)clientSub.asyncContext.getRequest()),
"polling",
clientSub.clientMd5Map.size(), clientSub.probeRequestSize, groupKey);
// 响应客户端消息
clientSub.sendResponse(Arrays.asList(groupKey));
}
}
} catch (Throwable t) {
LogUtil.defaultLog.error("data change error:" + t.getMessage(), t.getCause());
}
}
1.会遍历队列中所有的ClientLongPolling对象
2.判断请求过来的需要对比的key里面是否包含当前变更的配置key
3.包含则移除出队列,并直接返回响应客户端信息
总结
- 配置分三种形态,本地配置文件,本地缓存文件,本地缓存数据
- 客户端通过主动拉取和长轮询的方式来获取配置以及更新配置
- 主动拉取的顺序是本地配置文件 —> 服务端 —> 本地缓存文件
- 客户端长轮询中对比配置不同的方式是对比本地文件与本地缓存数据的MD5
- 长轮询是在客户端与服务端对比配置不同中发起的,存在不同配置服务端则立刻返回,没有则服务端会保持长连接延迟执行任务(30s左右),这中间服务端一旦有配置变更(LocalDataChangeEvent事件)则会提前响应返回
- 长轮询在获取到不同的配置后还会遍历这些配置主动拉取一次获取具体配置内容并写入本地缓存文件中