精华内容
下载资源
问答
  • 2021-08-05 10:05:22

    手机上各类App的信息推送既然给用户带来了便利,但是过多和不必要的推送绝对会对用户造成骚扰,目前不少国产手机系统已经有较为完善的智能中心来管理第三方应用的信息推送。

    科技君这两个月一直以魅族PRO6 Plus作为主力机型使用,过程中发现其设置中的“通知智能管理”只能关闭第三方App应用的信息推送,对于系统自带应用并没有关闭的选项。

    作为一个动手者的用户,科技君找到关闭了这类系统推送的办法。请往下继续看。

    当收到系统推送后,点开推送信息,先看看是来自哪个原生自带应用的推送。

    如这条推送是来自魅族flyme系统的“视频”应用程序的,然后点击选择“我的”分布标签,下拉到最下方,点击进入“设置”选项。

    在“设置”里就会有精选内容推送的开关,关闭即可,以后就不会再收到该应用的信息推送。同理其他flyme系统自带的应用程序也可以通过这样的办法关闭,而且你可以选择当收到推送后再单个关闭,也可以选择一次性逐个应用程序打开后把信息推送功能关闭。

    flyme作为国内其中一个优秀的手机系统(或者说ROM),一直以来都以体验都为主要的卖点,如果能把自带应用的推送选项开关也统一到通知管理中心,相信这样会大大提升用户体验,这远比目前这种推送方式来得更有价值。

    更多相关内容
  • 问题的提出 本次接入的是个推,其他...App在前台时,弹出Dialog提醒用户有新消息,但是最新版的个推文档接收推送消息是继承IntentService,无法获取弹出Dialog所需要的Context(注意不能用getApplicationContext()),
  • 安卓系统手机如何关闭微信的订阅号推送,让你从此不再受到广告的骚扰,从此有个清净的聊天界面
  • 关闭魅族Flyme系统通知栏推送的广告,只需以下几个步骤即可关闭。步骤一:点击“设置”,在设置中点击“新闻资讯”,随后在该设置界面中找到 “精选内容推送”并点击关闭。步骤二:在设置中点击“游戏中心”随后进入...

    关闭魅族Flyme系统通知栏推送的广告,只需以下几个步骤即可关闭。

    步骤一:点击“设置”,在设置中点击“新闻资讯”,随后在该设置界面中找到 “精选内容推送”并点击关闭。

    2db9c4e9955a0d8985ba43dc144eda17.png

    步骤二:在设置中点击“游戏中心”随后进入消息提醒选项,关闭“精选内容推送”。

    a587dc49ae30a61ef3d1767b512c9068.png

    步骤三:在设置中点击“应用商店”随后在该设置界面中,找到 “精选内容推送”并点击关闭。

    8fce26704a2d66e18dbfdb20894039c3.png

    步骤四:在设置中点击“福利中心”随后在该设置界面中找到“显示已结束活动 ”、“精选内容推送”、“任务完成后通知我抽奖”和“积分快过期时通知我兑换”并点击关闭。

    3a343d47e520bed48e5795dcc2e0277a.png

    步骤五:在设置中点击“主题美化”随后在该设置界面中找到“主题和字体更新通知”和“精选内容推送”并点击关闭。

    e9f1b3c8f3039ec72a6617fad722185a.png

    步骤六:在设置中点击“生活服务”随后在该设置界面中找到“精选内容推送”和“出行服务推荐”并点击关闭。

    f3ccd0e1b32280a4c0fe7c742e1b12ca.png

    步骤七:在设置中点击“计步器”随后在该设置界面中找到“精选内容推送”并点击关闭。

    176132cee59e1ffe2e63de724a4ea4b7.png

    步骤八:在设置中点击“音乐”随后在该设置界面中找到“精彩内容推送”并点击关闭。

    c4bfc3619945e552e71764c3d5da3cb1.png

    步骤九:在设置中点击“读书”随后在该设置界面中找到“精彩内容推送”并点击关闭。

    62ee07b1c1f497b5a569783ecfab7cde.png

    步骤十:在设置中点击“图库”随后在该设置界面中找到“精彩内容推送”并点击关闭。

    4d01c4e1cfe3dcdde5bfb980d61f34b4.png

    步骤十一:在设置中点击“钱包”随后在该设置界面中找到“精彩内容推送”并点击关闭。

    91cbccdb166241a2b2268f1a17675fb2.png

    Flyme 共有 11 处内置广告,只需将这11处的推荐内容一一关闭即可。

    展开全文
  • 本文将从推送权限申请,到本地和远程消息推送,再到 App 对推送消息的处理等多个步骤,详细介绍 iOS 中消息推送的工作流程。文章作者:赵哲,腾讯游戏运营研发工程师。一、概述消息推送是一种 App 向用户传递信息的...

    导语 |消息推送我们几乎每天都会用到,但你知道 iOS 中的消息推送是如何实现的吗?本文将从推送权限申请,到本地和远程消息推送,再到 App 对推送消息的处理等多个步骤,详细介绍 iOS 中消息推送的工作流程。文章作者:赵哲,腾讯游戏运营研发工程师。

    一、概述

    消息推送是一种 App 向用户传递信息的重要方式,无论 App 是否正在运行,只要用户打开了通知权限就能够收到推送消息。

    开发者通过调用 iOS 系统方法就可以发起本地消息推送,例如我们最常见的闹钟应用,App 能够根据本地存储的闹钟信息直接发起本地通知,因此即使没有网络也能收到闹钟提醒。

    远程消息推送则是由业务方服务器将消息内容按照固定格式发送到 Apple Push Notitfication service(简称APNs),然后再经由苹果的 APNs 服务器推送到用户设备上,例如腾讯新闻可以向用户推送时事热点新闻,QQ邮箱可以为用户推送收到新邮件的提醒,游戏 App 可以通过这种方式通知玩家有新的游戏福利。

    既能够及时地通知用户重要信息,也能够促使用户通过推送消息打开或唤醒App,提高App的使用率。

    除了标题、内容、提示音和角标数字等固定推送参数以外,开发者还可以在推送消息中增加自定义参数,让用户在点击推送消息时能够直达相关新闻、邮件或福利页面,提供更好的用户体验和页面的曝光率。

    b5efbd1d7bbaf1eafc7d93f90b26dc04.png

    二、XCode配置

    在使用消息推送相关功能之前,我们首先需要准备支持推送功能的证书,个人开发者可以参考腾讯云的 TPNS 文档 [1],在苹果开发者中心中配置和导出推送证书。

    此外,还需要在XCode的工程配置 Signing & Capabilities 配置中增加消息推送权限,在操作完成后 Xcode 会自动生成或更新工程的 entitlements 文件,增加如图所示的APS Environment 字段。

    0a30c53eb14db86731a87c5111616d56.png

    三、申请消息推送权限

    c0fd8845bf824c0d3c90226feab94561.png

    无论是本地推送还是远程推送,在推送前都必须要先向用户申请推送权限,只有用户授权后才能够收到推送消息。

    苹果在 iOS10 中引入了 UserNotifications 框架,将推送相关功能进行了封装和升级,除了以前 UIApplication 可以做到的一些基本的本地和远程消息推送功能外,还增加了撤回或修改推送消息、自定义通知 UI、推送消息前台显示等功能。

    在 iOS10 及以上的版本中,苹果推荐开发者使用:

    requestAuthorizationWithOptions:completionHandler: 方法向用户申请消息推送权限。

    该方法需要指定一个用于描述推送权限的 UNAuthorizationOptions 类型参数,包括 alert (消息的标题、文字等内容)、sound(消息提示音)、badge(App右上角显示的角标);还可以在该方法的 completionHandler 回调方法中通过 granted 参数来判断用户是否允许了授权。相关代码如下:

    在iOS9中,直接使用 UIApplication的registerUserNotificationSettings 方法即可,该方法同样需要通过配置 sound、alert、badge 等参数,但是没有提供用于判断用户点击了授权还是拒绝的回调方法。相关代码如下:

    要注意无论是 UserNotifications 还是 UIApplication 的申请推送权限的方法,上文中的申请用户授权的系统弹窗都只会显示一次,iOS 会记录用户对于该App的授权状态,不会向用户重复申请授权。

    消息推送是 App 的一项重要功能,同时也是很好的运营手段,因此很多 App 在启动后会检查消息推送的授权状态,如果用户拒绝了消息推送权限,仍然会以一定的频率弹窗提醒用户,在 iOS 的设置中心中再去打开 App 的推送权限。相关代码如下:

    四、本地推送

    d0e8d7a8c0b8dc981aaf159ac2fed13b.png

    在 iOS10 中,UserNotifications 框架为我们提供了 UNMutableNotificationContent 对象描述消息推送的标题、内容、提示音、角标等内容。UNNotificationTrigger 对象描述消息推送的推送时间策略,UNNotificationRequest 对象整合推送内容和时间。

    每个 Request 对象都需要配置一个 id 来标识该条推送内容,UNUserNotificationCenter 通过该 id 来管理(包括增加、删除、查询和修改)所有的 Request。

    UNNotificationTrigger 有四个子类,分别是 UNTimeIntervalNotificationTrigger 用于通过时间间隔控制消息推送;UNCalendarNotificationTrigger 通过日期控制消息推送;UNLocationNotificationTrigger 通过地理位置控制消息推送;UNPushNotificationTrigger 远程消息推送对象。相关代码如下:

    在 iOS9 中,UIApplication 提供了 presentLocalNotificationNow 和 scheduleLocalNotification 两个本地消息推送的方法。分别表示立即推送和按照固定日期推送,UILocalNotification 同时描述了消息内容和推送的时机。

    示例代码是一个 2s 后推送的本地消息,soundName 属性用于描述消息的提示音,用户可以自定义提示音(需要将音频文件打包到安装包中)或者使用默认提示音乐,repeatInterval 和 repeatCalendar 属性分别用于根据时间差和日期进行重复提示的操作。相关代码如下:

    五、远程推送

    e3c0be0f981be5079276344188cbfeeb.png

    不同于本地消息推送不依赖网络请求,可以直接调用 iOS 系统方法,远程消息推送的实现涉及到用户设备、我们自己的业务方服务器和苹果的 APNs 服务的交互。

    不同于 Android 系统中远程消息推送的实现,需要 App 自身通过后台服务与业务服务器维持长链接通信,iOS 中的消息推送是操作系统与苹果的 APNs 服务器直接交互实现的,App 自身并不需要维持与服务器的连接。

    只要用户开启了推送权限,我们的业务服务器就可以随时通过调用 APNs 服务向用户推送通知,这样既能够为开发者和用户提供安全稳定的推送服务,也够节省系统资源消耗,提高系统流畅度和电池续航能力。

    ba76d2d80d9070be24e483809516d9bf.png

    iOS 客户端远程消息推送的实现可以分为以下几个流程:

    用户的 iphone 通过 iOS 的系统方法调用与苹果的 APNs 服务器通信,获取设备的 deviceToken,它是由 APNs 服务分配的用于唯一标识不同设备上的不同 App,可以认为是由 deviceID、bundleId 和安装时的相关信息生成的,App 的升级操作 deviceToken 不变,卸载重装 App、恢复和重装操作系统后的 deviceToken 会发生变化。

    苹果的 APNs 服务是基于 deviceToken 实现的,因此需要将设备的 deviceToken 发送到我们的业务服务器中,用于后续的消息推送。一个设备可能登录过多个用户,一个用户也可能在多个设备中登录过,当我们需要给不同用户推送不同的消息时,除了 deviceToken 之外,我们还需要保存用户的 openid 与 deviceToken 的映射关系。我们可以在用户登录成功后的时机更新 openid 和 deviceToken 的映射关系,用户退出后取消映射关系,只保存用户最后登录设备的 deviceToken,避免一个设备收到多个重复通知和一个用户在不同设备收到多个通知等情况。

    在新闻类 App 出现事实热点新闻时,后台服务就可以携带消息内容和 deviceToken 等内容,向苹果的 APNs 服务发起消息推送请求,推送消息的实现是异步的,只要请求格式和 deviceToken 检查通过APNs服务就不会报错,但是用户还是可能因为网络异常或者关闭了推送权限等原因收不到推送消息。

    APNs 服务向用户设备推送消息这一步也是异步的,在用户关机或网络异常收不到推送的情况下,APNs 会为每个 deviceToken 保留最后一条推送消息,待网络恢复后再次推送。

    1. 获取设备deviceToken

    在 App 启动时,我们可以通过 UIApplication的registerForRemoteNotifications 方法向苹果的 APNS 服务器请求 deviceToken。

    如果请求成功,则 didRegisterForRemoteNotificationsWithDeviceToken 回调方法会被执行,为了便于业务服务器的调用,我们一般会将二进制的 deviceToken 转换为 16 进制的字符串后再进行存储。

    如果请求失败,则 didFailToRegisterForRemoteNotificationsWithError 方法也会被调用,并附带具体的错误信息。相关代码如下:

    2. 后台调用APNs推送

    业务方服务器调用 APNs 服务时首先要建立安全连接,进行开发者身份的认证,分为基于证书(Certificate-Based)和基于Token(Token-Based)的认证两种方式,比较常用的是基于证书的认证方式。

    推送证书分为开发环境和生产环境的证书,分别对应不同的 APNs 推送接口,我们从苹果开发者平台或者第三方平台导出的推送证书一般有 p12 和 pem 两种格式的文件,为了便于接口调用我们可以通过以下命令将 p12 格式的文件转换为 pem 证书。

    基于证书建立 TLS 连接的流程如下图所示:

    80a444014d56dd2e64eed0f8802420d5.png

    业务方服务器(Provider)向APNs服务器发起建立TLS连接的请求。

    APNs服务器返回的它的证书,供业务方服务器校验。

    业务方服务器提供自己的推送证书,供APNs服务器校验。

    APNs服务器验证业务方服务器提供的推送证书无误后,TLS连接就已经建立完成,之后业务方服务器就可以直接向APNs发送消息推送请求了。

    业务方与 APNs 建立请求的简易实现的 PHP 代码实现如下:

    业务方服务器通过证书与 APNs 建立安全连接后可以进行连续多次的消息推送操作,每次消息推送都要指定 deviceToken 和 Payload 参数。

    Payload 是一个 json 对象,用于配置 iOS 在收到远程消息推送时的展现形式,aps 参数包含了苹果预设的 alert、sound、badge 等参数,其中 alert 参数可以是字符串,或者包含 title、body 等参数的字典类型;badge 参数使用整形设置 App 图标右上角显示的数字,badge 设置为 0 时角标不会显示;sound 参数用于设置推送的声音,不传该参数或者传递空字符串则推送不会发出提示音,设置为 default 时使用系统默认提示音,也可以设置为具体的音频文件名,需要提前音频文件放到项目的 bundle 目录,且时长不能超过 30s。

    除了预设参数以外,我们还可以在 aps 的同级自定义一些参数,这些参数也可以是字典类型,再嵌套其他参数,例如示例代码中我们自定义的 userInfo 对象,但是一般推送消息的 payload 不宜过大,应控制在 4K 以内,建议只透传一些 id 和 url 等关键参数,具体的内容由客户端在收到推送时再去通过网络请求获取。

    上述 payload 包含了常见的推送消息的标题、副标题、内容、消息提示音、App 的角标数字等预设参数,以及一个开发者自定义的 gameID 参数。用户点击推送消息后会自动启动或从后台唤醒 App,我们可以在系统的回调方法中获取到自定义参数,并根据 gameID 自动为用户打开该游戏页面。

    3. 消息推送调试工具

    在进行 APNs 接口调试时,我们可以利用一些优秀的推送调试工具帮助我们验证 payload 或证书等内容的合法性。本文介绍两款比较流行的开源软件,分别是国外的 Knuff 和国内开发者维护的 smartPush。

    40d86f7ab6fdd71d16f863bf1ffabd1d.png

    Knuff:https://github.com/KnuffApp/Knuff

    SmartPush:https://github.com/shaojiankui/SmartPush

    六、App推送消息的处理

    在 iOS10 中,UserNotifications 框架为开发者提供了 UNUserNotificationCenterDelegate 协议,开发者可以通过实现协议中的方法,在 App 接收到推送消息和用户点击推送消息时进行一些业务逻辑的处理。

    无论是本地推送还是远程推送的消息,App的运行状态都可能处于以下三种状态:

    App 正在前台运行,此时用户正在使用 App,收到推送消息时默认不会弹出消息提示框,willPresentNotification 回调方法会被调用,开发者可以从 UNNotification 对象中获取该推送消息的 payload 内容,进而获取自定义参数,然后显示一个自定义弹窗提示用户收到了新的消息;也可以在 willPresentNotification 方法中通过 completionHandler 函数的调用让推送消息直接在前台显示,用户点击前台显示的推送消息时,didReceiveNotificationResponse 回调方法也会被执行。

    App 在后台运行,此时用户点击推送消息会将 App 从后台唤醒,didReceiveNotificationResponse 回调方法会被执行,开发者可以在该方法中获得 payload,解析自定义参数并自动打开对应的页面。

    App 尚未启动,此时用户点击推送消息会打开 App,开发者可以从 launchOptions 中获取本地或远程推送消息中的自定义参数,待页面初始化完成后进行相关页面的跳转。

    在 iOS9 中,UIApplication 提供了下面三个消息推送的处理方法,分别是远程消息推送、远程静默推送和本地消息推送的回调处理方法。

    前两个回调方法都能够用于 App 远程消息推送的处理,同时使用时只有远程静默推送方法会被调用,当 payload 包含参数 content-available=1 时,该推送就是静默推送,静默推送不会显示任何推送消息,当 App 在后台挂起时,静默推送的回调方法会被执行,开发者有 30s 的时间内在该回调方法中处理一些业务逻辑,并在处理完成后调用 fetchCompletionHandler。

    UIApplication 中的这三个方法在以下两种场景下都会被调用:

    App 在前台运行时收到通知;

    App 在后台运行时用户点击推送消息拉起 App。

    区别是前两种方法对应远程消息推送的接收和点击触发响应,didReceiveLocalNotification 用于本地消息推送。我们可以通过 UIApplication的applicationState 属性来判断 App 是否在前台运行,然后分别实现:

    用户点击消息唤起后台App并打开对应页面;

    用户前台使用App时显示自定义弹窗。

    七、结语

    本文首先介绍了消息推送相关的工程配置和推送权限的申请,然后分别介绍了本地和远程消息推送的不同使用场景和实现方法,最后介绍了 App 在收到推送消息后的相关回调方法和处理逻辑。

    在实际的项目开发中,我们往往会选择腾讯云推送或极光推送等更加成熟的第三方消息推送平台,这些平台都提供了相对完善的推送和数据统计服务,通过接口和 SDK 屏蔽了底层逻辑的实现,通过对 iOS 消息推送的实现过程的了解也能够帮助我们更好的使用这些平台。

    由于时间的关系,自己的研究并不深入,如有疏漏和错误,欢迎留言指正交流~

    参考资料:

    [1] 腾讯云的TPNS文档:

    https://cloud.tencent.com/document/product/548/36664#.E6.AD.A5.E9.AA.A43.EF.BC.9A.E4.B8.8A.E4.BC.A0.E8.AF.81.E4.B9.A6.E5.88.B0-tpns-.E6.8E.A7.E5.88.B6.E5.8F.B0

    [2] 苹果官方技术文档:

    https://developer.apple.com/documentation/usernotifications

    [3] 史上最全iOS Push技术详解:

    https://cloud.tencent.com/developer/article/1198303

    [4] iOS远程推送-APNs详解:

    https://juejin.im/post/6844903893592178696

    [5] iOS静默推送进阶知识:

    https://www.jianshu.com/p/c211bd295d58

    [6] iOS10自定义通知UI:

    https://www.jianshu.com/p/85ac47bdf387

    [7] 信鸽文档-推送服务介绍:

    https://xg.qq.com/docs/ios_access/ios_push_introduction.html

    [8] 浅谈iOS和Android后台实时消息推送的原理和区别:

    https://cloud.tencent.com/developer/article/1150967

    [9] 浅谈基于HTTP2推送消息到APNs:

    [10] PHP基于socket的ios

    推送的实现:

    https://www.fzb.me/2015-9-7-sockect-implement-for-apns.html

    [11] 如何构建一套高可用的移动消息推送平台:

    https://www.infoq.cn/article/HA-mobile-message-push-platform

    展开全文
  • WebSocket服务端消息推送

    千次阅读 2021-09-17 23:50:47
    股票曲线实时变化,在线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实现消息推送

    展开全文
  • 1.Android端进程被杀死后,目前自带的保护后台接收消息活跃机制。暂时没有什么好的机制保持任何情况下都活跃文章参考:http://blog.csdn.net/marswin89/article/details/50917098android原生系统用home键杀进程可以...
  • java实现后台服务器消息推送

    千次阅读 2021-02-12 10:54:11
    } //接收到消息的回调方法 websocket.onmessage = function(event) { alert("接收到消息的回调方法") alert("这是后台推送消息:"+event.data); websocket.close(); alert("webSocket已关闭!") } //连接关闭...
  • 大家手机里面或多或少会下载...如下图所示:图片发自简书App图片发自简书App此时,你会看到你手机下载的全部APP,想要屏蔽掉哪个APP的推送消息就点击进去,然后再关闭“允许通知”。这里以360浏览器为例。如下图:图片...
  • 如何进行app消息推送(push)?

    千次阅读 2021-06-07 10:59:12
    1 消息推送消息推送(push),是指运营人员通过自己产品后台或第三方工具对用户移动设备进行的主动消息推送,是厂商主动触达用户的通道。通过消息推送,目标用户可以在移动设备通知和状态栏看到消息通知,唤起用户点击...
  • Android端进程被杀死后,目前自带的保护后台接收消息活跃机制。暂时没有什么好的机制保持任何情况下都活跃 文章参考:http://blog.csdn.net/marswin89/article/details/50917098 android原生系统用home键杀进程可以...
  • 消息推送系统的设计

    千次阅读 2016-12-31 21:52:27
    一、消息推送系统设计需求 1、高性价比,在有限的硬件资源下,尽可能的提高消息系统的性能和可用性。 2、提高数据的一致性。 二、分析 消息推送,按数据量划分,包括两类: 1)持续的大量数据...
  • iPhone XS ios12系统隐式推送关闭方法:首先进入到桌面的设置选项并一直向下滑动屏幕;然后找到我们已经设置过饮食推送的app,点击进入;接着我们会发现在通知这个内容上面,就会显示着隐式推送;最后点击关闭即可。...
  • MQTT消息推送

    千次阅读 2021-12-25 13:25:39
    MQTT消息推送MQTT简介MQTT特性MQTT协议原理环境搭建MQTT协议数据包结构MQTT数据安全 MQTT简介 MQTT: Message Queuing Telemetry Transport,消息队列遥测传输协议),是一种基于发布/订阅(publish/subscribe)模式...
  • uniapp应用消息推送的实现(以安卓APP为例)
  • 消息推送系统设计思路

    万次阅读 2018-01-06 12:54:40
    一、消息推送系统设计需求 1、高性价比,在有限的硬件资源下,尽可能的提高消息系统的性能和可用性。 2、提高数据的一致性。 二、分析 消息推送,按数据量划分,包括两类: 1)持续的大量数据...
  • 最近有部分安卓用户(尤其华为),反馈老师的推送消息收取不到,或不及时,或部分能收到。经过技术人员的排查,总结了一下推送设置的经验。需要2个基本设置:1. 手机允许接受爱股票的通知;确定位置:设置-应用程序...
  • 本文为大家分享个推消息推送SDK【通知栏铃声】功能的使用窍门,帮助开发者用简单10行代码,即可以声传意,轻松收获用户喜爱。 一、功能介绍 众所周知,消息推送是App和用户之间交互的桥梁。App通过通知栏消息,...
  • 在好几年前,就已经注意到DDPush这款推送中间件,不过看近来发展也还是停留在V1.0...DDPush 任意门 消息推送 DDPush是什么 DDPush可以做什么 移动互联网消息推送 IM实时消息系统核心组件 物联网设备控制与交互 ...
  • 前段时间开发app的时候要开始做消息推送功能了,刚开始选择的是个推,最后由于。。。。。,em还是用了uniapp自带的uniPush,其实uniapp的推送就是集成版的个推,下面讲解详细的步骤 这里是官方的文档地址:uniPush...
  • 引言: 在互联网高速发展的时代里,web应用大有取代桌面应用的趋势...收发邮件提醒,在线IM聊天,自动化办公提示等等,web系统里总是能见到消息推送的应用。消息推送用好了能增强用户体验,用不好则会起相反的效果。...
  • 最近在完善毕设的路上,由于是设计一个远程控制物联网系统,所以服务端到硬件我选用了MQTT协议。因为MQTT的发布/订阅模式很适合这种场景。接下来就来聊聊遇到的一些坑吧。 小前奏 既然是基于MQTT协议的,那么前端...
  • 友盟+基于卓越的数据技术与算法能力,于...友盟+推出《2021 年度APP消息推送白皮书》,该白皮书从送达通道、用户送达偏好、分行业送达率等多个角度解读,带您了解行业推送现状的同时,有效提升APP的消息推送送达率。
  • Golang/Gin-WebSocket实现实时消息推送

    千次阅读 2021-09-10 21:39:40
    Golang/Gin-WebSocket实现实时消息推送前言gorilla/websocket基础用法实现实时消息推送代码api补充说明 前言 WebSocket在 HTML5 游戏和网页消息推送都使用比较多。 WebSocket 是 HTML5 的重要特性,它实现了基于...
  • 说Android端外推送比较烦,实际有两层意思:首先是说实现上比较麻烦,至今业界也没有找到一种完美的解决方案,Android程序员通常需要同时集成多家推送平台(如果有自己的端内推送,还要考虑与端内推送的配合);...
  • 首先使用SingleChildScrollView作为根组件让其可以滚动,然后获取键盘高度MediaQuery.of(context).viewInsets.bottom作为padding的bottom,因为这不在scaffold工作范围内,flutter不会为我们上界面。 状态更新 ...
  • 手机消息推送方案综述

    千次阅读 2021-10-22 19:48:34
    本文要分享的是消息推送是指手机APP被关闭或者处于后台时,还能收到消息的能力。这种消息已经广泛应用在以下场景。 IM即时通信应用,比如微信切后台了依然能收到消息。 新闻资讯应用, 安防APP的报警应用,比如...
  • 1.Android端进程被杀死后,目前自带的保护后台接收消息活跃机制。暂时没有什么好的机制保持任何情况下都活跃android原生...这个时候推送是收不到的如果是原生系统,没有root,只要不是强行停止,其他系统自带的停止...
  • java 消息推送的几种方式比较

    千次阅读 2021-01-08 14:37:44
    收发邮件提醒,在线IM聊天,自动化办公提示等等,web系统里总是能见到消息推送的应用。消息推送用好了能增强用户体验,用不好则会起相反的效果。在司空见惯的使用过程中,有没有对其中的原理产生兴趣呢?实现消息推...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 77,713
精华内容 31,085
关键字:

如何关闭系统推送消息