Uploading Streams of Data(译)

215 阅读4分钟

iOS开发文档中有一个文章"Uploading Streams of Data"专门讲如何传输流数据,它的实际原理是HTTP分块传输,下面会介绍分块传输是如何在iOS实现的

分块传输编码

分块传输会将实体主体分成多个块,每一块用16进制来标记块的大小,而实体的最后一块是用"0(CR+LF)"来标记 image.png

如果要使用分块传输编码的响应格式,我们需要设置HTTP头部字段:Transfer-Encoding:chunked,以请求报文为例

POST / HTTP/1.1
Transfer-Encoding: chunked
Content-Type: text/plain; charset=ISO-8859-1
Host: juejin.cn
Connection: Keep-Alive
Accept-Encoding: gzip,deflate
​
11
I sent a message!
5
apple
0


使用分块编码的特殊之处在于:请求报文头部有一个Transfer-Encoding: chunked字段。另外,我们也要关注下实体,11是16进制数字,代表分块传输第一块数据长度为11,"I sent a message!"长度就是17;5代表第二块数据长度为5;0代表分块传输结束(0后面一定跟2个空行)

问题

问:分块传输可以把数据分成很多块,那会发起多个HTTP请求吗?
答:依然只有一个HTTP请求。分块传输是基于HTTP1.1的keep-alive机制,底层原理是TCP连接的复用

问:流数据传输有哪些应用场景呢?
答:
1.视频在线播放功能。在HTTP通信过程中,视频资源尚未全部传输之前,视频无法播放,可以把视频数据分成很多块,让视频边边下载边播放。
2.在HTTP通信过程中,请求编码实体资源尚未全部传输之前,浏览器无法显示数据。在传输大容量数据时,通过把数据分成很多块,能够让浏览器逐步显示页面。
3.大文件上传:在视频上传过程中,如果把文件全部读取到内存中后发起上传请求,很容导致内存不足,造成crash,可以把大文件分片,每次只加载一片到内存中,分块传输。

注:还有一个技术方案是文件切片,多个HTTP请求,这个方案以后再讨论。

Upload Streams of Data

创建NSURLSession

创建一个默认配置的NSURLSession对象,代理对象设置为self, self要实现NSURLSessionTaskDelegate协议中的方法

NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:**self** delegateQueue:[NSOperationQueue mainQueue]];

创建流式任务

NSURL *url = [NSURL URLWithString:@"https://dog.ceo/api/breeds/image/random"];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:10];
request.HTTPMethod = @"POST";
NSURLSessionUploadTask *uploadTask = [session uploadTaskWithStreamedRequest:request];
[uploadTask resume];

使用绑定的流对来提供输入流

向上传任务提供流您将流数据作为NSInputStream提供给上传任务。该任务从该流中读取数据并将其上载到目的地。
向输入流提供数据的一个好方法是使用绑定的流对。绑定对包含一个NSOutputStream,您可以将数据写入其中。由于流的绑定,您写入输出流的数据可以用于输入流,然后任务可以从中读取。

image.png

NSInputStream *input = nil;
NSOutputStream *output = nil;
[NSStream getBoundStreamsWithBufferSize:1024 * 4 inputStream:&input outputStream:&output];
if (input == nil || output == nil) {
  [NSException exceptionWithName:@"Bounds stream error" reason:"On return of `getBoundStreams`, both `inputStream` and `outputStream` will contain non-nil streams." userInfo:nil];
}
self.inputStream = input;
self.outputStream = output;
output.delegate = self;
[output scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[output open];

准备就绪时将数据写入流

需要NSURLSessionTaskDelegate中方法URLSession:task:needNewBodyStream: 当你调用上传任务的resume方法后,这个方法就会被调用。此时直接传入流数据

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task needNewBodyStream:(void (^)(NSInputStream * _Nullable))completionHandler {
    completionHandler(self.inputStream);
}

为上传任务提供流

流准备就绪时写数据到输出流。收到流就绪通知时,调动NSStreamDelegate方法stream:handleEvent:,当流状态参数是NSStreamEventHasSpaceAvailable时,输出流就准备好读更多数据了。 如果此时还没有准备好写数据,可以使用一个标志位canWrite,表示输出流是否有空间写数据。之后再使用一个定时器,定时查询是否可以写数据。

- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode {
    if (eventCode == NSStreamEventHasSpaceAvailable) {
        self.canWrite = true;
    } else if (eventCode == NSStreamEventErrorOccurred){
        // Close the streams and alert the user that the upload failed.
    } else {

    }
}
- (void)writeDataToOutputstream {
    __weak typeof(self) weakSelf = self;
    [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        if (weakSelf.canWrite) {
            NSString *message = @"*** \(Date())\r\n";
            NSData *messageData = [message dataUsingEncoding:NSUTF8StringEncoding];
            NSUInteger messageCount = messageData.length;
            NSInteger bytesWritten = [self.outputStream write:messageData.bytes maxLength:messageCount];
            if (bytesWritten < messageCount) {
                // Handle writing less data than expected.
            }
        }
    }];
}