原始文件由 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 程式碼,我想它相當容易閱讀。
根據 Gal Shachor 發送給 jakarta-dev 郵件清單的電子郵件,JK(以及因此而來的 ajp13)的原始目標是擴充 mod_jserv 和 ajp12(我只包含與網路伺服器和 servlet 容器之間通訊相關的目標)
isSecure()
和 getScheme()
能在 servlet 容器中正常運作。客戶端憑證和加密組將作為要求屬性提供給 servlet。 ajp13 協定是封包導向的。二進位格式可能基於效能考量而選擇,而非較易讀取的純文字。網路伺服器透過 TCP 連線與 servlet 容器通訊。為了減少建立 socket 的昂貴程序,網路伺服器會嘗試維持與 servlet 容器的持續 TCP 連線,並重複使用連線進行多個要求/回應循環。
一旦連線指定給特定要求,在要求處理循環終止之前,它不會用於其他任何要求。換句話說,要求不會透過連線多工處理。雖然這會導致一次開啟更多連線,但它讓連線兩端的程式碼變得更簡單。
網路伺服器開啟與 servlet 容器的連線後,連線可以處於下列狀態之一
一旦連線指定用於處理特定要求,就會以高度濃縮的形式透過連線傳送基本要求資訊(例如 HTTP 標頭等)(例如,常見字串會編碼為整數)。該格式的詳細資訊如下方的「要求封包結構」。如果要求中有主體(內容長度 > 0),則會在緊接在後方的個別封包中傳送。
此時,servlet 容器應已準備好開始處理要求。在處理時,它可以將下列訊息傳送回網路伺服器
每則訊息都附帶不同格式的資料封包。有關詳細資訊,請參閱下方的「回應封包結構」。
此通訊協定承襲了 XDR 的一些特點,但在許多方面有所不同(例如,沒有 4 位元組對齊)。
AJP13 對所有資料類型使用網路位元組順序。
此通訊協定中有四種資料類型:位元組、布林值、整數和字串。
strlen
。這在 Java 端會造成一些混淆,其中充斥著奇怪的自動遞增陳述式來略過這些終止符。我相信這樣做的原因是為了讓 C 程式碼在讀取 Servlet 容器傳回的字串時能額外提高效率 -- 使用終止符 \0,C 程式碼可以在不複製的情況下傳遞對單一緩衝區的參照。如果沒有 \0,C 程式碼必須複製內容才能取得字串概念。請注意,大小 -1 (65535) 表示空字串,且在這種情況下,沒有資料會出現在長度之後。根據大部分程式碼,最大封包大小為 8 * 1024 位元組 (8K)。封包的實際長度編碼在標頭中。
從伺服器傳送至容器的封包以 0x1234
開頭。從容器傳送至伺服器的封包以 AB
開頭(這是 A 的 ASCII 碼,後面接著 B 的 ASCII 碼)。在頭兩個位元組之後,有一個整數(如上所述編碼)表示酬載長度。儘管這可能表示最大酬載可以大到 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 伺服器可以將下列訊息傳送至 Servlet 容器
代碼 | 封包類型 | 意義 |
---|---|---|
2 | 轉發請求 | 使用下列資料開始請求處理週期 |
7 | 關閉 | Web 伺服器要求容器關閉自身。 |
8 | Ping | Web 伺服器要求容器取得控制權 (安全登入階段)。 |
10 | CPing | Web 伺服器要求容器使用 CPong 快速回應。 |
無 | 資料 | 大小 (2 個位元組) 和對應的主體資料。 |
為了確保一些基本安全性,容器只會在請求來自其所寄存的相同機器時實際執行 關閉
。
第一個 資料
封包會在 Web 伺服器傳送 轉發請求
後立即傳送。
Servlet 容器可以將下列訊息類型傳送至 Web 伺服器
代碼 | 封包類型 | 意義 |
---|---|---|
3 | 傳送主體區塊 | 從 Servlet 容器傳送主體區塊至 Web 伺服器 (並假設傳送至瀏覽器)。 |
4 | 傳送標頭 | 從 Servlet 容器傳送回應標頭至 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」,因為它會決定容器是否立即尋找另一個封包。
Forward Request 元素的詳細說明。
모든 요청의 경우 2입니다. 다른 접두어 코드에 대한 자세한 내용은 위를 참조하세요.
HTTP 메서드가 단일 바이트로 인코딩됨
명령 이름 | 代碼 |
---|---|
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 코드는 처음 두 바이트 정수를 가져오고 가장 중요한 바이트에 '0xA0'
이 있는 경우 두 번째 바이트의 정수를 헤더 이름 배열의 인덱스로 사용합니다. 첫 번째 바이트가 '0xA0'이 아닌 경우 두 바이트 정수를 문자열의 길이로 가정하고 읽습니다.
이는 헤더 이름의 길이가 0x9FFF(==0xA000 - 1)보다 크지 않다는 가정 하에 작동합니다. 이는 완전히 타당하지만 다소 임의적입니다. (저와 같이 쿠키 사양과 헤더의 길이에 대해 생각하기 시작했다면 걱정하지 마세요. 이 제한은 헤더 이름에 적용되며 헤더 값에는 적용되지 않습니다. 엄청나게 큰 헤더 이름이 HTTP 사양에 곧 나타날 가능성은 거의 없습니다.)
注意:content-length
標頭非常重要。如果存在且非零,容器會假設要求有主體(例如 POST 要求),並立即從輸入串流讀取一個獨立封包以取得該主體。
以 ?
為前綴的屬性(例如 ?context
)都是選用的。對於每個屬性,有一個單一位元組碼表示屬性類型,然後用字串提供其值。它們可以按任何順序傳送(儘管 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 |
C 程式碼目前未設定 context
和 servlet_path
,而且大部分 Java 程式碼完全忽略這些欄位傳送的任何內容(而且如果在這些碼之後傳送字串,其中一些程式碼實際上會中斷)。我不知道這是一個錯誤、未實作的功能還是只是殘留程式碼,但它在連線的兩端都遺失了。
remote_user
和 auth_type
可能指的是 HTTP 層級驗證,並傳達遠端使用者的使用者名稱和用於建立其身分的驗證類型(例如 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」和「確定」)。回應標頭名稱的編碼方式與要求標頭名稱相同。請參閱上方有關如何將代碼與字串區分的詳細資料。常見標頭的代碼為
이름 | 코드 값 |
---|---|
內容類型 | 0xA001 |
內容語言 | 0xA002 |
內容長度 | 0xA003 |
日期 | 0xA004 |
上次修改時間 | 0xA005 |
位置 | 0xA006 |
設定 Cookie | 0xA007 |
設定 Cookie2 | 0xA008 |
Servlet 引擎 | 0xA009 |
狀態 | 0xA00A |
WWW 驗證 | 0xA00B |
在代碼或字串標頭名稱之後,標頭值會立即編碼。
表示此要求處理週期的結束。如果 reuse
旗標為 true(實際 C 程式碼中為 0 以外的任何值),則此 TCP 連線現在可用于處理新的傳入要求。如果 reuse
為 false(==0),則應關閉連線。
容器要求要求提供更多資料(如果主體太大而無法放入傳送的第一個封包,或當要求被區塊化時)。伺服器會傳送一個主體封包,其中資料量為 request_length
、最大傳送主體大小(8186(8 KB - 6))和實際上從要求主體傳送的位元組數量的最小值。
如果主體中沒有更多資料(即 Servlet 容器嘗試讀取超過主體的結尾),則伺服器會傳送一個「空」封包,也就是一個有效負載長度為 0 的主體封包。(0x12,0x34,0x00,0x00)
如果要求標頭 > 最大封包大小,會發生什麼事?如果超過 8K,沒有提供傳送第二個封包要求標頭的規定(我認為這對回應標頭處理正確,但我並不確定)。我不知道是否有辦法將超過 8K 的資料放入那組初始要求標頭中,但我敢打賭有(結合長 Cookie、長 SSL 資訊和許多環境變數,你應該可以輕易達到 8K)。我認為在這種情況下,連接器會在嘗試傳送任何標頭之前就失敗,但我並不確定。
驗證如何?網際網路伺服器和容器之間的連線似乎沒有任何驗證。這對我來說似乎很危險。