基于HTTP长轮询实现简单推送(webim系统)

应用场景:设备为安卓、PC以及服务器,要求PC端能够单向给移动端发送消息指令,安卓端解析消息,进行后续处理动作。其中安卓端为基于Phonegap开发,说白了,就是HTML+JS。
规模:正常应用为200移动端,PC端数量有限,不超过10台,最多移动端为不超过500台。
可以看出这是一个很小规模的应用,也正如此,才可以给我像这样大方的保有HTTP连接不释放的机会。

当前背景:目前关于推送的实现,无非就是谷歌,HTML5的websocket,韩国某牛写的androidpn,以及第三方和伪推送方式。
谷歌的推送在中国大陆据说不稳定,所以被中国人弃之不用,然后就是HTML5的websocket居然在安卓4.0的机器上还不能被很好的支持,这些足以让那位韩国人写的androidpn在国内火了一阵子,不过后来因为国内的第三方推送开始发力,大部分应用开发者只要不是特别需要的话,就不会自己再做推送了。而伪推送方式,无外乎就是HTTP的长连接或者AJAX的长轮询,以及iframe流的方式(或许还有其他方式),这种技术就被称为comet。

基本原理:安卓端页面不间断的发起轮询请求,服务器接收请求后,如果没有消息可以返回,就先不释放连接,即线程等待,等待超时或者中途被唤醒后,返回给页面,释放连接,安卓端的页面再次发起轮询请求。
服务器端接收到PC端指令后,唤醒等待线程,让安卓端的下次轮询可以获取消息。

 

代码实现:

服务器端启动时,像ServletContext内添加一个map用于存储PC端像安卓端发送的消息。

public class AppListener implements ServletContextListener{//监听ServletContext的初始化
@Override
public void contextDestroyed(ServletContextEvent arg0) {// TODO Auto-generated method stub
    }

    @Override
public void contextInitialized(ServletContextEvent event) {// TODO Auto-generated method stub
event.getServletContext().setAttribute(Constant.IMMSG, new HashMap<String,String>());
System.out.println("添加Map成功");
    }
}

 

View Code
接收安卓端长轮询的servlet:

private static int num=0;
public void service(HttpServletRequest req,HttpServletResponse res) 
throws UnsupportedEncodingException{
req.setCharacterEncoding("UTF-8");
        res.setContentType("text/html,charset=UTF-8");
        ServletContext application=req.getSession().getServletContext();
        req.getParameter("userID");
        HashMap<String,String>msg=new HashMap<String,String>();
        Map<String,String> map=((Map)application.getAttribute(Constant.IMMSG));
synchronized(map){
            String temp=map.remove(req.getParameter("userID"));
if(temp==null||temp.trim().equals("")){
try {
System.out.println("休眠等待60秒"+(++num));
map.wait(60000);//服务器保留此连接60秒
msg.put("msg","nomsg");//没有消息时,返回nomsg
} catch (InterruptedException e) {// TODO Auto-generated catch block
msg.put("msg", "error");
e.printStackTrace();
}
}else{
msg.put("msg", temp);//如果有消息,则立刻返回
}
}
PrintWriter out;
try {
out = res.getWriter();
out.print(JSONObject.fromObject(msg));
out.flush();
out.close();
System.out.println("----等待数"+(--num));
} catch (IOException e) {// TODO Auto-generated catch block
e.printStackTrace();
}
}

 

View Code
接收PC端消息的Servlet:

public class SendMsgService extends HttpServlet {
public void service(HttpServletRequest req,HttpServletResponse res) 
throws UnsupportedEncodingException{
req.setCharacterEncoding("UTF-8");
res.setContentType("text/html,charset=UTF-8");
ServletContext application=req.getSession().getServletContext();
Map<String,String> map=((Map)application.getAttribute(Constant.IMMSG));
synchronized(map){
map.put(req.getParameter("userID"), req.getParameter("msg"));
map.notifyAll();//通知所有等待的线程,让安卓端发起下次轮询
}
}

 

所谓安卓端的页面就是简单的js,在一次请求结束后发起下一次请求而已。

 

function longPolling(){
         $.ajax({             //url:ip+'/haveMsg',
url:"http://192.168.1.109:8081/mobileinspect/haveMsg",
             data:{'userID':111},
             dataType:'json',
             timeout:70000,
             cache:false,
             type:"post",
             success:function(data){
if(data.msg){
if(data.msg=="nomsg"){
                         window.setTimeout(longPolling,1000)
                     }else{
                        navigator.notification.confirm(data.msg,onConfirm,"新的消息","接受,拒绝");
                        window.setTimeout(navigator.notification.beep(1),100);
                        window.setTimeout(longPolling,1000)
                     }
                 }
             },
             error:function(xhr,err){//如果出现错误,则在十秒钟之后,再进行长轮询
window.setTimeout(longPolling,10000)
             }
         })

 }

 

 

然后就是修改Tomcat的最大连接数,以让服务器能够处理这么多的连接而不至于停止响应:

<Connector connectionTimeout="20000" port="8081" protocol="HTTP/1.1" redirectPort="8443" maxThreads="600" acceptCount="100"/>

针对我的这个应用,最大600个处理线程足以应付那500台机器了。单纯我的办公电脑就可以支持发起500个HTTP连接,并由本地的Tomcat处理,相信服务器更能够轻松应付。
另外,需要注意的是:据说单机windows下只支持2000左右的HTTP连接,而Linux下约是1000个,所以各位如果使用这种方法的时候,要注意是否会超出这些限制。
为什么要服务器hold住连接一段时间后释放呢?主要是因为长时间的静态连接容易出问题,另外移动端的网络复杂,所以才会有释放的必要。

 

 

上面所写的还要结合这篇文章一起看:

http://www.2cto.com/kf/201408/322289.html

 

public class CometServlet extends HttpServlet {

     

    @Override

    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {

         

        PrintWriter out = response.getWriter();

         

        while(true) {

            // 这里用Thread.sleep来模拟comet,相当于每隔5秒服务器向客户端推送一条消息

            try {

                Thread.sleep(5000);

            } catch (InterruptedException e) {

                e.printStackTrace();

            }

     

            out.println(helloworld

);

            out.flush(); // 这里一定要flush,否则数据不发送

        }

    }

 

}

 

 

 

 

 

 

@RequestMapping("/ajax")
public void ajax(long timed, HttpServletResponse response) throws Exception {
     PrintWriter writer = response.getWriter();


     Random rand = new Random();
     // 死循环 查询有无数据变化
     while (true) {
         Thread.sleep(300); // 休眠300毫秒,模拟处理业务等
         int i = rand.nextInt(100); // 产生一个0-100之间的随机数
         if (i > 20 && i < 56) { // 如果随机数在20-56之间就视为有效数据,模拟数据发生变化
             long responseTime = System.currentTimeMillis();
             // 返回数据信息,请求时间、返回数据时间、耗时
             writer.print("result: " + i + ", response time: " + responseTime + ", request time: " + timed + ", use time: " + (responseTime - timed));
             break; // 跳出循环,返回数据
         } else { // 模拟没有数据变化,将休眠 hold住连接
             Thread.sleep(1300);
         }
     }


}

 

 

===============================================================================================

2018年11月26日下午,时隔一年多,在公司闲来无事,重新思考了一下长轮询的后端优化,并写了一个后端演示demo的第二版,此版本的最主要改进为:减少了因线程被频繁唤醒导致的网络中传输的无用的数据量。

仍然存在的问题:无法唤醒指定的需要线程,最大程度利用CPU效率。

此次演示代码如下:

	/**
	 * 长轮询demo,具体代码请以实际业务为准
	 * 此处的currentHashMap仅仅作为演示使用,可以在实际应用中设置为一个公用的静态值,
	 * 并且尤其要注意对此静态的map进行修改的时候要加锁synchronized,防止多线程并发问题,
	 * 并且此值一般在其它写逻辑处唤醒,唤醒方法为:map.notifyAll()
	 * @author ylz
	 * @return message
	 */
	public String longPoll(){
		//key:id或qq号,,value:消息
		Map<String,Object> map = new ConcurrentHashMap<String, Object>();
		//如果有消息就返回
		if(map.containsKey("123")){
			return "value";
		}
		//没有消息就休眠120秒
		boolean first = true;
		long sleep = 120*1000;
		long t1 = System.currentTimeMillis();
		while(true){
			try {
				if(first){//首次进入循环则休眠120s
					first = false;
					map.wait(sleep);
				} else {//非首次则休眠120s-已经休眠的秒数
					long t2 = System.currentTimeMillis();
					if((t2-t1) > 0 
							&& (t2-t1) < 120*1000){
						map.wait(sleep-(t2-t1));
					} else {
						break;
					}
				}
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			//被唤醒之后查询是否有消息,没有则继续循环
			if(map.containsKey("123")){
				break;
			}
			//休眠时间到了并且没有数据则跳出循环返回无消息
			if((System.currentTimeMillis()-t1) > 120*1000){
				break;
			//	return "no message";
			}
		}
		
		if(map.containsKey("123")){
			return "value";
		} else {
			return "no message";
		}
	}

 

=============================================================================================

并与18年12月初完成了一个webim系统,此系统下载地址为:

https://pan.baidu.com/s/1glj5CPApzicI9ZibKtidlg#list/path=%2F

 

im系统详细说明待续ing……

 

 

参考文献:

https://blog.csdn.net/pengyufight/article/details/84978004

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值