HTTP长连接实现“服务器推”的技术快速入门及演示示例


转自: http://blog.csdn.net/xxd851116/article/details/10022015


在我的印象里HTTP是一种“无状态的协议”,也就是不知道以前请求的历史,无法保留上一次请求的结果。
Cookie的诞生,弥补了这个不足,浏览器可以通过本地持久化请求数据来记录上次请求的环境。但这个没有根本上改变HTTP请求本身的这种“客户端请求服务器端相应”模式——客户端是主动的,而服务器是被动的。
最近听说有“HTTP长连接”,去探索了一把,果然很有意思,能够实现“服务器推”的这种概念,也就是服务器是主动发送请求,客户端是被动接受请求。
关于“服务器推”及“HTTP长连接”的概念网上很多,给一个比较系统的介绍文章:
http://www.ibm.com/developerworks/cn/web/wa-lo-comet/

HTTP长连接这种把数据从服务器主动“推”到客户端的技术,能带来的好处不言而喻。它可以把最新的统计数据输出到客户端,也可以实现即时通讯。


简单实例共如下几部分:

1. 准备请求页面

我使用的是velocity,httpConnection.vm

<div id="monitor-window">服务器现在是:<span id="time"></span></div>
<form id="a-form" action="http://127.0.0.1:8080/HttpConnectionServlet" method="post" target="handleFrame">
    <input type="submit" name="submit" id="submit" value=" 获取并监控服务器时间 " />
</form>
<iframe name="handleFrame" id="handleFrame" style="display:none"></iframe>

<!-- <script type="text/javascript" src="http://code.jquery.com/jquery-1.10.2.min.js"></script> -->
<script type="text/javascript">
//<![CDATA[
function showServerTime(msg) {
    $("#time").html(msg);
}
//]]>
</script>


2. web.xml配置服务接收请求

<servlet>
        <servlet-name>HttpConnectionServlet</servlet-name>
        <servlet-class>com.baibutao.apps.linkshop.uxiang.server.web.action.HttpConnectionServlet</servlet-class>
        <init-param>
            <param-name>interval</param-name>
            <param-value>1</param-value>
        </init-param>
    </servlet>
    <servlet-mapping>
        <servlet-name>HttpConnectionServlet</servlet-name>
        <url-pattern>/HttpConnectionServlet</url-pattern>
    </servlet-mapping>


3.核心servlet请求处理

package com.baibutao.apps.linkshop.uxiang.server.web.action;

import java.io.IOException;
import java.util.Date;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import wint.core.service.ServiceContext;
import wint.core.service.bean.BeanFactoryService;
import wint.help.biz.result.Result;
import wint.mvc.holder.WintContext;

import com.baibutao.apps.linkshop.uxiang.server.biz.ao.ItemAO;
import com.baibutao.apps.linkshop.uxiang.server.biz.dal.dataobject.ItemDO;
import com.baibutao.apps.linkshop.uxiang.server.common.SystemClock;
import com.baibutao.apps.linkshop.uxiang.server.util.DateUtil;

/**
 * <p>标题: </p>
 * <p>描述: </p>
 * <p>版权: U箱</p>
 * <p>创建时间: Nov 7, 2014  9:33:47 AM</p>
 * <p>作者:聂鹏</p>
 */
public class HttpConnectionServlet extends HttpServlet {

	private static final long serialVersionUID = -5884316754388397984L;

	private String interval = "1";

	public void init(ServletConfig config) throws ServletException {
		this.interval = config.getInitParameter("interval");
		super.init();
	}

	public void destroy() {
		this.interval = null;
		super.destroy();
	}

	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, java.io.IOException {

		/*
		ServiceContext context = WintContext.getServiceContext();
		BeanFactoryService beanFactoryService = (BeanFactoryService) context.getObject("beanFactoryService");
		itemAO = (ItemAO)beanFactoryService.getBeanFactory().getObject("itemAO");
		Result result = itemAO.viewDetail(883, 11053);
		ItemDO item = null;
		if(result.isSuccess()) {
			item = (ItemDO)result.getModels().get("item");
		}
		*/

		for (int i = 0; i < 3; i++) {
			// Thread.sleep(1000 * Integer.valueOf(interval));
			SystemClock.sleepRandom(2000, 3000);
			writerResponse(response, DateUtil.format(new Date()), "showServerTime");
		}
		return;
	}

	protected void writerResponse(HttpServletResponse response, String body, String client_method) throws IOException {
		StringBuffer sb = new StringBuffer();
		sb.append("<script type=\"text/javascript\">//<![CDATA[\n");
		sb.append("     parent.").append(client_method).append("(\"").append(body).append("\");\n");
		sb.append("//]]></script>");
		System.out.println(sb.toString());

		response.setContentType("text/html;charset=UTF-8");
		response.addHeader("Pragma", "no-cache");
		response.setHeader("Cache-Control", "no-cache,no-store,must-revalidate");
		response.setHeader("Cache-Control", "pre-check=0,post-check=0");
		response.setDateHeader("Expires", 0);
		response.getWriter().write(sb.toString());
		response.flushBuffer();
	}

	public String getInterval() {
		return interval;
	}

	public void setInterval(String interval) {
		this.interval = interval;
	}

}



4.说明

仔细看JSP这段HTML代码,很有意思。

1、form通过POST请求把返回的Script代码输出到iframe中(form的target="handleFrame")。这段返回的Script代码在服务器的控制台输出如下:

<script type="text/javascript">//<![CDATA[
     parent.showServerTime("2014-11-07 10:38:20");
//]]></script>



2、iframe完成加载上面这段代码后解析并执行,这段代码的作用是调用父页面的“showServerTime”方法,把消息传入。

3、父页面showServerTime函数负责处理接收到的消息。

4、Servlet负责定时执行和输出,源源不断向客户端发送内容。

原来,这个隐藏的iframe起到了一个“纽带”的作用。



5.注意点

不要在同一客户端同时使用超过两个的 HTTP 长连接

我们使用 IE 下载文件时会有这样的体验,从同一个 Web 服务器下载文件,最多只能有两个文件同时被下载。第三个文件的下载会被阻塞,直到前面下载的文件下载完毕。这是因为 HTTP 1.1 规范中规定,客户端不应该与服务器端建立超过两个的 HTTP 连接, 新的连接会被阻塞。而 IE 在实现中严格遵守了这种规定。

HTTP 1.1 对两个长连接的限制,会对使用了长连接的 Web 应用带来如下现象:在客户端如果打开超过两个的 IE 窗口去访问同一个使用了长连接的 Web 服务器,第三个 IE 窗口的 HTTP 请求被前两个窗口的长连接阻塞。

所以在开发长连接的应用时, 必须注意在使用了多个 frame 的页面中,不要为每个 frame 的页面都建立一个 HTTP 长连接,这样会阻塞其它的 HTTP 请求,在设计上考虑让多个 frame 的更新共用一个长连接。

服务器端的性能和可扩展性

一般 Web 服务器会为每个连接创建一个线程,如果在大型的商业应用中使用 Comet,服务器端需要维护大量并发的长连接。在这种应用背景下,服务器端需要考虑负载均衡和集群技术;或是在服务器端为长连接作一些改进。

应用和技术的发展总是带来新的需求,从而推动新技术的发展。HTTP 1.1 与 1.0 规范有一个很大的不同:1.0 规范下服务器在处理完每个 Get/Post 请求后会关闭套接口连接; 而 1.1 规范下服务器会保持这个连接,在处理两个请求的间隔时间里,这个连接处于空闲状态。 Java 1.4 引入了支持异步 IO 的 java.nio 包。当连接处于空闲时,为这个连接分配的线程资源会返还到线程池,可以供新的连接使用;当原来处于空闲的连接的客户发出新的请求,会从线程池里分配一个线程资源处理这个请求。 这种技术在连接处于空闲的机率较高、并发连接数目很多的场景下对于降低服务器的资源负载非常有效。

但是 AJAX 的应用使请求的出现变得频繁,而 Comet 则会长时间占用一个连接,上述的服务器模型在新的应用背景下会变得非常低效,线程池里有限的线程数甚至可能会阻塞新的连接。Jetty 6 Web 服务器针对 AJAX、Comet 应用的特点进行了很多创新的改进,请参考文章“AJAX,Comet and Jetty”(请参见参考资源)。

控制信息与数据信息使用不同的 HTTP 连接

使用长连接时,存在一个很常见的场景:客户端网页需要关闭,而服务器端还处在读取数据的堵塞状态,客户端需要及时通知服务器端关闭数据连接。服务器在收到关闭请求后首先要从读取数据的阻塞状态唤醒,然后释放为这个客户端分配的资源,再关闭连接。

所以在设计上,我们需要使客户端的控制请求和数据请求使用不同的 HTTP 连接,才能使控制请求不会被阻塞。

在实现上,如果是基于 iframe 流方式的长连接,客户端页面需要使用两个 iframe,一个是控制帧,用于往服务器端发送控制请求,控制请求能很快收到响应,不会被堵塞;一个是显示帧,用于往服务器端发送长连接请求。如果是基于 AJAX 的长轮询方式,客户端可以异步地发出一个 XMLHttpRequest 请求,通知服务器端关闭数据连接。

在客户和服务器之间保持“心跳”信息

在浏览器与服务器之间维持一个长连接会为通信带来一些不确定性:因为数据传输是随机的,客户端不知道何时服务器才有数据传送。服务器端需要确保当客户端不再工作时,释放为这个客户端分配的资源,防止内存泄漏。因此需要一种机制使双方知道大家都在正常运行。在实现上:

  1. 服务器端在阻塞读时会设置一个时限,超时后阻塞读调用会返回,同时发给客户端没有新数据到达的心跳信息。此时如果客户端已经关闭,服务器往通道写数据会出现异常,服务器端就会及时释放为这个客户端分配的资源。
  2. 如果客户端使用的是基于 AJAX 的长轮询方式;服务器端返回数据、关闭连接后,经过某个时限没有收到客户端的再次请求,会认为客户端不能正常工作,会释放为这个客户端分配、维护的资源。
  3. 当服务器处理信息出现异常情况,需要发送错误信息通知客户端,同时释放资源、关闭连接。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值