今回は気象庁防災情報XMLを受信するためのsubscriberを作成します。
サーバー側アプリはsubscriber、XML記事のダウンロード、XML記事内容を整形するツールの合計3つ以上作る予定です。
まずはソリューション/プロジェクトを作成します。
ソリューション名を「JmaForecast」、subscriber用のプロジェクト名を「jmasubscriber」、プロジェクトの種類は「コンソールアプリケーション」とします。
今回はVisual Studio 2012を利用しました。
「ファイル」メニューの「新規作成」から「プロジェクト」を選択し、「Visual C#」の「コンソールアプリケーション」としてプロジェクトを作成。
プロジェクト名は「JmaForecast」としました。
そしてソリューションウインドウの「JmaForecast」プロジェクトを右クリックして現れたメニューから「削除」を選択して作成したばかりのプロジェクトを削除。
これだけではプロジェクトは完全には削除されていないので、エクスプローラーでソリューションのあるフォルダを開き、そこにある「JmaForecast」フォルダを削除してしまいます。
これで名前が「JmaForecast」で、プロジェクトが何もない空のソリューションができました。
次にソリューションウインドウの「JmaForecast」ソリューションを右クリックして現れたメニューから「追加」にある「新しいプロジェクト」を選択し、
「Visual C#」の「コンソールアプリケーション」を追加します。
ここでプロジェクト名は「jmasubscriber」、.NETのバージョンは「.NET Framework 4」としました。
これでソリューション/プロジェクトの準備は完了です。
次に一気にsubscriberのコードを作ってしまいます。
これは一番古典的な方法ですが、
C#アプリをCGIとして扱いたい場合は、アプリをコンソールアプリケーションとして作成し、
・GETで渡された内容は環境変数の「QUERY_STRING」を読む
・POSTで渡された内容はConsole.OpenStandardInput()からStreamで読む
という形で作れます。
CGIはコンソールアプリケーションではなくasp.netなどを使うのが一般的ですが、デバッグや応用が簡単なので今回もこれでいきます。
今回は関係ないですが、C#アプリはwindowsのテンポラリフォルダへ勝手に一時ファイルを作ることがあります。
そのためサーバー側ではWEBサーバーのユーザーに対してc:\Windows\tmp\へ書き込み権限を与える必要があります。
using System; using System.Collections.Generic; using System.Collections.Specialized; using System.IO; using System.Linq; using System.Text; namespace jmasubscriber { class Program { const string _strVerifyToken = "test_token"; const string _strFolerOut = "jma/"; static void Main(string[] args) { //POSTかGETか、というアクセス種別を取得する string strMethod = Environment.GetEnvironmentVariable("REQUEST_METHOD"); //GETの場合は、subscriberチェック→チャレンジコードを返す if (strMethod == "GET") { try { //GETで↓のような文字列が渡される // 「hub.topic=https://www.example.com/test.txt&hub.challenge=9567222552380101910&hub.verify_token=test_token&hub.mode=subscribe&hub.lease_seconds=432000」 string strQuery = Environment.GetEnvironmentVariable("QUERY_STRING"); NameValueCollection col = QueryToNameValueCollection(strQuery); string strHubMode = col["hub.mode"]; string strHubChallenge = col["hub.challenge"]; string strHubVerifyToken = col["hub.verify_token"]; if (strHubMode == "subscribe" || strHubMode == "unsubscribe") { if (strHubVerifyToken != _strVerifyToken || strHubChallenge == null || strHubChallenge == "") { Console.WriteLine("HTTP/1.1 404 \"Unknown Request\""); //unknown reqは404じゃないけど気にしない Console.WriteLine("Content-Type: text/plain"); Console.WriteLine(""); Console.WriteLine("failed(1)."); return; } Console.WriteLine("HTTP/1.1 200 \"OK\""); Console.WriteLine("Content-Type: text/plain"); Console.WriteLine(""); Console.Write(strHubChallenge); //WriteLine()はNG。チャンレンジコードを返す return; } } catch (Exception) { } Console.WriteLine("HTTP/1.1 404 \"Unknown Request\""); Console.WriteLine("Content-Type: text/plain"); Console.WriteLine(""); Console.WriteLine("failed(2)."); return; } //POSTの場合は、情報の配信→受信内容を保存する if (strMethod == "POST") { //何らかのエラーがありえる状態でも、とりあえず先に200 okを返しておく Console.WriteLine("HTTP/1.1 200 \"OK\""); Console.WriteLine("Content-Type: text/html"); Console.WriteLine(""); try { using (Stream st = Console.OpenStandardInput()) //標準入力から配信内容取得 using (BinaryReader br = new BinaryReader(st)) //StreamReader.ReadLine()などは使えない。BinaryReaderで処理 { string strLength = Environment.GetEnvironmentVariable("CONTENT_LENGTH"); int nLength = Int32.Parse(strLength); if (nLength < 1000000) //約1MB以上ある場合は無視する(そんなに大きなサイズが来ることはないはず) { byte[] data = new byte[nLength]; br.Read(data, 0, nLength); //効率は悪いけど一度文字列に変換しちゃう string strQuery = Encoding.UTF8.GetString(data, 0, nLength); //ファイル保存。ファイル名は日時ticks数値 string strFile = _strFolerOut + DateTime.Now.Ticks + ".txt"; using (StreamWriter sw = new StreamWriter(strFile, false, Encoding.UTF8)) { sw.Write(strQuery); } } } } catch (Exception) { } return; } } /// <summary> /// 「a=123&b=456&c=789」という形のクエリ文字列を、名前/値に分離してNameValueCollectionに格納する /// </summary> static NameValueCollection QueryToNameValueCollection(string strQuery) { NameValueCollection ret = new NameValueCollection(); string[] astrQuery = strQuery.Split('&'); foreach (string strItem in astrQuery) { string[] astrPair = strItem.Split('='); if (astrPair.Length == 0) continue; string strName = ""; string strValue = ""; if (astrPair.Length >= 1) strName = astrPair[0]; if (astrPair.Length >= 2) strValue = astrPair[1]; if (strName == "") continue; ret.Add(strName, strValue); } return ret; } } }
最後に作成したsubscriber CGIの動作確認を簡単に。
まずはビルドする前に、「プロジェクト」メニューにある「jmasubscriberのプロパティ」を選択し、構成「Release」にたいして、デバッグ」タブにある「Visual Studioホスティングプロセスを有効にする」のチェックをはずします。
そしてReleaseビルド。
出来上がった「jmasubscriber.exe」をサーバーへアップロードし、実行権限を付加します。
そしてアップロード先へWEBブラウザーでアクセスして404エラーが出れば成功です(「https://www.example.com/jmasubscriber.exe」などへアクセス)。
次にsubscriberのチェックをします。
汎用的なsubscriberのテストように公開されているサービス「https://pubsubhubbub.appspot.com/」を利用します。
「Subscribe to a feed or debug your subscriber」へアクセスし、
・「Callback URL」にexeへのURL(「https://www.example.com/jmasubscriber.exe」など)
・「Topic URL」にダミーとなるURL(「https://www.example.com/test.txt」など。とりあえずなんでもok)
・「Verify type」を「Synchronous」
・「Mode」を「Subscribe」
・「Verify token」を「test_token」(ソースコード内で設定した値)
として、「Do It!」ボタンを押して何も起きなければ成功。ページが切り替わってエラーが表示されたら失敗です。
さらに「Verify token」を「aaaaaa」のように適当な文字列に設定して「Do It!」ボタンを押してエラーが表示されたら成功です。
同様にpublishの動作確認をしたい場合はTopic URLに適当に作成したatom feedのxmlを置いて「publish」。feedを更新して「publish」というようにします。
subscriberが出来上がったら、「Callback URL」のURLと「Verify token」の値や住所氏名などを添えて指定様式で気象庁に登録申請します。