AJPv13

引言

原始文档由 Dan Milstein 撰写,danmil@shore.net于2000年12月。本文档由 XML 文件生成,以便更轻松地集成到 Tomcat 文档中。

本文描述了 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_jservajp12

  • 提高性能(特别是速度)。
  • 增加对 SSL 的支持,以便 isSecure()getScheme() 能够在 servlet 容器内正常工作。客户端证书和密码套件将作为请求属性提供给 servlet。

协议概述

ajp13 协议是面向数据包的。选择二进制格式而非更易读的纯文本,大概是出于性能原因。Web 服务器通过 TCP 连接与 Servlet 容器通信。为了减少套接字创建的昂贵过程,Web 服务器将尝试维护到 Servlet 容器的持久 TCP 连接,并为多个请求/响应周期重用连接。

一旦一个连接被分配给一个特定的请求,在请求处理周期结束之前,它将不会被用于其他任何请求。换句话说,请求不会在连接上多路复用。这使得连接两端的代码大大简化,尽管它确实会导致同时打开更多的连接。

一旦 Web 服务器打开与 Servlet 容器的连接,该连接可以处于以下状态之一:

  • 空闲
    此连接没有正在处理的请求。
  • 已分配
    此连接正在处理一个特定的请求。

一旦连接被分配用于处理特定请求,基本请求信息(例如 HTTP 头等)将以高度压缩的形式通过连接发送(例如,常用字符串被编码为整数)。有关该格式的详细信息,请参见下面的请求数据包结构。如果请求有正文(content-length > 0),则会在其后立即以单独的数据包发送。

此时,Servlet 容器应已准备好开始处理请求。在此过程中,它可以向 Web 服务器发送以下消息:

  • 发送头信息 (SEND_HEADERS)
    将一组头信息发送回浏览器。
  • 发送正文块 (SEND_BODY_CHUNK)
    将正文数据块发送回浏览器。
  • 获取正文块 (GET_BODY_CHUNK)
    如果请求尚未完全传输,则获取更多数据。这是必要的,因为数据包具有固定的最大大小,并且请求正文(例如上传文件)可以包含任意数量的数据。(注意:这与 HTTP 分块传输无关)。
  • 结束响应 (END_RESPONSE)
    结束请求处理周期。

每条消息都附带一个格式不同的数据包。有关详细信息,请参阅下面的响应数据包结构。

基本数据包结构

这个协议有点 XDR 传统,但在很多方面有所不同(例如,没有 4 字节对齐)。

AJP13 对所有数据类型都使用网络字节序。

协议中有四种数据类型:字节、布尔值、整数和字符串。

字节 (Byte)
一个单字节。
布尔值 (Boolean)
一个单字节,1 = 真,0 = 假。使用其他非零值作为真(即 C 风格)可能在某些地方有效,但在其他地方则无效。
整数 (Integer)
一个介于 0 到 2^16 (32768) 之间的数字。以 2 字节存储,高位字节在前。
字符串 (String)
可变大小的字符串(长度上限为 2^16)。编码时,长度先打包成两个字节,然后是字符串(包括终止符 '\0')。请注意,编码长度包括末尾的 '\0'——它类似于 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”,因为它决定了容器是否会立即寻找另一个数据包。

“转发请求”元素的详细描述。

请求前缀 (request_prefix)

对于所有请求,此值将为 2。有关其他 前缀代码 的详细信息,请参见上文。

方法 (method)

HTTP 方法,编码为单个字节

命令名称代码
OPTIONS1
GET2
HEAD3
POST4
PUT5
DELETE6
TRACE7
PROPFIND8
PROPPATCH9
MKCOL10
COPY11
MOVE12
LOCK13
UNLOCK14
ACL15
REPORT16
VERSION-CONTROL17
CHECKIN18
CHECKOUT19
UNCHECKOUT20
SEARCH21
MKWORKSPACE22
UPDATE23
LABEL24
MERGE25
BASELINE_CONTROL26
MKACTIVITY27

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

这些都相当一目了然。它们都是必需的,并将随每个请求发送。

头信息 (Headers)

request_headers 的结构如下:首先,编码头信息的数量 num_headers。然后,一系列头信息名称 req_header_name / 值 req_header_value 对随之而来。常见的头信息名称编码为整数,以节省空间。如果头信息名称不在基本头信息列表中,则正常编码(作为字符串,前缀长度)。常见头信息 sc_req_header_name 及其代码列表如下(所有都区分大小写):

名称代码值代码名称
accept0xA001SC_REQ_ACCEPT
accept-charset0xA002SC_REQ_ACCEPT_CHARSET
accept-encoding0xA003SC_REQ_ACCEPT_ENCODING
accept-language0xA004SC_REQ_ACCEPT_LANGUAGE
authorization0xA005SC_REQ_AUTHORIZATION
connection0xA006SC_REQ_CONNECTION
content-type0xA007SC_REQ_CONTENT_TYPE
content-length0xA008SC_REQ_CONTENT_LENGTH
cookie0xA009SC_REQ_COOKIE
cookie20xA00ASC_REQ_COOKIE2
host0xA00BSC_REQ_HOST
pragma0xA00CSC_REQ_PRAGMA
referer0xA00DSC_REQ_REFERER
user-agent0xA00E读取此内容的 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
0x0Dare_done
0xFF请求终止符 (request_terminator)contextservlet_path 当前未由 C 代码设置,并且大部分 Java 代码完全忽略这些字段发送过来的任何内容(其中一些甚至在这些代码之后发送字符串时会出错)。我不知道这是错误、未实现的功能还是仅仅是残余代码,但它在连接的两端都缺失。

remote_userauth_type 大概指的是 HTTP 级别的认证,并传输远程用户的用户名以及用于建立其身份的认证类型(例如 Basic, Digest)。我不清楚为什么密码没有一起发送,但我对 HTTP 认证了解不深。

query_stringssl_certssl_cipherssl_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)

该块基本上是二进制数据,并直接发送回浏览器。

发送正文块 (Send Body Chunk)

状态码和消息是通常的 HTTP 内容(例如“200”和“OK”)。响应头名称的编码方式与请求头名称相同。有关代码如何与字符串区分开来的详细信息,请参阅上文。常见头的代码是:

发送头信息 (Send Headers)

Content-Type

名称代码值
Content-Language0xA001
Content-Length0xA002
Date0xA003
Last-Modified0xA004
Location0xA005
Set-Cookie0xA006
Set-Cookie20xA007
Servlet-Engine0xA008
Status0xA009
WWW-Authenticate0xA00A
在代码或字符串头名称之后,头值立即被编码。0xA00B

标志着此请求处理周期的结束。如果 reuse 标志为 true(实际 C 代码中任何非 0 值),则此 TCP 连接现在可用于处理新的传入请求。如果 reuse 为 false (==0),则应关闭连接。

结束响应 (End Response)

容器请求更多请求数据(如果正文太大无法放入第一个发送的数据包中,或者当请求被分块时)。服务器将发送一个正文数据包,其中包含的数据量为 request_length、最大发送正文大小(8186 (8 KB - 6))以及请求正文实际剩余发送字节数中的最小值。

获取正文块 (Get Body Chunk)

如果正文中没有更多数据(即 servlet 容器尝试读取超出正文末尾),服务器将返回一个“空”数据包,这是一个有效载荷长度为 0 的正文数据包。(0x12,0x34,0x00,0x00)
我的疑问

如果请求头信息大于最大数据包大小会发生什么?没有规定在请求头信息超过 8K 时发送第二个请求头信息数据包(尽管我认为响应头信息处理得当,但我不太确定)。我不知道是否有办法在初始请求头信息集中获得超过 8K 的数据,但我敢打赌有(将长 cookie 与长 SSL 信息和大量环境变量结合起来,你就可以轻松达到 8K)。在这种情况下,连接器可能会在尝试发送任何头信息之前失败,但我不确定。

关于认证?Web 服务器和容器之间的连接似乎没有任何认证。这在我看来可能很危险。

版权所有 © 1999-2024, Apache 软件基金会