Apache Software Foundation | Jakarta Project | Apache Tomcat
はじめに

この文書の元となったのは、2000 年 12 月に Dan Milstein ( danmil@shore.net)によって書かれたものです。 現在公開されている文書は XML ファイルから生成され、 よりいっそう Tomcat の文書と統合しやすいものになっています。

この文書では、Apache JServプロトコル バージョン1.3 (以後は ajp13 )について説明します。 現在、プロトコルの動作方法についての文書はまったく存在しません。 この文書は、mod_jkの保守をおこなう人たちや、プロトコルを他に(たとえば、Jakarta 4.xに)移植したい人たちがより楽に作業できるように、この問題を改善しようとする試みです。


著者

私は、このプロトコルの設計者の一人ではありません - Gal Shachorがオリジナルの設計者だったと思います。 この文書のすべての情報は、私がTomcat 3.xのコードで見つけた実際の実装から得ています。 この文書が役に立つものであればと思いますが、完全に正確かどうかについては保証できません。 また、設計方針の決定の理由についても私は知りません。 私ができることは、いくつかの(設計での)選択に対して可能性のある理由づけを行うことだけですが、それらは単なる推測にすぎません。 全般的に、Shachorが書いたC言語のコードは非常にきれいで、(ほとんど文書化されていないにせよ)理解しやすいものです。 Java言語のコードについては、私が書き直して、かなり読みやすくなったと思います。


設計目標

Gal Shachorがjakarta-devメーリングリストに投稿した電子メールによると、 JK (と ajp13 )の最初の目標は、 mod_jserv ajp12 を以下のように拡張することでした(私は、WebサーバとServletコンテナ間の通信に関連した目標を取り込んだだけです)。

  • 性能の向上 (特に速度)。
  • SSLサポートを追加して、isSecure()getScheme()がServletコンテナ内で正しく動作するようにすること。 Servletで、リクエスト属性として、クライアント認証と暗号アルゴリズムが利用できるようになります。


プロトコルの概要

ajp13 プロトコルは、パケット指向です。 読みやすいプレーンテキスト形式ではなく、バイナリ形式を選択したのは、おそらく性能上の理由からだと思われます。 Webサーバは、ServletコンテナとTCPコネクション上で通信します。 コストのかかるソケット作成プロセスを減らすために、WebサーバはServletコンテナに対して永続的にTCPコネクションを保持して、複数のリクエスト/レスポンスのサイクルでコネクションを再利用しようとします。

いったんある特定のリクエストにコネクションを割り当てると、リクエスト処理サイクルが終了するまでは他に使用することはありません。 つまり、リクエストは、コネクション上で多重化されることはありません。 これによって、コネクションの両側のコードをより単純にできる反面、結果として、一度にオープンしているコネクション数が多くなってしまいます。

いったんWebサーバがServletコンテナに対するコネクションをオープンすると、コネクションは以下の状態のいずれかになります。

  • Idle
    このコネクション上で処理されているリクエストはありません。
  • Assigned
    このコネクションは、ある特定のリクエストを処理しています。

いったんコネクションがある特定のリクエストを処理するために割り当てられると、基本的なリクエスト情報 (たとえば、HTTPヘッダなど)は、コネクション上を高度に圧縮された形式(たとえば、一般的な文字列は整数にエンコードされます)で送信されます。 このフォーマットの詳細については、この後のリクエストパケット構造で説明します。 リクエストに対してボディ部が存在する(content-length > 0 である)場合には、直ちに別のパケットで送信されます。

この時点では、おそらくServletコンテナはリクエスト処理を開始する用意ができています。 その場合には、以下のメッセージをWebサーバに返信することができます。

  • SEND_HEADERS
    ヘッダの集合をブラウザに送信します。
  • SEND_BODY_CHUNK
    ボディのデータの一部(チャンク)をブラウザに返信します。
  • GET_BODY_CHUNK
    まだすべてのデータが転送されていない場合には、リクエストから、さらにデータを取得します。 これが必要なのは、パケットが固定最大長を持っているという制限のもとで、(たとえば、ファイルアップロードのために)任意の大きさのデータをリクエストのボディ部に取り込めるようにするためです。 (注意: これはHTTPのチャンク転送(chunked transfer)とは関係ありません)。
  • END_RESPONSE
    リクエスト処理サイクルを終了します。

各メッセージは、異なるフォーマットのデータパケットで実現します。 詳しくは、以下で述べるレスポンスパケット構造を参照してください。


基本パケット構造

このプロトコルは、XDRの特徴を少し受け継いでいますが、多くの点で異なっています(たとえば、4バイトのアライメントがないことです)。

バイトオーダー: 私はバイトのエンディアンがどうなっているかはよくわかりません。 リトルエンディアンだと思うのですが、それはXDRがそうだからです。 そしておそらく(C言語側のコードで)sys/socketライブラリが自動的に(バイトオーダーに関して)処理してくれているのではないかと思います。 ソケット呼び出しについて誰かもっと詳しく知っている人が参加してくれればすばらしいと思います。

このプロトコルには、byte, boolean, integer, stringという4つのデータ型があります。

Byte
1バイトの値です。
Boolean
1バイトの値であり、1 = true, 0 = falseです。 他の0以外の値をtrueに使用した場合には(すなわち、C言語風です)、ある環境では正しく動作しても、別の環境では正しく動作しないかもしれません。
Integer
0から2^16 (32768)までの範囲の数値です。 上位バイトを先にして2バイトで格納します。
String
可変サイズの文字列です (長さは2^16に制限されています)。 2バイトに圧縮した文字列の長さと、文字列(末尾に'\0'を含んでいます)をエンコードします。 ただし、エンコード長は、のように末尾の'\0'をことに注意してください。 これはJava言語側では少々混乱の元になっていて、これらのターミネータを読み飛ばすために、一見奇妙に見えるインクリメント演算子がコード中にちらばっています。 このようになったのは、Servletコンテナが送り返す文字列を読み込む時のC言語のコードでの処理をさらに効率的にするためだと思われます。'\0'文字で終了しておけば、C言語ならコピーをせずに、1つのバッファへの参照をそのまま使いまわすことができます。 '\0'がない場合には、C言語のコードでは、C言語の文字列の仕様に従うために、一度コピーしなければいけません。

パケットサイズ

多くのコードによると、最大パケットサイズは8 * 1024 バイト (8K) です。 パケットの実際の長さは、ヘッダ部にエンコードされます。


パケットのヘッダ

サーバからコンテナに送信されるパケットは、0x1234で始まります。 コンテナからサーバに送信されるパケットは、ABで始まります (つまり、ASCIIコードのAの直後にASCIIコードのBがきます)。 この最初の2バイトの直後に、送信データの長さの整数がきます(この前に説明したようにエンコードされています)。 つまり、論理的な最大データサイズは2^16ですが、実際の最大値は8Kに制限されています。

パケットフォーマット (サーバ->コンテナ)
バイト 0 1 2 3 4...(n+3)
内容 0x12 0x34 データ長 (n) データ
パケットフォーマット (コンテナ->サーバ)
バイト 0 1 2 3 4...(n+3)
内容 A B データ長 (n) データ


プレフィックスコード

大部分のパケットでは、送信データの最初の1バイトにメッセージのタイプがエンコードされています。 この例外は、サーバからコンテナに送信されるリクエスト内容のパケットです - これらは標準パケットヘッダ (0x1234の後にパケット長)を付加して送信されますが、その直後にプレフィクスコードはありません(これは私は間違いのように思います)。

Webサーバは、以下のメッセージをServletコンテナに送信することができます。

コード パケットのタイプ 意味
2 Forward Request その直後のデータを用いてリクエスト処理サイクルを開始します。
7 Shutdown Webサーバが、コンテナにシャットダウンするように依頼します。

Servletコンテナは、Webサーバに以下のタイプのメッセージを送信することができます。

コード パケットのタイプ 意味
3 Send Body Chunk ServletコンテナからWebサーバにメッセージボディの一部(チャンク)を送信します(そして、おそらくブラウザに対して送信されます)。
4 Send Headers ServletコンテナからWebサーバに、レスポンスヘッダを送信します(そして、おそらくブラウザに対して送信されます)。
5 End Response レスポンス(さらに、リクエスト処理サイクル)の終了を明示します。
6 Get Body Chunk まだリクエストがすべて転送されていない場合には、さらにデータを要求します。

上記のメッセージは、それぞれ別の内部構造を持っていますので、この後に説明します。



リクエストパケット構造

サーバからコンテナに送信される、タイプ"Forward Request"のメッセージは、以下の通りです。

AJP13_FORWARD_REQUEST :=
    prefix_code      (byte) 0x02 = JK_AJP13_FORWARD_REQUEST
    method           (byte)
    protocol         (string)
    req_uri          (string)
    remote_addr      (string)
    remote_host      (string)
    server_name      (string)
    server_port      (integer)
    is_ssl           (boolean)
    num_headers      (integer)
    request_headers *(req_header_name req_header_value)
    attributes      *(attribut_name attribute_value)
    request_terminator (byte) OxFF

request_headersは以下のような構造です。

req_header_name := 
    sc_req_header_name | (string)  [この解析方法については、この後を参照してください]

sc_req_header_name := 0xA0xx (integer)

req_header_value := (string)

attributesはオプションで、以下のような構造です。

attribute_name := (string)

attribute_value := (string)

"content-length"は極めて重要なヘッダであることに注意する必要があります。なぜなら、このヘッダはコンテナが次のパケットをすぐに探すかどうかを決めるからです。

転送するリクエストの要素の詳細を以下に示します。

request_prefix

すべてのリクエストで、この値は2になります。 詳しくは上記の プレフィックスコード を参照してください。


method

HTTPメソッドを、以下のように1バイトにエンコードしています。

コマンド名 コード
OPTIONS 1
GET 2
HEAD 3
POST 4
PUT 5
DELETE 6
TRACE 7
PROPFIND 8
PROPPATCH 9
MKCOL 10
COPY 11
MOVE 12
LOCK 13
UNLOCK 14
ACL 15
REPORT 16
VERSION-CONTROL 17
CHECKIN 18
CHECKOUT 19
UNCHECKOUT 20
SEARCH 21
MKWORKSPACE 22
UPDATE 23
LABEL 24
MERGE 25
BASELINE_CONTROL 26
MKACTIVITY 27


protocol, req_uri, remote_addr, remote_host, server_name, server_port, is_ssl

これらについては、名前を見れば簡単にわかるでしょう。 これらはすべて必須であり、リクエストごとに送信されます。


ヘッダ

request_headersの構造は以下の通りです。 最初に、ヘッダの数(num_headers)がエンコードされます。 次に、ヘッダ名(req_header_name)と値(req_header_value)の組の集合が続きます。 一般的なヘッダ名は、容量を節約するために整数としてエンコードします。 ヘッダ名が基本ヘッダ(=一般的なヘッダ)のリストにない場合には、普通に(最初に長さがついた文字列として)エンコードされます。 一般的なヘッダ(sc_req_header_name)のリストとそのコードは、以下に示します(大文字・小文字を区別します)。

名前 コードの値 コードの名前
accept 0xA001 SC_REQ_ACCEPT
accept-charset 0xA002 SC_REQ_ACCEPT_CHARSET
accept-encoding 0xA003 SC_REQ_ACCEPT_ENCODING
accept-language 0xA004 SC_REQ_ACCEPT_LANGUAGE
authorization 0xA005 SC_REQ_AUTHORIZATION
connection 0xA006 SC_REQ_CONNECTION
content-type 0xA007 SC_REQ_CONTENT_TYPE
content-length 0xA008 SC_REQ_CONTENT_LENGTH
cookie 0xA009 SC_REQ_COOKIE
cookie2 0xA00A SC_REQ_COOKIE2
host 0xA00B SC_REQ_HOST
pragma 0xA00C SC_REQ_PRAGMA
referer 0xA00D SC_REQ_REFERER
user-agent 0xA00E SC_REQ_USER_AGENT

これを読み込むJava言語のコードでは、まず最初の2バイトの整数を読み込んで、 MSB (Most Significant Byte)が'0xA0'の場合には、 2番目のバイトをヘッダ名の配列に対するインデックスである整数と見なします。 最初の1バイトが'0xA0'でない場合には、2バイトの整数が文字列の長さを表していると見なして、 それを読み込みます。

これは、0x9999 (==0xA000 - 1)より長いヘッダ名が存在しないことを仮定すれば動作しますが、これはやや独断的ですが、きわめて妥当でしょう。(もし、あなたが私のようにcookieの仕様と、どのくらいの長さのヘッダを得ることができるかについて考え始めたとしても、恐れることはありません - というのは、これはヘッダの 名前 の制限であって、ヘッダの の制限ではないからです。 管理できないくらい巨大なヘッダ名についてHTTP仕様が定義されることは、とてもありえません。) (訳注: 0x9999 は、0x9FFF の間違いだと思われる。)

注意: content-lengthヘッダは極めて重要です。 このヘッダが存在して、0以外の値をとる場合には、コンテナはリクエストに(たとえば、POSTリクエストのように)ボディ部があるものとして、ただちにボディ部を取得するために別のパケットをインプットストリームから読み込みます。


必須ではない情報

?が先頭についた属性(例 ?context)のリストのすべては必須ではありません。 それぞれについて、属性のタイプを示す1バイトコードが定義されていて、文字列をこの値に変換します。 これらのヘッダは、(C言語のコードは常に以下の順序で送信するのですが)任意の順序で送信できます。 必須ではない属性のリストの最後を知らせるために、特別な終了コードを送信します。 バイトコードのリストは以下の通りです。

情報 コードの値 備考
context 0x01 現在はまだ実装されていません
servlet_path 0x02 現在はまだ実装されていません
remote_user 0x03
auth_type 0x04
query_string 0x05
jvm_route 0x06
ssl_cert 0x07
ssl_cipher 0x08
ssl_session 0x09
req_attribute 0x0A
terminator 0xFF

contextservlet_pathは、現在はC言語のコードでは設定されませんし、 Java言語のコードの大部分では、これらのフィールドが送信されても完全に無視されます (そして、これらのコードの後に文字列が送信された場合には、実際に失敗します)。 私はこれがバグなのか、未実装の仕様なのか、それとも単に昔のコードの痕跡なのかは知りませんが、 コネクションの両側で実装されていません。

remote_userauth_typeは、どうやらHTTPレベルの認証に対応しているらしく、 リモートのユーザのユーザ名とその本人確認をおこなうために使用した認証のタイプ(例:BASIC認証、ダイジェスト認証)のようです。 なぜパスワードが一緒に送信されないのかは、よくわかりませんが、私はHTTP認証についてはまったくわからないのです。

query_stringssl_certssl_cipherssl_sessionは、HTTPとHTTPSに相当する部分に関するものです。

jvm_routeは、私が理解している限りでは、スティッキ・セッションをサポートするために - つまり複数のサーバで負荷分散している時に、ユーザのセッションとある特定のTomcatインスタンスを関連付けるために使用しているようです。 私は詳しいことについては知りません。

この基本属性のリスト以外にも、他の多くの属性をreq_attributeコード (0x0A)を用いて送信します。 属性名と値を表す文字列のペアは、このコードのすぐ後に送信されます。 環境変数は、このメソッドを用いて渡します。

最後に、すべての属性を送信した後に、属性のターミネータとして、0xFFを送信します。 これは、属性のリストの終了とともに、リクエストパケット全体の終了を知らせます。

サーバは、shutdownパケットも送信することができます。 基本的なセキュリティを保証するために、実際にはコンテナはリクエストが運用しているマシンと同じマシンから送信されてくる場合にのみ、終了します。



レスポンスパケット構造

コンテナがサーバに返信することができるメッセージは以下の通りです。

AJP13_SEND_BODY_CHUNK := 
  prefix_code   3
  chunk_length  (integer)
  chunk        *(byte)


AJP13_SEND_HEADERS :=
  prefix_code       4
  http_status_code  (integer)
  http_status_msg   (string)
  num_headers       (integer)
  response_headers *(res_header_name header_value)

res_header_name := 
    sc_res_header_name | (string)   [この解析方法については、この後を参照してください]

sc_res_header_name := 0xA0 (byte)

header_value := (string)

AJP13_END_RESPONSE :=
  prefix_code       5
  reuse             (boolean)


AJP13_GET_BODY_CHUNK :=
  prefix_code       6
  requested_length  (integer)

詳細は以下の通りです。

Send Body Chunk

チャンクは基本的にバイナリデータで、ブラウザに直接返送されます。


Send Headers

ステータスコードとメッセージは、通常のHTTPの定義に従います (例, "200"と"OK")。 レスポンスヘッダ名は、リクエストヘッダ名と同じ方法でエンコードします。 コードと文字列を区別する方法についての詳しい説明は、 上記 を参照してください。 一般的なヘッダのコードは、以下の通りです。

ヘッダ名 コードの値
Content-Type 0xA001
Content-Language 0xA002
Content-Length 0xA003
Date 0xA004
Last-Modified 0xA005
Location 0xA006
Set-Cookie 0xA007
Set-Cookie2 0xA008
Servlet-Engine 0xA009
Status 0xA00A
WWW-Authenticate 0xA00B

コードや文字列のヘッダ名のすぐ後に、ヘッダの値がエンコードされます。


End Response

このリクエスト処理サイクルの終了を知らせます。 reuseフラグがtrue (==1)の場合には、このTCPコネクションを新しく到着するリクエストを処理するために使用できます。 reuseがfalse (実際のC言語のコードでは1以外の値です)の場合には、 このコネクションをクローズしなければいけません。


Get Body Chunk

(リクエストのボディ部が非常に大きくて、送信された最初のパケット内に収まらなかった場合に)コンテナがリクエストからの追加データを要求します。 サーバは、ボディ部のパケットを、ある程度のデータと一緒に返信します。その中には少なくとも、request_lengthと、最大送信ボディサイズ (XXX)、それにリクエストボディからまだ送信されていないバイト数が含まれています。
ボディ部中にそれ以上データがない場合(すなわち、Servletコンテナがボディ部の最後を越えて読み込もうとした場合)には、サーバは送信データ長が0の"空の"パケットを返信します。



私の疑問点

リクエストヘッダの合計サイズが最大パケットサイズを越えた時に、何が起こるでしょうか? 8K以上の場合には、リクエストヘッダの二番目のパケットを送信する準備がされていません(確かめていませんが、私はレスポンスヘッダについてはうまく処理できると思います)。 私は、リクエストヘッダの初期集合に入っている8K以上のデータを取得する方法が存在するかどうかは知りませんが、おそらく存在するでしょう(長いSSL情報を持った長いCookieと多量の環境変数を組み合わせれば、簡単に8Kを越えるでしょう)。 私は、このような場合にヘッダが送信できるかを試す前に、コネクタが落ちるのではないかと思いますが、確かめたわけではありません。

認証についてはどうなのでしょうか? Webサーバとコンテナの間でコネクションの認証が行われてないように思われます。 これについては、私は潜在的な危険を感じてます。


[訳注: これは鰈崎 義之が翻訳しました。日本語訳に対するコメントがあれば、こちらに送って下さい。(風間氏翻訳のAJPv13.htmlをベースに一部追加・修正)]