iOS の Push 通知 のサーバー側を ASP.NET Web API で
iOS の Push 通知機能で、サーバー側の処理を ASP.NET Web API で実装してみました。
その手順について書こうと思います。
※注意
以下は、実装にあたり必要な部分ですがここでは省略しています。
- Push 通知の仕組みについて
- 証明書と鍵の取得と登録
- Web API の認証
また、Push 通知といえば Azure のモバイルサービスが気になるところですが、今回は利用しておりません。
Xcode のバージョンは 4.6.1、iOS 6.1 を使用しています。
Push 通知を試すには、iPad(iPhone)の実機が必要です。
目次
1. 実装する Push 通知機能について
ASP.NET MVC で作成したサイト上で、通知したいメッセージ「Hellow Mario!」を入力し、「通知する!」ボタンを押すと、iPad(iPhone)で「Hello Mario!」と通知アラートが出るようにします。
2. 実装の流れと、ASP.NET Web API の役割
最初に、Push 通知の実装の流れを簡単に紹介した後、
ASP.NET Web API はどこを受け持つのかを確認します。
Push 通知の登場人物は単純です。
C. のサーバー側では、通知先のデバイス先を制御するために、”デバイストークン”を知る必要があります。
”デバイストークン”は、1デバイスに1つ割り当てられる識別子のようなものです。
登場人物と Push 通知については iPhoneプッシュ通知まとめ こちらの記事にある図が大変わかりやすいです。
今回実装する Push 通知の流れを簡単に紹介します。
1.【iOS実装】iPad(iPhone)から APNs に Push 通知許可の登録をする。
この時、ユーザには「このアプリは通知機能を利用しますがよろしいですか?」という旨の
確認メッセージが表示されます。
2.【iOS実装】APNs からデバイストークンを受け取り、サーバー側にデバイストークンを送信する。
今回は関係ありませんが、デバイストークンがどのユーザのものなのかを識別するために、
ここでユーザ情報も一緒に送信する場合があります。
3.【サーバ側】デバイスから送信されるデバイストークンを、DB に保存する。
〜↓Push 通知を行う必要がある時(サイト上で「通知する!」ボタンが押された時)〜
4.【サーバ側】DB に保存しておいたデバイストークンと、通知したいメッセージを APNs 側に送信する。
5.【APNs 側】登録されているデバイストークンをもとに、Push 通知を行う。
6.【iOS 側】めでたく Push 通知を受け取とる。
流れは以上です。
サーバー側の ASP.NET Web API の仕事は2つです。
- 3. デバイスから送信されるデバイストークンを、DB に保存する。
- 4. 通知したいタイミングで、DB に保存しておいたデバイストークンと、通知したいメッセージを APNs 側に送信する。
後者については、通知したいタイミングで行うため、ASP.NET MVC 内で処理を行う場合もありますが、
今回は ASP.NET Web API 内で通知のトリガーを引きたいと思います。
3. 実装手順
3-1.【iOS実装】iPad(iPhone)から APNs に Push 通知許可の登録をする
Xcode のテンプレートから単純なアプリケーションを作成し、
アプリ起動時に Push 通知許可の登録を行い、デバイストークンを送信します。
ここでは、Single View Application を選択します。
プロジェクトが用意されたら、AppDelegate.m のアプリ起動時のメソッドに、Push 通知登録のコードを追加します。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { //Push 通知許可の登録を行い、デバイストークンを取得する NSLog(@"Push 通知許可の登録"); [[UIApplication sharedApplication] registerForRemoteNotificationTypes: (UIRemoteNotificationTypeBadge| UIRemoteNotificationTypeSound| UIRemoteNotificationTypeAlert)]; return YES; }
3-2.【iOS実装】APNs からデバイストークンを受け取り、サーバー側にデバイストークンを送信する
APNs からデバイストークンを受け取った時に呼ばれるメソッドを、同じ AppDelegate.m に定義します。
デバイストークンを受け取り、HTTP 通信でデバイストークンをサーバー側へ送信します。
//デバイストークンを受け取る -(void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { NSLog(@"受け取った deviceToken: %@", deviceToken); //サーバーへ送信 [self sendProviderDeviceToken:deviceToken]; } //デバイストークンをサーバー側へ送信する - (void)sendProviderDeviceToken:(NSData *)token { //★1リクエスト NSMutableData *data = [NSMutableData data]; [data appendData:[@"DeviceToken=" dataUsingEncoding:NSASCIIStringEncoding]]; [data appendData:[[token base64EncodedString] dataUsingEncoding:NSASCIIStringEncoding]]; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://192.168.0.55:82/api/push"]]; [request setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"]; [request setValue:@"application/json" forHTTPHeaderField:@"Accept"]; [request setHTTPMethod:@"POST"]; [request setHTTPBody:data]; [NSURLConnection connectionWithRequest:request delegate:self]; }
Base 64 への変換ライブラリ
ここでは、バイナリ値を Base64 へ変換するライブラリを使用しています。
バイナリ値であるデバイストークンを、Base64 で Body に書き込むためです。
https://github.com/nicklockwood/Base64 から DL し、AppDelegate.m の最初に以下のコードを追加します。
#import "Base64.h"
sendProviderDeviceToken メソッドは、デバイストークンをサーバー側へ送信する処理を書いています。
この時送信する HTTP リクエストは、
★1リクエスト URL: http://192.168.0.55:82/api/push (後に実装する ASP.NET Web API のホスト先) HTTPメソッド: POST Content-Type: application/x-www-form-urlencoded Body: DeviceToken=******(デバイストークンをBase64に変換したもの)
です。
次の ASP.NET Web API 側で、この HTTP リクエストを受け取った時の処理を実装します。
実装する際、このHTTP リクエストをよく参照するので、「★1リクエスト」と名付けます。
3-3.【サーバ側】デバイスから送信されるデバイストークンを、DB に保存する
Xcode から Visual Studio に移り、ASP.NET Web API を実装します。
DB 各種のモデルクラスを作成した後、デバイストークンを登録する API の実装を行います。
ASP.NET Web API のプロジェクトテンプレートを選択し、プロジェクトを作成します。
次に、送信されてくるデバイストークンをバインドさせるためのクラス、
「DeviceTokenRegistRequest」クラスを作成し、string 型のプロパティ「DeviceToken」を定義します。
場所はどこでも良いですが、「Models」フォルダの中が良いと思います。
public class DeviceTokenRegistRequest { public string DeviceToken { get; set; } }
この DeviceToken プロパティの名前は、★1リクエストの Body に記述した
DeviceToken と同じ名前にする必要があります。
DB モデルの定義
デバイストークンを DB に保存するための準備を行います。
ここでは、SQL Server、Entity Framework の Code First を利用します。
(Code First の詳細については省略します)
「Models」フォルダの中に、「MyContext」クラスを作成します。
同じく同フォルダに、エンティティである「DeviceToken」クラスも作成します。
2つのクラスのコードは以下の通りです。
using System; using System.Data.Entity; public class MyContext : DbContext { public DbSet<DeviceToken> DeviceTokens { get; set; } } public class DeviceToken { public int Id { get; set; } public byte[] Token { get; set; } public DateTime CreationDate { get; set; } }
以上で、DB にデバイストークンを保存する準備が整いました。
「DeviceTokenRegistRequest」クラスは、HTTP リクエストの Body の値を受け取るためのクラス、
「DeviceToken」クラスは、DB のエンティティを表すクラスになります。
API コントローラの作成
デバイストークンを受け取る API を作成し、DB に保存する処理を実装します。
ApiController を継承した Push コントローラを作成します。
「Controller」フォルダを右クリックし、追加>コントローラを選択し、「PushController」を作成します。
★1リクエストの処理を受け付けるエンドポイントを定義します。
作成した Push コントローラの中に、以下のコードを追加します。
public class PushController : ApiController { private MyContext db = new MyContext(); // api/push /// <summary> /// デバイストークンを登録します。 /// </summary> /// <param name="request"></param> [HttpPost] public HttpResponseMessage Post(DeviceTokenRegistRequest request) { if (!ModelState.IsValid) return Request.CreateResponse(HttpStatusCode.BadRequest); //DeviceToken エンティティの準備 var newDeviceToken = new DeviceToken(); newDeviceToken.Token = Convert.FromBase64String(request.DeviceToken); newDeviceToken.CreationDate = DateTime.UtcNow.AddHours(9); //もしすでに登録されている場合は、DBへの登録を行わない if (db.DeviceTokens.Any(e => e.Token == newDeviceToken.Token)) return Request.CreateResponse(HttpStatusCode.NoContent); //DBへ登録 db.DeviceTokens.Add(newDeviceToken); db.SaveChanges(); //作成したDeviceToken の情報をレスポンスに含め //ステータスコード 201 Created を返す。 HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.Created, newDeviceToken); return response; } protected override void Dispose(bool disposing) { db.Dispose(); base.Dispose(disposing); }
メソッドの引数に定義した DeviceTokenRegistRequest クラスの DeviceToken プロパティには、送信されたデバイストークンが代入されています。
★1リクエストで定義された「Content-Type:application/x-www-form-urlencoded」に従い、
ASP.NET Web API は DeviceTokenRegistRequestクラスに Body の値をバインドさせます。
Body の値であるデバイストークンは、Base64 に変換されているため Convert.FromBase64String メソッドで、バイナリ値に戻します。
ちなみにデバイストークンは、32 バイトです。
以上で、DB にデバイストークンが登録されるようになり、Push 通知の準備は完了です。
以降は、WEB サイトから Push 通知を行う機能の実装です。
3-4.【サーバ側】DB に保存しておいたデバイストークンを元に、通知したいメッセージを APNs 側に送信する
Web サイト上で「通知する!」ボタンを押した時に、APNs にデバイストークンと通知メッセージを送信する処理を実行します。
処理は、MVC で書いても良いのですが、せっかくなので JavaScript と Web API を使用します。
なので、先ほどの POST メソッドとは別に、もう1つメソッドを Push コントローラに追加します。
その後、JavaSciprt からその API を呼び出します。
APNs へ送信する処理は、PushSharp というライブラリを使用します。
DB に登録されている デバイストークンと、通知の種類、Push 通知に必要な鍵を、APNs へ送信します。
JavaScript の実装
ASP.NET Web API のプロジェクトテンプレートには、「Views」フォルダの「Home」フォルダに
「Index.cshtml」が含まれているはずです。
このコードファイルを、以下のように置き換えて、「通知する!」ボタンとメッセージを入力するテキストボックスを表示させます。
また、ボタンを押したときに、以下の HTTP リクエストが送信されるよう jQuery で実装します。
この HTTP リクエストを★2リクエストと呼ぶことにします。
★2リクエスト URL: ~/api/push/notify Content-Type: application/x-www-form-urlencoded Body: =Hello+Mario!(入力したメッセージ)
Index.csHtml の中身
<header> <div class="content-wrapper"> <div class="float-left"> <p class="site-title"> <a href="~/">ASP.NET Web API</a></p> </div> </div> </header> <div id="body"> <section class="featured"> <input type="textbox" value="Hello Mario!" id="messageTextBox" /> <input type="button" value="通知する!" id="pushButton"/> </section> </div> @section Scripts { <script type="text/javascript" > $("#pushButton").on('click', function () { $.ajax({ url : '/api/push/notify', type : "POST", data : { '' : $('#messageTextBox').val() } }); }); </script> }
APNs への送信
★2リクエストが送信された時に呼び出される API を定義します。
Push コントローラに以下のコードを追加…する前に、
APNs へ送信するライブラリ http://nuget.org/packages/PushSharp/:PushSharp をNuGet でインストールしておきます。
パッケージマネージャーコンソールにて
PM> Install-Package PushSharp
インストール後、Push コントローラにコードを追加します。
途中イベントがずらりと続くコードがありますが、エラーが起きたときの原因解析のためなので無くてもOKです。
// api/push/notify /// <summary> /// 通知を行います。 /// </summary> /// <param name="message">通知するメッセージ</param> [HttpPost] public void Notify([FromBody]string message) { var push = new PushBroker(); //Push通知の各イベントを設定(なくてもOK) push.OnNotificationSent += NotificationSent; push.OnChannelException += ChannelException; push.OnServiceException += ServiceException; push.OnNotificationFailed += NotificationFailed; push.OnDeviceSubscriptionExpired += DeviceSubscriptionExpired; push.OnDeviceSubscriptionChanged += DeviceSubscriptionChanged; push.OnChannelCreated += ChannelCreated; push.OnChannelDestroyed += ChannelDestroyed; //DBに登録されているデバイストークンをすべて取得 List<byte[]> allDeviceTokens = db.DeviceTokens.Select(d => d.Token).ToList(); foreach (byte[] token in allDeviceTokens) { //デバイストークンを16進数文字列に変換 string deviceToken = ToHexString(token); var appleCert = File.ReadAllBytes(@"C:\Users\miso_soup3\Desktop\push\my_apns_dev_cert.p12"); push.RegisterAppleService(new PushSharp.Apple.ApplePushChannelSettings(appleCert, "ここには証明書のpasswordを")); push.QueueNotification(new AppleNotification() .ForDeviceToken(deviceToken) .WithAlert(message)); //.WithBadge(7)); ←ちなみにバッジ通知の場合はこのように。 } } /// <summary> /// バイト配列から16進数の文字列を生成します。 /// </summary> /// <param name="bytes">バイト配列</param> /// <returns>16進数文字列</returns> private static string BytesToHexString(byte[] bytes) { StringBuilder sb = new StringBuilder(); for (int i=0;i<bytes.Length;i++) { sb.Append(bytes[i].ToString("X2")); } return sb.ToString(); } static void DeviceSubscriptionChanged(object sender, string oldSubscriptionId, string newSubscriptionId, INotification notification) { //Currently this event will only ever happen for Android GCM Debug.WriteLine("Device Registration Changed: Old-> " + oldSubscriptionId + " New-> " + newSubscriptionId + " -> " + notification); } static void NotificationSent(object sender, INotification notification) { Debug.WriteLine("Sent: " + sender + " -> " + notification); } static void NotificationFailed(object sender, INotification notification, Exception notificationFailureException) { Debug.WriteLine("Failure: " + sender + " -> " + notificationFailureException.Message + " -> " + notification); } static void ChannelException(object sender, IPushChannel channel, Exception exception) { Debug.WriteLine("Channel Exception: " + sender + " -> " + exception); } static void ServiceException(object sender, Exception exception) { Debug.WriteLine("Channel Exception: " + sender + " -> " + exception); } static void DeviceSubscriptionExpired(object sender, string expiredDeviceSubscriptionId, DateTime timestamp, INotification notification) { Debug.WriteLine("Device Subscription Expired: " + sender + " -> " + expiredDeviceSubscriptionId); } static void ChannelDestroyed(object sender) { Debug.WriteLine("Channel Destroyed for: " + sender); } static void ChannelCreated(object sender, IPushChannel pushChannel) { Debug.WriteLine("Channel Created for: " + sender); }
デバイストークンを16進数に変換するコードは、C# Tips - 16進数文字列とbyte型配列の相互変換
を参照致しました。
また、PushSharp の使い方については、How to Configure & Send Apple Push Notifications using PushSharp
が参考になります。証明書の取得方法についても書かれています。
ルーティングの設定
最後にルーティングの設定を行います。
これまでの作業で、2つの POST メソッドの API を Push コントローラ内に定義しました。
デバイストークンを登録する「~/api/push」と、通知を行う「~/api/push/notify」です。
このままではルーティングが被ってしまうので、「App_Start」フォルダ内の WebApiConfig.cs に以下のコードを記述します。
public static class WebApiConfig { public static void Register(HttpConfiguration config) { config.Routes.MapHttpRoute( name: "Notify", routeTemplate: "api/push/notify", defaults: new { controller = "Push", action = "Notify" } ); config.Routes.MapHttpRoute( name: "registDeviceToken", routeTemplate: "api/push", defaults: new { controller = "Push", action = "Post" } ); config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ); } }
この URL 設計は RESTful とは言えませんが、便宜上このようにしました。
以上、長くなりましたが Push 通知の実装は完了です。
「通知する!」ボタンを押すと、入力したメッセージが iPad(iPhone)に通知されます。
4. 参考サイト
参考サイト
- 【iphone】push-notificationの実装方法
- iPhoneプッシュ通知まとめ
- Windows Azure Mobile Services for iOS チュートリアル
- こちらは Azure Mobile Services を利用した例です。
- XCode + Azure 開発エミュレーターを使用した iOS アプリケーション開発