【WP 8.1开发】推送通知测试服务端程序_.NET_编程开发_程序员俱乐部

中国优秀的程序员网站程序员频道CXYCLUB技术地图
热搜:
更多>>
 
您所在的位置: 程序员俱乐部 > 编程开发 > .NET > 【WP 8.1开发】推送通知测试服务端程序

【WP 8.1开发】推送通知测试服务端程序

 2014/8/10 23:17:41  东邪独孤  程序员俱乐部  我要评论(0)
  • 摘要:所谓推送通知,用老爷爷都能听懂的话说,就是:1、我的服务器将通知内容发送到微软的通知服务器,再由通知服务器帮我转发消息。2、那么,微软的推送服务器是如何知道我的服务器要发消息给哪台手机呢?手机客户端应用程序在创建推送通道时,微软的通知服务器会为手机分配一个URL,我的服务器只要知道这个URL就可以向指定的手机发送消息。所以,手机客户端必须通过网络把获取到的手机URL发给我的服务器,方法很多,如使用Socket、HTTP提交、Web服务、WCF等都可以。要测试推送通知,可以通过WP8
  • 标签:程序 服务端 测试 开发 服务

所谓推送通知,用老爷爷都能听懂的话说,就是:

1、我的服务器将通知内容发送到微软的通知服务器,再由通知服务器帮我转发消息。

2、那么,微软的推送服务器是如何知道我的服务器要发消息给哪台手机呢?手机客户端应用程序在创建推送通道时,微软的通知服务器会为手机分配一个URL,我的服务器只要知道这个URL就可以向指定的手机发送消息。所以,手机客户端必须通过网络把获取到的手机URL发给我的服务器,方法很多,如使用Socket、HTTP提交、Web服务、WCF等都可以。

 

要测试推送通知,可以通过WP 8.1的模拟器的模拟通知功能来实现,但是,这个模拟通知功能目前不太稳定,为啥不稳定呢?我发现可能与Hyper-V的虚拟交换机有关,如果人品好的时候,就可以顺利完成模拟通知;如果哪天人品值波动,就有可能出现错误

话又说回来,模拟终究是模拟,假的!如果不进行真实的推送测试,说不定你的应用给用户使用时发生意外情况,那就非同一般的痛苦了。对于手机用于接收通知的URL,在调试的时候,可以通过DeBug类输出,然后我们像厦大的教授写论文一样,直接按Ctrl + C,再在服务端Ctrl+V一下就行了。

调试归调试,在实际运作中,应用运行在用户的手机上,你不可能拿每个用户的爱机来debug一下,然后再取得URL的,显然这种做法只能在神话故事才可行,在人间是行不通的。因而我们在开发WP应用时,一定要注意通过网络把手机的通道URL发送到我们的服务器,有了URL才能向某手机发送消息。这就好像你得知道妹子的手机号码,才能向妹子发短信一样,当然,现在人们都用微信来找妹子了。

 为了能学会如何使用WP的推送通知,我们首先得有一个服务器,当然不是叫大家去买一台服务器,我们自己动手,写一个测试服务器就行了。

要实现推送,我们必须拥有开发者帐号,至于如何获得,呵呵,方法多着。你可以付款去购买一个,这样的开发者帐号限制较少,可以提交桌面应用;如果你是学生当然首选学生帐号;如果你既不是学生也不想花钱,也是可以的,微软做了一个WP App Studio,是web版的开发工具,不过大家莫笑,这个是给不会编程的人或小孩子玩的,说不得专业。但是,注凹App Studio是免费的,这是重点。至于如何注册,我相信各位都玩过微博、QQ人人网等东东了,不用我介绍了,无非是填资料,下一步,下一步,完成。如果你不打算在应用商店发布应用,而只是想玩一下,或学习一下,付款信息可以不填,不管它。

 

创建应用

有了开发者帐号后,我们要在商店中先创建一个应用,应用信息随便填就可以了。如下图,我创建了一个名为“示例应用”的牛X应用程序。

 

点击应用,进入应用描述页,点击“详细信息”,查看应用的详细信息。

向下滚动页面,找到“Windows推送通知(WNS)”,然后点击超链接进入。

 

之后会看到这一系列东东。

程序包SID:发送通知的服务器(即我的服务器)在申请Access Token时需要这个,做过微博API的调用的朋友肯定不会陌生,我们在调用微博API前必须得到授权,并换取一个AccessToken,然后我们用这个Token来调用API,相当于一个门卡,通过它可以进入旅店房间。SID除了在申请token时使用,还要把它写到WP应用的清单文件中,即“包名”,两者必须匹配才能允许发送推送消息。

应用程序标识:是应用程序清单文件(实质是XML)中的Identity节点,一定要与“仪表盘”中显示的一致。

客户端ID:完成推送暂时用不到它。

客户端密钥:我的服务器要申请Access Token时需要用到,即我们调用微博API时用到的Secret key,这个一般不要对外公开,开发者自己知道就可以了,不然别人也可以冒充你来发送通知了。

在以上各字段的数据中,要实现推送通知,我们要用到的有:SID、应用程序标识、客户端密钥这三个东东。

 

开发者身份验证

和微博API调用相似,我们要先验证自己的身份,获得一个access token,才能向指定的手机发送通知,以下是向WNS服务器发送的HTTPS请求的示例。

POST /accesstoken.srf HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Host: https://login.live.com
Content-Length: 211
 
grant_type=client_credentials&client_id={SID}&client_secret={客户端密钥}&scope=notify.windows.com

从中,我们看到该请求有以下几个特点:
1、使用HTTPS方案,地址为https://login.live.com/accesstoken.srf;

2、提交方式为POST;

3、内容格式为application/x-www-form-urlencoded;

4、POST的内容包括四个值:grant_type的值固定为client_credentials,这个不用讲,照抄就行;

注意client_id不是“客户端标识”,应该填上SID,不要填错了。client_secret填上“客户端密钥”。

最后那个scope字段也是固定值,照抄就行了,值为notify.windows.com。

发送POST验证成功后,会返回以下内容:

HTTP/1.1 200 OK   
Cache-Control: no-store
Content-Length: 422
Content-Type: application/json
 
{
    "access_token":"EgAcAQMAAAAALYAAY/c+Huwi3Fv4Ck10UrKNmtxRO6Njk2MgA=", 
    "token_type":"bearer"
}

返回的JSON中,access_token的值就是申请的access token的值了,我们就是用这个来传送通知的。

 

发送通知

在拿到token后,我们就可以用它来发送通知了。如:

POST https://db3.notify.windows.com/?token=AgUAAADCQmTg7OMlCg%2fK0K8rBPcBqHuy%2b1rTSNPMuIzF6BtvpRdT7DM4j%2fs%2bNNm8z5l1QKZMtyjByKW5uXqb9V7hIAeA3i8FoKR%2f49ZnGgyUkAhzix%2fuSuasL3jalk7562F4Bpw%3d HTTP/1.1
Authorization: Bearer EgAaAQMAAAAEgAAACoAAPzCGedIbQb9vRfPF2Lxy3K//QZB79mLTgK
X-WNS-RequestForStatus: true
X-WNS-Type: wns/toast
Content-Type: text/xml
Host: db3.notify.windows.com
Content-Length: 196

<toast launch="">
  <visual lang="en-US">
    <binding template="ToastImageAndText01">
      <image id="1" src="World" />
      <text id="1">Hello</text>
    </binding>
  </visual>
</toast>

上面所示的请求中,目标URL就是手机客户端注册的通过URL,这就是为什么客户端程序要把URL发送给服务器的原因,如果服务器不知道通道URL,就无法知晓要把消息发送给哪一台手机了。

 通知的内容都是以XML的形式表示的(RAW通知除外,RAW是自定义通知),关于这些XML模板,参考文档上有介绍,而且我们也可以通过API来得到这些模板,这个现在先不说,后面我们会提到。注意几个HTTP头。

Authorization——传递服务器所获取到的access token,格式为“Bearer <token>”,Bearer与token之间有空格。

X-WNS-Type——通知的类型。wns/toast表示发送Toast通知;wns/tile表示磁贴通知;wns/badge表示锁屏通知……

其他标头可以参考这里:http://msdn.microsoft.com/zh-cn/library/windows/apps/xaml/hh868245.aspx#pncodes_x_wns_type

 

开发测试服务器

现在,估计大家对推送通知的过程已有了解,趁热打铁、付诸实践是成为编程高手的重要因素,因此,为了成为编程高手,我们接下来马上开工,开发一个可以用于测试推送通的Win Forms程序。

1、以管理员身份运行VS 2013 Express for Desktop。我一直很喜欢Express版本,虽然我有MSDN订阅,但我还是用Express版,它比较简洁,但已经集成了VS的核心功能,用来做商业开发都没问题,最重要的是这家伙是免费的。偏偏有些垃圾公司喜欢装逼,开发个破产品还要弄个旗舰版,而且许多功能也用不上。为什么要以管理员身份运行呢?因为我计划用WCF服务来接收客户端APP发来的URL。

2、新建一个Windows窗体应用程序,我就不建WPF了,WinForm大家熟悉一点,用最成熟的方式有时候很爽。

3、我们首先完成的功能是如何根据不同通知生成XML模板。做过RT应用开发的朋友肯定会记得,在Windows.UI.Notifications命名空间下,使用ToastNotificationManager类或TileUpdateManager类,或BadgeUpdateManager类的GetTemplateContent方法,可以返回指定通知的XML模板。可是,我们这里建的是桌面应用,那有没有可能,在桌面应用中调用RT的API呢?告诉你,是可以的,但前题是你用的是Win 8.1系统。

来吧,咱们试试看。首先打开VS的“解决方案资源管理器”,选中刚才创建的桌面项目,右击鼠标,从快捷菜单中选择“卸载项目”,如下图。

 

同样,在项目节点上右击,从菜单中选择“编辑 <项目名>”。

 

这时候,就会用XML编辑器打开项目文件,找到第一个PropertyGroup节点,注意是第一个,不要找到其他去了,在第一个PropertyGroup节点下,加入一个TargetPlatformVersion节点,版本号为8.1。

    <TargetPlatformVersion>8.1</TargetPlatformVersion>

然后保存并关闭文件。重新加载项目,在添加引用对话框中,就会看到Windows 8.1的选项卡了。

但是,这里列出的“Windows”程序集不是WP8.1的,通过下面的浏览按钮,找到C:\Program Files (x86)\Windows Phone Kits\8.1\References\CommonConfiguration\Neutral目录下的Windows.winmd文件,这才是WP 8.1的运行时API所在的程序集。另外,为了能正常访问,还要添加以下程序集:

C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\Facades\System.Runtime.dll

C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\Facades\System.Runtime.InteropServices.dll

C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETCore\v4.5.1\System.Runtime.WindowsRuntime.dll

其中,System.Runtime.InteropServices.dll不一定要添加,但建议添加,因为如果不加,个别API无法附加事件处理程序。

现在,我们这个WinForm程序就可以访问WP8.1中的API了。

我正是要通过这种方法,把所有的通知模板的XML文档都读出来。

下面是部分代码:

        private Type enumType = null;

        void cmbNotifiType_SelectedIndexChanged(object sender, EventArgs e)
        {
            ComboBox cb = sender as ComboBox;
            if (cb.SelectedIndex == -1) return;

            string[] enumNames = null;
            // 判断通知类型
            switch (cb.SelectedIndex)
            {
                case 0: //Toast通知
                    enumType = typeof(ToastTemplateType);
                    break;
                case 1: //磁贴通知
                    enumType = typeof(TileTemplateType);
                    break;
                case 2: //锁屏通知
                    enumType = typeof(BadgeTemplateType);
                    break;
                case 3: //自定义通知
                    enumType = null; //
                    break;
            }
            enumNames = enumType == null ? null : Enum.GetNames(this.enumType);
            cmbTemplate.DataSource = enumNames;
        }

        void cmbTemplate_SelectedIndexChanged(object sender, EventArgs e)
        {
            if (cmbTemplate.SelectedIndex == -1)
            {
                return;
            }
            // 显示内容
            if (enumType == typeof(ToastTemplateType))
            {
                ToastTemplateType tem = (ToastTemplateType)Enum.Parse(enumType, cmbTemplate.SelectedItem as string);
                XmlDocument doc = ToastNotificationManager.GetTemplateContent(tem);
                txtContent.Text = doc.GetXml();
            }
            else if (enumType == typeof(TileTemplateType))
            {
                TileTemplateType tem = (TileTemplateType)Enum.Parse(enumType, cmbTemplate.SelectedItem as string);
                XmlDocument doc = TileUpdateManager.GetTemplateContent(tem);
                txtContent.Text = doc.GetXml();
            }
            else if (enumType == typeof(BadgeTemplateType))
            {
                BadgeTemplateType tem = (BadgeTemplateType)Enum.Parse(enumType, cmbTemplate.SelectedItem as string);
                XmlDocument doc = BadgeUpdateManager.GetTemplateContent(tem);
                txtContent.Text = doc.GetXml();
            }
            else
            {
                txtContent.Text = string.Empty;
            }
        }

通过Enum.GetNames方法可以把一个枚举类型在所有值的名字取出,以字符串数组的形式返回,使用这思路,我们可以将ToastTemplateType、TileTemplateType、BadgeTemplateType几个枚举的值的名称全部读出,显示在下拉列表框(ComboBox)中,当我们在界面上选择一个值时,又通过Enum.Parse方法从枚举值的名称生成枚举实例。最后可以通过GetTemplateContent方法来获取XML文档了。

这样做的好处在于,我们不用手动去准备XML文档。

 

4、打开Program.cs文件,在Program类中添加用于接收URL的代码。

    static class Program
    {
        /// <summary>
        /// 应用程序的主入口点。
        /// </summary>
        [STAThread]
        static void Main()
        {
            HttpListener listener = new HttpListener();
            listener.Prefixes.Add("http://+:85/svr/");
            try
            {
                listener.Start();
                listener.BeginGetContext(new AsyncCallback(OnGetAcceptCallback), listener);
            }
            catch { }

            /*  WinForm项目生成的代码  */

            try
            {
                listener.Stop();
            }
            catch { }
        }

        private static void OnGetAcceptCallback(IAsyncResult ar)
        {
            HttpListener listener = (HttpListener)ar.AsyncState;

            try
            {
                var context = listener.EndGetContext(ar);
                string url;
                using (var stream = context.Request.InputStream)
                {
                    long len = context.Request.ContentLength64;
                    byte[] buffer = new byte[len];
                    stream.Read(buffer, 0, buffer.Length);
                    url = System.Text.Encoding.UTF8.GetString(buffer);
                }
                if (OnGetUrl != null)
                {
                    OnGetUrl(url);
                }
            }
            catch { }

            try
            {
                listener.BeginGetContext(new AsyncCallback(OnGetAcceptCallback), listener);
            }
            catch { }
        }

        // 当收到WP客户端应用发来的URL时会触发该事件
        public static event Action<string> OnGetUrl;
    }

这里我选用HttpListener对象来监听HTTP请求,注意如果你用真实手机发送时,要配置一下防火墙,如果你嫌麻烦,就暂时把防火墙关了。地址http://+:85/svr/表示监听本机所有地址,如http://192.168.1.50:85/svr/,端口号是85,当然你可以根据实际情况自己改一下。

Program类中还定义了一个静态事件OnGetUrl,当接收到手机发来的URL后会引发这个事件,我们在主窗口中可以通过处理该事件来在用户界面上显示URL。如

            this.Load += (s1, a1) =>
            {
                Program.OnGetUrl += GetUrlService_OnUrlGot;
            };
            this.FormClosed += (s2, a2) =>
            {
                Program.OnGetUrl -= GetUrlService_OnUrlGot;
            };

            ……

        void GetUrlService_OnUrlGot(string url)
        {
            BeginInvoke(new Action(() =>
            {
                if (cmbURLs.FindString(url) < 0)
                {
                    cmbURLs.Items.Add(url);
                }
            }));
        }

当收到URL后,将URL放进一个ComboBox控件的下拉列表中。

 

5、下面要完成access token验证功能的实现。

            // 发起请求
            using (HttpClient client = new HttpClient())
            {
                // 准备要提交的数据
                IDictionary<string, string> formdata = new Dictionary<string, string>()
                {
                    { "grant_type", "client_credentials" }, /* 固定值 */
                    { "client_id", txtSID.Text.Trim() }, /* SID */
                    { "client_secret", txtSecret.Text.Trim() }, /* 客户端密钥,勿公开 */
                    { "scope", "notify.windows.com" } /* 固定值 */
                };
                FormUrlEncodedContent content = new FormUrlEncodedContent(formdata.AsEnumerable());
                // POST,并获取返回数据
                btnVertify.Enabled = false;
                HttpResponseMessage resp = await client.PostAsync("https://login.live.com/accesstoken.srf", content);
                btnVertify.Enabled = true;
                if (resp.StatusCode == HttpStatusCode.OK) //成功
                {
                    System.IO.Stream streamIn = await resp.Content.ReadAsStreamAsync();
                    //序列化,得到access token
                    DataContractJsonSerializer js = new DataContractJsonSerializer(typeof(OAuth2Data));
                    OAuth2Data odata = (OAuth2Data)js.ReadObject(streamIn);
                    streamIn.Close();
                    if (odata != null)
                    {
                        // 显示token
                        txtToken.Text = odata.AccessToken;
                    }
                }
                else
                {
                    StringBuilder sb = new StringBuilder();
                    foreach (var hd in resp.Headers)
                    {
                        sb.AppendLine(hd.Key + " : " + string.Join(",", hd.Value.ToArray()));
                    }
                    MessageBox.Show(string.Format("验证失败,错误码:{0}。\n{1}", (int)resp.StatusCode, sb.ToString()));
                }

前面说过,申请token需要向服务器POST格式为application/x-www-form-urlencoded的数据。上面代码中我用的是比较智能的HttpClient类,它可以很轻松地向服务器发送Content,此处应选用FormUrlEncodedContent类。注意这个类比较智能,它会自动帮我们做URL编码处理,因此我们放进去的内容是不需要手动编码的,如果将已编码的内容放进去,会导致重复编码,导致意外错误,token申请失败。
Access Token是以一个JSON对象的形式出现的。还记得吗,上文中我们说过的。如何处理服务器返回的JSON呢?最简单的方法是使用反序列化,直接把JSON对象反序列化为一个类实例就好了。

用来封装token的类定义如下。

    [DataContract]
    public class OAuth2Data
    {
        [DataMember(Name="access_token")]
        public string AccessToken { get; set; }
        [DataMember(Name = "token_type")]
        public string TokenType { get; set; }
    }

DataMember特性的Name所指定的值一定要与JSON数据的字段名匹配,否则反序列化时无法识别。

 

6、接下来完成发送通知的功能。

            string wns_type_header = ""; //推送类型
            string content_type = "text/xml"; //内容格式标头
            if (enumType == typeof(ToastTemplateType)) //toast
            {
                wns_type_header = "wns/toast";
            }
            else if (enumType == typeof(TileTemplateType)) //tile
            {
                wns_type_header = "wns/tile";
            }
            else if (enumType == typeof(BadgeTemplateType)) //badge
            {
                wns_type_header = "wns/badge";
            }
            else
            {
                wns_type_header = "wns/raw";
                content_type = "application/octet-stream";
            }
            // 验证标头
            string author_header = string.Format("Bearer {0}", txtToken.Text);

            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(cmbURLs.Text);
            // 添加必要标头
            request.ContentType = content_type;
            request.Headers.Add("X-WNS-Type", wns_type_header);
            request.Headers.Add("Authorization", author_header);
            // 添加可选标头
            if (ckbSuppressPop.Checked && enumType == typeof(ToastTemplateType))
            {
                request.Headers.Add("X-WNS-SuppressPopup", "true");
            }
            if (ckbWns_Tag.Checked && string.IsNullOrWhiteSpace(txtWNS_Tag.Text) == false)
            {
                request.Headers.Add("X-WNS-Tag", txtWNS_Tag.Text);
            }
            if (ckbWns_Group.Checked && string.IsNullOrWhiteSpace(txtWns_Group.Text) == false)
            {
                request.Headers.Add("X-WNS-Group", txtWns_Group.Text);
            }

            // POST
            request.Method = "POST";
            // 内容
            byte[] data = Encoding.UTF8.GetBytes(txtContent.Text);
            //request.ContentLength = data.Length;
            // 写入
            using (var streamout = request.GetRequestStream())
            {
                streamout.Write(data, 0, data.Length);
            }

            // 发起请求
            HttpWebResponse response = null;
            try
            {
                response = (HttpWebResponse)request.GetResponse();
                if (response.StatusCode == HttpStatusCode.OK)
                {
                    MessageBox.Show("发送成功。");
                }
                else
                {
                    MessageBox.Show("发送失败。");
                }
            }
            catch (WebException webex)
            {
                ……
            }

通知的正文是XML文档(RAW通知除外),以下几个标头是必须的:
Authorization:验证后获取的token就通过该头来传递,格式为Bearer <access token>,注意Bearer与Token之间有个空格,之个access token就是我们通过验证后获取的Token。

X-WNS-Type:通知的类型。如果是Toast通知就wns/toast,如果是磁贴通知就wns/tile,反正照着MSDN的文档套就行了,就像抄袭论文一样轻松。

ContentType:基本上都是text/xml,只有RAW通知使用application/octet-stream

然后是一些可选的标头:

X-WNS-Tag和X-WNS-Group可以一起用,tag相当于通知的id,Toast通知比较明显,如果我发送了一条tag为a的Toast通知,然后再发一条tag为b的Toast通知,就算a和b的内容完全一样,但由于tag不同,它们在手机上会显示为两条消息。

如果第一条Toast的tag为c,然后再发一条tag也为c的Toast通知,那么,第二条Toast会替换掉第一条Toast,也就是说始终在手机上只会提示一条通知。

如果用上了X-WNS-Group,就可以对Tag进行分组,同一组下的tag必须唯一,但不同组之间可以存在相同的tag的通知,比如,组1中有一条通知的tag为f1,组2中有一条通知的tag为f1,虽然它们tag相同,但处于不同的组中,因此它们在手机上将显示为两条通知。

X-WNS-SuppressPopup标头是针对Toast通知而言的,如果为false,则Toast会弹出,如下图所示。

如果使用了X-WNS-SuppressPopup了标头,并设置为true,则Toast通知不会弹出来,而直接放进通知中心了。如下图

 

现在,这个用于测试推送通知的服务器已经完成了,我们可以用它来发送通知了。源码下载地址:http://files.cnblogs.com/tcjiaan/PushNotificationTestServer.rar

下一篇文章咱们来完成WP手机客户端程序,来接收推送通知。

 

发表评论
用户名: 匿名