YMHTTP
YMHTTP
是一个适用于 iOS 平台的基于 libcurl 的 IO 多路复用 HTTP 框架,它的 API 设计和行为与 NSURLSession
保持高度一致。
由于 YMHTTP
是基于 libcurl 封装的,因此具有很高的定制性,目前的版本保持与 NSURLSession
在 API 上一致的同时,还扩展了 DNS 的功能(包括 SNI 场景)。
如果您对 DNS 相关的问题比较感兴趣,可以查看这篇文章 文章,它汇总了在 iOS 上支持 DNS 需要面对的一些技术难点、相关解决方案以及 YMHTTP 诞生的初衷。
说明
- 您可以通过 NSURLSession 来查阅具体细节。
- 这里有一份非常不错的NSURLSession最全攻略,可以查漏补缺(来自搜狐技术产品)。
- YMHTTP 和 NSURLSession 非常相似,一个是 YM 前缀,一个是 NS 前缀,对外提供的 API 相互一致
- 如果您已经非常了解 NSURLSession,那么可以直接查阅 Connect to specific host and port 部分来获取 DNS 相关内容
- 不支持 System Background Task 相关功能,这个真的无能为力
安装
目前 YMHTTP
的 UT覆盖率
大约在 80%,覆盖了各个 case,目前已经发布了 beta 版本,欢迎大家测试反馈~。
pod 'YMHTTP', '1.0.0-beta.1'
要求
- iOS 10.0
- Xcode 11.3.1
- libcurl 7.64.1 + SecureTransport
使用
0x01 YMSession
// 创建 sharedSession
YMURLSession *sharedSession = [YMURLSession sharedSession];
// 使用指定配置创建会话
YMURLSessionConfiguration *config = [YMURLSessionConfiguration defaultSessionConfiguration];
YMURLSession *sessionNoDelegate = [YMURLSession sessionWithConfiguration:config];
// 创建具有指定会话配置,委托和操作队列的会话
YMURLSession *session = [YMURLSession sessionWithConfiguration:config
delegate:self
delegateQueue:nil];
0x02 将数据任务添加到会话
- (YMURLSessionTask *)taskWithRequest:(NSURLRequest *)request;
- (YMURLSessionTask *)taskWithURL:(NSURL *)url;
通过指定URL或请求来创建任务
- (YMURLSessionTask *)taskWithRequest:(NSURLRequest *)request
completionHandler:(void (^)(NSData *_Nullable data,
NSHTTPURLResponse *_Nullable response,
NSError *_Nullable error))completionHandler;
- (YMURLSessionTask *)taskWithURL:(NSURL *)url
completionHandler:(void (^)(NSData *_Nullable data,
NSHTTPURLResponse *_Nullable response,
NSError *_Nullable error))completionHandler;
通过指定URL或请求来创建任务,任务完成后调用completionHandler
示例
- 代理方式
// create
YMURLSessionTask *task = [session taskWithURL:[NSURL URLWithString:@"http://httpbin.org/get"]];
[task resume];
// delegate
- (void)YMURLSession:(YMURLSession *)session task:(YMURLSessionTask *)task didCompleteWithError:(NSError *)error {
}
- (void)YMURLSession:(YMURLSession *)session task:(YMURLSessionTask *)task didReceiveData:(NSData *)data {
}
- (void)YMURLSession:(YMURLSession *)session task:(YMURLSessionTask *)task willCacheResponse:(NSCachedURLResponse *)proposedResponse completionHandler:(void (^)(NSCachedURLResponse * _Nullable))completionHandler {
completionHandler(proposedResponse);
}
-(void)YMURLSession:(YMURLSession *)session task:(YMURLSessionTask *)task didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(YMURLSessionResponseDisposition))completionHandler {
completionHandler(YMURLSessionResponseAllow);
}
- completionHandler方式
YMURLSessionTask *task = [session taskWithURL:[NSURL URLWithString:@"http://httpbin.org/get"] completionHandler:^(NSData * _Nullable data, NSHTTPURLResponse * _Nullable response, NSError * _Nullable error) {
}];
[task resume];
0x03 将上传任务添加到会话
- (YMURLSessionTask *)taskWithRequest:(NSURLRequest *)request fromFile:(NSURL *)fileURL;
- (YMURLSessionTask *)taskWithRequest:(NSURLRequest *)request fromData:(NSData *)bodyData;
- (YMURLSessionTask *)taskWithStreamedRequest:(NSURLRequest *)request;
通过指定请求创建一个上传任务。
taskWithStreamedRequest
方法会调用 YMURLSession:task:needNewBodyStream:
代理方法,您需要通过 completionHandler
返回一个 NSInputStream
对象。当然,您也可以使用 NSURLMutableRequest
创建对象,并在 bodyStream
传入 NSInputStream
对象。
如果您需要上传大文件,建议使用 fromFile
方法,虽然 taskWithStreamedRequest
也支持大文件的传输,但其形式为循环执行 读取指定长度内容 -> 上传该内容
,该行为在内部线程是同步的,而 fromFile
方式会每次异步获取 3 * CURL_MAX_WRITE_SIZE 长度的内容供 libcurl 上传(CURL_MAX_WRITE_SIZE 为单次支持的最大上传长度),不仅减少文件 I/O 次数,也减少同步阻塞的时间,优化上传效率。
- (YMURLSessionTask *)taskWithRequest:(NSURLRequest *)request
fromFile:(NSURL *)fileURL
completionHandler:(void (^)(NSData *_Nullable data,
NSHTTPURLResponse *_Nullable response,
NSError *_Nullable error))completionHandler;
- (YMURLSessionTask *)taskWithRequest:(NSURLRequest *)request
fromData:(nullable NSData *)bodyData
completionHandler:(void (^)(NSData *_Nullable data,
NSHTTPURLResponse *_Nullable response,
NSError *_Nullable error))completionHandler;
通过指定请求创建任务,任务完成后调用 completionHandler
0x04 将下载任务添加到会话
- (YMURLSessionTask *)taskWithDownloadRequest:(NSURLRequest *)request;
- (YMURLSessionTask *)taskWithDownloadURL:(NSURL *)url;
通过指定请求创建一个下载任务,并返回临时文件,由于该文件是临时文件,因此必须打开该文件进行读取,或将其移动到应用程序沙盒容器目录中的永久位置,支持大文件下载。
当然,您也可以使用 taskWithRequest
和 taskWithURL
来自定义下载任务。
- (YMURLSessionTask *)taskWithDownloadRequest:(NSURLRequest *)request
completionHandler:(void (^)(NSURL *_Nullable location,
NSHTTPURLResponse *_Nullable response,
NSError *_Nullable error))completionHandler;
- (YMURLSessionTask *)taskWithDownloadURL:(NSURL *)url
completionHandler:(void (^)(NSURL *_Nullable location,
NSHTTPURLResponse *_Nullable response,
NSError *_Nullable error))completionHandler;
0x05 连接到特定主机和端口
- (YMURLSessionTask *)taskWithURL:(NSURL *)url connectToHost:(NSString *)host;
- (YMURLSessionTask *)taskWithURL:(NSURL *)url connectToHost:(NSString *)host connectToPort:(NSInteger)port;
// 创建包含 host port 的 request
[[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:@"https://httpbin.org/get"] connectToHost:@"52.202.2.199"];
[[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:@"https://httpbin.org/get"] connectToHost:@"52.202.2.199" connectToPort:443];
[[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:@"https://httpbin.org/get"]
connectToHost:@"52.202.2.199"
connectToPort:443
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:60];
连接到特定的主机和端口,其中主机支持IP地址的形式。如果使用正常的域名+主机+端口的请求方式,那么框架内部可以自动处理Cookie、Cache以及302等问题,当然该接口也支持SNI场景。
备注:该接口不会影响到DNS缓存,更多信息可以参考这里 https://curl.haxx.se/libcurl/c/CURLOPT_CONNECT_TO.html。
libcurl
libcurl是免费的,线程安全的,IPv6兼容的,功能丰富,支持良好,速度快,有详尽的文档,并被许多知名的大型和成功的公司使用。
libcurl是免费的,线程安全的,IPv6兼容的,功能丰富,支持良好,速度快,有完整的文档记录,已经被许多知名的,大的和成功的公司使用。
您可以在此查看更多内容:https://curl.haxx.se/
libcurl 版本
当前使用libcurl 7.64.1版本,与macOS Catalina保持一致,使用curl-android-ios进行构建,你也可以选择喜欢的版本进行构建。
HTTP/2
目前版本不支持HTTP/2,你可以使用Build-OpenSSL-cURL来构建支持HTTP/2功能的版本。
注意:在Build-OpenSSL-cURL中使用的是OpenSSL,而目前macOS Catalina中则是使用LibreSSL。我在Build-OpenSSL-cURL的基础上修改了一个支持LibreSSL的版本,链接地址:https://github.com/zymxxxs/libcurl-nghttp2-libressl-ios。
备注:
- 支持HTTP/2需要考虑包大小的影响
- 目前第一阶段的工作主要是在外接口与NSURLSession对齐以及支持DNS,HTTP/2暂时不在支持范围内,所以上述脚本只能保证构建出静态库,暂未做过较多的验证,请知悉。
最后
如果你研究过swift-corelibs-foundation的相关源码,你会发现其NSURLSession系列的功能(HTTP、FTP)是基于libcurl进行封装的。如果你想学习它,建议你直接学习官方源码,YMHTTP也是参考了其中大量的实现,然后补充了一些尚未实现的功能以及修复了一些BUG。
YMHTTP的诞生初衷是为了彻底解决HTTP DNS的问题(性能+SNI场景+ Cache + Cookies + 302),现在回头来看,倒像是切开了一道口,获得了更多的自由度,如果您需要什么功能,请通过ISSUE告诉我。
如何贡献
非常欢迎你的加入!你可以在 提出问题 或提交一个 Pull Request。
许可协议
YMHTTP 采用 MIT 许可协议。更多详情请参阅 LICENSE 文件。
感谢
- lindean 破老师,目前就职于PDD。感谢其初版 HTTP DNS 的实现,作为先驱者,填了无数坑,尤其是在 libcurl 中各种参数及 Cache 层的相关实现。
- amendgit 二老师,人称二哥,目前就职于支付宝,感谢其在
IO 多路复用
上解惑与指导。 - libcurl
- swift-corelibs-foundation
- curl-android-ios
- Build-OpenSSL-cURL
待办事项
- 目前指定 IP 的功能是通过 CURLOPT_CONNECT_TO 来实现的,其优点是不会影响 DNS Cache,但在 Charles 中会直接显示 IP 的请求。需要考虑是否替换为 CURLOPT_RESOLVE 参数,但对于 DNS Cache 的问题,是需要影响还是不能影响,需要删除吗?或者说,是使用 CURLOPT_CONNECT_TO 还是 CURLOPT_RESOLVE,哪一个更为合理?
- 目前大多数仍然基于 AFNetworking 进行封装,需要考虑是否提供一个 YMNetworking 版本以方便接入,也可以参考 retrofit 的接口实现。
- NSURLSessionTaskMetrics 尚未实现。使用 curl_easy_getinfo 实现,目前实现这个功能确实需要较大的代码改动,需要着重设计一下,目前属于重要不紧急,可以延后。