使用FTP(IOS FTP客戶端開發(fā)教程)

?本文翻譯自新近Wrox出版社出版的殖氏,由Peter van de Put所著的《Professional.iOS.Programming》众雷。該書題材比較新穎,結構合理绎巨,是一本不錯的IOS開發(fā)書籍伤提。本文譯自該書第八章《Using FTP》。本文開放使用认烁,不局限于轉(zhuǎn)載肿男、修改、增刪却嗡,引用舶沛,請保留出處說明。禁止任何商業(yè)用途窗价。歡迎任何修改建議如庭。

本章有哪些內(nèi)容?

?? 理解文件傳輸協(xié)議

?? 開發(fā)一個簡單FTP客戶端

?? 實現(xiàn)網(wǎng)絡流(Network streams)

WROX.COM CODE DOWNLOADS FOR THIS CHAPTER

The wrox.com code downloads for thischapter are found at www.wrox.com/go/proiosprog on the Download Code tab. Thecode is in the Chapter 8 download and individually named according to the namesthroughout the chapter.

在第七章撼港,你已經(jīng)學習了網(wǎng)絡部分坪它,包括從web服務器下載文件,向RESTful和SOAP服務器發(fā)送post請求帝牡。在某些情況下往毡,你可能還需要處理視頻之類的大文件,下載到你的應用或者上傳到服務器進行處理靶溜。

你可以將圖片或者PDF文件編碼成base64字符串开瞭,然后發(fā)送給服務器,但是這不是十分高效罩息、迅速的方法嗤详。創(chuàng)建base64字符串需要花費很長時間,然后你仍然要傳輸這些數(shù)據(jù)瓷炮。

文件傳輸協(xié)議FTP是一個用于FTP服務器和客戶端之間進行通信的協(xié)議葱色。


開發(fā)一個FTP客戶端

?????? 為IOS應用開發(fā)一個FTP客戶端的同時,你應當了解一些FTP協(xié)議的基本要素娘香〔哉客戶端使用預先確定端口上的Internet連接來創(chuàng)建到服務器的連接恐锣。默認地,一個FTP系統(tǒng)配置使用以下端口:

?? 端口20用于服務器創(chuàng)建到客戶端的連接

?? 端口21用于客戶端創(chuàng)建到服務器的連接注


譯注:控制連接以通常的客戶服務器方式建立舞痰。服務器以被動方式打開眾所周知的用于F T P 的端口(21),等待客戶的連接【饕Γ客戶則以主動方式打開TCP端口21,來建立連接响牛。控制連接始終等待客戶與服務器之間的通信赫段。該連接將命令從客戶傳給服務器, 并傳回服務器的應答呀打。

?????? FTP服務器的配置可以有所不同,所以你同意FTP服務器管理員關于使用那個端口就很重要了糯笙。

?????? 除了對某個端口號用于建立網(wǎng)絡流達成一致外贬丛,F(xiàn)TP協(xié)議預定義了交換的命令,因此客戶端可以告訴服務器它需要什么给涕,反過來也一樣豺憔。最常用的命令包括:

?? open:打開一個連接

?? close:關閉一個連接

?? get:從服務器拷貝一個文件到客戶端(下載)

?? mget:從服務器拷貝多份文件到客戶端(下載)

?? put:從客戶端拷貝一個文件到服務端(上傳)

?? mput:從客戶端拷貝多份文件到服務端(上傳)

?? delete:從當前遠端目錄刪除一個文件

?? cd:改變服務器上的目錄(由客戶端發(fā)起)

?? lcd:改變客戶端上的目錄

?? mkdir或者mkd:在服務端創(chuàng)建一個目錄(由客戶端發(fā)起)


如果你以前從來沒用過FTP客戶端,你可以從http://filezilla-project.org/下載FileZilla項目够庙。FileZilla是一個開源的FTP客戶端和服務端恭应,可用于分析在客戶端和服務端兩者之間交換的特定的命令。

?????? 根據(jù)你應用的需求耘眨,你有兩個基本的選項要考慮昼榛。如果你僅僅是從FTP服務端上傳或者下載文件,你可以使用由CFNetwork提供的高層API剔难。這個方案能力有限胆屿,確定地說不是一個完整的FTP客戶端。


寫一個簡單的FTP客戶端

?????????? 啟動Xcode并創(chuàng)建一個使用Single View ApplicationProject模板的工程偶宫,命名為SimpleFTPClinet非迹,使用表8-1所示選項。

將CFNetwork framework添加到你的工程纯趋。

?????? 創(chuàng)建一個繼承自NSObject彻秆、命名為FTPManager的類。打開FTPManager.h

文件结闸,使用“#includes”將CFNetworkframework包含進去唇兑。

?????? 接著定義FTPManagerDelegate協(xié)議,該協(xié)議包括一些將向delegate提供反饋的方法桦锄。

?????? 創(chuàng)建一個接受用于FTP連接的server, username, 以及password的初始化器扎附。實現(xiàn)方面假定你總是需要一個username和password來建立FTP連接。因為打開一個公眾的FTP服務端對于你應用的意圖來說毫無意義结耀。

?????? 接下去留夜,聲明四個方法用于與FTP命令相關的匙铡、受支持的操作。如表8—1所示碍粥。

?????? 最后鳖眼,為該類的delegate創(chuàng)建一個共有property。

?????? 完整的代碼如清單8-1所示嚼摩。

[objc]?view plain?copy

LISTING?8-1:?Chapter8/SimpleFTPClient/FTPManager.h???

#import???

#include????


enum?{??

kSendBufferSize?=32768???

};??


@protocol?FTPManagerDelegate???

-(void)ftpUploadFinishedWithSuccess:(BOOL)success;?-(void)ftpDownloadFinishedWithSuccess:(BOOL)success;??

-?-(void)directoryListingFinishedWithSuccess:(NSArray?*)arr;???

-(void)ftpError:(NSString?*)err;??


@end??



@interface?FTPManager?:?NSObject??

-?(id)initWithServer:(NSString?*)server?user:(NSString?*)username??

password:(NSString?*)pass;??

-?(void)downloadRemoteFile:(NSString?*)filename?localFileName:(NSString?*)localname;??

-?(void)uploadFileWithFilePath:(NSString?*)filePath;??

-?(void)createRemoteDirectory:(NSString?*)dirname;??

-?(void)listRemoteDirectory;??

@property?(nonatomic,?assign)?id???

@end??

?????? 打開FTPManager.m文件钦讳,在import頭文件語句后寫入私有接口。

?????? 因為你在為網(wǎng)絡通信使用流(streams)枕面,所以你要用到兩種不同的流:NSOutputStream和NSInputStream愿卒。

?????? 這時候,理解FTP的put和mkd是你發(fā)送給服務端的命令潮秘,因此要用到NSOutputStream是很重要的琼开。List和get命令通過打開的socket傳送,并且作為回應枕荞,將收到你請求的數(shù)據(jù)柜候。你需要去讀取這些數(shù)據(jù),因此你將用到NSInputStream躏精。

?????? 定義兩個BOOL型properties(isReceiving 和isSending)用于跟蹤流的狀態(tài)改橘,避免流操作間的數(shù)據(jù)混淆。

?????? 你可以在本章的下載中找到FTPManager的完整實現(xiàn)玉控。

?????? 這里飞主,TPManager.m的實現(xiàn)被分解開來,并且一步一步解釋高诺。第一步碌识,用自定義初始化器初始化FTPManager對象,你可以向該初始化器傳連接和認證所需的properties虱而,如清單8-2所示筏餐。

[objc]?view plain?copy

LISTING?8-2:?The?initWithServer?method??

-(id)initWithServer:(NSString?*)server?user:(NSString?*)username?password:(NSString?*)pass??

{??

if?((self?=?[super?init]))??

{??

self.ftpServer?=?server;???

self.ftpUsername=username;???

self.ftpPassword=pass;??

}??

return?self;??

?}??

流處理的一個潛在問題是鎖住。一個正在從一個流里寫入或者讀取的線程也許不得不無限期等待直到流里有空間進行寫入牡拇,或者流里有可用數(shù)據(jù)(bytes)進行讀取魁瞪。為了克服這個問題,你需要一個接受NSStream作為參數(shù)的方法惠呼,并將它加入到當前NSRunLoop中進行調(diào)度安排(schedule)导俘,那樣delegate就能只收到流相關事件報告的信息,那時鎖住就不會發(fā)生剔蹋。為了這個目的旅薄,寫一個如清單8-3所示的簡單helper方法。

NSRunLoop類聲明了面向管理輸入資源的對象的編程接口泣崩。一個NSRunLoop對象處理輸入資源少梁,例如窗口系統(tǒng)的鼠標和鍵盤事件洛口,NSPort和NSConnetion對象。一個NSRunLoop對象也處理NSTimer事件凯沪。

你的應用既不能創(chuàng)建第焰,也不能顯式地管理NSrunLoop對象。每個NSThread對象妨马,包括應用的主線程挺举,出于需要,都有一個自動創(chuàng)建的NSRunLoop對象身笤。假如你要訪問當前線程的runloop,你使用類方法currentRunLoop葵陵。

[objc]?view plain?copy

LISTING?8-3:?The?scheduleInCurrentThread?method??

-???(void)scheduleInCurrentThread:(NSStream*)aStream???

{??

[aStream?scheduleInRunLoop:[NSRunLoop?currentRunLoop]??

forMode:NSRunLoopCommonModes];??

}??

smartURLForString:方法接受一個字符串并將它轉(zhuǎn)化為一個有效的NSURL對象液荸。假如你傳入一個類似127.0.0.1這樣的IP地址,它返回一個ftp://127.0.0.1這樣的NSURL對象脱篙。

?????? smartURLForString:如清單8-4所示娇钱。

[objc]?view plain?copy

LISTING?8-4:?The?smartURLForString?method??

-(NSURL?*)smartURLForString:(NSString?*)str??

?{??

NSURL?*?result;???

NSString?*?trimmedStr;??

NSRange?schemeMarkerRange;??

NSString?*?scheme;??

result?=?nil;??

trimmedStr?=?[str?stringByTrimmingCharactersInSet:??

[NSCharacterSet?whitespaceCharacterSet]];??

if?(?(trimmedStr?!=?nil)?&&?([trimmedStr?length]?!=?0)?)?{??

schemeMarkerRange?=?[trimmedStr?rangeOfString:@"://"];??

if?(schemeMarkerRange.location?==?NSNotFound)?{??

result?=?[NSURL?URLWithString:[NSString?stringWithFormat:?@"ftp://%@",?trimmedStr]];??

}else?{??

scheme?=?[trimmedStr?substringWithRange:??

NSMakeRange(0,?schemeMarkerRange.location)];??

if?(?([scheme?compare:@"http"?options:??

NSCaseInsensitiveSearch]?==?NSOrderedSame)?)?{??

result?=?[NSURL?URLWithString:trimmedStr];??

}else?{??

//unsupported?url?schema??

}???

}??

}??

return?result;???

}??

isReceiving方法用來檢查dataStream是否已初始化,isSending方法用來檢查commandStream是否已初始化绊困。這些方法用于網(wǎng)絡通信中以避免在一條命令尚在處理中又執(zhí)行另一條命令的情況文搂。?????? 這兩個方法在清單8-5中。

[objc]?view plain?copy

LISTING?8-5:?The?isReceiving?and?isSending?methods??

-?(BOOL)isReceiving?{??

return?(_dataStream?!=?nil);???

}??

-?(BOOL)isSending?{??

return?(_commandStream?!=?nil);???

}??

[objc]?view plain?copy

LISTING?8-6:?The?closeAll?metho??

-(void)closeAll?{??

if?(_commandStream?!=?nil)?{???

[_commandStream?removeFromRunLoop:??

[NSRunLoop?currentRunLoop]?forMode:NSDefaultRunLoopMode];???

_commandStream.delegate?=?nil;??

[_commandStream?close];??

_commandStream?=?nil;??

}??

if?(_uploadStream?!=?nil)?{??

[_uploadStream?close];??

_uploadStream?=?nil;?}??

if?(_downloadfileStream?!=?nil)?{???

[_downloadfileStream?close];???

_downloadfileStream?=?nil;??

}??

if?(_dataStream?!=?nil)?{??

[_dataStream?removeFromRunLoop:??

[NSRunLoop?currentRunLoop]?forMode:NSDefaultRunLoopMode];??

_dataStream.delegate?=?nil;???

[_dataStream?close];???

_dataStream?=?nil;??

}??

_currentOperation?=@"";???

}??

下載一個遠程文件

?????? 你調(diào)用downloadRemoteFile:localFileName:方法秤朗,并傳給它一個服務端文件的文件名煤蹭,例如picture1.png,和一個本地文件名取视,來從FTP服務端下載一個文件硝皂。已下載的文件將會以傳入的本地文件名命名,寫入到你的應用的根目錄下的臨時目錄作谭。

該方法先用“FTP服務端地址/遠程文件名”生成一個smartURLForString稽物,返回NSURL對象,例如ftp://127.0.0.1/picture1.png.假如isReceiving方法返回YES折欠,代理方法ftpError被調(diào)用贝或,并傳入一個錯誤信息。否則锐秦,用已創(chuàng)建的路徑初始化downloadStream咪奖。downloadStream被創(chuàng)建后,數(shù)據(jù)就可以從該流中讀取酱床,currentOperation property設置為GET赡艰。流代理(stream delegate)負責從不同的流中讀取和寫入數(shù)據(jù),需要為GET命令(從流中讀取數(shù)據(jù)斤葱,寫入到一個文件)LIST命令(以流形式傳送將要分析的目錄列表)進行不同的操作慷垮。出于這個目的揖闸,你需要一個currentOperation的property。

注意:你應當使用FTP服務端地址代替127.0.0.1

?????? 使用CFBridgingRelease( CFReadStreamCreateWithFTPURL(NULL,

(__bridge CFURLRef)url));傳入一個早先創(chuàng)建的url料身,創(chuàng)建一個commandStream汤纸。因為在使用認證,你需要將傳給FTPManager初始化器的用戶名和密碼設置給commandStream芹血。最后將delegate設置為self贮泞。調(diào)用scheduleInCurrentThread:將commandStream調(diào)度安排到當前線程,并打開commandStream幔烛。

?????? 當commandStream被打開啃擦,連接被建立,用戶名和密碼被用于認證該流饿悬。服務端響應在stream:handleEvent:方法中被捕獲令蛉,該方法將在后面講解,因為當前你在學習的其他方法也同樣使用該方法進行結果處理狡恬。

?????? downloadRemoteFile:方法如清單8-7所示珠叔。

[objc]?view plain?copy

-(void)downloadRemoteFile:(NSString?*)filename?localFileName:(NSString?*)localname???

{??

BOOL?success;??

NSURL?*?url;??

url?=?[self?smartURLForString:[NSString?stringWithFormat:??

@"%@/%@",_ftpServer,filename]];??

success?=?(url?!=?nil);???

if?(?!?success)?{??

[self.delegate?ftpError:@"invalid?url?for?downloadRemoteFile?method"];???

}else?{??

if?(self.isReceiving){??

[self.delegate?ftpError:@"receiving?in?progress"];???

return?;??

????????}??

NSString?*path?=?[NSTemporaryDirectory()??

stringByAppendingPathComponent:localname];??

_downloadfileStream?=?[NSOutputStream?outputStreamToFileAtPath:??

path?append:NO];??

[_downloadfileStream?open];??

_currentOperation?=@"GET";??

_dataStream=CFBridgingRelease(?CFReadStreamCreateWithFTPURL(NULL,(__bridge?CFURLRef)?url));???

[_dataStream?setProperty:_ftpUsername??

forKey:(id)kCFStreamPropertyFTPUserName];??

[_dataStream?setProperty:_ftpPassword???

forKey:(id)kCFStreamPropertyFTPPassword];??

_dataStream.delegate?=?self;??

[self?performSelector:@selector(scheduleInCurrentThread:)??

onThread:[[self?class]?networkThread]??

?withObject:_dataStream?waitUntilDone:YES];??

[_dataStream?open];??

}??

}??

創(chuàng)建一個遠程目錄

?????? 你可以使用createRemotedirectory:方法在服務端創(chuàng)建一個目錄。當然你提供的用于建立連接的證書需要有權限創(chuàng)建目錄弟劲。實現(xiàn)方法和剛才你學習的下載文件的例子很相似祷安。使用CFURLCreateCopyAppendingPathComponent將請求創(chuàng)建的目錄名拼接到已創(chuàng)建的URL后面,形成的新的URL類似ftp://127.0.0.1/newdirname兔乞。因為你在發(fā)送一個命令到FTP服務端汇鞭,commandStream被創(chuàng)建而不是dataStream,認證信息被傳遞庸追,流被打開虱咧。同樣地,這里也由stream:handleEvent:方法負責處理響應锚国。createRemotedirectory:方法如清單8-8所示腕巡。

[objc]?view plain?copy

-?(void)createRemoteDirectory:(NSString?*)dirname??

?{??

BOOL?success;?NSURL?*?url;??

url?=?[self?smartURLForString:_ftpServer];???

success?=?(url?!=?nil);??

if?(success)?{??

url=CFBridgingRelease(?CFURLCreateCopyAppendingPathComponent(NULL,(__bridge?CFURLRef)?url,(__bridge?CFStringRef)?dirname,?true)?);??

success?=?(url?!=?nil);?}??

if?(?!?success)?{??

[self.delegate?ftpError:@"invalid?url?for?createRemoteDirectory?method"];??

}else?{??

if?(self.isSending){??

[self.delegate?ftpError:@"sending?in?progress"];??

return?;???

}??

_???????commandStream=CFBridgingRelease(??

CFWriteStreamCreateWithFTPURL(NULL,??

?(__bridge?CFURLRef)?url)??

);??

//set?credentials??

[_commandStream?setProperty:_ftpUsername??

forKey:(id)kCFStreamPropertyFTPUserName];???

[_commandStream?setProperty:_ftpPassword??

forKey:(id)kCFStreamPropertyFTPPassword];???

_commandStream.delegate?=?self;??

[self?performSelector:@selector(scheduleInCurrentThread:)???

onThread:[[self?class]???

networkThread]?withObject:_commandStreamwaitUntilDone:YES];??

[_commandStream?open];??

}???

}??

列出一個遠程目錄

?????? listRemoteDirectory方法等同于FTP的list命令,該方法列出服務端某個目錄下的內(nèi)容血筑。盡管一些RFC適用于FTP程序绘沉,但是FTP list命令的結果正如你所希望的那樣,不是100%標準化的豺总,因此它可以很容易解析到一個數(shù)組里车伞。

?????? 檢查完你并沒在dataStream上接收數(shù)據(jù)之后,你設置listDataproperty并初始化listEntries property喻喳。你將currentOperation property設置為LIST另玖,以此區(qū)別GET和LIST正如前面解釋的那樣,調(diào)用CFReadStreamCreateWithFTPURL,使用創(chuàng)建的URL來初始化dataStream谦去。

?????? 再次地慷丽,設置dataStream的認證信息并將它調(diào)度安排并打開。

?????? 為了能夠解析LIST命令的結果鳄哭,實現(xiàn)用于解析傳入數(shù)據(jù)流并最終將結果寫入listEntries property的三個helper方法要糊。所有數(shù)據(jù)都被處理以后,directoryListingFinishedWithSuccess方法被調(diào)用妆丘,該方法返回一個數(shù)組锄俄,包含在FTP服務端上找到的文件。listRemoveDirectory方法如清單8-9所示勺拣。

[objc]?view plain?copy

-?(void)listRemoteDirectory?{??

BOOL?success;??

NSURL?*?url;??

url?=?[self?smartURLForString:_ftpServer];??

success?=?(url?!=?nil);??

if?(?!?success)?{??

[self.delegate?ftpError:@"invalid?url?for?listRemoteDirectory?method"];???

}else?{??

if?(self.isReceiving){??

[self.delegate?ftpError:@"receiving?in?progress"];???

return?;??

}??

self.listData?=?[NSMutableData?data];??

if?(self.listEntries)?self.listEntries=nil;??

self.listEntries=[[NSMutableArray?alloc]?init];?_??

currentOperation?=@"LIST";??

self.dataStream?=?CFBridgingRelease(??

CFReadStreamCreateWithFTPURL(NULL,??

(__bridge?CFURLRef)?url));???

//set?credentials??

[self.dataStream?setProperty:?self.ftpUsername??

?forKey:(id)kCFStreamPropertyFTPUserName];??

[self.dataStream?setProperty:?self.ftpPassword???

?forKey:(id)kCFStreamPropertyFTPPassword];??

self.dataStream.delegate?=?self;??

[self?performSelector:@selector(scheduleInCurrentThread:)??

onThread:[[self?class]?networkThread]???

withObject:_dataStream???

waitUntilDone:YES];??

[self.dataStream?open];??

?}??

}??

下面的方法來自蘋果公司的例子奶赠,并且已經(jīng)清理過。因為從FTP服務端得到的響應并不是100%標準化的药有,所以你可以使用蘋果公司提供的解析示例毅戈,該示例在大多數(shù)服務端實現(xiàn)上能工作,并呈現(xiàn)給你一個包含返回的文件信息的數(shù)組塑猖。解析helper方法如清單8-10所示

[objc]?view plain?copy

#pragma?listing?helpers??

-??(void)addListEntries:(NSArray?*)newEntries???

{?[self.listEntries?addObjectsFromArray:newEntries];??

[self?closeAll];??

[self.delegate?directoryListingFinishedWithSuccess:?self.listEntries];???

}??

//this?function?is?taken?over?from?Apple?samples???

-??(NSDictionary?*)entryByReencodingNameInEntry:(NSDictionary?*)??

entry?encoding:(NSStringEncoding)newEncoding???

{??

NSDictionary??*?result;??

NSString??*name;??

NSData?*?nameData;??

NSString??*newName;??

newName?=?nil;??

//?Try?to?get?the?name,?convert?it?back?to?MacRoman,?and?then?reconvert?it??

//?with?the?preferred?encoding.??

name?=?[entry?objectForKey:(id)?kCFFTPResourceName];???

if?(name?!=?nil)?{??

nameData?=?[name???

?dataUsingEncoding:NSMacOSRomanStringEncoding];??

if?(nameData?!=?nil)?{??

newName?=?[[NSString?alloc]??

initWithData:nameData?encoding:newEncoding];??

}???

}??

if?(newName?==?nil)?{??

result?=?(NSDictionary?*)?entry;??

}else?{??

NSMutableDictionary?*?newEntry;newEntry?=?[entry?mutableCopy];??

[newEntry?setObject:newName?forKey:(id)?kCFFTPResourceName];??

?result?=?newEntry;??

}??

return?result;???

}??


//also?this?function?is?taken?over?from?Apple?samples???

-?(void)parseListData??

{??

NSMutableArray?*?newEntries;???

NSUInteger?offset;???

newEntries?=?[NSMutableArray?array];??

offset?=0;??

do?{??

CFIndex?bytesConsumed;??

CFDictionaryRef?thisEntry;??

thisEntry?=NULL;??

bytesConsumed?=?CFFTPCreateParsedResourceListing(NULL,?&((const?uint8_t?*)self.listData.bytes)[offset],(CFIndex)?([self.listData?length]?-?offset),?&thisEntry);??

if?(bytesConsumed?>?0)?{??

if?(thisEntry?!=?NULL)?{??

NSDictionary?*?entryToAdd;??

entryToAdd?=?[self?entryByReencodingNameInEntry:??

(__bridgeNSDictionary?*)?thisEntry???

encoding:NSUTF8StringEncoding];??

[newEntries?addObject:entryToAdd];?}??

//?We?consume?the?bytes?regardless?of?whether?we?get?an?entry.???

offset?+=?(NSUInteger)?bytesConsumed;??

}??

if?(thisEntry?!=?NULL)?{??

CFRelease(thisEntry);???

}??

if?(bytesConsumed?==?0)?{??

//?We?hven't?yet?got?enough?data?to?parse?an?entry.???

//Wait?for?more?data?to?arrive??

break;??

}else?if?(bytesConsumed?<?0)?{??

//?We?totally?failed?to?parse?the?listing.?Fail.???

break;??

}??

}while?(YES);??

if?([newEntries?count]?!=?0)?{??

[self?addListEntries:newEntries];??

}??

if?(offset?!=?0)?{??

[self.listData?replaceBytesInRange:NSMakeRange(0,?offset)???

?withBytes:NULL?length:0];??

}???

}??

結果數(shù)組會提供類似如下的信息:

[objc]?view plain?copy

{??

kCFFTPResourceGroup?=?ftp;???

kCFFTPResourceLink?="";??

kCFFTPResourceModDate?="2013-03-22?14:34:00?+0000";???

kCFFTPResourceMode?=420;??

kCFFTPResourceName?="1.jpg";??

kCFFTPResourceOwner?=?ftp;??

kCFFTPResourceSize?=5855;???

kCFFTPResourceType?=8;??

}??

數(shù)組中的每個實體包含一個字典竹祷,字典中元素含義解釋如表8-2所示

上傳一個文件

?????? 為上傳一個文件到FTP服務端谈跛,你可以使用uploadFileWithFilePath:方法羊苟,向其傳送一個你想上傳的文件的fileath。

?????? 你可以使用CFURLCreateCopyAppendingPathComponent在smartURLString后面拼接傳入的filePath的lastPathComponent感憾,以形成新的URL蜡励。

?????? uploadStream使用filePathPath創(chuàng)建并打開,那樣就能使用NSInputStream讀取文件阻桅。接著調(diào)用CFWriteStreamCreateWithFTPURL函數(shù)凉倚,使用剛才創(chuàng)建的URL創(chuàng)建commandStream,并設置其認證信息嫂沉。

?????? commandStream調(diào)度安排到NSRunLoop并打開稽寒,再次地,stream:handleEvent:負責處理響應趟章。uploadFileWithFilePath方法如清單8-11所示杏糙。

[objc]?view plain?copy

-?(void)uploadFileWithFilePath:(NSString?*)filePath???

{??

BOOL?success;??

NSURL?*?url;??

url?=?[self?smartURLForString:_ftpServer];???

success?=?(url?!=?nil);??

if?(success)?{??

url=CFBridgingRelease(?CFURLCreateCopyAppendingPathComponent(NULL,(?CFURLRef)?url,(?CFStringRef)?[filePath?lastPathComponent],?false));??

success?=?(url?!=?nil);?}??

if?(?!?success)?{??

[self.delegate?ftpError:@"invalid?url?for?uploadFileWithFilePath?method"];??

}else?{??

if?(self.isSending){??

[self.delegate?ftpError:@"sending?in?progress"];??

return?;??

?}??

self.uploadStream?=?[NSInputStream???

?inputStreamWithFileAtPath:filePath];??

[self.uploadStream?open];??

self.commandStream=CFBridgingRelease(CFWriteStreamCreateWithFTPURL(NULL,(__bridge?CFURLRef)?url));??

//set?credentials??

[self.commandStream?setProperty:_ftpUsername??

?forKey:(id)kCFStreamPropertyFTPUserName];??

[self.commandStream?setProperty:_ftpPassword??

?forKey:(id)kCFStreamPropertyFTPPassword];??

self.commandStream.delegate?=?self;??

[self?performSelector:@selector(scheduleInCurrentThread:)??

onThread:[[self?class]???

networkThread]???

withObject:self.commandStream???

waitUntilDone:YES];??

[self.commandStream?open];???

}??

}??

正如前面提到的,stream:handleWithEvent:正是流通信魔法發(fā)生的地方蚓土。該實現(xiàn)首先使用switch語句來為各種可能發(fā)生的NSStreamEvent提供不同的處理宏侍。




從NSStream中讀取

?????? 你可以從清單8-12所示的實現(xiàn)中發(fā)現(xiàn),假如eventCode是NSStreamEventHasBytesAvailable時蜀漆,意味著有可讀取的比特谅河,你可以創(chuàng)建一個緩沖區(qū),并從流中讀取比特到緩沖區(qū)中。一旦所有的比特全都從流中讀取完畢绷耍,根據(jù)當前操作吐限,假如你正在執(zhí)行l(wèi)istRemoteDirectory,你可以調(diào)用parseListData方法來解析結果锨天,或者你正在下載一個文件毯盈,你可以調(diào)用parseListData方法來將它寫到一個文件并存儲。




寫入NSStream

?????? 當你想要寫入一個NSStream病袄,你需要檢查事件是不是NSStreamEventHasSpaceAvailable搂赋,該事件僅在NSStream仍有可用空間用于寫入時被調(diào)用。因為流被當前runloop所調(diào)度安排益缠,所以當無可用寫入空間時脑奠,線程不會鎖住。現(xiàn)在你可以簡單地從緩沖區(qū)向流寫入比特幅慌。

為使用FTPManager宋欺,你要用FTP服務端的IP地址來初始化它,并且在viewDidLoad方法里提供可用的用戶名和密碼胰伍。

你實現(xiàn)你所要求的代理方法齿诞,創(chuàng)建你所需要的用戶界面,在FTPManager實例中調(diào)用適當?shù)暮瘮?shù)骂租,走你祷杈!

正如你已知的,你不應當在主線程運行網(wǎng)絡操作渗饮。因此最好的方法是使用DISPATCH_QUEUE_PRIORITY_BACKGROUND在dispatch_queue里創(chuàng)建FTPManager但汞,這樣你的用戶界面就不會被鎖住。

一個實現(xiàn)例子如清單8-14所示互站。

[objc]?view plain?copy

LISTING?8-14:?Chapter8/SimpleFTPClient/YDViewController.m???

#import?"YDViewController.h"??

@interface?YDViewController?()??

@end??

@implementation?YDViewController??

-?(void)viewDidLoad?{??

[super?viewDidLoad];??

dispatch_queue_t?defQueue?=?dispatch_get_global_queue?(DISPATCH_QUEUE_PRIORITY_BACKGROUND,0);??

dispatch_async(defQueue,?^{??

ftpmanager=[[FTPManager?alloc]?initWithServer:@"YOUR_SERVERNAME"??

user:@"YOUR_USERNAME"?password:@"YOUR_PASSWORD!"];??

ftpmanager.delegate=self;??

});??

}??

-(IBAction)uploadFile:(id)sender?{??

[ftpmanager?listRemoteDirectory];??

}??

-?(void)ftpDownloadFinishedWithSuccess:(BOOL)success?{??

if?(!success)?{??

//handle?your?error??

}???

}??


-(void)ftpError:(NSString?*)err??

{??

//handle?your?error??

}??


-(void)directoryListingFinishedWithSuccess:(NSArray?*)arr?{??

//use?the?array?the?way?you?need?it???

}??


-?(void)ftpUploadFinishedWithSuccess:(BOOL)success?{??

if?(!success)?{??

//handle?your?error??

}???

}??


-?(void)didReceiveMemoryWarning?{??

[super?didReceiveMemoryWarning];???

}??

@end??

你可以將創(chuàng)建的FTPManager類用于你第一章創(chuàng)建的應用框架私蕾,這樣當你開發(fā)一個新應用時候它就可以用了。




寫一個復雜的FTP客戶端

??????????? 使用你創(chuàng)建的FTPManager胡桃,你可以進行基本的操作踩叭,例如下載上傳一個文件,在遠程服務端端創(chuàng)建一個目錄翠胰,取回一個目錄的清單容贝,就這些。假如你想對FTP過程有更多控制或者想進行本地操作亡容,那你就需要一個不同的方案嗤疯,該方案在更低層實現(xiàn)FTP協(xié)議。在本例子中你將學到如何寫一個復雜的FTP客戶端闺兢,給你FTP操作和應答處理的完全控制茂缚。

??????????? 啟動Xcode并創(chuàng)建一個Single ViewApplication Project模板的工程戏罢,命名為ComplexFTPClient使用如圖8-2所示的選項。

作為第一步脚囊,再次添加CFNetwork到你的工程龟糕。

??????????? 創(chuàng)建一個繼承自NSObject的新類,命名為YDFTPClient悔耘。創(chuàng)建YDFTPClientDelegate協(xié)議和公開方法如清單8-15所示讲岁。

[objc]?view plain?copy

LISTING?8-15:?Chapter8/ComplexFTPClient/YDFTPClient.h??

#import????

@protocol?YDFTPClientDelegate???

-(void)logginFailed;??

-(void)loggedOn;??

-(void)serverResponseReceived:(NSString?*)lastResponseCode??

message:(NSString?*)lastResponseMessage;?-(void)ftpError:(NSString?*)err;??

@end??

@interface?YDFTPClient?:?NSObject??

@property?(nonatomic,?strong)?id?delegate;???

@property?(readonly)?UInt64?numberOfBytesSent;??

@property?(readonly)?UInt64?numberOfBytesReceived;??

-?(id)initClient;??

-(void)sendRAWCommand:(NSString?*)command;??

-(void)connect;??

-(void)disconnect;??

@end??

YDFTPClient.m包括由它的變量定義的私有接口和不同方法的實現(xiàn)。像前面工程那樣衬以,YDFTPClient中的關鍵邏輯是流缓艳。你可以在本章的下載中找到完整的YDFTPClient.m實現(xiàn)。這不是所有FTP命令和應答的100%實現(xiàn)看峻,但是是一個你可以通過實現(xiàn)你需要的命令來擴展的骨骼類(skeleton class)阶淘。

??????????? 在YDFTPClient私有接口中創(chuàng)建properties,如清單8-16所示互妓。

[objc]?view plain?copy

LISTING?8-16:?Private?Interface?of?the?YDFTPClient?class??

@interface?YDFTPClient()?{??

UInt64?numberOfBytesSent;??

UInt64?numberOfBytesReceived;??

int?uploadbytesreadSoFar;??

}??

@property?(readwrite,?assign)?NSString*?dataIPAddress;??

@property?(readwrite,?assign)?UInt16?dataPort;??

@property?(nonatomic,?assign,?readonly?)?uint8_t?*?buffer;??

@property?(nonatomic,?assign,?readwrite)?size_t?bufferOffset;??

@property?(nonatomic,?assign,?readwrite)?size_t?bufferLimit;??

@property?(nonatomic,assign)?int?lastResponseInt;??

@property?(nonatomic,assign)?NSString*?lastResponseCode;???

@property?(nonatomic,assign)?NSString*?lastCommandSent;???

@property?(nonatomic,assign)?NSString*?lastResponseMessage;??

@property?(nonatomic,retain,?strong)?NSInputStream?*inputStream;???

@property?(nonatomic,?retain,strong)?NSOutputStream?*outputStream;??

@property(nonatomic,?retain,strong)?NSInputStream?*dataInStream;???

@property?(nonatomic,?retain,strong)?NSOutputStream?*dataOutStream;??

@property?(nonatomic,assign)?BOOL?isConnected;??

@property?(nonatomic,assign)?BOOL?loggedOn;??

@property?(nonatomic,assign)?BOOL?isDataStreamConfigured;???

@property?(nonatomic,assign)?BOOL?isDataStreamAvailable;??


@end??

讓我們來一步步分解該實現(xiàn)溪窒,initClient方法是你的將用于初始化本地變量和properties的自定義初始化器。該方法如清單8-17所示冯勉。

[objc]?view plain?copy

LISTING?8-17:?The?initClient?method??

-(id)initClient???

{??

if?((self?=?[super?init]))??

{??

self.isConnected=NO;???

self.dataIPAddress=0;???

self.dataPort=0;???

self.isConnected=NO;???

self.isDataStreamAvailable=NO;???

self.lastCommandSent=@"";???

self.lastResponseCode=@"";??

self.lastResponseMessage=@"";???

}??

return?self;??

?}??

你定義connect和disconnect方法澈蚌。connect方法僅僅調(diào)用initNet-workCommunication:方法,disconnect方法調(diào)用logoff:方法灼狰,如清單8-18所示宛瞄。

[objc]?view plain?copy

LISTING?8-18:?The?connect?and?disconnect?methods??

-(void)connect???

{??

if?(!self.isConnected)??

[self?initNetworkCommunication];??

}???

-(void)disconnect???

{??

if?(self.isConnected)???

[self?logoff];??

}??

scheduleInCurrentThread:方法和你前面的例子中的完全一樣。initNetworkCommunication:方法被connect:方法調(diào)用伏嗜,用于創(chuàng)建inputStream和outputStream坛悉,將它們安排調(diào)度到當前NSRunLoop伐厌,并打開承绸,你可以在清單8-19中看到。

[objc]?view plain?copy

LISTING?8-19:?The?initNetworkCommunication?method??

-?(void)?initNetworkCommunication?{???

CFReadStreamRef?readStream;???

CFWriteStreamRef?writeStream;???

CFStreamCreatePairWithSocketToHost(NULL,??

(__bridge?CFStringRef)kFTPServer,?kFTPPort,?&readStream,?&writeStream);??

self.inputStream?=?(__bridge_transfer?NSInputStream?*)readStream;???

self.outputStream?=?(__bridge_transfer?NSOutputStream?*)writeStream;???

[self.inputStream?setDelegate:self];??

[self.outputStream?setDelegate:self];??

[self?performSelector:@selector(scheduleInCurrentThread:)??

onThread:[[self?class]?networkThread]??

?withObject:self.inputStream??

waitUntilDone:YES];??

[self?performSelector:@selector(scheduleInCurrentThread:)??

onThread:[[self?class]?networkThread]???

withObject:self.outputStream??

waitUntilDone:YES];???

[self.inputStream?open];??

[self.outputStream?open];???

self.isConnected=YES;???

self.isDataStreamConfigured=NO;??

}??

stream:handleEvent:方法也在該實現(xiàn)中挣轨,是一個關鍵方法军熏,包含了從流對象讀取和寫入的控制邏輯,你可以在清單8-20中看到卷扮。在NSStreamEventHasBytesAvailable事件中荡澎,你在緩沖區(qū)中讀取來自流的比特,增加用于網(wǎng)絡統(tǒng)計的numberOfBytesReceived property的值晤锹,發(fā)送讀取輸出到負責處理的messageReceived:方法中摩幔。

[objc]?view plain?copy

LISTING?8-20:?The?stream:?handleEvent:?method??

-?(void)stream:(NSStream?*)theStream??

?handleEvent:(NSStreamEvent)streamEvent?{??

switch?(streamEvent)?{??

case?NSStreamEventOpenCompleted:??

break;??

case?NSStreamEventNone:??

break;??

case?NSStreamEventHasBytesAvailable:??

if?(theStream?==?self.inputStream)?{??

uint8_t?buffer[1024];??

int?len;??

while?([self.inputStream?hasBytesAvailable])?{??

len?=?[self.inputStream?read:buffer?maxLength:sizeof(buffer)];??

numberOfBytesReceived+=len;???

if?(len?>?0)?{??

NSString?*output?=?[[NSString?alloc]?initWithBytes:buffer??

length:len?encoding:NSASCIIStringEncoding];???

if?(output)?{??

[self?messageReceived:output];???

}??

}??

}??

}??

else?if?(theStream?==?self.dataInStream)?{??

uint8_t?buffer[8192];//8kB?block???

int?len;??

while?([self.dataInStream?hasBytesAvailable])?{???

len?=?[self.dataInStream?read:buffer??

maxLength:sizeof(buffer)];???

????????????????????numberOfBytesReceived+=len;??

if?(len?>?0)?{??

NSString?*output?=?[[NSString?alloc]?initWithBytes:buffer??

length:len???

encoding:NSASCIIStringEncoding];??

if?(output)?{??

[self?messageReceived:output];??

}???

}??

}???

}??

break;??

case?NSStreamEventHasSpaceAvailable:??

if?(theStream?==?self.dataOutStream)?{??

//write?your?custom?code?for?upload?and?download??

}???

break;??

case?NSStreamEventErrorOccurred:??

[self.delegate?ftpError:@"Network?stream?error?occured"];???

break;??

case?NSStreamEventEndEncountered:???

break;??

}???

}??

messageReceived方法是一個簡單的、用于追蹤最近一次來自服務端的響應鞭铆,使用一個整形或者一個代碼和消息或衡,你可以去RFC協(xié)議中查閱應答代碼。messageReceived:方法如清單8-21所示。

[objc]?view plain?copy

LISTING?8-21:?The?messageReceived:?method??

-?(void)?messageReceived:(NSString?*)message?{???

self.lastResponseCode?=?[message?substringToIndex:3];???

self.lastResponseMessage=message;??

int?response?=?[_lastResponseCode?intValue];???

self.lastResponseInt=response;??

[self.delegate?serverResponseReceived:???

self.lastResponseCode?message:_lastResponseMessage];??

switch?(response)?{???

case?150:??

//connection?accepted?break;??

case?200:??

[self?sendCommand:@"PASV"];??

case?220:?//server?welcome?message?so?wait?for?username??

[self?sendUsername];??

break;???

case?226:??

//transfer?OK?break;??

case?227:??

[self?acceptDataStreamConfiguration:message];???

break;??

case?230:?//server?logged?in???

self.loggedOn=YES;??

[self?sendCommand:@"PASV"];??

[self.delegate?loggedOn];?break;??

case?331:?//server?waiting?for?password???

[self?sendPassword];??

break;??

case?530:?//Login?or?passwod?incorrect??

[self.delegate?logginFailed];???

self.loggedOn=NO;??

break;??

default:???

break;??

}???

}??

代碼中應注意到封断,出于安全目的斯辰,大多數(shù)客戶端使用被動連接模式诞仓。因這個原因墩剖,當發(fā)送PASV命令到FTP服務端時怀薛,應答會包含將建立的岁歉、用于服務端和客戶端數(shù)據(jù)交換的data socket的IP地址和端口號镀迂。

acceptDataStreamConfiguration方法負責使用正則表達式解析上述應答結果茬射。前四組數(shù)字代表IP地址折晦,后兩組用于創(chuàng)建端口號货抄。所以教沾,例如應答是a.b.c.d.x.y湖苞,端口號用如下公式計算:(x * 256) + y。acceptDataStreamconfiguration方法如清單8-22所示详囤。

[objc]?view plain?copy

LISTING?8-22:?acceptDataStreamConfiguration?method??

-(void)acceptDataStreamConfiguration:(NSString*)serverResponse???

{NSString?*pattern=??

@"([-\\d]+),([-\\d]+),([-\\d]+),([-\\d]+),([-\\d]+),([-\\d]+)";???

NSError?*error?=?nil;??

NSRegularExpression?*regex?=?[NSRegularExpression??

regularExpressionWithPattern:pattern?options:0??

error:&error];??

NSTextCheckingResult?*match?=?[regex?firstMatchInString:?serverResponse??

options:0?range:NSMakeRange(0,?[serverResponse?length])];??

self.dataIPAddress?=?[NSString?stringWithFormat:@"%@.%@.%@.%@",???

[serverResponse?substringWithRange:[match?rangeAtIndex:1]],??

[serverResponse?substringWithRange:[match?rangeAtIndex:2]],???

[serverResponse?substringWithRange:[match?rangeAtIndex:3]],??

[serverResponse?substringWithRange:[match?rangeAtIndex:4]]];??

self.dataPort?=?([[serverResponse?substringWithRange:??

[match?rangeAtIndex:5]]?intValue]?*?256)+???

[[serverResponse?substringWithRange:[match?rangeAtIndex:6]]?intValue];??

self.isDataStreamConfigured=YES;??

[self?openDataStream];??

}??

最后财骨,你需要一些生命周期管理的代碼在你的實現(xiàn)里,如清單8-23所示藏姐。openDataStream創(chuàng)建一個inputStream和outputStream并且使用scheduleInCurrentThread:方法將他們調(diào)度安排到當前runLoop隆箩。closeDataStream方法恰當?shù)仃P閉并且移除數(shù)據(jù)流。

[objc]?view plain?copy

LISTING?8-23:?openDataStream,?closeDataStream,?and?logoff?methods??

-(void)openDataStream???

{??

if?(self.isDataStreamConfigured?&&?!self.isDataStreamAvailable){???

CFReadStreamRef?readStream;??

CFWriteStreamRef?writeStream;??

CFStreamCreatePairWithSocketToHost(NULL,??

(__bridge?CFStringRef)self.dataIPAddress,???

self.dataPort,?&readStream,?&writeStream);??

self.dataInStream?=?(__bridge_transfer?NSInputStream?*)readStream;??

self.dataOutStream?=?(__bridge_transfer?NSOutputStream?*)writeStream;??

[self.dataInStream?setDelegate:self];??

[self.dataOutStream?setDelegate:self];??

[self?performSelector:@selector(scheduleInCurrentThread:)???

onThread:[[self?class]?networkThread]??

withObject:self.dataInStream?waitUntilDone:YES];??

[self?performSelector:@selector(scheduleInCurrentThread:)???

onThread:[[self?class]?networkThread]??

withObject:self.dataOutStream?waitUntilDone:YES];??

[self.dataInStream?open];???

[self.dataOutStream?open];???

self.isDataStreamAvailable=YES;??

}??

}???


-(void)closeDataStream??

?{??

if?(self.dataInStream.streamStatus?!=?NSStreamStatusClosed)?{??

[self.dataInStream?removeFromRunLoop:???

[NSRunLoop?currentRunLoop]??

forMode:NSDefaultRunLoopMode];???

self.dataInStream.delegate?=?nil;???

[self.dataInStream?close];??

}??

if?(self.dataOutStream.streamStatus?!=?NSStreamStatusClosed)?{??

[self.dataOutStream?removeFromRunLoop:???

[NSRunLoop?currentRunLoop]??

forMode:NSDefaultRunLoopMode];??

self.dataOutStream.delegate?=?nil;???

[self.dataOutStream?close];??

}??

}??



-(void)logoff?{??

[self?sendCommand:@"QUIT"];??

[self?closeDataStream];??

if?(self.inputStream.streamStatus?!=?NSStreamStatusClosed)??

{??

[self.inputStream?removeFromRunLoop:??

[NSRunLoop?currentRunLoop]??

forMode:NSDefaultRunLoopMode];??

self.inputStream.delegate?=?nil;???

[self.inputStream?close];??

}??

if?(_outputStream.streamStatus?!=?NSStreamStatusClosed)???

{??

[self.outputStream?removeFromRunLoop:???

[NSRunLoop?currentRunLoop]?forMode:NSDefaultRunLoopMode];??

self.outputStream.delegate?=?nil;???

[self.outputStream?close];??

}??

self.isConnected=NO;???

self.isDataStreamAvailable=NO;???

self.isDataStreamConfigured=NO;??

}??

運行FTP客戶端

??????????? 為使用你開發(fā)的FTP客戶端羔杨,了解一系列發(fā)送的FTP命令和處理相關FTP回應是非常重要的捌臊。

??????????? 你可以在http://www.w3.org/Protocols/rfc959找到完整的RFC說明文檔。

??????????? 當流打開兜材,服務端會回應以220應答(220 response)和某種歡迎消息理澎。假定你的FTP服務端不允許匿名連接,第一個期望的命令是USER命令后加用戶名曙寡。假如用戶名是正確的糠爬,服務端會回應331,表示等待回應一個密碼举庶。你使用PASS命令后加密碼來發(fā)送密碼执隧。

??????????? YDFTPClinet類中的sendUsername和sendPassword方法僅僅是說明發(fā)送用戶名和密碼的簡單包裝。服務端或回應230登陸成功户侥,或530登錄失敗镀琉。

??????????? 在登陸之后,直接向服務端發(fā)送一個PASV命令是個好習慣蕊唐,該命令告訴FTP服務端使用一個被動連接屋摔,被動連接比主動連接(一直是同樣的IP端口)更安全。一個被動連接從服務端配置的可用端口范圍中替梨,為數(shù)據(jù)流選擇和使用一個隨機端口钓试。

??????????? 你可以將這個YDFTPClient類添加到你第一章創(chuàng)建的應用框架中署尤。




總結

??????????? 在本章,你學到兩種實現(xiàn)使用流實現(xiàn)FTP客戶端的不同方法亚侠,使用你所學的技術曹体,你可以:

?? 創(chuàng)建到FTP客戶端的流連接

?? 從FTP服務端下載或向其上傳文件

?? 發(fā)送原生FTP命令到FTP服務端并處理響應

代碼下載:

http://media.wiley.com/product_ancillary/33/11186611/DOWNLOAD/ch08.zip

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市硝烂,隨后出現(xiàn)的幾起案子箕别,更是在濱河造成了極大的恐慌,老刑警劉巖滞谢,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件串稀,死亡現(xiàn)場離奇詭異,居然都是意外死亡狮杨,警方通過查閱死者的電腦和手機母截,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來橄教,“玉大人清寇,你說我怎么就攤上這事』さ” “怎么了华烟?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長持灰。 經(jīng)常有香客問我盔夜,道長,這世上最難降的妖魔是什么堤魁? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任喂链,我火速辦了婚禮,結果婚禮上妥泉,老公的妹妹穿的比我還像新娘椭微。我一直安慰自己,他們只是感情好涛漂,可當我...
    茶點故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布赏表。 她就那樣靜靜地躺著检诗,像睡著了一般匈仗。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上逢慌,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天悠轩,我揣著相機與錄音,去河邊找鬼攻泼。 笑死火架,一個胖子當著我的面吹牛鉴象,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播何鸡,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼纺弊,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了骡男?” 一聲冷哼從身側響起淆游,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎隔盛,沒想到半個月后犹菱,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡吮炕,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年腊脱,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片龙亲。...
    茶點故事閱讀 39,779評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡陕凹,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出鳄炉,到底是詐尸還是另有隱情捆姜,我是刑警寧澤,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布迎膜,位于F島的核電站泥技,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏磕仅。R本人自食惡果不足惜珊豹,卻給世界環(huán)境...
    茶點故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望榕订。 院中可真熱鬧店茶,春花似錦、人聲如沸劫恒。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽两嘴。三九已至丛楚,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間憔辫,已是汗流浹背趣些。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留贰您,地道東北人坏平。 一個月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓拢操,卻偏偏與公主長得像,于是被迫代替她去往敵國和親舶替。 傳聞我的和親對象是個殘疾皇子令境,可洞房花燭夜當晚...
    茶點故事閱讀 44,700評論 2 354

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