【WP 8.1開發】推送通知測試服務端程序


所謂推送通知,用老爺爺都能聽懂的話說,就是:

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手機客戶端程序,來接收推送通知。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM