継続した接続とチャンクド応答
(Tomcat 4.0でHTTP/1.1応答を調べる)
|
VisualAge for JavaのWebsphereテスト環境、Apache Tomcatテスト環境双方ともに現在はHTTP/1.1サーバに対応していない。しかしながら、Tomcatのスタンドアロンのモードは4.0版からHTTP/1.1対応である。4.0版はこれまでと別のCatalinaと呼ばれるプロジェクトグループ(TomcatもCatalinaも有名な海軍機なので、その辺からこの名前がつけられたのかもしれない)が開発したものを採用したもので、まだベータ版であるが、実験的にこれを用いて、皆さんのコンピュータだけでこの継続した接続とチャンク応答を実習することができる。Tomcat 4.0は Servlet仕様書2.3版及びJSP 1.2仕様にも対応している。Tomcat 4.0のダウンロードとインストールは添付資料を参照されたい。
継続した接続に対するHTTP/1.1サーバの対応は以下のようになるのが好ましい。しかしながら、Tomcatの4.0のスタンドアロンのモードではちょっと異なる。即ち:
- HTTP/1.0クライアントではとにかく応答したらTCP接続を切る
- HTTP/1.1クライアントには応答の後は、クライアントが切断するかタイムアウトになるまでは絶対TCP接続を切らない
- HTTP/1.1クライアントにはサーブレットでContent-Lengthを付けない場合はChunkedで応答を返す(つまりバッファに入ったコンテントはその都度直ちに送信、即ちコミットされた状態にする)。サーブレットがContent-Lengthを付けた場合はChunkedにはしない
クライアントの要求 |
HTTP/1.1サーバの対応 |
Tomcat 4.0スタンドアロンの対応 |
|
HTTP/1.0クライアント |
Connection: Keep-Aliveヘッダ行なし。またはConnection: Close行つき。 |
応答にConnection: Closeをかける。 |
TomcatでConnection: Close行を付加してTCP接続を切る。Connection: Keep-Aliveヘッダ行は無視。 |
Connection: Keep-Aliveヘッダ行つき。 |
Content-length:ヘッダ行をつけ、Connection: Keep-Aliveヘッダ行または、Connection: Close行をつける。Content-length:ヘッダ行をつけないときはConnection: Close行をつける |
サーブレットでContent-length:ヘッダ行をつけてもつけなくても、TomcatでConnection: Close行を付加してTCP接続を切る。またConnection: Keep-Aliveヘッダ行をつけてもTCP接続を切る。 |
|
HTTP/1.1クライアント |
Connection: Close行つき。 |
応答にConnection: Closeをかける。 |
Chunkedエンコードして、サーブレットが作ったConnection: Close行は無視してTCP接続は切らない。 |
その他 |
Content-length:ヘッダ行またはTransfer-Encoding: Chunked行をつけ、最後の応答以外はConnection: Close行をつけない。Content-length:ヘッダ行もTransfer-Encoding: Chunked行も付けないときはConnection: Closeをかける。 |
Chunkedエンコードして、TCP接続は切らない。 |
皆さん各自これをHelloWorldやAnotherHelloWorldのサーブレットを使ってTelnetで確認していただきたい。HTTP/1.0クライアントに「継続した接続」を全く許していないのは、Netscapeのユーザには不満かもしれない。このようにサーブレット・コンテナによって細かいところで実装に相違がでるので、最終的に皆さんのアプリケーションが導入(デプロイ)されるサーバの特性を確認することが重要である。
以下にチャンクド・エンコーディングについて説明する。
HTTP/1.1サーバは、複数の要求に対応したコンテントをひとつの応答に含めて返すことができる。その場合はコンテントごとを固まり (Chunking:厚切りとか、かたまりとかいう意味だが、おだんごとか、もっと下品に馬糞とか?いうイメージ) にしてこれを複数ボディ部に収容する。このようなHTTP応答のヘッダには:
Transfer-Encoding: Chunked
なる行を含めるとともに、以下のルールでボディ部を構成する。
チャンクド・エンコーディング
- ボディ部は複数のチャンクからなり、最後のチャンクは長さゼロ(“0”)とする。 - その後にオプショナルなフッタがつき、最後に空白行(\r\nのみ)がボディ部の終わりを示す。 - 各チャンクは長さを示す行とデータからなる。 - 長さは16進表示である。この行には長さの後にセミコロンに続くパラメタを付すこともできるが、これの用途は特に規定されていない。この行の終わりに復帰改行(\r\n)がつく。 - データ部はデータ自身でそれに復帰改行(\r\n)がつく。 |
下表はその例である。ヘッダ行にはTransfer-Encoding: chunkedなる行があり、ボディ部がチャンクの集まりであることを示している。最初のチャンクは26バイト(16進で1a)、2番目のチャンクは16バイト(16進で10)のデータからなる。一連のチャンクの終わりは長さ0のチャンクである。次の2行がオプショナルなフッタ行で、最後の空白行がボディの終わりを意味する。データがテキストでなく、完全なバイナリの場合はMIMEエンコーディングで7ビット文字列に変換する場合が多い。
チャンクド応答の例 |
これと等価な非チャンクド応答 |
HTTP/1.1 200 OK Date: Fri, 25 May 2001 23:59:59 GMT Content-Type: text/plain Transfer-Encoding: chunked [空白行] 1a; ignore-this-parameter abcdefghijklmnopqrstuvwxyz 10 1234567890abcdef 0 some-footer: some-value another-footer: another-value [空白行] |
HTTP/1.1 200 OK Date: Fri, 25 May 2001 23:59:59 GMT Content-Type: text/plain Content-Length: 42 some-footer: some-value another-footer: another-value [空白行] abcdefghijklmnopqrstuvwxyz1234567890abcdef |
以下は、実際にHelloWorldのサーブレットをTomcat 4.0がチャンクドで返したものである。なお、Host:のヘッダ行はHTTP/1.1では必須の行であり、これを省略すると誤りとされるので、面倒ではあるが省略してはならない。詳細は添付資料「HTTP(Hyper Text Transfer Protocol)の基礎」を参照のこと。
GET /examples/servlet/HelloWorld HTTP/1.1 <HTTP/1.1で要求した> Host: localhost:8080
<HTTP/1.1ではこの行は必須> Connection: Close
<この指示はエンジンが無視> HTTP/1.1 200 OK Content-Type: text/html; charset=Shift_JIS Date: Fri, 08 Jun 2001 05:52:11 GMT <エンジンが付加> Transfer-Encoding: chunked
<エンジンが付加> Server: Apache Tomcat/4.0-b5 (HTTP/1.1
Connector) 6
<チャンク1の長さ> <HTML> 2
<チャンク2は\r\nの2バイト> 27 <HEAD><TITLE>Hello
World</TITLE></HEAD> 2 6 <BODY> 2 27 <BIG>Hello World from(株)クレス</BIG> 2 e </BODY></HTML> 2 0
<終了チャンクは0バイト>
|
ところで、サーブレットでConnection: Chunkedヘッダ行をつけ、チャンクド形式のデータをストリームで出力したらTomcat 4.0エンジンはどのような対応をするであろうか?応えは否である。次のようなChunkedHelloWorldサーブレットで試してみよう。
import java.lang.*; import java.io.*; import javax.servlet.*; import javax.servlet.http.*; /** *
ChunkedHelloWorldは、チャンク(塊)としてHTTP応答のボディを作成するサンプルである。 *
@author: Terry */ public class ChunkedHelloWorld extends
HttpServlet { /** *
Process incoming HTTP GET requests */ public void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException { performTask(request,
response); } /** *
Process incoming HTTP POST requests */ public void doPost(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException { performTask(request,
response); } /** *
Returns the servlet info string. */ public String getServletInfo() { return
"ChunkedHelloWorld for Persistent-Connection evaluation, Version 1.0 by
Terry"; } /** *
ChunkedHelloWorldの要求処理部では、日本語混じりのHTMLテキストをOutputStreamWriterで * バイトデータに変換してByteArrayOutputStreamバッファに蓄積する。このバッファのサイズから * バイトサイズを知ってチャンクを作成できる。 */ public void performTask(HttpServletRequest
request, HttpServletResponse response) { try { //プロトコル指定可能ではありませんでした // response.setHeader("Protocol",
"HTTP/1.1"); response.setContentType("text/html"); //必要ならここに文字セット指定を以下のように追加する // response.setContentType("text/html;
charaset=Shift_JIS"); //ボディがチャンクドであることをヘッダ行で知らせる response.setHeader("Transfer-Encoding",
"Chunked"); //必要ならこれでTCP接続の開放(Close)や継続(Keep-Alive)を行う response.setHeader("Connection",
"Keep-Alive"); //バイト応答出力用OutputStreamの取得 OutputStream
os = response.getOutputStream(); //バイト変換したデータ受領用ストリームを用意する ByteArrayOutputStream
baos = new ByteArrayOutputStream(1024); //指定したサイズは、不足が生じたら自動的に拡大される //バイトデータ一時退避用ストリームを用意する ByteArrayOutputStream
bytes = new ByteArrayOutputStream(1024); //指定したサイズは、不足が生じたら自動的に拡大される //バイト列に変換のためのOutputStreamWriterの取得 OutputStreamWriter
osw = new OutputStreamWriter(baos, "Shift_JIS"); System.out.println("OutputStreamWriter.getEncoding:
" + osw.getEncoding()); //エンコーディングは"Shift_JIS"や"ISO-2022-JP"や"EUC-JP"などを指定する //デフォルトのサイズは8192バイトである //これを更にBufferdWriterで包んでコードコンバータがwrite()毎に呼ばれないようにする Writer
out = new BufferedWriter(osw); //デフォルトのサイズは8192バイトである //HTMLページの最初の部分の記述 out.write("<HTML>"); out.write("<HEAD><TITLE>Chunked
Hello World: Part 1</TITLE></HEAD>"); out.write("<BODY><BIG>Chunked
Hello World from クレス (Part 1)<BR>"); //文字変換とバイト形式でバイトアレーバッファへの移しこみ out.flush(); // osw.flush(); //これはflush()の伝播により不要であるので今後書かない //最初のチャンクの退避 baos.writeTo(bytes); //最初のチャンクのサイズ表示行の入力 baos.reset(); out.write(Integer.toHexString(bytes.size())+"\r\n"); out.flush(); bytes.writeTo(baos); bytes.reset(); //最初のチャンクを送信バッファに移してこれをコミットする System.out.println("ByteArrayOutputStream:
" + baos.toString()); baos.writeTo(os); baos.reset(); os.flush(); //HTMLページの最後の部分の記述 out.write("Chunked
Hello World from クレス (Part 2)</BIG></BODY>"); out.write("</HTML>"); //文字変換とバイト形式でバイトアレーバッファへの移しこみ out.flush(); //最後のチャンクの退避 baos.writeTo(bytes); //最後のチャンクのサイズ表示行の入力 baos.reset(); out.write("\r\n"+Integer.toHexString(bytes.size())+"\r\n"); out.flush(); bytes.writeTo(baos); bytes.reset(); //最終チャンク表示行とパケット終了の空白行を入力 out.write("\r\n0\r\n\r\n"); out.flush(); //最後のチャンクと終了行を送信バッファに移してこれをコミットする System.out.println("ByteArrayOutputStream:
" + baos.toString()); baos.writeTo(os); baos.reset(); os.flush(); //全てのバッファのクローズ out.close(); osw.close(); bytes.close(); os.close(); } catch(UnsupportedEncodingException
e){ System.err.println("Encoding
not supported: " + e.getMessage()); } catch(IOException
e) { System.err.println("IOException
during AnotherHelloWorld.performTask: " + e.getMessage()); } } } |
これに対するTomcat 4.0エンジンの応答は次のようになる。
GET /examples/servlet/ChunkedHelloWorld
HTTP/1.1 Host: localhost:8080 HTTP/1.1 200 OK Content-Type: text/html Date: Mon, 11 Jun 2001 03:24:10 GMT Transfer-Encoding: Chunked Transfer-Encoding: chunked Server: Apache Tomcat/4.0-b5 (HTTP/1.1
Connector) Connection: Keep-Alive 78 74 <HTML><HEAD><TITLE>Chunked
Hello World: Part 1</TITLE></HEAD><BODY><BIG>Chunked Hello World from クレス (Part 1)<BR> 49 3c Chunked Hello World from クレス (Part
2)</BIG></BODY></HTML> 0 0 |
つまりTomcat 4.0エンジンはサーブレットが作ったTransfer-Encoding: chunkedヘッダ行を受け付けず、テキストであろうがバイトストリームであろうが勝手にチャンクド形式に変換してしまうのである。従って、チャンクド形式はプログラマからは操作不能となっているのである。