身份验证和退避逻辑很头疼,让我们一次性完成并忘记它!这是一个库,允许您集中此逻辑,并忘记创建 HTTP 请求的丑陋部分。
-
📱 iOS 10.0+ -
💻 OS X 10.12+ -
⌚️ watchOS 3.0+ -
📺 tvOS 10.0+
还有另一个网络库?好吧,除了内置速率限制和强大的请求身份验证等独特优势之外,对您而言的一个主要优势是,任何标记的版本都已在生产环境中进行了测试。只有当 Spotify 应用程序(每天有数百万活跃用户)使用了两周后,我们才会标记一个新版本。因此,您可以确信标记的版本尽可能稳定。
至于 Spotify,我们希望有一个轻量级网络库,我们完全控制,以便快速修复错误,并仔细选择所需和支持的功能。该架构也与我们的 MVVM 和视图聚合服务架构很好地结合我们的 Spotify。
📐
架构SPTDataLoader
被设计为在 NSURLSession
之上具有 3 个额外层的 HTTP 栈。
- 应用层,控制每个服务的速率限制和退避策略,尊重“Retry-After”头部,并知道何时或是否应该重试请求。
- 用户层,控制 HTTP 请求的身份验证。
- 视图层,允许在视图释放时自动取消请求。
身份验证
在这种情况下,身份验证是抽象的,允许SPTDataLoaderFactory的创建者定义自己的令牌获取和注入语义。它允许在令牌无效的情况下异步获取令牌,并与HTTP请求-响应模式无缝集成。
退避策略
数据加载器服务允许显式设置URL的速率限制,或者由服务器通过使用“Retry-After”语义来决定。它通过使用抖动指数退避来允许退避重试,以防止在可预测的指数退避期结束后产生请求风暴。
🏗️
安装SPTDataLoader可以通过多种方式安装,可以是动态框架、静态库或通过CocoaPods或Carthage等依赖项管理器安装。
手动
动态框架
将框架拖入目标“General”部分的“Frameworks, Libraries, and Embedded Content”区域。
静态库
将SPTDataLoader.xcodeproj
拖入你的App的Xcode项目中,并在目标“Build Phases”部分中将其库链接到你的应用。
CocoaPods
要使用 CocoaPods 集成 SPTDataLoader 到您的项目中,请在您的 Podfile
中添加它
pod 'SPTDataLoader', '~> 2.1'
Carthage
要使用 Carthage 集成 SPTDataLoader 到您的项目中,请在您的 Cartfile
中添加它
github "spotify/SPTDataLoader" ~> 2.1
👀
使用示例有关此框架的使用示例,请查看 SPTDataLoader.xcodeproj
中的示例应用程序 SPTDataLoaderDemo
。只需按照 ClientKeys.h
中的说明操作即可。
创建 SPTDataLoaderService
在您的应用程序中,您应该只有一个 SPTDataLoaderService 的实例,理想情况下,您可以在类似于 AppDelegate 的地方构建它。它接受速率限制器、解析器、用户代理和 NSURLProtocols 数组。速率限制器允许外部对象更改不同端点的速率限制,解析器允许重写主机名,而 NSURLProtocols 数组允许支持除 http/s 之外的其他协议。
SPTDataLoaderRateLimiter *rateLimiter = [SPTDataLoaderRateLimiter rateLimiterWithDefaultRequestsPerSecond:10.0];
SPTDataLoaderResolver *resolver = [SPTDataLoaderResolver new];
self.service = [SPTDataLoaderService dataLoaderServiceWithUserAgent:@"Spotify-Demo"
rateLimiter:rateLimiter
resolver:resolver
customURLProtocolClasses:nil];
请注意,如果愿意,您可以提供所有这些作为 nil,最好在确定需要这些不同的配置选项之前使用 nil。
定义您自己的 SPTDataLoaderAuthoriser
如果您不需要验证您的请求,则可以跳过此步骤。为了验证您的请求与后端,您需要创建一个 SPTDataLoaderAuthoriser 的实现。在示例项目中,SPTDataLoaderAuthoriserOAuth 类有一个示例。在这个例子中,我们检查请求是否为我们尝试验证的域,然后执行验证(在这个案例中,我们向 HTTP 头中注入一个认证令牌)。为了在请求进行飞行时执行令牌刷新以保持其直到验证就绪,此接口是异步的。一旦您获得一个有效的令牌,就可以调用委托(在这种情况下将是工厂),以通知它请求已被认证。或者,如果您无法验证请求,则将错误通知给委托。
创建 SPTDataLoaderFactory
您的应用最好在用户登录后或在没有对您的调用进行认证的情况下仅创建一次 SPTDataLoaderFactory。该工厂控制您构建的授权对象对不同请求的授权。
id<SPTDataLoaderAuthoriser> oauthAuthoriser = [[SPTDataLoaderAuthoriserOAuth alloc] initWithDictionary:oauthTokenDictionary];
self.oauthFactory = [self.service createDataLoaderFactoryWithAuthorisers:@[ oauthAuthoriser ]];
我们在这里使用一个授权器的实现,将此工厂创建的所有请求汇总到这些授权器中。
创建 SPTDataLoader
您的应用应为想要发出请求的每个视图创建一个 SPTDataLoader 对象(例如,最好不要在类之间共享这些对象)。这样,当视图模型被取消分配时,视图模型发出的请求也将被取消。
SPTDataLoader *dataLoader = [self.oauthFactory createDataLoader];
请注意,此数据加载器只会授权从提供给其工厂的授权器中提供的请求。
创建 SPTDataLoaderRequest
为了创建请求,您所需的信息只有 URL 和请求来源。有关更高级的请求,请查看 SPTDataLoaderRequest 的属性,这些属性允许您更改方法、超时、重试以及是否流式传输结果。
SPTDataLoaderRequest *request = [SPTDataLoaderRequest requestWithURL:meURL
sourceIdentifier:@"playlists"];
[self.dataLoader performRequest:request];
完成请求后,您的数据加载器将调用其委托以处理请求的结果。
处理流式请求数据
有时,您可能想要一个接一个地处理HTTP请求,而不是在最后接收大量的回调数据,这对于内存和某些类型的媒体处理更有效。对于Spotify的用途,它适用于我们的歌曲预览流式MP3的处理。使用流式API的例子
void AudioSampleListener(void *, AudioFileStreamID, AudioFileStreamPropertyID, UInt32 *);
void AudioSampleProcessor(void *, UInt32, UInt32, const void *, AudioStreamPacketDescription *);
- (void)load
{
NSURL *URL = [NSURL URLWithString:@"http://i.spotify.com/mp3_preview"];
SPTDataLoaderRequest *request = [SPTDataLoaderRequest requestWithURL:URL sourceIdentifier:@"preview"];
request.chunks = YES;
[self.dataLoader performRequest:request];
}
- (void)dataLoader:(SPTDataLoader *)dataLoader
didReceiveDataChunk:(NSData *)data
forResponse:(SPTDataLoaderResponse *)response
{
void *mp3Data = calloc(data.length, 1);
memcpy(mp3Data, data.bytes, data.length);
AudioFileStreamParseBytes(_audioFileStream, data.length, mp3Data, 0);
free(mp3Data);
}
- (void)dataLoader:(SPTDataLoader *)dataLoader didReceiveInitialResponse:(SPTDataLoaderResponse *)response
{
AudioFileStreamOpen((__bridge void *)self,
AudioSampleListener,
AudioSampleProcessor,
kAudioFileMP3Type,
&_audioFileStream);
}
- (BOOL)dataLoaderShouldSupportChunks:(SPTDataLoader *)dataLoader
{
return YES;
}
确保在您的代理中渲染YES,以便告诉数据加载器您支持分块,并将请求的分块属性设置为YES。
针对特定端点限流
如果您在服务中指定了速率限制器,则可以为其提供一个默认的每秒请求次数指标,该指标适用于所有从您的应用程序发出的请求。(参见“创建SPTDataLoaderService
”)。但是,您也可以指定特定HTTP端点的速率限制,如果需要强制控制客户端向执行大量工作的后端发送请求的速率,这可能很有用。
SPTDataLoaderRateLimiter *rateLimiter = [SPTDataLoaderRateLimiter rateLimiterWithDefaultRequestsPerSecond:10.0];
NSURL *URL = [NSURL URLWithString:@"http://www.spotify.com/thing/thing"];
[rateLimiter setRequestsPerSecond:1 forURL:URL];
需要注意的是,当您为一个URL设置每秒请求次数时,它考虑了主机以及URL的第一个组件,并限制了所有符合该描述的内容。
替换所有请求的主机
SPTDataLoaderService接受一个解析器对象作为其参数之一。如果您选择将其设置为非nil,则可以在接收请求数据时切换不同请求的主机。在Spotify,我们的请求可以通过多个DNS匹配,这为我们提供备份和应急措施,以防其中一台机器故障。这些操作发生在SPTDataLoaderResolver中,您可以指定多个替代地址作为主机。
SPTDataLoaderResolver *resolver = [SPTDataLoaderResolver new];
NSArray *alternativeAddresses = @[ @"spotify.com",
@"backup.spotify.com",
@"backup2.spotify.com",
@"backup3.spotify.com",
@"192.168.0.1",
@"final.spotify.com" ];
[resolver setAddresses:alternativeAddresses forHost:@"spotify.com"];
这允许对spotify.com发出的任何请求使用这些地址(按顺序)之一,如果spotify.com变得不可用。
使用抖动指数计时器
这个库中包含一个名为 SPTDataLoaderExponentialTimer 的类,它内部使用这个类来执行带有重试的退避。之所以要添加随机性,是为了防止出现“可预测的猛攻”造成服务崩溃。为了使用这个类,有一些注意事项。例如,不要这样初始化类
SPTDataLoaderExponentialTimer *timer = [SPTDataLoaderExponentialTimer exponentialTimerWithInitialTime:0.0
maxTime:10.0];
NSTimeInterval backoffTime = 0.0;
for (int i = 0; i < 1000; ++i) {
backoffTime = timer.timeIntervalAndCalculateNext;
}
这将导致 backoffTime 剩余为 0。为什么?因为 0.0 乘以指数数仍然是 0。一个好的初始时间可能是 0.5 或 1.0 秒。你还会注意到,随着你计算下一个间隔次数的增多,backoffTime 将越来越远离原始指数时间。
SPTDataLoaderExponentialTimer *timer = [SPTDataLoaderExponentialTimer exponentialTimerWithInitialTime:1.0
maxTime:1000.0];
NSTimeInterval backoffTime = 0.0;
for (int i = 0; i < 1000; ++i) {
backoffTime = timer.timeIntervalAndCalculateNext;
}
这将导致 backoffTime 与其普通的指数计算相去甚远。为什么?因为我们向计算中添加了随机抖动,以防止客户端同时连接,以便在重新连接风暴中均匀分散负载。随着指数的增长,抖动也会变得更大。
消费观察
SPTDataLoaderService 允许您添加一个消费观察者,该观察者的目的是监控服务的上传和下载的数据消耗。这个对象必须符合 SPTDataLoaderConsumptionObserver 协议。考虑到它只有一个方法,这相当简单,如下所示
- (void)load
{
[self.service addConsumptionObserver:self];
}
- (void)unload
{
[self.service removeConsumptionObserver:self];
}
- (void)endedRequestWithResponse:(SPTDataLoaderResponse *)response
bytesDownloaded:(int)bytesDownloaded
bytesUploaded:(int)bytesUploaded
{
NSLog(@"Bytes Downloaded: %d", bytesDownloaded);
NSLog(@"Bytes Uploaded: %d", bytesUploaded);
}
请注意,这不仅仅是指内容负载,还包括头部信息。
创建一个自定义授权器
SPTDataLoader 架构旨在围绕用户级别集中认证(在这种情况下由工厂表示)。为了做到这一点,必须在创建工厂时注入您自己制作的授权器。通常情况下,授权器会注入一个授权头到任何它要授权的请求中。下面是一个用于 OAuth 流的常规授权器构建示例。
@synthesize delegate = _delegate;
- (NSString *)identifier
{
return @"OAuth";
}
- (BOOL)requestRequiresAuthorisation:(SPTDataLoaderRequest *)request
{
// Here we check the hostname to see if it one of the hostnames we authorise against
// It is also advisable to check whether we are using HTTPS, if we are not we should not inject our Authorisation
// header in order to keep it secret from prying eyes
return [request.URL.host isEqualToString:@"myauth.com"] && [request.URL.scheme isEqualToString:@"https"];
}
- (void)authoriseRequest:(SPTDataLoaderRequest *)request
{
[request addValue:@"My Token" forHeader:@"Authorization"];
[self.delegate dataLoaderAuthoriser:self authorisedRequest:request];
}
- (void)requestFailedAuthorisation:(SPTDataLoaderRequest *)request response:(SPTDataLoaderResponse *)response
{
// This tells us that the server returned a 400 error code indicating that the authorisation did not work
// Commonly this means you should attempt to get another authorisation token
// Or the response object should be inspected for additional information from the backend
}
- (void)refresh
{
// Forces a refresh of the authorisation token
}
如你所见,我们在这里仅仅是在处理头部信息。需要注意的是,如果你收到了一个 authoriseRequest: 调用,剩余的请求将不会执行,直到你已经向代理发送信号,告知请求已经授权或未能授权。
Swift 追加
Swift应用程序中增强使用的额外API可以通过SPTDataLoaderSwift
库获得。
// Creating a DataLoader instance
let dataLoader = dataLoaderFactory.makeDataLoader(/* optional */responseQueue: myCustomQueue)
// Creating a Request instance -- all functions can be chained
let request = dataLoader.request(modelURL, sourceIdentifier: "model-page")
// Modifying the request properties
request.modify { request in
request.body = modelData
request.method = .patch
request.addValue("application/json", forHeader: "Accept")
}
// Adding a response validator
request.validate { response in
guard response.statusCode.rawValue == 200 else {
throw ValidationError.badStatus(code: response.statusCode.rawValue)
}
}
// Adding a response serializer (and executing the request)
request.responseDecodable { response in
modelResultHandler(response.result)
}
// Cancelling the request
request.cancel()
您也可以定义序列化器来处理自定义数据类型
struct ProtobufResponseSerializer<Message: SwiftProtobuf.Message>: ResponseSerializer {
func serialize(response: SPTDataLoaderResponse) throws -> Message {
guard response.error == nil else {
throw response.error.unsafelyUnwrapped
}
guard let data = response.body else {
throw ResponseSerializationError.dataNotFound
}
return try Message(serializedData: data)
}
}
let modelSerializer = ProtobufResponseSerializer<MyCustomModel>()
request.responseSerializable(serializer: modelSerializer) { response in
modelResultHandler(response.result)
}
📖
背景故事在Spotify,我们开始转向去中心化的HTTP架构,在这个过程中经历了一些成长的烦恼。最初我们有一个数据加载器,会在访问令牌无效时尝试刷新它,但我们很快发现这很难跟踪。我们需要一种方法,可以在HTTP请求中自动注入这种授权数据,而不需要我们的特性做任何比当前更多的繁重工作。
因此,我们想出了一个优雅的方法来在请求需要时即时注入令牌。我们还想从我们专有协议的错误中吸取教训,并在早期加入回退策略,以避免我们因为大量的错误请求而DDoS自己的后端。
📚
文档有关完整文档,请参阅SPTDataLoader
文档,网址为CocoaDocs.org。
如果您想的话,还可以通过以下Dash源将其添加到Dash中
dash-feed://http%3A%2F%2Fcocoadocs.org%2Fdocsets%2FSPTDataLoader%2FSPTDataLoader.xml
📬
贡献欢迎贡献,有关更多信息,请查看CONTRIBUTING.md文档。
📝
许可该项目在Apache 2.0许可下可用。