miso_soup3 Blog

主に ASP.NET 関連について書いています。

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 通知機能について
  • 2. 実装の流れと、ASP.NET Web API の役割
  • 3. 実装手順
    • 3-1.【iOS実装】iPad(iPhone)から APNs に Push 通知許可の登録をする
    • 3-2.【iOS実装】APNs からデバイストークンを受け取り、サーバー側にデバイストークンを送信する
    • 3-3.【サーバ側】デバイスから送信されるデバイストークンを、DB に保存する
    • 3-4.【サーバ側】DB に保存しておいたデバイストークンと、通知したいメッセージを APNs 側に送信する
  • 4. 参考サイト

1. 実装する Push 通知機能について

ASP.NET MVC で作成したサイト上で、通知したいメッセージ「Hellow Mario!」を入力し、「通知する!」ボタンを押すと、iPad(iPhone)で「Hello Mario!」と通知アラートが出るようにします。

Web サイトにて

iPad (iPhone) にて

2. 実装の流れと、ASP.NET Web API の役割

最初に、Push 通知の実装の流れを簡単に紹介した後、
ASP.NET Web API はどこを受け持つのかを確認します。

Push 通知の登場人物は単純です。

  • A. Apple の Push 通知サービス ― APNs
  • B. ユーザーが操作するデバイス ― iPad(iPhone)
  • C. 通知を送ったり、ユーザのデバイスを管理しておくサーバー側
    • (↑今回はこの C を ASP.NET Web API で実装します)

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)に通知されます。