iOS开发文档中有一个文章"Uploading Streams of Data"专门讲如何传输流数据,它的实际原理是HTTP分块传输,下面会介绍分块传输是如何在iOS实现的
分块传输编码
分块传输会将实体主体分成多个块,每一块用16进制来标记块的大小,而实体的最后一块是用"0(CR+LF)"来标记
如果要使用分块传输编码的响应格式,我们需要设置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,您可以将数据写入其中。由于流的绑定,您写入输出流的数据可以用于输入流,然后任务可以从中读取。
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.
}
}
}];
}