HTTP所在的協(xié)議層
HTTP工作過程
一次HTTP操作稱為一個事務,其工作整個過程如下:
地址解析
如用客戶端瀏覽器請求這個頁面: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)絡與服務器建立連接,該連接是通過TCP來完成的,該協(xié)議與IP協(xié)議共同構(gòu)建Internet,即著名的TCP/IP協(xié)議族,因此Internet又被稱作是TCP/IP網(wǎng)絡。HTTP是比TCP更高層次的應用層協(xié)議,根據(jù)規(guī)則,只有低層協(xié)議建立之后才能,才能進行更層協(xié)議的連接,因此,首先要建立TCP連接,一般TCP連接的端口號是80。這里是8080端口
客戶機發(fā)送請求命令
建立連接后,客戶機發(fā)送一個請求給服務器,請求方式的格式為:統(tǒng)一資源標識符(URL)、協(xié)議版本號,后邊是MIME信息包括請求修飾符、客戶機信息和可內(nèi)容。
服務器響應
服務器接到請求后,給予相應的響應信息,其格式為一個狀態(tài)行,包括信息的協(xié)議版本號、一個成功或錯誤的代碼,后邊是MIME信息包括服務器信息、實體信息和可能的內(nèi)容。
實體消息是服務器向瀏覽器發(fā)送頭信息后,它會發(fā)送一個空白行來表示頭信息的發(fā)送到此為結(jié)束,接著,它就以Content-Type應答頭信息所描述的格式發(fā)送用戶所請求的實際數(shù)據(jù)
服務器關(guān)閉TCP連接
一般情況下,一旦Web服務器向瀏覽器發(fā)送了請求數(shù)據(jù),它就要關(guān)閉TCP連接,然后如果瀏覽器或者服務器在其頭信息加入了這行代碼
Connection:keep-alive
TCP連接在發(fā)送后將仍然保持打開狀態(tài),于是,瀏覽器可以繼續(xù)通過相同的連接發(fā)送請求。保持連接節(jié)省了為每個請求建立新連接所需的時間,還節(jié)約了網(wǎng)絡帶寬。
HTTP組件封裝
封裝的好處
- 使用者只需要了解如何通過類的接口使用類,而不用關(guān)心類的內(nèi)部數(shù)據(jù)結(jié)構(gòu)和數(shù)據(jù)組織方法。
- 高內(nèi)聚,低耦合一直是我們所追求的,用好封裝恰恰可以減少耦合
- 只要對外接口不改變,可以任意修改內(nèi)部實現(xiàn),這個可以很好的應對變化
- 類具有了簡潔清晰的對外接口,降低了使用者的學習過程
封裝的過程
大致分為以下三步:設計接口、填充實現(xiàn)、單元測試。
設計接口
這一步很重要,接口的好壞,直接決定了整個組件的好壞。
這個接口,需要有以下內(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請求,不要在此設置值。
*/
@property (nonatomic, strong) NSDictionary *postParams;
/**
上傳文件需要的數(shù)據(jù),不需要設置此項。
*/
@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;
/**
下載文件存儲的目標路徑,要精確到文件名,在設定之前,需要在外部判定文件是否存在,是否需要刪除。
*/
@property (nonatomic, copy) NSString *downloadDestinationPath;
/**
下載文件存儲的臨時路徑,如果下載時不設定此項,會有默認的臨時路徑。
*/
@property (nonatomic, copy) NSString *downloadTemporaryPath;
- (instancetype)initWithURL:(NSURL *)url;
+ (instancetype)requestWithURL:(NSURL *)url;
@end
其中ZHHTTPUploadComponent, 是為上傳準備的一個配置類,會接收上傳文件的名稱、路徑、類型等:
@interface ZHHTTPUploadComponent : NSObject
/**
dataKey: 每一個dataKey對應于一個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ù),對請求方法做了一個校準,然后再是根據(jù)是POST
還是GET
方法,再相應的私有方法。到現(xiàn)在為止還沒有看出來,這個ZHHTTPClient
內(nèi)部是封裝的哪種網(wǎng)絡庫,以后所有的更換網(wǎng)絡庫這種操作,是不需要動接口和這個實現(xiàn)函數(shù)的。
在具體的實現(xiàn)中,可以選擇自己需要的網(wǎng)絡框架,因為歷史原因,追求穩(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);
}