罗嗦几句:

1.轮询(Pull)客户端定时的去询问服务器是否有新消息需要下发;确点很明显Android后台不停的访问网络费电还浪费流量。2.推送(Push)服务端有新消息立即发送给客户端,这就没有时间的延迟,消息及时到达。当时需求过来之后就首先考虑的这两个,开发的角度Pull实现起来简单省事,但从用户来说省电和省流量才是主要的,所以最后选用Push。客户端与服务端使用长连接,客户端定时向服务端发送心跳包维持长连接,当时自己使用socket来写的,最后发现客户端数量大了之后不太好维护而且会莫名的掉线,最后无奈用了别人集成好的[Netty](http://netty.io/),用了之后才发现对并发处理和心跳的维护都处理的很好。

正文开始

先看最终的效果
编辑好发送的内容,点击发送立即群发消息!


消息发送成功成功后会有消息提示,因为我只有一个手机连接,所以显示发送成功数1个,耗费的时间是2毫秒!

最后看手机的推送消息,看到效果还是挺不错的!

服务端设计:

  1. 数据库设计
    一.用户表,用户表的作用是在实际的项目运营中知道推送给指定的用户。主要有5个字段,第一个ID是主键,第二个use_id对应的是自己数据库中用户的编号,第三个ios_device_token方便给苹果用户推送,第四个mobile可以推送短信消息,第五个openid是用户授权登录系统后获取的用户标识,还有5个字段是保留字段方便扩展。

    CREATE TABLE `user` (`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',`user_id` bigint(20) NOT NULL COMMENT '用户的ID',`ios_device_token` varchar(128) DEFAULT NULL COMMENT 'IOS的device_token令牌',`mobile` varchar(20) DEFAULT NULL COMMENT '用户手机号',`openid` varchar(200) DEFAULT NULL COMMENT '微信的OPENID',`obligate_1` varchar(100) DEFAULT NULL,`obligate_2` varchar(100) DEFAULT NULL,`obligate_3` varchar(100) DEFAULT NULL,`obligate_4` varchar(100) DEFAULT NULL,`obligate_5` varchar(100) DEFAULT NULL,PRIMARY KEY (`id`),KEY `user_id` (`user_id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=1028 DEFAULT CHARSET=utf8;

    二.消息表,系统在运营中会产生消息记录,消息记录的记载,记载之后可以保存用户离线消息,等待上线后Push给对应的用户,并且有消息有效期,超过有消息自动作废。同样的有4个保留字段方便扩展使用。

    CREATE TABLE `msg_que` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`user_id` bigint(20) NOT NULL COMMENT '用户ID',`msg_title` varchar(20) DEFAULT NULL COMMENT '消息头',`msg_type` smallint(1) DEFAULT '0' COMMENT '消息类型(0:普通消息;1:透传消息)',`msg_content` text COMMENT '消息内容',`create_time` timestamp NULL DEFAULT '0000-00-00 00:00:00' ON UPDATE CURRENT_TIMESTAMP COMMENT '消息创建时间',`valid_time` datetime DEFAULT '0000-00-00 00:00:00' COMMENT '消息有效时间(小时)',`state` smallint(1) DEFAULT '0' COMMENT '消息状态(0:未送达;1:已送达;2:失效)',`obligate_1` varchar(100) DEFAULT NULL,`obligate_2` varchar(100) DEFAULT NULL,`obligate_3` varchar(100) DEFAULT NULL,`obligate_4` varchar(100) DEFAULT NULL,PRIMARY KEY (`id`),KEY `user_id_refrence` (`user_id`),CONSTRAINT `user_id_refrence` FOREIGN KEY (`user_id`) REFERENCES `user` (`user_id`) ON DELETE CASCADE ON UPDATE CASCADE
    ) ENGINE=InnoDB AUTO_INCREMENT=4446 DEFAULT CHARSET=utf8;
    
  2. 代码设计
    推送服务端使用的是Spring+SpringMVC+MyBatis,下面两个类就是服务端的启动代码与消息处理代码,一张图解释设计流程(凑合看吧):

    /***服务端启动Netty*/@Controllerpublic class NettyInitial implements InitializingBean, ServletContextAware {static final boolean SSL = System.getProperty("ssl") != null;//服务端netty的端口static final int NETTYPORT = Integer.parseInt(System.getProperty("port", "9800"));@Overridepublic void setServletContext(ServletContext arg0) {//使用线程启动Netty服务端,否则会阻塞线程new Thread(new Runnable() {@Overridepublic void run() {try{startNettyConnect();}catch (Exception e) {}}}).start();}@Overridepublic void afterPropertiesSet() throws Exception {}public void startNettyConnect() throws CertificateException, SSLException{final SslContext sslCtx;if (SSL) {SelfSignedCertificate ssc = new SelfSignedCertificate();sslCtx = SslContext.newServerContext(ssc.certificate(),ssc.privateKey());} else {sslCtx = null;}// Configure the server.EventLoopGroup bossGroup = new NioEventLoopGroup(10);EventLoopGroup workerGroup = new NioEventLoopGroup(10);try {ServerBootstrap b = new ServerBootstrap();b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).option(ChannelOption.SO_BACKLOG, 2048).childHandler(new ChannelInitializer<SocketChannel>() {@Overridepublic void initChannel(SocketChannel ch)throws Exception {ChannelPipeline p = ch.pipeline();if (sslCtx != null) {p.addLast(sslCtx.newHandler(ch.alloc()));}p.addLast("idle", new IdleStateHandler(300, 300, 300));p.addLast(new ObjectEncoder(), new ObjectDecoder(ClassResolvers.cacheDisabled(null)),new NettyServerHandler());}});b.option(ChannelOption.TCP_NODELAY, true);//保持长连接状态b.childOption(ChannelOption.SO_KEEPALIVE, true);// Start the server.ChannelFuture f = b.bind(NETTYPORT).sync();// Wait until the server socket is closed.f.channel().closeFuture().sync();}catch (Exception e) {// TODO: handle exception} finally {// Shut down all event loops to terminate all threads.bossGroup.shutdownGracefully();workerGroup.shutdownGracefully();}}}/*** Netty模块消息处理* @author Administrator**/public class NettyServerHandler extends ChannelInboundHandlerAdapter{private MainHashMapObserver mainHashMapObserver = MainHashMapObserver.getInstace();@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) {mainHashMapObserver.addObserver(msg.toString(), (SocketChannel) ctx.channel());ctx.writeAndFlush("连接成功="+msg);}@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {super.channelActive(ctx);}@Overridepublic void channelReadComplete(ChannelHandlerContext ctx) {ctx.flush();}@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {mainHashMapObserver.deleteObserver((SocketChannel) ctx.channel());ctx.close();}@Overridepublic void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {super.userEventTriggered(ctx, evt);if (evt instanceof IdleStateEvent) {IdleStateEvent event = (IdleStateEvent) evt;if (event.state().equals(IdleState.READER_IDLE)) {// 读超时}if(event.state().equals(IdleState.WRITER_IDLE)){//写超时ctx.writeAndFlush("y");}}}}

客户端设计

  1. service代码

    将Android服务定义成远程服务,可以稍微避免一下下被系统杀死;在接收到服务器推送的消息后判断消息类型,如果是心跳包就不必理会,如果是有用的消息就选择notification或者后台处理

    public class NettyRemoteService extends Service {/*** 单一线程池*/private ExecutorService executorService= Executors.newSingleThreadExecutor();/*** 是否需要继续运行*/private boolean running = true;@Overridepublic void onCreate() {super.onCreate();}@Overridepublic IBinder onBind(Intent intent) {return mBinder;}@TargetApi(Build.VERSION_CODES.JELLY_BEAN)@Overridepublic int onStartCommand(Intent intent, int flags, int startId) {try {create();} catch (SSLException e) {}executorService.execute(new Mythread());return START_STICKY;}@Overridepublic void onDestroy() {running = false;executorService.shutdown();if(f != null){f.channel().close();}stopSelf();super.onDestroy();}/************************************************************************************ start Netty长连接服务器保持数据通讯***********************************************************************************/private static final boolean SSL = System.getProperty("ssl") != null;SslContext sslCtx;private void create() throws SSLException {if (SSL) {sslCtx = SslContext.newClientContext(InsecureTrustManagerFactory.INSTANCE);} else {sslCtx = null;}}ChannelFuture f;public void createBootStrap() {L.v("重连......");EventLoopGroup group = new NioEventLoopGroup();try {Bootstrap bootstrap = new Bootstrap();bootstrap.group(group).channel(NioSocketChannel.class).option(ChannelOption.TCP_NODELAY, true).handler(new ChannelInitializer<SocketChannel>() {@Overridepublic void initChannel(SocketChannel ch) throws Exception {ChannelPipeline p = ch.pipeline();if (sslCtx != null) {p.addLast(sslCtx.newHandler(ch.alloc(), "192.168.0.103", 9800));}p.addLast("idle", new IdleStateHandler(300, 300, 300));p.addLast(new ObjectEncoder(), new ObjectDecoder(ClassResolvers.cacheDisabled(null)),new CarmgrClientHandler());}});//设置TCP协议的属性bootstrap.option(ChannelOption.SO_KEEPALIVE, true);bootstrap.option(ChannelOption.TCP_NODELAY, true);bootstrap.option(ChannelOption.SO_TIMEOUT, 5000);// Start the client. IP和Port是服务器运行的IP和自己设置的端口f = bootstrap.connect("192.168.0.103", 9800).sync();f.addListener(new ChannelFutureListener() {@Overridepublic void operationComplete(ChannelFuture channelFuture) throws Exception {if (channelFuture.isSuccess()) {} else {}}});f.channel().closeFuture().sync();} catch (Exception e) {} finally {// Shut down the event loop to terminate all threads.group.shutdownGracefully();}}private class Mythread extends Thread{@Overridepublic void run() {while(running) {createBootStrap();try {//休息30s重连Thread.sleep(30 * 1000);} catch (InterruptedException e) {}}}}/*** 开启推送消息展示*/private int notificaionId = 100;public void startNotification(String title, String context) {NotificationCompat.Builder builder = new NotificationCompat.Builder(this);// 设置通知的基本信息:icon、标题、内容builder.setSmallIcon(R.mipmap.launcher_icon);builder.setContentTitle(title);builder.setContentText(context);builder.setAutoCancel(true);// 设置通知的优先级builder.setPriority(NotificationCompat.PRIORITY_MAX);Uri alarmSound = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);// 设置通知的提示音builder.setSound(alarmSound);builder.setDefaults(Notification.DEFAULT_ALL);// 设置通知的点击行为:这里启动一个 ActivityIntent intent = new Intent(this, MainActivity.class);PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);builder.setContentIntent(pendingIntent);if (notificaionId < 1000) {notificaionId++;} else {notificaionId = 100;}// 发送通知 id 需要在应用内唯一NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);notificationManager.notify(notificaionId, builder.build());}/*** Netty消息接收内部类*/private class CarmgrClientHandler extends ChannelInboundHandlerAdapter {/*** Creates a client-side handler.*/public CarmgrClientHandler() {//TODO}@Overridepublic void channelActive(ChannelHandlerContext ctx) {ctx.writeAndFlush("1");}@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) {if (msg.toString().equals("y")) {//心跳不用理会} else {//处理服务端推送的消息}}@Overridepublic void channelReadComplete(ChannelHandlerContext ctx) {ctx.flush();}@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {// Close the connection when an exception is raised.ctx.close();}@Overridepublic void channelInactive(ChannelHandlerContext ctx) throws Exception {super.channelInactive(ctx);}@Overridepublic void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {super.userEventTriggered(ctx, evt);if (evt instanceof IdleStateEvent) {IdleStateEvent e = (IdleStateEvent) evt;switch (e.state()) {case WRITER_IDLE:ctx.writeAndFlush("1");break;default:break;}}}}/************************************************************************************ end Netty长连接服务器保持数据通讯***********************************************************************************/
}

在完成这个推送的过程中也遇到过很多的问题,比如说Android service在后台运行一段时间就会挂掉然后掉线,这个问题的解决主要是提高service的存活率来着手,比如说设置成remote service、监听一些广播事件,两个服务相互监听互相吊起等,但是注意最后别搞成关不了的流氓软件了。

还有就是连接的用户数过多的时候群发消息问题,我自己的机器模拟启动1000个线程去连接服务端,写一个接口去获取当前的连接数:

运行2个小时后再次查看接口,发现还是833个,也就说一个都没有掉线(电脑端的网络比较稳定,手机端很复杂,所以手机端会出现掉线然后迅速重连的现象!);这时候再次测试群发数据查看到达率和耗时




消息推送所耗费的时间跟所发送数据大小(下面是发送少量数据所耗时)、心跳的时间间隔、电脑的带宽、电脑的性能都有影响

发送的消息我封装成JOSN字符串,所以实际的发送数据量比看见的还会稍微大点,全部送达而且耗费的时间还可以接受。正常情况下以800个120毫秒计算,每个0.15毫秒算在用户量不太大的情况下还可以接受。

数据量极少的情况下出现了耗时6ms的情况,也就是1s不到可以推送10万,当然这是理想的情况,服务端连接数达到10万还不知道会发生什么情况;单机测试就是这么的有局限性,如果多配几个电脑就能测试的更加准确。

至此这个推送就完成了,有很多代码或者说细节的问题没有在这里展示,因为是公司的项目所以选择性的粘贴了部分主流代码和思路,按照思路来的话是可以实现的,如果有不懂的欢迎留言咨询,互相进步!

Java Netty长连接实现Android推送相关推荐

  1. 如何实现android和服务器长连接呢?推送消息的原理

    前言:现在的大多数移动端应用都有实时得到消息的能力,简单来说,有发送消息的主动权和接受消息的被动权.例如:微信,QQ,天气预报等等,相信好处和用户体验相信大家都知道吧. 提出问题:这种功能必须涉及cl ...

  2. Android推送实现原理

    心跳机制是定时发送一个自定义的结构体(心跳包),让对方知道自己还活着,以确保连接的有效性的机制. android系统的推送和iOS的推送有什么区别: 首先我们必须知道,所有的推送功能必须有一个客户端和 ...

  3. java nio长连接实现_kio: kio是基于jdk 1.6 nio实现的TCP长连接即时通讯框架。

    #kio: TCP长连接框架 适用于Android前端 #为何要开发kio# 由于之前开发的Android推送系统,使用HTTP实现的,随着业务的扩展,服务器的压力随之增大,开始考虑使用TCP长连接来 ...

  4. Android推送分析

     cpu多核利用能够实现Android推送的吞吐量. 讲明白这点,我们需要了解Android推送的基本原理了.如果实现C(客户端)与server(客户端)实时通讯了.这里有两种思路了: 1.一种是定时 ...

  5. Android推送机制的实现原理

    Android 推送机制实现原理 APP传统获取服务端信息途径:Pull模式–这种模式客户端和服务器端维持的是短连接.当然也存在由服务器端主动向客户端主动发送消息的通信模式,称为Push模式,即为推送 ...

  6. Android推送通知指南(转)

    在开发Android和iPhone应用程序时,我们往往需要从服务器不定的向手机客户端即时推送各种通知消息,iPhone上已经有了比较简单的和完美的推送通知解决方案,可是Android平台上实现起来却相 ...

  7. Android 推送通知指南(转载)

    在开发Android和iPhone应用程序时,我们往往需要从服务器不定的向手机客户端即时推送各种通知消息,iPhone上已经有了比较简单的和完美的推送通知解决方案,可是Android平台上实现起来却相 ...

  8. Android推送技术总结

    Android推送技术总结 Android平台接入推送机制一般有两种,一种是Google官方提供的C2DM云端推送方案:另外一种是第三方,如极光.个推和友盟等推送平台. C2DM云端推送方案 1)简单 ...

  9. Android推送通知指南

    在开发Android和iPhone应用程序时,我们往往需要从服务器不定的向手机客户端即时推送各种通知消息,iPhone上已经有了比较简单的和完美的推送通知解决方案,可是Android平台上实现起来却相 ...

最新文章

  1. 内部排序——直接插入排序
  2. python深度爬虫_总结:常用的 Python 爬虫技巧
  3. linux 以某个用户执行,Linux root用户肿么以制定的用户去执行某个程序。
  4. 【 CodeForces - 864B】Polycarp and Letters(水题,字符串,有坑)
  5. 面试官:使用SpringBoot如何开发邮件发送系统?
  6. 【APIO2010】巡逻
  7. linux认证哪家好,linux认证 考哪种好?
  8. 前端学习-jQuery源码学习
  9. openssl加解密原理
  10. win7开机有画面进系统黑屏怎么办
  11. 软件测试面试题及答案,2022最强版
  12. 项目管理(软件开发)之甘特图使用
  13. linux samba文件共享,网络凭据总是出错解决方法
  14. stm32f103 延时20ns,在STM32上生成C中的纳秒延迟
  15. 读书笔记 | 4.1 信用报告及其国际比较
  16. 读博期间应该明确的50件事!
  17. 从源数据库抽取数据到中间库
  18. python 过滤掉字符串中的回车符与换行符(\t\n)
  19. 商络转债上市价格预测
  20. Linux管道通信【操作系统】利用pipe

热门文章

  1. Ubuntu磁盘扩充
  2. C++计算绝对值的函数
  3. SQL根据身份证,统计用户的省份
  4. PPP协议与WAN技术
  5. 《实战Java高并发程序设计》.pdf
  6. 抖音企业号抖音智能营销系统源码待开发技术。。。。。
  7. Python爬虫获取代理ip及端口
  8. 自动语音播报WinCC报警消息
  9. centos7解决hadoop2.6.4多次格式化导致的slave节点datanode无法启动的问题
  10. 【测试】用Chrome在电脑上模拟微信浏览器