iOS

iOS 简单模拟服务器如何解析客户端传来的表单数据及图片格式数据并本地保存

0f88cb69504844109507f191c0a5831b~tplv-k3u1fbpfcp-zoom-crop-mark:1304:1304:1304:734.awebp?


废话开篇:在日常开发中经常会有上传表单及图片到服务器场景,这里有两种实现方式:一、单独封装一个图片文件格式存储代码,服务器对 Response 返回值里面返回服务器图片路径,再通过其他接口绑定服务器图片路径;二、表单及图片文件直接提交。那么,其实说是两种方式,其实归根到底就是一种:数据传输与接收。那么,下面就在 OC 上简单模拟服务器如何解析客户端传来的表单数据及图片格式数据

以前文章地址:

# iOS 简单模拟 https 证书信任逻辑

# iOS 基于 CocoaHTTPServer 搭建手机内部服务器,实现 http 及 https 访问、传输数据

基于上述文章继续进行本次的 模拟服务器如何解析客户端传来的表单数据及图片格式数据

效果如下:

屏幕录制2021-11-18 下午4.17.38.gif

前言说明:

这里简单说一下 AFNetwork 下是如何同时进行数据参数提交及文件上传的。这里只是简单的说一下思路:

先上一段简单的 AF 请求代码

    AFHTTPSessionManager * m = [[AFHTTPSessionManager alloc] initWithBaseURL:[NSURL URLWithString:@"https://10.10.60.20"]];

NSDictionary * dic = @{@"title":@"中国万岁",@"name":@"中国人"};

    [m POST:@"https://10.10.60.20:12345/doPost" parameters:dic headers:@{} constructingBodyWithBlock:^(**id**<AFMultipartFormData>  _Nonnull formData) {

        NSDate *date = [NSDate dateWithTimeIntervalSinceNow:0]; // 获取当前时间0秒后的时间

        NSTimeInterval time = [date timeIntervalSince1970]*1000;// *1000 是精确到毫秒(13位),不乘就是精确到秒(10位)

        NSString *timeString = [NSString stringWithFormat:@"iOS%.0f", time];

        UIImage * image = [UIImage imageNamed:@"sea"];

        NSData *data = UIImageJPEGRepresentation(image, 0.5f);

        [formData appendPartWithFileData:data name:@"file" fileName:[NSString stringWithFormat:@"%@.jpg",timeString] mimeType:@"image/jpg"];

        } progress:^(NSProgress * _Nonnull uploadProgress) {          

        } success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {

        } failure:^(NSURLSessionDataTask * **_Nullable** task, NSError * _Nonnull error) {   

        }
     ];

1、网络请求参数的传入

这里代码无需过多解释,dic 就是要传输的请求参数,那么,在这个参数完成之后,其实 AFNetworking 就对参数进行了存储,并且在后面的图片上传的时候用拼接的 NSData 的方式进行数据拼接。

2、图片数据获取及 NSData 拼接

AF 调用下面的方法进行了请求数据的拼接。

[formData appendPartWithFileData:data name:@"file" fileName:[NSString stringWithFormat:@"%@.jpg",timeString] mimeType:@"image/jpg"];

3、基于第二步骤,创建多个数据读取对象,通过 Stream 进行 NSData 的依次读取,因为 AF 下的 POST 请求会跟一个 Stream 进行绑定

[self.request setHTTPBodyStream:self.bodyStream];

那么,在开启的发送请求前,AF 又重写了 Stream 下

- (NSInteger)read:(uint8_t *)buffer maxLength:(NSUInteger)length

方法。进而可以在 Stream 读取的过程中对多个文件 data 进行拼接,最终将整个数据进行一次传输。

4、注意事项:(1)AF 会在 header 里面进行数据总长度的标定,这样服务器在最先拿到 header 时便可以知晓此次传输的数据总长度。(2)AF 会随机生成一个 boundary 也放到 header 里面,这个参数的目的就是将请求中不通的 参数文件进行边界划分,这样,服务器在解析的时候就知道了哪些 data 是一个完整的数据。当然,AF 也会标定一下传输类型在 header 里,比如:Content-Type

好了,上述其实只是一个铺垫,来看一下最终如何总 data 里解析出请求参数及图片文件

步骤一、基于 CocoaHTTPServer 搭建完的本地 OC 服务器进行数据解析

对于如何搭建的请参考上面的文章链接

这里要处理的就是下面的这个方法,客户端传过来的数据都会在这个方法里执行,因为一个系统的 Stream 一次性读取最大数是有限制的,所以,对于大文件上传的过程,此方法会走多次。

- (void)processBodyData:(NSData *)postDataChunk;

思路:因为服务器收到所有的 data 里完整的参数数据都是用换行符来分割的,那么通过对 "\r\n" 换行符进行切割,那么,两个换行符之间的数据就是一个完整的参数。

- (void)parseData:(NSData *)postDataChunk
{
//这里记录图片文件 data 在数据接收总 data 里的初始位置索引
    int fileDataStartIndex = 0;
    //换行符\r\n
    UInt16 separatorBytes = 0X0A0D;
    NSData * separatorData = [NSData dataWithBytes:&separatorBytes length:2];
    int l = (int)[separatorData length];
//遍历接收的数据,找到所有以 0A0D 分割的完整 data 数据
    for (int i = 0; i < [postDataChunk length] - l; i++) {
//以换行符长度为单位依次排查、寻找
        NSRange searchRange = {i,l};
        //是换行符
        if ([[postDataChunk subdataWithRange:searchRange] isEqualToData:separatorData]) {
            
            //获取换行符之间的data的位置
            NSRange newDataRange = {self.dataStartIndex,i - self.dataStartIndex};
            self.dataStartIndex = i + l;
//这里先进性请求参数的筛选,文件data保存位置偏后,那么,一开始就需要 self.paramReceiveComplete 标识来标定是否排查到文件 data 了
            if (self.paramReceiveComplete) {
                fileDataStartIndex = i + l;
                continue;
            }

            //跳过换行符
            i += (l-1);
//获取换行符之间的完整数据格式
            NSData * newData = [postDataChunk subdataWithRange:newDataRange];
//判断是否为空
            if ([newData length]) {
//获取文本信息
                NSString *content = [[NSString alloc] initWithData:newData encoding:NSUTF8StringEncoding];
//替换所有的换行特殊字符
                content = [content stringByReplacingOccurrencesOfString:@"\r\n" withString:@""];
//这里注意的是边界信息 Boundary ,也就是 AF 给钉里面的数据不解析
                if (content.length && ![content containsString:@"--Boundary"]) {
//如果解析到文件,那么 content 里会包含 name="file" 的标识,用此标识进行数据格式的判断
                    if ([content containsString:@"name=\"file\""]){
//读到文件了
                        self.currentParserType = @"file";
                    } else {
//请求参数
                        self.currentParserType = @"text/plain";
                    }

                    //表单数据解析
                    if ([self.currentParserType containsString:@"text/plain"]){
//content 里面包含 form-data,说明是数据参数说明,里面会包含 key 值
                        if ([content containsString:@"form-data"]) {
                            NSString * key = [content componentsSeparatedByString:@"name="].lastObject;
                            key = [key stringByReplacingOccurrencesOfString:@"\"" withString:@""];
//这里临时保存了key值,在后面解析到 value 的时候进行数据绑定
                            self.currentParamKey = key;
                        } else {
//解析到了 value 用 self.currentParamKey 进行绑定
                            if (self.currentParamKey && content) {
                                [self.receiveParamDic setValue:content forKey:self.currentParamKey];
                            }
                        }
                    } else {
                        //开始文件处理,标定一下,因为由于文件大小的影响,此方法会走多次,那么,在一开始标定后,下一次再进来就直接进行文件数据的拼接
                        self.paramReceiveComplete = YES;
                    }
                }
            }
        }
    }

//文件的写入(其实这里不是很严谨,因为请求参数较小的原因,所以,即便是第一次执行此方法,里面也会有文件 data 开始读取的情况)
    NSRange fileDataRange = {fileDataStartIndex,postDataChunk.length - fileDataStartIndex};
    NSData * fileData = [postDataChunk subdataWithRange:fileDataRange];
    [self.outputStream write:[fileData bytes] maxLength:fileData.length];

}

步骤二、数据写入沙盒

声明一个 NSOutputStream 对象

@property (nonatomic,strong) NSOutputStream * outputStream;

CocoaHTTPServer -> HTTPConnection 类是不进行常规化的 init 的,所以,初始化 outputStream 这里用懒加载的形式。

- (NSOutputStream *)outputStream

{

    if (!_outputStream) {
        NSString * cachePath = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject;
        NSString * filePath = [cachePath stringByAppendingPathComponent:@"wsl.png"];
        NSLog(@"filePath = %@",filePath);
        _outputStream = [[NSOutputStream alloc] initToFileAtPath:filePath append:**YES**];
        [_outputStream open];
    }
    return _outputStream;
}

进行文件写入沙盒操作:

    NSRange fileDataRange = {fileDataStartIndex,postDataChunk.length - fileDataStartIndex};
    NSData * fileData = [postDataChunk subdataWithRange:fileDataRange];
    [self.outputStream write:[fileData bytes] maxLength:fileData.length];

在处理完数据后关闭流

- (NSObject<HTTPResponse> *)httpResponseForMethod:(NSString *)method URI:(NSString *)path
{
    [self.outputStream close];
    self.outputStream = nil;
}

步骤三、查看运行结果

先看是否获取了请求的参数:

image.png

在看图片是否保存完成,通过打印模拟器的沙盒路径,直接 前往文件夹 即可找到沙盒文件

image.png

可以看到,这里保存图片也成功了。

image.png

这里说明一下:

遵循 MultipartFormDataParserDelegate 协议也可以直接获取文件的 data ,直接去读,再去存即可。但是它没有暴露给外界数据请求的 key 而只有 value,但是如果仅作为文件的传输还是很方便的。

如下:

遵循代理协议

image.png

声明 MultipartFormDataParser 对象

image.png

MultipartFormDataParser 对象进行数据解析

image.png

进行文件数据解析代理执行

image.png

其实 CocoaHTTPServer 封装的解析工具类实现原理亦是如此。

好了,简单模拟服务器如何解析客户端传来的表单数据及图片格式数据并本地保存 功能就实现完了,代码拙劣,大神勿笑。

0 个评论

要回复文章请先登录注册