美文网首页iOS开发精进iOS开发
iOS开发之Socket实现HTTPS GET请求通过Body传

iOS开发之Socket实现HTTPS GET请求通过Body传

作者: 逐水而上 | 来源:发表于2016-11-25 14:39 被阅读2604次
    这篇文章主要介绍以下几个技术点:
    • 使用CocoaAsyncSocket进行socket连接
    • - (void)startTLS:(NSDictionary *)tlsSettings这个字典内容相关设置,(https的设置)
    • HTTP请求头和请求体的自定义设置(其实就是拼接的一长串带指定格式的字符串转换成data)
    • 对网络请求参数的暂存,应对多条网络请求同时发生的情况
    具体实现:

    Cocoa框架里,无论是用OS层基于 C 的BSD socket还是用对BSD socket进行了轻量级的封装的CFNetwork,对于我这种C语言不及格的同学,那都是极其痛苦的体验,因此我们就用CocoaAsyncSocket来进行socket连接,完全OC风格,非常愉快。

    • 你需要用CocoaPods导入这个库:
    source 'https://github.com/CocoaPods/Specs.git'
    source 'https://github.com/Artsy/Specs.git'
    
    platform :ios, ‘9.0’
    
    target ‘你的项目名称’ do
    
    pod 'CocoaAsyncSocket'
    
    end
    
    • 而后我们新建一个DCSocketManager类来管理这个类的一些代理方法,并生成一个本类的单例,单例就不再写了(自己写吧网上很多):

    .h文件里没有什么内容只是暴露了一个供外界调用的请求接口,后面介绍,主要是.m文件里的扩展属性:

    @interface DCSocketManager()  <GCDAsyncSocketDelegate>
    {
        NSString       *_serverHost;//IP或者域名
        int             _serverPort;//端口,https一般是443
        GCDAsyncSocket *_asyncSocket;//一个全局的对象
    }
    @property (nonatomic, strong) NSMutableData     *sendData;//最终拼接好的需要发送出去的数据
    @property (nonatomic, copy)   NSString          *uriString;//具体请求哪个接口,比如https://xxx.xxxxx.com/verificationCode里的verificationCode
    @property (nonatomic, strong) NSDictionary      *paramters;//Body里面需要传递的参数
    @property (nonatomic, copy)   CompletionHandler  completeHandler;//收到返回数据后的回调Block
    @property (nonatomic, strong) NSMutableArray *dcNetArr;//网络请求参数的暂存数组,后面会用到
    @end
    

    GCDAsyncSocketDelegate代理的实现:

    @implementation DCSocketManager
    
    Singleton_Implementation(DCSocketManager)//单例
    
    - (instancetype)init {//对socket进行初始化
        if (self = [super init]) {
            _serverHost = @"xxx.xxxxxx.com";
            _serverPort = 443;
            _asyncSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue() socketQueue:nil];
            _dcNetArr = [NSMutableArray arrayWithCapacity:20];
        }
        return self;
    }
    
    #pragma mark GCDAsyncSocketDelegate method
    
    - (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err
    {//断开连接时会调用
        NSLog(@"didDisconnect...");
        if (self.dcNetArr.count > 0) {
            [_asyncSocket connectToHost:_serverHost onPort:_serverPort error:nil];
        }
    }
    
    - (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port
    {//连接上服务器时会调用
        [self doTLSConnect:sock];//连接上服务器就要进行tls认证,后面介绍,如果只是http连接就不需要这句
       NSLog(@"didConnectToHost: %@, port: %d", host, port);
        if (self.dcNetArr.count > 0) {
            DCNetCache *net = [self.dcNetArr firstObject];
            self.uriString = net.uri;
            self.paramters = net.params;
            self.completeHandler = net.completeHandler;
            [self.dcNetArr removeObjectAtIndex:0];
        }
        [sock writeData:self.sendData withTimeout:-1 tag:0];//往服务器传递请求数据,之后会介绍self.sendData的拼接
        [sock readDataWithTimeout:-1 tag:0];//马上读取一下
    }
    
    - (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag
    {//读取到返回数据时会调用
        NSLog(@"didReadData length: %lu, tag: %ld", (unsigned long)data.length, tag);
        if (nil != self.completeHandler) {//如果请求成功,读取到服务器返回的data数据一般是一串字符串,需要根据返回数据格式做相应处理解析出来
            NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
            //NSLog(@"%@", string);
            NSRange start = [string rangeOfString:@"{"];
            NSRange end = [string rangeOfString:@"}\r\n"];
            NSString *sub;
            if (end.location != NSNotFound && start.location != NSNotFound) {//如果返回的数据中不包含以上符号,会崩溃
                sub = [string substringWithRange:NSMakeRange(start.location, end.location-start.location+1)];//这就是服务器返回的body体里的数据
                NSData *subData = [sub dataUsingEncoding:NSUTF8StringEncoding];;
                NSDictionary *subDic = [NSJSONSerialization JSONObjectWithData:subData options:0 error:nil];
                self.completeHandler(subDic);
            }
        }
        [sock readDataWithTimeout:-1 tag:0];
    }
    
    - (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag
    {//成功发送数据时会调用
        NSLog(@"didWriteDataWithTag: %ld", tag);
        [sock readDataWithTimeout:-1 tag:tag];
    }
    
    - (void)socketDidSecure:(GCDAsyncSocket *)sock 
    {//https安全认证成功时会调用
        NSLog(@"SSL握手成功,安全通信已经建立连接!");
    }
    @end
    

    这里重点说一下sendData这个属性的拼接(很重要,这里的格式决定了你发送的请求数据是否被服务器认可,并给你返回信息,相当于NSURLRequest的作用,其实就是拼接一个http协议):

    - (NSMutableData *)sendData {
        NSMutableData *packetData = [[NSMutableData alloc] init];
        NSData *crlfData = [@"\r\n" dataUsingEncoding:NSUTF8StringEncoding];//回车换行是http协议中每个字段的分隔符
    
        [packetData appendData:[[NSString stringWithFormat:@"GET /%@ HTTP/1.1", self.uriString] dataUsingEncoding:NSUTF8StringEncoding]];//拼接的请求行
        [packetData appendData:crlfData];//每个字段后面都要跟一个回车换行
    
        [packetData appendData:[@"DCVer: 1" dataUsingEncoding:NSUTF8StringEncoding]];//拼接的请求头字段,这个键值对和服务器协商内容,一般不止一个
        [packetData appendData:crlfData];
        [packetData appendData:[@"DCAid: test" dataUsingEncoding:NSUTF8StringEncoding]];//拼接的请求头字段,这个键值对和服务器协商内容,一般不止一个
        [packetData appendData:crlfData];
        
        [packetData appendData:[@"Content-Type: application/json; charset=utf-8" dataUsingEncoding:NSUTF8StringEncoding]];//发送数据的格式
        [packetData appendData:crlfData];
        
        [packetData appendData:[@"User-Agent: GCDAsyncSocket8.0" dataUsingEncoding:NSUTF8StringEncoding]];//代理类型,用来识别用户的操作系统及版本等信息,这里我随便填的,一般情况没什么用
        [packetData appendData:crlfData];
        
        [packetData appendData:[@"Host: xxx.xxxxxx.com" dataUsingEncoding:NSUTF8StringEncoding]];//IP或者域名
        [packetData appendData:crlfData];
        
        NSError *error;
        NSData *bodyData = [NSJSONSerialization dataWithJSONObject:self.paramters
                                                           options:0
                                                             error:&error];
        NSString *bodyString = [[NSString alloc] initWithData:bodyData encoding:NSUTF8StringEncoding];//生成请求体的内容
        [packetData appendData:[[NSString stringWithFormat:@"Content-Length: %ld", bodyString.length] dataUsingEncoding:NSUTF8StringEncoding]];//说明请求体内容的长度
        [packetData appendData:crlfData];
        
        [packetData appendData:[@"Connection:close" dataUsingEncoding:NSUTF8StringEncoding]];
        [packetData appendData:crlfData];
        [packetData appendData:crlfData];//注意这里请求头拼接完成要加两个回车换行
      //以上http头信息就拼接完成,下面继续拼接上body信息
        NSString *encodeBodyStr = [NSString stringWithFormat:@"%@\r\n\r\n", bodyString];//请求体最后也要加上两个回车换行说明数据已经发送完毕
        [packetData appendData:[encodeBodyStr dataUsingEncoding:NSUTF8StringEncoding]];
        return packetData;
    }
    

    以上就是建立HTTP连接收发数据的全部内容,如果不需要支持https的话,这个GET请求已经可以完成,下面介绍进行https连接需要进行的设置(在.m文件里实现):(上面提到的[self doTLSConnect:sock]这个方法)

    - (void)doTLSConnect:(GCDAsyncSocket *)sock {
        //HTTPS
        NSMutableDictionary *sslSettings = [[NSMutableDictionary alloc] init];
        NSData *pkcs12data = [[NSData alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"xxx.xxxxxxx.com" ofType:@"p12"]];//已经支持https的网站会有CA证书,给服务器要一个导出的p12格式证书
        CFDataRef inPKCS12Data = (CFDataRef)CFBridgingRetain(pkcs12data);
        CFStringRef password = CFSTR("xxxxxx");//这里填写上面p12文件的密码
        const void *keys[] = { kSecImportExportPassphrase };
        const void *values[] = { password };
        CFDictionaryRef options = CFDictionaryCreate(NULL, keys, values, 1, NULL, NULL);
        
        CFArrayRef items = CFArrayCreate(NULL, 0, 0, NULL);
        
        OSStatus securityError = SecPKCS12Import(inPKCS12Data, options, &items);
        CFRelease(options);
        CFRelease(password);
        
        if (securityError == errSecSuccess) {
            NSLog(@"Success opening p12 certificate.");
        }
        
        CFDictionaryRef identityDict = CFArrayGetValueAtIndex(items, 0);
        SecIdentityRef myIdent = (SecIdentityRef)CFDictionaryGetValue(identityDict, kSecImportItemIdentity);
        SecIdentityRef  certArray[1] = { myIdent };
        CFArrayRef myCerts = CFArrayCreate(NULL, (void *)certArray, 1, NULL);
        [sslSettings setObject:(id)CFBridgingRelease(myCerts) forKey:(NSString *)kCFStreamSSLCertificates];
        [sslSettings setObject:@"api.pandaworker.com" forKey:(NSString *)kCFStreamSSLPeerName];
        [sock startTLS:sslSettings];//最后调用一下GCDAsyncSocket这个方法进行ssl设置就Ok了
    }
    

    至此发送HTTPS GET请求并且用body传递参数就实现了,是不是很神奇。下面封装一个对外调用的接口(在.h文件中把这个接口暴露出去就行了):

    - (void)getRequestUriName:(NSString *)uri Param:(NSDictionary *)params Complete:(CompletionHandler)handler{
        DCNetCache *net = [[DCNetCache alloc] initWithUri:uri Params:params CompleteHandler:handler];
        [self.dcNetCacheArr addObject:net];
        [_asyncSocket connectToHost:_serverHost onPort:_serverPort error:nil];
    }
    

    ** 其中的DCNetCache类用来暂存网络请求的参数,它是这样子滴:**

    typedef void(^CompletionHandler)(NSDictionary *response);
    @interface DCNetCache : NSObject
    
    @property (nonatomic, copy) NSString *uri;
    @property (nonatomic, strong) NSDictionary *params;
    @property (nonatomic, copy) CompletionHandler completeHandler;
    
    - (instancetype)initWithUri:(NSString *)uri Params:(NSDictionary *)params CompleteHandler:(CompletionHandler)handler;
    
    @end
    
    @implementation DCNetCache
    
    - (instancetype)initWithUri:(NSString *)uri Params:(NSDictionary *)params CompleteHandler:(CompletionHandler)handler{
        self = [super init];
        if (self) {
            _uri = uri;
            _params = params;
            _completeHandler = handler;
        }
        return self;
    }
    @end
    

    这样子就大功告成了,注意把上面的host换成自己的,这里还有许多不完善的地方,我只是实现了简单的GET请求并暂存请求参数,至于你需要其他的功能自己加上就是了。

    附一篇讲GCDAsyncSocket的干货文章,非常值得一读

    相关文章

      网友评论

      • 卓敦:socket不是用来长连接吗,那个connection属性怎么设置为close 了
        逐水而上:socket只是通信的基本手段,并没有规定必须是长连接或者短连接。如果服务器支持keepalive那你可以设置成keepalive,这样更好。具体的自行google吧:smile:
      • 5e99e81683aa:现在苹果更新 [sslSettings setObject:(id)CFBridgingRelease(myCerts) forKey:(NSString *)kCFStreamSSLCertificates];
        [sslSettings setObject:@"api.pandaworker.com" forKey:(NSString *)kCFStreamSSLPeerName];
        这两个已经不能用了,请问一下怎么解决呢
      • fonglaaam:苹果上架说要https 那我们用socket的话不是http请求啊 该怎么操作?
        逐水而上:首先现在还没有强制https,其次我这里写的socket方案已经实现了https传输,应该是可以的……
      • iOS_Rainbow:
        CFArrayRef items = CFArrayCreate(NULL, 0, 0, NULL);

        OSStatus securityError = SecPKCS12Import(inPKCS12Data, options, &items);
        CFRelease(options);
        CFRelease(password);

        if (securityError == errSecSuccess) {
        NSLog(@"Success opening p12 certificate.");
        }

        CFDictionaryRef identityDict = CFArrayGetValueAtIndex(items, 0); items数组为空,这句话肯定会奔溃啊,我现在也在搞 socket ssl ,大神你这边是怎么解决的
      • 不会唱歌的程序员不是好厨师:这明明是socket了 跟http get没关系了
        不会唱歌的程序员不是好厨师:那也可以说是http post
        逐水而上:嗯,也不能说完全没关系,因为是按照http协议来拼接传输的数据的……

      本文标题:iOS开发之Socket实现HTTPS GET请求通过Body传

      本文链接:https://www.haomeiwen.com/subject/regipttx.html