原始文档由 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)的最初目标是通过(我只包含与 Web 服务器和 Servlet 容器之间通信相关的目标)扩展 mod_jserv 和 ajp12:
isSecure()
和 getScheme()
能够在 servlet 容器内正常工作。客户端证书和密码套件将作为请求属性提供给 servlet。ajp13 协议是面向数据包的。选择二进制格式而非更易读的纯文本,大概是出于性能原因。Web 服务器通过 TCP 连接与 Servlet 容器通信。为了减少套接字创建的昂贵过程,Web 服务器将尝试维护到 Servlet 容器的持久 TCP 连接,并为多个请求/响应周期重用连接。
一旦一个连接被分配给一个特定的请求,在请求处理周期结束之前,它将不会被用于其他任何请求。换句话说,请求不会在连接上多路复用。这使得连接两端的代码大大简化,尽管它确实会导致同时打开更多的连接。
一旦 Web 服务器打开与 Servlet 容器的连接,该连接可以处于以下状态之一:
一旦连接被分配用于处理特定请求,基本请求信息(例如 HTTP 头等)将以高度压缩的形式通过连接发送(例如,常用字符串被编码为整数)。有关该格式的详细信息,请参见下面的请求数据包结构。如果请求有正文(content-length > 0),则会在其后立即以单独的数据包发送。
此时,Servlet 容器应已准备好开始处理请求。在此过程中,它可以向 Web 服务器发送以下消息:
每条消息都附带一个格式不同的数据包。有关详细信息,请参阅下面的响应数据包结构。
这个协议有点 XDR 传统,但在很多方面有所不同(例如,没有 4 字节对齐)。
AJP13 对所有数据类型都使用网络字节序。
协议中有四种数据类型:字节、布尔值、整数和字符串。
strlen
。这在 Java 端有点令人困惑,那里充斥着奇怪的自增语句来跳过这些终止符。我相信这样做的原因是允许 C 代码在读取 servlet 容器发回的字符串时效率更高——有了终止符 \0 字符,C 代码可以传递单个缓冲区中的引用,而无需复制。如果缺少 \0,C 代码就必须复制内容才能获得其对字符串的概念。请注意,大小为 -1 (65535) 表示空字符串,并且在这种情况下,长度后不跟数据。根据许多代码,最大数据包大小为 8 * 1024 字节(8K)。数据包的实际长度编码在头部。
从服务器发送到容器的数据包以 0x1234
开头。从容器发送到服务器的数据包以 AB
开头(即 ASCII 码 A 后跟 ASCII 码 B)。在这两个字节之后,是一个整数(如上所述编码),表示有效载荷的长度。尽管这可能表明最大有效载荷可以达到 2^16,但实际上,代码将最大值设置为 8K。
数据包格式 (服务器->容器) | |||||
---|---|---|---|---|---|
字节 (Byte) | 0 | 1 | 2 | 3 | 4...(n+3) |
内容 | 0x12 | 0x34 | 数据长度 (n) | 数据 |
数据包格式 (容器->服务器) | |||||
---|---|---|---|---|---|
字节 (Byte) | 0 | 1 | 2 | 3 | 4...(n+3) |
内容 | A | B | 数据长度 (n) | 数据 |
对于大多数数据包,有效载荷的第一个字节编码消息的类型。例外情况是服务器发送到容器的请求正文数据包——它们带有标准数据包头部(0x1234,然后是数据包长度),但此后没有任何前缀代码(这对我来说似乎是个错误)。
Web 服务器可以向 Servlet 容器发送以下消息:
代码 | 数据包类型 | 含义 |
---|---|---|
2 | 转发请求 (Forward Request) | 使用以下数据开始请求处理周期 |
7 | 关机 (Shutdown) | Web 服务器要求容器自行关机。 |
8 | Ping | Web 服务器要求容器获取控制权(安全登录阶段)。 |
10 | CPing | Web 服务器要求容器快速响应 CPong。 |
无 | 数据 | 大小(2 字节)和相应的正文数据。 |
为确保一些基本安全性,容器只会在请求来自其托管的同一台机器时才实际执行 Shutdown
。
第一个 Data
数据包由 Web 服务器在 Forward Request
后立即发送。
Servlet 容器可以向 Web 服务器发送以下类型的消息:
代码 | 数据包类型 | 含义 |
---|---|---|
3 | 发送正文块 (Send Body Chunk) | 将正文块从 Servlet 容器发送到 Web 服务器(可能进而发送到浏览器)。 |
4 | 发送头信息 (Send Headers) | 将响应头信息从 Servlet 容器发送到 Web 服务器(可能进而发送到浏览器)。 |
5 | 结束响应 (End Response) | 标记响应(以及请求处理周期)的结束。 |
6 | 获取正文块 (Get Body Chunk) | 如果请求尚未完全传输,则获取更多数据。 |
9 | CPong 回复 (CPong Reply) | 对 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 |
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 | 读取此内容的 Java 代码获取前两个字节的整数,如果它在最高有效字节中看到 '0xA0' ,则将第二个字节中的整数用作头名称数组的索引。如果第一个字节不是 '0xA0',则假定该两字节整数是一个字符串的长度,然后读取该字符串。 |
这基于一个假设:没有头名称的长度会大于 0x9FFF (==0xA000 - 1),这是完全合理的,尽管有些武断。(如果你和我一样,在这里开始考虑 cookie 规范以及头可以有多长,请不要担心——这个限制是针对头名称而不是头值。看来在 HTTP 规范中短期内不太可能出现无法管理的巨大头名称)。
注意:content-length
头极为重要。如果它存在且非零,则容器假定请求具有正文(例如 POST 请求),并立即从输入流中读取一个单独的数据包以获取该正文。
属性 (Attributes)
?
前缀的属性(例如 ?context
)都是可选的。对于每个属性,都有一个单字节代码来指示属性的类型,然后是一个字符串来给出其值。它们可以以任何顺序发送(尽管 C 代码总是按下面列出的顺序发送)。发送一个特殊的终止代码来表示可选属性列表的结束。字节代码列表如下:信息
代码值 | 备注 | ?context |
---|---|---|
0x01 | 当前未实现 | ?servlet_path |
0x02 | ?remote_user | ?servlet_path |
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 级别的认证,并传输远程用户的用户名以及用于建立其身份的认证类型(例如 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
名称 | 代码值 |
---|---|
Content-Language | 0xA001 |
Content-Length | 0xA002 |
Date | 0xA003 |
Last-Modified | 0xA004 |
Location | 0xA005 |
Set-Cookie | 0xA006 |
Set-Cookie2 | 0xA007 |
Servlet-Engine | 0xA008 |
Status | 0xA009 |
WWW-Authenticate | 0xA00A |
在代码或字符串头名称之后,头值立即被编码。 | 0xA00B |
标志着此请求处理周期的结束。如果 reuse
标志为 true(实际 C 代码中任何非 0 值),则此 TCP 连接现在可用于处理新的传入请求。如果 reuse
为 false (==0),则应关闭连接。
容器请求更多请求数据(如果正文太大无法放入第一个发送的数据包中,或者当请求被分块时)。服务器将发送一个正文数据包,其中包含的数据量为 request_length
、最大发送正文大小(8186 (8 KB - 6))以及请求正文实际剩余发送字节数中的最小值。
如果正文中没有更多数据(即 servlet 容器尝试读取超出正文末尾),服务器将返回一个“空”数据包,这是一个有效载荷长度为 0 的正文数据包。(0x12,0x34,0x00,0x00)
我的疑问
关于认证?Web 服务器和容器之间的连接似乎没有任何认证。这在我看来可能很危险。
版权所有 © 1999-2024, Apache 软件基金会