最近的一個項目中,需要向 safari 前端頁面?zhèn)鬏敂?shù)據(jù)亡鼠,研究了一番之后發(fā)現(xiàn)只有搭建本地http服務(wù)才能完美解決這一需求柜去。查詢一番資料之后,我決定采用CocoaHttpServer這個現(xiàn)成的輪子拆宛。CocoaHttpServer是由deusty designs開源的一個項目嗓奢,支持異步socket,ipv4和ipv6浑厚,http Authentication和TLS加密股耽,小巧玲瓏,而且使用方法也非常簡單钳幅。
開啟http服務(wù)
首先物蝙,我們需要開啟http服務(wù),代碼如下
// Configure our logging framework.
[DDLog addLogger:[DDTTYLogger sharedInstance]];
// Initalize our http server
httpServer = [[HTTPServer alloc] init];
// Tell the server to broadcast its presence via Bonjour.
[httpServer setType:@"_http._tcp."];
// Normally there's no need to run our server on any specific port.
[httpServer setPort:12345];
// We're going to extend the base HTTPConnection class with our MyHTTPConnection class.
[httpServer setConnectionClass:[YDHTTPConnection class]];
// Serve files from our embedded Web folder
NSString *webPath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"Web"];
DDLogInfo(@"Setting document root: %@", webPath);
[httpServer setDocumentRoot:webPath];
NSError *error = nil;
if(![httpServer start:&error])
{
DDLogError(@"Error starting HTTP Server: %@", error);
}
[httpServer setPort:12345]
用來設(shè)置端口號敢艰,此處可設(shè)置成80端口诬乞,如果是80端口,訪問手機服務(wù)器的時候可以不用寫端口號了。[httpServer setDocumentRoot:webPath]
用來設(shè)置服務(wù)器根路徑震嫉。這里要注意我們設(shè)置根路徑的文件夾在拖進工程時應(yīng)選擇create folder references方式森瘪,這樣才能在外部瀏覽器通過路徑訪問到文件夾內(nèi)部的文件。
設(shè)置GET與POST路徑
GET與POST路徑的配置是在一個繼承自HTTPConnection
的類中完成的票堵,即上一步[httpServer setConnectionClass:[YDHTTPConnection class]]
中的YDHTTPConnection
類扼睬。我們要在該類中重寫以下方法。
#pragma mark - get & post
- (BOOL)supportsMethod:(NSString *)method atPath:(NSString *)path
{
HTTPLogTrace();
// Add support for POST
if ([method isEqualToString:@"POST"])
{
if ([path isEqualToString:@"/calculate"])
{
// Let's be extra cautious, and make sure the upload isn't 5 gigs
return YES;
}
}
return [super supportsMethod:method atPath:path];
}
- (BOOL)expectsRequestBodyFromMethod:(NSString *)method atPath:(NSString *)path
{
HTTPLogTrace();
// Inform HTTP server that we expect a body to accompany a POST request
if([method isEqualToString:@"POST"]) return YES;
return [super expectsRequestBodyFromMethod:method atPath:path];
}
- (NSObject<HTTPResponse> *)httpResponseForMethod:(NSString *)method URI:(NSString *)path
{
HTTPLogTrace();
//獲取idfa
if ([path isEqualToString:@"/getIdfa"])
{
HTTPLogVerbose(@"%@[%p]: postContentLength: %qu", THIS_FILE, self, requestContentLength);
NSString *idfa = [[[ASIdentifierManager sharedManager] advertisingIdentifier] UUIDString];
NSData *responseData = [idfa dataUsingEncoding:NSUTF8StringEncoding];
return [[HTTPDataResponse alloc] initWithData:responseData];
}
//加減乘除計算
if ([method isEqualToString:@"POST"] && [path isEqualToString:@"/calculate"])
{
HTTPLogVerbose(@"%@[%p]: postContentLength: %qu", THIS_FILE, self, requestContentLength);
NSData *requestData = [request body];
NSDictionary *params = [self getRequestParam:requestData];
NSInteger firstNum = [params[@"firstNum"] integerValue];
NSInteger secondNum = [params[@"secondNum"] integerValue];
NSDictionary *responsDic = @{@"add":@(firstNum + secondNum),
@"sub":@(firstNum - secondNum),
@"mul":@(firstNum * secondNum),
@"div":@(firstNum / secondNum)};
NSData *responseData = [NSJSONSerialization dataWithJSONObject:responsDic options:0 error:nil];
return [[HTTPDataResponse alloc] initWithData:responseData];
}
return [super httpResponseForMethod:method URI:path];
}
- (void)prepareForBodyWithSize:(UInt64)contentLength
{
HTTPLogTrace();
// If we supported large uploads,
// we might use this method to create/open files, allocate memory, etc.
}
- (void)processBodyData:(NSData *)postDataChunk
{
HTTPLogTrace();
// Remember: In order to support LARGE POST uploads, the data is read in chunks.
// This prevents a 50 MB upload from being stored in RAM.
// The size of the chunks are limited by the POST_CHUNKSIZE definition.
// Therefore, this method may be called multiple times for the same POST request.
BOOL result = [request appendData:postDataChunk];
if (!result)
{
HTTPLogError(@"%@[%p]: %@ - Couldn't append bytes!", THIS_FILE, self, THIS_METHOD);
}
}
#pragma mark - 私有方法
//獲取上行參數(shù)
- (NSDictionary *)getRequestParam:(NSData *)rawData
{
if (!rawData) return nil;
NSString *raw = [[NSString alloc] initWithData:rawData encoding:NSUTF8StringEncoding];
NSMutableDictionary *paramDic = [NSMutableDictionary dictionary];
NSArray *array = [raw componentsSeparatedByString:@"&"];
for (NSString *string in array) {
NSArray *arr = [string componentsSeparatedByString:@"="];
NSString *value = [arr.lastObject stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
[paramDic setValue:value forKey:arr.firstObject];
}
return [paramDic copy];
}
其中悴势,- (BOOL)supportsMethod:(NSString *)method atPath:(NSString *)path
用來配置需要支持的POST路徑窗宇。父類HTTPConnection
中對GET方法是默認支持的,而POST方法則必須通過重寫來支持特纤。而- (NSObject<HTTPResponse> *)httpResponseForMethod:(NSString *)method URI:(NSString *)path
方法中則用來配置不同方法及路徑對應(yīng)的處理方式及返回數(shù)據(jù)军俊。
外部訪問
當我們的服務(wù)啟動后,假如要在外部訪問上圖中的index.html文件捧存,只需通過http://localhost:12345/index.html這樣的路徑即可粪躬。當然,也可以通過http://127.0.0.1:12345/index.html或者將127.0.0.1替換成設(shè)備ip矗蕊。而GET和POST方法我們也可以通過以下前端代碼來進行驗證短蜕。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="http://code.jquery.com/jquery-latest.js"></script>
</head>
<body>
<button onclick="getIdfa()">獲取idfa</button>
<button onclick="calculate()">加減乘除</button>
<script type="text/javascript">
function getIdfa() {
$.ajax({
type: "get", //請求方式
async: true, //是否異步
url: "http://localhost:12345/getIdfa",
success: function (data) {
alert(data);
},
error: function () {
alert("error");
}
});
}
function calculate() {
$.ajax({
type: "post", //請求方式
async: true, //是否異步
url: "http://localhost:12345/calculate",
data: {"firstNum":9, "secondNum":7},
success: function (data) {
alert(data);
},
error: function () {
alert("error");
}
});
}
</script>
</body>
</html>
另外氢架,在h5訪問本地服務(wù)時傻咖,還會存在跨域問題。這個問題我們需要通過在HTTPConnection
類的- (NSData *)preprocessResponse:(HTTPMessage *)response
和- (NSData *)preprocessErrorResponse:(HTTPMessage *)response
方法中加入以下代碼來解決岖研。
//允許跨域訪問
[response setHeaderField:@"Access-Control-Allow-Origin" value:@"*"];
[response setHeaderField:@"Access-Control-Allow-Headers" value:@"X-Requested-With"];
[response setHeaderField:@"Access-Control-Allow-Methods" value:@"PUT,POST,GET,DELETE,OPTIONS"];
后臺運行
我們都知道卿操,蘋果對APP占用硬件資源管理的很嚴格,更不要說應(yīng)用在后臺運行時的資源占用了孙援。正常情況下害淤,使用應(yīng)用時,APP從硬盤加載到內(nèi)存后拓售,便開始正常工作窥摄。當用戶按下home鍵,APP便被掛起到后臺础淤。當內(nèi)存不夠用時崭放,系統(tǒng)會自動把之前掛起狀態(tài)下的APP從內(nèi)存中清除。如果要使程序在后臺常駐鸽凶,則需要申請后臺權(quán)限币砂。
因此,我們要想保持本地服務(wù)在后臺運行玻侥,便必須要保證APP擁有后臺運行的權(quán)限决摧,并需要根據(jù)APP的具體類型(如:音樂播放、定位、VOIP等)在 Capabilities 中添加相應(yīng)的 Background Modes 鍵值對掌桩,如下圖所示
同時需要在代理方法中添加下述代碼边锁。當然,如果你的APP不存在和Background Modes 相符合的功能的話拘鞋,這么做可能會導(dǎo)致 AppStore 審核不通過砚蓬。
- (void)applicationDidEnterBackground:(UIApplication *)application {
_bgTask = [application beginBackgroundTaskWithExpirationHandler:^{
[application endBackgroundTask:_bgTask];
_bgTask = UIBackgroundTaskInvalid;
}];
}
配置https
利用 CocoaHttpServer 也可以搭建出https服務(wù)。只需要在YDHTTPConnection
中重寫以下兩個方法盆色。
#pragma mark - https
- (BOOL)isSecureServer
{
HTTPLogTrace();
return YES;
}
- (NSArray *)sslIdentityAndCertificates
{
HTTPLogTrace();
SecIdentityRef identityRef = NULL;
SecCertificateRef certificateRef = NULL;
SecTrustRef trustRef = NULL;
NSString *thePath = [[NSBundle mainBundle] pathForResource:@"localhost" ofType:@"p12"];
NSData *PKCS12Data = [[NSData alloc] initWithContentsOfFile:thePath];
CFDataRef inPKCS12Data = (__bridge CFDataRef)PKCS12Data;
CFStringRef password = CFSTR("123456");
const void *keys[] = { kSecImportExportPassphrase };
const void *values[] = { password };
CFDictionaryRef optionsDictionary = CFDictionaryCreate(NULL, keys, values, 1, NULL, NULL);
CFArrayRef items = CFArrayCreate(NULL, 0, 0, NULL);
OSStatus securityError = errSecSuccess;
securityError = SecPKCS12Import(inPKCS12Data, optionsDictionary, &items);
if (securityError == 0) {
CFDictionaryRef myIdentityAndTrust = CFArrayGetValueAtIndex (items, 0);
const void *tempIdentity = NULL;
tempIdentity = CFDictionaryGetValue (myIdentityAndTrust, kSecImportItemIdentity);
identityRef = (SecIdentityRef)tempIdentity;
const void *tempTrust = NULL;
tempTrust = CFDictionaryGetValue (myIdentityAndTrust, kSecImportItemTrust);
trustRef = (SecTrustRef)tempTrust;
} else {
NSLog(@"Failed with error code %d",(int)securityError);
return nil;
}
SecIdentityCopyCertificate(identityRef, &certificateRef);
NSArray *result = [[NSArray alloc] initWithObjects:(__bridge id)identityRef, (__bridge id)certificateRef, nil];
return result;
}
在實驗過程中我使用的為自簽名SSL證書灰蛙,因此訪問文件時會出現(xiàn)彈框提示不安全的問題,而GET與POST接口也出現(xiàn)了訪問失敗的情況隔躲。目前我想到的解決方案是將一個域名和127.0.0.1進行綁定摩梧,并使用該域名的SSL證書替換自簽名證書。至于可行性宣旱,還沒有做過實驗仅父,如果各位讀者有更好的想法,歡迎一起討論浑吟。
本文相關(guān)demo下載歡迎到我的github:Github地址