构建消息推送系统之HTTP长连接实践

#前言
从Servlet3规范出来以后,利用Servlet3支持的异步特性,我们创建异步上下文asyncContext之后将它保存下来,同时不释放,那么这样就达到了长连接的目的。同时在配合tomcat nio的使用,利用Servlet3构建一个http长连接推送系统就有了支持基础,本篇文章将重点介绍基于Servlet3构建http长连接推送系统的实践。有关Servlet3异步的详细介绍可以参看《servlet3异步原理与实践》
#一、WEB网络结构及配置

##1.1、网络结构
WEB网络结构.png

用户访问vip–>vip发布在lvs上–>lvs将请求转发给后端的haproxy–>haproxy再把请求代理转发给后端的nginx。vip实际路由发布在lvs上,但是vip配置属性在haproxy上(比如ACL, 域名,规则之类)
这里lvs转发给后端的haproxy,用户请求经过lvs,但是响应是haproxy直接反馈给客户端的,这也就是lvs的dr模式。

##1.2、基本配置
我们知道http连接的特点就是一个request,一个response,然后关闭连接。这个过程包括建立连接和关闭连接。再往深处说就是调用了TCP/IP协议的三次握手,TCP协议多次传输,以及关闭连接的时候四次握手。频繁的做这些操作肯定很耗费系统的资源。从HTTP1.1以后,开始支持keepalive ,比如浏览器一旦与服务器建立连接后,会保持住一段时间,也就是减少了上面的握手和传输的次数,在这个时间段内传输数据都是复用同一个连接。当客户端主动告知关闭,或者达到了TCP关闭的条件,TCP/IP再关闭。那么通过HTTP keepalive 机制就可以让TCP连接保持住,具体保持多长时间可以通过参数来设置,下文会有介绍。
如果要保持长连接,那么根据上图的结构,浏览器与haproxy之间保持长连接(timeout http-keep-alive),haproxy与nginx之间保持长连接,nginx与tomcat之间保持长连接。我们的web应用架构一般都是如上图所示,会包含LVS、转发、反向代理。但简单起来说就是nginx+tomcat,也就是虚线框内标识的,其实我们研发人员能接触到的也是这两层,其余由运维和网络组的同学来维护。那么我重点介绍一下nginx层的配置参数。

http {
	//...
	keepalive_timeout     	3600s; //Nginx 默认是支持 keepalive的,是通过 keepalive_timeout 设置的,默认值是75s。它表示在长连接开启的情况下,在75s内如果没有 http 请求,则关闭长连接(其实就是关闭 tcp)
	keepalive_requests 		800; //此值容易被忽略,它是值在 keepalive_timeout 的时间范围内,一个长连接最大允许的请求次数,如果超过此值,也会关闭此长连接。默认值为100。
	gzip   					off; //这个在1.3中叙述
	//...
	upstream  TEST_BACKEND {
	    server   192.168.1.1:8080  weight=1 max_fails=2 fail_timeout=30s;
	    server   192.168.1.2:8080  weight=1 max_fails=2 fail_timeout=30s;
	
	    keepalive 1000;        //此处keepalive的含义不是开启、关闭长连接的开关;也不是用来设置超时的timeout;更不是设置长连接池最大连接数;而是连接程池中最大空闲连接的数量
	}
	
	server {
	    listen 8080 default_server;
	    server_name "";
	
	    location /  {
	        proxy_pass http://TEST_BACKEND;
	        
			//...
			
	        proxy_http_version 1.1;         //指定 HTTP 版本,防止 1.0 版本导致 keepalive 无效。
	        proxy_set_header Connection "";	//清空将客户端的一些设置,防止导致 keepalive 无效
	
	        //...
	    }
	}
}

##1.3、Transfer-Encoding: chunked
普通短连接的时候浏览器根据连接关闭的状态来写response的内容。在长连接下,一段时间内传输的内容,连接都是不关闭的。因此如果没有一种机制来告知什么节点吐出内容,浏览器就只能一直等待后面是否还有数据,则迟迟不会写response的内容。那么我们可以想到利用Content-Length在传输之前标识一个包的大小,但是对于动态输出的内容,传输之前就不太好判断Content-Length的长度。在HTTP1.1最新的规范中定义了一种传输方式,就是chunked,分块编码。请求头部加入 Transfer-Encoding: chunked 之后,就代表这个报文采用了分块编码。报文中的实体需要改为用一系列分块来传输。每个分块包含十六进制的长度值和数据,长度值独占一行,长度不包括它结尾的 CRLF(\r\n),也不包括分块数据结尾的 CRLF。最后一个分块长度值必须为 0,对应的分块数据没有内容,表示实体结束。这样在长连接下动态输出内容的时候浏览器就能够判断当前这次报文结束的位置了。
在1.2中我们留了一个gzip没有介绍,我们知道开启gzip,在文本传输的情况下,所需流量大约会降至1/4-1/3。在gzip关闭的情况下,以前长连接没有任何问题,但是如果gzip打开,长连接则会失效。这是因为整个压缩过程在内存中完成,是流式的。也就是说,Nginx 不会等文件 gzip 完成再返回响应,而是边压缩边响应,这样可以显著提高 TTFB(Time To First Byte,首字节时间,WEB 性能优化重要指标)。这样唯一的问题是,Nginx 开始返回响应时,它无法知道将要传输的文件最终有多大,
也就是无法给出 Content-Length 这个响应头部。因此根据chunked传输方式原理,解决了既可压缩传输也能支持长连接方式传输了。

#二、HTTP长连接系统组成结构

系统组成.png

##2.1、SESSION管理
SESSION是客户端到服务端的一次会话或者说是连接会话,会话信息中保存了用户PIN、连接创建时间、这次request产生的AsyncContext上下文信息。我们会将会话信息保存到内存一份,
private Map<String, Session> sessions = new ConcurrentHashMap<String, Session>(); MAP的key为用户PIN。同时把这份HASH数据也保存到redis一份,并设置好过期时间,具体设置多久没有固定的标准,我们设置是8小时。这个在心跳逻辑中,如果没有心跳会将SESSION信息删除。
##2.2、心跳
心跳的目的是判断连接客户端是否还活着,隔一段时间比如5s发一次心跳包,一般是从客户端往服务端发送心跳包,我们现在HTTP长连接是从服务端往客户端发送,当初的想法是节省客户端资源。心跳的逻辑是从当前服务器内存中轮询出所有的会话信息,在发送心跳包后如果收到错误信息则标记会失败,关闭上下文asyncContext.complete();this.asyncContext = null;同时从会话列表中删除,内存和redis中都要删除。
##2.3、消息接收
消息推送系统负责消息会话的创建、保持、心跳、通知推送。另外一部分就是通过MQ接收业务变更信息,通过MQ的广播机制保证每台推送系统服务器都能够收到业务变更信息。
##2.4、消息推送
利用了MQ的广播所有的服务器都会收到消息,那么推送的时候是如何找到需要哪一台服务器来负责推送任务呢,在创建会话的时候我们将用户会话信息保存到了本台服务器的内存中,那么只需要判断消息中的USERPIN是否在本机内存中即可。如果不在本机内存直接丢弃该条消息。通过MQ接收到业务信息,解析出USERPIN,再根据USERPIN找到会话,拿到asyncContext,然后将通知包发送给客户端。
##2.5、消息追踪
整个消息推送链相对比较长,需要做到对每个环节的埋点和跟踪,便与后续问题的跟踪处理。在业务中是通过kafka+hbase的方式,系统中把埋点数据写到本地,由采集器将数据发送到kafka,进而消费kafka插入到hbase集群。

#三、HTTP长连接系统时序调用
时序图.png

结合第二节和本节的时序图我们清楚的知道实现一个推送系统主要包含会话维护、心跳、消息接收、消息推送,这其中共涉及以下三个数据包

创建会话连接包:{"protocol":1,"time":1510210650650,"state":"registered"}
心跳包:{"protocol":0,"time":1510211080780}
发送通知包:{"protocol":2,"time":1448610190241,"cmd":110001}

接下来看下重要环节的代码实现:
##3.1、创建会话(连接)

public  Session createSession(String sessionId, HttpServletRequest request, HttpServletResponse response) {
   
		//省略代码...

        try {
   
            //省略代码...
            session = new HttpStreamingSession();
            session.setSessionId(sessionId);
            session.setValid(true);
            session.setMaxInactiveInterval(this.getMaxInactiveInterval());
            session.setCreationTime(System.currentTimeMillis());
            session.setLastAccessedTime(System.currentTimeMillis());
            session.setSessionManager(this);
    
            session.setConnection(createHttpConnection(session, request, response));
    
            //省略代码...
    
            return
  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值