741 lines
37 KiB
Objective-C
741 lines
37 KiB
Objective-C
//
|
||
// HttpdnsNWHTTPClient_EdgeCasesAndTimeoutTests.m
|
||
// TrustHttpDNSTests
|
||
//
|
||
// @author Created by Claude Code on 2025-11-01
|
||
// Copyright © 2025 trustapp.com. All rights reserved.
|
||
//
|
||
// 边界æ<C592>¡ä»¶ä¸Žè¶…时测è¯?- 包å<E280A6>«è¾¹ç•Œæ<C592>¡ä»¶ (M)ã€<C3A3>超时交äº?(P) å’?Connection 头部 (R) 测试ç»?
|
||
// 测试总数�5 个(M:4 + P:6 + R:5�
|
||
//
|
||
|
||
#import "HttpdnsNWHTTPClientTestBase.h"
|
||
|
||
@interface HttpdnsNWHTTPClient_EdgeCasesAndTimeoutTests : HttpdnsNWHTTPClientTestBase
|
||
|
||
@end
|
||
|
||
@implementation HttpdnsNWHTTPClient_EdgeCasesAndTimeoutTests
|
||
|
||
#pragma mark - M. 边界æ<C592>¡ä»¶ä¸ŽéªŒè¯<C3A8>测è¯?
|
||
|
||
// M.1 连接å¤<C3A5>用边界:端å<C2AF>£å†…å¤<C3A5>用,端å<C2AF>£é—´éš”离
|
||
- (void)testEdgeCase_ConnectionReuseWithinPortOnly_NotAcross {
|
||
XCTestExpectation *expectation = [self expectationWithDescription:@"Reuse boundaries"];
|
||
|
||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||
// 请求 A 到端å<C2AF>?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 到端å<C2AF>?11443(应该å¤<C3A5>用连接,å<C592>¯èƒ½æ›´å¿«ï¼?
|
||
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 到端å<C2AF>?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 到端å<C2AF>?11444(应该å¤<C3A5>用端å<C2AF>?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);
|
||
|
||
// 验è¯<C3A8>所有请求都æˆ<C3A6>功
|
||
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 高端å<C2AF>£æ•°é‡<C3A9>压力测è¯?
|
||
- (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];
|
||
|
||
// 第一轮:å<C5A1>‘所有端å<C2AF>£å<C2A3>„å<E2809E>‘起一个请æ±?
|
||
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);
|
||
}
|
||
|
||
// 第二轮:å†<C3A5>次å<C2A1>‘所有端å<C2AF>£å<C2A3>‘起请求(应该å¤<C3A5>用连接ï¼?
|
||
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 å¹¶å<C2B6>‘æ± è®¿é—®å®‰å…¨æ€?
|
||
- (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;
|
||
|
||
// å<>‘三个端å<C2AF>£å¹¶å<C2B6>‘å<E28098>‘起请æ±?
|
||
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];
|
||
// 如果没有崩溃或æ–言失败,说明并å<C2B6>‘访问安å…?
|
||
XCTAssertTrue(response != nil || error != nil);
|
||
[expectation fulfill];
|
||
});
|
||
}
|
||
}
|
||
|
||
[self waitForExpectations:expectations timeout:50.0];
|
||
}
|
||
|
||
// M.4 端å<C2AF>£è¿<C3A8>移模å¼<C3A5>
|
||
- (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:å<C5A1>‘端å<C2AF>£ 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:切æ<E280A1>¢åˆ°ç«¯å<C2AF>£ 11444,å<C592>‘起多个请æ±?
|
||
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 秒,让端å<C2AF>?11443 的连接过æœ?
|
||
[NSThread sleepForTimeInterval:31.0];
|
||
|
||
// 阶段 3:验è¯<C3A8>端å<C2AF>?11444 ä»<C3A4>ç„¶å<C2B6>¯ç”¨
|
||
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:端å<C2AF>?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 å<>•次超时å<C2B6>Žè¿žæŽ¥è¢«æ£ç¡®ç§»é™¤
|
||
- (void)testTimeout_SingleRequest_ConnectionRemovedFromPool {
|
||
[self.client resetPoolStatistics];
|
||
NSString *poolKey = @"127.0.0.1:11080:tcp";
|
||
|
||
// 验è¯<C3A8>åˆ<C3A5>始状æ€?
|
||
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];
|
||
|
||
// 验è¯<C3A8>请求失败
|
||
XCTAssertNil(response, @"Response should be nil on timeout");
|
||
XCTAssertNotNil(error, @"Error should be set on timeout");
|
||
|
||
// ç‰å¾…å¼‚æ¥ returnConnection 完æˆ<C3A6>
|
||
[NSThread sleepForTimeInterval:0.5];
|
||
|
||
// 验è¯<C3A8>æ± çŠ¶æ€<C3A6>:超时连接应该被移é™?
|
||
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");
|
||
|
||
// 验è¯<C3A8>统计
|
||
XCTAssertEqual(self.client.connectionCreationCount, 1,
|
||
@"Should have created 1 connection");
|
||
XCTAssertEqual(self.client.connectionReuseCount, 0,
|
||
@"No reuse for timed-out connection");
|
||
}
|
||
|
||
// P.2 è¶…æ—¶å<C2B6>Žè¿žæŽ¥æ± æ<C2A0>¢å¤<C3A5>能力
|
||
- (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);
|
||
|
||
// ç‰å¾…清ç<E280A6>†å®Œæˆ<C3A6>
|
||
[NSThread sleepForTimeInterval:0.5];
|
||
|
||
// 第二个请求:æ£å¸¸ï¼ˆéªŒè¯<C3A8>æ± å·²æ<C2B2>¢å¤<C3A5>)
|
||
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];
|
||
|
||
// 验è¯<C3A8>æ± æ<C2A0>¢å¤<C3A5>:现在应该æœ?1 个连接(æ<CB86>¥è‡ªç¬¬äºŒä¸ªè¯·æ±‚)
|
||
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey], 1,
|
||
@"Pool should have 1 connection from second request");
|
||
|
||
// 第三个请求:应该å¤<C3A5>用第二个请求的连接
|
||
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);
|
||
|
||
// 验è¯<C3A8>统计ï¼? 个超时(创建å<C2BA>Žç§»é™¤ï¼‰+ 1 个æˆ<C3A6>功(创建ï¼? 1 个å¤<C3A5>ç”?
|
||
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 å¹¶å<C2B6>‘场景:部分超时ä¸<C3A4>å½±å“<C3A5>æˆ<C3A6>功请求的连接å¤<C3A5>ç”?
|
||
- (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];
|
||
|
||
// 验è¯<C3A8>结果
|
||
XCTAssertEqual(successCount, 5, @"5 requests should succeed");
|
||
XCTAssertEqual(timeoutCount, 5, @"5 requests should timeout");
|
||
|
||
// 验è¯<C3A8>连接创建数å<C2B0>ˆç<CB86>†ï¼ˆ5个æˆ<C3A6>åŠ?+ 5个超æ—?= 最å¤?0个,å<C592>¯èƒ½æœ‰å¤<C3A5>用)
|
||
XCTAssertGreaterThan(self.client.connectionCreationCount, 0,
|
||
@"Should have created connections for concurrent requests");
|
||
XCTAssertLessThanOrEqual(self.client.connectionCreationCount, 10,
|
||
@"Should not create more than 10 connections");
|
||
|
||
// ç‰å¾…æ‰€æœ‰è¿žæŽ¥å½’è¿˜ï¼ˆå¼‚æ¥æ“<C3A6>作需è¦<C3A8>更长时间)
|
||
[NSThread sleepForTimeInterval:2.0];
|
||
|
||
// 验è¯<C3A8>总连接数å<C2B0>ˆç<CB86>†ï¼ˆæ— 泄æ¼<C3A6>ï¼? 关键验è¯<C3A8>ç‚?
|
||
// 在并å<C2B6>‘场景下,æˆ<C3A6>功的连接å<C2A5>¯èƒ½å·²ç»<C3A7>被关é—(remote closeï¼‰ï¼Œæ± å<C2A0>¯èƒ½ä¸ºç©?
|
||
XCTAssertLessThanOrEqual([self.client totalConnectionCount], 4,
|
||
@"Total connections should not exceed pool limit (no leak)");
|
||
|
||
// å<>‘起新请求验è¯<C3A8>æ± ä»<C3A4>ç„¶å<C2B6>¥åº·ï¼ˆèƒ½åˆ›å»ºæ–°è¿žæŽ¥ï¼‰
|
||
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 连ç»è¶…æ—¶ä¸<C3A4>导致连接泄æ¼?
|
||
- (void)testTimeout_ConsecutiveTimeouts_NoConnectionLeak {
|
||
[self.client resetPoolStatistics];
|
||
NSString *poolKey = @"127.0.0.1:11080:tcp";
|
||
|
||
// 连ç»å<C2AD>‘èµ· 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);
|
||
|
||
// ç‰å¾…清ç<E280A6>†
|
||
[NSThread sleepForTimeInterval:0.2];
|
||
}
|
||
|
||
// 验è¯<C3A8>æ± çŠ¶æ€<C3A6>ï¼šæ— è¿žæŽ¥æ³„æ¼?
|
||
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey], 0,
|
||
@"Pool should be empty after consecutive timeouts");
|
||
XCTAssertEqual([self.client totalConnectionCount], 0,
|
||
@"No connections should leak");
|
||
|
||
// 验è¯<C3A8>统计:æ¯<C3A6>æ¬¡éƒ½åˆ›å»ºæ–°è¿žæŽ¥ï¼ˆå› ä¸ºè¶…æ—¶çš„è¢«ç§»é™¤ï¼?
|
||
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 è¶…æ—¶ä¸<C3A4>é˜»å¡žè¿žæŽ¥æ± ï¼ˆå¹¶å<C2B6>‘æ£å¸¸è¯·æ±‚ä¸<C3A4>å<EFBFBD>—å½±å“<C3A5>)
|
||
- (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:æ£å¸¸ï¼ˆåº”该ä¸<C3A4>å<EFBFBD>— A 阻塞ï¼?
|
||
dispatch_async(queue, ^{
|
||
// ç¨<C3A7>微延迟,确ä¿?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);
|
||
|
||
// 验è¯<C3A8>请求 B 没有被请æ±?A 阻塞(应该很快完æˆ<C3A6>)
|
||
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];
|
||
|
||
// 验è¯<C3A8>æ± çŠ¶æ€<C3A6>:å<C5A1>ªæœ‰è¯·æ±‚ 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 多端å<C2AF>£åœºæ™¯ä¸‹çš„è¶…æ—¶éš”ç¦?
|
||
- (void)testTimeout_MultiPort_IsolatedPoolCleaning {
|
||
[self.client resetPoolStatistics];
|
||
|
||
NSString *poolKey11443 = @"127.0.0.1:11443:tls";
|
||
NSString *poolKey11444 = @"127.0.0.1:11444:tls";
|
||
|
||
// 端å<C2AF>£ 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");
|
||
|
||
// 端å<C2AF>£ 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];
|
||
|
||
// 验è¯<C3A8>端å<C2AF>£éš”离:端å<C2AF>?11443 æ— è¿žæŽ¥ï¼Œç«¯å<C2AF>£ 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");
|
||
|
||
// 验è¯<C3A8>总连接数
|
||
XCTAssertEqual([self.client totalConnectionCount], 1,
|
||
@"Total should be 1 (only from port 11444)");
|
||
|
||
// å†<C3A5>次请求端å<C2AF>£ 11444:应该å¤<C3A5>用连æŽ?
|
||
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);
|
||
|
||
// 验è¯<C3A8>å¤<C3A5>用å<C2A8>‘生
|
||
XCTAssertEqual(self.client.connectionReuseCount, 1,
|
||
@"Second request to port 11444 should reuse connection");
|
||
}
|
||
|
||
#pragma mark - R. Connection 头部处ç<E2809E>†æµ‹è¯•
|
||
|
||
// R.1 Connection: close 导致连接被立å<E280B9>³å¤±æ•?
|
||
- (void)testConnectionHeader_Close_ConnectionInvalidated {
|
||
[self.client resetPoolStatistics];
|
||
NSString *poolKey = @"127.0.0.1:11080:tcp";
|
||
|
||
// 请求 1:æœ<C3A6>务器返回 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 完æˆ<C3A6>
|
||
[NSThread sleepForTimeInterval:0.5];
|
||
|
||
// 验è¯<C3A8>:连接ä¸<C3A4>åœ¨æ± ä¸ï¼ˆå·²å¤±æ•ˆï¼‰
|
||
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);
|
||
|
||
// 验è¯<C3A8>统计:创建了 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 å…<C3A5>许连接å¤<C3A5>用
|
||
- (void)testConnectionHeader_KeepAlive_ConnectionReused {
|
||
[self.client resetPoolStatistics];
|
||
NSString *poolKey = @"127.0.0.1:11080:tcp";
|
||
|
||
// 请求 1:æœ<C3A6>务器返回 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];
|
||
|
||
// 验è¯<C3A8>ï¼šè¿žæŽ¥åœ¨æ± ä¸
|
||
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey], 1,
|
||
@"Connection with 'Connection: keep-alive' should be returned to pool");
|
||
|
||
// 请求 2:应该å¤<C3A5>用第一个连æŽ?
|
||
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);
|
||
|
||
// 验è¯<C3A8>统计:å<C5A1>ªåˆ›å»ºäº?1 个连接,第二个请求å¤<C3A5>用了å®?
|
||
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:æœ<C3A6>务器返回 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 完æˆ<C3A6>
|
||
[NSThread sleepForTimeInterval:0.5];
|
||
|
||
// 验è¯<C3A8>:连接ä¸<C3A4>åœ¨æ± ä¸ï¼ˆ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);
|
||
|
||
// 验è¯<C3A8>统计:创建了 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 头部大å°<C3A5>写ä¸<C3A4>æ•<C3A6>感
|
||
- (void)testConnectionHeader_CaseInsensitive_AllVariantsWork {
|
||
[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-uppercase"
|
||
userAgent:@"UppercaseTest"
|
||
timeout:15.0
|
||
error:&error1];
|
||
XCTAssertNotNil(response1);
|
||
XCTAssertEqual(response1.statusCode, 200);
|
||
|
||
[NSThread sleepForTimeInterval:0.5];
|
||
|
||
// 验è¯<C3A8>:连接ä¸<C3A4>åœ¨æ± ä¸ï¼ˆå¤§å†™ CLOSE 也应生效ï¼?
|
||
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey], 0,
|
||
@"'CONNECTION: CLOSE' (uppercase) should also close connection");
|
||
|
||
// 测试 2:Connection: Close (æ··å<C2B7>ˆå¤§å°<C3A5>å†?
|
||
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];
|
||
|
||
// 验è¯<C3A8>:连接ä¸<C3A4>åœ¨æ± ä¸ï¼ˆæ··å<C2B7>ˆå¤§å°<C3A5>写也应生效)
|
||
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey], 0,
|
||
@"'Connection: Close' (mixed case) should also close connection");
|
||
|
||
// 验è¯<C3A8>统计:创建了 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 å¹¶å<C2B6>‘场景:混å<C2B7>?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 ä¸?closeï¼? ä¸?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];
|
||
|
||
// 验è¯<C3A8>结果
|
||
XCTAssertEqual(closeCount, 5, @"5 close requests should succeed");
|
||
XCTAssertEqual(keepAliveCount, 5, @"5 keep-alive requests should succeed");
|
||
|
||
// ç‰å¾…所有连接归è¿?
|
||
[NSThread sleepForTimeInterval:1.0];
|
||
|
||
// 验è¯<C3A8>æ± çŠ¶æ€<C3A6>:
|
||
// - close 连接全部被失效(ä¸<C3A4>åœ¨æ± ä¸ï¼?
|
||
// - keep-alive 连接å<C2A5>¯èƒ½åœ¨æ± ä¸ï¼ˆå<CB86>–决于并å<C2B6>‘æ—¶åº<C3A5>å’Œ remote closeï¼?
|
||
// - 关键是:总数ä¸<C3A4>è¶…è¿?4ï¼ˆæ± é™<C3A9>åˆ¶ï¼‰ï¼Œæ— æ³„æ¼?
|
||
NSInteger poolSize = [self.client connectionPoolCountForKey:poolKey];
|
||
XCTAssertLessThanOrEqual(poolSize, 4,
|
||
@"Pool size should not exceed limit (no leak from close connections)");
|
||
|
||
// 验è¯<C3A8>统计:close 连接ä¸<C3A4>应该被å¤<C3A5>用
|
||
// 创建数应è¯?>= 6 (至少 5 ä¸?close + 1 ä¸?keep-aliveï¼Œå› ä¸?close ä¸<C3A4>能å¤<C3A5>用)
|
||
XCTAssertGreaterThanOrEqual(self.client.connectionCreationCount, 6,
|
||
@"Should have created at least 6 connections (5 close + at least 1 keep-alive)");
|
||
|
||
// å<>Žç»è¯·æ±‚验è¯<C3A8>æ± ä»<C3A4>ç„¶å<C2B6>¥åº?
|
||
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
|