從HTTP原理到HTTP網(wǎng)絡(luò)組件封裝

HTTP所在的協(xié)議層

TCP:IP參考模型.jpg

HTTP工作過程

一次HTTP操作稱為一個事務(wù)蒜哀,其工作整個過程如下:

地址解析

如用客戶端瀏覽器請求這個頁面:http://localhost:8080/info宛官,從中分解出協(xié)議名巩踏、主機名纳击、端口、對象路徑等部分炊苫,對于我們的這個地址,解析得到的結(jié)果如下:

協(xié)議名:http 主機名:localhost 端口:8080 對象路徑:/info

在這一步柔昼,需要域名系統(tǒng)DNS解析域名localhost,得主機的IP地址炎辨。

封裝HTTP請求數(shù)據(jù)包

Header

常見的媒體格式類型如下:

    text/html : HTML格式
    text/plain :純文本格式      
    text/xml :  XML格式
    image/gif :gif圖片格式    
    image/jpeg :jpg圖片格式 
    image/png:png圖片格式

以application開頭的媒體格式類型:

   application/xhtml+xml :XHTML格式
   application/xml     : XML數(shù)據(jù)格式
   application/atom+xml  :Atom XML聚合格式    
   application/json    : JSON數(shù)據(jù)格式
   application/pdf       :pdf格式  
   application/msword  : Word文檔格式
   application/octet-stream : 二進制流數(shù)據(jù)(如常見的文件下載)
   application/x-www-form-urlencoded : form表單數(shù)據(jù)被編碼為key/value格式(default表單提交)
   multipart/form-data : 需要在表單中進行文件上傳時捕透,就需要使用該格式

請求格式

  • Get
GET /plaintext?name=Shawn HTTP/1.1
Host: localhost:8080
User-Agent: ZHHTTP 0.1.0 rv:1 (iPhone; iOS 10.3.1; en_US)
Connection: keep-alive
Accept-Encoding: gzip
  • Post表單
POST /postName HTTP/1.1
Host: localhost:8080
Content-Type: application/x-www-form-urlencoded; charset=utf-8
User-Agent: ZHHTTP 0.1.0 rv:1 (iPhone; iOS 10.3.1; en_US)
Content-Length: 16
Accept-Encoding: gzip
Connection: close

name=Shawn&sex=1
  • Post上傳
    上傳時,傳了一個參數(shù)key:name碴萧, value:Shawn乙嘀,一個文件upload0.txt,一個data數(shù)據(jù)塊破喻。
POST /upload HTTP/1.1
Host: localhost:8080
Content-Type: multipart/form-data; charset=utf-8; boundary=0xKhTmLbOuNdArY-089BC7C0-F1B0-44F6-B59C-6C86BDFED87A
User-Agent: ZHHTTP 0.1.0 rv:1 (iPhone; iOS 10.3.1; en_US)
Content-Length: 500
Accept-Encoding: gzip
Connection: close

--0xKhTmLbOuNdArY-089BC7C0-F1B0-44F6-B59C-6C86BDFED87A
Content-Disposition: form-data; name="name"

Shawn
--0xKhTmLbOuNdArY-089BC7C0-F1B0-44F6-B59C-6C86BDFED87A
Content-Disposition: form-data; name="data0"; filename="upload0.txt"
Content-Type: text/plain

Shawn0

--0xKhTmLbOuNdArY-089BC7C0-F1B0-44F6-B59C-6C86BDFED87A
Content-Disposition: form-data; name="data1"; filename="file"
Content-Type: application/octet-stream

Shawn1
--0xKhTmLbOuNdArY-089BC7C0-F1B0-44F6-B59C-6C86BDFED87A--

把以上部分結(jié)合本機自己的信息虎谢,封裝成一個HTTP請求數(shù)據(jù)包

封裝成TCP包,建立TCP連接(TCP的三次握手)

在HTTP工作開始之前曹质,客戶機(Web瀏覽器)首先要通過網(wǎng)絡(luò)與服務(wù)器建立連接婴噩,該連接是通過TCP來完成的,該協(xié)議與IP協(xié)議共同構(gòu)建Internet羽德,即著名的TCP/IP協(xié)議族几莽,因此Internet又被稱作是TCP/IP網(wǎng)絡(luò)。HTTP是比TCP更高層次的應(yīng)用層協(xié)議玩般,根據(jù)規(guī)則银觅,只有低層協(xié)議建立之后才能礼饱,才能進行更層協(xié)議的連接坏为,因此,首先要建立TCP連接镊绪,一般TCP連接的端口號是80匀伏。這里是8080端口

客戶機發(fā)送請求命令

建立連接后,客戶機發(fā)送一個請求給服務(wù)器蝴韭,請求方式的格式為:統(tǒng)一資源標(biāo)識符(URL)够颠、協(xié)議版本號,后邊是MIME信息包括請求修飾符榄鉴、客戶機信息和可內(nèi)容履磨。

服務(wù)器響應(yīng)

服務(wù)器接到請求后蛉抓,給予相應(yīng)的響應(yīng)信息,其格式為一個狀態(tài)行剃诅,包括信息的協(xié)議版本號巷送、一個成功或錯誤的代碼,后邊是MIME信息包括服務(wù)器信息矛辕、實體信息和可能的內(nèi)容笑跛。
實體消息是服務(wù)器向瀏覽器發(fā)送頭信息后,它會發(fā)送一個空白行來表示頭信息的發(fā)送到此為結(jié)束聊品,接著飞蹂,它就以Content-Type應(yīng)答頭信息所描述的格式發(fā)送用戶所請求的實際數(shù)據(jù)

服務(wù)器關(guān)閉TCP連接

一般情況下,一旦Web服務(wù)器向瀏覽器發(fā)送了請求數(shù)據(jù)翻屈,它就要關(guān)閉TCP連接陈哑,然后如果瀏覽器或者服務(wù)器在其頭信息加入了這行代碼
Connection:keep-alive
TCP連接在發(fā)送后將仍然保持打開狀態(tài),于是伸眶,瀏覽器可以繼續(xù)通過相同的連接發(fā)送請求芥颈。保持連接節(jié)省了為每個請求建立新連接所需的時間,還節(jié)約了網(wǎng)絡(luò)帶寬赚抡。

HTTP組件封裝

封裝的好處

  • 使用者只需要了解如何通過類的接口使用類爬坑,而不用關(guān)心類的內(nèi)部數(shù)據(jù)結(jié)構(gòu)和數(shù)據(jù)組織方法。
  • 高內(nèi)聚涂臣,低耦合一直是我們所追求的盾计,用好封裝恰恰可以減少耦合
  • 只要對外接口不改變,可以任意修改內(nèi)部實現(xiàn)赁遗,這個可以很好的應(yīng)對變化
  • 類具有了簡潔清晰的對外接口署辉,降低了使用者的學(xué)習(xí)過程

封裝的過程

大致分為以下三步:設(shè)計接口、填充實現(xiàn)岩四、單元測試哭尝。

設(shè)計接口

這一步很重要,接口的好壞剖煌,直接決定了整個組件的好壞材鹦。
這個接口,需要有以下內(nèi)容:

  • 發(fā)起HTTP請求耕姊,具體是什么請求桶唐,需要自定義這個請求;
  • 需要一個請求完成回來的回調(diào);
  • 回調(diào)中需要帶一些內(nèi)容茉兰。
    這樣在ZHHTTPClient類尤泽,只有一個類方法:
+ (void)sendRequest:(ZHHTTPRequest *)request
           complete:(ZHCompleteHandler)completeHandler
            failure:(ZHFailureHandler)failureHandler;

request是一個繼承自NSObject的類,需要接收外部配置參數(shù):

@interface ZHHTTPRequest : NSObject
/**
 請求的url,若為GET請求,直接在url后面拼接參數(shù)坯约。
 */
@property (nonatomic, copy) NSURL *url;
@property (nonatomic) NSTimeInterval timeoutInterval;
/**
 http請求頭
 */
@property (nonatomic, strong) NSDictionary <NSString *, NSString *> *HTTPRequestHeaders;
/**
 http請求參數(shù),GET請求會拼接到url后面熊咽,POST請求會拼接到body里面。若為GET請求闹丐,不要在此設(shè)置值网棍。
 */
@property (nonatomic, strong) NSDictionary *postParams;
/**
 上傳文件需要的數(shù)據(jù),不需要設(shè)置此項妇智。
 */
@property (nonatomic, strong) NSArray<ZHHTTPUploadComponent *> *uploadComponents;
/**
 defaut is NO,不對證書做校驗
 */
@property (nonatomic) BOOL validatesSecureCertificate;
/*!
 @abstract Sets the HTTP request method of the receiver. POST or GET,default is GET.
 */
@property (nonatomic) ZHHTTPMethod HTTPMethod;

@property (nonatomic, strong) NSData *HTTPBody;
/**
 default is NO,不做同步請求
 */
@property (nonatomic) BOOL shouldSynchronous;
/**
 下載文件存儲的目標(biāo)路徑滥玷,要精確到文件名,在設(shè)定之前巍棱,需要在外部判定文件是否存在惑畴,是否需要刪除。
 */
@property (nonatomic, copy) NSString *downloadDestinationPath;
/**
 下載文件存儲的臨時路徑,如果下載時不設(shè)定此項航徙,會有默認的臨時路徑如贷。
 */
@property (nonatomic, copy) NSString *downloadTemporaryPath;

- (instancetype)initWithURL:(NSURL *)url;
+ (instancetype)requestWithURL:(NSURL *)url;
@end

其中ZHHTTPUploadComponent, 是為上傳準(zhǔn)備的一個配置類到踏,會接收上傳文件的名稱杠袱、路徑、類型等:

@interface ZHHTTPUploadComponent : NSObject

/**
 dataKey: 每一個dataKey對應(yīng)于一個filePath或者data數(shù)據(jù),在同一次傳輸中要保證dataKey唯一窝稿,不能為空楣富;
 fileName: 指定上傳文件的名字,可以為空伴榔,為空時取原文件名字纹蝴;
 filePath: 上傳的文件路徑
 data: 上傳的data數(shù)據(jù)
 */
@property (nonatomic, copy, readonly) NSString *dataKey;
@property (nonatomic, copy, readonly) NSString *filePath;
@property (nonatomic, copy, readonly) NSString *fileName;
@property (nonatomic, copy, readonly) NSString *mimeType;
@property (nonatomic, strong, readonly) NSData *data;

/**
   Appends the HTTP header `Content-Disposition: file; filename=#{filename}; name=#{name}"` and `Content-Type: #{mimeType}`, followed by the data from the input stream and the multipart form boundary.
 @param dataKey 上傳的data所需要的key,不能為空
 @param filePath 上傳的文件路徑踪少,不能為空
  The fileName and MIME type for this data in the form will be automatically generated, using the last path component of the `filePath` and system associated MIME type for the `filePath` extension, respectively.
 @return A newly-created and autoreleased ZHHTTPUploadComponent instance.
 */
- (instancetype)initWithDataKey:(NSString *)dataKey filePath:(NSString *)filePath;
@end

這一個初始化方法是不夠的塘安,還有另外5個初始化方法,比如:

/**
  Appends the HTTP header `Content-Disposition: file; filename=#{filename}; name=#{name}"` and `Content-Type: #{mimeType}`, followed by the data from the input stream and the multipart form boundary.

 @param dataKey 上傳的data所需要的key援奢,不能為空
 @param filePath 上傳的文件路徑兼犯,不能為空
 @param fileName 指定上傳文件的名字,不能為空
 @param mimeType The MIME type of the specified data. (For example, the MIME type for a JPEG image is image/jpeg.) For a list of valid MIME types, see http://www.iana.org/assignments/media-types/. This parameter must not be `nil`.
 @return A newly-created and autoreleased ZHHTTPUploadComponent instance.
 */
- (instancetype)initWithDataKey:(NSString *)dataKey
                        filePath:(NSString *)filePath
                       fileName:(NSString *)fileName
                       mimeType:(NSString *)mimeType;

再加上response集漾,回調(diào)block切黔,HTTPMethod enum:

@interface ZHHTTPResponse : NSObject
@property (nonatomic) NSInteger code;
@property (nonatomic, strong) NSData *data;
@property (nonatomic, copy) NSString *responseString;
@end

typedef void(^ZHCompleteHandler)(ZHHTTPResponse *response);
typedef void(^ZHFailureHandler)(NSError *error);
typedef NS_ENUM(NSInteger, ZHHTTPMethod) {
    POST,
    GET
};

這些是全部的ZHHTTPClient Header中的內(nèi)容。

填充實現(xiàn)

對主接口的實現(xiàn)如下:

+ (void)sendRequest:(ZHHTTPRequest *)request
           complete:(ZHCompleteHandler)completeHandler
            failure:(ZHFailureHandler)failureHandler {
    if (request.uploadComponents.count > 0 || request.postParams || request.HTTPBody) {
        request.HTTPMethod = POST;
    }
    if (request.HTTPMethod == GET) {
        [self getMethodRequest:request complete:completeHandler failure:failureHandler];
    } else if (request.HTTPMethod == POST) {
        [self postMethodRequest:request complete:completeHandler failure:failureHandler];
    }
}

先是根據(jù)設(shè)置的參數(shù)帆竹,對請求方法做了一個校準(zhǔn)绕娘,然后再是根據(jù)是POST還是GET方法脓规,再相應(yīng)的私有方法栽连。到現(xiàn)在為止還沒有看出來,這個ZHHTTPClient內(nèi)部是封裝的哪種網(wǎng)絡(luò)庫,以后所有的更換網(wǎng)絡(luò)庫這種操作秒紧,是不需要動接口和這個實現(xiàn)函數(shù)的绢陌。
在具體的實現(xiàn)中,可以選擇自己需要的網(wǎng)絡(luò)框架熔恢,因為歷史原因脐湾,追求穩(wěn)定性,這里選擇了ASI叙淌,代碼如下:

+ (void)getMethodRequest:(ZHHTTPRequest *)request
                complete:(ZHCompleteHandler)completeHandler
                 failure:(ZHFailureHandler)failureHandler {
    ASIHTTPRequest *asiRequest = [ASIHTTPRequest requestWithURL:request.url];
    [asiRequest setRequestMethod:@"GET"];
    [self configureASIRequest:asiRequest ZHHTTPRequest:request complete:completeHandler failure:failureHandler];
    if (!request.shouldSynchronous) {
        [asiRequest startAsynchronous];
    } else {
        [asiRequest startSynchronous];
    }
}

+ (void)postMethodRequest:(ZHHTTPRequest *)request
                 complete:(ZHCompleteHandler)completeHandler
                  failure:(ZHFailureHandler)failureHandler {
    ASIFormDataRequest *asiRequest = [ASIFormDataRequest requestWithURL:request.url];
    [asiRequest setRequestMethod:@"POST"];
    if (request.postParams) {
        for (id key in request.postParams) {
            [asiRequest setPostValue:request.postParams[key] forKey:key];
        }
    } else {
        if (request.HTTPBody) {
            [asiRequest setPostBody:[NSMutableData dataWithData:request.HTTPBody]];
        }
    }
    if (request.uploadComponents) {
        for (NSInteger i = 0; i < request.uploadComponents.count; i++) {
            ZHHTTPUploadComponent *component = request.uploadComponents[i];
            if (component.filePath) {
                [asiRequest addFile:component.filePath withFileName:component.fileName andContentType:component.mimeType forKey:component.dataKey];
            } else if (component.data) {
                [asiRequest addData:component.data withFileName:component.fileName andContentType:component.mimeType forKey:component.dataKey];
            }
        }
    }
    [self configureASIRequest:asiRequest ZHHTTPRequest:request complete:completeHandler failure:failureHandler];
    if (!request.shouldSynchronous) {
        [asiRequest startAsynchronous];
    } else {
        [asiRequest startSynchronous];
    }
}

+ (void)configureASIRequest:(ASIHTTPRequest *)asiRequest
              ZHHTTPRequest:(ZHHTTPRequest *)request
                   complete:(ZHCompleteHandler)completeHandler
                    failure:(ZHFailureHandler)failureHandler {
    [asiRequest setValidatesSecureCertificate:request.validatesSecureCertificate];
    [asiRequest setTimeOutSeconds:request.timeoutInterval];
    if (request.HTTPRequestHeaders) {
        NSMutableDictionary *dict = [request.HTTPRequestHeaders copy];
        [asiRequest setRequestHeaders:dict];
    }
    if (request.downloadDestinationPath) { //有下載路徑時秤掌,認為是下載
        [asiRequest setDownloadDestinationPath:request.downloadDestinationPath];
        [asiRequest setTemporaryFileDownloadPath:request.downloadTemporaryPath];
    }
    __weak typeof(asiRequest) weakAsiRequest = asiRequest;
    asiRequest.completionBlock = ^{
        __strong typeof(weakAsiRequest) strongAsiRequest = weakAsiRequest;
        ZHHTTPResponse *response = [ZHHTTPResponse new];
        response.code = strongAsiRequest.responseStatusCode;
        response.data = strongAsiRequest.responseData;
        response.responseString = strongAsiRequest.responseString;
        if (completeHandler) {
            completeHandler(response);
        }
    };
    [asiRequest setFailedBlock:^{
        __strong typeof(weakAsiRequest) strongAsiRequest = weakAsiRequest;
        if (failureHandler) {
            failureHandler(strongAsiRequest.error);
        }
    }];
}

ZHHTTPUploadComponent的實現(xiàn)就是對多個初始化方法指向一個全能初始化方法,把傳進來的參數(shù)賦值到實例變量中鹰霍。
ZHHTTPRequest實現(xiàn)在闻鉴,會做默認值處理:

@implementation ZHHTTPRequest
- (instancetype)initWithURL:(NSURL *)url{
    if (self = [super init]) {
        _url = url;
        _timeoutInterval = 10;
        _HTTPMethod = GET;
    }
    return self;
}
+ (instancetype)requestWithURL:(NSURL *)url {
    return [[self alloc] initWithURL:url];
}
@end

最后還有一個取MIME type的方法:

static NSString *const kDefaultMimeType = @"application/octet-stream";
static inline NSString * ZHContentTypeForPathExtension(NSString *extension) {
    NSString *UTI = (__bridge_transfer NSString *)UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)extension, NULL);
    NSString *contentType = (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)UTI, kUTTagClassMIMEType);
    if (!contentType) {
        return kDefaultMimeType;
    } else {
        return contentType;
    }
}

單元測試

static NSString * const author = @"Shawn";

正常POST:

- (void)testNormalPost {
    ZHHTTPRequest *request = [[ZHHTTPRequest alloc] initWithURL:[NSURL URLWithString:@"http://localhost:8080/postName"]];
    request.HTTPMethod = POST;
    request.postParams = @{@"name": author, @"sex": @(1)};
    NSString *expectedResult = author;
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    [ZHHTTPClient sendRequest: request complete:^(ZHHTTPResponse *response) {
        NSString *responseString = [[NSString alloc] initWithData:response.data encoding:NSUTF8StringEncoding];
        NSLog(@"response: %@", responseString);
        XCTAssertTrue([[[NSString alloc] initWithData:response.data encoding:NSUTF8StringEncoding] isEqualToString:expectedResult], @"Strings are not equal %@ != %@", expectedResult, responseString);
        dispatch_semaphore_signal(semaphore);
    } failure:^(NSError *error) {
        NSLog(@"error: %@", error.description);
        dispatch_semaphore_signal(semaphore);
    }];
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}

文件上傳:

- (void)testFileUpload {
    ZHHTTPRequest *request = [[ZHHTTPRequest alloc] initWithURL:[NSURL URLWithString:@"http://localhost:8080/upload"]];
    request.HTTPMethod = POST;
    request.postParams = @{@"name": @"Shawn"};
    NSString *filePath1 = [[NSBundle mainBundle] pathForResource:@"upload0" ofType:@"txt"];
    NSData *data = [@"Shawn1" dataUsingEncoding:NSUTF8StringEncoding];

    ZHHTTPUploadComponent *comp0 = [[ZHHTTPUploadComponent alloc] initWithDataKey:@"data0" filePath:filePath1];
    ZHHTTPUploadComponent *comp1 = [[ZHHTTPUploadComponent alloc] initWithDataKey:@"data1" data:data];

    request.uploadComponents = @[comp0, comp1];
    NSString *expectedResult = @"Shawn0Shawn1";
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    [ZHHTTPClient sendRequest: request complete:^(ZHHTTPResponse *response) {
        NSString *responseString = [[NSString alloc] initWithData:response.data encoding:NSUTF8StringEncoding];
        NSLog(@"response: %@", responseString);
        XCTAssertTrue([[[NSString alloc] initWithData:response.data encoding:NSUTF8StringEncoding] isEqualToString:expectedResult], @"Strings are not equal %@ != %@", expectedResult, responseString);
        dispatch_semaphore_signal(semaphore);
    } failure:^(NSError *error) {
        NSLog(@"error: %@", error.description);
        dispatch_semaphore_signal(semaphore);
    }];
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}

文件下載:

- (void)testDownload {
    ZHHTTPRequest *request = [[ZHHTTPRequest alloc] initWithURL:[NSURL URLWithString:@"http://devthinking.com/wp-content/uploads/2017/07/runtime.jpg"]];
    NSString *cacheFolder = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory,
                                                          NSUserDomainMask,
                                                          YES) lastObject];
    NSString *docFolder = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
                                                               NSUserDomainMask,
                                                               YES) lastObject];
    NSDate *now = [NSDate new];
    NSString *cachePath = [cacheFolder stringByAppendingPathComponent:[NSString stringWithFormat:@"%@", now]];
    NSString *dstPath = [docFolder stringByAppendingPathComponent:@"temp.jpg"];
    NSLog(@"tmp: %@, cachePath: %@", dstPath, cachePath);
    request.downloadDestinationPath = dstPath;
    request.downloadTemporaryPath = cachePath;
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    [ZHHTTPClient sendRequest: request complete:^(ZHHTTPResponse *response) {
        XCTAssertTrue([[NSFileManager defaultManager] fileExistsAtPath:dstPath], @"file is not exist at path: %@", dstPath);
        dispatch_semaphore_signal(semaphore);
    } failure:^(NSError *error) {
        NSLog(@"error: %@", error.description);
        dispatch_semaphore_signal(semaphore);
    }];
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}

直接填充body:

- (void)testBodyDataPost {
    ZHHTTPRequest *request = [[ZHHTTPRequest alloc] initWithURL:[NSURL URLWithString:@"http://localhost:8080/postBodyData"]];
    request.HTTPMethod = POST;
    NSDictionary *dict = @{@"name": author};
    NSData *data = [NSJSONSerialization dataWithJSONObject:dict options:0 error:nil];
    request.HTTPBody = data;
    NSString *expectedResult = author;
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    [ZHHTTPClient sendRequest: request complete:^(ZHHTTPResponse *response) {
        NSString *responseString = [[NSString alloc] initWithData:response.data encoding:NSUTF8StringEncoding];
        NSLog(@"response: %@", responseString);
        XCTAssertTrue([[[NSString alloc] initWithData:response.data encoding:NSUTF8StringEncoding] isEqualToString:expectedResult], @"Strings are not equal %@ != %@", expectedResult, responseString);
        dispatch_semaphore_signal(semaphore);
    } failure:^(NSError *error) {
        NSLog(@"error: %@", error.description);
        dispatch_semaphore_signal(semaphore);
    }];
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}

Get方法:

- (void)testGet {
    ZHHTTPRequest *request = [[ZHHTTPRequest alloc] initWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"http://localhost:8080/plaintext?name=%@", author]]];
    request.HTTPMethod = GET;
    NSString *expectedResult = author;
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    [ZHHTTPClient sendRequest: request complete:^(ZHHTTPResponse *response) {
        NSString *responseString = [[NSString alloc] initWithData:response.data encoding:NSUTF8StringEncoding];
        NSLog(@"response: %@", responseString);
        XCTAssertTrue([[[NSString alloc] initWithData:response.data encoding:NSUTF8StringEncoding] isEqualToString:expectedResult], @"Strings are not equal %@ != %@", expectedResult, responseString);
        dispatch_semaphore_signal(semaphore);
    } failure:^(NSError *error) {
        NSLog(@"error: %@", error.description);
        dispatch_semaphore_signal(semaphore);
    }];
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市茂洒,隨后出現(xiàn)的幾起案子孟岛,更是在濱河造成了極大的恐慌,老刑警劉巖督勺,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件渠羞,死亡現(xiàn)場離奇詭異,居然都是意外死亡智哀,警方通過查閱死者的電腦和手機次询,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來瓷叫,“玉大人渗蟹,你說我怎么就攤上這事≡薇纾” “怎么了雌芽?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長辨嗽。 經(jīng)常有香客問我世落,道長,這世上最難降的妖魔是什么糟需? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任屉佳,我火速辦了婚禮,結(jié)果婚禮上洲押,老公的妹妹穿的比我還像新娘武花。我一直安慰自己,他們只是感情好杈帐,可當(dāng)我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布体箕。 她就那樣靜靜地躺著专钉,像睡著了一般。 火紅的嫁衣襯著肌膚如雪累铅。 梳的紋絲不亂的頭發(fā)上跃须,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天,我揣著相機與錄音娃兽,去河邊找鬼菇民。 笑死,一個胖子當(dāng)著我的面吹牛投储,可吹牛的內(nèi)容都是我干的第练。 我是一名探鬼主播,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼玛荞,長吁一口氣:“原來是場噩夢啊……” “哼复旬!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起冲泥,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤驹碍,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后凡恍,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體志秃,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年嚼酝,在試婚紗的時候發(fā)現(xiàn)自己被綠了浮还。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡闽巩,死狀恐怖钧舌,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情涎跨,我是刑警寧澤洼冻,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站隅很,受9級特大地震影響撞牢,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜叔营,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一屋彪、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧绒尊,春花似錦畜挥、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽躯泰。三九已至,卻和暖如春矮湘,著一層夾襖步出監(jiān)牢的瞬間斟冕,已是汗流浹背口糕。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工缅阳, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人景描。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓十办,卻偏偏與公主長得像,于是被迫代替她去往敵國和親超棺。 傳聞我的和親對象是個殘疾皇子向族,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內(nèi)容