1 示例
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><html xmlns="http://www.w3.org/1999/xhtml" xml:lang="de" lang="de"> <head> <meta http-equiv="Content-type" content="text/html; charset=utf-8" /> <meta http-equiv="Content-language" content="de" /> <script type="text/javascript">function arrived(id,text) { var b=document.getElementById(id); b.innerHTML = text; }</script></HEAD><BODY><div>Progressive Loading<div id="content1">-</div> <div id="content2">-</div> <div id="content3">-</div> <div id="content4">-</div> <div id="content5">-</div> <div id="content6">-</div> </div> <script>arrived("content1", "Wohooo1");</script> <script>arrived("content2", "Wohooo2");</script> <script>arrived("content5", "Wohooo5");</script> </BODY></HTML>
先输出框架和占位符,然后服务端分步输出脚本,用脚本渲染html
2 flush最终渲染结果是由tomcat容器做的,在 org.apache.catalina.connector.CoyoteAdapter.service(Request, Response)
/** * End request. * * @throws IOException an underlying I/O error occurred */ @Override public void endRequest() throws IOException { super.endRequest(); if (useSocketBuffer) { socketBuffer.flushBuffer(); } }
@Override public void service(org.apache.coyote.Request req, org.apache.coyote.Response res) throws Exception { Request request = (Request) req.getNote(ADAPTER_NOTES); Response response = (Response) res.getNote(ADAPTER_NOTES); if (request == null) { // Create objects request = connector.createRequest(); request.setCoyoteRequest(req); response = connector.createResponse(); response.setCoyoteResponse(res); // Link objects request.setResponse(response); response.setRequest(request); // Set as notes req.setNote(ADAPTER_NOTES, request); res.setNote(ADAPTER_NOTES, response); // Set query string encoding req.getParameters().setQueryStringEncoding (connector.getURIEncoding()); } if (connector.getXpoweredBy()) { response.addHeader("X-Powered-By", POWERED_BY); } boolean comet = false; boolean async = false; try { // Parse and set Catalina and configuration specific // request parameters req.getRequestProcessor().setWorkerThreadName(Thread.currentThread().getName()); boolean postParseSuccess = postParseRequest(req, request, res, response); if (postParseSuccess) { //check valves if we support async request.setAsyncSupported(connector.getService().getContainer().getPipeline().isAsyncSupported()); // Calling the container connector.getService().getContainer().getPipeline().getFirst().invoke(request, response); if (request.isComet()) { if (!response.isClosed() && !response.isError()) { if (request.getAvailable() || (request.getContentLength() > 0 && (!request.isParametersParsed()))) { // Invoke a read event right away if there are available bytes if (event(req, res, SocketStatus.OPEN)) { comet = true; res.action(ActionCode.COMET_BEGIN, null); } } else { comet = true; res.action(ActionCode.COMET_BEGIN, null); } } else { // Clear the filter chain, as otherwise it will not be reset elsewhere // since this is a Comet request request.setFilterChain(null); } } } AsyncContextImpl asyncConImpl = (AsyncContextImpl)request.getAsyncContext(); if (asyncConImpl != null) { async = true; } else if (!comet) { request.finishRequest(); response.finishResponse(); if (postParseSuccess && request.getMappingData().context != null) { // Log only if processing was invoked. // If postParseRequest() failed, it has already logged it. // If context is null this was the start of a comet request // that failed and has already been logged. ((Context) request.getMappingData().context).logAccess( request, response, System.currentTimeMillis() - req.getStartTime(), false); } req.action(ActionCode.POST_REQUEST , null); } } catch (IOException e) { // Ignore } finally { req.getRequestProcessor().setWorkerThreadName(null); // Recycle the wrapper request and response if (!comet && !async) { request.recycle(); response.recycle(); } else { // Clear converters so that the minimum amount of memory // is used by this processor request.clearEncoders(); response.clearEncoders(); } } }
而socket的关闭也是在tomcat里的连接器里
org.apache.tomcat.util.net.JIoEndpoint.SocketProcessor.run()
boolean launch = false; synchronized (socket) { try { SocketState state = SocketState.OPEN; try { // SSL handshake serverSocketFactory.handshake(socket.getSocket()); } catch (Throwable t) { ExceptionUtils.handleThrowable(t); if (log.isDebugEnabled()) { log.debug(sm.getString("endpoint.err.handshake"), t); } // Tell to close the socket state = SocketState.CLOSED; } if ((state != SocketState.CLOSED)) { if (status == null) { state = handler.process(socket, SocketStatus.OPEN); } else { state = handler.process(socket,status); } } if (state == SocketState.CLOSED) { // Close socket if (log.isTraceEnabled()) { log.trace("Closing socket:"+socket); } countDownConnection(); try { socket.getSocket().close(); } catch (IOException e) { // Ignore } } else if (state == SocketState.OPEN || state == SocketState.UPGRADING || state == SocketState.UPGRADED){ socket.setKeptAlive(true); socket.access(); launch = true; } else if (state == SocketState.LONG) { socket.access(); waitingRequests.add(socket); } } finally { if (launch) { try { getExecutor().execute(new SocketProcessor(socket, SocketStatus.OPEN)); } catch (NullPointerException npe) { if (running) { log.error(sm.getString("endpoint.launch.fail"), npe); } } } } } socket = null; // Finish up this request
3 bigpipe关键步骤是分步输出字符流,在一个连接中,一次http请求-响应中分步输出,要保证所有内容都输出后,才能关闭输出流和socket。
如果是单线程的话,不会有任何问题;如果是多线程处理同一个请求的话,要用一些同步工具,比如latch或者barrier。
参考 http://www.searchtb.com/2011/04/an-introduction-to-bigpipe.html
http://codemonkeyism.com/facebook-bigpipe-java/
4 将bigpipe集成到webx里面,首先是页面框架
<script type="text/javascript">function arrivedHtml(id,text) { var b=document.getElementById(id); b.innerHTML = text; }</script></HEAD><BODY> <div>Progressive Loading <div class="pagelet" id="content1">-</div> <div class="pagelet" id="content2">-</div> <div class="pagelet" id="content3">-</div> <div class="pagelet" id="content4">-</div> </div>
然后分别定义每个content页面,如content1
hello world $name
编写bigpipevalve
package com.alibaba.webx.tutorial1.common; import static com.alibaba.citrus.turbine.TurbineConstant.LAYOUT_TEMPLATE; import static com.alibaba.citrus.turbine.TurbineConstant.SCREEN_MODULE_NO_TEMPLATE; import static com.alibaba.citrus.turbine.TurbineConstant.SCREEN_PLACEHOLDER_KEY; import static com.alibaba.citrus.turbine.TurbineConstant.SCREEN_TEMPLATE; import static com.alibaba.citrus.turbine.util.TurbineUtil.getTurbineRunData; import static com.alibaba.citrus.util.Assert.assertNotNull; import static com.alibaba.citrus.util.BasicConstant.EMPTY_STRING; import static com.alibaba.citrus.util.ObjectUtil.defaultIfNull; import java.io.IOException; import java.util.ArrayList; import java.util.List; import javax.servlet.http.HttpServletRequest; import org.htmlparser.Node; import org.htmlparser.Parser; import org.htmlparser.tags.Div; import org.htmlparser.util.NodeIterator; import org.htmlparser.util.NodeList; import org.htmlparser.util.ParserException; import org.springframework.beans.factory.annotation.Autowired; import com.alibaba.citrus.service.mappingrule.MappingRuleService; import com.alibaba.citrus.service.moduleloader.Module; import com.alibaba.citrus.service.moduleloader.ModuleLoaderException; import com.alibaba.citrus.service.moduleloader.ModuleLoaderService; import com.alibaba.citrus.service.moduleloader.ModuleNotFoundException; import com.alibaba.citrus.service.pipeline.PipelineContext; import com.alibaba.citrus.service.pipeline.support.AbstractValve; import com.alibaba.citrus.service.pipeline.support.AbstractValveDefinitionParser; import com.alibaba.citrus.service.requestcontext.RequestContext; import com.alibaba.citrus.service.requestcontext.basic.BasicRequestContext; import com.alibaba.citrus.service.requestcontext.buffered.BufferedRequestContext; import com.alibaba.citrus.service.template.TemplateException; import com.alibaba.citrus.service.template.TemplateService; import com.alibaba.citrus.turbine.Context; import com.alibaba.citrus.turbine.TurbineConstant; import com.alibaba.citrus.turbine.TurbineRunData; import com.alibaba.citrus.turbine.TurbineRunDataInternal; import com.alibaba.citrus.turbine.support.ContextAdapter; import com.alibaba.citrus.util.StringUtil; import com.alibaba.citrus.webx.WebxException; public class BigPipeValve extends AbstractValve { @Autowired private BufferedRequestContext bufferedRequestContext; @Autowired private BasicRequestContext basicRequestContext; @Autowired private HttpServletRequest request; @Autowired private TemplateService templateService; @Autowired private MappingRuleService mappingRuleService; @Autowired private ModuleLoaderService moduleLoaderService; public void invoke(PipelineContext pipelineContext) throws Exception { // 拿出request,解析出下一批要执行的screen,flush当前渲染的。 // 遍历执行PerformScreenValve和RenderTemplateValve TurbineRunDataInternal rundata = (TurbineRunDataInternal) getTurbineRunData(request); System.out.println("valve started."); RequestContext tempRC = basicRequestContext.getWrappedRequestContext(); String body = bufferedRequestContext.popCharBuffer(); System.out.println(body); List<String> pageletList = parsePagelet(body); tempRC.getResponse().getWriter().write(body); tempRC.getResponse().getWriter().flush(); for (String id : pageletList) { rundata.setTarget("//" + id); rundata.setLayoutEnabled(false); performScreenModule(rundata); render(rundata); body = bufferedRequestContext.popCharBuffer(); body = renderPagelet(id, body); System.out.println(body); tempRC.getResponse().getWriter().write(body); tempRC.getResponse().getWriter().flush(); } pipelineContext.invokeNext(); // 调用后序valves System.out.println("valve ended."); } private String renderPagelet(String id, String body) { body = body.replace("\r\n", "").replace("\n", "").replace("\"", "\'"); // body = StringEscapeUtils.escapeJavaScript(body); String pagelet = "<script>arrivedHtml(\"contentId\", \"body\");</script>"; return pagelet.replace("contentId", id).replace("body", body); } private List<String> parsePagelet(String body) throws ParserException { Parser parser = new Parser(); parser.setInputHTML(body); NodeIterator iterator = parser.elements(); Node node = iterator.nextNode(); node = iterator.nextNode(); node = iterator.nextNode(); List<String> pageletList = new ArrayList<String>(); travel(node, pageletList); return pageletList; } private void travel(Node node, List<String> pageletList) { if (node == null) { return; } else { if (node instanceof Div) { String cls = ((Div) node).getAttribute("class"); if (cls != null && cls.contains("pagelet")) { System.out.println(((Div) node).getAttribute("id")); pageletList.add(((Div) node).getAttribute("id")); } } } NodeList childrens = node.getChildren(); if (childrens == null || childrens.size() == 0) { return; } for (int i = 0; i < childrens.size(); i++) { Node temp = childrens.elementAt(i); travel(temp, pageletList); } } private void render(TurbineRunDataInternal rundata) throws TemplateException, IOException { String target = assertNotNull(rundata.getTarget(), "Target was not specified"); // 妫�煡閲嶅畾鍚戞爣蹇楋紝濡傛灉鏄噸瀹氬悜锛屽垯涓嶉渶瑕佸皢椤甸潰杈撳嚭銆� if (!rundata.isRedirected()) { Context context = rundata.getContext(); renderTemplate(getScreenTemplate(target), context, rundata); // layout鍙绂佺敤銆� if (rundata.isLayoutEnabled()) { String layoutTemplateOverride = rundata.getLayoutTemplateOverride(); if (layoutTemplateOverride != null) { target = layoutTemplateOverride; } String layoutTemplate = getLayoutTemplate(target); if (templateService.exists(layoutTemplate)) { String screenContent = defaultIfNull(bufferedRequestContext.popCharBuffer(), EMPTY_STRING); context.put(SCREEN_PLACEHOLDER_KEY, screenContent); renderTemplate(layoutTemplate, context, rundata); } } } } /** 璁剧疆content type銆� */ protected void setContentType(TurbineRunData rundata) { // 璁剧疆content // type锛屼笉闇�璁剧疆charset锛屽洜涓篠etLocaleRequestContext宸茬粡璁剧疆浜哻harset銆� // 閬垮厤瑕嗙洊鍒汉璁剧疆鐨刢ontentType銆� if (StringUtil.isEmpty(rundata.getResponse().getContentType())) { rundata.getResponse().setContentType("text/html"); } } /** 鎵цscreen妯″潡銆� */ protected void performScreenModule(TurbineRunData rundata) { String target = assertNotNull(rundata.getTarget(), "Target was not specified"); // 浠巘arget涓彇寰梥creen module鍚嶇О String moduleName = getModuleName(target); try { Module module = moduleLoaderService.getModuleQuiet(TurbineConstant.SCREEN_MODULE, moduleName); // 褰撴寚瀹氫簡templateName鏃讹紝鍙互娌℃湁鐨剆creen module锛岃�鍗曞崟娓叉煋妯℃澘銆� // 杩欐牱灏卞疄鐜颁簡page-driven锛屽嵆鍏堝啓妯℃澘锛屽繀瑕佹椂鍐嶅啓涓�釜module class涓庝箣瀵瑰簲銆� if (module != null) { module.execute(); } else { if (isScreenModuleRequired()) { throw new ModuleNotFoundException("Could not find screen module: " + moduleName); } } } catch (ModuleLoaderException e) { throw new WebxException("Failed to load screen module: " + moduleName, e); } catch (Exception e) { throw new WebxException("Failed to execute screen: " + moduleName, e); } } /** 濡傛灉杩斿洖<code>true</code>锛岄偅涔堝綋妯″潡鎵句笉鍒版椂锛屼細鎶涘紓甯搞�瀛愮被鍙互瑕嗙洊姝ゆ柟娉曪紝浠ユ敼鍙樿涓恒� */ protected boolean isScreenModuleRequired() { return false; } /** 鏍规嵁target鍙栧緱screen妯″潡鍚嶃�瀛愮被鍙互淇敼鏄犲皠瑙勫垯銆� */ protected String getModuleName(String target) { return mappingRuleService.getMappedName(SCREEN_MODULE_NO_TEMPLATE, target); } protected String getScreenTemplate(String target) { return mappingRuleService.getMappedName(SCREEN_TEMPLATE, target); } protected String getLayoutTemplate(String target) { return mappingRuleService.getMappedName(LAYOUT_TEMPLATE, target); } protected void renderTemplate(String templateName, Context context, TurbineRunDataInternal rundata) throws TemplateException, IOException { rundata.pushContext(context); try { templateService.writeTo(templateName, new ContextAdapter(context), rundata.getResponse().getWriter()); } finally { rundata.popContext(); } } public static class DefinitionParser extends AbstractValveDefinitionParser<BigPipeValve> { } }
pipeline配置
<?xml version="1.0" encoding="UTF-8" ?> <beans:beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:services="http://www.alibaba.com/schema/services" xmlns:pl-conditions="http://www.alibaba.com/schema/services/pipeline/conditions" xmlns:pl-valves="http://www.alibaba.com/schema/services/pipeline/valves" xmlns:beans="http://www.springframework.org/schema/beans" xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation=" http://www.alibaba.com/schema/services http://localhost:8080/schema/services.xsd http://www.alibaba.com/schema/services/pipeline/conditions http://localhost:8080/schema/services-pipeline-conditions.xsd http://www.alibaba.com/schema/services/pipeline/valves http://localhost:8080/schema/services-pipeline-valves.xsd http://www.springframework.org/schema/beans http://localhost:8080/schema/www.springframework.org/schema/beans/spring-beans.xsd "> <services:pipeline xmlns="http://www.alibaba.com/schema/services/pipeline/valves"> <!-- 初始化turbine rundata,并在pipelineContext中设置可能会用到的对象(如rundata、utils),以便valve取得。 --> <prepareForTurbine /> <!-- 设置日志系统的上下文,支持把当前请求的详情打印在日志中。 --> <setLoggingContext /> <!-- 分析URL,取得target。 --> <analyzeURL /> <!-- 检查csrf token,防止csrf攻击和重复提交。假如request和session中的token不匹配,则出错,或显示expired页面。 --> <checkCsrfToken /> <loop> <choose> <when> <!-- 执行带模板的screen,默认有layout。 --> <pl-conditions:target-extension-condition extension="null, vm, jsp, jspx" /> <performAction /> <performTemplateScreen /> <renderTemplate /> </when> <when> <!-- 执行不带模板的screen,默认无layout。 --> <pl-conditions:target-extension-condition extension="do" /> <performAction /> <performScreen /> </when> <otherwise> <!-- 将控制交还给servlet engine。 --> <exit /> </otherwise> </choose> <!-- 假如rundata.setRedirectTarget()被设置,则循环,否则退出循环。 --> <breakUnlessTargetRedirected /> </loop> <valve class="com.alibaba.webx.tutorial1.common.BigPipeValve" /> </services:pipeline> </beans:beans>