May
5
restful接口时代来临后,我们很少会提到http分块传输这件事情。http分块传输是指的服务端以数据流的方式分段输出数据到客户端,并且客户端分段接收数据并展示,此时返回体的header会有标示Transfer-Encoding: chunked,当然因为是分端返回的,就不存在Content-Length了(参考https://www.jianshu.com/p/d9941adfe58f)。
目前随着轻客户端重服务端的原理,服务端处理返回的数据量是越来越大,并且一些重业务的场景,restful过于庞大并且越来越缓慢。为了保持接口一致性的前提下提升用户体验,http分块传输不失为一种可以讨论方案。
前期设定
如果是分块传输,目前常用的restful传输接口实现——json就不可用了,json本身一般是不支持流式阅读的。如果一定要一整个阅读,分块传输就没意义了。场景的流式阅读的文件格式是:xml。比如可以想象html网页就是从上到下一边加载一边渲染的。同时考虑对于复杂的字段,xml的字段确实也太繁琐了,还是json精简一些。因此可以设定为以xml来包裹最小阅读分块数据,每个数据块里通过json去表达。
- 设定1:通过xml来构成返回体结构,通过json去填充每个返回数据块的内容。
同时由于是一边输出一边计算,因此它不想restful接口一样结果有一个明确的状态成功或者失败,有可能输出着输出着突然发生了异常并且传输中断了。为了异常处理,至少期望传输的数据中有客户端的格式即xml中可以捕获的异常。因此异常输出需要商定一个xml可以处理的异常格式,并且保证在遇到异常标签为止,xml格式符合预期。
- 设定2:异常处理需要在xml数据流中返回并通过xml处理。
基于以上设定,我们按现代iOS开发模式,选择关键类:网络请求URLSession(支持分端接收数据),xml解析NSXMLParser(支持流式输出数据)
同时编写服务端demo:http://lrdcq.com/test/oc_xml_reader.php
正常数据如下:
使用URLSession接收分块传输数据
URLSession有NSURLSessionDelegate中可以处理类似的处理方法,主要包括:
这个有几个坑:
1. NSURLSession的创建要指明delegate和delegateQueue(默认主线程)
2. 数据传输用NSURLSessionDataTask即可,不需要downloadTask之类的
3. didReceiveResponse的触发比较奇怪,会在buffer接收一定数据之后才会触发。但是如果服务端返回的是json格式,就会立刻触发。这可能是苹果的一个bug(future):https://developer.apple.com/forums/thread/64875
4. task创建时不要用带completionHandler的构造方法,相关方法不会调用delegate:These methods create tasks that bypass the normal delegate calls for response and data delivery, and provide a simple cancelable asynchronous interface to receiving data.
xml解析
NSXMLParser可以以一个NSInputStream的方式进行输入,并且一边输出一边处理。
这里我们按demo的数据,假设我们要处理一个叫node和error的节点,并且不考虑嵌套。其中node中是一个json数据,储存到一个array里。error是错误处理专用数据。那么最粗暴的处理代码为:
需要节点开始/节点结束,和节点内容处理的3个方法实现。其中也有几个坑:
1. NSXMLParser的开始方法是同步方法,Stream有多久它就会卡多久,因此务必单独开现场去启动。
2. 因为是流式处理,foundCharacters方法有可能分段的返回节点数据,因此用了一个NSMutableString来储存拼装。
3. NSXMLParser的delegate返回是在异步现场的并且在多个线程中跳转,所以用上面这种写法其实是不安全的,需要添加额外的保护。
stream流动起来
如何从URLSession拿到一个输出的stream并且塞给NSXMLParser呢?通过一个输入输出Stream绑定即可:
还是有几个小坑:
1. getBoundStreamsWithBufferSize:需要一个buffer,即每次streamOutput写入时可能存在的最大size,否则write会写入不完整,为了保险起见还是尽量大吧。反正数据网络请求再大大不了多少。要么,write部分做相关保护逻辑。
2. streamOutput记得创建即open,并且在网络请求完成时关闭streamOutput(inputStream也会自动关闭)。这样也才能出发NSXMLParser的解析完成。
3. NSXMLParser解析到错误的时候,也可以主动关闭网络请求与stream,避免存在内存泄漏。错误的情况如下:
其他
1. 虽然是分块传输,但是视网络通信情况,客户端收到的块和服务端发出的块并不完全一致,可能存在风险。
2. 同样,node在异常处理的情况下会有截断风险,error处理除了xml标签也可以通过特殊符号在网络层处理而不是xml解析层处理。渲染数据同理。
3. 分块http通信时间过长有中断的风险,不宜使用到适合长链接解决的场景里(比如聊天室)。同时现代客户端通信方式也有不少替代方案,怀旧党酌情使用,梦回1998。
other:如上图demo代码如下:
目前随着轻客户端重服务端的原理,服务端处理返回的数据量是越来越大,并且一些重业务的场景,restful过于庞大并且越来越缓慢。为了保持接口一致性的前提下提升用户体验,http分块传输不失为一种可以讨论方案。
前期设定
如果是分块传输,目前常用的restful传输接口实现——json就不可用了,json本身一般是不支持流式阅读的。如果一定要一整个阅读,分块传输就没意义了。场景的流式阅读的文件格式是:xml。比如可以想象html网页就是从上到下一边加载一边渲染的。同时考虑对于复杂的字段,xml的字段确实也太繁琐了,还是json精简一些。因此可以设定为以xml来包裹最小阅读分块数据,每个数据块里通过json去表达。
- 设定1:通过xml来构成返回体结构,通过json去填充每个返回数据块的内容。
同时由于是一边输出一边计算,因此它不想restful接口一样结果有一个明确的状态成功或者失败,有可能输出着输出着突然发生了异常并且传输中断了。为了异常处理,至少期望传输的数据中有客户端的格式即xml中可以捕获的异常。因此异常输出需要商定一个xml可以处理的异常格式,并且保证在遇到异常标签为止,xml格式符合预期。
- 设定2:异常处理需要在xml数据流中返回并通过xml处理。
基于以上设定,我们按现代iOS开发模式,选择关键类:网络请求URLSession(支持分端接收数据),xml解析NSXMLParser(支持流式输出数据)
同时编写服务端demo:http://lrdcq.com/test/oc_xml_reader.php
<?php
header('Content-type: application/json; charset=utf-8');
echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>";
echo '<root>';
for ($i = 0; $i < 10; $i++) {
echo "<node>{\"name\":\"你好啊,$i!\",\"desc\":\"我是第 $i 个lrdcq的描述。\"}</node>";
flush();
ob_flush();
sleep(1);
if ($_GET["exception"] && $i == 6) {
throw new Exception('<error>数据解析异常</error>');
}
}
echo '</root>';
?>
正常数据如下:
使用URLSession接收分块传输数据
URLSession有NSURLSessionDelegate中可以处理类似的处理方法,主要包括:
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
//接收到部分数据(data)
}
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {
completionHandler(NSURLSessionResponseAllow);
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(nullable NSError *)error {
//请求完成,处理流关闭
}
这个有几个坑:
1. NSURLSession的创建要指明delegate和delegateQueue(默认主线程)
2. 数据传输用NSURLSessionDataTask即可,不需要downloadTask之类的
_session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:nil];
NSURLSessionDataTask *dataTask = [_session dataTaskWithRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://lrdcq.com/test/oc_xml_reader.php"]]];
[dataTask resume];
3. didReceiveResponse的触发比较奇怪,会在buffer接收一定数据之后才会触发。但是如果服务端返回的是json格式,就会立刻触发。这可能是苹果的一个bug(future):https://developer.apple.com/forums/thread/64875
4. task创建时不要用带completionHandler的构造方法,相关方法不会调用delegate:These methods create tasks that bypass the normal delegate calls for response and data delivery, and provide a simple cancelable asynchronous interface to receiving data.
xml解析
NSXMLParser可以以一个NSInputStream的方式进行输入,并且一边输出一边处理。
这里我们按demo的数据,假设我们要处理一个叫node和error的节点,并且不考虑嵌套。其中node中是一个json数据,储存到一个array里。error是错误处理专用数据。那么最粗暴的处理代码为:
_xml = [[NSXMLParser alloc] initWithStream:_streamInput];
_xml.delegate = self;
dispatch_async(dispatch_queue_create("parser.queue", NULL), ^{
[_xml parse];
NSLog(@"parse end");
});
- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(nullable NSString *)namespaceURI qualifiedName:(nullable NSString *)qName attributes:(NSDictionary<NSString *, NSString *> *)attributeDict {
if ([elementName isEqualToString:@"node"] || [elementName isEqualToString:@"error"]) {
_elementString = [NSMutableString string];
}
}
- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string {
[_elementString appendString:string];
}
- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(nullable NSString *)namespaceURI qualifiedName:(nullable NSString *)qName {
if ([elementName isEqualToString:@"node"]) {
NSError *err;
NSDictionary *node = [NSJSONSerialization JSONObjectWithData:[[_elementString copy] dataUsingEncoding:NSUTF8StringEncoding] options:NSJSONReadingMutableContainers error:&err];
[_nodes addObject:node];
} else if ([elementName isEqualToString:@"error"]) {
self.error = [_elementString copy];
[parser abortParsing];
}
_elementString = nil;
}
需要节点开始/节点结束,和节点内容处理的3个方法实现。其中也有几个坑:
1. NSXMLParser的开始方法是同步方法,Stream有多久它就会卡多久,因此务必单独开现场去启动。
2. 因为是流式处理,foundCharacters方法有可能分段的返回节点数据,因此用了一个NSMutableString来储存拼装。
3. NSXMLParser的delegate返回是在异步现场的并且在多个线程中跳转,所以用上面这种写法其实是不安全的,需要添加额外的保护。
stream流动起来
如何从URLSession拿到一个输出的stream并且塞给NSXMLParser呢?通过一个输入输出Stream绑定即可:
NSInputStream *inputStream = nil;
NSOutputStream *outputStream = nil;
[NSStream getBoundStreamsWithBufferSize:2048 inputStream:&inputStream outputStream:&outputStream];
[inputStream open];
[outputStream open];
_streamInput = inputStream;
_streamOutput = outputStream;
//把streamInput丢给NSXMLParser
_xml = [[NSXMLParser alloc] initWithStream:_streamInput];
//往streamOutput写入didReceiveData的数据
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
[self.streamOutput write:[data bytes] maxLength:data.length];
}
还是有几个小坑:
1. getBoundStreamsWithBufferSize:需要一个buffer,即每次streamOutput写入时可能存在的最大size,否则write会写入不完整,为了保险起见还是尽量大吧。反正数据网络请求再大大不了多少。要么,write部分做相关保护逻辑。
2. streamOutput记得创建即open,并且在网络请求完成时关闭streamOutput(inputStream也会自动关闭)。这样也才能出发NSXMLParser的解析完成。
3. NSXMLParser解析到错误的时候,也可以主动关闭网络请求与stream,避免存在内存泄漏。错误的情况如下:
其他
1. 虽然是分块传输,但是视网络通信情况,客户端收到的块和服务端发出的块并不完全一致,可能存在风险。
2. 同样,node在异常处理的情况下会有截断风险,error处理除了xml标签也可以通过特殊符号在网络层处理而不是xml解析层处理。渲染数据同理。
3. 分块http通信时间过长有中断的风险,不宜使用到适合长链接解决的场景里(比如聊天室)。同时现代客户端通信方式也有不少替代方案,怀旧党酌情使用,梦回1998。
other:如上图demo代码如下:
@interface DemoViewController () <NSXMLParserDelegate, NSURLSessionDataDelegate, UITableViewDelegate, UITableViewDataSource>
//数据加载
@property (strong, nonatomic) NSXMLParser *xml;
@property (strong, nonatomic) NSURLSession *session;
@property (strong, nonatomic) NSInputStream *streamInput;
@property (strong, nonatomic) NSOutputStream *streamOutput;
//数据处理
@property (strong, nonatomic) NSMutableString *elementString;//临时储存节点数据
//渲染数据
@property (strong, nonatomic) NSString *error;
@property (strong, nonatomic) NSMutableArray<NSDictionary *> *nodes;
@property (strong, nonatomic) UITableView *table;
@end
@implementation DemoViewController
- (void)viewDidLoad {
[super viewDidLoad];
_nodes = [NSMutableArray array];
//数据加载
_session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:nil];
NSURLSessionDataTask *dataTask = [_session dataTaskWithRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://lrdcq.com/test/oc_xml_reader.php?exception=1"]]];
NSInputStream *inputStream = nil;
NSOutputStream *outputStream = nil;
[NSStream getBoundStreamsWithBufferSize:2048 inputStream:&inputStream outputStream:&outputStream];
[inputStream open];
[outputStream open];
_streamInput = inputStream;
_streamOutput = outputStream;
_xml = [[NSXMLParser alloc] initWithStream:_streamInput];
_xml.delegate = self;
[dataTask resume];
dispatch_async(dispatch_queue_create("parser.queue", NULL), ^{
[_xml parse];
NSLog(@"parse end");
});
//界面组成
_table = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain];
_table.delegate = self;
_table.dataSource = self;
[_table registerClass:[UITableViewCell class] forCellReuseIdentifier:@"TableCellId"];
[self.view addSubview:_table];
}
#pragma mark - TableView
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.row < _nodes.count) {
NSDictionary *node = _nodes[indexPath.row];
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"TableCellId"];
[cell.textLabel setText:[NSString stringWithFormat:@"%@ - %@", node[@"name"], node[@"desc"]]];
[cell.textLabel setFont:[UIFont systemFontOfSize:16]];
[cell.textLabel setTextColor:[UIColor grayColor]];
cell.backgroundColor = [UIColor whiteColor];
return cell;
} else {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"TableCellId"];
[cell.textLabel setText:_error];
[cell.textLabel setFont:[UIFont systemFontOfSize:20]];
[cell.textLabel setTextColor:[UIColor redColor]];
cell.backgroundColor = [UIColor whiteColor];
return cell;
}
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return _nodes.count + (_error.length ? 1 : 0);
}
#pragma mark - NSURLSession
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
NSLog(@"didReceiveData = %@", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
const uint8_t *dataBytes = [data bytes];
[self.streamOutput write:dataBytes maxLength:data.length];
}
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {
completionHandler(NSURLSessionResponseAllow);
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(nullable NSError *)error {
[self.streamOutput close];
}
#pragma mark - NSXMLParser
- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(nullable NSString *)namespaceURI qualifiedName:(nullable NSString *)qName attributes:(NSDictionary<NSString *, NSString *> *)attributeDict {
if ([elementName isEqualToString:@"node"] || [elementName isEqualToString:@"error"]) {
_elementString = [NSMutableString string];
}
}
- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string {
[_elementString appendString:string];
}
- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(nullable NSString *)namespaceURI qualifiedName:(nullable NSString *)qName {
if ([elementName isEqualToString:@"node"]) {
NSError *err;
NSDictionary *node = [NSJSONSerialization JSONObjectWithData:[[_elementString copy] dataUsingEncoding:NSUTF8StringEncoding] options:NSJSONReadingMutableContainers error:&err];
[_nodes addObject:node];
dispatch_async(dispatch_get_main_queue(), ^{
[_table reloadData];
});
} else if ([elementName isEqualToString:@"error"]) {
self.error = [_elementString copy];
[parser abortParsing];
dispatch_async(dispatch_get_main_queue(), ^{
[_table reloadData];
});
}
_elementString = nil;
}
@end