元のドキュメントは Dan Milstein 氏(
これは、Apache JServ プロトコルバージョン 1.3 (以降 ajp13) について説明しています。現在、プロトコルの動作に関するドキュメントは存在しないようです。このドキュメントは、JK のメンテナや、プロトコルをどこか(例えば jakarta 4.x)に移植したい人のために、その状況を改善しようとする試みです。
元のドキュメントは Dan Milstein 氏(
これは、Apache JServ プロトコルバージョン 1.3 (以降 ajp13) について説明しています。現在、プロトコルの動作に関するドキュメントは存在しないようです。このドキュメントは、JK のメンテナや、プロトコルをどこか(例えば jakarta 4.x)に移植したい人のために、その状況を改善しようとする試みです。
私はこのプロトコルの設計者ではありません。元の設計者は Gal Shachor 氏だと考えています。このドキュメントの内容はすべて、tomcat 3.x のコードで見つけた実際の実装から派生したものです。これが役に立つことを願っていますが、完全な正確性を保証することはできません。また、なぜ特定の設計上の決定がなされたのかはわかりません。可能な限り、特定の選択の理由を推測してみましたが、それはあくまでも私の推測です。一般的に、Shachor 氏が書いた C コードは非常にきれいで理解しやすいものです(ほとんどドキュメント化されていませんが)。Java コードはクリーンアップしたので、 reasonably 可読性が高いと思います。
Gal Shachor 氏から jakarta-dev メーリングリストに送られたメールによると、JK (そして ajp13) の当初の目標は、mod_jserv と ajp12 を拡張することでした (Web サーバーとサーブレットコンテナ間の通信に関連する目標のみを記載しています)。
isSecure()
と getScheme()
がサーブレットコンテナ内で正しく機能するようにする。クライアント証明書と暗号スイートは、リクエスト属性としてサーブレットで利用できるようになります。ajp13 プロトコルはパケット指向です。パフォーマンス上の理由から、より可読性の高いプレーンテキストではなく、バイナリ形式が選択されたと考えられます。Web サーバーは、TCP 接続を介してサーブレットコンテナと通信します。ソケット作成というコストのかかるプロセスを削減するために、Web サーバーはサーブレットコンテナへの永続的な TCP 接続を維持し、複数のリクエスト/レスポンスサイクルで接続を再利用しようとします。
接続が特定のリクエストに割り当てられると、リクエスト処理サイクルが終了するまで、他のリクエストには使用されません。つまり、リクエストは接続上で多重化されません。これにより、接続の両端のコードが大幅に簡素化されますが、一度に開かれる接続の数が増加します。
Web サーバーがサーブレットコンテナへの接続を開くと、接続は以下のいずれかの状態になります。
接続が特定のリクエストを処理するために割り当てられると、基本的なリクエスト情報(HTTP ヘッダーなど)は、非常に圧縮された形式(例えば、一般的な文字列は整数としてエンコードされる)で接続を介して送信されます。その形式の詳細は、以下のリクエストパケット構造に記載されています。リクエストに本文がある場合 (content-length > 0)、それは直後に別のパケットで送信されます。
この時点で、サーブレットコンテナはリクエストの処理を開始できる状態になっているはずです。処理中に、サーブレットコンテナは Web サーバーに以下のメッセージを送信できます。
各メッセージには、異なる形式のパケットデータが付属しています。詳細は、以下のレスポンスパケット構造を参照してください。
このプロトコルには XDR の影響が多少ありますが、多くの点で異なります (例えば、4 バイトアライメントがありません)。
AJP13 は、すべてのデータ型にネットワークバイトオーダーを使用します。
プロトコルには、バイト、ブール値、整数、文字列の 4 つのデータ型があります。
strlen
のようです。これは Java 側では少し混乱します。Java 側では、これらの終端文字をスキップするために、奇妙な自動インクリメント文が散らばっています。これが行われた理由は、サーブレットコンテナが送り返す文字列を読み取るときに C コードを非常に効率的にするためだと考えています。終端文字 \0 を使用すると、C コードはコピーすることなく、単一バッファへの参照を渡すことができます。\0 がない場合、C コードは文字列の概念を得るために、内容をコピーする必要があります。サイズ -1 (65535) はヌル文字列を示し、この場合は長さの後にデータが続きません。多くのコードによると、最大パケットサイズは 8 * 1024 バイト (8K) です。パケットの実際の長さはヘッダーにエンコードされています。
サーバーからコンテナに送信されるパケットは 0x1234
で始まります。コンテナからサーバーに送信されるパケットは AB
で始まります (これは A の ASCII コードと B の ASCII コードが続きます)。最初の 2 バイトの後には、ペイロードの長さを示す整数 (上記のようにエンコードされます) があります。これは、最大ペイロードが 2^16 になる可能性があることを示唆しているように見えますが、実際にはコードでは最大値を 8K に設定しています。
パケット形式 (サーバー->コンテナ) | |||||
---|---|---|---|---|---|
バイト | 0 | 1 | 2 | 3 | 4...(n+3) |
内容 | 0x12 | 0x34 | データ長 (n) | データ |
パケット形式 (コンテナ->サーバー) | |||||
---|---|---|---|---|---|
バイト | 0 | 1 | 2 | 3 | 4...(n+3) |
内容 | A | B | データ長 (n) | データ |
ほとんどのパケットでは、ペイロードの最初のバイトがメッセージのタイプをエンコードします。例外は、サーバーからコンテナに送信されるリクエスト本文パケットです。これらは標準のパケットヘッダー (0x1234 とパケットの長さ) で送信されますが、その後にプレフィックスコードはありません (これは私には間違いのように思えます)。
Web サーバーは、サーブレットコンテナに以下のメッセージを送信できます。
コード | パケットのタイプ | 意味 |
---|---|---|
2 | リクエストの転送 | 以下のデータでリクエスト処理サイクルを開始します。 |
7 | シャットダウン | Web サーバーはコンテナにシャットダウンを要求します。 |
8 | ピング | Web サーバーはコンテナに制御を要求します(セキュアログインフェーズ)。 |
10 | CPing | Web サーバーはコンテナに CPong で迅速に応答するように要求します。 |
なし | データ | サイズ (2 バイト) と対応する本文データ。 |
基本的なセキュリティを確保するため、コンテナはリクエストがホストされているマシンと同じマシンからのものである場合にのみ、実際に シャットダウン
を実行します。
最初の データ
パケットは、Web サーバーによって リクエストの転送
の直後に送信されます。
サーブレットコンテナは、Web サーバーに以下のタイプのメッセージを送信できます。
コード | パケットのタイプ | 意味 |
---|---|---|
3 | 本文チャンクの送信 | サーブレットコンテナから Web サーバー (そしておそらくブラウザ) に本文のチャンクを送信します。 |
4 | ヘッダーの送信 | サーブレットコンテナから Web サーバー (そしておそらくブラウザ) にレスポンスヘッダーを送信します。 |
5 | レスポンスの終了 | レスポンス (したがってリクエスト処理サイクル) の終了をマークします。 |
6 | 本文チャンクの取得 | リクエストデータがすべて転送されていない場合は、さらにデータを取得します。 |
9 | CPong 応答 | CPing リクエストへの応答 |
上記の各メッセージは、以下に詳述するように、異なる内部構造を持っています。
サーバーからコンテナへの「リクエストの転送」タイプのメッセージの場合
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) [see below for how this is parsed]
sc_req_header_name := 0xA0xx (integer)
req_header_value := (string)
attributes
はオプションで、以下の構造を持っています。
attribute_name := sc_a_name | (sc_a_req_attribute string)
attribute_value := (string)
最も重要なヘッダーは "content-length" であることに注意してください。これは、コンテナが直後に別のパケットを探すかどうかを決定するためです。
リクエストの転送の要素の詳細な説明。
すべてのリクエストで、これは 2 になります。その他の プレフィックスコード については、上記を参照してください。
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 |
これらはすべて、かなり自明です。これらはすべて必須であり、すべてのリクエストで送信されます。
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バイト整数を読み込み、最上位バイトに`0xA0`がある場合、2番目のバイトの整数をヘッダー名の配列へのインデックスとして使用します。最初のバイトが`0xA0`でない場合、2バイト整数は読み込む文字列の長さであるとみなします。
これは、ヘッダー名が0x9FFF(== 0xA000 - 1)より長くないという前提で機能します。これは完全に合理的ですが、やや恣意的です。(もしあなたが私のように、ここでCookieの仕様とヘッダーの長さについて考え始めたとしても、心配しないでください - この制限はヘッダーの**名前**に適用され、ヘッダーの**値**には適用されません。HTTP仕様に管理不能なほど巨大なヘッダー名が出現する可能性は低いと思われます)。
**注意:** `content-length`ヘッダーは非常に重要です。存在し、ゼロ以外の場合、コンテナはリクエストに本文(POSTリクエストなど)があるとみなし、入力ストリームから別のパケットをすぐに読み取ってその本文を取得します。
`?`で始まる属性(例:`?context`)はすべてオプションです。それぞれについて、属性のタイプを示す1バイトのコードと、その値を示す文字列があります。これらは任意の順序で送信できます(ただし、Cコードは常に以下の順序で送信します)。オプション属性のリストの終わりを示す特別な終了コードが送信されます。バイトコードのリストは次のとおりです。
情報 | コード値 | 備考 |
---|---|---|
?context | 0x01 | 現在実装されていません |
?servlet_path | 0x02 | 現在実装されていません |
?remote_user | 0x03 | |
?auth_type | 0x04 | |
?query_string | 0x05 | |
?route | 0x06 | |
?ssl_cert | 0x07 | |
?ssl_cipher | 0x08 | |
?ssl_session | 0x09 | |
?req_attribute | 0x0A | 名前(属性の名前が続く) |
?ssl_key_size | 0x0B | |
?secret | 0x0C | |
?stored_method | 0x0D | |
are_done | 0xFF | request_terminator |
`context`と`servlet_path`は現在Cコードによって設定されておらず、ほとんどのJavaコードはこれらのフィールドに送信されたものを完全に無視します(そして、これらのコードの後に文字列が送信されると、一部のコードは実際に壊れます)。これがバグなのか、実装されていない機能なのか、単なる痕跡コードなのかわかりませんが、接続の両側で欠落しています。
`remote_user`と`auth_type`は、HTTPレベルの認証を参照し、リモートユーザーのユーザー名と、IDを確立するために使用される認証のタイプ(例:Basic、Digest)を伝達すると考えられます。パスワードも送信されない理由は明確ではありませんが、HTTP認証を熟知しているわけではありません。
`query_string`、`ssl_cert`、`ssl_cipher`、`ssl_session`は、HTTPおよびHTTPSの対応する部分を参照します。
私の理解では、`route`はスティッキーセッション(複数のロードバランシングサーバーが存在する場合に、ユーザーのセッションを特定のTomcatインスタンスに関連付ける)をサポートするために使用されます。詳細はわかりません。
この基本属性のリストに加えて、`req_attribute`コード(0x0A)を介して任意の数の他の属性を送信できます。属性名と値を表す文字列のペアは、そのコードの各インスタンスの直後に送信されます。環境変数は、このメソッドを介して渡されます。
最後に、すべての属性が送信された後、属性ターミネータ0xFFが送信されます。これは、属性リストの終わりと、リクエストパケットの終わりを示します。
コンテナがサーバーに返送できるメッセージ用。
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) [see below for how this is parsed]
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)
詳細
チャンクは基本的にバイナリデータであり、ブラウザに直接送り返されます。
ステータスコードとメッセージは、通常の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 |
コードまたは文字列ヘッダー名の後、ヘッダー値がすぐにエンコードされます。
このリクエスト処理サイクルの終わりを示します。 `reuse`フラグがtrue(実際のCコードでは0以外)の場合、このTCP接続を使用して新しい着信リクエストを処理できるようになりました。 `reuse`がfalse(== 0)の場合、接続を閉じる必要があります。
コンテナはリクエストからより多くのデータを要求します(本文が大きすぎて最初の送信パケットに収まらない場合、またはリクエストがチャンクされている場合)。サーバーは、`request_length`、最大送信本文サイズ(8186(8 KB - 6))、およびリクエスト本文から実際に送信する必要がある残りのバイト数の最小値であるデータ量を含む本文パケットを送り返します。
本文にデータがなくなった場合(つまり、サーブレットコンテナが本文の終わりを超えて読み取ろうとしている場合)、サーバーはペイロード長が0の「空の」パケットである本文パケットを送り返します。(0x12,0x34,0x00,0x00)
リクエストヘッダーが最大パケットサイズを超えた場合はどうなりますか? 8Kを超えるリクエストヘッダーがある場合に、2番目のリクエストヘッダーパケットを送信する規定はありません(レスポンスヘッダーの場合は正しく処理されていると思いますが、確信はありません)。 8Kを超えるデータをリクエストヘッダーの初期セットに取得する方法があるかどうかはわかりませんが、あると確信しています(長いCookieと長いssl情報を多くの環境変数と組み合わせると、簡単に8Kに到達するはずです)。この場合、コネクタはヘッダーを送信しようとせずに失敗すると思いますが、確信はありません。
認証はどうですか? Webサーバーとコンテナ間の接続の認証はないようです。これは潜在的に危険なように思われます。