EMASCurl/EMASCurlManager.m (117 lines of code) (raw):
//
// MultiCurlManager.m
// EMASCurl
//
// Created by xuyecan on 2024/12/9.
//
#import "EMASCurlManager.h"
@interface EMASCurlManager () {
CURLM *_multiHandle;
CURLSH *_shareHandle;
NSThread *_networkThread;
NSCondition *_condition;
BOOL _shouldStop;
NSMutableDictionary<NSNumber *, void (^)(BOOL, NSError *)> *_completionMap;
}
@end
@implementation EMASCurlManager
+ (instancetype)sharedInstance {
static EMASCurlManager *manager;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
manager = [[EMASCurlManager alloc] initPrivate];
});
return manager;
}
- (instancetype)initPrivate {
self = [super init];
if (self) {
curl_global_init(CURL_GLOBAL_ALL);
_multiHandle = curl_multi_init();
// cookie手动管理,所以这里不共享
// 如果有需求,需要做实例隔离,整个架构要重新设计
_shareHandle = curl_share_init();
curl_share_setopt(_shareHandle, CURLSHOPT_SHARE, CURL_LOCK_DATA_DNS);
curl_share_setopt(_shareHandle, CURLSHOPT_SHARE, CURL_LOCK_DATA_SSL_SESSION);
curl_share_setopt(_shareHandle, CURLSHOPT_SHARE, CURL_LOCK_DATA_CONNECT);
_completionMap = [NSMutableDictionary dictionary];
_condition = [[NSCondition alloc] init];
_shouldStop = NO;
_networkThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkThreadEntry) object:nil];
_networkThread.qualityOfService = NSQualityOfServiceUserInitiated;
[_networkThread start];
}
return self;
}
- (void)dealloc {
[self stop];
if (_multiHandle) {
curl_multi_cleanup(_multiHandle);
_multiHandle = NULL;
}
if (_shareHandle) {
curl_share_cleanup(_shareHandle);
_shareHandle = NULL;
}
curl_global_cleanup();
}
- (void)stop {
[_condition lock];
_shouldStop = YES;
[_condition signal];
[_condition unlock];
}
- (void)enqueueNewEasyHandle:(CURL *)easyHandle completion:(void (^)(BOOL, NSError *))completion {
NSNumber *easyKey = @((uintptr_t)easyHandle);
_completionMap[easyKey] = completion;
[_condition lock];
curl_easy_setopt(easyHandle, CURLOPT_SHARE, _shareHandle);
curl_multi_add_handle(_multiHandle, easyHandle);
[_condition signal];
[_condition unlock];
}
#pragma mark - Thread Entry and Main Loop
- (void)networkThreadEntry {
@autoreleasepool {
[_condition lock];
while (!_shouldStop) {
if (_completionMap.count == 0) {
[_condition wait];
if (_shouldStop) {
break;
}
}
[self performCurlTransfers];
if (_completionMap.count > 0 && !_shouldStop) {
[_condition unlock];
curl_multi_wait(_multiHandle, NULL, 0, 1000, NULL);
[_condition lock];
}
}
[_condition unlock];
}
}
- (void)performCurlTransfers {
int stillRunning = 0;
CURLMsg *msg = NULL;
int msgsLeft = 0;
do {
curl_multi_perform(_multiHandle, &stillRunning);
while ((msg = curl_multi_info_read(_multiHandle, &msgsLeft))) {
if (msg->msg == CURLMSG_DONE) {
CURL *easy = msg->easy_handle;
NSNumber *easyKey = @((uintptr_t)easy);
void (^completion)(BOOL, NSError *) = _completionMap[easyKey];
[_completionMap removeObjectForKey:easyKey];
BOOL succeeded = (msg->data.result == CURLE_OK);
NSError *error = nil;
if (!succeeded) {
char *urlp = NULL;
curl_easy_getinfo(easy, CURLINFO_EFFECTIVE_URL, &urlp);
NSString *url = urlp ? @(urlp) : @"unknownURL";
NSDictionary *userInfo = @{ NSLocalizedDescriptionKey: @(curl_easy_strerror(msg->data.result)), NSURLErrorFailingURLStringErrorKey: url };
error = [NSError errorWithDomain:@"MultiCurlManager" code:msg->data.result userInfo:userInfo];
}
curl_multi_remove_handle(_multiHandle, easy);
if (completion) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
completion(succeeded, error);
});
}
}
}
} while (stillRunning > 0);
}
@end