原始文档由 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(我只包括与 Web 服务器和 servlet 容器之间的通信相关的目标)
isSecure()
和 getScheme()
可以在 servlet 容器中正常运行。客户端证书和密码套件将作为请求属性提供给 servlet。 ajp13 协议是面向数据包的。出于性能原因,可能选择二进制格式而不是更易读的纯文本。Web 服务器通过 TCP 连接与 servlet 容器通信。为了减少昂贵的套接字创建过程,Web 服务器将尝试保持与 servlet 容器的持久 TCP 连接,并对多个请求/响应周期重复使用连接。
一旦连接被分配给特定请求,在请求处理周期终止之前,它将不会用于任何其他请求。换句话说,请求不会通过连接多路复用。这使得连接两端的代码更加简单,尽管它确实导致一次打开更多连接。
一旦 Web 服务器打开与 servlet 容器的连接,该连接可以处于以下状态之一
一旦连接被分配来处理特定请求,基本请求信息(例如 HTTP 头等)将以高度浓缩的形式通过连接发送(例如,通用字符串被编码为整数)。该格式的详细信息如下所示在请求数据包结构中。如果请求有正文(内容长度 > 0),则在紧随其后的单独数据包中发送该正文。
此时,servlet 容器可能已准备好开始处理请求。在处理过程中,它可以将以下消息发回 Web 服务器
每条消息都附带一个格式不同的数据包。有关详细信息,请参见下面的响应数据包结构。
此协议继承了 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 字节)和相应的主体数据。 |
为了确保一些基本安全性,只有当请求来自其托管的同一台机器时,容器才会实际执行 Shutdown
。
第一个 Data
数据包在 Web 服务器发出 Forward Request
后立即发送。
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”,因为它决定容器是否立即查找另一个数据包。
转发请求中元素的详细说明。
对于所有请求,此值为 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 |
取消签出 | 20 |
搜索 | 21 |
创建工作区 | 22 |
更新 | 23 |
标签 | 24 |
合并 | 25 |
基准控制 | 26 |
创建活动 | 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),这是完全合理的,尽管有些武断。(如果你像我一样,开始考虑这里的 cookie 规范以及标头可以有多长,请不要担心——此限制适用于标头名称,而不是标头值。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”和“OK”)。响应头名称的编码方式与请求头名称的编码方式相同。有关如何将代码与字符串区分开的详细信息,请参见上方。常见标头的代码为
名称 | 代码值 |
---|---|
Content-Type | 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)。我认为在这种情况下,连接器会在尝试发送任何标头之前失败,但我并不确定。
身份验证呢?Web 服务器和容器之间的连接似乎没有任何身份验证。这让我觉得有潜在危险。