この記事は主に開発者向けの内容です。
この記事は2014年1月に作成したもので、最新の情報ではありません。特にTwitPaneおよびTwitter4Jは2014年3月15日現在、SPDYだけでなくHTTP/2.0にも対応しています。
Twitter4J を次世代 HTTP の SPDY に対応させてみました。
拙作の Twitter クライアント TwitPane では2013年12月リリースの Ver.3.3.1 より SPDY に対応しました。
本ページでは TwitPane で利用してみた結果も踏まえて、Twitter4J の SPDY 対応方法についてまとめています(作者のブログ で随時書いていた内容のまとめ+αになります)。
Twitter4J-spdy-support
- SPDY対応版(twitter4j-spdy-support):
導入方法
Twitter4J を導入済みのアプリに対して、下記2つの jar をクラスパスに(Androidならlibsに)追加してください。
- twitter4j-spdy-support-3.0.5-SNAPSHOT.jar
- okhttp-1.2.1-jar-with-dependencies.jar
- http://square.github.io/okhttp/ からダウンロードできます
あとは twitter4j が自動的に(この jar に含まれる) “twitter4j.internal.http.alternative.HttpClientImpl” を見つけて使ってくれます。
対応環境
- Twitter4J および OkHttp の対応環境に依存します
- 但し、Android の場合 SPDY は Android 4.1 以降で対応します(Android 4.0 以前は HTTP/1.1 でアクセスします)
- Java の場合は npn-boot が必要です(後述)
詳細な使い方とTips
SPDY通信の確認方法
Twitter4J の SPDY 拡張(twitter4j-spdy-support)を利用しても本当に SPDY 通信してるか不安ですよね。
一応下記のようなコードで Twitter オブジェクト(twitter) から SPDY のコネクションプール数を取得できます(リフレクションを利用しているので Twitter4J Ver.3.0.5 以降の版では動作しなくなる可能性があります)。
String message = "SPDY : ";
if (!twitter4j.internal.http.alternative.HttpClientImpl.sPreferSpdy) {
message += "Disabled";
} else {
message += "Enabled(";
try {
final Class<?> clazz = Class.forName("twitter4j.TwitterBaseImpl");
final Field f1 = clazz.getDeclaredField("http");
f1.setAccessible(true);
// wrapper = twitter.http
final HttpClientWrapper wrapper = (HttpClientWrapper) f1.get(twitter);
final Field f2 = HttpClientWrapper.class.getDeclaredField("http");
f2.setAccessible(true);
// http = wrapper.http
final twitter4j.internal.http.alternative.HttpClientImpl http =
(twitter4j.internal.http.alternative.HttpClientImpl) f2.get(wrapper);
final Field f3 = http.getClass().getDeclaredField("client");
f3.setAccessible(true);
// client = http.client
final OkHttpClient client = (OkHttpClient) f3.get(http);
if (client != null) {
final ConnectionPool p = client.getConnectionPool();
message += "SPDY[" + p.getSpdyConnectionCount() + "], ";
message += "HTTP[" + p.getHttpConnectionCount() + "])";
} else {
message += "no connections yet)";
}
final String lastRequestTransport = http.getLastRequestTransport();
if (lastRequestTransport != null) {
message += "\nOkHttp-Selected-Transport:[" + http.getLastRequestTransport() + "]";
}
} catch (Exception e) {
Log.e(TAG, e.getMessage(), e);
}
}
Log.d(TAG, message);
Twitter.getHomeTimeline()
などを呼び出したあとに ConnectionPool.getSpdyConnectionCount()
が 1 以上であれば正しくSPDY通信していると判定できます。
(2014/1/5追記) さらに、上記のように HttpClientImpl.getLastRequestTransport()
で(後述の) OkHttp-Selected-Transport
ヘッダーを取れるようにしました。”spdy/3″ のようなプロトコル名を取得できます。
ちなみに TwitterFactory.getInstance()
で Twitter オブジェクトを作った単位でプールが共有されます。 通信のたびに TwitterFactory.getInstance()
していると SPDY の意味がないので注意が必要です。
SPDYをオフにする設定
上の例のように twitter4j.internal.http.alternative.HttpClientImpl.sPreferSpdy
を false
に設定することで SPDY 通信を強制的にオフにすることが出来ます。 これで、SPDY通信をオプション化することができます(TwitPaneでSPDY切替をするためにこの仕組みを入れてあります)。
OkHttp 単体を利用する場合の SPDY 通信の確認方法
Twitter4J ではなく OkHttp 自体を使う場合は (OkHttpClient.open()
で取得した) HttpURLConnection
からレスポンスヘッダーを見ると下記のようなカスタムヘッダーが取れます。
OkHttp-Received-Millis:[1388254484917]
OkHttp-Response-Source:[NETWORK 200]
OkHttp-Selected-Transport:[http/1.1]
OkHttp-Sent-Millis:[1388254484817]
SPDY 通信をしている場合は OkHttp-Selected-Transport
が spdy/3
などになります。
Java(非Android)で使うときの設定
Eclipseやコンソール上のjavaコマンドから実行するとどうにもSPDY通信してくれないので困っていました。
シンプルなコンソールプログラムで生のOkHttp叩いてるんだけど、WindowsでもUbuntuでも OkHttp-Selected-Transport:[http/1.1] って言われて辛い。なんでSPDYにならないかなー。
— 竹内裕昭 (@takke)
Android上だとサクッとSPDYで通信してくれるのになー。
— 竹内裕昭 (@takke)
その後いろいろと調査した結果、どうやらJVMにnpn-bootというものが必要らしいです。
Jetty/Feature/NPN – Eclipsepedia
To enable NPN support, you need to start the JVM with:
java -Xbootclasspath/p:
…
肝心の npn_boot_jar も上記リンクからダウンロードできます(「もうjettyのバージョンに合わせてないのでファイル名のバージョンではなく日付に注目してね」って書いてあるので注意)。
NPN の仕組みは JVM が標準ではサポートしていないということですね。
むしろAndroid/Dalvik VM(4.1以降)が標準でサポートしているところがさすが Google 製といったところでしょうか。。
D:\(略)>java -version
java version "1.7.0_04"
Java(TM) SE Runtime Environment (build 1.7.0_04-b20)
Java HotSpot(TM) 64-Bit Server VM (build 23.0-b21, mixed mode)
D:\(略)>"C:\Program Files\Java\jdk1.7.0_04\bin\java" -Xms30m -Xmx30m -Xbootclasspath/p:..\npn-boot-8.1.2.v20120308.jar -classpath (中略) twitter4j.examples.tweets.ShowStatusBenchmark
start
[1357ms]
[160ms]
[176ms]
average:[564ms]
rate limit:[159/180], [687sec]
SPDY : [1/1]
たぶん java で SPDY やってる人には常識なんでしょうけど、ググラビリティが低くて2時間も悩んでしまったのでどなたかの参考になれば。
簡易ベンチマーク(Androidの場合)
簡易ベンチマーク(並列実行)
拙作の Twitter クライアント TwitPane に入れてベンチマークを取ってみました。
単一のツイートを取得するメソッドである showStatus を 100ms ずつずらして11回1リクエストした結果です。SPDY版がいい感じで速いです。
GalaxyNexus SC-04D/Android 4.2
環境 | 試行1回目(平均所要時間) | 2回目 |
---|---|---|
no SPDY, 3G | 2095ms | 1980ms |
SPDY, 3G | 1088ms(48%高速) | 1398ms(29%高速) |
no SPDY, WiFi/B-Flets | 654ms | 606ms |
SPDY, WiFi/B-Flets | 527ms(19%高速) | 505ms(17%高速) |
この条件では概ね 20% 程度速くなりそうな感じです。
測定に使ったコード: https://gist.github.com/takke/8143276
簡易ベンチマーク(シーケンシャル実行)
先ほどの showStatus のコードを単純に11回繰り返し実行するパターンもやってみました。
環境 | 平均所要時間 |
---|---|
no SPDY, 3G | 1409ms |
SPDY, 3G | 780ms(45%高速) |
no SPDY, WiFi/B-Flets | 689ms |
SPDY, WiFi/B-Flets | 452ms(34%高速) |
シーケンシャルに取得するパターンでも34%~45% ほど高速化されました。 これは使ってみたいと思える改善っぷりですね。素晴らしいです。
Android版Twitter4Jの注意
上記2件の測定結果は試行回数が少ないとはいえ、10%~40%程度の有意な改善結果になっています。 しかし実際にはこれは Android版Twitter4J 特有の事情によるものと思われます。
Android/Dalvik 環境で Twitter4J を利用する場合、とあるバグ の回避のために HTTP/1.1 の KeepAlive が無効になります。そのため、通信のたびに再接続するというオーバーヘッドがあります。
SPDY 版ではこのロスを回避できるため、比較的大きな改善になっているのだと思います。 (つまり、AndroidでSPDYを利用すれば(GoogleやFacebookなど他の)あらゆるサービスに対してこれだけの改善効果が出るというわけではないと思います)
(2014/3/24追記) Android2.3以降はKeepAlive有効なのかも?
Twitter4Jは「Android 2.3 (API 9)以前の環境ではVMのバグ回避のために」Keep-Aliveを無効化しているけど、それ以降は無効化しているわけではなさそう https://t.co/TuUogpRKdm
— beec1e (@beec1e)
↑のように、Android2.3以降ではKeepAliveは有効かもしれないという情報がありました。
@yusuke https://t.co/S6bfjlgQKZ ですね。VMRuntimeはAPI 9で削除されている http://t.co/cbyIbmnyvY ので、Android 2.3以降ならKeep-Aliveは有効…という感じでしょうか。
— beec1e (@beec1e)
で解説されてるようにAndroid2.3以降は有効とのことでした。
ですが、実際にNexus5/Android4.4.2で確認してみるとhttp.keepAliveはfalseでした。
http://gyazo.com/148f8f0ca91ff17fc654a52db2c348f4
Android2.3 以降では確かに dalvik.system.VMRuntime が存在しないのですが、Class.forName は通るようです。つまりそういうことなんでしょう。
というわけで、Android 2.3 以降も Twitter4J を使うと KeepAlive は無効になっていて、その分、spdy-support を導入すると速くなるのではないかと考えています。
※このペナルティがけっこう大きいと思うので(元のバグはAndroid2.2で解消されているので)Android 2.1以前でのみkeepAliveを無効にする修正を行ってプルリク送っています。 記事スポンサー カラフルで明るくスタイリッシュなソックスのコレクションから、どなたにもぴったりのものが見つかります。個別に購入することも、セットで購入して引き出しに彩りを添えることもできます。
@takke @roundrop ここをDalvik検出ではなく、Froyo以前かどうか検出、にすれば・・ https://t.co/wc0govVMIM
— 山本裕介 (@yusuke)
そのうちマージされるでしょう⇒ https://github.com/yusuke/twitter4j/pull/137
簡易ベンチマーク(Java/Windowsの場合)
シーケンシャルに実行するパターンではSPDYを使うと3%~5%ほど速いっぽい
Java/Windows で測定してみたところ、平均値ではSPDYを使わないほうが速いのですが、よく見ると1発目が遅いだけで2回目以降はSPDYのほうが3%~5%ほど速いことが分かりました。
NPNの負荷について
上記の結果で「1発目はSPDYが遅い」結果になっていましたが、これはSPDYのNPNによるプロトコル選択が影響していると推測できます。
Adopting SPDY in Line – Part 1: An Overview で解説されているように、LINEではNPNなしでSPDYを利用しているようですし、サーバ側のプロトコルサポート状況が既知である場合はNPNは不要ですよね。
LINEではNPNなしでSPDY使ってるのか。そりゃそうか、サーバもクライアントも自前だもんな。あとモバイルでは使ってない、と。あとでちゃんと読もう。 // NAVER Engineers’ Blog http://t.co/3o2Wzt8Bq9
— 竹内裕昭 (@takke)
そこで okhttp でも NPN をスキップできないかソースを読んでみましたが、どうやら現状の実装ではできないようです。
okhttpでNPNをskipできるか調べてみたけど、この部分で各プラットフォームのNPNをgetNpnSelectedProtocolで呼び出していて、その前後に抜け道はなさそう。 https://t.co/Jc1dvK1pDL
— 竹内裕昭 (@takke)
(2014/1/7追記) NPNを不要にするOkHttpパッチとベンチマーク結果など
OkHttp で特定のホストに対して NPN を不要にするパッチを作ってみました。 性能はほとんど変わらなかったので前述の「NPNの負荷について」に書いた推測は間違っていました。SSL/TLS自体の接続が遅いという理解でいいかと思います。
okhttpにNPN不要にするパッチ作って測定してみたけど性能差は誤差レベルだった。そりゃねぇ、プロトコル一覧やり取りするだけのパケット1往復が省かれるだけだもんね。むしろnpn-boot不要になったりAndroid4.0以前でもSPDYが使えるようになるかどうかだが。
— 竹内裕昭 (@takke)
Java/Eclipseでnpn-bootなしでSPDY通信できることは確認した。
— 竹内裕昭 (@takke)
Android2.2のIS03でもSPDY/3で通信できた。IS03のスクリーンショットの取り方忘れてたしw pic.twitter.com/yC4Y5jefnc
— 竹内裕昭 (@takke)
okhttpでNPNスキップするコードを試しにプルリクしたら華麗に却下された。ブラウザと同じ振る舞いをしたいからっていう理由は納得ですね。 https://t.co/nF2SSkGfdI
— 竹内裕昭 (@takke)
近いうちにSPDY/3のサポートをやめてHTTP/2に移行するつもりというコメントもあり、OkHttpの方向性が聞けて良かった。つまりSPDYってそういうもんだよね。
— 竹内裕昭 (@takke)
というわけで https://github.com/square/okhttp/pull/402#issuecomment-31606818 のプルリクを送りましたが OkHttp の開発方針と合わないといった理由で却下されました。まぁ、OkHttp自体も利用するソフトウェアも保守しにくくなるんで妥当だと思います。
まとめ:Twitter4J で SPDY を有効活用できるパターンはあるのかな?
@takke Twitter APIはサーバサイドの処理結構重いのでJettyでmockサーバ立てるとより顕著に差がわかるかも
— 山本U介 (@yusuke)
TwitterのAPIはいずれも重く、最短でも300ms~500ms程度はかかるので、SPDY による高速性は相対的に低く感じられます。
ただ、Twitter、CocoaSPDYをオープンソースに の記事にもあるように、iOS向けのTwitter公式アプリでは SPDY がデフォルトで有効になっています。
Twitterはこれにより通信遅延を最大30%削減でき、「ユーザのネットワーク状況が悪いとき」ほど改善が顕著に見られると説明している。
Twitterに限らず、2014年はSPDY対応が当然になっていくのかもしれませんね。
また、SPDYの一般的な利点として、「リクエストの多重化」があります。単一の TCP セッションで複数・バラバラのレスポンスを受信できるという機能ですが、Twitter4J の使い方でもこのパターンはあり得るかと思います。
拙作のTwitPaneはタブベースのTwitterクライアントですが、タブAのロード中にタブBを開くとタブAとBが同時にロードすることになります。さらに、バックグラウンドで新着の返信やDMを取得するタスクもあるため、複数のリクエストを同時に投げるシチュエーションがわずかながら存在します。 このような状況下では1本の TCP セッションを共有できるため、SPDYの恩恵は多少なりとも得られるのかと思います。特にモバイル環境下では顕著かもしれません。
- 10回のつもりだったけどベンチマーク用に10回繰り返したあとに1回本来の処理が走ってた… ↩