使用JAVA实现BigPipe服务端的例子在网上很难寻觅,笔者经过多次尝试,在Servlet3.0和Servlet2.5规范下成功实现了BigPipe的分段输出效果。好东西不敢独享,在这里与大家分享。
要搭建BigPipe服务端程序,首先我们必须了解BigPipe服务端的工作原理。BigPipe的最终目标是通过一次请求向浏览器输出页面框架和若干个Pagelet。浏览器在接收完页面框架后立即进行展现,然后并行的接收后面的Pagelets,从而加快页面的渲染速度。Pagelet是通过调用前端的一个JS方法向页面输出的。
为了达到一次请求多次输出的效果,服务端程序必须采用分段输出的技术(chunked)。这里所说的chunked是指HTTP 1.1规范中定义的一个特殊的HTTP头信息,下面的一段HTTP响应头包含了chunked定义:
HTTP/1.1 200 OK Content-Type: text/html;charset=UTF-8 Transfer-Encoding: chunked
通过在HTTP响应头中增加“Transfer-Encoding: chunked”标识,我们通知浏览器后面的响应内容是分段输出的,每段输出使用一个16进制的数字来声明段长度,然后紧跟一个回行换行符,后面是分段的内容体,最后使用一个0长度的分段来结束整个的chunked输出。下面是一个用Socket模拟的chunked分段输出:
public static void main(String[] args) throws IOException, InterruptedException { StringBuffer content1 = new StringBuffer("<html><body>"); content1.append("<div id='a1' style='width:100px;height:100px;border:1px solid black'></div>"); content1.append("<div id='a2' style='width:100px;height:100px;border:1px solid black'></div>"); content1.append("<div id='a3' style='width:100px;height:100px;border:1px solid black'></div>"); content1.append("<div id='a4' style='width:100px;height:100px;border:1px solid black'></div>"); content1.append("</body></html>"); String content2 = "<script type='text/javascript'>document.getElementById('a1').innerHTML='<h1>1</h1>'</script>"; String content3 = "<script type='text/javascript'>document.getElementById('a2').innerHTML='<h1>2</h1>'</script>"; String content4 = "<script type='text/javascript'>document.getElementById('a3').innerHTML='<h1>3</h1>'</script>"; String content5 = "<script type='text/javascript'>document.getElementById('a4').innerHTML='<h1>4</h1>'</script>"; ServerSocket ss = new ServerSocket(80); Socket s; while (true) { s = ss.accept(); s.getInputStream().read(); PrintWriter pw = new PrintWriter(new OutputStreamWriter(s.getOutputStream(), "UTF-8")); //输出响应头 pw.println("HTTP/1.1 200 OK"); pw.println("Content-Type: text/html;charset=UTF-8"); //声明分段输出 pw.println("Transfer-Encoding: chunked"); pw.println(); //第一段 //先输出段长度,注意必须是16进制,加2是因为后面有个回车换行符 pw.println(Integer.toHexString(content1.length() + 2)); //输出分段内容 pw.println(content1.toString()); //输出回车换行符结束本段输出 pw.println(); //立即清空输出缓冲区,通知浏览器立即处理本段内容 pw.flush(); //暂停1秒模拟后台处理过程 Thread.sleep(1000); //第二段 pw.println(Integer.toHexString(content2.length() + 2)); pw.println(content2); pw.println(); pw.flush(); Thread.sleep(1000); //第三段 pw.println(Integer.toHexString(content3.length() + 2)); pw.println(content3); pw.println(); pw.flush(); Thread.sleep(1000); //第四段 pw.println(Integer.toHexString(content4.length() + 2)); pw.println(content4); pw.println(); pw.flush(); Thread.sleep(1000); //第五段 pw.println(Integer.toHexString(content5.length() + 2)); pw.println(content5); pw.println(); pw.flush(); //输出一个0长段结束整个输出 pw.println(0); pw.println(); pw.close(); //最后别忘了关闭输出流 s.close(); } }运行上面的代码,在浏览器中输入"http://localhost"应该就可以看到分段输出的效果了,注意和传统的AJAX异步请求不同,这里只有一次请求!
@WebServlet(value = { "/syncServlet" }, asyncSupported = true, loadOnStartup = 1) public class SyncTestServlet extends HttpServlet { private static final long serialVersionUID = -126107068129496624L; private final BlockingQueue<Runnable> quere = new ArrayBlockingQueue<Runnable>(10); private final ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 10, 10, TimeUnit.HOURS, quere); public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html"); final AsyncContext sc = request.startAsync(request, response); final StringBuffer content = new StringBuffer("<html><body>"); content.append("<div id='a1' style='width:100px;height:100px;border:1px solid black'></div>"); content.append("<div id='a2' style='width:100px;height:100px;border:1px solid black'></div>"); content.append("<div id='a3' style='width:100px;height:100px;border:1px solid black'></div>"); content.append("<div id='a4' style='width:100px;height:100px;border:1px solid black'></div>"); content.append("</body></html>"); final PrintWriter pw = response.getWriter(); pw.println(content.toString()); pw.flush(); executor.execute(new Job(sc, 1)); executor.execute(new Job(sc, 2)); executor.execute(new Job(sc, 3)); } } class Job implements Runnable { private static int count; private AsyncContext syncContext; private int no; public Job(AsyncContext syncContext, int no) { this.syncContext = syncContext; this.no = no; count++; } public void run() { HttpServletResponse resp = (HttpServletResponse) syncContext.getResponse(); try { Thread.sleep(no * 1000); PrintWriter out = resp.getWriter(); out.println("<script type='text/javascript'>document.all.a" + no + ".innerHTML='<h1>" + no + "</h1>'</script>"); out.flush(); } catch (IOException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } count--; if (count == 0) { syncContext.complete(); } } }下面是使用旧的Servlet规范实现分段输出的代码,由于旧的Servet规范没有包含异步处理功能,我们只能自己处理线程的同步和页面输出流的抢占问题。
public class BigpipeServlet extends HttpServlet { private static final long serialVersionUID = 1L; protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=UTF-8"); final PrintWriter pw = response.getWriter(); final StringBuffer content = new StringBuffer("<html><body>"); content.append("<div id='a1' style='width:100px;height:100px;border:1px solid black'></div>"); content.append("<div id='a2' style='width:100px;height:100px;border:1px solid black'></div>"); content.append("<div id='a3' style='width:100px;height:100px;border:1px solid black'></div>"); content.append("<div id='a4' style='width:100px;height:100px;border:1px solid black'></div>"); content.append("</body></html>"); pw.println(content.toString()); pw.flush(); final Paglet paglet1 = new Paglet(pw, 1); final Paglet paglet2 = new Paglet(pw, 2); final Paglet paglet3 = new Paglet(pw, 3); paglet1.start(); paglet2.start(); paglet3.start(); try { paglet3.join(); paglet2.join(); paglet1.join(); } catch (InterruptedException e) { e.printStackTrace(); } pw.close(); } class Paglet extends Thread { private int id; private PrintWriter out; public Paglet(PrintWriter out, int id) { this.out = out; this.id = id; } @Override public void run() { try { sleep(id * 1000); final String content = "<script>document.all.a" + id + ".innerHTML='<h1>" + id + "</h1>'</script>"; synchronized (out) { out.println(content); out.println(); out.flush(); } } catch (InterruptedException e) { e.printStackTrace(); } } } }
更多精彩原创文章请关注笔者的原创博客:http://www.coolfancy.com