feat(download): 分块路径写续传元数据并自证旧字节(M293)
台账 P21 第 2 条 + P19(b):分块路径此前进来先 `part.discard()`、失败也 `discard()`, 188MB 的 JDK 任何一次失败都从 0 重下;M289 又给预算耗尽加了「不再重试、不再换源」的 短路,慢链路上一次晚到的失败就变成「这个文件下不来」。 写这一侧: * `ResumeMeta` 新增 `chunks: Option<Vec<ChunkRecord>>`(段区间 + 该段 SHA-1), 由新增的 `chunk_resume::ChunkJournal` 每成功回收一段就原子落一次盘; * `fetch_chunk` 顺带带回段摘要、`Content-Range` 总长与 ETag/Last-Modified, 所以元数据里同时有 total / 源 URL / 分块清单 / 校验所需信息; * 分块失败不再丢中间文件,只有 `RangeUnsupported`(单连接接不住这种形状)才丢。 判这一侧: * `chunk_resume::decide`(纯函数)要求策略允许、URL / 总长 / 摘要 / 校验器全对得上, 且每条记录精确命中本次分块规划里的一段,否则从头再来; * `chunk_resume::verify_records` 按记录重读旧段、重算摘要:任何一段对不上就整份丢掉 (元数据本身已不可信,同批其它记录也不再被信任); * 续传的段请求带 `If-Range`,让服务器回答「内容变了没有」; * 整文件摘要照旧在末尾整份重算,覆盖新下 + 复用的全部字节。 单连接路径零改动:`resume::decide` 的判定一字未动,只新增 「元数据里带分块清单 ⇒ 强行判 Restart 交给分块路径」这一条,避免预分配全长文件被 单连接的 `Complete` 误判成「已下满」。 判据(都是「服务端实际吐了多少字节」,不是「最终 hash 对」): * 3/4 处失败后续传完成:41984 字节 / 40960 文件 = 1.025×(断言 < 1.5×); * 变异(关掉续传同样夹具):72704 = 1.775×,`< 1.5×` 必红(已实测); * 变异(去掉旧段摘要自证):坏段被复用 ⇒ 51200 ≠ 40960 必红(已实测); * 变异(去掉总长比较):旧大小的段被复用 ⇒ 61440 ≠ 40960 必红(已实测)。
This commit is contained in:
1 parent
6e1d5fa82e
commit
3ade2d14a8
13 files changed
+1428
-359
No files matched your search
@@ -1,17 +1,17 @@
|
||||
//! 测试支撑:临时目录、摘要工具、进程内极简 HTTP 服务器(零额外依赖)。
|
||||
//!
|
||||
//! 为什么自己写 mock server 而不用 wiremock:P0 阶段控制依赖面(Win7 Tier-3 构建 +
|
||||
//! 单文件体积),一个 60 行的 `TcpListener` 足够表达「成功 / 500 / 校验不符 / 无视 Range」。
|
||||
//! 单文件体积),一个 `TcpListener` + 脚本队列足够表达「成功 / 500 / 校验不符 / 无视 Range」。
|
||||
//! 服务器本体拆在 [`server`] 里(守 400 行硬线),这里只做再导出,调用方路径不变。
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
mod server;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
pub use self::server::{MockResponse, MockServer};
|
||||
|
||||
/// 断言用的小工具:唯一临时目录(进程内计数 + 时间戳,避免并行测试互相踩)。
|
||||
pub fn temp_dir(tag: &str) -> std::path::PathBuf {
|
||||
@@ -61,328 +61,3 @@ pub fn age_file(path: &std::path::Path, age: std::time::Duration) {
|
||||
.set_times(times)
|
||||
.expect("改 mtime 失败");
|
||||
}
|
||||
|
||||
/// mock 服务器给一次请求的应答。
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MockResponse {
|
||||
/// HTTP 状态码。
|
||||
pub status: u16,
|
||||
/// 响应体。
|
||||
pub body: Vec<u8>,
|
||||
/// 是否声明 `Accept-Ranges: bytes` 并在收到 `Range` 时真的切段。
|
||||
pub range_support: bool,
|
||||
/// `ETag`(续传测试用)。
|
||||
pub etag: Option<String>,
|
||||
/// 返回 206 时故意把 `Content-Range` 写成从 0 开始(模拟段错位的坏服务器)。
|
||||
pub lie_content_range: bool,
|
||||
/// 读完请求头后**一个字节都不写**(服务端接了连接但永不回话)。
|
||||
///
|
||||
/// 这是「无界 `await`」唯一能表达成用例的形态:读方不设上界就永远不返回。
|
||||
pub silent: bool,
|
||||
/// 写出这么多字节正文后就**停住不再写**(中途卡住),连接保持打开。
|
||||
pub stall_at: Option<usize>,
|
||||
/// 正文实际只发这么多字节(头部仍声称全量 = 短包)。
|
||||
pub send_len: Option<usize>,
|
||||
/// 正文之后**多**发这么多字节,且故意不发 `Content-Length`(超长包:HTTP/1.1 靠关连接分帧)。
|
||||
pub trailing_extra: Option<usize>,
|
||||
/// 正文按 `piece` 字节一片、片间睡 `gap` 滴流(慢但持续,用来证明超时不会误杀它)。
|
||||
pub drip: Option<(usize, Duration)>,
|
||||
}
|
||||
|
||||
/// 夹具「挂住」的时长:远大于用例自己的上界,保证读方只能靠超时脱身。
|
||||
const HOLD: Duration = Duration::from_secs(30);
|
||||
|
||||
impl MockResponse {
|
||||
/// 200 + 完整内容(支持 Range)。
|
||||
pub fn ok(body: impl Into<Vec<u8>>) -> Self {
|
||||
Self {
|
||||
status: 200,
|
||||
body: body.into(),
|
||||
range_support: true,
|
||||
etag: Some("\"v1\"".to_string()),
|
||||
lie_content_range: false,
|
||||
silent: false,
|
||||
stall_at: None,
|
||||
send_len: None,
|
||||
trailing_extra: None,
|
||||
drip: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 固定状态码 + 空体。
|
||||
pub fn status(status: u16) -> Self {
|
||||
Self {
|
||||
status,
|
||||
body: Vec::new(),
|
||||
range_support: false,
|
||||
etag: None,
|
||||
lie_content_range: false,
|
||||
silent: false,
|
||||
stall_at: None,
|
||||
send_len: None,
|
||||
trailing_extra: None,
|
||||
drip: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 200 但**永不回话**:读完请求头就挂住,一个字都不写。
|
||||
pub fn silent() -> Self {
|
||||
Self {
|
||||
silent: true,
|
||||
..Self::ok(Vec::new())
|
||||
}
|
||||
}
|
||||
|
||||
/// 写完 `bytes` 字节正文后停住(中途卡住,连接不断)。
|
||||
pub fn stalling_after(mut self, bytes: usize) -> Self {
|
||||
self.stall_at = Some(bytes);
|
||||
self
|
||||
}
|
||||
|
||||
/// 头部声称全量、正文只发 `sent` 字节(短包,客户端读不满)。
|
||||
pub fn short_body(mut self, sent: usize) -> Self {
|
||||
self.send_len = Some(sent);
|
||||
self
|
||||
}
|
||||
|
||||
/// 正文之后多发 `extra` 字节且不发 `Content-Length`(超长包)。
|
||||
pub fn oversize_body(mut self, extra: usize) -> Self {
|
||||
self.trailing_extra = Some(extra);
|
||||
self
|
||||
}
|
||||
|
||||
/// 正文按 `piece` 字节一片、片间 `gap` 滴流(慢但持续)。
|
||||
pub fn dripping(mut self, piece: usize, gap: Duration) -> Self {
|
||||
self.drip = Some((piece, gap));
|
||||
self
|
||||
}
|
||||
|
||||
/// 声明不支持 Range(模拟部分镜像)。
|
||||
pub fn without_range(mut self) -> Self {
|
||||
self.range_support = false;
|
||||
self
|
||||
}
|
||||
|
||||
/// 206 但 `Content-Range` 谎报(模拟段错位的坏 CDN)。
|
||||
pub fn with_wrong_content_range(mut self) -> Self {
|
||||
self.lie_content_range = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// 进程内极简 HTTP/1.1 服务器:按脚本顺序返回响应,脚本用尽后一律 500。
|
||||
pub struct MockServer {
|
||||
addr: SocketAddr,
|
||||
hits: Arc<AtomicUsize>,
|
||||
handle: tokio::task::JoinHandle<()>,
|
||||
}
|
||||
|
||||
impl MockServer {
|
||||
/// 启动服务器(脚本是「每次请求一个响应」的队列)。
|
||||
pub async fn start(script: Vec<MockResponse>) -> Self {
|
||||
let listener = TcpListener::bind("127.0.0.1:0")
|
||||
.await
|
||||
.expect("绑定端口失败");
|
||||
let addr = listener.local_addr().expect("取本地地址失败");
|
||||
let hits = Arc::new(AtomicUsize::new(0));
|
||||
let script = Arc::new(std::sync::Mutex::new(script));
|
||||
let counter = Arc::clone(&hits);
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
loop {
|
||||
let Ok((stream, _)) = listener.accept().await else {
|
||||
break;
|
||||
};
|
||||
let script = Arc::clone(&script);
|
||||
let counter = Arc::clone(&counter);
|
||||
tokio::spawn(async move {
|
||||
let index = counter.fetch_add(1, Ordering::Relaxed);
|
||||
let response = {
|
||||
let script = script.lock().unwrap_or_else(|err| err.into_inner());
|
||||
script.get(index).cloned()
|
||||
};
|
||||
let response = response.unwrap_or_else(|| MockResponse::status(500));
|
||||
let _ = serve(stream, response).await;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Self { addr, hits, handle }
|
||||
}
|
||||
|
||||
/// 第 `index` 个请求该用的 URL(换成不同路径可模拟「不同源」)。
|
||||
pub fn url(&self, path: &str) -> String {
|
||||
format!("http://{}{}", self.addr, path)
|
||||
}
|
||||
|
||||
/// 换 host 拼 URL(用 `localhost` vs `127.0.0.1` 造出两个不同源)。
|
||||
pub fn url_with(&self, host: &str, path: &str) -> String {
|
||||
format!("http://{host}:{}{}", self.addr.port(), path)
|
||||
}
|
||||
|
||||
/// 端口号。
|
||||
pub fn port(&self) -> u16 {
|
||||
self.addr.port()
|
||||
}
|
||||
|
||||
/// 已收到的请求数。
|
||||
pub fn hits(&self) -> usize {
|
||||
self.hits.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// 关停。
|
||||
pub fn stop(self) {
|
||||
self.handle.abort();
|
||||
}
|
||||
}
|
||||
|
||||
async fn serve(mut stream: TcpStream, response: MockResponse) -> std::io::Result<()> {
|
||||
let mut buffer = Vec::new();
|
||||
let mut chunk = [0_u8; 1024];
|
||||
loop {
|
||||
let read = stream.read(&mut chunk).await?;
|
||||
if read == 0 {
|
||||
break;
|
||||
}
|
||||
buffer.extend_from_slice(&chunk[..read]);
|
||||
if buffer.windows(4).any(|window| window == b"\r\n\r\n") {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let request = String::from_utf8_lossy(&buffer).to_ascii_lowercase();
|
||||
let range = request
|
||||
.lines()
|
||||
.find_map(|line| line.strip_prefix("range: bytes="))
|
||||
.map(str::trim)
|
||||
.and_then(parse_range_header);
|
||||
|
||||
if response.silent {
|
||||
// 接了连接、读完请求头、一个字都不回:读方不设上界就永远挂在这里。
|
||||
tokio::time::sleep(HOLD).await;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let total = response.body.len() as u64;
|
||||
// 超长包不能发 `Content-Length`,否则读方按声明截断、永远看不到多余字节。
|
||||
let with_length = response.trailing_extra.is_none();
|
||||
let (status, headers, body) = match (range, response.range_support) {
|
||||
(Some((start, requested_end)), true) if start < total => {
|
||||
// 尊重请求里的结束偏移(分块下载必须逐段精确,否则组装后长度对不上)。
|
||||
let end = requested_end.unwrap_or(total - 1).min(total - 1);
|
||||
let slice = response.body[start as usize..=end as usize].to_vec();
|
||||
// 谎报模式:body 仍是正确的那一段,但头部声称从 0 开始(长度恰好 → 只有
|
||||
// 严格比对 Content-Range 才抓得住)。
|
||||
let (claimed_start, claimed_end) = if response.lie_content_range {
|
||||
(0, slice.len().saturating_sub(1) as u64)
|
||||
} else {
|
||||
(start, end)
|
||||
};
|
||||
let mut headers = vec![
|
||||
format!("Content-Range: bytes {claimed_start}-{claimed_end}/{total}"),
|
||||
"Accept-Ranges: bytes".to_string(),
|
||||
];
|
||||
if with_length {
|
||||
headers.push(format!("Content-Length: {}", slice.len()));
|
||||
}
|
||||
(206, headers, slice)
|
||||
}
|
||||
(Some(_), true) => (
|
||||
416,
|
||||
vec![
|
||||
format!("Content-Range: bytes */{total}"),
|
||||
"Content-Length: 0".to_string(),
|
||||
],
|
||||
Vec::new(),
|
||||
),
|
||||
_ => {
|
||||
let mut headers = Vec::new();
|
||||
if with_length {
|
||||
headers.push(format!("Content-Length: {total}"));
|
||||
}
|
||||
if response.range_support {
|
||||
headers.push("Accept-Ranges: bytes".to_string());
|
||||
}
|
||||
(response.status, headers, response.body.clone())
|
||||
}
|
||||
};
|
||||
let mut head = format!("HTTP/1.1 {status} {}\r\n", reason(status));
|
||||
if let Some(etag) = &response.etag {
|
||||
head.push_str(&format!("ETag: {etag}\r\n"));
|
||||
}
|
||||
for header in headers {
|
||||
head.push_str(&header);
|
||||
head.push_str("\r\n");
|
||||
}
|
||||
head.push_str("Connection: close\r\n\r\n");
|
||||
stream.write_all(head.as_bytes()).await?;
|
||||
write_body(&mut stream, &body, &response).await?;
|
||||
stream.flush().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 按夹具的口径写正文:正常 / 卡在中途 / 短包 / 超长尾巴 / 滴流。
|
||||
async fn write_body(
|
||||
stream: &mut TcpStream,
|
||||
body: &[u8],
|
||||
response: &MockResponse,
|
||||
) -> std::io::Result<()> {
|
||||
if let Some(stall_at) = response.stall_at {
|
||||
// 头部 + 这么多字节已发出,之后一个字不再写(连接不断):读方只能靠超时脱身。
|
||||
let sent = stall_at.min(body.len());
|
||||
stream.write_all(&body[..sent]).await?;
|
||||
stream.flush().await?;
|
||||
tokio::time::sleep(HOLD).await;
|
||||
return Ok(());
|
||||
}
|
||||
let limit = response.send_len.unwrap_or(body.len()).min(body.len());
|
||||
match response.drip {
|
||||
Some((piece, gap)) => {
|
||||
let piece = piece.max(1);
|
||||
let mut offset = 0;
|
||||
while offset < limit {
|
||||
let end = (offset + piece).min(limit);
|
||||
stream.write_all(&body[offset..end]).await?;
|
||||
stream.flush().await?;
|
||||
offset = end;
|
||||
if offset < limit {
|
||||
tokio::time::sleep(gap).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
stream.write_all(&body[..limit]).await?;
|
||||
stream.flush().await?;
|
||||
}
|
||||
}
|
||||
if let Some(extra) = response.trailing_extra {
|
||||
let filler = vec![0_u8; extra];
|
||||
stream.write_all(&filler).await?;
|
||||
stream.flush().await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 解析 `Range: bytes=<start>-<end>`(`end` 可缺省 = 到文件末尾)。
|
||||
fn parse_range_header(value: &str) -> Option<(u64, Option<u64>)> {
|
||||
let (start, end) = value.split_once('-')?;
|
||||
let start = start.trim().parse::<u64>().ok()?;
|
||||
let end = end.trim();
|
||||
let end = if end.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(end.parse::<u64>().ok()?)
|
||||
};
|
||||
Some((start, end))
|
||||
}
|
||||
|
||||
fn reason(status: u16) -> &'static str {
|
||||
match status {
|
||||
200 => "OK",
|
||||
206 => "Partial Content",
|
||||
404 => "Not Found",
|
||||
416 => "Range Not Satisfiable",
|
||||
500 => "Internal Server Error",
|
||||
503 => "Service Unavailable",
|
||||
_ => "Status",
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,368 @@
|
||||
//! 进程内极简 HTTP/1.1 夹具服务器(零额外依赖)。
|
||||
//!
|
||||
//! 为什么自己写而不用 wiremock:P0 阶段控制依赖面(Win7 Tier-3 构建 + 单文件体积),一个
|
||||
//! `TcpListener` + 脚本队列足够表达「成功 / 500 / 校验不符 / 无视 Range / 中途卡住」。
|
||||
//!
|
||||
//! 正文写出的字节数记在 [`MockServer::bytes_sent`] 里:续传用例的判据是「服务端到底吐了
|
||||
//! 多少字节」,只看最终 hash 抓不到「其实整份又下了一遍」(见 `chunked/resume.rs`)。
|
||||
//!
|
||||
//! 从 `support/mod.rs` 拆出只为守 400 行硬线(M293)。
|
||||
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
|
||||
/// mock 服务器给一次请求的应答。
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MockResponse {
|
||||
/// HTTP 状态码。
|
||||
pub status: u16,
|
||||
/// 响应体。
|
||||
pub body: Vec<u8>,
|
||||
/// 是否声明 `Accept-Ranges: bytes` 并在收到 `Range` 时真的切段。
|
||||
pub range_support: bool,
|
||||
/// `ETag`(续传测试用)。
|
||||
pub etag: Option<String>,
|
||||
/// 返回 206 时故意把 `Content-Range` 写成从 0 开始(模拟段错位的坏服务器)。
|
||||
pub lie_content_range: bool,
|
||||
/// 读完请求头后**一个字节都不写**(服务端接了连接但永不回话)。
|
||||
///
|
||||
/// 这是「无界 `await`」唯一能表达成用例的形态:读方不设上界就永远不返回。
|
||||
pub silent: bool,
|
||||
/// 写出这么多字节正文后就**停住不再写**(中途卡住),连接保持打开。
|
||||
pub stall_at: Option<usize>,
|
||||
/// 正文实际只发这么多字节(头部仍声称全量 = 短包)。
|
||||
pub send_len: Option<usize>,
|
||||
/// 正文之后**多**发这么多字节,且故意不发 `Content-Length`(超长包:HTTP/1.1 靠关连接分帧)。
|
||||
pub trailing_extra: Option<usize>,
|
||||
/// 正文按 `piece` 字节一片、片间睡 `gap` 滴流(慢但持续,用来证明超时不会误杀它)。
|
||||
pub drip: Option<(usize, Duration)>,
|
||||
}
|
||||
|
||||
/// 夹具「挂住」的时长:远大于用例自己的上界,保证读方只能靠超时脱身。
|
||||
const HOLD: Duration = Duration::from_secs(30);
|
||||
|
||||
impl MockResponse {
|
||||
/// 200 + 完整内容(支持 Range)。
|
||||
pub fn ok(body: impl Into<Vec<u8>>) -> Self {
|
||||
Self {
|
||||
status: 200,
|
||||
body: body.into(),
|
||||
range_support: true,
|
||||
etag: Some("\"v1\"".to_string()),
|
||||
lie_content_range: false,
|
||||
silent: false,
|
||||
stall_at: None,
|
||||
send_len: None,
|
||||
trailing_extra: None,
|
||||
drip: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 固定状态码 + 空体。
|
||||
pub fn status(status: u16) -> Self {
|
||||
Self {
|
||||
status,
|
||||
body: Vec::new(),
|
||||
range_support: false,
|
||||
etag: None,
|
||||
lie_content_range: false,
|
||||
silent: false,
|
||||
stall_at: None,
|
||||
send_len: None,
|
||||
trailing_extra: None,
|
||||
drip: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 200 但**永不回话**:读完请求头就挂住,一个字都不写。
|
||||
pub fn silent() -> Self {
|
||||
Self {
|
||||
silent: true,
|
||||
..Self::ok(Vec::new())
|
||||
}
|
||||
}
|
||||
|
||||
/// 写完 `bytes` 字节正文后停住(中途卡住,连接不断)。
|
||||
pub fn stalling_after(mut self, bytes: usize) -> Self {
|
||||
self.stall_at = Some(bytes);
|
||||
self
|
||||
}
|
||||
|
||||
/// 头部声称全量、正文只发 `sent` 字节(短包,客户端读不满)。
|
||||
pub fn short_body(mut self, sent: usize) -> Self {
|
||||
self.send_len = Some(sent);
|
||||
self
|
||||
}
|
||||
|
||||
/// 正文之后多发 `extra` 字节且不发 `Content-Length`(超长包)。
|
||||
pub fn oversize_body(mut self, extra: usize) -> Self {
|
||||
self.trailing_extra = Some(extra);
|
||||
self
|
||||
}
|
||||
|
||||
/// 正文按 `piece` 字节一片、片间 `gap` 滴流(慢但持续)。
|
||||
pub fn dripping(mut self, piece: usize, gap: Duration) -> Self {
|
||||
self.drip = Some((piece, gap));
|
||||
self
|
||||
}
|
||||
|
||||
/// 声明不支持 Range(模拟部分镜像)。
|
||||
pub fn without_range(mut self) -> Self {
|
||||
self.range_support = false;
|
||||
self
|
||||
}
|
||||
|
||||
/// 206 但 `Content-Range` 谎报(模拟段错位的坏 CDN)。
|
||||
pub fn with_wrong_content_range(mut self) -> Self {
|
||||
self.lie_content_range = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// 进程内极简 HTTP/1.1 服务器:按脚本顺序返回响应,脚本用尽后一律 500。
|
||||
pub struct MockServer {
|
||||
addr: SocketAddr,
|
||||
hits: Arc<AtomicUsize>,
|
||||
bytes: Arc<std::sync::atomic::AtomicU64>,
|
||||
handle: tokio::task::JoinHandle<()>,
|
||||
}
|
||||
|
||||
impl MockServer {
|
||||
/// 启动服务器(脚本是「每次请求一个响应」的队列)。
|
||||
pub async fn start(script: Vec<MockResponse>) -> Self {
|
||||
let listener = TcpListener::bind("127.0.0.1:0")
|
||||
.await
|
||||
.expect("绑定端口失败");
|
||||
let addr = listener.local_addr().expect("取本地地址失败");
|
||||
let hits = Arc::new(AtomicUsize::new(0));
|
||||
let bytes = Arc::new(std::sync::atomic::AtomicU64::new(0));
|
||||
let script = Arc::new(std::sync::Mutex::new(script));
|
||||
let counter = Arc::clone(&hits);
|
||||
let body_bytes = Arc::clone(&bytes);
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
loop {
|
||||
let Ok((stream, _)) = listener.accept().await else {
|
||||
break;
|
||||
};
|
||||
let script = Arc::clone(&script);
|
||||
let counter = Arc::clone(&counter);
|
||||
let bytes = Arc::clone(&body_bytes);
|
||||
tokio::spawn(async move {
|
||||
let index = counter.fetch_add(1, Ordering::Relaxed);
|
||||
let response = {
|
||||
let script = script.lock().unwrap_or_else(|err| err.into_inner());
|
||||
script.get(index).cloned()
|
||||
};
|
||||
let response = response.unwrap_or_else(|| MockResponse::status(500));
|
||||
let _ = serve(stream, response, &bytes).await;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Self {
|
||||
addr,
|
||||
hits,
|
||||
bytes,
|
||||
handle,
|
||||
}
|
||||
}
|
||||
|
||||
/// 第 `index` 个请求该用的 URL(换成不同路径可模拟「不同源」)。
|
||||
pub fn url(&self, path: &str) -> String {
|
||||
format!("http://{}{}", self.addr, path)
|
||||
}
|
||||
|
||||
/// 换 host 拼 URL(用 `localhost` vs `127.0.0.1` 造出两个不同源)。
|
||||
pub fn url_with(&self, host: &str, path: &str) -> String {
|
||||
format!("http://{host}:{}{}", self.addr.port(), path)
|
||||
}
|
||||
|
||||
/// 端口号。
|
||||
pub fn port(&self) -> u16 {
|
||||
self.addr.port()
|
||||
}
|
||||
|
||||
/// 已收到的请求数。
|
||||
pub fn hits(&self) -> usize {
|
||||
self.hits.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// 已发出的**正文字节**总数(不含响应头)。
|
||||
///
|
||||
/// 续传用例的判据靠它:「最终 hash 对」证明不了没白下字节,只有统计服务端真正吐了多少
|
||||
/// 才知道是不是又整份下了一遍(见 `chunked/resume.rs`)。
|
||||
pub fn bytes_sent(&self) -> u64 {
|
||||
self.bytes.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// 关停。
|
||||
pub fn stop(self) {
|
||||
self.handle.abort();
|
||||
}
|
||||
}
|
||||
|
||||
async fn serve(
|
||||
mut stream: TcpStream,
|
||||
response: MockResponse,
|
||||
sent: &std::sync::atomic::AtomicU64,
|
||||
) -> std::io::Result<()> {
|
||||
let mut buffer = Vec::new();
|
||||
let mut chunk = [0_u8; 1024];
|
||||
loop {
|
||||
let read = stream.read(&mut chunk).await?;
|
||||
if read == 0 {
|
||||
break;
|
||||
}
|
||||
buffer.extend_from_slice(&chunk[..read]);
|
||||
if buffer.windows(4).any(|window| window == b"\r\n\r\n") {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let request = String::from_utf8_lossy(&buffer).to_ascii_lowercase();
|
||||
let range = request
|
||||
.lines()
|
||||
.find_map(|line| line.strip_prefix("range: bytes="))
|
||||
.map(str::trim)
|
||||
.and_then(parse_range_header);
|
||||
|
||||
if response.silent {
|
||||
// 接了连接、读完请求头、一个字都不回:读方不设上界就永远挂在这里。
|
||||
tokio::time::sleep(HOLD).await;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let total = response.body.len() as u64;
|
||||
// 超长包不能发 `Content-Length`,否则读方按声明截断、永远看不到多余字节。
|
||||
let with_length = response.trailing_extra.is_none();
|
||||
let (status, headers, body) = match (range, response.range_support) {
|
||||
(Some((start, requested_end)), true) if start < total => {
|
||||
// 尊重请求里的结束偏移(分块下载必须逐段精确,否则组装后长度对不上)。
|
||||
let end = requested_end.unwrap_or(total - 1).min(total - 1);
|
||||
let slice = response.body[start as usize..=end as usize].to_vec();
|
||||
// 谎报模式:body 仍是正确的那一段,但头部声称从 0 开始(长度恰好 → 只有
|
||||
// 严格比对 Content-Range 才抓得住)。
|
||||
let (claimed_start, claimed_end) = if response.lie_content_range {
|
||||
(0, slice.len().saturating_sub(1) as u64)
|
||||
} else {
|
||||
(start, end)
|
||||
};
|
||||
let mut headers = vec![
|
||||
format!("Content-Range: bytes {claimed_start}-{claimed_end}/{total}"),
|
||||
"Accept-Ranges: bytes".to_string(),
|
||||
];
|
||||
if with_length {
|
||||
headers.push(format!("Content-Length: {}", slice.len()));
|
||||
}
|
||||
(206, headers, slice)
|
||||
}
|
||||
(Some(_), true) => (
|
||||
416,
|
||||
vec![
|
||||
format!("Content-Range: bytes */{total}"),
|
||||
"Content-Length: 0".to_string(),
|
||||
],
|
||||
Vec::new(),
|
||||
),
|
||||
_ => {
|
||||
let mut headers = Vec::new();
|
||||
if with_length {
|
||||
headers.push(format!("Content-Length: {total}"));
|
||||
}
|
||||
if response.range_support {
|
||||
headers.push("Accept-Ranges: bytes".to_string());
|
||||
}
|
||||
(response.status, headers, response.body.clone())
|
||||
}
|
||||
};
|
||||
let mut head = format!("HTTP/1.1 {status} {}\r\n", reason(status));
|
||||
if let Some(etag) = &response.etag {
|
||||
head.push_str(&format!("ETag: {etag}\r\n"));
|
||||
}
|
||||
for header in headers {
|
||||
head.push_str(&header);
|
||||
head.push_str("\r\n");
|
||||
}
|
||||
head.push_str("Connection: close\r\n\r\n");
|
||||
stream.write_all(head.as_bytes()).await?;
|
||||
write_body(&mut stream, &body, &response, sent).await?;
|
||||
stream.flush().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 按夹具的口径写正文:正常 / 卡在中途 / 短包 / 超长尾巴 / 滴流。发出的正文逐块计数。
|
||||
async fn write_body(
|
||||
stream: &mut TcpStream,
|
||||
body: &[u8],
|
||||
response: &MockResponse,
|
||||
sent: &std::sync::atomic::AtomicU64,
|
||||
) -> std::io::Result<()> {
|
||||
if let Some(stall_at) = response.stall_at {
|
||||
// 头部 + 这么多字节已发出,之后一个字不再写(连接不断):读方只能靠超时脱身。
|
||||
let sent_len = stall_at.min(body.len());
|
||||
stream.write_all(&body[..sent_len]).await?;
|
||||
stream.flush().await?;
|
||||
sent.fetch_add(sent_len as u64, Ordering::Relaxed);
|
||||
tokio::time::sleep(HOLD).await;
|
||||
return Ok(());
|
||||
}
|
||||
let limit = response.send_len.unwrap_or(body.len()).min(body.len());
|
||||
match response.drip {
|
||||
Some((piece, gap)) => {
|
||||
let piece = piece.max(1);
|
||||
let mut offset = 0;
|
||||
while offset < limit {
|
||||
let end = (offset + piece).min(limit);
|
||||
stream.write_all(&body[offset..end]).await?;
|
||||
stream.flush().await?;
|
||||
sent.fetch_add((end - offset) as u64, Ordering::Relaxed);
|
||||
offset = end;
|
||||
if offset < limit {
|
||||
tokio::time::sleep(gap).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
stream.write_all(&body[..limit]).await?;
|
||||
stream.flush().await?;
|
||||
sent.fetch_add(limit as u64, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
if let Some(extra) = response.trailing_extra {
|
||||
let filler = vec![0_u8; extra];
|
||||
stream.write_all(&filler).await?;
|
||||
stream.flush().await?;
|
||||
sent.fetch_add(extra as u64, Ordering::Relaxed);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 解析 `Range: bytes=<start>-<end>`(`end` 可缺省 = 到文件末尾)。
|
||||
fn parse_range_header(value: &str) -> Option<(u64, Option<u64>)> {
|
||||
let (start, end) = value.split_once('-')?;
|
||||
let start = start.trim().parse::<u64>().ok()?;
|
||||
let end = end.trim();
|
||||
let end = if end.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(end.parse::<u64>().ok()?)
|
||||
};
|
||||
Some((start, end))
|
||||
}
|
||||
|
||||
fn reason(status: u16) -> &'static str {
|
||||
match status {
|
||||
200 => "OK",
|
||||
206 => "Partial Content",
|
||||
404 => "Not Found",
|
||||
416 => "Range Not Satisfiable",
|
||||
500 => "Internal Server Error",
|
||||
503 => "Service Unavailable",
|
||||
_ => "Status",
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user