基于Netty的面向移动终端的推送服务设计
2016-01-24代超邓中亮
代超++邓中亮
摘要:随着智能手机和平板电脑等移动多媒体终端的普及和4G的加速发展,移动互联网近年呈现了迅猛的发展态势。基于Android操作系统的各类APP应用如雨后春笋,影响着人们的生活习惯。面向移动端的推送服务通过分析用户喜好给用户推送其感兴趣的内容,能大大提升用户的活跃度和留存率,因此成为了APP应用不可或缺的重要组成部分。然而由于Android官方的消息推送机制C2DM(Cloud to Device Messaging)却有着覆盖率偏低的缺陷,APP开发者需要自己开发消息推送系统。本文通过研究开源消息推送和即时通信系统,分析比较常用的网络通信协议和网络10框架,最终采用Java NIO网络框架Netty和开源数据序列化工具Protocol Buffers实现了轻量级的面向移动端的推送服务系统。
关键词:计算机应用技术;Netty网络框架;推送;Protocol Buffers
中图分类号:TP311.1
文献标识码:A
DOI:10.3969/j.issn.1003-6970.2015.12.001
本文著录格式:代超,邓中亮.基于Netty的面向移动终端的推送服务设计[J].软件,2015,36(12):01-04
0 引言
互联网时代,推送技术在各行各业得到应用。随着移动瓦联网的发展,很多APP应用都集成了推送服务,如微信、网易新闻等。消息推送主要有两种实现方式,客户端定时“拉取”和服务器主动“推送”。“拉取”方式是客户端按照预设的触发条件和时间间隔,不停地向服务器查询更新,然后发出拉取请求以获取最新消息;而“推送”的方式则是在客户端和服务器之间保持一条连接通道,当服务器有新消息时丰动将消息直接发送给客户端,减少交瓦次数,提高了推送效率。以上两种方式各有利弊,但是为了实现移动终端的低功耗和低流量,通常采用服务器丰动“推送”技术。由于服务器丰动“推送”需要在客户端和服务器之间保持TCP长连接,当用户量庞大时,单台服务器可能要保持上卣万个TCP连接,这对于网络服务器的开发要求很高。传统的网络服务器使用BIO(阻塞10)开发,多采用一连接一线程(One thread per connection)的线程模型,即每接受一个连接请求则产牛一个子线程处理该请求,这种模型导致服务器无法承受大量客户端的并发连接,而且频繁的线程上下文切换导致CPU利用效率不高。Netty是一个基于NIO的客户端/服务器框架,NIO采用反应堆(Reactor)模型,其单线程模型如图l所示,其中一个Reactor线程聚合一个多路复用器Selector,可以同时注册、监听和轮询成千上万个客户端连接。Netty可以通过调整参数灵活配置成Reactor单线程、多线程和丰从多线程模型,用少量的线程即可以处理上万条TCP连接,同时Netty中集成了丰流的编解码框架和灵活的自定义编解码器实现,能轻松实现私有的协议栈,很适合开发基于TCP长连接的推送服务。
1 传输协议的制定和编解码实现
网络服务器主要任务是处理与客户端之间的数据交互,为了实现高效率可扩展的推送服务,数据传输协议的制定尤为重要。
1.1 数据传输协议的制定
当前能直接用于生产环境的协议主要有XMPP、MQTT及部分私有协议。XMPP是一种基于XML的实时通信协议,具有很强的扩展性,但是由于XML文本协议带来的数据冗余使其不太适合于移动端使用。MQTT是一种轻量级的、基于代理的“发布/订阅”模式的消息传输协议,专门为低带宽、不稳定网络所设计,协议小巧可扩展性强,比较适合作为移动端的消息协议,但是其不够成熟、实现复杂且没有成熟的Java开源实现。协议就是原数据和消息协议数据之间的一组关系映射,可看作是一种序列化机制,本文基于开源数据序列框架Protocol Buffers实现了一个可扩展的私有消息协议。Protocol Buffers是一个灵活、高效、结构化的数据序列化框架,支持跨语言使用,在拥有.proto文件和知悉POJO对象类型的情况下可以进行POJO对象的编解码。利用Protocol Buffers的特点设计的具体的数据传输协议如图2所示,帧末尾是将POJO对象序列化后的二进制数据,由于Protobuf对POJO解码一般来说需要知悉POJO对象的类型,所以在Protobuf Data之前采用两个字节来表示消息对象的类型。TCP是个“流协议”,TCP协议层不保证消息的完整性,TCP协议底层存在粘包和拆包的问题,所以需要应用层协议来保证消息边界的正确性,一般可以通过在消息头中添加表示消息总长度的字段FrameLen来解决。
1.2 协议编解码框架实现
由于TCP协议的粘包/拆包等特点,白行实现该协议并不简单,幸运的是Netty提供的众多编解码工具可以帮助我们轻松实现该数据传输协议的编解码。Netty提供了基于责任链模式的编解码及业务处理框架ChanneIPipeline,同时提供对主流编解码框架如Protocol Buffers、Marshalling的原生支持。Netty中提供的LengthFieldBasedFrameDecoder/Length-FieldPrepender编解码器能通过在消息头添加长度域解决TCP粘包/拆包问题。协议整体的逻辑框架图如图3所示,Netty为了尽可能的提升性能,采用了串行无锁化设计Cha nneIPipeline,在10线程内部各个Handler依次被调用,避免线程竞争导致的性能下降。在接收消息时LengthFieldBasedFrameDecoder读取消息长度域,根据长度读取对应长度的二进制数据并递交给NameFieldBasedProtobufDecoder进行处理。NameFieldBasedProtobufDecoder先读取两个字节的消息类型名(Message TypeName),通过服务端维护的消息类型名-POJO的Java类型的查找表得到具体的Java类型,通过Protobuf工具类将二进制数据转化成POJO对象传递给MessageHandler进行业务处理。在发送消息时,NameFieldPrepender根据发送的消息类型名将POJO对象转换成Protobuf二进制数据并在数据帧加上TypeName域后递交给LengthFieldPrepender, LengthFieldPrepender计算出长度在数据帧前加上后通过网络发送。整个推送服务的私有协议设计简单,增加消息类型时只需重编译.proto文件和在服务端添加对应的消息类型名-POJO的Java类型项即可,可扩展性强。
2 服务端设计与实现
推送系统的主要任务是将消息通过TCP长连接发送到客户端,根据业务需求的不同,会有多种不同的推送方式,例如广播消息(推送给全体用户),根据标签(Tag)推送等。同时面向移动端的推送服务还需要处理由于移动无线网络不稳定性带来的TCP断连,消息丢失等问题。下文阐述了使用Netty及数据库存储实现广播消息和基于标签推送,同时采用心跳机制、消息回执等方法来保证消息到达率和稳定性。
2.1 心跳机制的设计
移动终端连上移动网络获得的IP实际上是运营商内网IP,移动终端连接Internet需要通过运营商的GGSN(GateWay GPRS Support Note)模块进行网络地址转化。运营商为了减少网络NAT映射表的负荷,会清除一段时间内没有通信的链路对应的NAT映射表,造成链路中断。解决该问题的常用方法就是心跳机制,即客户端或服务端每隔一段时间发送心跳包激活链路。心跳机制同时也能检测链路可用性,帮助客户端或服务端即时断开失效链路,释放宝贵的10资源。心跳机制原理主要如图4所描述,图中描述的是Ping-Pong型心跳,通常由通信一方定时发送Ping消息,对方收到Ping后,立即返回Pong消息。如果连续N次心跳检测都没有收到对方的Pong应答消息,则认为链路已经发生逻辑失效,可以关闭该链路以节约资源。Android端可以利用白带的AlarmManager机制定时发送Ping消息,客户端检测到断线时可以启动重连机制;服务端可以利用Netty的空闲检测机制来完成心跳检测。Netty中集成的空闲检测机制ReadTimeoutHandler在时间t内没有收到任何消息时发生超时异常,可以利用其检测t时间内有无收到Ping消息,如果连续N次发生超时,则及时关闭链路避免产生过多的CLOSE WAIT状态占用服务器10资源。
2.2 离线消息推送与消息回执
心跳检测和断线重连的机制能够保证客户端与服务器之间有稳定的TCP长连接。Netty采用NIO实现,采用Channel表示一条连接。当客户端需要使用推送服务时,必须先连接、注册并登陆到服务器。Android客户端可以使用随机生成的用户名和密码进行注册,服务器验证合法后Android端将用户名和密码持久化存储以备以后登陆使用。Android客户端将注册成功的用户名和密码发送给服务端,服务端验证成功后将该通道Channel及用户信息封装成UserSession存入用户名Username-UserSession表中。推送功能一般都是以服务的形式提供给其他用户或者其他应用,采用Spring管理Netty可以很方便地对外提供RestFul接口的推送服务。采用Spring管理Netty并提供HTTP调用接口,整个推送服务的流程图如图5所示。最常用的推送服务类型为广播类型,即推送给当前在线或者离线的全体用户。为了使暂时离线用户在以后也能收到该条消息,必须采用数据库存储离线消息。在数据库中建立两张表,message(id.content,type...)、user_message(id.username.message_id,state...)。message表中type表示推送消息类型,如广播,tag等,user_message中message_id为message表的id,state表示消息状态,如未读、已读、过期等。当服务器通过HTTP收到服务使用者推送消息请求时,会先判断推送消息的类型并往message表中添加该条消息,然后根据消息类型查找数据库中相应的用户集合,例如广播消息类型会取出所有用户,基于标签推送会取出关注该标签的用户。遍历用户集合如用户在线则根据Username取出Usersession中的Channel利用其write()方法将消息推送给用户,如果不在线则往user_message表中添加一条消息记录。为了确保将离线消息推送给用户,用户每次上线后将取出user_message及message表中的未读消息,如果已经过期则修改state为过期,如果未过期则将消息推送给用户。由于网络的不确定性,服务端推送出去的消息可能不能到达用户端,为此还需要有回执确认的机制来保证达到率。即客户端收到推送消息后,将发送回执ACK消息给服务端,服务端根据ACK中的message_id信息改变user message表中对应消息的state为已读。有了消息回执机制,可以保证消息的到达率,大大提高用户体验和留存率。
3 系统测试
3.1 功能测试
系统测试分为功能测试和性能测试。功能性测试需要验证两个方面。首先客户端需要能注册登录到服务器并接收到服务器推送的消息,其次客户端在断线情况下需要能够发起重新连接。为了方便测试与观察,采用安卓模拟器连接到服务器。模拟器收到的推送结果如图6所示。为了测试客户端的断线重连机制,先断开服务器端的网络连接,通过LogCat可以看到客户端检测到了断线并发起了重连请求,当恢复服务器端网络连接后,客户端成功连接上服务器,能够接收到推送消息。
3.2 性能测试
性能测试用于测试服务端能够承载多少在线用户。由于无法使用Android平台模拟大量的TCP连接,这里在PC上实现了一个客户端程序来模拟大量的用户连接到服务器,服务器配置如表1所示,测试网络结构如图7所示。
采用三台PC机连接到服务器,每台PC最多发起15000个连接,在服务器接受所有连接以后,分别测试基于标签的推送(拥有某标签的用户占总用户的l0%)和广播消息。根据客户端的消息回执机制测试各种情况下发送到客户端所需的时间,推送成功率以及服务器负载情况,所得结果如表2所示。
从结果可以看到,在客户端与服务器在局域网环境下,推送成功率达到l00%,最大推送延时在12s内,单台服务器可以完成45k在线用户的推送服务。
4 结论
本文论述了基于Netty的面向移动端的推送服务设计与实现。目前已完成广播消息和基于标签的推送服务,单台服务器能支撑45k在线用户。下一步的工作将集中在增加富媒体消息的推送功能,进一步提高单台服务器的并发连接数和稳定性以及构建服务器集群以支撑百万级用户量。