Continuation of iOS development network programming breakpoint

  • 2020-06-23 02:03:50
  • OfStack

preface

Network download is a function we often use in the project. If it is a small file download, such as pictures and text, we can directly request the source address, and then download it once. But if you are downloading a large audio or video file, it is impossible to download it all at once. Users may download it for a period of time, close the program, and go home to download it. At this point, you need to implement the breakpoint continuation function. It allows users to pause the download at any time, and then continue the download the next time.

Today we'll look at how to simply wrap a breakpoint continuation of the class and implement the following functions.

1. The user only needs to call one interface to download and get the progress of the download.

2. Successfully downloaded, you can get the file storage location

3. Download failed and give the reason for failure

4. You can pause the download, start the next download, and continue the download as before

The principle of interpretation

To implement breakpoint continuation, it is usually necessary for the client to record the current download progress and notify the server of the content fragment to be downloaded when the continuation is needed.

In the HTTP1.1 protocol (RFC2616), a breakpoint is defined to continue the associated HTTP header Range and Content-Range Field, the simplest breakpoint continuation implementation is as follows:

1. The client downloaded a file of 1024K, of which 512K has been downloaded

2. The network is interrupted, and the client requests the continuation of transmission. Therefore, the segment to be continued this time needs to be declared in the HTTP header: Range:bytes=512000- This header informs the server to start transferring files from the file's location of 512K
3. The server receives the breakpoint to continue the transmission request from the file's location of 512K, and adds in the HTTP header: Content-Range:bytes 512000-/1024000 And the HTTP status code returned by the server should be 206 instead of 200.

The difficulty that

1. How does the client get the number of bytes of the downloaded file

On the client side, we need to record the size of each file downloaded by each user, and then implement the function of step 1 in the principle explanation.

So how do you record that?

In fact, we can directly get the size of the file under the specified path. iOS has provided related functions. The implementation code is as follows.


[[[NSFileManager defaultManager] attributesOfItemAtPath: FileStorePath error:nil][NSFileSize] integerValue]

2. How do I get the total number of bytes in the downloaded file

In step 1, we get the number of bytes of the downloaded file, here we need to get the total number of bytes of the downloaded file, with these two values, we can calculate the download progress.

So how do you get it? Here we need the http header conten-length Field, let's look at what that field means

Content-Length Used to describe the transport length of the HTTP message entity the transfer-length of the message-body . In HTTP protocol, the length of message entity is different from the transmission length of message entity. For example, under gzip compression, the length of message entity is the length before compression, while the transmission length of message entity is the length after COMPRESSION of gzip.

To put it simply, content-length Represents the number of bytes of the downloaded file.

In comparison to step 3, we can see that if we want to calculate the total number of bytes in the file, we must add the number of bytes already downloaded content-length .

We need to store the total number of bytes per downloaded file, and we chose to use the plist file, which contains a dictionary. Sets the file name to the key value, and the number of bytes of the file that have been downloaded is the value.

To prevent duplication, here we set the file name to download url hash Value, can guarantee not heavy.

The implementation code is as follows:


- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSHTTPURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler
{
  self.totalLength = [response.allHeaderFields[@"Content-Length"] integerValue] + DownloadLength;

  NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithContentsOfFile: TotalLengthPlist];

  if (dict == nil) dict = [NSMutableDictionary dictionary];
  dict[ Filename] = @(self.totalLength);

  [dict writeToFile: TotalLengthPlist atomically:YES];
}

The above Content-Range0 Method will be called once when the request receives a response, we can get the response information in this method, out content-length Field.

3. Encapsulate 1 method to achieve download progress, prompt for success and failure

We can imitate AFNetwork , encapsulate the download into one method and then use a different one block To implement the download progress, success, failure after the callback.

The definition is as follows:


-(void)downLoadWithURL:(NSString *)URL
       progress:(progressBlock)progressBlock
        success:(successBlock)successBlock
         faile:(faileBlock)faileBlock
{
  self.successBlock = successBlock;
  self.failedBlock = faileBlock;
  self.progressBlock = progressBlock;
  self.downLoadUrl = URL;
  [self.task resume];
}

The top three block Both macros are defined in a way that makes them look more concise. See the complete code below for details.

And then we can go in NSURLSessionDataDelegate To implement three of the corresponding proxy methods block Call, and then pass in the corresponding parameters. So that when someone else calls our method, it can be in the corresponding block Implements a callback. Refer to the complete code below for details

Full code implementation

Here is the complete code implementation


#import <Foundation/Foundation.h>
typedef void (^successBlock) (NSString *fileStorePath);
typedef void (^faileBlock) (NSError *error);
typedef void (^progressBlock) (float progress);

@interface DownLoadManager : NSObject <NSURLSessionDataDelegate>
@property (copy) successBlock successBlock;
@property (copy) faileBlock   failedBlock;
@property (copy) progressBlock  progressBlock;


-(void)downLoadWithURL:(NSString *)URL
       progress:(progressBlock)progressBlock
        success:(successBlock)successBlock
         faile:(faileBlock)faileBlock;

+ (instancetype)sharedInstance;
-(void)stopTask;

@end

#import "DownLoadManager.h"
#import "NSString+Hash.h"

@interface DownLoadManager ()
/**  Download task  */
@property (nonatomic, strong) NSURLSessionDataTask *task;
/** session */
@property (nonatomic, strong) NSURLSession *session;
/**  Write a stream object to a file  */
@property (nonatomic, strong) NSOutputStream *stream;
/**  The total size of the file  */
@property (nonatomic, assign) NSInteger totalLength;
@property(nonatomic,strong)NSString *downLoadUrl;

@end


//  File name (in the sandbox), used md5 The hash url Is generated so that the filename is guaranteed to be unique 1
#define Filename self.downLoadUrl.md5String
//  File storage path ( caches ) 
#define FileStorePath [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent: Filename]
//  use plist The file stores the size of the downloaded file 
#define TotalLengthPlist [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"totalLength.plist"]
//  The size of a file that has been downloaded 
#define DownloadLength [[[NSFileManager defaultManager] attributesOfItemAtPath: FileStorePath error:nil][NSFileSize] integerValue]

@implementation DownLoadManager

#pragma mark -  To create a singleton 
static id _instance;

+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    _instance = [super allocWithZone:zone];
  });
  return _instance;
}

+ (instancetype)sharedInstance
{
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    _instance = [[self alloc] init];
  });
  return _instance;
}

- (id)copyWithZone:(NSZone *)zone
{
  return _instance;
}

- (id)mutableCopyWithZone:(NSZone *)zone {
  return _instance;
}

#pragma mark -  Public methods 

-(void)downLoadWithURL:(NSString *)URL
       progress:(progressBlock)progressBlock
        success:(successBlock)successBlock
         faile:(faileBlock)faileBlock
{
  self.successBlock = successBlock;
  self.failedBlock = faileBlock;
  self.progressBlock = progressBlock;
  self.downLoadUrl = URL;
  [self.task resume];


}

-(void)stopTask{
  [self.task suspend ];

}


#pragma mark - getter methods 
- (NSURLSession *)session
{
  if (!_session) {
    _session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[[NSOperationQueue alloc] init]];
  }
  return _session;
}

- (NSOutputStream *)stream
{
  if (!_stream) {
    _stream = [NSOutputStream outputStreamToFileAtPath: FileStorePath append:YES];
  }
  return _stream;
}


- (NSURLSessionDataTask *)task
{
  if (!_task) {
    NSInteger totalLength = [[NSDictionary dictionaryWithContentsOfFile: TotalLengthPlist][ Filename] integerValue];

    if (totalLength && DownloadLength == totalLength) {
      NSLog(@"###### The file has been downloaded ");
      return nil;
    }

    //  Create a request 
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString: self.downLoadUrl]];

    //  Set request header 
    // Range : bytes=xxx-xxx , starting with the length of the file already downloaded and ending with the total length of the file 
    NSString *range = [NSString stringWithFormat:@"bytes=%zd-", DownloadLength];
    [request setValue:range forHTTPHeaderField:@"Range"];

    //  create 1 a Data task 
    _task = [self.session dataTaskWithRequest:request];
  }
  return _task;
}

#pragma mark - <NSURLSessionDataDelegate>
/**
 * 1. Received response 
 */
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSHTTPURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler
{
  //  Open the stream 
  [self.stream open];

  /*
    ( Content-Length Field returns the size of the file to be downloaded by the server for each client request. 
    For example, the first time a client requests to download a file A , the size of 1000byte , then the first 1 Returned by the secondary server Content-Length = 1000 . 
    Client downloads to 500byte , interrupted abruptly and requested again range for   " bytes=500- ", then the server returns at this time Content-Length for 500
    So for multiple downloads of a single file (breakpoint continuation), the total size of the file must be returned to the server content-length Plus the size of the downloaded file stored locally 
   */
  self.totalLength = [response.allHeaderFields[@"Content-Length"] integerValue] + DownloadLength;

  //  Store the size of the downloaded file in plist file 
  NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithContentsOfFile: TotalLengthPlist];
  if (dict == nil) dict = [NSMutableDictionary dictionary];
  dict[ Filename] = @(self.totalLength);
  [dict writeToFile: TotalLengthPlist atomically:YES];

  //  Receive this request, allowing the server to receive data 
  completionHandler(NSURLSessionResponseAllow);
}

/**
 * 2. Received data from the server (this method may be called) N Times) 
 */
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
  //  Write data 
  [self.stream write:data.bytes maxLength:data.length];

  float progress = 1.0 * DownloadLength / self.totalLength;
  if (self.progressBlock) {
    self.progressBlock(progress);
  }
  //  Download progress 
}

/**
 * 3. Request completed (successful \ Failure) 
 */
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
  if (error) {
    if (self.failedBlock) {
      self.failedBlock(error);
    }
    self.stream = nil;
    self.task = nil;

  }else{
    if (self.successBlock) {
      self.successBlock(FileStorePath);
    }
    //  Close the stream 
    [self.stream close];
    self.stream = nil;
    //  Remove the task 
    self.task = nil;
  }
}

@end

How to call


@interface ViewController ()
@end

@implementation ViewController
/**
 *  Start the download 
 */
- (IBAction)start:(id)sender {
  //  Start the task 
  NSString * downLoadUrl = @"http://audio.xmcdn.com/group11/M01/93/AF/wKgDa1dzzJLBL0gCAPUzeJqK84Y539.m4a";

  [[DownLoadManager sharedInstance]downLoadWithURL:downLoadUrl progress:^(float progress) {
    NSLog(@"###%f",progress);

  } success:^(NSString *fileStorePath) {
    NSLog(@"###%@",fileStorePath);

  } faile:^(NSError *error) {
    NSLog(@"###%@",error.userInfo[NSLocalizedDescriptionKey]);
  }];
}
/**
 *  Pause to download 
 */
- (IBAction)pause:(id)sender {
  [[DownLoadManager sharedInstance]stopTask];
}

@end

conclusion

Here can only achieve a single task download, you can think of their own way, see how to achieve multi-task download, and the implementation of breakpoint continuation function. And in order to make it easier to operate, it is recommended to use database storage instead of storing information. That's all for this article, and I hope it will be helpful for you to learn IOS development.


Related articles: