Files
waf-platform/EdgeHttpDNS/sdk/ios/AlicloudHttpDNSTests/Network/HttpdnsNWHTTPClient_EdgeCasesAndTimeoutTests.m
2026-02-20 17:56:24 +08:00

741 lines
37 KiB
Objective-C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// HttpdnsNWHTTPClient_EdgeCasesAndTimeoutTests.m
// AlicloudHttpDNSTests
//
// @author Created by Claude Code on 2025-11-01
// Copyright © 2025 alibaba-inc.com. All rights reserved.
//
// 边界条件与超时测试 - 包含边界条件 (M)、超时交互 (P) 和 Connection 头部 (R) 测试组
// 测试总数15 个M:4 + P:6 + R:5
//
#import "HttpdnsNWHTTPClientTestBase.h"
@interface HttpdnsNWHTTPClient_EdgeCasesAndTimeoutTests : HttpdnsNWHTTPClientTestBase
@end
@implementation HttpdnsNWHTTPClient_EdgeCasesAndTimeoutTests
#pragma mark - M. 边界条件与验证测试
// M.1 连接复用边界:端口内复用,端口间隔离
- (void)testEdgeCase_ConnectionReuseWithinPortOnly_NotAcross {
XCTestExpectation *expectation = [self expectationWithDescription:@"Reuse boundaries"];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 请求 A 到端口 11443
CFAbsoluteTime timeA = CFAbsoluteTimeGetCurrent();
NSError *errorA = nil;
HttpdnsNWHTTPClientResponse *responseA = [self.client performRequestWithURLString:@"https://127.0.0.1:11443/get"
userAgent:@"RequestA"
timeout:15.0
error:&errorA];
CFAbsoluteTime elapsedA = CFAbsoluteTimeGetCurrent() - timeA;
XCTAssertNotNil(responseA);
// 请求 B 到端口 11443应该复用连接可能更快
CFAbsoluteTime timeB = CFAbsoluteTimeGetCurrent();
NSError *errorB = nil;
HttpdnsNWHTTPClientResponse *responseB = [self.client performRequestWithURLString:@"https://127.0.0.1:11443/get"
userAgent:@"RequestB"
timeout:15.0
error:&errorB];
CFAbsoluteTime elapsedB = CFAbsoluteTimeGetCurrent() - timeB;
XCTAssertNotNil(responseB);
// 请求 C 到端口 11444应该创建新连接
CFAbsoluteTime timeC = CFAbsoluteTimeGetCurrent();
NSError *errorC = nil;
HttpdnsNWHTTPClientResponse *responseC = [self.client performRequestWithURLString:@"https://127.0.0.1:11444/get"
userAgent:@"RequestC"
timeout:15.0
error:&errorC];
CFAbsoluteTime elapsedC = CFAbsoluteTimeGetCurrent() - timeC;
XCTAssertNotNil(responseC);
// 请求 D 到端口 11444应该复用端口 11444 的连接)
CFAbsoluteTime timeD = CFAbsoluteTimeGetCurrent();
NSError *errorD = nil;
HttpdnsNWHTTPClientResponse *responseD = [self.client performRequestWithURLString:@"https://127.0.0.1:11444/get"
userAgent:@"RequestD"
timeout:15.0
error:&errorD];
CFAbsoluteTime elapsedD = CFAbsoluteTimeGetCurrent() - timeD;
XCTAssertNotNil(responseD);
// 验证所有请求都成功
XCTAssertEqual(responseA.statusCode, 200);
XCTAssertEqual(responseB.statusCode, 200);
XCTAssertEqual(responseC.statusCode, 200);
XCTAssertEqual(responseD.statusCode, 200);
[expectation fulfill];
});
[self waitForExpectations:@[expectation] timeout:70.0];
}
// M.2 高端口数量压力测试
- (void)testEdgeCase_HighPortCount_AllPortsManaged {
XCTestExpectation *expectation = [self expectationWithDescription:@"High port count"];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSArray<NSNumber *> *ports = @[@11443, @11444, @11445, @11446];
// 第一轮:向所有端口各发起一个请求
for (NSNumber *port in ports) {
NSString *urlString = [NSString stringWithFormat:@"https://127.0.0.1:%@/get", port];
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:urlString
userAgent:@"Round1"
timeout:15.0
error:&error];
XCTAssertNotNil(response, @"First round request to port %@ should succeed", port);
}
// 第二轮:再次向所有端口发起请求(应该复用连接)
for (NSNumber *port in ports) {
NSString *urlString = [NSString stringWithFormat:@"https://127.0.0.1:%@/get", port];
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:urlString
userAgent:@"Round2"
timeout:15.0
error:&error];
XCTAssertNotNil(response, @"Second round request to port %@ should reuse connection", port);
}
[expectation fulfill];
});
[self waitForExpectations:@[expectation] timeout:120.0];
}
// M.3 并发池访问安全性
- (void)testEdgeCase_ConcurrentPoolAccess_NoDataRace {
NSMutableArray<XCTestExpectation *> *expectations = [NSMutableArray array];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSArray<NSNumber *> *ports = @[@11443, @11444, @11445];
NSInteger requestsPerPort = 5;
// 向三个端口并发发起请求
for (NSNumber *port in ports) {
for (NSInteger i = 0; i < requestsPerPort; i++) {
XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"Port %@ Req %ld", port, (long)i]];
[expectations addObject:expectation];
dispatch_async(queue, ^{
NSString *urlString = [NSString stringWithFormat:@"https://127.0.0.1:%@/get", port];
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:urlString
userAgent:@"ConcurrentAccess"
timeout:15.0
error:&error];
// 如果没有崩溃或断言失败,说明并发访问安全
XCTAssertTrue(response != nil || error != nil);
[expectation fulfill];
});
}
}
[self waitForExpectations:expectations timeout:50.0];
}
// M.4 端口迁移模式
- (void)testEdgeCase_PortMigration_OldConnectionsEventuallyExpire {
if (getenv("SKIP_SLOW_TESTS")) {
return;
}
XCTestExpectation *expectation = [self expectationWithDescription:@"Port migration"];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 阶段 1向端口 11443 发起多个请求
for (NSInteger i = 0; i < 5; i++) {
NSError *error = nil;
[self.client performRequestWithURLString:@"https://127.0.0.1:11443/get"
userAgent:@"Port11443"
timeout:15.0
error:&error];
}
// 阶段 2切换到端口 11444发起多个请求
for (NSInteger i = 0; i < 5; i++) {
NSError *error = nil;
[self.client performRequestWithURLString:@"https://127.0.0.1:11444/get"
userAgent:@"Port11444"
timeout:15.0
error:&error];
}
// 等待超过 30 秒,让端口 11443 的连接过期
[NSThread sleepForTimeInterval:31.0];
// 阶段 3验证端口 11444 仍然可用
NSError *error1 = nil;
HttpdnsNWHTTPClientResponse *response1 = [self.client performRequestWithURLString:@"https://127.0.0.1:11444/get"
userAgent:@"Port11444After"
timeout:15.0
error:&error1];
XCTAssertNotNil(response1, @"Port 11444 should still work after 11443 expired");
// 阶段 4端口 11443 应该创建新连接(旧连接已过期)
NSError *error2 = nil;
HttpdnsNWHTTPClientResponse *response2 = [self.client performRequestWithURLString:@"https://127.0.0.1:11443/get"
userAgent:@"Port11443New"
timeout:15.0
error:&error2];
XCTAssertNotNil(response2, @"Port 11443 should work with new connection after expiry");
[expectation fulfill];
});
[self waitForExpectations:@[expectation] timeout:120.0];
}
#pragma mark - P. 超时与连接池交互测试
// P.1 单次超时后连接被正确移除
- (void)testTimeout_SingleRequest_ConnectionRemovedFromPool {
[self.client resetPoolStatistics];
NSString *poolKey = @"127.0.0.1:11080:tcp";
// 验证初始状态
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey], 0,
@"Pool should be empty initially");
// 发起超时请求delay 10s, timeout 1s
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/delay/10"
userAgent:@"TimeoutTest"
timeout:1.0
error:&error];
// 验证请求失败
XCTAssertNil(response, @"Response should be nil on timeout");
XCTAssertNotNil(error, @"Error should be set on timeout");
// 等待异步 returnConnection 完成
[NSThread sleepForTimeInterval:0.5];
// 验证池状态:超时连接应该被移除
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey], 0,
@"Timed-out connection should be removed from pool");
XCTAssertEqual([self.client totalConnectionCount], 0,
@"No connections should remain in pool");
// 验证统计
XCTAssertEqual(self.client.connectionCreationCount, 1,
@"Should have created 1 connection");
XCTAssertEqual(self.client.connectionReuseCount, 0,
@"No reuse for timed-out connection");
}
// P.2 超时后连接池恢复能力
- (void)testTimeout_PoolRecovery_SubsequentRequestSucceeds {
[self.client resetPoolStatistics];
NSString *poolKey = @"127.0.0.1:11080:tcp";
// 第一个请求:超时
NSError *error1 = nil;
HttpdnsNWHTTPClientResponse *response1 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/delay/10"
userAgent:@"TimeoutTest"
timeout:1.0
error:&error1];
XCTAssertNil(response1, @"First request should timeout");
XCTAssertNotNil(error1);
// 等待清理完成
[NSThread sleepForTimeInterval:0.5];
// 第二个请求:正常(验证池已恢复)
NSError *error2 = nil;
HttpdnsNWHTTPClientResponse *response2 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"RecoveryTest"
timeout:15.0
error:&error2];
XCTAssertNotNil(response2, @"Second request should succeed after timeout");
XCTAssertEqual(response2.statusCode, 200);
// 等待 returnConnection
[NSThread sleepForTimeInterval:0.5];
// 验证池恢复:现在应该有 1 个连接(来自第二个请求)
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey], 1,
@"Pool should have 1 connection from second request");
// 第三个请求:应该复用第二个请求的连接
NSError *error3 = nil;
HttpdnsNWHTTPClientResponse *response3 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"ReuseTest"
timeout:15.0
error:&error3];
XCTAssertNotNil(response3);
XCTAssertEqual(response3.statusCode, 200);
// 验证统计1 个超时(创建后移除)+ 1 个成功(创建)+ 1 个复用
XCTAssertEqual(self.client.connectionCreationCount, 2,
@"Should have created 2 connections (1 timed out, 1 succeeded)");
XCTAssertEqual(self.client.connectionReuseCount, 1,
@"Third request should reuse second's connection");
}
// P.3 并发场景:部分超时不影响成功请求的连接复用
- (void)testTimeout_ConcurrentPartialTimeout_SuccessfulRequestsReuse {
[self.client resetPoolStatistics];
NSString *poolKey = @"127.0.0.1:11080:tcp";
NSMutableArray<XCTestExpectation *> *expectations = [NSMutableArray array];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSLock *successCountLock = [[NSLock alloc] init];
NSLock *timeoutCountLock = [[NSLock alloc] init];
__block NSInteger successCount = 0;
__block NSInteger timeoutCount = 0;
// 发起 10 个请求5 个正常5 个超时
for (NSInteger i = 0; i < 10; i++) {
XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"Request %ld", (long)i]];
[expectations addObject:expectation];
dispatch_async(queue, ^{
NSError *error = nil;
NSString *urlString;
NSTimeInterval timeout;
if (i % 2 == 0) {
// 偶数:正常请求
urlString = @"http://127.0.0.1:11080/get";
timeout = 15.0;
} else {
// 奇数:超时请求
urlString = @"http://127.0.0.1:11080/delay/10";
timeout = 0.5;
}
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:urlString
userAgent:@"ConcurrentTest"
timeout:timeout
error:&error];
if (response && response.statusCode == 200) {
[successCountLock lock];
successCount++;
[successCountLock unlock];
} else {
[timeoutCountLock lock];
timeoutCount++;
[timeoutCountLock unlock];
}
[expectation fulfill];
});
}
[self waitForExpectations:expectations timeout:20.0];
// 验证结果
XCTAssertEqual(successCount, 5, @"5 requests should succeed");
XCTAssertEqual(timeoutCount, 5, @"5 requests should timeout");
// 验证连接创建数合理5个成功 + 5个超时 = 最多10个可能有复用
XCTAssertGreaterThan(self.client.connectionCreationCount, 0,
@"Should have created connections for concurrent requests");
XCTAssertLessThanOrEqual(self.client.connectionCreationCount, 10,
@"Should not create more than 10 connections");
// 等待所有连接归还(异步操作需要更长时间)
[NSThread sleepForTimeInterval:2.0];
// 验证总连接数合理(无泄漏)- 关键验证点
// 在并发场景下成功的连接可能已经被关闭remote close池可能为空
XCTAssertLessThanOrEqual([self.client totalConnectionCount], 4,
@"Total connections should not exceed pool limit (no leak)");
// 发起新请求验证池仍然健康(能创建新连接)
NSError *recoveryError = nil;
HttpdnsNWHTTPClientResponse *recoveryResponse = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"RecoveryTest"
timeout:15.0
error:&recoveryError];
XCTAssertNotNil(recoveryResponse, @"Pool should recover and handle new requests after mixed timeout/success");
XCTAssertEqual(recoveryResponse.statusCode, 200, @"Recovery request should succeed");
}
// P.4 连续超时不导致连接泄漏
- (void)testTimeout_ConsecutiveTimeouts_NoConnectionLeak {
[self.client resetPoolStatistics];
NSString *poolKey = @"127.0.0.1:11080:tcp";
// 连续发起 10 个超时请求
for (NSInteger i = 0; i < 10; i++) {
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/delay/10"
userAgent:@"LeakTest"
timeout:0.5
error:&error];
XCTAssertNil(response, @"Request %ld should timeout", (long)i);
// 等待清理
[NSThread sleepForTimeInterval:0.2];
}
// 验证池状态:无连接泄漏
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey], 0,
@"Pool should be empty after consecutive timeouts");
XCTAssertEqual([self.client totalConnectionCount], 0,
@"No connections should leak");
// 验证统计:每次都创建新连接(因为超时的被移除)
XCTAssertEqual(self.client.connectionCreationCount, 10,
@"Should have created 10 connections (all timed out and removed)");
XCTAssertEqual(self.client.connectionReuseCount, 0,
@"No reuse for timed-out connections");
}
// P.5 超时不阻塞连接池(并发正常请求不受影响)
- (void)testTimeout_NonBlocking_ConcurrentNormalRequestSucceeds {
[self.client resetPoolStatistics];
XCTestExpectation *timeoutExpectation = [self expectationWithDescription:@"Timeout request"];
XCTestExpectation *successExpectation = [self expectationWithDescription:@"Success request"];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 请求 A超时delay 10s, timeout 2s
dispatch_async(queue, ^{
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/delay/10"
userAgent:@"TimeoutRequest"
timeout:2.0
error:&error];
XCTAssertNil(response, @"Request A should timeout");
[timeoutExpectation fulfill];
});
// 请求 B正常应该不受 A 阻塞)
dispatch_async(queue, ^{
// 稍微延迟,确保 A 先开始
[NSThread sleepForTimeInterval:0.1];
CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent();
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"NormalRequest"
timeout:15.0
error:&error];
CFAbsoluteTime elapsed = CFAbsoluteTimeGetCurrent() - startTime;
XCTAssertNotNil(response, @"Request B should succeed despite A timing out");
XCTAssertEqual(response.statusCode, 200);
// 验证请求 B 没有被请求 A 阻塞(应该很快完成)
XCTAssertLessThan(elapsed, 5.0,
@"Request B should complete quickly, not blocked by A's timeout");
[successExpectation fulfill];
});
[self waitForExpectations:@[timeoutExpectation, successExpectation] timeout:20.0];
// 等待连接归还
[NSThread sleepForTimeInterval:0.5];
// 验证池状态:只有请求 B 的连接
NSString *poolKey = @"127.0.0.1:11080:tcp";
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey], 1,
@"Pool should have 1 connection from successful request B");
}
// P.6 多端口场景下的超时隔离
- (void)testTimeout_MultiPort_IsolatedPoolCleaning {
[self.client resetPoolStatistics];
NSString *poolKey11443 = @"127.0.0.1:11443:tls";
NSString *poolKey11444 = @"127.0.0.1:11444:tls";
// 端口 11443超时请求
NSError *error1 = nil;
HttpdnsNWHTTPClientResponse *response1 = [self.client performRequestWithURLString:@"https://127.0.0.1:11443/delay/10"
userAgent:@"Port11443Timeout"
timeout:1.0
error:&error1];
XCTAssertNil(response1, @"Port 11443 request should timeout");
// 端口 11444正常请求
NSError *error2 = nil;
HttpdnsNWHTTPClientResponse *response2 = [self.client performRequestWithURLString:@"https://127.0.0.1:11444/get"
userAgent:@"Port11444Success"
timeout:15.0
error:&error2];
XCTAssertNotNil(response2, @"Port 11444 request should succeed");
XCTAssertEqual(response2.statusCode, 200);
// 等待连接归还
[NSThread sleepForTimeInterval:0.5];
// 验证端口隔离:端口 11443 无连接,端口 11444 有 1 个连接
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey11443], 0,
@"Port 11443 pool should be empty (timed out)");
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey11444], 1,
@"Port 11444 pool should have 1 connection");
// 验证总连接数
XCTAssertEqual([self.client totalConnectionCount], 1,
@"Total should be 1 (only from port 11444)");
// 再次请求端口 11444应该复用连接
NSError *error3 = nil;
HttpdnsNWHTTPClientResponse *response3 = [self.client performRequestWithURLString:@"https://127.0.0.1:11444/get"
userAgent:@"Port11444Reuse"
timeout:15.0
error:&error3];
XCTAssertNotNil(response3);
XCTAssertEqual(response3.statusCode, 200);
// 验证复用发生
XCTAssertEqual(self.client.connectionReuseCount, 1,
@"Second request to port 11444 should reuse connection");
}
#pragma mark - R. Connection 头部处理测试
// R.1 Connection: close 导致连接被立即失效
- (void)testConnectionHeader_Close_ConnectionInvalidated {
[self.client resetPoolStatistics];
NSString *poolKey = @"127.0.0.1:11080:tcp";
// 请求 1服务器返回 Connection: close
NSError *error1 = nil;
HttpdnsNWHTTPClientResponse *response1 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/connection-test?mode=close"
userAgent:@"CloseTest"
timeout:15.0
error:&error1];
XCTAssertNotNil(response1, @"Request with Connection: close should succeed");
XCTAssertEqual(response1.statusCode, 200);
// 等待异步 returnConnection 完成
[NSThread sleepForTimeInterval:0.5];
// 验证:连接不在池中(已失效)
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey], 0,
@"Connection with 'Connection: close' header should be invalidated and not returned to pool");
// 请求 2应该创建新连接第一个连接已失效
NSError *error2 = nil;
HttpdnsNWHTTPClientResponse *response2 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/connection-test?mode=keep-alive"
userAgent:@"KeepAliveTest"
timeout:15.0
error:&error2];
XCTAssertNotNil(response2);
XCTAssertEqual(response2.statusCode, 200);
// 验证统计:创建了 2 个连接(第一个被 close第二个正常
XCTAssertEqual(self.client.connectionCreationCount, 2,
@"Should have created 2 connections (first closed by server)");
XCTAssertEqual(self.client.connectionReuseCount, 0,
@"No reuse after Connection: close");
}
// R.2 Connection: keep-alive 允许连接复用
- (void)testConnectionHeader_KeepAlive_ConnectionReused {
[self.client resetPoolStatistics];
NSString *poolKey = @"127.0.0.1:11080:tcp";
// 请求 1服务器返回 Connection: keep-alive
NSError *error1 = nil;
HttpdnsNWHTTPClientResponse *response1 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/connection-test?mode=keep-alive"
userAgent:@"KeepAlive1"
timeout:15.0
error:&error1];
XCTAssertNotNil(response1);
XCTAssertEqual(response1.statusCode, 200);
// 等待连接归还
[NSThread sleepForTimeInterval:0.5];
// 验证:连接在池中
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey], 1,
@"Connection with 'Connection: keep-alive' should be returned to pool");
// 请求 2应该复用第一个连接
NSError *error2 = nil;
HttpdnsNWHTTPClientResponse *response2 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/connection-test?mode=keep-alive"
userAgent:@"KeepAlive2"
timeout:15.0
error:&error2];
XCTAssertNotNil(response2);
XCTAssertEqual(response2.statusCode, 200);
// 验证统计:只创建了 1 个连接,第二个请求复用了它
XCTAssertEqual(self.client.connectionCreationCount, 1,
@"Should have created only 1 connection");
XCTAssertEqual(self.client.connectionReuseCount, 1,
@"Second request should reuse the first connection");
}
// R.3 Proxy-Connection: close 也会导致连接失效
- (void)testConnectionHeader_ProxyClose_ConnectionInvalidated {
[self.client resetPoolStatistics];
NSString *poolKey = @"127.0.0.1:11080:tcp";
// 请求 1服务器返回 Proxy-Connection: close (+ Connection: keep-alive)
NSError *error1 = nil;
HttpdnsNWHTTPClientResponse *response1 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/connection-test?mode=proxy-close"
userAgent:@"ProxyCloseTest"
timeout:15.0
error:&error1];
XCTAssertNotNil(response1, @"Request with Proxy-Connection: close should succeed");
XCTAssertEqual(response1.statusCode, 200);
// 等待异步 returnConnection 完成
[NSThread sleepForTimeInterval:0.5];
// 验证连接不在池中Proxy-Connection: close 应该失效连接)
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey], 0,
@"Connection with 'Proxy-Connection: close' header should be invalidated");
// 请求 2应该创建新连接
NSError *error2 = nil;
HttpdnsNWHTTPClientResponse *response2 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/connection-test?mode=keep-alive"
userAgent:@"RecoveryTest"
timeout:15.0
error:&error2];
XCTAssertNotNil(response2);
XCTAssertEqual(response2.statusCode, 200);
// 验证统计:创建了 2 个连接
XCTAssertEqual(self.client.connectionCreationCount, 2,
@"Should have created 2 connections (first closed by proxy header)");
XCTAssertEqual(self.client.connectionReuseCount, 0,
@"No reuse after Proxy-Connection: close");
}
// R.4 Connection 头部大小写不敏感
- (void)testConnectionHeader_CaseInsensitive_AllVariantsWork {
[self.client resetPoolStatistics];
NSString *poolKey = @"127.0.0.1:11080:tcp";
// 测试 1CONNECTION: CLOSE (全大写)
NSError *error1 = nil;
HttpdnsNWHTTPClientResponse *response1 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/connection-test?mode=close-uppercase"
userAgent:@"UppercaseTest"
timeout:15.0
error:&error1];
XCTAssertNotNil(response1);
XCTAssertEqual(response1.statusCode, 200);
[NSThread sleepForTimeInterval:0.5];
// 验证:连接不在池中(大写 CLOSE 也应生效)
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey], 0,
@"'CONNECTION: CLOSE' (uppercase) should also close connection");
// 测试 2Connection: Close (混合大小写)
NSError *error2 = nil;
HttpdnsNWHTTPClientResponse *response2 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/connection-test?mode=close-mixed"
userAgent:@"MixedCaseTest"
timeout:15.0
error:&error2];
XCTAssertNotNil(response2);
XCTAssertEqual(response2.statusCode, 200);
[NSThread sleepForTimeInterval:0.5];
// 验证:连接不在池中(混合大小写也应生效)
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey], 0,
@"'Connection: Close' (mixed case) should also close connection");
// 验证统计:创建了 2 个连接,都被 close
XCTAssertEqual(self.client.connectionCreationCount, 2,
@"Should have created 2 connections (both closed due to case-insensitive matching)");
XCTAssertEqual(self.client.connectionReuseCount, 0,
@"No reuse for any closed connections");
}
// R.5 并发场景:混合 close 和 keep-alive
- (void)testConnectionHeader_ConcurrentMixed_CloseAndKeepAliveIsolated {
[self.client resetPoolStatistics];
NSString *poolKey = @"127.0.0.1:11080:tcp";
NSMutableArray<XCTestExpectation *> *expectations = [NSMutableArray array];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSLock *closeCountLock = [[NSLock alloc] init];
NSLock *keepAliveCountLock = [[NSLock alloc] init];
__block NSInteger closeCount = 0;
__block NSInteger keepAliveCount = 0;
// 发起 10 个请求5 个 close5 个 keep-alive
for (NSInteger i = 0; i < 10; i++) {
XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"Request %ld", (long)i]];
[expectations addObject:expectation];
dispatch_async(queue, ^{
NSError *error = nil;
NSString *urlString;
if (i % 2 == 0) {
// 偶数close
urlString = @"http://127.0.0.1:11080/connection-test?mode=close";
} else {
// 奇数keep-alive
urlString = @"http://127.0.0.1:11080/connection-test?mode=keep-alive";
}
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:urlString
userAgent:@"ConcurrentMixed"
timeout:15.0
error:&error];
if (response && response.statusCode == 200) {
if (i % 2 == 0) {
[closeCountLock lock];
closeCount++;
[closeCountLock unlock];
} else {
[keepAliveCountLock lock];
keepAliveCount++;
[keepAliveCountLock unlock];
}
}
[expectation fulfill];
});
}
[self waitForExpectations:expectations timeout:30.0];
// 验证结果
XCTAssertEqual(closeCount, 5, @"5 close requests should succeed");
XCTAssertEqual(keepAliveCount, 5, @"5 keep-alive requests should succeed");
// 等待所有连接归还
[NSThread sleepForTimeInterval:1.0];
// 验证池状态:
// - close 连接全部被失效(不在池中)
// - keep-alive 连接可能在池中(取决于并发时序和 remote close
// - 关键是:总数不超过 4池限制无泄漏
NSInteger poolSize = [self.client connectionPoolCountForKey:poolKey];
XCTAssertLessThanOrEqual(poolSize, 4,
@"Pool size should not exceed limit (no leak from close connections)");
// 验证统计close 连接不应该被复用
// 创建数应该 >= 6 (至少 5 个 close + 1 个 keep-alive因为 close 不能复用)
XCTAssertGreaterThanOrEqual(self.client.connectionCreationCount, 6,
@"Should have created at least 6 connections (5 close + at least 1 keep-alive)");
// 后续请求验证池仍然健康
NSError *recoveryError = nil;
HttpdnsNWHTTPClientResponse *recoveryResponse = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"RecoveryTest"
timeout:15.0
error:&recoveryError];
XCTAssertNotNil(recoveryResponse, @"Pool should still be healthy after mixed close/keep-alive scenario");
XCTAssertEqual(recoveryResponse.statusCode, 200);
}
@end