feat: sync httpdns sdk/platform updates without large binaries
This commit is contained in:
@@ -0,0 +1,740 @@
|
||||
//
|
||||
// HttpdnsNWHTTPClient_EdgeCasesAndTimeoutTests.m
|
||||
// TrustHttpDNSTests
|
||||
//
|
||||
// @author Created by Claude Code on 2025-11-01
|
||||
// Copyright © 2025 trustapp.com. All rights reserved.
|
||||
//
|
||||
// 边界条件与超时测<EFBFBD><EFBFBD>?- 包含边界条件 (M)、超时交<EFBFBD><EFBFBD>?(P) <EFBFBD><EFBFBD>?Connection 头部 (R) 测试<EFBFBD><EFBFBD>?
|
||||
// 测试总数<EFBFBD><EFBFBD>?5 个(M:4 + P:6 + R:5<EFBFBD><EFBFBD>?
|
||||
//
|
||||
|
||||
#import "HttpdnsNWHTTPClientTestBase.h"
|
||||
|
||||
@interface HttpdnsNWHTTPClient_EdgeCasesAndTimeoutTests : HttpdnsNWHTTPClientTestBase
|
||||
|
||||
@end
|
||||
|
||||
@implementation HttpdnsNWHTTPClient_EdgeCasesAndTimeoutTests
|
||||
|
||||
#pragma mark - M. 边界条件与验证测<EFBFBD><EFBFBD>?
|
||||
|
||||
// M.1 连接复用边界:端口内复用,端口间隔离
|
||||
- (void)testEdgeCase_ConnectionReuseWithinPortOnly_NotAcross {
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"Reuse boundaries"];
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
// 请求 A 到端<EFBFBD><EFBFBD>?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 到端<EFBFBD><EFBFBD>?11443(应该复用连接,可能更快<EFBFBD><EFBFBD>?
|
||||
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 到端<EFBFBD><EFBFBD>?11444(应该创建新连接<EFBFBD><EFBFBD>?
|
||||
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 到端<EFBFBD><EFBFBD>?11444(应该复用端<EFBFBD><EFBFBD>?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 高端口数量压力测<EFBFBD><EFBFBD>?
|
||||
- (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];
|
||||
|
||||
// 第一轮:向所有端口各发起一个请<EFBFBD><EFBFBD>?
|
||||
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);
|
||||
}
|
||||
|
||||
// 第二轮:再次向所有端口发起请求(应该复用连接<EFBFBD><EFBFBD>?
|
||||
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 并发池访问安全<EFBFBD><EFBFBD>?
|
||||
- (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;
|
||||
|
||||
// 向三个端口并发发起请<EFBFBD><EFBFBD>?
|
||||
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];
|
||||
// 如果没有崩溃或断言失败,说明并发访问安<EFBFBD><EFBFBD>?
|
||||
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,发起多个请<EFBFBD><EFBFBD>?
|
||||
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 秒,让端<EFBFBD><EFBFBD>?11443 的连接过<EFBFBD><EFBFBD>?
|
||||
[NSThread sleepForTimeInterval:31.0];
|
||||
|
||||
// 阶段 3:验证端<EFBFBD><EFBFBD>?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:端<EFBFBD><EFBFBD>?11443 应该创建新连接(旧连接已过期<EFBFBD><EFBFBD>?
|
||||
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";
|
||||
|
||||
// 验证初始状<EFBFBD><EFBFBD>?
|
||||
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey], 0,
|
||||
@"Pool should be empty initially");
|
||||
|
||||
// 发起超时请求(delay 10s, timeout 1s<EFBFBD><EFBFBD>?
|
||||
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];
|
||||
|
||||
// 验证池状态:超时连接应该被移<EFBFBD><EFBFBD>?
|
||||
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];
|
||||
|
||||
// 验证池恢复:现在应该<EFBFBD><EFBFBD>?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);
|
||||
|
||||
// 验证统计<EFBFBD><EFBFBD>? 个超时(创建后移除)+ 1 个成功(创建<EFBFBD><EFBFBD>? 1 个复<EFBFBD><EFBFBD>?
|
||||
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 并发场景:部分超时不影响成功请求的连接复<EFBFBD><EFBFBD>?
|
||||
- (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 个超<EFBFBD><EFBFBD>?
|
||||
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) {
|
||||
// 偶数:正常请<EFBFBD><EFBFBD>?
|
||||
urlString = @"http://127.0.0.1:11080/get";
|
||||
timeout = 15.0;
|
||||
} else {
|
||||
// 奇数:超时请<EFBFBD><EFBFBD>?
|
||||
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个成<EFBFBD><EFBFBD>?+ 5个超<EFBFBD><EFBFBD>?= 最<EFBFBD><EFBFBD>?0个,可能有复用)
|
||||
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];
|
||||
|
||||
// 验证总连接数合理(无泄漏<EFBFBD><EFBFBD>? 关键验证<EFBFBD><EFBFBD>?
|
||||
// 在并发场景下,成功的连接可能已经被关闭(remote close),池可能为<EFBFBD><EFBFBD>?
|
||||
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 连续超时不导致连接泄<EFBFBD><EFBFBD>?
|
||||
- (void)testTimeout_ConsecutiveTimeouts_NoConnectionLeak {
|
||||
[self.client resetPoolStatistics];
|
||||
NSString *poolKey = @"127.0.0.1:11080:tcp";
|
||||
|
||||
// 连续发起 10 个超时请<EFBFBD><EFBFBD>?
|
||||
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];
|
||||
}
|
||||
|
||||
// 验证池状态:无连接泄<EFBFBD><EFBFBD>?
|
||||
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey], 0,
|
||||
@"Pool should be empty after consecutive timeouts");
|
||||
XCTAssertEqual([self.client totalConnectionCount], 0,
|
||||
@"No connections should leak");
|
||||
|
||||
// 验证统计:每次都创建新连接(因为超时的被移除<EFBFBD><EFBFBD>?
|
||||
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<EFBFBD><EFBFBD>?
|
||||
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 阻塞<EFBFBD><EFBFBD>?
|
||||
dispatch_async(queue, ^{
|
||||
// 稍微延迟,确<EFBFBD><EFBFBD>?A 先开<EFBFBD><EFBFBD>?
|
||||
[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 没有被请<EFBFBD><EFBFBD>?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 的连<EFBFBD><EFBFBD>?
|
||||
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 多端口场景下的超时隔<EFBFBD><EFBFBD>?
|
||||
- (void)testTimeout_MultiPort_IsolatedPoolCleaning {
|
||||
[self.client resetPoolStatistics];
|
||||
|
||||
NSString *poolKey11443 = @"127.0.0.1:11443:tls";
|
||||
NSString *poolKey11444 = @"127.0.0.1:11444:tls";
|
||||
|
||||
// 端口 11443:超时请<EFBFBD><EFBFBD>?
|
||||
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:正常请<EFBFBD><EFBFBD>?
|
||||
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];
|
||||
|
||||
// 验证端口隔离:端<EFBFBD><EFBFBD>?11443 无连接,端口 11444 <EFBFBD><EFBFBD>?1 个连<EFBFBD><EFBFBD>?
|
||||
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:应该复用连<EFBFBD><EFBFBD>?
|
||||
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 导致连接被立即失<EFBFBD><EFBFBD>?
|
||||
- (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:应该创建新连接(第一个连接已失效<EFBFBD><EFBFBD>?
|
||||
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,第二个正常<EFBFBD><EFBFBD>?
|
||||
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:应该复用第一个连<EFBFBD><EFBFBD>?
|
||||
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);
|
||||
|
||||
// 验证统计:只创建<EFBFBD><EFBFBD>?1 个连接,第二个请求复用了<EFBFBD><EFBFBD>?
|
||||
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 应该失效连接<EFBFBD><EFBFBD>?
|
||||
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 个连<EFBFBD><EFBFBD>?
|
||||
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";
|
||||
|
||||
// 测试 1:CONNECTION: CLOSE (全大<EFBFBD><EFBFBD>?
|
||||
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 也应生效<EFBFBD><EFBFBD>?
|
||||
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey], 0,
|
||||
@"'CONNECTION: CLOSE' (uppercase) should also close connection");
|
||||
|
||||
// 测试 2:Connection: Close (混合大小<EFBFBD><EFBFBD>?
|
||||
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 并发场景:混<EFBFBD><EFBFBD>?close <EFBFBD><EFBFBD>?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 <EFBFBD><EFBFBD>?close<EFBFBD><EFBFBD>? <EFBFBD><EFBFBD>?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");
|
||||
|
||||
// 等待所有连接归<EFBFBD><EFBFBD>?
|
||||
[NSThread sleepForTimeInterval:1.0];
|
||||
|
||||
// 验证池状态:
|
||||
// - close 连接全部被失效(不在池中<EFBFBD><EFBFBD>?
|
||||
// - keep-alive 连接可能在池中(取决于并发时序和 remote close<EFBFBD><EFBFBD>?
|
||||
// - 关键是:总数不超<EFBFBD><EFBFBD>?4(池限制),无泄<EFBFBD><EFBFBD>?
|
||||
NSInteger poolSize = [self.client connectionPoolCountForKey:poolKey];
|
||||
XCTAssertLessThanOrEqual(poolSize, 4,
|
||||
@"Pool size should not exceed limit (no leak from close connections)");
|
||||
|
||||
// 验证统计:close 连接不应该被复用
|
||||
// 创建数应<EFBFBD><EFBFBD>?>= 6 (至少 5 <EFBFBD><EFBFBD>?close + 1 <EFBFBD><EFBFBD>?keep-alive,因<EFBFBD><EFBFBD>?close 不能复用)
|
||||
XCTAssertGreaterThanOrEqual(self.client.connectionCreationCount, 6,
|
||||
@"Should have created at least 6 connections (5 close + at least 1 keep-alive)");
|
||||
|
||||
// 后续请求验证池仍然健<EFBFBD><EFBFBD>?
|
||||
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
|
||||
Reference in New Issue
Block a user