精华内容
下载资源
问答
  • 如何进行app消息推送(push)?
    千次阅读
    2021-06-07 10:59:12

    1 消息推送

    消息推送(push),是指运营人员通过自己产品后台或第三方工具对用户移动设备进行的主动消息推送,是厂商主动触达用户的通道。通过消息推送,目标用户可以在移动设备通知和状态栏看到消息通知,唤起用户点击消息去往app页面。平时手机弹出的微信、全球消息等都属于app消息推送。

    消息推送具有投放精准、成本低廉的优点,能起到提醒沉默用户、提高用户活跃度、增强用户黏性的作用。一般来说,如果当日有推送的话,当日的DAU会有一定程度上涨。另外,注意使用push不要太频繁,因为推送太多消息会引起用户反感,导致用户关闭推送通知的,甚至卸载APP(信鸽和友盟具有卸载统计功能)。现在,push已经成为每一款移动端APP必备的一个功能和最重要的运营手段之一。

    2 消息推送的方式

    消息推送具有两种主要方式。第一种方式是自己研发,但由于研发成本较高,大多数app都会选择使第二种方式,即使用第三方工具进行推送。目前,国内较为常用的第三方推送服务工具有:极光推送、个推、腾讯信鸽、百度云推送、华为推送、小米推送。

    3 消息推送的特点

    量大面广。在app获得所有用户的消息推送授权情况下,app的用户数量=消息推送覆盖的数量。

    目标精准。同其他媒介渠道相比,消息推送的用户定位精准,消息推送的目标用户=下载安装使用app的用户。

    免费。厂商进行消息推送是免费的,而用户获取推送内容也是免费的。在信息过载的情况下,用户对第三方筛选内容的需求越来越大,而消息推送便是帮助用户进行筛选的一个过程,厂商借此将优质的内容直接push到用户的客户端上。

    但这种免费也导致了推送的滥用,可能带来的结果是用户关闭推送授权,甚至直接卸载app。因此,如何掌握好消息推送的度也是很重要的。

    4 如何进行消息推送

    4.1 产品特点决定推送内容

    明确产品的定位,预测用户使用场景。

    如新闻类app,则要保证内容是最新发生的事情的报道,对于旧内容,用户则不会过多关心,但新的事物无时无刻不在发生,却不是所有新近发生的事物都能成为新闻,对于运营人员来说,大多数人会关心的内容才是值得推送的内容。

    以天气类app为例,用户一般关心的不外乎具体的天气情况,当天的空气指数,还有穿衣指数等。而对于电商导购类app来说,新品的上市,商品的折扣情况和促销活动的宣传则是主要的内容。

    4.2 推送内容决定推送时间

    在确定产品定位之后,我们将会明确用户的使用场景,但具体的推送时间又该如何确定呢?

    从用户的使用场景出发,思考一下在什么情况下我们会用到某一类型的app。

    不同的产品决定用户的使用场景,不同的使用场景决定了推送的时间。

    但一般来说,消息推送时间应当是在人们高频率使用手机的时候,对于上班族来说,通勤路上的时间会是他们打开手机频率较高的时候,还有休息的时间也是人们浏览手机的高频率时间。因此早上中午(12-14点)、下午(18-19点)还有晚上临睡前(21-22点)这几个时间段都是推送消息比较好的时候。

    但具体的推送时间要视用户使用场景决定。对于电商导购类app来说,节假日还有商品的折扣日是需要人们提前获取的信息,需要提前做好预热预告,不可能同新闻类信息一样进行“突击”推送,使用户防不胜防,可能会导致损失大量的消费者。

    5 确认消息推送成功率

    为了确保消息够实时推送到客户端,一般第三方推送服务大多采用建立长连接的方式,在云端与客户端之间建立长连接。

    长连接,指在一个连接上可以连续发送多个数据包,在连接保持期间,如果没有数据包发送,需要双方发链路检测包。

    但这可能导致的问题有:权限不高,送达率也低,且耗电量高。

    就消息推送本身来说,运营人员使用push的目的就是我为了达到唤醒用户,提高app活跃度的作用,如果送达率不高这一效果将会大打折扣。

    为了理解送达率,首先要清楚一些相关概念:

    iOS 目标数 匹配推送条件的 iOS 用户的数量。

    iOS 成功数

    通知 :推送到 APNS 并被 APNS 成功接收的数量。如果 device token 变更,过期或者与推送环境不匹配则不会成功。

    自定义消息:用户通过应用内 JPush 通道收到的消息数,如果有效期内用户没有打开过应用,那么应用内通道未建立过则不会收到自定义消息。

    iOS 点击数 本次APNS推送,用户通过通知栏点击的次数。对于 iOS 自定义消息没有点击的概念。

    iOS 通知送达数 送达到设备并展示出来的通知数量。与 “iOS 成功数” 区别在于是否真实送达到了设备上。

    Android 目标数 匹配推送的条件的Android 用户数(1个月内与服务器有过连接的用户。如果超过1个月都没有与 JPush server 产生任何连接,那么将不向此用户推送)。

    Android 在线数 消息推送时,目标用户在线,通过在线下发的消息数。Android 用户长连接在线会通过在线下发,其余用户通恢复网络后触发离线消息。

    Android 送达数 消息送达到客户端,并且服务端确认收到了客户端的应答的数量。

    Android 点击数 本次推送被Android 用户点击的次数。

    消息推送之后都会得到一个消息送达率。

    在消息推送时,目标用户分为在线和离线两类。

    系统会根据用户状态再对用户进行推送,在线的用户会立即收到系统的推送消息,而离线的用户系统会将推送消息保存为离线消息,在消息有效期之内用户恢复在线后,再将消息推送到用户客户端。

    由于用户的在线、离线不同情况,其消息推送的送达率也有所不同:

    在线送达率 = 在线用户中成功接收的数量/在线用户数

    离线送达率 = 离线消息送达数/离线消息下下发数

    以上的送达率才是消息推送成功的,另外的一些用户虽然是目标用户,但是用户一直处于离线状态或者已经卸载了应用,那么用户是接收不到系统的消息推送的。

    6 确保用户不会收到过期消息推送

    运营人员可以后台设置消息有效期,以确保用户不会收到过期的信息。在对推送消息掌握合适时间的同时,也必须保证用户收到消息的及时性。

    以极光推送为例,极光推送的默认保留天数为1天,可设置的消息有效期为0-10天。对于新闻资讯类内容来说,1天的消息保留时间是比较合理的。

    更多相关内容
  • Django1.9.2 websocket 实时消息推送 服务端主动推送 调用 send(username, title, data, url) username:用户名 title:消息标题 data:消息内容,ulr:消息内容 ulr
  • (2)SockJS、Stomp、RabbitMQ... (4)可靠消息推送(Stomp持久化队列、客户端ACK确认机制); (5)Java原生、Stomp客户端实现(非浏览器客户端); (6)Websocket拦截器结合 Spring security、jwt token认证授权。
  • 个推消息推送demo

    2016-05-18 09:48:41
    1、个推推送透传消息的实现 2、消息在通知栏中显示 3、点击通知栏进入相应页面 4、工程目录结构介绍及效果展示
  • java二次开发接微信公众号接口,实现根据用户授权,获取用户code,再获取openid,然后向其推送模版消息
  • 小米推送之消息推送官方Demo,有Eclipse和android studio两种版本
  • WebSocket服务端消息推送

    千次阅读 2021-09-17 23:50:47
    一、Web端实现即时消息推送五种方式 股票曲线实时变化,在线IM聊天等等,Web系统里总是能见到消息推送的应用。消息推送用好了能增强用户体验,实现消息推送有N种解决方案。 1.1、什么是消息推送 消息推送(Push...

    前言:移动互联网蓬勃发展的今天,大部分手机 APP和网站都提供了消息推送功能,如新闻客户端的热点新闻推荐,IM 工具的聊天消息提醒,电商产品促销信息,企业应用的通知和审批流程等等。推送对于提高产品活跃度、提高功能模块使用率、提升用户粘性、提升用户留存率起到了重要作用,作为 APP 和网站运营中一个关键的渠道,对消息推送的合理运用能有效促进目标的实现。

    一、浅析web端的消息推送原理

    股票曲线实时变化,在线IM聊天等等,Web系统里总是能见到消息推送的应用。消息推送用好了能增强用户体验,实现消息推送有N种解决方案。

    1.1、什么是消息推送

    消息推送(Push)指运营人员通过自己的产品或第三方工具对用户当前网页或移动设备进行的主动消息推送。用户可以在网页上或移动设备锁定屏幕和通知栏看到push消息通知。以此来实现用户的多层次需求,使得用户能够自己设定所需要的信息频道,得到即时消息,简单说就是一种定制信息的实现方式。我们平时浏览邮箱时突然弹出消息提示收到新邮件就属于web端消息推送,在手机锁屏上看到的微信消息等等都属于APP消息推送。 

    Web网站推送:

    当我们在浏览网站观望犹豫时,突然看到了系统发来一条消息,一位神秘的神豪老板竟然爆出了麻痹戒指!!!我的天,于是我果断开始了游戏!这消息很及时!

    APP移动推送:

      

    上述两种经典场景,是生活中比较常见的场景,也引出了两大推送种类,Web端消息推送和移动端消息推送。本篇博客主要介绍Web推送,顺便提一句移动端App常见第三方推送SDK有极光推送、小米推送等等。

    1.2、Web端实现消息推送的四种方式

    主要介绍web端其中的四种实现方式:短轮询、Comet长轮询、Server-sent、WebSocket。

    (1)短轮询

    指在特定的的时间间隔(如每10秒),由浏览器对服务器发出HTTP request,然后由服务器返回最新的数据给客户端的浏览器。浏览器做处理后进行显示。无论后端此时是否有新的消息产生,都会进行响应。字面上看,这种方式是最简单的。这种方式的优点是,后端编写非常简单,逻辑不复杂。但是缺点是请求中大部分中是无用的,浪费了带宽和服务器资源。总结来说,简单粗暴,适用于小型(偷懒)应用。

    (2)Comet长轮询

    长轮询是客户端向服务器发送Ajax请求,服务器接到请求后hold住连接,直到有新消息才返回响应信息并关闭连接,客户端处理完响应信息后再向服务器发送新的请求;长连接是在页面中的iframe发送请求到服务端,服务端hold住请求并不断将需要返回前端的数据封装成调用javascript函数的形式响应到前端,前端不断收到响应并处理。Comet的实现原理和短轮询相比,很明显少了很多无用请求,减少了带宽压力,实现起来比短轮询复杂一丢丢。想比用短轮询的同学有梦想时,就可以用Comet来实现自己的推送。

    长轮询的优点很明显,在无消息的情况下不会频繁的请求,耗费资小并且实现了服务端主动向前端推送的功能,但是服务器hold连接会消耗资源,返回数据顺序无保证,难于管理维护。WebQQ(好像挂了)就是这样实现的。

    (3)Server-sent

    服务器推指的是HTML5规范中提供的服务端事件EventSource,浏览器在实现了该规范的前提下创建一个EventSource连接后,便可收到服务端的发送的消息,实现一个单向通信。客户端进行监听,并对响应的信息处理显示。该种方式已经实现了服务端主动推送至前端的功能。优点是在单项传输数据的场景中完全满足需求,开发人员扩展起来基本不需要改后端代码,直接用现有框架和技术就可以集成。

    (4)WebSocket

    WebSocket是HTML5下一种新的协议,是基于TCP的应用层协议,只需要一次连接,便可以实现全双工通信,客户端和服务端可以相互主动发送消息。客户端进行监听,并对响应的消息处理显示。

    这个技术相信基本都听说过,就算没写过代码,也大概知道干嘛的。通过名字就能知道,这是一个Socket连接,一个能在浏览器上用的Socket连接。WebSocket是HTML5标准中的一个内容,浏览器通过javascript脚本手动创建一个TCP连接与服务端进行通讯。优点是双向通信,都可以主动发送消息,既可以满足“问”+“答”的响应机制,也可以实现主动推送的功能。缺点就是编码相对来说会多点,服务端处理更复杂(我觉得当一条有情怀的咸鱼就应该用这个!)。

    1.3、实现个性化的推送 

    上面说了很多实现方案,针对自己系统的应用场景选择合适的推送方案才是合理的,因此最后简单说一下实现个性化推送的两种方式。第一种很简单,直接使用第三方实现的推送,无需复杂的开发运维,直接可以使用。第二种就是自己封装,可以选择如今较为火热的WebSocket来实现系统的消息推送。

    ①直接用第三方的消息推送服务(并发量多了会收费)

    在这里推荐一个第三方推送平台,GoEasy。

    推荐理由是GoEasy的理念符合我们的选择(可参考http://t.cn/Ex6jg3q):

    (1)更简单的方式将消息从服务器端推送至客户端
    (2)更简单的方式将消息从各种客户端推送至客户端

    GoEasy具体的使用方式这里不再赘述,详见官网。对于后端后端开发者,可直接使用Rest方式调用推送,对于前端或web开发者,可以从web客户端用javascript脚本进行调用推送。

    ②封装自己的推送服务

    如果是一个老系统进行扩展,那么更推荐使用Server-sent,服务端改动量不会很大。如果是新系统,更推荐websocket,实现的功能功能更全面。

    我们如果需要使用websocket技术实现自己的推送服务,需要注意哪些点,或者说需要踩哪些坑呢,本文列出几点供大家参考:

    长连接的心跳激活处理;

    服务端调优实现高并发量client同时在线(单机服务器可以实现百万并发长连接);

    群发消息;

    服务端维持多用户的状态;

    从WebSocket中获取HttpSession进行用户相关操作;

    等等等….


    二、WebSocket简介

    2.1、websocket的由来

    我们经常用的是HTTP协议,而HTTP协议是一种无状态的协议,要实现有状态的会话必须借助一些外部机制如session/cookie或者Token,这或多或少会带来一些不便,尤其是服务端和客户端需要实时交换数据的时候(监控,聊天),这个问题更加明显。为了适应这种环境,websocket就产生了,目的是即时通讯,替代轮询。

    2.2、websocket概述

    WebSocket 是HTML5一种新的协议。它实现了浏览器与服务器全双工通信(full-duplex)。一开始的握手需要借助HTTP请求完成。WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。

    websocket的特点或作用

    • 允许服务端主动向客户端推送数据

    • 在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

    websocket使用的优点

    • 更强的实时性

    • 保持连接状态,创建一次连接后,之后通信时可以省略部分状态信息。较少的控制开销,在连接创建后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对较小。在不包含扩展的情况下,对于服务器到客户端的内容,此头部大小只有2至10字节(和数据包长度有关);对于客户端到服务器的内容,此头部还需要加上额外的4字节的掩码。相对于HTTP请求每次都要携带完整的头部,此项开销显著减少了

    websocket使用的缺点

    由于websocket使用的持久连接,与服务器一直保持连接,对服务器压力很大

    2.3、websocket请求头和响应头

    浏览器发送websocket请求头类似如下:

     下面是对请求头部解释(比http协议多了Upgrade和Connection,是告诉服务器包协议设计ws):

    • Accept-Encoding:浏览器可以接受的数据的压缩类型。

    • Accept-Language:浏览器可以接受的语言类型。

    • Cache-Control:no-cache不使用强缓存。

    • Connection:Upgrade 通知服务器通信协议提升。

    • Host:主机名。

    • Origin:用于验证浏览器域名是否在服务器许可范围内。

    • Pragma:no-cache HTTP/1.0定义的不使用本地缓存。

    • Sec-WebSocket-Extensions:permessage-deflate; client_max_window_bits

    • Sec-WebSocket-Key:lb69kw8CsB4CrSk9tKa3 g==
      握手协议密钥,base64编码的16字节的随机字符串。

    • Sec-WebSocket-Version:13 版本号。

    • Upgrade:websocket 使用websocket协议进行传输数据,而不使用HTTP/1.1。

    • User-Agent:用户代理字符串。

    服务器接收到客户端请求后做出响应并返回如下:

     下面是服务器返回的头部解释:

    • Connection:Upgrade 通信协议提升。

    • Date:通信时间

    • Upgrade: websocket 传输协议升级为websocket。

    • Sec-WebSocket-Extensions:permessage-deflate

    • Sec-WebSocket-Accept:q9g5u1WfIWaAjNgMmjlTQTqkS/k=
      将Sec-WebSocket-Key的值进行一定的运算和该值进行比较来判断是否是目标服务器响应了WebSocket请求。

    • Upgrade: 使用websocket协议进行数据传输

    2.4、WebSocket和Socket的区别

    短答案:就像Java和JavaScript,并没有什么太大的关系,但又不能说完全没关系。可以这么说:

    • 命名方面,Socket是一个深入人心的概念,WebSocket借用了这一概念;

    • 使用方面,完全两个东西。

    Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口(不是协议,为了方便使用TCP或UDP而抽象出来的一层,是位于应用层和传输控制层之间的一组接口)。

    WebSocket是应用层协议。

    2.5、向指定用户发送WebSocket消息并处理对方不在线的情况

    给指定用户发送消息:

    • 如果接收者在线,则直接发送消息;

    • 否则将消息存储到redis,等用户上线后主动拉取未读消息。

    2.6、WebSocket心跳机制

    在使用WebSocket的过程中,有时候会遇到网络异常断开的情况,但是在网络断开的时候服务器端并没有触发onclose的事件。这样会有:服务器会继续向客户端发送多余的连接,并且这些数据还会丢失。所以就需要一种机制来检测客户端和服务端是否处于正常的连接状态,因此就有了WebSocket的心跳机制了。还有心跳,说明还活着,没有心跳说明已经挂掉了。

    心跳机制

    心跳机制是每隔一段时间会向服务器发送一个数据包,告诉服务器自己还活着,同时客户端会确认服务器端是否还活着,如果还活着的话,就会回传一个数据包给客户端来确定服务器端也还活着,否则的话,有可能是网络断开连接了,需要重连。

    2.7、Netty可以实现WebSocket

    Netty是由jboss提供的一款开源框架,常用于搭建RPC中的TCP服务器和WebSocket服务器,甚至是类似Tomcat的web服务器,反正就是各种网络服务器,在处理高并发的项目中,功能丰富且性能良好,基于Java中NIO的二次封装,具有比原生NIO更好更稳健的体验。


     三、基于Netty实现WebSocket消息推送

    因为产品需求,要实现服务端推送消息至客户端,并且支持客户端对用户点对点消息发送的社交功能。服务端给客户端推送消息,可以选择原生的WebSocket,或者更加高级的Netty框架实现

    在此我极力推荐netty,因为一款好的框架一般都是在原生的基础上进行包装成更加实用方便,很多我们需要自己考虑的问题都基本可以不用去考虑,不过此文不会去讲netty有多么的高深莫测,因为这些概念性的东西随处可见,而是通过实战来达到推送消息的目的。

    这个小节,我们主要讲解下如何整合Netty和WebSocket。我们需要使用netty对接websocket连接,实现双向通信,这一步需要有服务端的netty程序,用来处理客户端的websocket连接操作,例如建立连接,断开连接,收发数据等。

    WebSocket消息推送实现思路:

    前端使用WebSocket与服务端创建连接的时候,将用户ID传给服务端,服务端将用户ID与channel关联起来存储,同时将channel放入到channel组中。

    如果需要给所有用户发送消息,直接执行channel组的writeAndFlush()方法;

    如果需要给指定用户发送消息,根据用户ID查询到对应的channel,然后执行writeAndFlush()方法;

    前端获取到服务端推送的消息之后,将消息内容展示到文本域中

    下面是具体的代码实现,基本上每一步操作都配有注释说明,配合注释看应该还是比较容易理解的。

    3.1、引入Netty的依赖

    netty-all包含了netty的所有封装,hutool-all封装了常用的一些依赖,如Json相关

    <dependency>
    	<groupId>io.netty</groupId>
    	<artifactId>netty-all</artifactId>
    	<version>4.1.33.Final</version>
    </dependency>
    
    <dependency>
    	<groupId>cn.hutool</groupId>
    	<artifactId>hutool-all</artifactId>
    	<version>5.2.3</version>
    </dependency>
    

    3.2、修改配置文件application.yml

    server:
      port: 8899
    
    #netty的配置信息(端口号,webSocket路径)
    webSocket:
      netty:
        port: 58080
        path: /webSocket
        readerIdleTime: 30 #读空闲超时时间设置(Netty心跳检测配置)
        writerIdleTime: 30 #写空闲超时时间设置(Netty心跳检测配置)
        allIdleTime: 30 #读写空闲超时时间设置(Netty心跳检测配置)
    

    3.3、创建NettyConfig

    在NettyConfig中定义一个单例的channel组,管理所有的channel,再定义一个map,管理用户与channel的对应关系

    import io.netty.channel.Channel;
    import io.netty.channel.group.ChannelGroup;
    import io.netty.channel.group.DefaultChannelGroup;
    import io.netty.util.concurrent.GlobalEventExecutor;
    import java.util.concurrent.ConcurrentHashMap;
    
    /**
     * NettyConfig类
     *
     * @author hs
     * @date 2021-09-18
     */
    public class NettyConfig {
    
        /**
         * 定义一个channel组,管理所有的channel
         * GlobalEventExecutor.INSTANCE 是全局的事件执行器,是一个单例
         */
        private static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
    
        /**
         * 存放用户与Chanel的对应信息,用于给指定用户发送消息
         */
        private static ConcurrentHashMap<String,Channel> userChannelMap = new ConcurrentHashMap<>();
    
        private NettyConfig() {}
    
        /**
         * 获取channel组
         * @return
         */
        public static ChannelGroup getChannelGroup() {
            return channelGroup;
        }
    
        /**
         * 获取用户channel map
         * @return
         */
        public static ConcurrentHashMap<String,Channel> getUserChannelMap(){
            return userChannelMap;
        }
    }
    

    3.4、创建Netty的初始化类NettyServer(重点)

    定义两个EventLoopGroup,bossGroup辅助客户端的tcp连接请求, workGroup负责与客户端之间的读写操作,需要说明的是,需要开启一个新的线程来执行netty server,要不然会阻塞主线程,到时候就无法调用项目的其他controller接口了。

    import io.netty.bootstrap.ServerBootstrap;
    import io.netty.channel.ChannelFuture;
    import io.netty.channel.ChannelInitializer;
    import io.netty.channel.EventLoopGroup;
    import io.netty.channel.nio.NioEventLoopGroup;
    import io.netty.channel.socket.SocketChannel;
    import io.netty.channel.socket.nio.NioServerSocketChannel;
    import io.netty.handler.codec.http.HttpObjectAggregator;
    import io.netty.handler.codec.http.HttpServerCodec;
    import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
    import io.netty.handler.codec.serialization.ObjectEncoder;
    import io.netty.handler.stream.ChunkedWriteHandler;
    import io.netty.handler.timeout.IdleStateHandler;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.stereotype.Component;
    
    import javax.annotation.PostConstruct;
    import javax.annotation.PreDestroy;
    import java.net.InetSocketAddress;
    import java.util.concurrent.TimeUnit;
    
    /**
     * Netty初始化服务
     *
     * @author hs
     */
    @Component
    public class NettyServer{
    
        private static final Logger log = LoggerFactory.getLogger(NettyServer.class);
    
        /**
         * webSocket协议名
         */
        private static final String WEBSOCKET_PROTOCOL = "WebSocket";
    
        /**
         * 端口号
         */
        @Value("${webSocket.netty.port}")
        private int port;
    
        /**
         * webSocket路径
         */
        @Value("${webSocket.netty.path}")
        private String webSocketPath;
    
        /**
         * 在Netty心跳检测中配置 - 读空闲超时时间设置
         */
        @Value("${webSocket.netty.readerIdleTime}")
        private long readerIdleTime;
    
        /**
         * 在Netty心跳检测中配置 - 写空闲超时时间设置
         */
        @Value("${webSocket.netty.writerIdleTime}")
        private long writerIdleTime;
    
        /**
         * 在Netty心跳检测中配置 - 读写空闲超时时间设置
         */
        @Value("${webSocket.netty.allIdleTime}")
        private long allIdleTime;
    
        @Autowired
        private WebSocketHandler webSocketHandler;
    
        private EventLoopGroup bossGroup;
        private EventLoopGroup workGroup;
    
        /**
         * 启动
         * @throws InterruptedException
         */
        private void start() throws InterruptedException {
            bossGroup = new NioEventLoopGroup();
            workGroup = new NioEventLoopGroup();
            ServerBootstrap bootstrap = new ServerBootstrap();
            // bossGroup辅助客户端的tcp连接请求, workGroup负责与客户端之前的读写操作
            bootstrap.group(bossGroup,workGroup);
            // 设置NIO类型的channel
            bootstrap.channel(NioServerSocketChannel.class);
            // 设置监听端口
            bootstrap.localAddress(new InetSocketAddress(port));
            // 连接到达时会创建一个通道
            bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
    
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    // 心跳检测(一般情况第一个设置,如果超时了,则会调用userEventTriggered方法,且会告诉你超时的类型)
                    ch.pipeline().addLast(new IdleStateHandler(readerIdleTime, writerIdleTime, allIdleTime, TimeUnit.MINUTES));
                    // 流水线管理通道中的处理程序(Handler),用来处理业务
                    // webSocket协议本身是基于http协议的,所以这边也要使用http编解码器
                    ch.pipeline().addLast(new HttpServerCodec());
                    ch.pipeline().addLast(new ObjectEncoder());
                    // 以块的方式来写的处理器
                    ch.pipeline().addLast(new ChunkedWriteHandler());
                    /*
                        说明:
                        1、http数据在传输过程中是分段的,HttpObjectAggregator可以将多个段聚合
                        2、这就是为什么,当浏览器发送大量数据时,就会发送多次http请求
                     */
                    ch.pipeline().addLast(new HttpObjectAggregator(8192));
                    /*
                        说明:
                        1、对应webSocket,它的数据是以帧(frame)的形式传递
                        2、浏览器请求时 ws://localhost:58080/xxx 表示请求的uri
                        3、核心功能是将http协议升级为ws协议,保持长连接
                    */
                    ch.pipeline().addLast(new WebSocketServerProtocolHandler(webSocketPath, WEBSOCKET_PROTOCOL, true, 65536 * 10));
                    // 自定义的handler,处理业务逻辑
                    ch.pipeline().addLast(webSocketHandler);
                }
            });
            // 配置完成,开始绑定server,通过调用sync同步方法阻塞直到绑定成功
            ChannelFuture channelFuture = bootstrap.bind().sync();
            log.info("Server started and listen on:{}",channelFuture.channel().localAddress());
            // 对关闭通道进行监听
            channelFuture.channel().closeFuture().sync();
        }
    
        /**
         * 释放资源
         * @throws InterruptedException
         */
        @PreDestroy
        public void destroy() throws InterruptedException {
            if(bossGroup != null){
                bossGroup.shutdownGracefully().sync();
            }
            if(workGroup != null){
                workGroup.shutdownGracefully().sync();
            }
        }
    
        /**
         * 初始化(新线程开启)
         */
        @PostConstruct()
        public void init() {
            //需要开启一个新的线程来执行netty server 服务器
            new Thread(() -> {
                try {
                    start();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
    

    注意:启动方法需要开启一个新线程执行netty server服务,服务中配置了IdleStateHandler心跳检测,此类要在创建一个通道的第一个设置,如果超时了,则会调用userEventTriggered方法,且会告诉你超时的类型)

    3.5、具体实现业务的WebSocketHandler(重点)

    创建Netty配置的操作执行类WebSocketHandler,userEventTriggered为心跳检测超时所调用的方法,超时后ctx.channel().close()执行完毕会主动调用handlerRemoved删除通道及用户信息。

    import cn.hutool.json.JSONObject;
    import cn.hutool.json.JSONUtil;
    import io.netty.channel.*;
    import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
    import io.netty.handler.timeout.IdleState;
    import io.netty.handler.timeout.IdleStateEvent;
    import io.netty.util.AttributeKey;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.stereotype.Component;
    
    
    /**
     * 操作执行类
     *
     * TextWebSocketFrame类型,表示一个文本帧
     * @author hs
     */
    @Component
    @ChannelHandler.Sharable
    public class WebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
    
        private static final Logger log = LoggerFactory.getLogger(WebSocketHandler.class);
    
        /**
         * 一旦连接,第一个被执行
         * @param ctx
         * @throws Exception
         */
        @Override
        public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
            log.info("handlerAdded 被调用"+ctx.channel().id().asLongText());
            // 添加到channelGroup 通道组
            NettyConfig.getChannelGroup().add(ctx.channel());
        }
    
        /**
         * 读取数据
         */
        @Override
        protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
            // 获取用户ID,关联channel
            JSONObject jsonObject = JSONUtil.parseObj(msg.text());
            String uid = jsonObject.getStr("uid");
            // 当用户ID已存入通道内,则不进行写入,只有第一次建立连接时才会存入,其他情况发送uid则为心跳需求
            if(!NettyConfig.getUserChannelMap().containsKey(uid)){
                log.info("服务器收到消息:{}",msg.text());
                NettyConfig.getUserChannelMap().put(uid,ctx.channel());
                // 将用户ID作为自定义属性加入到channel中,方便随时channel中获取用户ID
                AttributeKey<String> key = AttributeKey.valueOf("userId");
                ctx.channel().attr(key).setIfAbsent(uid);
                // 回复消息
                ctx.channel().writeAndFlush(new TextWebSocketFrame("服务器连接成功!"));
            }else{
                // 前端定时请求,保持心跳连接,避免服务端误删通道
                ctx.channel().writeAndFlush(new TextWebSocketFrame("keep alive success!"));
            }
        }
    
        /**
         * 移除通道及关联用户
         * @param ctx
         * @throws Exception
         */
        @Override
        public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
            log.info("handlerRemoved 被调用"+ctx.channel().id().asLongText());
            // 删除通道
            NettyConfig.getChannelGroup().remove(ctx.channel());
            removeUserId(ctx);
        }
    
        /**
         * 异常处理
         * @param ctx
         * @param cause
         * @throws Exception
         */
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            log.info("异常:{}",cause.getMessage());
            // 删除通道
            NettyConfig.getChannelGroup().remove(ctx.channel());
            removeUserId(ctx);
            ctx.close();
        }
    
        /**
         * 心跳检测相关方法 - 会主动调用handlerRemoved
         * @param ctx
         * @param evt
         * @throws Exception
         */
        @Override
        public void userEventTriggered(final ChannelHandlerContext ctx, Object evt) throws Exception {
            if(evt instanceof IdleStateEvent){
                IdleStateEvent event = (IdleStateEvent)evt;
                if(event.state() == IdleState.ALL_IDLE){
                    //清除超时会话
                    ChannelFuture writeAndFlush = ctx.writeAndFlush("you will close");
                    writeAndFlush.addListener(new ChannelFutureListener() {
    
                        @Override
                        public void operationComplete(ChannelFuture future) throws Exception {
                            ctx.channel().close();
                        }
                    });
                }
            }else{
                super.userEventTriggered(ctx, evt);
            }
        }
    
        /**
         * 删除用户与channel的对应关系
         * @param ctx
         */
        private void removeUserId(ChannelHandlerContext ctx){
            AttributeKey<String> key = AttributeKey.valueOf("userId");
            String userId = ctx.channel().attr(key).get();
            NettyConfig.getUserChannelMap().remove(userId);
            log.info("删除用户与channel的对应关系,uid:{}",userId);
        }
    }
    

    3.6、具体消息推送的接口

    /**
     * 推送消息接口
     *
     * @author hs
     */
    public interface PushService {
    
        /**
         * 推送给指定用户
         * @param userId 用户ID
         * @param msg 消息信息
         */
        void pushMsgToOne(String userId,String msg);
    
        /**
         * 推送给所有用户
         * @param msg 消息信息
         */
        void pushMsgToAll(String msg);
    
        /**
         * 获取当前连接数
         * @return 连接数
         */
        int getConnectCount();
    }
    
    

    3.7、消息推送接口实现类

    import io.netty.channel.Channel;
    import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
    import org.springframework.stereotype.Service;
    import java.util.concurrent.ConcurrentHashMap;
    
    /**
     * 推送消息接口实现类
     *
     * @author hs
     */
    @Service
    public class PushServiceImpl implements PushService {
    
        /**
         * 推送给指定用户
         * @param userId 用户ID
         * @param msg 消息信息
         */
        @Override
        public void pushMsgToOne(String userId, String msg){
            ConcurrentHashMap<String, Channel> userChannelMap = NettyConfig.getUserChannelMap();
            Channel channel = userChannelMap.get(userId);
            channel.writeAndFlush(new TextWebSocketFrame(msg));
        }
    
        /**
         * 推送给所有用户
         * @param msg 消息信息
         */
        @Override
        public void pushMsgToAll(String msg){
            NettyConfig.getChannelGroup().writeAndFlush(new TextWebSocketFrame(msg));
        }
    
        /**
         * 获取当前连接数
         * @return 连接数
         */
        @Override
        public int getConnectCount() {
            return NettyConfig.getChannelGroup().size();
        }
    }
    
    

    3.8、提供消息推送服务的Controller

    主要为了测试

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.*;
    
    /**
     * 请求Controller(用于postman测试)
     *
     * @author hs
     */
    @RestController
    @RequestMapping("/push")
    public class PushController {
    
        @Autowired
        private PushService pushService;
    
        /**
         * 推送给所有用户
         * @param msg 消息信息
         */
        @PostMapping("/pushAll")
        public void pushToAll(@RequestParam("msg") String msg){
            pushService.pushMsgToAll(msg);
        }
    
        /**
         * 推送给指定用户
         * @param userId 用户ID
         * @param msg 消息信息
         */
        @PostMapping("/pushOne")
        public void pushMsgToOne(@RequestParam("userId") String userId,@RequestParam("msg") String msg){
            pushService.pushMsgToOne(userId,msg);
        }
    
        /**
         * 获取当前连接数
         */
        @GetMapping("/getConnectCount")
        public int getConnectCout(){
            return pushService.getConnectCount();
        }
    }
    

    3.9、Web前端通过websocket与服务端连接

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
        <script src="/static/jquery-2.2.4.min.js" charset="utf-8"></script>
    </head>
    <body>
    <script>
        var socket;
        var userId = "123456";
        // 判断当前浏览器是否支持webSocket
        if(window.WebSocket){
            socket = new WebSocket("ws://127.0.0.1:58080/webSocket")
            // 相当于channel的read事件,ev 收到服务器回送的消息
            socket.onmessage = function (ev) {
                var rt = document.getElementById("responseText");
                rt.value = rt.value + "\n" + ev.data;
            }
            // 相当于连接开启
            socket.onopen = function (ev) {
                var rt = document.getElementById("responseText");
                rt.value = "连接开启了..."
                socket.send(
                    JSON.stringify({
                        // 连接成功将,用户ID传给服务端
                        uid: userId
                    })
                );
            }
    
            //接受到服务端关闭连接时的回调方法
            socket.onclose = function (ev) {
                var rt = document.getElementById("responseText");
                rt.value = rt.value + "\n" + "连接关闭了...";
            }
    
       // 监听窗口事件,当窗口关闭时,主动断开websocket连接,防止连接没断开就关闭窗口,server端报错
             window.onbeforeunload = function(){
                socket.close();
             }
    
        }
        else
        {
            alert("当前浏览器不支持webSocket")
        }
    
        // 如果前端需要保持连接,则需要定时往服务器针对自己发送请求,返回的参数和发送参数一致则证明时间段内有交互,服务端则不进行连接断开操作
        var int = self.setInterval("clock()",10000);
        function clock() {
            socket.send(
                JSON.stringify({
                    // 连接成功将,用户ID传给服务端
                    uid: userId
                })
            );
        }
    
    </script>
    <form onsubmit="return false">
        <textarea id="responseText" style="height: 150px; width: 300px;"></textarea>
        <br>
        <input type="button" value="清空内容" onclick="document.getElementById('responseText').value=''">
    </form>
    </body>
    </html>
    

    3.10、WebSocket断开的原因

    原因有很多,最好在WebSocket断开时,将错误打印出来。

    ws.onclose = function (ev) {
      console.log('websocket 断开: ' + ev.code + ' ' + ev.reason + ' ' + ev.wasClean)
      console.log(ev)
    }

    错误状态码:

    WebSocket断开时,会触发CloseEvent, CloseEvent会在连接关闭时发送给使用 WebSockets 的客户端. 它在 WebSocket 对象的 onclose 事件监听器中使用。CloseEvent的code字段表示了WebSocket断开的原因。可以从该字段中分析断开的原因。

    CloseEvent有三个字段需要注意, 通过分析这三个字段,一般就可以找到断开原因

    • CloseEvent.code: code是错误码,是整数类型

    • CloseEvent.reason: reason是断开原因,是字符串

    • CloseEvent.wasClean: wasClean表示是否正常断开,是布尔值。一般异常断开时,该值为false

    状态码名称描述
    0–999保留段, 未使用.
    1000CLOSE_NORMAL正常关闭; 无论为何目的而创建, 该链接都已成功完成任务.
    1001CLOSE_GOING_AWAY终端离开, 可能因为服务端错误, 也可能因为浏览器正从打开连接的页面跳转离开.
    1002CLOSE_PROTOCOL_ERROR由于协议错误而中断连接.
    1003CLOSE_UNSUPPORTED由于接收到不允许的数据类型而断开连接 (如仅接收文本数据的终端接收到了二进制数据).
    1004保留. 其意义可能会在未来定义.
    1005CLOSE_NO_STATUS保留. 表示没有收到预期的状态码.
    1006CLOSE_ABNORMAL保留. 用于期望收到状态码时连接非正常关闭 (也就是说, 没有发送关闭帧).
    1007Unsupported Data由于收到了格式不符的数据而断开连接 (如文本消息中包含了非 UTF-8 数据).
    1008Policy Violation由于收到不符合约定的数据而断开连接. 这是一个通用状态码, 用于不适合使用 1003 和 1009 状态码的场景.
    1009CLOSE_TOO_LARGE由于收到过大的数据帧而断开连接.
    1010Missing Extension客户端期望服务器商定一个或多个拓展, 但服务器没有处理, 因此客户端断开连接.
    1011Internal Error客户端由于遇到没有预料的情况阻止其完成请求, 因此服务端断开连接.
    1012Service Restart服务器由于重启而断开连接.
    1013Try Again Later服务器由于临时原因断开连接, 如服务器过载因此断开一部分客户端连接.
    1014由 WebSocket标准保留以便未来使用.
    1015TLS Handshake保留. 表示连接由于无法完成 TLS 握手而关闭 (例如无法验证服务器证书).
    1016–1999由 WebSocket标准保留以便未来使用.
    2000–2999由 WebSocket拓展保留使用.
    3000–3999可以由库或框架使用.? 不应由应用使用. 可以在 IANA 注册, 先到先得.
    4000–4999可以由应用使用.

    四、WebSocket和Http之长连接和短连接区别

    4.1、HTTP1.0、HTTP1.1 和 HTTP2.0 的区别

    HTTP是一个应用层协议,无状态的,端口号为80。主要的版本有1.0/1.1/2.0.

    (1) HTTP/1.0

         一次请求-响应,建立一个连接,用完关闭;

    (2) HTTP/1.1 

         HTTP 1.1支持长连接(PersistentConnection)和请求的流水线(Pipelining)处理

        串行化单线程处理,可以同时在同一个tcp链接上发送多个请求,但是只有响应是有顺序的,只      有上一个请求完成后,下一个才能响应。一旦有任务处理超时等,后续任务只能被阻塞(线头阻塞);

    (3)HTTP/2

       HTTP2支持多路复用,所以通过同一个连接实现多个http请求传输变成了可能。请求并行执行,某任务耗时严重,不会影响到任务正常执行。

    4.2、什么是websocket?

    Websocket是html5提出的一个协议规范,是为解决客户端与服务端实时通信。本质上是一个基于tcp,先通过HTTP/HTTPS协议发起一条特殊的http请求进行握手后创建一个用于交换数据的TCP连接。

    WebSocket优势: 浏览器和服务器只需要要做一个握手的动作,在建立连接之后,双方可以在任意时刻相互推送信息。同时,服务器与客户端之间交换的头信息很小。

    4.3、什么是Http长连接和短连接

    在HTTP/1.0中默认使用短连接。也就是说,客户端和服务器每进行一次HTTP操作,就建立一次连接,任务结束就中断TCP连接。当客户端浏览器访问的某个HTML或其他类型的Web页中包含有其他的Web资源(如JavaScript文件、图像文件、CSS文件等),每遇到这样一个Web资源,浏览器就会重新建立一个HTTP会话。

    而从HTTP/1.1起,默认使用长连接,用以保持连接特性。使用长连接的HTTP协议,会在响应头加入这行代码:

    Connection:keep-alive
    

    在使用长连接的情况下,当一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的TCP连接不会关闭,客户端再次访问这个服务器时,会继续使用这一条已经建立的连接。Keep-Alive不会永久保持连接,它有一个保持时间,可以在不同的服务器软件(如Apache)中设定这个时间。实现长连接需要客户端和服务端都支持长连接。

    HTTP协议的长连接和短连接,实质上是TCP协议的长连接和短连接。

    4.4、http和websocket的长连接区别

    HTTP1.1通过使用Connection:keep-alive进行长连接,HTTP 1.1默认进行持久连接。在一次 TCP 连接中可以完成多个 HTTP 请求,但是对每个请求仍然要单独发 header,Keep-Alive不会永久保持连接,它有一个保持时间,可以在不同的服务器软件(如Apache)中设定这个时间。这种长连接是一种“伪链接”

    websocket的长连接,是一个真的全双工。长连接第一次tcp链路建立之后,后续数据可以双方都进行发送,不需要发送请求头。

    keep-alive双方并没有建立正真的连接会话,服务端可以在任何一次请求完成后关闭。WebSocket 它本身就规定了是正真的、双工的长连接,两边都必须要维持住连接的状态。

    4.5、HTTP2.0和HTTP1.X相比的新特性

    • 新的二进制格式(Binary Format),HTTP1.x的解析是基于文本。基于文本协议的格式解析存在天然缺陷,文本的表现形式有多样性,要做到健壮性考虑的场景必然很多,二进制则不同,只认0和1的组合。基于这种考虑HTTP2.0的协议解析决定采用二进制格式,实现方便且健壮。

    • 多路复用(MultiPlexing),即连接共享,即每一个request都是是用作连接共享机制的。一个request对应一个id,这样一个连接上可以有多个request,每个连接的request可以随机的混杂在一起,接收方可以根据request的 id将request再归属到各自不同的服务端请求里面。

    • header压缩,如上文中所言,对前面提到过HTTP1.x的header带有大量信息,而且每次都要重复发送,HTTP2.0使用encoder来减少需要传输的header大小,通讯双方各自cache一份header fields表,既避免了重复header的传输,又减小了需要传输的大小。

    • 服务端推送(server push),同SPDY一样,HTTP2.0也具有server push功能。


    参考链接:

    WebSocket使用

    SpringBoot+WebSocket+Netty实现消息推送

    展开全文
  • 百度消息推送(最简单的Demo)

    千次下载 热门讨论 2014-02-24 11:21:19
    一个百度消息推送的最小demo,有兴趣的朋友可以看我的博文中相关部分:http://blog.csdn.net/dawanganban/
  • 服务器端推送消息-客户端接收消息,利用websocket实现长连接无刷新消息推送
  • 1、项目需要,定时向所有在线用户推送一个广告或是推送一个通知之类的(比如服务器升级,请保存好手头工作之类的)。 2、相关环境 , Nginx、tomcat7、centos 6.5 3、项目框架,springMvc 4.0.6、layer
  • 基于消息推送的聊天工具

    千次下载 热门讨论 2013-06-08 15:20:46
    本应用是基于百度云推送的一款轻量级聊天工具,包含多个开源项目库,同时本代码也已经开源,欢迎访问我的博客主页:http://blog.csdn.net/weidi1989,由于时间仓促,错误与疏忽之处在所难免,希望各位朋友们以邮件的...
  • 这个是本人项目要用到自己写的一个demo,包括点对点消息推动,广播消息推送、离线推动,当然用这个前,你要到RabbitMQ官网上去下载RabbitMQ的server,这个很简单(因为上传资源大小限制,所以没传)。
  • java实现后台服务器消息推送

    千次阅读 2021-02-12 10:54:11
    优点 在以前的消息推送机制中,用的都是 Ajax 轮询(polling),在特定的时间间隔由浏览器自动发出请求,将服务器的消息主动的拉回来,这种方式是非常消耗资源的,因为它本质还是http请求,而且显得非常笨拙。...

    1.什么是WebSocket

    WebSocket协议是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工(full-duplex)通信——允许服务器主动发送信息给客户端。

    2.实现原理

    在实现websocket连线过程中,需要通过浏览器发出websocket连线请求,然后服务器发出回应,这个过程通常称为“握手” 。在 WebSocket API,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。

    3.优点

    在以前的消息推送机制中,用的都是 Ajax 轮询(polling),在特定的时间间隔由浏览器自动发出请求,将服务器的消息主动的拉回来,这种方式是非常消耗资源的,因为它本质还是http请求,而且显得非常笨拙。而WebSocket 在浏览器和服务器完成一个握手的动作,在建立连接之后,服务器可以主动传送数据给客户端,客户端也可以随时向服务器发送数据。

    4.WebSocket和Socket的区别

    1.WebSocket:

    websocket通讯的建立阶段是依赖于http协议的。最初的握手阶段是http协议,握手完成后就切换到websocket协议,并完全与http协议脱离了。建立通讯时,也是由客户端主动发起连接请求,服务端被动监听。通讯一旦建立连接后,通讯就是“全双工”模式了。也就是说服务端和客户端都能在任何时间自由得发送数据,非常适合服务端要主动推送实时数据的业务场景。交互模式不再是“请求-应答”模式,完全由开发者自行设计通讯协议。通信的数据是基于“帧(frame)”的,可以传输文本数据,也可以直接传输二进制数据,效率高。当然,开发者也就要考虑封包、拆包、编号等技术细节。

    2.Socket:

    服务端监听通讯,被动提供服务;客户端主动向服务端发起连接请求,建立起通讯。每一次交互都是:客户端主动发起请求(request),服务端被动应答(response)。服务端不能主动向客户端推送数据。通信的数据是基于文本格式的。二进制数据(比如图片等)要利用base64等手段转换为文本后才能传输。

    5.WebSocket客户端:

    var websocket = null;

    var host = document.location.host;

    var username = "${loginUsername}"; // 获得当前登录人员的userName

    // alert(username)

    //判断当前浏览器是否支持WebSocket

    if ('WebSocket' in window) {

    alert("浏览器支持Websocket")

    websocket = new WebSocket('ws://'+host+'/mm-dorado/webSocket/'+username);

    } else {

    alert('当前浏览器 Not support websocket')

    }

    //连接发生错误的回调方法

    websocket.onerror = function() {

    alert("WebSocket连接发生错误")

    setMessageInnerHTML("WebSocket连接发生错误");

    };

    //连接成功建立的回调方法

    websocket.onopen = function() {

    alert("WebSocket连接成功")

    setMessageInnerHTML("WebSocket连接成功");

    }

    //接收到消息的回调方法

    websocket.onmessage = function(event) {

    alert("接收到消息的回调方法")

    alert("这是后台推送的消息:"+event.data);

    websocket.close();

    alert("webSocket已关闭!")

    }

    //连接关闭的回调方法

    websocket.onclose = function() {

    setMessageInnerHTML("WebSocket连接关闭");

    }

    //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。

    window.onbeforeunload = function() {

    closeWebSocket();

    }

    //关闭WebSocket连接

    function closeWebSocket() {

    websocket.close();

    }

    //将消息显示在网页上

    function setMessageInnerHTML(innerHTML) {

    document.getElementById('message').innerHTML += innerHTML + '
    ';

    }

    6.WebSocket服务端(java后台):

    1.核心类:

    package com.mes.util;

    import

    10273594.htmljava.io.IOException;

    import java.util.Map;

    import java.util.concurrent.ConcurrentHashMap;

    import javax.websocket.OnClose;

    import javax.websocket.OnError;

    import javax.websocket.OnMessage;

    import javax.websocket.OnOpen;

    import javax.websocket.Session;

    import javax.websocket.server.PathParam;

    import javax.websocket.server.ServerEndpoint;

    import org.springframework.stereotype.Component;

    import org.springframework.stereotype.Service;

    import com.google.gson.JsonObject;

    import net.sf.json.JSONObject;

    @ServerEndpoint("/webSocket/{username}")

    public class WebSocket {

    private static int onlineCount = 0;

    private static Map clients = new ConcurrentHashMap();

    private Session session;

    private String username;

    @OnOpen

    public void onOpen(@PathParam("username") String username, Session session) throws IOException {

    this.username = username;

    this.session = session;

    addOnlineCount();

    clients.put(username, this);

    System.out.println("已连接");

    }

    @OnClose

    public void onClose() throws IOException {

    clients.remove(username);

    subOnlineCount();

    }

    @OnMessage

    public void onMessage(String message) throws IOException {

    JSONObject jsonTo = JSONObject.fromObject(message);

    String mes = (String) jsonTo.get("message");

    if (!jsonTo.get("To

    展开全文
  • uni-app消息推送方案

    万次阅读 热门讨论 2019-11-06 01:13:40
    uni-app是支持消息推送的,参考如下文档: UniPush介绍 UniPush使用指南 UniPush开通指南 如何自定义推送通知的图标? 在 uni-app 中使用 UniPush 二、效果 开源项目uniapp-admin 三、需求 不同角色的用户登陆App,...

    一、引言

    uni-app是支持消息推送的,参考如下文档:

    UniPush介绍

    UniPush使用指南

    UniPush开通指南

    如何自定义推送通知的图标?

    在 uni-app 中使用 UniPush

    二、效果

    开源项目uniapp-admin

    三、需求

    不同角色的用户登陆App,收到不同的待办提醒。即谁处理这个待办任务,谁会收到这个提醒。对不同角色的用户推送待办消息

    四、方案步骤

    4.1 查看个推文档

    因为uni-app的推送是集成了个推,所以查看个推文档接入方案

    因为后台是java语言,所以查看java集成指南

    • 下载服务端SDK开发工具包,下载地址为:http://www.getui.com/download/docs/getui/server/GETUI_JAVA_SDK_4.1.0.5.zip

    • 导入"GETUI_SERVER_SDK\资源文件”目录下的所有jar包

    # uni-push
    mvn install:install-file -Dfile="gexin-rp-fastjson-1.0.0.3.jar" -DgroupId=com.gexin.platform -DartifactId=gexin-rp-fastjson -Dversion=1.0.0.3 -Dpackaging=jar
    mvn install:install-file -Dfile="gexin-rp-sdk-base-4.0.0.30.jar" -DgroupId=com.gexin.platform -DartifactId=gexin-rp-sdk-base -Dversion=4.0.0.30 -Dpackaging=jar
    mvn install:install-file -Dfile="gexin-rp-sdk-http-4.1.0.5.jar" -DgroupId=com.gexin.platform -DartifactId=gexin-rp-sdk-http -Dversion=4.1.0.5 -Dpackaging=jar
    mvn install:install-file -Dfile="gexin-rp-sdk-template-4.0.0.24.jar" -DgroupId=com.gexin.platform -DartifactId=gexin-rp-sdk-template -Dversion=4.0.0.24 -Dpackaging=jar
    mvn install:install-file -Dfile="protobuf-java-2.5.0.jar" -DgroupId=com.google.protobuf -DartifactId=protobuf-java -Dversion=2.5.0 -Dpackaging=jar
    
    <!-- uni push -->
    <dependency>
        <groupId>com.gexin.platform</groupId>
        <artifactId>gexin-rp-sdk-base</artifactId>
        <version>4.0.0.30</version>
    </dependency>
    <dependency>
        <groupId>com.gexin.platform</groupId>
        <artifactId>gexin-rp-sdk-template</artifactId>
        <version>4.0.0.24</version>
    </dependency>
    <dependency>
        <groupId>com.gexin.platform</groupId>
        <artifactId>gexin-rp-sdk-http</artifactId>
        <version>4.1.0.5</version>
    </dependency>
    <dependency>
        <groupId>com.gexin.platform</groupId>
        <artifactId>gexin-rp-fastjson</artifactId>
        <version>1.0.0.3</version>
    </dependency>
    <dependency>
        <groupId>com.google.protobuf</groupId>
        <artifactId>protobuf-java</artifactId>
        <version>2.5.0</version>
    </dependency>
    

    4.2 编写服务端简单demo

    按照UniPush开通指南,开通UniPush,获取appId、appKey等,编写下面简单demo,客户端就会收到消息啦~

    客户端接收消息代码,参考4.3

    public class AppPush {
    
        // STEP1:获取应用基本信息
        private static String appId = "";
        private static String appKey = "";
        private static String masterSecret = "";
        private static String url = "http://sdk.open.api.igexin.com/apiex.htm";
    
        public static void main(String[] args) throws IOException {
    
            IGtPush push = new IGtPush(url, appKey, masterSecret);
    
            Style0 style = new Style0();
            // STEP2:设置推送标题、推送内容
            style.setTitle("请输入通知栏标题");
            style.setText("请输入通知栏内容");
            // 注释采用默认图标
            // style.setLogo("push.png");  // 设置推送图标
            // STEP3:设置响铃、震动等推送效果
            style.setRing(true);  // 设置响铃
            style.setVibrate(true);  // 设置震动
    
            // STEP4:选择通知模板
            NotificationTemplate template = new NotificationTemplate();
            template.setAppId(appId);
            template.setAppkey(appKey);
            template.setStyle(style);
            // 点击消息打开应用
            template.setTransmissionType(1);
            // 传递自定义消息
            template.setTransmissionContent("自定义消息,可以是json
            字符串");
    
    
            // STEP5:定义"AppMessage"类型消息对象,设置推送消息有效期等推送参数
            List<String> appIds = new ArrayList<String>();
            appIds.add(appId);
            AppMessage message = new AppMessage();
            message.setData(template);
            message.setAppIdList(appIds);
            message.setOffline(true);
            message.setOfflineExpireTime(1000 * 600);  // 时间单位为毫秒
    
            // STEP6:执行推送
            IPushResult ret = push.pushMessageToApp(message);
            System.out.println(ret.getResponse().toString());
        }
    }
    

    4.3 客户端App.vue添加消息处理逻辑

    /**
      * 处理推送消息
      */
    handlePush() {
      // #ifdef APP-PLUS
      const _self = this
      const _handlePush = function(message) {
        // 获取自定义信息
        let payload = message.payload
        try {
          // JSON解析
          payload = JSON.parse(payload)
          // 携带自定义信息跳转应用页面
          uni.navigateTo({
            url: '/pages/xxx?data=' + JSON.stringify(payload)
          })
        } catch(e) {}
      }
      // 事件处理
      plus.push.addEventListener('click', _handlePush)
      plus.push.addEventListener('receive', _handlePush)
      // #endif
    },
    

    五、思考

    应用确实接收到了消息,但是所有角色的用户都会接收到和自己不想关的待办任务消息。这是违背需求的!所以基于此,作者研究了服务端发送到客户端消息的原理:

    实际上,消息不管是单推还是群推,推送的目标都是clientid,clientid标识每个客户端的身份

    问题来了,怎么获取客户端的clientid呢?

    六、最终方案

    6.1 获取客户端clientid

    经过查询资料,有一个api是getClientInfo方法,可以获取客户端信息,但是必须条件编译,因为是plus接口。

    以下代码,用户登陆完成时,获取客户端信息(appid,appkey,clientid)用户信息(账户名、角色等)、其他信息,向服务端提交api请求,保存客户端clientid和角色的关联信息。

    // 保存clientid到服务器
    // #ifdef APP-PLUS
    const clientInfo = plus.push.getClientInfo()
    let pushUser = {
      clientid: clientInfo.clientid,
      appid: clientInfo.appid,
      appkey: clientInfo.appkey,
      userName: '用户名',
      userRole: '用户角色'
    }
    // 提交api请求,服务端保存客户端角色信息
    Vue.prototype.$minApi.savePushUser(pushUser)
    // #endif
    

    6.2 服务端接收客户端角色信息处理

    服务端接收到信息,根据自己的业务逻辑,保存或者更新,作者的处理逻辑时已经保存的clientid,不在新增,更新角色信息。

    clientid和角色关系,数据库表结构

    数据库表结构

    6.3 服务端根据不同角色发送待办提醒

    改进消息发送方式,采用个推toList:简称“批量推”,指向制定的一批用户推送消息

    /**
    * @params pushMessage推送消息
    * @params appPushList推送角色目标列表
    */
    public static void pushMessage(PushMessage pushMessage, List<AppPush> appPushList) {
      IGtPush push = new IGtPush(url, appKey, masterSecret);
    
      Style0 style = new Style0();
      // STEP2:设置推送标题、推送内容
      style.setTitle(pushMessage.getTitle());
      style.setText(pushMessage.getContent());
    //        style.setLogo("push.png"); // 设置推送图标
      // STEP3:设置响铃、震动等推送效果
      style.setRing(true);  // 设置响铃
      style.setVibrate(true);  // 设置震动
    
      // STEP4:选择通知模板
      NotificationTemplate template = new NotificationTemplate();
      template.setAppId(appId);
      template.setAppkey(appKey);
      template.setStyle(style);
      // 点击消息打开应用
      template.setTransmissionType(1);
      // 传递自定义消息
      template.setTransmissionContent(JSONUtil.toJsonStr(pushMessage));
    
    
      // STEP5:定义"AppMessage"类型消息对象,设置推送消息有效期等推送参数
      // 采用toList方案,定义ListMessage消息类型
    //        List<String> appIds = new ArrayList<String>();
    //        appIds.add(appId);
      ListMessage message = new ListMessage();
      message.setData(template);
    //        message.setAppIdList(appIds);
      message.setOffline(true);
      message.setOfflineExpireTime(1000 * 600);  // 时间单位为毫秒
    
      String contentId = push.getContentId(message);
      // 获取推送目标
      List targets = new ArrayList();
      for (AppPush ap : appPushList) {
          Target target = new Target();
          target.setAppId(appId);
          target.setClientId(ap.getClientid());
          targets.add(target);
      }
    
      // STEP6:执行推送,不采用toApp方案,采用toList方案
    //        IPushResult ret = push.pushMessageToApp(message);
      IPushResult ret = push.pushMessageToList(contentId, targets);
      System.out.println(ret.getResponse().toString());
    }
    

    PushMessage类是一个model

    public class PushMessage {
        private String title;
        private String content;
        // 用户角色
        private String userRole;
        // 其他对象
    
        // 省略,getter setter方法
    }
    

    AppPush类是数据库表映射类

    public class AppPush
      private String appid;//appid
      private String appkey;//appkey
      private String clientid;//clientid
      private String userName;//账户
      private String userRole;//用户角色
      // 其他对象
    
      // 省略,getter setter方法
    }
    

    七、自定义通知图标

    在客户端manifest.json文件中的sdkConfigs中添加如下配置,图标自己添加

    /* SDK配置 */
    "sdkConfigs" : {
        "push" : {
            "unipush" : {
    "icons": {
      "push": {
        "ldpi": "unpackage/res/icons/48x48.png",
        "mdpi": "unpackage/res/icons/48x48.png",
        "hdpi" : "unpackage/res/icons/72x72.png",
        "xhdpi" : "unpackage/res/icons/96x96.png",
        "xxhdpi" : "unpackage/res/icons/144x144.png",
        "xxxhdpi" : "unpackage/res/icons/192x192.png"
      },
      "small": {
        "ldpi": "unpackage/res/icons/18x18.png",
        "mdpi": "unpackage/res/icons/24x24.png",
        "hdpi": "unpackage/res/icons/36x36.png",
        "xhdpi": "unpackage/res/icons/48x48.png",
        "xxhdpi": "unpackage/res/icons/72x72.png"
      }
    }
    }
        }
    },
    

    八、开源项目

    开源项目uniapp-admin

    开源不易,且用且珍惜!


    赞助作者,互相交流

    转载请注明:溜爸 » uni-app消息推送方案

    展开全文
  • Android实现系统消息推送

    千次阅读 2021-06-07 16:04:18
    现在好多应用都接入了推送功能,市面上也有很多关于推送的第三方,例如极光等等,那么我们需求不大,接入极光会造成很大的资源浪费,下面我们来看下利用android服务进行本地推送消息,1.注册一个Serviceimport ...
  • 在好几年前,就已经注意到DDPush这款推送中间件,不过看近来发展也还是停留在V1.0...DDPush 任意门 消息推送 DDPush是什么 DDPush可以做什么 移动互联网消息推送 IM实时消息系统核心组件 物联网设备控制与交互 ...
  • 这篇文章介绍一下目前企业微信所支持的推送消息的格式和使用方法,大部分内容与消息示例均来源于目前钉钉的开发文档。
  • App消息推送的原理

    万次阅读 2019-04-29 16:12:06
    Android消息推送原理3.1 操作系统有自身的消息推送功能(系统级别)3.2 三种基本的推送方式:Push、Pull 和 SMS3.3 七种主流的Android消息推送方式 1. 基本概念 目的: 在用户未打开App时,App主动向用户推送...
  • vue消息推送【个推】

    千次阅读 2019-04-30 14:20:34
    公司要求我使用Vue写的App具有消息推送功能。而我知道Vue打包成App,则使用Hbuilder的SDK配置,发现他只有小米推送和个推。由于个推在市场上使用量比较多,于是就开启了我的Vue消息推送个推之旅。 而此时发现个推...
  • 微信企业消息推送方案

    千次阅读 2019-12-02 11:20:48
    在软件程序实际应用中,在软件中推送可能还不能满足实际需求,需要把消息推送到用户手机,目前比较好的方式,可能是微信消息推送。因此做一个记录 企业微信/企业号注册 微信消息推送需要配合 企业微信号 做消息推送 ...
  • springboot 实现微信公众号的模板消息推送

    千次阅读 热门讨论 2020-09-13 13:37:03
    发送消息模板可不配置公众号对接相关功能,直接使用openId 发送模板信息功能即可,openId 可让用户关注公众号录入系统, 或者在公众号添加h5 表单,绑定系统账号, 微信打开的h5 页面可获取当前用户的openId 二、实现...
  • 前面写过一篇云开发实现小程序订阅消息(模板消息)推送的文章,《借助云开发实现小程序订阅消息和模板消息的推送功能》是有好多同学用的是Java写后台,所以今天就再来写一篇Java后台实现小程序订阅消息推送的文章。...
  • 小程序消息推送

    千次阅读 2021-05-10 16:38:58
    小程序消息推送 文章目录前言使用步骤1.开通小程序消息推送功能2.接收消息推送并自动应答总结 前言 最近公司一个项目需要实现小程序客服自动为用户发送企业微信二维码功能 主要用到了小程序的消息推送功能 本文大概...
  • 极光推送是比较完善的第三方消息推送平台,利用该平台能够轻松实现Android消息推送,该推送为长连接实时消息推送
  • android通过Service实现消息推送(客户端+服务器)

    千次下载 热门讨论 2013-11-21 14:21:43
    模拟android客户端通过Service,每隔一段时间向系统发送一个请求,已获取重要的,实时更新的消息
  • Springboot之整合SSE实现消息推送

    千次阅读 2022-04-10 22:32:31
    Springboot整合SSE长链接,实现服务端主动推送消息及踩坑记录
  • 本例子分为客户端(就是android手机),网页端为后台服务器。可以实现网页消息推送到手机,手机也可发消息到网页,很好的实现了消息的同步,不会有延迟,刷新现象。可以实现网页微信,二维码扫描登陆,聊天室等等。
  • HTTP消息推送

    千次阅读 2022-04-11 13:57:00
    目前支持设备数据变化通知、设备指令响应通知、设备事件上报通知、设备上下线通知等消息类型的订阅,各协议对应消息类型及格式参见订阅推送消息格式。订阅级别分为设备级,产品级和分组级。目前订阅生效时间为1分钟...
  • 工信部统一 Android 消息推送标准

    千次阅读 2019-06-25 14:54:45
    二、推送技术发展 1、轮询方式 2、SMS短信推送方式 3、长连接推送方式 三、统一推送 四、作用 五、具体时间表 一、介绍 统一推送联盟成立于2017年10月,挂靠单位是电信终端产业协会(TAF),接受工业和信息...
  • 导语 | 消息推送我们几乎每天都会用到,但你知道 iOS 中的消息推送是如何实现的吗?本文将从推送权限申请,到本地和远程消息推送,再到 App 对推送消息的处理等多个步骤,详细介绍 iO...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 211,264
精华内容 84,505
关键字:

消息推送