RestTemplateでトークン認証なAPIにアクセスした時のトークン有効期限切れに対応してみた。
この記事の概要
この記事はSpringBoot 1.5.6でRestTemplateを使い、認証トークンの有効期限切れに対応した際の記録を紹介しています。SpringBootとSpringを熟知しているわけではないので内容に多分の誤解がふくまれているかもしれません。 トークン認証が必要なAPIにアクセスする場合に、事前に何らかの方法で認証トークンを取得し、リクエストヘッダにトークンをセットしてAPIにアクセスするの方法が一般的です。また、多くのAPIで認証トークンの漏洩などによる不正利用に対応するため、認証トークンには有効期限が設けられています。 そのため、RestTemplateでAPIにアクセスする直前に都度認証トークンを取得する必要があります。しかし、APIサーバへのリクエストが遅い場合(ネットワーク的に遠くにあるなど)2回アクセスに時間がかかりパフォーマンスを落としてしまいます。そこで、トークンをキャッシュに保存しておき、使い回す方法が考えられます。 しかし、この場合、キャッシュに保存された有効期限を考慮しなければ、期限切れのトークンでアクセスし、リクエストが受け付けられない事態が発生します。 この記事ではトークンの有効期限を考慮せずに、キャッシュに保存したトークンを使い回すため、アクセス時に認証エラーが返ってきた場合に再度トークンを取得し、リクエストをリトライする方法を紹介します。
キャッシュ事態に有効期限を設ける方法も考えられますが、Spring Cacheの利用を考えたばい、キャッシュに有効期限を設定できるかはキャッシュに利用する製品に依存します。(Redisを利用する場合はできるようです)
以下の3つを順を追って説明します。
1.認証トークンの取得とセット
2.認証トークンのキャッシュ化
認証トークンの取得とセット
そもそもリクエストへの認証トークンの設定はRestTemplateのClientHttpRequestInterceptorインタフェースを実装したInterceptorの中で行います。実際のTokenの取得処理はTokenManagerクラスを作り、そちらで取得を行います。
- ClientHttpRequestInterceptorを実装したRequestTokenInterceptor(ソース1)
@Component public class RequestTokenInterceptor implements ClientHttpRequestInterceptor { private BTokenManager tokenManager; public TokenInterceptor(TokenManager tokenManager){ this.tokenManager = tokenManager; } @Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { request.getHeaders().add("Authorization", "Bearer "+tokenManager.getAuthenticationToken());//(1) ClientHttpResponse response = null; response = execution.execute(request, body); return response; } }
(1)リクエストの ヘッダにTokenManagerから取得したトークンをBearerトークンとして設定する。
また、この時、TokenManagerはAPIに対して何らかの認証処理を行い、API Tokenを取得しています。
- TokenManager(ソース2)
@Component public class TokenManager { ; @Value("${bing.auth.url:}") private String authUrl; private RestTemplate restTemplate; public TokenManager(RestTemplateBuilder restTemplateBuilder, BasicAuthRequestInterceptor interceptors){ restTemplate = restTemplateBuilder.additionalInterceptors(interceptors).build();//(1) } public String getAuthenticationToken(){ URI uri = UriComponentsBuilder.fromUriString(authUrl).build().toUri(); String result = restTemplate.postForObject(uri, null, String.class);//(2) return result; } }
(1) BasicAuthのためのヘッダを設定するInterceptorを認証処理用のRestTemplateに設定する。今回はToken取得処理の認証情報の設定もInterceptorを利用。 (2) getAuthenticationTokenメソッドが実行されると認証URIにPOSTリクエストを発行し、トークンを取得してからStringとして返す。
この方法ですと、APIにアクセスするたびに、認証リクエストが発行され、1回余分にリクエストが実行されることになります。
認証トークンのキャッシュ化
そこで、Spring Cacheの機能を使い、認証トークン取得メソッドの結果をキャッシュし、2回目以降のリクエストはキャッシュを利用するようにします。変更点はTokenManagerのみで、以下のように getAuthenticationTokenメソッドに@Cachableアノテーションを設定します。Spring Cacheを有効にするため、別途クラスの先頭に@CacheEnableを付与する必要があります。
- トークン取得処理のメソッドのキャッシュ対応(ソース3)
@Cachable("authToken") public String getAuthenticationToken(){ URI uri = UriComponentsBuilder.fromUriString(authUrl).build().toUri(); String result = restTemplate.postForObject(uri, null, String.class);//(2) return result; }
これで、2回目のアクセス以降はキャッシュからトークンが返され、認証リクエストは実行されなくなります。ですが前述の通り、キャッシュに登録されているトークンの有効期限が切れた場合にAPIサーバからエラーで弾き返されることになります。APIサーバの使用にもよりますが、多くの場合401や403が帰ってくるかと思います。
トークン有効期限切れのリクエストリトライ
続いて、本題のリクエストリトライの方法です。まずTokenManagerにキャッシュをクリアするメソッドを追加します。以下のメソッドをTokenManagerに追加します。 特に処理はないですが、@CacheEvictアノテーションで指定したキャッシュが削除されます。
- キャッシュクリアするメソッド(ソース4)
@CacheEvict(value="authToken") public void clearCacheTokenInfo() { log.info("Rfresh token cache."); }
APIへのリクエストが実行された時、RequestTokenInterceptor::interceptメソッドの中で40xのエラーを検知して、キャッシュをクリアし、新しく Tokenを取得して再度、リクエストを投げます。具体的には上記ソース1を以下のソース5のように修正します。
@Component public class RequestTokenInterceptor implements ClientHttpRequestInterceptor { private BTokenManager tokenManager; public TokenInterceptor(TokenManager tokenManager){ this.tokenManager = tokenManager; } @Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { request.getHeaders().add("Authorization", "Bearer "+tokenManager.getAuthenticationToken()); ClientHttpResponse response = null; response = execution.execute(request, body); if(response.getStatusCode()==HttpStatus.FORBIDDEN || response.getStatusCode()==HttpStatus.UNAUTHORIZED){//(1) tokenManager.clearCacheTokenInfo();//(2) List<String> token = new ArrayList<>(); token.add("Bearer "+tokenManager.getAuthenticationToken()); request.getHeaders().put("Authorization", token);//(3) response = execution.execute(request, body);(4) } return response; } }
(1) responseのステータスコードを確認して、FORBIDDENまたはUNAUTHORIZEDが帰って来ればリクエストリトライの処理を実行する。トークンエクスパイアの際の実際の挙動については各APIの仕様を調べる必要がある。 (2) リクエストリトライの処理の最初に現在のキャッシュをクリアする。 (3) 今回はAUthorizationヘッダを上書きするため、HttpHeaders::addではなく、HttpHeaders::putを使い値を上書きする。また、この時、TokenManager::getAuthenticationTokenを実行することで、再度トークンがキャッシュされる。 (4) リクエストを実行し、新しい認証トークンを使いAPIにアクセスする。
あとはAPIにアクセスするためのRestTemplateにRestTemplateBuilder経由でinterceptorを追加すれば、トークンの有効期限切れを機にすることなく、透過的にAPIにアクセスすることができます。
まとめ
そもそも認証トークンを一定期間キャッシュで保持するのは一般的なのか知りたいところです。今回の方法ではトークンの有効期限切れを避けるため、キャッシュの残存時間の設定を調整する必要がないのがミソでしょうか。
RestTemplateでレスポンスヘッダ内のContent-Typeを変更する。
この記事で紹介すること
この記事ではSpringのRestTemplateで受け取ったレスポンスのContent-Typeを変更し、任意のHttpMessageConverterを実行させる方法を考えたのでメモとして記載します。この記事はQiitaに投稿した記事をサンプルとして転載しています。
外部のAPIサーバを利用した際に、JSONフォーマットのbodyにContent-Type: text/plainで帰ってくることがありました。レスポンスのJSONに対応したBeanに値をバインドさせようとしてもContent-Typeがtext/plainのため、期待したHttpMessageConverterにマッピングされません。 そこで、RestTemplateにレスポンスのヘッダを変更するClientHttpRequestInterceptorを登録し解決しました。
ClientHttpRequestInterceptorの実装とRestTemplateBuilderの利用方法は以下のサイトを参考にしました。
またBing Speech API を対象に今回の内容を実装したものを以下のリポジトリに置いています。認証などの要素も入っているため、今回の説明とは多少異なります。 https://github.com/masato-ka/speech-api-proxy-service
ClientHttpRequestInterceptorの実装
- ResponseHeaderInterceptor.java
@Component public class ResponseHeaderInterceptor implements ClientHttpRequestInterceptor { @Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { ClientHttpResponse response = execution.execute(request, body);//(1) response.getHeaders().setContentType(MediaType.APPLICATION_JSON);//(2) return response; } }
(1)実際のリクエスト実行処理
(2)リクエスト実行後、レスポンスオブジェクトのContentTypeをapplication/jsonで上書きします。
RestTemaplateの設定と実行
- RestClient.java
@Component public class RestClient { private final String apiUri = "https://hoge.com/api/v1/foo" private final RestTemplate restTemplate; public RestClient(RestTemplateBuilder restTemplateBuilder, ResponseHeaderInterceptor interceptor){//(1) restTemplate = restTemplateBuilder.additionalInterceptors(interceptor).build();//(2) } public Result request(){ URI targetUri = UriComponentsBuilder .fromUriString(apiUri) .build() .toUri(); Result result = restTemplate.getForObject(targetUri, Result.class);//(3) return result; } }
(1) ResponseHeaderInterceptorをInjection
(2) RestTemplateBuilderを通してResponseHeaderInterceptorをRestTemaplateに設定
(3) リクエストを実行、この際にResponseHeaderInterceptorの処理がフックされる
まとめ
そもそもサービス側がContent-Typeを正しく返してくれればいいのですが、コントロールできない他者のサービスだったため、このような方法をとりました。 そもそもInterceptorを実装しない簡単な方法があるかもしれません。
Konashiを使ってみた
ユカイ工学さんのKonashiで遊んでみた。
http://konashi.ux-xu.com/
はじめてのObjective-Cつらい。。。グラフ書くのにCore-Plotというライブラリを利用したんだけでも、
ただ折れ線グラフ書くだけで心が折れそうに。
説明:
iOSシミュレータでKonashiに接続後、iOS側からデジタルIOを制御してます。
その後圧力センサの値をAD変換で取り込んでグラフに出力しています。
何作ろうかな。
detectFace APIのPythonサンプル
FaceBookに買収されたFACE.comが開発者向けAPIを停止するとのことなので、他の類似サービスがないか探してみました。そうすると、日本の会社がhttp://detectface.com/:detectFace();という類似サービスを公開しているようです。こちらはアプリケーションキーの登録などは必要なく、POSTやGETで画像データを送るだけで利用できます。
今回はPythonでローカルに置いている画像をPOSTするクラスを作ってみました。
#!/usr/bin/env python # -*- coding: utf-8 -*- ''' Created on 2012/07/15 @author: masato-ka ''' import os import urllib2 import urllib class detectFaceProxy(object): def __init__(self,mode=0,force=0): self.detect_face_url='http://detectface.com/api/detect?m={m}&f={f}'.format(m=mode,f=force) self.data='' def fetch_feature(self,image_file_path): """ detectFaceAPIに画像を投げ込み、特徴点XMLを返す。 :param String image_file_path: 顔画像のファイルパス :rtype:String :return: 結果xml """ path,type=os.path.splitext(image_file_path) content_type=type[1:] if content_type=="jpg": content_type="jpeg" data = self._read_image_binary(image_file_path) request = urllib2.Request(self.detect_face_url) request.add_header('Content-Type','image/{t}'.format(t=content_type)) request.add_data(data) opener = urllib2.build_opener() self.data = opener.open(request).read() return self.data def _read_image_binary(self,file_path): return open(file_path,"r").read()
detectFaceProxyクラスのインスタンスからfetch_featureメソッドを実行してください。fetch_featureメソッドには画像ファイルのパスを渡します。返値として結果xmlがかえってきます。また、detectFaceProxy.dataにも同じものが格納されます。結果のxmlはxml.dom.minidomなんかで適当にパースすると扱いやすくなります。
単純に顔と目の位置、特徴点の位置と信頼度を返すだけですので、FACE.comのように性別の判定などは行っていないようです。
特徴点をJubatusなんかに食わせられるだろうか。
PythonでJubatusのregressionクライアントのサンプルコードを作ってみた。
Jubatusnoのregressionのサンプルコードを作ってみました。基本的には公式サンプルのclassifierをベースに書いてるので、対比して読みやすいかと思います。例によってPythonです。
regression(回帰)
regression(回帰)とは独立変数と従属変数のデータセットからその関係性を求めます。未知の値の独立変数を入力したときに、その独立変数に対する従属変数の値を推定できます。
簡単に説明すると、最小二乗法を考えるといいのですが、X(独立変数)とY(従属変数)のデータの集まりから、Y=b+aXのa とbを求めます。こうすることで、未知のXが来た際もYの値を推定することができます。もちろん実際は独立変数がX1 X2 X3....と複数存在したり、推定すべき式の形(モデル)もn次関数であったり、複雑です。また、これらを求める手法としては前述の最小二乗法だけでなくベイズ理論を用いた方法などが存在します。JubatusではPAと呼ばれるアルゴリズムを用いたPA-regressionというものを用いています。
回帰ができると、株価の予想や、気温と消費電力の関係、市場予測など様々な分野に応用できそうですね。センサーの値と事象を学習させて未知の事象をセンサーが観測してもそれに対する正しい出力を予想できそうです。
サンプルコード解説
細かいことはどうでもいいのでサンプルコードは以下のようになっています。
#!/usr/bin/env python # -*- coding: utf-8 -*- import sys from jubatus.regression import client from jubatus.regression import types def parse_args(): from optparse import OptionParser, OptionValueError p = OptionParser() p.add_option('-s', '--server_ip', action='store', dest='server_ip', type='string', default='127.0.0.1') p.add_option('-p', '--server_port', action='store', dest='server_port', type='int', default='9199') p.add_option('-n', '--name', action='store', dest='name', type='string', default='tutorial3') p.add_option('-a', '--algo', action='store', dest='algo', type='string', default="PA") return p.parse_args() if __name__ == '__main__': options, remainder = parse_args() regression = client.regression(options.server_ip,options.server_port) str_fil_types = {} str_fil_rules = [] num_fil_types = {} num_fil_rules = [] str_type= {} str_rules = [] num_type = {} num_rules = [types.num_rule("value","num")] converter = types.converter_config(str_fil_types, str_fil_rules, num_fil_types, num_fil_rules, str_type, str_rules, num_type, num_rules) config = types.config_data(options.algo, converter); pname = options.name regression.set_config(pname,config) print regression.get_config(pname) print regression.get_status(pname) with open(".//train.csv") as f: rows = f.read().split("\r") power_list = [row.split(",") for row in rows] for p in power_list: print p train_data=[(float(list[0].replace(":",".")),types.datum([],[["value", float(list[1])]])) for list in power_list] for t in train_data: regression.train(pname,[t]) regression.get_status(pname) regression.save(pname, "tutorial3") regression.load(pname, "tutorial3") regression.set_config(pname, config) regression.get_config(pname) with open(".//test.csv") as f: rows = f.read().split("\r") power_list = [row.split(",") for row in rows] for p in power_list: datum = types.datum( [], [["value",float(p[1])]] ) ans = regression.estimate(pname,[(datum)]) print "estimate:%s actual:%s" %(ans[0],p[0])
以下かいつまんで説明します。
import モジュール
次のモジュールをインポートします。
from jubatus.regression import client from jubatus.regression import types
regressionのクライアントインスタンス
regression = client.regression(options.server_ip,options.server_port)
classifierとは違って今回はregressionです。お間違いのないよう。引数はclassifierのサンプルと同じになっています。
フィルタの記述
str_fil_types = {} str_fil_rules = [] num_fil_types = {} num_fil_rules = [] str_type= {} str_rules = [] num_type = {} num_rules = [types.num_rule("value","num")]
filterの記述方法についてはJubatusの公式ページにリファレンスが公開されているので、詳しくはそちらを参照いただければと思います。今回は読み込ませるデータが数値だったため、num_rulesのみ追加しています。valueの値をそのまま使うという意味です。numのほかにも値の対数値を使うlogという指定があります。フィルターは利用しません。
テストデータの読み込み
with open(".//train.csv") as f: rows = f.read().split("\r") power_list = [row.split(",") for row in rows] for p in power_list: print p
Jubatusと関係ないところですが、trac.csvというファイルからcsv形式のデータをリスト形式で読み込んでいます。今回使用したデータは[時間,東京の消費電力]というものです。元データは東京電力の電気予報のサイトから入手できます。http://www.tepco.co.jp/forecast/html/images/juyo-2011.csv:東京電力公開2011年の消費電力
このデータからヘッダ情報、年月日を取り除き、時間と消費電力のデータのみを利用しています。
データの作成と学習
train_data=[(float(list[0].replace(":",".")),types.datum([],[["value", float(list[1])]])) for list in power_list] for t in train_data: regression.train(pname,[t]) regression.get_status(pname)
train_dataが(ラベル,datumオブジェクトのリスト)です。datumオブジェクトは次のように定義しています。[ [], [ ["value",float型の数値] ] ]
Jubatusでは学習データや結果をdatumというオブジェクトで表現しています。datumは以下の用になっています。
( [ ["user/id", "ippy"], ["user/name", "Loren Ipsum"], ["message", "<H>Hello World</H>"] ], [ ["user/age", 29] , ["user/income", 100000] ] )
データはkeyとvalueのペアになっています。このkeyとvalueはリストの要素になっており、[key,value]という形で表します。また、前半のリストはvalueが文字列のもの。後半のリストはvalueが数値になっているものです。datum[ svkey,value, nv[key,value]]という形になっています。
まとめると、[(label,datum),(label,datum)....]という形式のデータをregression.train()に与えています。なおregressionではlabelもdatumで与える数値もfloat型でないとだめなようです。ちなみにlabelが従属変数、datumの中の値が独立変数となります。
推定
with open(".//test.csv") as f: rows = f.read().split("\r") power_list = [row.split(",") for row in rows] for p in power_list: datum = types.datum( [], [["value",float(p[1])]] ) ans = regression.estimate(pname,[(datum)]) print "estimate:%s actual:%s" %(ans[0],p[0])
テストデータを読み込んで未知データを与えてみます。結果では推定値と実際の値を表示させます。ちなみに、電力と時間の関係では期待通りの結果を得ることができませんでした。消費電力と時間ではあまり相関がないと思うので当たり前かもしれませんが。気温と時間と消費電力だとかわってくるかもしれませんね。
まとめ
regressionは基本的にclassifierとそんなに違いがないので取っ付きやすいかもしれません。また、Jubatusのリファレンスもしっかりしてるので困らないと思います。
かなりおおざっぱで汚いコードですが、ご参考にしていただければ幸いです。もし、わかりづらい箇所があればコメントなどを残していただくとできる限り対応したいと思います。regressionできると応用範囲がさらに広がりそう!
JubatusをCentOS5.8にインストールした奮闘記
JubatusをCentOS5.8にインストールしました。インストール中にはまりどこがあったので、個人的な備忘録代わりに記しておきます。
Jubatusとは?
Jubatusはオンラインで大規模・リアルタイムな機会学習が可能なOSSです。NTT研究所とPFIが開発を行っています。
Jubatus
現在のバージョンは0.3.0です。Jubatusはサーバ・クライアント構成になっており、文章の多値分類、レコメンデーション、回帰、特徴抽出、ネットワークグラフの分析などをリアルタイムに行うことができます。また、zoopkeeperを使って複数のサーバ側で分散して学習と分類・レコメンデーションを処理できます。
使用方法はJubatusサーバを立ち上げ、クライアントからRPCでmsgpack形式の設定や分類データを送付し、結果を受け取ります。クライアントはC++/Pythonなどが用意されています。
CentOS5.8へのインストール
Jubatusのインストールは複数の関連パッケージを事前にインストールしておかなければならず、yumやaptで一発という訳にはいかないようです。*1今回はJubatusのインストールシェルが公開されているのでこれを利用します。関連パッケージの入手とビルドの手順がシェルスクリプトにまとめられています。
そのまえに、CentOSの環境を整えます。
事前準備
CentOSに次のパッケージをインストールしておきます。
CentOS 5.8はPython2.4がデフォルトになっているため、Python2.7にバージョンアップしておきます。この時、PythonのSSLサポートをONにしておく必要があります。Googlecodeからre2ライブラリをmercurialでクローンするときに転けます。私はSSLをONにしてなかったので、install.shでre2ライブラリのクローン先をhttsからhttpにしました。またgccも4.1なので4.4以上にバージョンアップしておきます。
Jubatusインストーラのダウンロードとシェルスクリプトの修正
GithubからJubatusのインストールスクリプトをcloneして、ディレクトリに移動します。
$ git clone https://github.com/odasatoshi/jubatus-installer.git
$ cd jubatus-installer
このままではインストールに失敗してしまうので、installer.shの中身を修正します。修正内容はmsgpackのビルド部分で、CFLAGSとCPPFLAGSを追加することです。このオプションをつけずにビルドすることも可能ですが、msgpackのライブラリをリンクさせるときに失敗します。
cd ../msgpack-${MSG_VER} ./configure --prefix=${PREFIX} CFLAGS="-march=i686" CPPFLAGS="-march=i686" make make install
続いて、jubatus.profileの内容を.bashrcにコピーするか、source jubatus.profileを実行します。これで環境変数にインストールに必要なパスが追加されるはずです。.bashrcにコピーした場合もsource .bashrcを忘れないようにしてください。
Jubatus起動の確認
最後にJubatusサーバの起動確認をしましょう。以下のコマンドで多値分類器の起動を行います。
$ jubaclassifier --name=tutorial
またtutorialのコードが以下のGithubで公開されています。git でクローンしてください。
中のReadmeに従い以下のURLからテストデータをwgetして展開します。
$ wget http://people.csail.mit.edu/jrennie/20Newsgroups/20news-bydate.tar.gz
その後Jubatusのサーバ(jubaclassifier)を立ち上げた状態でtutorial.py を実行すると文章分類を開始します。
$ python tutorial.py
まとめ
とりあえず、Jubatus動いたので何か作ってみたいです。tutorial.pyもさほど難しくないので、文章分類なら簡単にできるのではないかと思いました。
mbedとDjangoでWebSocket!
mbedを買ったので、django-websocketで作ったアプリケーションにセンサーデータをWebSocketで投げ込んでみました。全体的な構成は次の図のような感じです。
各構成要素説明
Websocket Server
Mac OS X上にDjangoフレームワークとdjango-websocket 0.3.0を使ってechoサーバーを構成しています。ほぼ前々回のエントリーで紹介したアプリケーションと同じものです。django-weboskcetは前回のエントリーで紹介したRFC6455対応版です。
mbed
mbedに関しては細かい説明は省略します。知らない人はググって下さい。Arduinoとかそんな感じのラピッドプロトタイピング向けマイコンです。WebSocketのライブラリが提供(RFC6455対応)されているのでそれを使ってセンサー情報の値をサーバーに上げています。今回はセンサーとしてFRSの圧力センサーを用いています。
iPad
今回はクライアントとしてiPad*1を使いました。普通のWEBブラウザでも十分です。サーバから送られてくる値をもとにJavaScriptでグラフを描画しています。
そんなわけでYoutubeに実験動画をアップロードしてみました。
画面の一番奥がWebsocket ServerのMacBook proです。その手前にiPadを置いています。丸くぴょこっとしてるのが圧力センサになっています。この圧力センサーを指で握ると、それに合わせてiPadに表示されているグラフの表示が変わるのが解ると思います。グラフはSafari上にJavaScriptで描画しています。
最初はWebsocket通信を500ms周期でやっていたのですが、どうも途中でハングすることが多いので動画は900ms周期にしています。このあたりはDjangoの開発用サーバを使っていたり、作りが悪かったりいろいろあるのかなと。もう少し周期を短くできるといいかなと思います。
このようにWebSocketを使うと簡単にWEB上でリアルタイム通信を実現することができます。こういったマイコンからの情報をサーバに上げてWEBブラウザで可視化したいという用途や、逆にWEBブラウザからマイコンへ指示を送りたい場合にはかなり強力な仕掛けになると思います。AjaxやCommetでも良いのですが、いずれにしろタイマでの更新またはポーリング処理を行うためタイムラグが発生すると思います。(まあ、実用上問題なければ大丈夫でしょうが)またHTTPセッションを1リクエストごと(今回だと16bitのデータを一つ送るたびに)接続しなければ行けないので、多くの機器と通信したい場合はAjaxやCommetは不利になるでしょう。
とりあえず今週末はここまで!
今回は本質じゃないところで、BUFFALOのルータが無線LANと有線LAN間で通信出来ないというバグ持ちだったので、最初うまくいかずに一晩悩みました。最終的にルータのファームウェア更新で解決!
この仕組みを利用して、もうちょっとアプリケーション的なものを作ってみようかな。