切换深浅色
第24章 XHTTP深入解析
24.1 先建立逻辑会话,再决定它占用哪些HTTP请求
一条代理连接需要同时读和写。HTTP的基本组织却是一份请求对应一份响应,不同中间节点对持续上传和持续下载的处理也不同。XHTTP将这个差异放到承载层解决:用一条或多条HTTP请求取得上行字节,用相应响应取得下行字节,再对上层呈现一条双向逻辑连接。
本章固定Xray-core提交0888beb38a9ba45440557175e71ce21cf8ba3b13。配置名xhttp映射到源码目录transport/internet/splithttp,当前实现已经包含多种模式和可调整元数据放置方式,不能只按早期“一个GET加若干POST”的图概括所有版本。
先以默认放置方式的packet-up说明。客户端为一条逻辑连接生成会话标识S,用GET持续读取S的下载响应,再把上行字节分批上传,每批带S和序号。服务端按S将这些请求归到同一份httpSession,由uploadQueue按序提供上行,下载请求的ResponseWriter提供下行,组合成splitConn交给VLESS。
正在绘制图表…
查看图表原文
flowchart LR
A[上层VLESS写入连续字节] --> U[上传分批请求 S加序号]
U --> Q[服务端S的有序上传队列]
Q --> V[VLESS读取上行]
R[VLESS写入下行] --> D[S的持续下载响应]
D --> C[客户端读取连续字节]
GET的响应不是一份先生成完整文件再下载的静态对象。VLESS返回的字节可以持续写入其中,源站会主动刷新;边缘是否同样即时转发还取决于第20章的缓冲、缓存和超时边界。上传HTTP请求的200回应也不承载相同方向的全部网站返回,它主要说明这一上传请求在相应处理点被接受。
stream-up保留上、下行分离,却用一条持续请求体作为上行。服务端把这个请求的Reader接入S的队列,而不是等待若干有限负载批。stream-one进一步取消这份独立的S关联:同一个HTTP请求体承担上行,它自己的响应体承担下行,服务端直接把两个方向组成逻辑连接。
所以会话、HTTP请求、HTTP流和底层连接是四个层次。packet-up的一条会话可产生许多请求;HTTP/2中多个请求可以共享一条TCP/TLS连接;HTTP/3中它们也可共享一条QUIC连接。stream-one的一份会话只有一个主要请求上下文,却不意味着整个客户端进程只有一条HTTP连接。
对基本VLESS而言,目标只在自己的请求头内出现一次,后续上传批没有各自重新认证一个VLESS请求。若第19章34字节头被分在两个上传批里,服务端应向VLESS恢复有序连续输入,而不是让第二批从版本字段重新开始。若上层启用Encryption,XHTTP看到的内容则先是那一层握手和记录,基本VLESS头要等解密后才可解析。
会话生命周期也不只由一份上传请求决定。固定服务端允许先收到上行再收到下载,创建的会话在尚未完成下载关联时有30秒回收等待;下载请求建立后,isFullyConnected取消这条早期回收路径,会话转为随下载请求生命周期管理。不能把30秒简单写成每条已连接XHTTP会话最多只能使用30秒。
下载结束后服务端删除关联,队列及读写器还沿各自关闭路径释放。一个上传HTTP请求结束,只能表示这一批或这一方向的对应输入结束,不应由中间节点擅自当成整个VLESS双向连接完成。上层结束、请求取消和HTTP连接断开又有不同影响范围。
会话标识只是把请求对应起来,不替代用户授权。服务端还会检查Host、路径、Padding、模式和负载限制;真正的VLESS账户检查在逻辑连接交给VLESS后发生。HTTP端已经回复200甚至刷新响应头时,VLESS认证仍可能随后失败。把“下载请求状态200”当作“UUID可用、网站已连接”,会越过几个尚未完成的步骤。
本节对应httpSession与ServeHTTP、splitConn及客户端Dial。下面先展开S和序号究竟放在哪里,再分别跟踪每种模式。
24.2 会话标识和序号不是二进制VLESS字段
默认会话标识由uuid.New生成,并以带连字符的UUID文本进入HTTP。它不是把VLESS头中的16字节用户ID直接拿来当会话;同一用户可以同时产生很多S,每条关联不同的上传队列与下载响应。stream-one不需要独立S,固定客户端会保留空值。
新版本也允许配置字符表与长度生成S,使用相应的随机字符选择分支;构建器检查字符和候选空间等条件。这个可变格式说明接收端依赖的是会话标识字符串,而不是强制解析成一把UUID账户。自定义分支使用math/rand/v2,与默认uuid.New路径也不同;本协议没有靠S本身完成VLESS用户认证,不能给两条生成路径随意附加同一份密码凭据保证。
默认路径配置先归一化为前导斜杠;当会话或序号放在路径时,还保证需要的末尾斜杠。假设基本路径为/book/,选择虚构S为76543210-abcd-4321-8123-0123456789ab,则默认元数据片段为:
text
下载:GET /book/76543210-abcd-4321-8123-0123456789ab
上传:POST /book/76543210-abcd-4321-8123-0123456789ab/0
上传:POST /book/76543210-abcd-4321-8123-0123456789ab/1这里只展示方法与请求目标片段,省略HTTP版本、Padding和其他字段,不是一份完整可接受请求。0与1是十进制文本序号,服务器用ParseUint按十进制64位无符号整数解释;不是大端八字节整数,也不是TCP序号。序号按上传批次递增,不按正文累计字节递增;每条新S的正常上传序列从0开始。
会话与序号还可分别放入query、header或cookie,双方必须按相同约定读取。默认名称如下:
| 放置位置 | 会话默认键 | 序号默认键 | 表示范围 |
|---|---|---|---|
| path | 无独立字段键 | 无独立字段键 | 基本路径后依次追加对应段 |
| query | x_session | x_seq | URL查询参数 |
| header | X-Session | X-Seq | HTTP字段值 |
| cookie | x_session | x_seq | Cookie名和值 |
它们可以独立组合。例如S放Header、序号放路径时,路径只需附加序号;服务端提取器按已配置位置推进pathPart,而非固定认为路径第一段永远是S。若某个中间节点重写路径、删除自定义字段或合并Cookie时改变语义,关联也可能失败。
放置位置影响可见性和限制,不提供加密。终止HTTP TLS的边缘能读取路径、字段和Cookie,并可据S关联上下行;Query与路径也可能进入访问日志。换一个字段名并不能从密码学上阻止关联。若需要保护基本VLESS内容,还要看HTTP内部是否有独立端点加密。
上传方法也并非永久固定POST。当前配置默认POST并转成大写,还允许在明确packet-up模式下使用GET上传;服务端在GET且序号非空时将它识别为上行请求。没有序号的GET则属于下载路径。因此看见GET不能单独判断方向,必须把方法、S、序号与模式放在一起解释。
持续stream-up通常有S而没有上传批次序号,上传请求体作为一个持续Reader;stream-one则连S也为空。固定服务端正利用这些条件区分后续分支,名字相近的三个模式不是只在请求头里改一段文本。
所有与S关联的请求还必须到达同一份实际会话状态。固定sessions属于requestHandler,而不是仅凭相同IP或同一进程就全局共享。两个不同监听入口、两台后端或两个独立处理器,如果没有让请求汇聚到同一状态对象,不会因为S文字相同自动配对。DownloadSettings可选择不同入口,需要其网络路由最终满足这一状态归属。
对VLESS解码器,这些HTTP元数据都已被承载层消费,不进入19章的基本头。反过来,VLESS UUID、目标和Flow也不应被HTTP路径提取器当作S或序号解析。区分清楚之后,才能解释某条HTTP请求200、但上层仍在等缺失上传批的现象。
具体生成、归一化、填入与提取见config.go,模式与字段组合限制见SplitHTTPConfig.Build,方法与序号分派见hub.go。
24.3 packet-up:合批上传,再按序恢复字节流
packet-up让上层连续写入一个带背压的内存通道,上传任务从中取出已经积累的内容,按最大批量拆分并构造HTTP请求。它没有规定一次上层Write必须对应一次POST;合批正是为了摊薄请求字段、调度和封装成本。
固定默认scMaxEachPostBytes为1000000字节,是十进制一百万,不是1 MiB。客户端在这条逻辑连接建立时选取maxUploadSize,再将读出的MultiBuffer拆成不超过它的批次。默认scMinPostsIntervalMs为30,限制相邻批次发起的节奏;若上一次发送以来已经等得足够久,不会再无条件睡满30毫秒,首批也不会因为lastWrite初始值而固定多等一次。
批次取得序号后,客户端发起PostPacket。普通HTTP客户端路径通过WroteRequest跟踪“请求已写出”,随后可以开始下一批,不必将每份上传都串行为“等收到完整200再发下一份”。HTTP/1.1另有上传连接池与响应读取策略;这一点也不能一概套用HTTP/2单流请求的行为。
服务端读取有限负载,检查大小,解析序号,再将Packet推入有界uploadQueue。队列用按序号排序的堆和nextSeq恢复顺序。比如批0内容为ab、批1为cd、批2为ef,即使到达顺序是1、0、2,上层仍应得到abcdef。先到的cd要等0,不能直接交给VLESS拼成cdabef。
若上层一次只读取一字节,队列还会保留当前批未读完的负载;只有该批完整消费后才推进nextSeq。HTTP请求边界、队列Packet和应用Read边界因此再次分离。重复到来的旧序号在已推进后不会重新作为正常下一批交付,但这并不意味着HTTP200就是一份上层业务“恰好一次执行”的确认。
排序也会产生本层等待:缺少0而已有1—20,仍不能把后面的字节提前交付。HTTP/3可以消除不同QUIC流之间某些传输交付阻塞,却不能消除同一XHTTP上行字节流对批次顺序的依赖。队列不是越大越好;固定实现对积累与乱序重组设限,超过相应条件会失败,而不是无限占用内存。
默认scMaxBufferedPosts为30,这是实现的缓冲参数,不能直接翻译成“任意时刻内存严格只有30个Packet”。推送通道、重组堆及正在处理的对象分属不同位置,检查代码也在具体分支发生。评估内存需要把批次大小和各阶段占用一起计算。
上传内容默认作为原始请求体传递。更精确地说,配置构建器把空uplinkDataPlacement归一化为auto;客户端FillPacketRequest对auto选择body,而服务端auto可接收并串联Header、Cookie与Body中提取的内容。这是双方生成与接受范围的差别,不应把auto误写成先尝试一种、失败才回退另一种。
明确packet-up模式时,还可把负载放进Header或Cookie。客户端先对整批数据作无填充Base64URL,再将编码文本拆分;Header字段形如X-Data-0、X-Data-1,Cookie则按相应键形成x_data_0、x_data_1。服务端按编号取出并拼接编码文本,最后才整体解码,不把每个文本片段都当成一份独立Base64消息。
例如五字节hello编码为aGVsbG8,七个字符。若为了说明切分,把它分成aGVs和bG8,必须先拼回七字符再解码;后一个片段并不是一份单独的完整应用消息。真实片段尺寸由配置与归一化规则决定,这个四/三字符划分仅用于解释编码层次。
Base64URL增加表示长度,不提供加密。头部传输还受到HTTP字段总量、单字段、Cookie和中间节点限制;正文批量允许一百万字节,不意味着可以把同样大小无条件搬进HTTP Header。服务端会对重建负载再次检查最大批次,客户端与服务端限制也要相容。
上传200是在Push相应阶段成功后产生的HTTP结果。队列可能仍在等先前序号,VLESS也可能还没有读到完整ID;因此它不证明正文已转发给网站。相反,上传请求错误可中断客户端上传通道,缺批不由一套独立XHTTP ACK/NACK协议自动无限重传。HTTP/1.1池中失效连接的特定写入重试,是实现分支,不能扩展成任意应用内容无损自动重放。
packet-up的可靠字节服务建立在下层HTTP可靠传输、批次排序、错误传播与状态生命周期之上。它没有把这些批次直接发成裸UDP,也不提供应用数据报边界;基本VLESS UDP或XUDP仍需自己表达那层语义。
批次和发送等待见dialer.go,上传与H1分支见client.go,大小及负载重建见hub.go,排序和消费见upload_queue.go。
24.4 stream-up:一份持续上行,与另一份持续下行
packet-up通过有限请求体适应某些中间节点,却要持续建立上传请求、携带字段并排序。若整条路径能够边收到请求体边向源站转发,可以把上行改成一份持续请求体,这就是stream-up。
固定客户端为这一会话生成S,先打开独立下载,再用io.Pipe连接上层Write与上传请求Body。OpenStream根据Body是否存在选择上传方法或GET;stream-up的上传调用带uploadOnly=true,得到的上传响应不会作为上层下行内容。真正的VLESS响应从独立下载请求取得。
服务端看到有S、属于上行方法且没有批次序号时,将该请求Body包装为httpServerConn Reader,放入S的uploadQueue。队列一旦接入这个持续Reader,后续读取直接从请求体取得,不再为每段内容等待一个递增Packet序号,也不再把这条上传当作不断追加的packet-up批。
text
客户端Write → io.Pipe → 持续上传请求体 → 服务端Reader → VLESS上行
客户端Read ← 独立GET响应体 ← 服务端Writer ← VLESS下行上行仍是字节流,HTTP/2 DATA帧或QUIC STREAM片段不变成应用消息边界。一次网站TLS记录可以横跨多份HTTP DATA,服务端也可以一次Read取得多次上层写入。stream-up少掉的是每个有限批的额外请求与排序,不是让TCP或QUIC取消流量控制。
固定FillStreamRequest对有Body的stream-up/stream-one默认设置Content-Type: application/grpc,除非NoGRPCHeader启用。但本路径直接传递Body,没有第20章Hunk Protobuf与五字节gRPC消息包络。这个字段是HTTP呈现的一部分,不能据此把XHTTP流当成标准gRPC Tun调用,也不能按每段数据额外扣除一个不存在的Hunk头。
上传响应在这里主要维持上传HTTP请求生命周期。客户端会读取并丢弃其响应内容,再关闭相应对象;如果立即关闭响应Body,底层客户端可能同时打断仍在发送的上传,因此源码特意先持续消费。上传响应中的字节不会因其HTTP状态200就被混进独立下载的VLESS正文。
服务端在存在相应兼容标记或已接受新Padding方式、且间隔范围有效时,还会向上传响应周期性写入X填充,并刷新。scStreamUpServerSecs控制这类响应写入之间的秒间隔,固定归一化默认20—80范围;随机取值仍按下界包含、上界不包含解释。它不是“持续上传达到这些秒数就强制换连接”的字段。
这份响应填充与VLESS正文、Vision Padding、Encryption握手Padding都不同。它留在上传请求自己的回应路径里,被客户端丢弃,用于相应HTTP中间路径的进展;真正下载仍由GET响应负责。把四种同名或近似Padding混算,会同时算错流量方向和应用字节。
下载先建立的调用顺序也不意味着客户端已收到可读VLESS字节。DefaultDialerClient.OpenStream在后台执行HTTP请求,等待取得连接或相关错误信号后即可返回WaitReadCloser;状态码和响应Body还可能稍后才交给它。因此Dial返回、HTTP连接取得、200到达、VLESS基本响应到达是不同事件。
背压从VLESS读端向请求体与io.Pipe传递:源站不继续消费上行,HTTP流窗口和本地Pipe最终会限制客户端继续写入。若中间节点坚持缓存完整请求体,再转发给P,则持续请求可能迟迟不能交付首批VLESS字节。即使下行GET已回复200,上行也可能在另一个依赖上停住;一份活跃响应不能证明双方向都已畅通。
此外,上下行仍必须进入同一S状态,上传Body只能以预期方式接入一次;不能通过再开一个同S、无序号持续上传,随意拼出多个上行并发入口。固定队列对已存在Reader有拒绝检查。请求取消、上传错误与下载关闭共同影响整个会话,不能靠无限保持下载连接掩盖已经失效的上传。
持续上行与打开顺序见dialer.go,OpenStream及uploadOnly消费见client.go,服务端Reader与响应填充见hub.go,HTTP字段见FillStreamRequest。
24.5 stream-one:用同一请求的两个方向持续交互
若一条HTTP请求能够持续发送请求体,同时持续读取响应体,就能在一个请求上下文中提供代理的双向流。stream-one采用这条路径,省去另一个下载请求及独立S关联,但要求中间节点和HTTP实现允许请求、响应同时前进。
固定客户端在stream-one模式不生成S,建立io.Pipe作为上传Body,调用一次OpenStream,uploadOnly为false。请求方法取归一化后的上传方法,默认POST;取得的响应Body直接作为这条逻辑连接的下行Reader。与stream-up不同,这份响应不是用来丢弃填充的旁路回应,而是承载真正VLESS返回字节。
服务端看到相应无S请求,进入stream-one分支。它把请求Body作为httpServerConn.Reader,把当前ResponseWriter作为写端,组合splitConn,交给上层。没有先向sessions表插入一份S,也没有先等待另一条GET来认领它,因此不使用24.1中那条未关联会话的30秒配对回收逻辑。
正在绘制图表…
查看图表原文
sequenceDiagram
participant C as 客户端
participant P as XHTTP服务端
C->>P: POST及持续请求体开始
P-->>C: 200响应头并刷新
C->>P: 请求体中的后续代理字节
P-->>C: 响应体中的代理返回字节
C->>P: 同一请求体继续上传
P-->>C: 同一响应体继续下载
图中交错并不是“收到一个请求完整正文才允许回复”。浏览器类交互经常先发送一点数据、等对端回应、再发送下一段;如果中间层必须等整个请求体结束才把200或响应体交给客户端,就可能产生双方相互等待。stream-one兼容性必须检查这项进展条件,不能只检查最终能否上传一个有限文件。
在HTTP/2中,这一请求流的客户端DATA承载上行,服务端DATA承载下行,两方向各有流量控制,并共享相应连接窗口。HTTP/3里一条双向QUIC流同样可承载相应请求和响应方向。它们依然不是不受窗口约束的无限管道;下载不读、上传不消费或连接级窗口紧张,都能形成背压。
HTTP/1.1则不能仅从模式配置被接受就断言此交互一定可用。Go HTTP服务端默认会在开始写响应前消费未读请求体,需要EnableFullDuplex改变相应行为;所读XHTTP服务端没有显式调用它。因此本书不把stream-one在本提交任意H1路径上的双向行为当成已经验证的能力。协议语法允许存在请求体与响应,不代表所有实现允许预期的同时进展。Go1.27.0接口说明
stream-one的服务端下载回应默认设置Cache-Control: no-store及X-Accel-Buffering: no,并按配置设置text/event-stream类型,写出200后刷新。与前面一样,text/event-stream不会把内部二进制代理内容自动改成标准SSE事件,源站Flush也不保证中间每段连接立即向下游送出同样片段。
固定配置明确禁止在显式stream-one模式下使用DownloadSettings,因为它已经使用同一请求的响应体。它也不使用packet-up的有限批上传和序号排序;把scMaxEachPostBytes当作这份持续正文每到上限就自动换请求的机制,同样会读错路径。
结束行为要以请求上下文为中心。服务端处理器不能在返回后继续用原ResponseWriter写内容,因此等待请求取消或逻辑连接关闭;客户端关闭其读写对象后,底层HTTP实现再释放或取消相应流。上层的半关闭是否被完整保留,还要检查具体适配,不可只凭HTTP/2存在END_STREAM就声称net.Conn的所有关闭行为都一样。
固定BrowserDialerClient对含Body的OpenStream直接返回尚未实现双向流式浏览器拨号的错误。因此“XHTTP一般有stream-one”不能扩展成“本版本每种拨号后端都实现stream-one”。下一节把auto决策与这些实际能力条件合起来,避免用一个模式名掩盖不同代码分支。
模式分支见dialer.go、服务端无S分支,组合限制见配置构建器,浏览器后端限制见browser_client.go。
24.6 auto是确定的本地分支,不是测速协商
auto容易让人联想到客户端试完多种模式、比较速度,再与服务器协商最优选择。固定Xray的这段逻辑没有执行这种过程:它根据本地安全设置与DownloadSettings,直接选定一个模式,然后构造相应请求。
| 本地条件,Mode为空或auto | 选定模式 |
|---|---|
| 没有REALITY设置,包括普通TLS路径 | packet-up |
| 有REALITY,且没有独立DownloadSettings | stream-one |
| 有REALITY,同时配置独立DownloadSettings | stream-up |
这个选择与HTTP版本不是同一份判断。普通TLS即使选HTTP/2或HTTP/3,在这里auto仍取packet-up;需要其他模式时必须有相应有效配置与实现路径。选择auto也没有给服务器发送一份“我支持三模式”的能力清单,更不会在每个请求中动态按丢包率切换模式。
HTTP版本由Default拨号后端另一函数决定:
| 本地安全设置 | 决定的HTTP版本路径 |
|---|---|
| REALITY | 2 |
| 没有TLS也没有REALITY | 1.1 |
| TLS的NextProtocol恰好只有http/1.1 | 1.1 |
| TLS的NextProtocol恰好只有h3 | 3 |
| 其他TLS列表形状 | 2 |
尤其是列表同时写h3与h2,不会在这个函数中自动先试QUIC;列表长度不是1时直接选2。此表描述本地构造哪一种HTTP客户端,还不是远端必然协商成功的证明。TLS ALPN、实际入口和服务器实现不匹配,连接仍可失败;它也不承诺失败后自动逐一回退所有版本。
服务端的auto则主要表示允许相应请求形状进入各模式分支,而不是与客户端执行同样的本地安全判断。它依据是否有S、是否有序号、方法及配置限制区分持续上行、分批上行和单请求路径。显式模式会收窄一些接受范围;例如packet-up拒绝无S的单请求,而本提交stream-up入口的无S检查仍可接受stream-one形状,不能把服务端模式名机械视为一条完全对称的字符串相等检查。
配置构建还有更早的限制:Header/Cookie上行负载和GET上传要求明确packet-up模式;显式stream-one禁止DownloadSettings;Host不能藏进自定义headers里,须使用独立Host字段。这些约束可能在网络握手之前就拒绝配置,与运行时收到400是不同阶段。
Extra的处理也影响“最终配置”是什么。固定Build在extra存在时先解析一份新的SplitHTTPConfig,只把外层host、path、mode赋给它;其他字段取extra那份,而非对两份对象进行所有字段的逐项合并。若在外层写了某个复用或Padding值,又假定extra只追加一个小选项,就可能读错实际生效输入。
默认值还可能先在构建器注入,再由运行时GetNormalized补齐。比如完全空的XMUX配置会获得一套构建器默认范围;不能只读GetNormalizedMaxConnections返回0,就宣布普通JSON配置默认不管理多个客户端。下一节会同时读取这两层,区分策略对象和真实连接数量。
浏览器拨号是另一个后端。getHTTPClient在没有REALITY且启用浏览器拨号时,可以返回BrowserDialerClient;具体HTTP网络行为由浏览器相关路径处理,不能把上表Default后端的版本构造逻辑当作浏览器实际协商结果。该后端支持下载及分批上传,而含持续Body的OpenStream在所列源码中仍返回未实现错误,所以显式选择stream-up或stream-one还需考虑后端能力。
这些分支带来一个实用的分析顺序:先还原最终配置,再确定拨号后端,再判断本地模式与HTTP版本,最后检查服务端接受路径与中间节点行为。每一步都可在源码定位,不能用auto三个字替代这条证据链。
具体选择见dialer.go,配置覆盖及组合限制见transport_method.go,服务端模式接受与浏览器路径分别见hub.go和browser_client.go。
24.7 XMUX:复用对象如何选择、计数和退役
一个持续下载请求可能存在几小时,分批上传却在此期间产生许多短请求。若每批都从零建立安全连接,握手成本会反复出现;若所有会话永久挤在同一条连接上,故障和拥塞的影响又会集中。XMUX把“本次使用哪个HTTP客户端、何时不再给旧对象安排新工作”独立管理。它并没有在负载里添加一层VLESS Mux帧,也不是第11章的gRPC MultiHunk。
固定实现中,管理器的池元素是XmuxClient,其中XmuxConn实际通常是DefaultDialerClient。该对象持有HTTP Transport。Transport再管理实际TCP或QUIC连接。因此必须分清三层:XMUX池元素、HTTP请求/流、底层网络连接。创建一个池元素不等于已经完成一次TCP握手;底层连接断开后,HTTP库也可能重新拨号。HTTP/1.1的上传还另有原始连接池,不能把池元素数量当作抓包里所有连接数量。
管理器以目标地址及MemoryStreamConfig对象为键保存。两个配置内容看似相同,却使用不同内存配置对象时,不能仅据JSON相等认定一定共享同一个池。浏览器拨号分支直接返回浏览器客户端,不经过这里的默认XMUX管理。
| 控制量 | 固定实现中的作用 | 容易误读的边界 |
|---|---|---|
| maxConnections | 池未达到抽样数量时,新的取得操作优先创建对象;达到后在可选对象中选择 | 不是预先同时拨出所有连接,也不是进程所有Socket的硬上限 |
| maxConcurrency | 依据对象Running计数筛选,已有对象都达到限制时创建新对象 | 不等于HTTP/2 SETTINGS_MAX_CONCURRENT_STREAMS或QUIC的流额度 |
| cMaxReuseTimes | 限制池元素被取得的次数;创建时已算第一次 | 不直接数每个POST |
| hMaxRequestTimes | 请求路径扣减的使用预算 | 与活动会话计数是两套量 |
| hMaxReusableSecs | 到达时间后不再把对象当作可复用候选 | 不是到点强行截断正在下载的字节流 |
构建器禁止同时设置正的maxConnections和maxConcurrency。前者控制池规模方向,后者允许随会话负载扩展,两种策略不能按名字相加。在管理器创建时,数量或并发范围抽样一次;每个池元素创建时,再分别抽样自己的使用次数和可复用时间。范围上下限不相等时,所用随机函数取下限、排除上限;相等时取该常数。
普通JSON配置若整个XMUX结构都是零值,Build会注入maxConnections为3、hMaxRequestTimes范围600—900、hMaxReusableSecs范围1800—3000。因此后两个实际抽样集合分别是600到899次、1800到2999秒。若只显式提供了部分XMUX字段,整个结构已不是零值,这套整体默认注入就不再发生;其余字段要按运行时归一化规则读取。不能把“完全未写XMUX”的默认值逐项套到每份部分配置。
取得对象时,管理器先清理已经标记关闭、使用次数耗尽、请求预算耗尽或超过可复用时间的元素。剩余候选中若有并发限制,则只保留Running低于限制者,再随机选择;没有候选且允许创建时建立新对象。cMaxReuseTimes为正时,创建把剩余次数设为初值减1,后续取得再减1;默认无限制路径使用负值表示,而非不停把0减成负数。
Running主要由Dial建立逻辑连接及其关闭路径增减。若上下行使用不同池元素,两边分别登记;同一元素则避免重复登记。packet-up的上传循环还会在预算或时间触发时重新取得动态上传客户端,而持续下载可继续使用原对象。这个循环不会把每个在途POST都变成一次对称的AddRunning/DoneRunning,因此Running应按这些调用路径理解,不能当作HTTP库实时活动请求总数。
退役分成“从候选池移除”和“关闭资源”两步。移除时置NotUsed;只有Running降至0或以下,maybeClose才尝试关闭它。假定下载会话A仍使用对象P,P的复用时间已经结束,新会话B可以取得新对象Q,而A继续从P读取。这样的排空可减少为了换连接而主动中断长下载,但并不保证网络自身不发生断线。
关闭动作也依后端而异。DefaultDialerClient.Close显式处理HTTP/3 Transport;HTTP/1.1和HTTP/2在此没有对应的强制关闭调用。故“XMUX已移除某对象”并不足以证明某条TCP已经立即发送FIN。资源最终何时回收,还受请求结束、HTTP库空闲管理和连接错误影响。
这个模型解释了复用的代价:更多池元素可能分散单连接故障、流量控制和拥塞状态,也会增加握手、内存与空闲维护;较长复用减少重建成本,却让更多请求共享同一条路径的故障。XMUX提供的是分配与生命周期策略,无法替代HTTP和底层传输自身的拥塞控制,也不赋予复用数量某种普遍最优值。
上述对象与退役条件见固定提交的mux.go,调用计数及动态上传更换见dialer.go,默认注入见transport_method.go,后端关闭见client.go。
24.8 HTTP/2与HTTP/3:相同会话模型,不同传输约束
XHTTP所恢复的是有序双向字节流。使用HTTP/3时,它仍主要把字节放进可靠HTTP请求流,并没有因为底层变成UDP,就把每个VLESS数据块改成不可靠QUIC DATAGRAM。第12章所讲的QUIC流重传、流量控制、连接级拥塞控制,仍然约束这条路径。
先看HTTP/2的packet-up。持续GET的响应占用一个HTTP请求流;每个上传POST又是一个请求流。若这些请求共用一条HTTP/2连接,HTTP/2帧可以交错排列,上传不必等持续下载结束后才发送。同一个请求中的DATA必须按该流的顺序交付,XHTTP再用seq重排不同上传请求。这里有两次不同层次的排序:HTTP流内排序,以及XHTTP上传批次排序。
假设批次0和1分别使用HTTP/2流3和5。服务端可能先完整读到流5,仍须等待批次0才能把批次1的字节交给VLESS。多流提高并行度,不会取消最终字节流的有序性。更下层若TCP有一个缺失区间,即使接收端已经收到后面携带其他HTTP流的数据,TCP仍不能越过空洞交付;它可能同时推迟下载和上传请求。这是TCP层的连接内队头阻塞,不能靠再增加一个HTTP/2流消除。
HTTP/3把不同请求放进不同QUIC流。某个上传流的数据包丢失,其他流中已完整到达的数据通常可以继续交付,不必等待这个流补齐。不过同一连接仍共享拥塞控制和连接接收额度;大量丢包会压低总发送速率。若先完成的是XHTTP批次1,而缺失的是批次0,即使QUIC已允许流5交付,XHTTP也仍要等待0。消除了一个层次的跨流等待,不代表最终应用完全不受先后顺序约束。
| 层次 | HTTP/2路径 | HTTP/3路径 |
|---|---|---|
| 网络运输 | TCP | UDP之上的QUIC |
| 安全机制 | 通常TLS;本实现也有REALITY承载路径 | QUIC集成TLS密钥建立 |
| 请求承载 | HTTP/2流和DATA帧 | QUIC流中的HTTP/3帧 |
| 丢失数据补齐 | TCP重传缺失字节 | QUIC重传需要可靠交付的流数据 |
| 多请求最终拼接 | XHTTP会话和seq | 相同的XHTTP会话和seq |
| 不同请求共享的限制 | TCP拥塞状态、HTTP连接窗口等 | QUIC拥塞状态、连接级流额度等 |
固定客户端通过http2.Transport或http3.Transport分别构造后端。HTTP/2的自定义DialTLSContext返回已经经过所选拨号、安全层处理的连接;不能仅因方法名带TLS就认定它一定调用了标准TLS握手。REALITY由对应分支处理,而本提交decideHTTPVersion把REALITY固定选入2,不进入QUIC路径。
HTTP/3客户端把目标网络改为UDP,经QUIC Transport建立连接;配置可以控制流接收窗口、连接接收窗口及拥塞算法等。源码调用DialEarly,并不单独证明某次请求已经作为0-RTT发送且被服务端接受:是否存在可用恢复状态、库是否发送早期数据、对端是否接受,都需另行确认。也不能把QUIC恢复握手与第22章VLESS Encryption的状态缓存恢复混为同一个0-RTT。
服务端在TLS ALPN列表恰好只含h3时进入独立UDP监听路径,使用HTTP/3 Server。其余主要进入TCP或Unix监听路径,可在外层包装TLS或REALITY,再交给HTTP服务器。固定代码为该服务器启用了HTTP/1和未加密HTTP/2处理能力。这一点尤其适用于反向代理已经结束TLS、向源站转发明文HTTP/2的分段部署;它不表示普通客户端配置在没有TLS时会自动选择h2c,第24.6节的客户端决策仍选1.1。
经过HTTP反向代理时,两段协议可以不同:
text
客户端 ── QUIC/TLS + HTTP/3 ── 边缘节点 ── TCP + HTTP/2 ── XHTTP源站
独立拥塞、加密与流号 独立拥塞、流号及窗口边缘节点解析前段请求,再在后段重新发起请求。客户端的QUIC流号、HTTP/3帧和QPACK状态不会原封不动到达源站。源站日志显示HTTP/2,与客户端使用HTTP/3并不矛盾;前段没有TCP队头阻塞,也不能免除后段TCP可能产生的等待。请求方法、路径、Body字节及必要的元数据能否保留,才决定XHTTP会话是否仍能被源站还原。
持续流模式还要求中间节点允许上传与下载按需推进。HTTP/2或HTTP/3具备双向流能力,仅说明协议可表达这种行为,不保证反向代理立即转发Body。节点可以先缓冲完整请求,再向后段发送;这会改变交互行为,下一节将分析其后果。
因此,不能只根据“HTTP/3”这个标签给出更低延迟或更高吞吐的结论。路径是否允许UDP、丢包位置在哪一段、窗口是否足以覆盖带宽时延积、请求是否被缓冲、多个会话是否共享连接,都比标签更接近实际瓶颈。
版本与后端构造见固定dialer.go,TCP/h2c及UDP监听见hub.go。HTTP/2、QUIC及HTTP/3的层次保证分别以RFC 9113、RFC 9000及RFC 9114为准。
24.9 中间节点:缓冲、缓存、字段改写与Padding
一个HTTP请求能到达源站,不等于它适合承载实时双向通信。考虑浏览器已发出网站ClientHello,本地代理把它放进一个尚未结束的stream-up请求。若边缘节点坚持收齐整个请求Body后才向源站转发,源站就看不到ClientHello;浏览器等ServerHello,上行Body却因为浏览器仍在等待而保持打开。这是由缓冲策略造成的进度依赖,TLS和HTTP语法本身可以都没有错误。
packet-up把上行改成有限批次,节点至少能够等一个批次结束后转发,因此更容易适应这种上行缓冲。代价是批次边界、附加HTTP头和调度等待。下载方向仍是一条持续响应;若中间节点积攒响应Body,网站的短小握手消息同样可能延迟很久。增加上传模式的兼容性,并不会自动解决下载缓冲。
固定服务端在下载路径设置X-Accel-Buffering: no、Cache-Control: no-store,并在相应位置Flush;默认还可以使用text/event-stream。这些动作分别表达“希望及时转发”“不要存储响应”以及一种内容类型。它们不把负载改造成标准SSE事件语法。Flush也只能要求本地HTTP实现推进输出,不能强制所有远端缓冲区立即排空。
作为具体例子,Nginx官方文档明确说明proxy_buffering控制响应缓冲,并可通过X-Accel-Buffering响应头影响该行为;proxy_ignore_headers又可禁止处理这种字段。因此该头是某些中间节点认识的控制信号,而不是HTTP通用的强制实时交付承诺。请求缓冲与响应缓冲也是两个不同方向的策略。Nginx代理模块文档
缓存则是另一个问题。实时代理响应属于某个会话的字节序列,不能因为请求路径相似就复用给别的客户端。no-store要求遵循HTTP缓存规则的节点不存储对应响应,但不能保护数据免于被正在处理请求的终结节点读取,也不是端到端加密。RFC 9111
在此基础上,XHTTP还加入HTTP层Padding。它与第22章加密握手Padding、第23章Vision块Padding各处于不同层:HTTP Padding改变HTTP字段或相应填充响应的形状;Vision Padding处于恢复出的代理字节流中;Encryption Padding由自己的握手状态解析。不能把一种Padding的长度规则用于另一种。
普通未启用混淆布局的请求,主要把Padding放在Referer字段所含URL的x_padding查询参数里;普通响应使用X-Padding字段。固定构建器还允许启用另一套布局,把Padding放进Cookie、指定Header、真实请求查询参数,或Header里所含URL的查询参数。响应可以使用Header或Cookie等对应能力,不能据此假设响应也存在一个请求URL可供修改。
例如下面是解释布局的HTTP字段片段,省略完整请求和实际Padding长度:
text
Referer: https://proxy.example/book/?x_padding=XXXXXXXX...这里带查询参数的URL属于Referer字段值,不是实际请求的目标路径。默认请求构造先填Padding,再添加会话和序号元数据,所以不能假定Referer一定逐字复制最终带S/seq的请求URL。分析访问日志时,真实请求查询参数、Referer中嵌入的查询参数及Cookie必须分别辨认。
默认长度范围由归一化函数给出100—1000,生成时抽取100到999;接收端范围检查则包含端点。repeat-x方法直接重复X。HPACK/QPACK所用静态Huffman表中,X对应8位编码,故这串字符本身不会因该Huffman编码缩短。然而整个字段还存在名字编码、长度编码、索引选择和压缩状态,不能从“一个X是8位”推出“整个HTTP头在每次线上发送中都恰好增长N字节”。
另一种tokenish方法先生成Base62字符,再计算HPACK Huffman编码长度并调整字符串,使它接近目标长度,允许2字节偏差。源码以0.8字节/字符估算初始长度,随后通过实际编码长度修正,循环设有上限。0.8是生成器的估算参数,不是整个连接或所有HTTP头节省20%的性能结论。接收端此模式也按Huffman编码长度及容差检查,而非只数可见字符。
“验证Padding”容易被误解成某种隐藏口令认证。实际IsPaddingValid主要检查非空和长度范围;repeat-x分支并不逐字要求字符必须为X,tokenish分支也不是验证一段带密钥的MAC。知道布局的人可以构造符合范围的值。它最多属于入口格式筛选和流量形状调整;真正的代理身份、机密性和完整性仍应由所选VLESS、安全层及内层网站TLS分别承担。
中间节点可能删掉Referer,重写路径,拒绝很长的Header,或者把请求分配给不同源站实例。前两者可能令会话元数据或Padding提取失败;长度限制可能在进入XHTTP之前返回HTTP错误;最后一种会令同一个S找不到原来的下载/上传状态。上下行经过不同域名也不例外,最终仍须汇合到共享该会话状态的处理器,不能仅凭域名都指向同一台机器就认定成立。
此外,终结外层TLS的CDN可以看见这些HTTP字段及Body。若Body内另有经验证的Encryption层,其保护范围按第22、23章的状态变化判断;若Body只是普通VLESS加网站TLS,CDN可见的代理请求头和看不见的网站明文又是不同范围。Padding增加字节并改变形状,不会弥补缺失的端到端认证或加密。
本节布局、生成与验证规则来自固定xpadding.go,请求填充见config.go,响应头与服务端入口见hub.go。
24.10 把请求与字节拼回去:一个分批上行案例
下面构造一个可逐字节分析的packet-up会话。它展示XHTTP承载格式,未声称来自真实抓包,也不声称example.com的443端口会接受hello这种应用数据。沿用第19章的公开教学UUID,VLESS请求指定example.com:443;负载hello仅用于识别字节边界。此案例没有Vision或VLESS Encryption,外层假定已经建立并验证普通TLS及HTTP/2。
为了使HTTP表示紧凑,选择本实现支持的Padding布局:XPaddingObfsMode启用,Placement为header,Header为X-Padding,Method为repeat-x,长度固定100;会话和序号仍放路径,负载放Body。以下使用HTTP/2解码后的字段列表,绝非HTTP/1.1文本请求原样上网。真正线上会先经过HPACK和HEADERS/DATA帧封装,再经TLS。
会话标识S取11111111-2222-4333-8444-555555555555,基础路径/book/。持续下载请求使用流1:
text
:method: GET
:scheme: https
:authority: proxy.example
:path: /book/11111111-2222-4333-8444-555555555555
x-padding: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX服务端通过Padding及路径检查,找到或创建S对应的上传队列。它可以先发出如下响应字段并Flush,随后才把恢复出的连接交给VLESS处理:
text
:status: 200
x-padding: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
x-accel-buffering: no
cache-control: no-store
content-type: text/event-stream此时下载响应Body尚可没有VLESS响应字节。这个200只说明HTTP入口已进入下载处理路径,不说明UUID认证成功、目标已连接,更不说明网站TLS成功。
完整待上传字节如下,共39字节:
text
00 00 11 22 33 44 55 46 77 88 99 aa bb cc dd ee ff
00 01 01 bb 02 0b 65 78 61 6d 70 6c 65 2e 63 6f 6d
68 65 6c 6c 6f其中前34字节是无Addon的VLESS请求头,最后5字节是hello。现在人为选取20+19的批次边界,以观察跨请求解析;这不是默认批量大小,也不声称一次应用Write必然产生一个POST。流3提交批次0:
text
:method: POST
:scheme: https
:authority: proxy.example
:path: /book/11111111-2222-4333-8444-555555555555/0
content-length: 20
x-padding: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
DATA字节:00 00 11 22 33 44 55 46 77 88 99 aa bb cc dd ee ff 00 01 01流5提交批次1:
text
:method: POST
:scheme: https
:authority: proxy.example
:path: /book/11111111-2222-4333-8444-555555555555/1
content-length: 19
x-padding: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
DATA字节:bb 02 0b 65 78 61 6d 70 6c 65 2e 63 6f 6d 68 65 6c 6c 6fDATA字节那一行是本书的十六进制标注,不是请求Body中真的发送了“DATA字节”这些字符。每个content-length只数该请求的19或20个负载字节,不含HTTP头、HTTP/2帧头、Padding字段或TLS记录。
批次0恰好把目标端口的两个字节01 bb切开。上传队列交付第一个批次后,VLESS解析器已经有版本、UUID、Addon长度、命令以及端口的首字节,仍须等待第二个端口字节。下一次读取提供bb后,它才得到443,再解析地址类型02、域名长度0b和11字节域名。一个正确的流解析器不会因第一个HTTP请求已结束,就把尚未收齐的VLESS头判为格式错误;它看到的是队列提供的连续字节流。
若批次1先到,服务器可在成功入队后对它返回上传200,但队列仍暂存19字节,直到0到达才按0、1交付。上传200没有把“序号1已加入队列”提升为“前面的所有字节已被目标接收”。若0始终不来,1即使已经收到成功HTTP响应,也不能独自组成这次完整VLESS请求。
正在绘制图表…
查看图表原文
sequenceDiagram
participant C as XHTTP客户端
participant H as XHTTP处理器
participant V as VLESS处理器
participant W as 目标连接
C->>H: GET S,建立持续下载
H-->>C: HTTP 200,响应Body保持打开
H->>V: 提交由上传队列驱动的逻辑连接
C->>H: POST S/1,19字节(假定先到)
H-->>C: 上传HTTP 200,暂存seq=1
C->>H: POST S/0,20字节
H-->>C: 上传HTTP 200
H->>V: 按0、1交付连续39字节
V->>V: 校验UUID,解析目标及负载
V->>W: 按路由发起目标连接并转发负载
V-->>H: VLESS响应头及目标返回数据(若有)
H-->>C: 持续GET响应Body中的字节
图中的VLESS响应仍须遵循第19章的缓冲时机:默认响应头00 00不是目标成功状态码;DispatchLink、目标拨号和缓冲写出的具体先后不能由HTTP 200倒推。目标拒绝、超时或收到hello后关闭,都可能让最终Body只含部分结果或直接终止,不能为本例编造一个网站响应。
若将模式改为stream-up,仍可用同一个S关联GET和一个持续上传请求,但不再为每个批次增加/0、/1,也不再通过两个独立请求完成这次切分。若改为stream-one,则连S也不需要,由同一个请求的Body与Response Body组成双向通道。三种模式真正不同的是HTTP请求组织;VLESS仍要求相同的字节序和增量解析。
本例的字段与入口行为可对照固定config.go、client.go、hub.go及upload_queue.go。
24.11 开销从哪里来:批量、窗口、排队与拷贝
XHTTP的性能不能只用“多包了一层HTTP”解释。固定开销会随批量大小摊薄,等待时间却未必随之下降;为了减少请求数而等待更多字节,可能恰好延误浏览器正在等待的握手消息。吞吐和交互延迟必须放进同一个处理路径分析。
设一个有限上传批次含B字节,解码后可见的普通字段、Padding和元数据经HTTP压缩与分帧后产生H字节;相关安全记录额外占E字节。忽略IP等下层头时,这批次的有效比例为B/(B+H+E)。H依压缩表、字段变化和帧拆分而变,E依记录拆分而变,不能当作协议固定常数。
作为纯算术例子,假定某组测得或指定的H+E为300字节:B为1000时有效比例约76.9%,B为100000时约99.7%。这里的300是人为指定的比较条件,不是XHTTP实测头长。大批次摊薄开销的结论成立,但不能由这两个比例推出真实链路会快多少。
批量需要数据来源。如果应用已经连续提供大量字节,缓冲管道可以把多个Write合成批次,避免每次小Write都触发一次独立请求。若应用只写几百字节就等响应,上传调度、最小POST间隔和中间节点缓冲就可能直接进入首轮交互的关键路径。固定实现的间隔等待按距离上次写出的时间扣除;它不是对每个POST一律再睡完整间隔,也不是等待下载确认后才发送下一批。
增加并行POST可以让后一个批次较早进入网络,但会引入上传队列暂存。若有k个已到达的后续批次,每批B字节,单这些负载就约占kB字节,尚不含请求对象、切片容量及HTTP库缓冲。比如假设10批、每批1,000,000字节,负载本身就是10,000,000字节,约9.54 MiB。不能把scMaxBufferedPosts的名字直接解释成进程总内存绝不会超过某个数;通道、重排堆、在读请求与其他会话还各有状态。
流量控制的窗口又限制在途数据。某段路径往返时延R、希望持续速率v,其带宽时延积为vR。例如人为选取100 Mbit/s和80 ms,乘积为8 Mbit,约1,000,000字节。如果持续下载流的可用窗口远小于这个量,即使链路容量足够,也可能因等待窗口更新而不能持续填满链路。HTTP/2的流窗口与连接窗口、QUIC的流与连接额度,以及TCP自身的窗口和拥塞窗口,须分别分析。仅调大应用缓冲不能越过下层未开放的发送额度。
背压沿相反方向传播。目标网站读取慢,会令服务端向目标写入变慢,VLESS读取上传队列变慢,上传请求的读取又可能受阻;再向前是HTTP窗口、客户端上传管道和本地应用Write。合理背压使内存不必无界增长,但用户感受到的“卡住”可能正是这条链在工作。若用无限缓冲掩盖它,短期写入看似顺畅,长期只是把延迟转移到内存队列。
客户端和源站之间若存在CDN,前后段窗口与拥塞控制分别推进。边缘节点可以先接收大量客户端数据,再慢慢向源站转发,令客户端侧的写入完成时间与源站交付时间显著不同。反过来,即使源站已Flush,边缘到客户端的拥塞仍会阻止立即到达。一次局部Write或WroteRequest事件并没有跨越所有这些阶段。
CPU开销也分成不同工作:HTTP字段生成和压缩、请求对象分配、分批排序、Body读写、安全层AEAD及网络系统调用。Header/Cookie承载负载还要增加Base64编码。无填充Base64URL对n字节产生ceil(4n/3)个字符,因此单算编码体积,大块数据趋近原始大小的4/3;还未加字段名称、分段编号和HTTP压缩的实际效果。1000字节对应1334字符,不应把这部分当成免费的布局变化。
这些处理大多需要应用层理解字节。即使内侧使用Vision并跳过某层Encryption记录,外侧XHTTP仍须形成HTTP Body、处理帧与安全连接。第23章所讲的“已经到达裸TCP且所有就绪条件满足后可能使用splice”不能直接跨越一个仍在工作的HTTP Transport。是否避免某次拷贝,也不等于整条路径零CPU、零内存访问。
XMUX改变哪些请求共享HTTP客户端,因而可改变握手频率、拥塞状态共享和故障集中范围。它不能保证更多连接一定更快:多条拥塞控制实例可能争抢同一瓶颈,也会占用更多状态。长流减少重复HTTP头,分批流更容易经过某些缓冲入口,二者都是机制上的取舍。没有相同链路、相同负载和相同实现版本的测量,不能给出普遍的性能名次。
具体批量与调度见固定dialer.go,暂存与重排见upload_queue.go,Base64布局见config.go。本节数值均为注明假设的成本算术,没有进行吞吐基准测试。
24.12 让部署条件与安全职责对齐
读一个XHTTP方案,首先应画出HTTP请求实际经过的节点,而不是只列出客户端与源站两端的软件。客户端到边缘节点、边缘到反向代理、反向代理到具体XHTTP处理器,每一段都可能有独立的连接复用、超时、缓冲和负载均衡。逻辑会话需要的是这些节点共同保留足够的信息与进度,而非仅仅支持某个HTTP版本。
三种模式可以用同一组问题比较:
| 模式 | 上行组织 | 下行组织 | 关键部署依赖 |
|---|---|---|---|
| packet-up | 带S和seq的有限请求 | 带S的持续GET响应 | 保留元数据;上传重排;下载持续转发;上下行汇合会话状态 |
| stream-up | 带S、无seq的持续Body | 带S的独立GET响应 | 持续上传及时转发;同一会话状态;两条请求各自的时限 |
| stream-one | 无S的单请求Body | 同一请求的响应Body | 允许双向持续推进;前后段不因等待完整Body阻塞交互 |
packet-up并不让会话无状态。它允许单批HTTP请求结束,但持续下载和上传排序仍依赖活着的处理器。若负载均衡把GET S送到实例A,把POST S/0送到实例B,B可能新建自己的同名会话,A却一直等不到这些字节。普通的“所有请求都能成功返回HTTP响应”无法发现这种分裂。需要保证的是会话状态的汇合;本实现并没有自动提供跨进程分布式共享队列。
分离上下行入口也不等于两边可以任意接到独立Xray实例。DownloadSettings改变客户端对下载侧地址、安全层和HTTP客户端的选择;它没有把两台服务器的sessions表复制成一份。请求最终可以通过不同中转路径进入同一个处理器,也可以由外部架构保证所需汇合,但不能把配置字段本身当作状态同步协议。
超时应按其所测对象判断。有的是等待请求头,有的是网络空闲,有的是上游响应间隔,还有的是整个请求最长寿命。stream-up响应中周期写出的填充只影响它真正到达的那一段、那一方向;不保证持续GET、上传Body和整个边缘会话都被视为活动。若服务提供方对总请求寿命设硬上限,周期填充也未必能延长。
安全职责则应逐层标记。外层普通TLS认证所连接的HTTP入口;若在边缘终结,保护终点也在那里。REALITY路径要求客户端最终完成与相应REALITY服务端的专用验证,不能让普通HTTP终结节点替代。VLESS负责传达代理命令及用户标识;Encryption可在承载之上建立自己的记录保护,但Vision切换后保留什么层必须按第23章判断;网站TLS认证真正的网站。某层能连接,不会自动完成另外几层的认证。
HTTP层的S只是会话关联值。seq是排序信息。Padding是格式与形状控制。它们都不是需要秘密密钥才能计算的完整性校验,也不能替代UUID和安全握手。尤其是普通TLS在边缘终结时,边缘可读取或修改这些HTTP元素;其信任边界必须写进架构,而不是因路径看似随机就忽略。
错误也沿不同层暴露:入口的404可能是路径检查失败;大Header可能由反向代理先行拒绝;POST的413表示批次或重建负载超过允许大小;持续响应在200后结束,则可能来自后续HTTP流错误、VLESS拒绝或目标连接失败。HTTP响应码只能解释它所在层已经发生的事,不能独自定位整条代理链。
在需求确定后,模式选择才有意义。只能可靠处理有限上传请求的中间节点,为packet-up提供了理由;能稳定处理持续Body和独立下载的路径,可以考虑stream-up;支持单请求双向持续推进的端到端路径,才有stream-one的前提。HTTP/2、HTTP/3、XMUX和Padding分别解决承载、复用与外观问题,不能彼此替代。下一章将固定一组兼容配置,把这些职责重新落到浏览器、两端代理及目标网站的完整处理过程上。
本节部署限制由固定hub.go的本地sessions表及dialer.go的DownloadSettings处理推导;它不是任一CDN产品的兼容性承诺。入口限制、重连行为和超时仍应以具体版本与实际网络证据为准。