2017年12月19日火曜日

HTTP/1.1 Transfer-Encoding: chunked

Web Scraping 用のプログラムを組んでいて、HTTP レベルの Socket プログラムを自前で組むか、Client レベルの処理は言語環境のライブラリーに任せて HTML を解析・処理する程度のものを組むかという、二択問題によく悩む。これまでの経験上、後者の方が敷居が低く取りかかるのに容易であるというメリットがある。一方、一旦何らかの壁にぶち当たると、ブラックボックスのない前者の環境で試行錯誤を進めた方が、心理的にもまさしく“急がば回れ”の典型のような結果となり、トータルで楽になる。

そんなわけで、今回取り組んでいる物件についても、スタートアップは Java の URLConnection で一通り組み上げてしまって、それで問題なく全体的に動くものが出来上がっていたので、そのまま運用していたものだった。ゼロ状態からのスタートとなるプロジェクトでは、まずはどんな形であれ、全体としてちゃんと動くものへと到達できるかどうかが不明な状態なので、「まずは全体としてちゃんと動くもの」まで辿り着くことが何よりも重要である。そのためには、Client レベルのものは Java のコアライブラリーに任せて、Web Scraping にターゲットを絞った方が確実にプロジェクトを成功まで推進することができる。

それでそのまま動くようになったままで運用し続けていて、プログラムには手を入れていなかったのだが、今度、Lanterna を使って GUI 化したりして、使い勝手が向上したので、さらに前から欲しいと考えていた機能を追加したりした。最終的には GUI 化したらしたで CUI の時は気にならなかった処理速度が気になったので、ネットワークからのデータの読み込みをマルチスレッド化するところまで行き着いた。

このネットワーク読み込み処理のマルチスレッド化で、ある問題が表面化したのだった。

どうやら Java の URLConnection は、スレッドセーフではないようなのだ。とはいっても、このプロジェクトに関して言えば、Cookie がスレッド間で共有されていることが問題の核心であり、それを URLConnection 自体がスレッドセーフではないという話に含めていいのかどうかはわからない。ともかく、Cookie が上書きされてしまうのが原因で、サーバー側が付与する JSESSIONID に一貫性がなくなり、問題が発生することとなった。

URLConnection による Client 機能は Cookie なども宜しく処理してくれるので便利だが、一旦、Cookie を自前で操作したいとなると、逆に厄介だ。また、Apache のライブラリーを使えば、スレッドセーフに設計されているらしいが、こちらについては、Android の Java では Apache のネットワークライブラリーは非推奨であるという理由で、依存したくないので見送った。

そこで結局は HTTP レベルにフォールバックして Socket プログラムを自前で組むという方針にすることにした。

そもそも、Client レベルのものを言語環境のライブラリーに任せる場合の大きなメリットは

  1. Cookie を自前で処理する必要がなくなる
  2. HTTPS 対応を自前で行う必要がなくなる
  3. Transfer-Encoding: chunked を自前で処理する必要がなくなる

ではないかと思う。これまで、Cookie については面倒なだけで、必要に迫られた場合は、自前で HTTP レベルで処理する方策を採ってきた。HTTPS は調べてみたら SSLSocket を使えば Socket とほぼ同様に使えるので、意外と敷居は高くなかった。ところが chunked についてはお手上げで、自分が可能なことならば HTTP クライアントを自前で組むことを避ける主な理由はこれだった。

chunked の場合、HTTP/1.0 にして逃げることもできるので、無理に立ち向かう必要はない。ところで今回、HTTP/2.0 も使われ出したことだし、さすがに HTTP/1.0 に逃げるという消極的な方策はあまり気持の良いものでもないように感じた。それで今回は最後の壁である chunked へと立ち向かうことにした。

――で、chunked 対応な HTTP クライアントプログラムを自前で組んでみたのだが、なぜかセオリー通りに動作しない。何をやっても chunk のサイズ計算が合わないのである。レスポンスが Shift_JIS とかいう最悪の糞エンコーディングのせいだからかな? と思って色々試してみるも打破できない。行単位で処理する BufferedReader ではなく、 char 単位で処理する InputStreamReader で扱うようにしても改善せず。最後には byte 単位で処理する InputStream で直接扱っても改善しない。なぜかどうやっても、chunk サイズで指定された通りのサイズを読み込んでも、読み込まれるデータが chunk サイズよりも小さくなるのである。byte 単位なので、Shift_JIS による影響は完全に断てている。なのにこれは何なのか? 手は尽したのに、これではお手上げだ。

それでふと思い付いて chunk サイズをわざと +1000 してみたら、どうなるか試してみた。これが決定打だった。なんと chunk サイズを多くしても、全く読み込まれるデータが変わっていないのである。データが同じ場所でブツ切れている。

chunk サイズの問題ではないことに最後に行き着いて、それがどうやら、InputStream からのデータの読取に起因することだということが判明した。chunk サイズに相当する byte 配列を一つ用意して、それを inputStream.read(buffer) で一発で読み取ろうとしていたが、その処理でブツ切れになってしまっていたのだ。byte 配列を 1 バイト分だけ用意して、chunk サイズ分ループで回して 1 バイトずつ読み取ったら、完全に問題はクリアされた。

Java 以前には Perl がメインだったこともあって、ファイル IO 関係は一番感覚的に不慣れさが残っていたジャンルである。Perl の場合はデータはバイナリーモード(Java の byte に相当)か、文字モード(Java の char に相当)か程度で、一文字か文字列かという区別はなく、もちろん、バッファどうのということを気にすることもなく、とてもシンプルで苦労がなかった。そのため、Java を使うようになってからも、ファイル IO 関係は半ば「おまじない」的にコードを記述する感覚で、なぜ InputStream → InputStreamReader → BufferedReader と多段階になっているのか、不便な感じがする程度で深く検証することを避けていた。そのため、このようにデータの読み込みが半端な状況に陥いるなどというアイデアは最後になるまで想定できなかったのである。こういう経験で反対に、Perl の場合も、ファイル IO に関しては、while(<>) という記述をすることの由来がわかった。ところが Perl は大体が言語処理系側が宜くやってくれるので、記述上は配列に一気に読み込んでも、このようなブツ切れになってしまう羽目にはならなかったわけだが。Perl と比較すると、たまに C 言語の不便さが断片的に残っているのが Java なのかもしれない。それで Kotlin なんて話が出てくるのだろう。

2017年12月8日金曜日

Exception の使い処

Perl では Exception のことは気にしないでプログラミングできたので、Java で当初特異に感じたのが Exception の扱いだ。仕様に強制される形で try-catch を行う必要があるという認識で今まで凌いできた。

そもそも CUI でプログラミングしている段階では、例外処理については別段の配慮はせずとも、コンソールに JVM が吐くエラーメッセージが表示されるので、自分でエラーに備える処理を明示的にコーディングする必要はなかった。CUI というのはそういった意味でユーザーとプログラマーが完全に同一レベルの視点で体験を共有しているので、ある意味貴重な環境なのかもしれない。

そういった CUI の貴重な環境の習慣をそのまま引き摺ってしまっていて、GUI で Android のアプリなどを作っていても、例外処理について、標準メソッド側が要求するから受動的に try-catch を記述する程度で、自分から能動的に Exception クラスを用いるという経験があまりなかった。

ところで最近 Web のフォーム送信操作を自動化する Java プログラムを作成していて、想定外のエラーが発生した時、そのエラーがどの段階で出たエラーなのかということがわかった方が、自分自身で利用するだけでなくもしアプリを公開するという前提では、その方が良いと考えた。そこで、各段階で、想定通りではない結果になっていた場合、Exception を発生させるようなコードにすることにした。

当初は字義的に、RuntimeException を使っていた。ところがこれでも「どの段階で発生したのか?」がわかりにくかったので、最終的には IOException を使うことに落ち着いた。

GUI ライブラリーとして Lanterna という ncurses ライクなライブラリーを使っており、コンソールではなく、GUI の表示にエラーメッセージも表示させたいと考えた。当初の RuntimeException だと、どこかでエラーが発生していることがコンソールには表示されるのだが、どの段階で起ったのかが把握しにくく、GUI 側でもどこでキャッチすればいいのかがわかりにくい。そこで IOException にして、GUI 側で catch させることにしたのである。

ネットワークにアクセスしているので、標準ライブラリーからも IOException が発生する可能性があり、try-catch または throws 対応をしなくてはならない。これまで、throws の使い処がよくわからず、throws してもどの道さらにその上流の呼出元で catch しなければならなくなるだけで、いくらでも連鎖するので、とっとと catch した方がいい、と単純に考えていた。

ところが、今回のように例外はできるだけ GUI の層(MVC のうちの VC に相当)で処理することにすると、中核のフォームの送信などのネットワーク処理を行う層(MVC の M に相当)では例外はひたすら throws して呼出元に回した方がいいとわかった。標準ライブラリーから発生するものも throws し、また想定外の状態に陥った場合は throw new IOException() して throw する。catch するのは、Lanterna を使った GUI コードの層のクラスにおいて行う。そこで catch したエラーを GUI を通じてメッセージとして表示すればいい。

このようにしておくと、他のユーザーが使ってエラーが発生した時、エラーメッセージを表示してその情報を元にサポートする手掛かりを易くできる。