推送通知服务

概述

  Windows Phone 中的Microsoft Push Notification Service向第三方开发人员提供了一个弹性,专注,而且持续的渠道,使得开发人员可以从web service向移动应用程序发送信息和更新。

  过去移动应用程序需要经常主动去调查其相应的Web服务,以了解是否有任何等待处理的通知。这样做虽然有效,但是会导致手机的无线设备频繁打开,从而对电池续航时间带来负面影响. 使用推送通知的方式取代主动调查,web service能够提醒应用程序获取所需要的重要更新。

 

                       1  推送 Notifications

  当一个Web service信息发送到应用程序,它发送一个通知Push Notification Service该服务随后将通知路由到应用程序。根据推送通知的格式和装载量,信息作为原始数据传递到应用程序,应用程序的标题明显地更新或显示一个Toast通知。然后如果需要的话应用程序可以使用自己的协议联系web service以获取更新。

   Push Notification Service 在推送通知发送后向你的 web service 发送一个回复码.然而, Push Notification Service 不能为你的推送提醒是否成功传递到应用程序提供端到端的确认.了解更多信息,请参考 Push Notification Service Response Codes for Windows Phone


推送消息过程:

 

          图 2

  1. WP设备到MSNS注册PN服务,并得到唯一的服务URI
  2. WP设备把服务URI传递给Cloud服务,并注册
  3. 当有更新消息发送时,Cloud服务往MSNS服务发送更新消息
  4. MSNS把更新消息发送到WP设备
  5. 需要时WP设备往Cloud服务读取更多的数据

 

推送通知服务类型:

  • Raw Notification
    • 可以发送任何格式的数据
    • 应用程序可以根据需要加工数据
    • 应用程序相关(Application-specific)的通知消息
    • 只有在应用程序运行时,才发送
  • Toast Notification
    • 发送的数据为指定的XML格式
    • 如果应用程序正在运行,内容发送到应用程序中
    • 如果应用程序不在运行,弹出Toast消息框显示消息
      • App图标加上2个文本描述
      • 打断用户当前操作,但这是临时的
      • 用户可以点击进行跟踪
  • Title Notification
    • 发送的数据为指定的XML格式
    • 不会往应用程序进行发送
    • 如果用户把应用程序pin to start,那么更新数据发送到start screen的titile里
      • 包含三个属性,背景,标题和计算器
      • 每个属性都有固定的格式与位置
      • 可以使用其中的属性,不一定三个属性一起使用

示例1:(摘自Webcast)

首先建立服务端Windows窗体程序,相当于图2中的Your Cloud Application,界面如下

 

 加入“发送”按钮事件:

ExpandedBlockStart.gif View Code
private   void  button1_Click( object  sender, EventArgs e)
        {
            
string  msg  =  String.Format( " {0}{1}, {2}度 " , LocationComboBox.Text,
                    WeatherComboBox.Text, TemperatureTextBox.Text);
            
string  type  =  NotificationTypeComboBox.Text  as   string ;
            
if  (type  ==   " Raw " )
            {
                
byte [] strBytes  =   new  UTF8Encoding().GetBytes(msg);
                SendRawNotification(strBytes);
            }
            
else   if  (type  ==   " Toast " )
            {
                
string  toastMessage  =   " <?xml version=\"1.0\" encoding=\"utf-8\"?> "   +
                        
" <wp:Notification xmlns:wp=\"WPNotification\"> "   +
                           
" <wp:Toast> "   +
                              
" <wp:Text1>天气更新</wp:Text1> "   +
                              
" <wp:Text2> "   +  msg  +   " </wp:Text2> "   +
                           
" </wp:Toast> "   +
                        
" </wp:Notification> " ;
                
byte [] strBytes  =   new  UTF8Encoding().GetBytes(toastMessage);
                SendToastNotification(strBytes);
            }
            
else   if  (type  ==   " Tile " )
            {
                
string  tileMessage  =   " <?xml version=\"1.0\" encoding=\"utf-8\"?> "   +
                    
" <wp:Notification xmlns:wp=\"WPNotification\"> "   +
                       
" <wp:Tile> "   +
                          
" <wp:BackgroundImage>/Images/ "   +  WeatherComboBox.Text  +   " .png</wp:BackgroundImage> "   +
                          
" <wp:Count> "   +  TemperatureTextBox.Text  +   " </wp:Count> "   +
                          
" <wp:Title> "   +  LocationComboBox.Text  +   " </wp:Title> "   +
                       
" </wp:Tile>  "   +
                    
" </wp:Notification> " ;
                
byte [] strBytes  =   new  UTF8Encoding().GetBytes(tileMessage);
                SendTileNotification(strBytes);
            }
        }

加入Raw Notification的方法:

ExpandedBlockStart.gif View Code
private   void  SendRawNotification( byte [] Payload)
        {
            
//  The URI that the Push Notification Service returns to the Push Client when creating a notification channel.
            HttpWebRequest sendNotificationRequest  =  (HttpWebRequest)WebRequest.Create(NotificationUriTextBox.Text);

            
//  HTTP POST is the only allowed method to send the notification.
            sendNotificationRequest.Method  =  WebRequestMethods.Http.Post;

            
//  The optional custom header X-MessageID uniquely identifies a notification message. If it is present, the 
            
//  same value is returned in the notification response. It must be a string that contains a UUID.
            sendNotificationRequest.Headers[ " X-MessageID " =  Guid.NewGuid().ToString();

            
//  Sets raw notification
            sendNotificationRequest.ContentType  =   " text/xml; charset=utf-8 " ;
            sendNotificationRequest.Headers.Add(
" X-NotificationClass " " 3 " );
            
//  Possible batching interval values:
            
//  3: The message is delivered by the Push Notification Service immediately.
            
//  13: The message is delivered by the Push Notification Service within 450 seconds.
            
//  23: The message is delivered by the Push Notification Service within 900 seconds.

            
//  Sets the web request content length.
            sendNotificationRequest.ContentLength  =  Payload.Length;

            
//  Sets the notification payload to send.
             byte [] notificationMessage  =  Payload;

            
//  Sends the notification.
             using  (Stream requestStream  =  sendNotificationRequest.GetRequestStream())
            {
                requestStream.Write(notificationMessage, 
0 , notificationMessage.Length);
            }

            
//  Gets the response.
            HttpWebResponse response  =  (HttpWebResponse)sendNotificationRequest.GetResponse();
            
string  notificationStatus  =  response.Headers[ " X-NotificationStatus " ];
            
string  notificationChannelStatus  =  response.Headers[ " X-SubscriptionStatus " ];
            
string  deviceConnectionStatus  =  response.Headers[ " X-DeviceConnectionStatus " ];
            MsgLabel.Text 
=  String.Format( " 通知状态:{0},管道状态:{1},设备状态:{2} " ,
                notificationStatus, notificationChannelStatus, deviceConnectionStatus);
        }

加入Toast Notification类型的方法:

ExpandedBlockStart.gif View Code
private   void  SendToastNotification( byte [] Payload)
        {
            
//  The URI that the Push Notification Service returns to the Push Client when creating a notification channel.
            HttpWebRequest sendNotificationRequest  =  (HttpWebRequest)WebRequest.Create(NotificationUriTextBox.Text);

            
//  HTTP POST is the only allowed method to send the notification.
            sendNotificationRequest.Method  =  WebRequestMethods.Http.Post;

            
//  The optional custom header X-MessageID uniquely identifies a notification message. If it is present, the 
            
//  same value is returned in the notification response. It must be a string that contains a UUID.
            sendNotificationRequest.Headers[ " X-MessageID " =  Guid.NewGuid().ToString();

            
//  Sets toast notification
            sendNotificationRequest.ContentType  =   " text/xml; charset=utf-8 " ;
            sendNotificationRequest.Headers.Add(
" X-WindowsPhone-Target " " toast " );
            sendNotificationRequest.Headers.Add(
" X-NotificationClass " " 2 " );
            
//  Possible batching interval values:
            
//  2: The message is delivered by the Push Notification Service immediately.
            
//  12: The message is delivered by the Push Notification Service within 450 seconds.
            
//  22: The message is delivered by the Push Notification Service within 900 seconds.

            
//  Sets the web request content length.
            sendNotificationRequest.ContentLength  =  Payload.Length;

            
//  Sets the notification payload to send.
             byte [] notificationMessage  =  Payload;

            
//  Sends the notification.
             using  (Stream requestStream  =  sendNotificationRequest.GetRequestStream())
            {
                requestStream.Write(notificationMessage, 
0 , notificationMessage.Length);
            }

            
//  Gets the response.
            HttpWebResponse response  =  (HttpWebResponse)sendNotificationRequest.GetResponse();
            
string  notificationStatus  =  response.Headers[ " X-NotificationStatus " ];
            
string  notificationChannelStatus  =  response.Headers[ " X-SubscriptionStatus " ];
            
string  deviceConnectionStatus  =  response.Headers[ " X-DeviceConnectionStatus " ];
            MsgLabel.Text 
=  String.Format( " 通知状态:{0},管道状态:{1},设备状态:{2} " ,
                notificationStatus, notificationChannelStatus, deviceConnectionStatus);
        }

 加入Tile Notification类型的方法:

ExpandedBlockStart.gif View Code
private   void  SendTileNotification( byte [] Payload)
        {
            
//  The URI that the Push Notification Service returns to the Push Client when creating a notification channel.
            HttpWebRequest sendNotificationRequest  =  (HttpWebRequest)WebRequest.Create(NotificationUriTextBox.Text);

            
//  HTTP POST is the only allowed method to send the notification.
            sendNotificationRequest.Method  =  WebRequestMethods.Http.Post;

            
//  The optional custom header X-MessageID uniquely identifies a notification message. If it is present, the 
            
//  same value is returned in the notification response. It must be a string that contains a UUID.
            sendNotificationRequest.Headers[ " X-MessageID " =  Guid.NewGuid().ToString();

            
//  Sets toast notification
            sendNotificationRequest.ContentType  =   " text/xml; charset=utf-8 " ;
            sendNotificationRequest.Headers.Add(
" X-WindowsPhone-Target " " token " );
            sendNotificationRequest.Headers.Add(
" X-NotificationClass " " 1 " );
            
//  Possible batching interval values:
            
//  1: The message is delivered by the Push Notification Service immediately.
            
//  11: The message is delivered by the Push Notification Service within 450 seconds.
            
//  21: The message is delivered by the Push Notification Service within 900 seconds.

            
//  Sets the web request content length.
            sendNotificationRequest.ContentLength  =  Payload.Length;

            
//  Sets the notification payload to send.
             byte [] notificationMessage  =  Payload;

            
//  Sends the notification.
             using  (Stream requestStream  =  sendNotificationRequest.GetRequestStream())
            {
                requestStream.Write(notificationMessage, 
0 , notificationMessage.Length);
            }

            
//  Gets the response.
            HttpWebResponse response  =  (HttpWebResponse)sendNotificationRequest.GetResponse();
            
string  notificationStatus  =  response.Headers[ " X-NotificationStatus " ];
            
string  notificationChannelStatus  =  response.Headers[ " X-SubscriptionStatus " ];
            
string  deviceConnectionStatus  =  response.Headers[ " X-DeviceConnectionStatus " ];
            MsgLabel.Text 
=  String.Format( " 通知状态:{0},管道状态:{1},设备状态:{2} " ,
                notificationStatus, notificationChannelStatus, deviceConnectionStatus);
        }

 

 

新建Windows Phone Application,界面如下图所示:

 

在MainPage.xaml.cs类中添加字段代码

ExpandedBlockStart.gif View Code
private  HttpNotificationChannel httpChannel;
        
private   const   string  channelName  =   " Channel1 " ;

添加 “连接”按钮事件代码

ExpandedBlockStart.gif View Code
private   void  button1_Click( object  sender, RoutedEventArgs e)
        {
            httpChannel 
=  HttpNotificationChannel.Find(channelName);
            
// Delete the Channel if exists
             if  (httpChannel  !=   null )
            {
                httpChannel.Close();

                httpChannel.Dispose();
            }
            
// Create the channel
            httpChannel  =   new  HttpNotificationChannel(channelName,  " NotificationService " );

            
// Register to UriUpdated event - occurs when channel successfully opens
            
// WP设备到MSNS注册PN服务,并得到唯一的服务URI 
            httpChannel.ChannelUriUpdated  +=   new  EventHandler < NotificationChannelUriEventArgs > (httpChannel_ChannelUriUpdated);

            
// general error handling for push channel
            httpChannel.ErrorOccurred  +=   new  EventHandler < NotificationChannelErrorEventArgs > (httpChannel_ErrorOccurred);

            
// Subscribed to Raw Notification
            httpChannel.HttpNotificationReceived  +=   new  EventHandler < HttpNotificationEventArgs > (httpChannel_HttpNotificationReceived);

            
// subscrive to toast notification when running app
            
// 程序运行时收到消息
            httpChannel.ShellToastNotificationReceived  +=   new  EventHandler < NotificationEventArgs > (httpChannel_ShellToastNotificationReceived);
            
            
// Open the channel
            httpChannel.Open();

            
// subscrive to toast notification
            
// 程序没有运行时,消息会在WP设备顶部弹出显示
            httpChannel.BindToShellToast();

            
// subscrive to tile notification            
            httpChannel.BindToShellTile();
        }

添加事件代码

ExpandedBlockStart.gif View Code
void  httpChannel_ShellToastNotificationReceived( object  sender, NotificationEventArgs e)
        {
            
string  msg  =   "" ;
            
foreach  (var key  in  e.Collection.Keys)
            {
                msg 
+=  key  +   " "   +  e.Collection[key]  +  Environment.NewLine;
            }
            Dispatcher.BeginInvoke(() 
=>
            {
                MsgTextBlock.Text 
=  msg;
            });
        }

        
void  httpChannel_HttpNotificationReceived( object  sender, HttpNotificationEventArgs e)
        {
            
using  (var reader  =   new  StreamReader(e.Notification.Body))
            {
                
string  msg  =  reader.ReadToEnd();
                Dispatcher.BeginInvoke(() 
=>
                {
                    MsgTextBlock.Text 
=  msg;
                });
            }
        }

        
void  httpChannel_ErrorOccurred( object  sender, NotificationChannelErrorEventArgs e)
        {
            Dispatcher.BeginInvoke(() 
=>
            {
                MsgTextBlock.Text 
=  e.Message;
            });
        }

        
void  httpChannel_ChannelUriUpdated( object  sender, NotificationChannelUriEventArgs e)
        {
            Debug.WriteLine(
" ChannelUri: {0} " , e.ChannelUri);
        }


示例1代码 


示例2:

本示例演示了Toast Notification的使用。示例包含3个项目:

1.WCF服务应用程序,模拟Cloud Application

ExpandedBlockStart.gif IService1.cs
using  System;
using  System.Collections.Generic;
using  System.Linq;
using  System.Runtime.Serialization;
using  System.ServiceModel;
using  System.ServiceModel.Web;
using  System.Text;

namespace  WcfService1
{
    [ServiceContract]
    
public   interface  IService1
    {
        [OperationContract]
        
void  Subscribe( string  uri);

        [OperationContract]
        
void  SendToast( string  title,  string  message);
    }

}
ExpandedBlockStart.gif Service1.svc.cs
using  System;
using  System.Collections.Generic;
using  System.Linq;
using  System.Runtime.Serialization;
using  System.ServiceModel;
using  System.ServiceModel.Web;
using  System.Text;

using  System.Net;

namespace  WcfService1
{
    
public   class  Service1 : IService1
    {
        
private   static  Uri uri;

        
public   void  Subscribe( string  subscriberUri)
        {
            uri 
=   new  Uri(subscriberUri);
        }

        
public   void  SendToast( string  title,  string  message)
        {
            
string  toastMessage  =   " <?xml version=\"1.0\" encoding=\"utf-8\"?> "   +
                
" <wp:Notification xmlns:wp=\"WPNotification\"> "   +
                   
" <wp:Toast> "   +
                      
" <wp:Text1> "   +  title  +   " </wp:Text1> "   +
                      
" <wp:Text2> "   +  message  +   " </wp:Text2> "   +
                   
" </wp:Toast> "   +
                
" </wp:Notification> " ;

            
byte [] messageBytes  =  System.Text.Encoding.UTF8.GetBytes(toastMessage);
            SendMessage(uri, messageBytes);
        }


        
private   static   void  SendMessage(Uri uri,  byte [] message)
        {
            HttpWebRequest request 
=  (HttpWebRequest)WebRequest.Create(uri);
            request.Method 
=  WebRequestMethods.Http.Post;
            request.ContentType 
=   " text/xml " ;
            request.ContentLength 
=  message.Length;

            request.Headers.Add(
" X-MessageID " , Guid.NewGuid().ToString());

            request.Headers[
" X-WindowsPhone-Target " =   " toast " ;
            request.Headers.Add(
" X-NotificationClass " " 2 " );

            var requestStream 
=  request.GetRequestStream();
            requestStream.Write(message, 
0 , message.Length);
        }
    }


}

2.WPF应用程序,消息推送触发程序

ExpandedBlockStart.gif MainWindow.xaml
< Window x:Class = " WpfApplication1.MainWindow "
        xmlns
= " http://schemas.microsoft.com/winfx/2006/xaml/presentation "
        xmlns:x
= " http://schemas.microsoft.com/winfx/2006/xaml "
        Title
= " MainWindow "  Height = " 350 "  Width = " 525 " >
    
< Grid >
        
< Button Content = " Button "  Height = " 23 "  HorizontalAlignment = " Left "  Margin = " 218,101,0,0 "  Name = " button1 "  VerticalAlignment = " Top "  Width = " 75 "  Click = " button1_Click "   />
    
</ Grid >
</ Window >
ExpandedBlockStart.gif MainWindow.xaml.cs
using  System;
using  System.Collections.Generic;
using  System.Linq;
using  System.Text;
using  System.Windows;
using  System.Windows.Controls;
using  System.Windows.Data;
using  System.Windows.Documents;
using  System.Windows.Input;
using  System.Windows.Media;
using  System.Windows.Media.Imaging;
using  System.Windows.Navigation;
using  System.Windows.Shapes;

namespace  WpfApplication1
{
    
///   <summary>
    
///  Interaction logic for MainWindow.xaml
    
///   </summary>
     public   partial   class  MainWindow : Window
    {
        
public  MainWindow()
        {
            InitializeComponent();
        }

        
private   void  button1_Click( object  sender, RoutedEventArgs e)
        {
            ServiceReference1.Service1Client svc 
=   new  ServiceReference1.Service1Client();
            svc.SendToast(
" Check app " " new data has arrived " );

        }
    }
}
ExpandedBlockStart.gif app.config
<? xml version = " 1.0 "  encoding = " utf-8 "   ?>
< configuration >
    
< system.serviceModel >
        
< bindings >
            
< basicHttpBinding >
                
< binding name = " BasicHttpBinding_IService1 "  closeTimeout = " 00:01:00 "
                    openTimeout
= " 00:01:00 "  receiveTimeout = " 00:10:00 "  sendTimeout = " 00:01:00 "
                    allowCookies
= " false "  bypassProxyOnLocal = " false "  hostNameComparisonMode = " StrongWildcard "
                    maxBufferSize
= " 65536 "  maxBufferPoolSize = " 524288 "  maxReceivedMessageSize = " 65536 "
                    messageEncoding
= " Text "  textEncoding = " utf-8 "  transferMode = " Buffered "
                    useDefaultWebProxy
= " true " >
                    
< readerQuotas maxDepth = " 32 "  maxStringContentLength = " 8192 "  maxArrayLength = " 16384 "
                        maxBytesPerRead
= " 4096 "  maxNameTableCharCount = " 16384 "   />
                    
< security mode = " None " >
                        
< transport clientCredentialType = " None "  proxyCredentialType = " None "
                            realm
= ""   />
                        
< message clientCredentialType = " UserName "  algorithmSuite = " Default "   />
                    
</ security >
                
</ binding >
            
</ basicHttpBinding >
        
</ bindings >
        
< client >
            
< endpoint address = " http://localhost:10317/Service1.svc "  binding = " basicHttpBinding "
                bindingConfiguration
= " BasicHttpBinding_IService1 "  contract = " ServiceReference1.IService1 "
                name
= " BasicHttpBinding_IService1 "   />
        
</ client >
    
</ system.serviceModel >
</ configuration >

3.Windows Phone Application,手机接收程序

ExpandedBlockStart.gif MainPage.xaml
< phone:PhoneApplicationPage 
    x:Class
= " WindowsPhoneApplication1.MainPage "
    xmlns
= " http://schemas.microsoft.com/winfx/2006/xaml/presentation "
    xmlns:x
= " http://schemas.microsoft.com/winfx/2006/xaml "
    xmlns:phone
= " clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone "
    xmlns:shell
= " clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone "
    xmlns:d
= " http://schemas.microsoft.com/expression/blend/2008 "
    xmlns:mc
= " http://schemas.openxmlformats.org/markup-compatibility/2006 "
    FontFamily
= " {StaticResource PhoneFontFamilyNormal} "
    FontSize
= " {StaticResource PhoneFontSizeNormal} "
    Foreground
= " {StaticResource PhoneForegroundBrush} "
    SupportedOrientations
= " Portrait "  Orientation = " Portrait "
    mc:Ignorable
= " d "  d:DesignWidth = " 480 "  d:DesignHeight = " 768 "
    shell:SystemTray.IsVisible
= " True " >

    
<!-- LayoutRoot contains the root grid  where  all other page content  is  placed -->
    
< Grid x:Name = " LayoutRoot "  Background = " Transparent " >
        
< Grid.RowDefinitions >
            
< RowDefinition Height = " Auto " />
            
< RowDefinition Height = " * " />
        
</ Grid.RowDefinitions >

        
<!-- TitlePanel contains the name of the application and page title -->
        
< StackPanel x:Name = " TitlePanel "  Grid.Row = " 0 "  Margin = " 24,24,0,12 " >
            
< TextBlock x:Name = " ApplicationTitle "  Text = " MY APPLICATION "  Style = " {StaticResource PhoneTextNormalStyle} " />
            
< TextBlock x:Name = " PageTitle "  Text = " page name "  Margin = " -3,-8,0,0 "  Style = " {StaticResource PhoneTextTitle1Style} " />
        
</ StackPanel >

        
<!-- ContentPanel  -  place additional content here -->
        
< Grid x:Name = " ContentGrid "  Grid.Row = " 1 " >
            
< TextBlock Height = " 276 "  HorizontalAlignment = " Left "  Margin = " 48,108,0,0 "  Name = " textBlock1 "  Text = " TextBlock "  VerticalAlignment = " Top "  Width = " 350 "   />
        
</ Grid >
    
</ Grid >
</ phone:PhoneApplicationPage >
ExpandedBlockStart.gif MainPage.xaml.cs
using  System;
using  System.Windows;
using  Microsoft.Phone.Controls;
using  Microsoft.Phone.Notification;

namespace  WindowsPhoneApplication1
{
    
public   partial   class  MainPage : PhoneApplicationPage
    {
        
private  HttpNotificationChannel channel;
        
private   const   string  ChannelName  =   " MyChannel " ;

        
public  MainPage()
        {
            InitializeComponent();

            SetupNotificationChannel();
        }

        
private   void  SetupNotificationChannel()
        {
            channel 
=  HttpNotificationChannel.Find(ChannelName);

            
if  (channel  ==   null )
            {
                channel 
=   new  HttpNotificationChannel(ChannelName);
                channel.ChannelUriUpdated 
+=   new  EventHandler < NotificationChannelUriEventArgs > (channel_ChannelUriUpdated);
                channel.Open();
            }
            
else
            {
                RegisterForNotifications();
            }
        }

        
void  channel_ChannelUriUpdated( object  sender, NotificationChannelUriEventArgs e)
        {
            channel 
=  HttpNotificationChannel.Find(ChannelName);
            channel.BindToShellToast();
            RegisterForNotifications();
        }

        
private   void  RegisterForNotifications()
        {
            ServiceReference1.Service1Client svc 
=   new  ServiceReference1.Service1Client();
            svc.SubscribeAsync(channel.ChannelUri.ToString());

            channel.ShellToastNotificationReceived 
+=  (s, e)  =>  Deployment.Current.Dispatcher.BeginInvoke(()  =>  ToastReceived(e));
        }

        
private   void  ToastReceived(NotificationEventArgs e)
        {
            textBlock1.Text 
=   string .Format( " Title:  "   +  e.Collection[ " wp:Text1 " +   " \nMessage:  "   +  e.Collection[ " wp:Text2 " ]);
        }
    }

}

 

示例3:Windows Phone 7 SDK

微软提供的SDK示例,可以访问参考链接4 ,查看完整的示例代码。

 

参考链接:

 

2.Code Download 

 
3.MSDN Webcast - Silverlight for Windows Phone 开发系列课程(12):推送通知服务
 

 

 

转载于:https://www.cnblogs.com/dnso/articles/1961814.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值