本章有哪些内容?
Ø 理解文件传输协议
Ø 开发一个简单FTP客户端
Ø 实现网络流(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.
在第七章,你已经学习了网络部分,包括从web服务器下载文件,向RESTful和SOAP服务器发送post请求。在某些情况下,你可能还需要处理视频之类的大文件,下载到你的应用或者上传到服务器进行处理。
你可以将图片或者PDF文件编码成base64字符串,然后发送给服务器,但是这不是十分高效、迅速的方法。创建base64字符串需要花费很长时间,然后你仍然要传输这些数据。
文件传输协议FTP是一个用于FTP服务器和客户端之间进行通信的协议。
为IOS应用开发一个FTP客户端的同时,你应当了解一些FTP协议的基本要素。客户端使用预先确定端口上的Internet连接来创建到服务器的连接。默认地,一个FTP系统配置使用以下端口:
Ø 端口20用于服务器创建到客户端的连接
Ø 端口21用于客户端创建到服务器的连接注
译注:控制连接以通常的客户服务器方式建立。服务器以被动方式打开众所周知的用于F T P 的端口(21),等待客户的连接。客户则以主动方式打开TCP端口21,来建立连接。控制连接始终等待客户与服务器之间的通信。该连接将命令从客户传给服务器, 并传回服务器的应答。
FTP服务器的配置可以有所不同,所以你同意FTP服务器管理员关于使用那个端口就很重要了。
除了对某个端口号用于建立网络流达成一致外,FTP协议预定义了交换的命令,因此客户端可以告诉服务器它需要什么,反过来也一样。最常用的命令包括:
Ø open:打开一个连接
Ø close:关闭一个连接
Ø get:从服务器拷贝一个文件到客户端(下载)
Ø mget:从服务器拷贝多份文件到客户端(下载)
Ø put:从客户端拷贝一个文件到服务端(上传)
Ø mput:从客户端拷贝多份文件到服务端(上传)
Ø delete:从当前远端目录删除一个文件
Ø cd:改变服务器上的目录(由客户端发起)
Ø lcd:改变客户端上的目录
Ø mkdir或者mkd:在服务端创建一个目录(由客户端发起)
如果你以前从来没用过FTP客户端,你可以从http://filezilla-project.org/下载FileZilla项目。FileZilla是一个开源的FTP客户端和服务端,可用于分析在客户端和服务端两者之间交换的特定的命令。
根据你应用的需求,你有两个基本的选项要考虑。如果你仅仅是从FTP服务端上传或者下载文件,你可以使用由CFNetwork提供的高层API。这个方案能力有限,确定地说不是一个完整的FTP客户端。
启动Xcode并创建一个使用Single View ApplicationProject模板的工程,命名为SimpleFTPClinet,使用表8-1所示选项。
将CFNetwork framework添加到你的工程。
创建一个继承自NSObject、命名为FTPManager的类。打开FTPManager.h
文件,使用“#includes<CFNetwork/CFNetwork.h>”将CFNetworkframework包含进去。
接着定义FTPManagerDelegate协议,该协议包括一些将向delegate提供反馈的方法。
创建一个接受用于FTP连接的server, username, 以及password的初始化器。实现方面假定你总是需要一个username和password来建立FTP连接。因为打开一个公众的FTP服务端对于你应用的意图来说毫无意义。
接下去,声明四个方法用于与FTP命令相关的、受支持的操作。如表8—1所示。
最后,为该类的delegate创建一个共有property。
完整的代码如清单8-1所示。
[objc] view plain copy LISTING 8-1: Chapter8/SimpleFTPClient/FTPManager.h #import <Foundation/Foundation.h> #include <CFNetwork/CFNetwork.h> enum { kSendBufferSize = 32768 }; @protocol FTPManagerDelegate <NSObject> -(void)ftpUploadFinishedWithSuccess:(BOOL)success; -(void)ftpDownloadFinishedWithSuccess:(BOOL)success; - -(void)directoryListingFinishedWithSuccess:(NSArray *)arr; -(void)ftpError:(NSString *)err; @end @interface FTPManager : NSObject<NSStreamDelegate> - (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<FTPManagerDelegate> @end打开FTPManager.m文件,在import头文件语句后写入私有接口。
因为你在为网络通信使用流(streams),所以你要用到两种不同的流:NSOutputStream和NSInputStream。
这时候,理解FTP的put和mkd是你发送给服务端的命令,因此要用到NSOutputStream是很重要的。List和get命令通过打开的socket传送,并且作为回应,将收到你请求的数据。你需要去读取这些数据,因此你将用到NSInputStream。
定义两个BOOL型properties(isReceiving 和isSending)用于跟踪流的状态,避免流操作间的数据混淆。
你可以在本章的下载中找到FTPManager的完整实现。
这里,TPManager.m的实现被分解开来,并且一步一步解释。第一步,用自定义初始化器初始化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; }流处理的一个潜在问题是锁住。一个正在从一个流里写入或者读取的线程也许不得不无限期等待直到流里有空间进行写入,或者流里有可用数据(bytes)进行读取。为了克服这个问题,你需要一个接受NSStream作为参数的方法,并将它加入到当前NSRunLoop中进行调度安排(schedule),那样delegate就能只收到流相关事件报告的信息,那时锁住就不会发生。为了这个目的,写一个如清单8-3所示的简单helper方法。
NSRunLoop类声明了面向管理输入资源的对象的编程接口。一个NSRunLoop对象处理输入资源,例如窗口系统的鼠标和键盘事件,NSPort和NSConnetion对象。一个NSRunLoop对象也处理NSTimer事件。
你的应用既不能创建,也不能显式地管理NSrunLoop对象。每个NSThread对象,包括应用的主线程,出于需要,都有一个自动创建的NSRunLoop对象。假如你要访问当前线程的runloop,你使用类方法currentRunLoop。
[objc] view plain copy LISTING 8-3: The scheduleInCurrentThread method - (void)scheduleInCurrentThread:(NSStream*)aStream { [aStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; }smartURLForString:方法接受一个字符串并将它转化为一个有效的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是否已初始化。这些方法用于网络通信中以避免在一条命令尚在处理中又执行另一条命令的情况。 这两个方法在清单8-5中。 [objc] view plain copy <span style="font-family:'AvenirLTStd';font-size: 8.000000pt; font-weight: 800">LISTING 8-5: </span><span style="font-family:'AvenirLTStd';font-size: 10.000000pt; font-weight: 500">The isReceiving and isSending methods </span>- (BOOL)isReceiving { return (_dataStream != nil); } - (BOOL)isSending { return (_commandStream != nil); } [objc] view plain copy <span style="font-family:'AvenirLTStd';font-size: 8.000000pt; font-weight: 800">LISTING 8-6: </span><span style="font-family:'AvenirLTStd';font-size: 10.000000pt; font-weight: 500">The closeAll metho </span>-(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 = @""; }你调用downloadRemoteFile:localFileName:方法,并传给它一个服务端文件的文件名,例如picture1.png,和一个本地文件名,来从FTP服务端下载一个文件。已下载的文件将会以传入的本地文件名命名,写入到你的应用的根目录下的临时目录。
该方法先用“FTP服务端地址/远程文件名”生成一个smartURLForString,返回NSURL对象,例如ftp://127.0.0.1/picture1.png.假如isReceiving方法返回YES,代理方法ftpError被调用,并传入一个错误信息。否则,用已创建的路径初始化downloadStream。downloadStream被创建后,数据就可以从该流中读取,currentOperation property设置为GET。流代理(stream delegate)负责从不同的流中读取和写入数据,需要为GET命令(从流中读取数据,写入到一个文件)LIST命令(以流形式传送将要分析的目录列表)进行不同的操作。出于这个目的,你需要一个currentOperation的property。
注意:你应当使用FTP服务端地址代替127.0.0.1
使用CFBridgingRelease( CFReadStreamCreateWithFTPURL(NULL,
(__bridge CFURLRef)url));传入一个早先创建的url,创建一个commandStream。因为在使用认证,你需要将传给FTPManager初始化器的用户名和密码设置给commandStream。最后将delegate设置为self。调用scheduleInCurrentThread:将commandStream调度安排到当前线程,并打开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]; } }你可以使用createRemotedirectory:方法在服务端创建一个目录。当然你提供的用于建立连接的证书需要有权限创建目录。实现方法和刚才你学习的下载文件的例子很相似。使用CFURLCreateCopyAppendingPathComponent将请求创建的目录名拼接到已创建的URL后面,形成的新的URL类似ftp://127.0.0.1/newdirname。因为你在发送一个命令到FTP服务端,commandStream被创建而不是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:_commandStream waitUntilDone:YES]; [_commandStream open]; } }listRemoteDirectory方法等同于FTP的list命令,该方法列出服务端某个目录下的内容。尽管一些RFC适用于FTP程序,但是FTP list命令的结果正如你所希望的那样,不是100%标准化的,因此它可以很容易解析到一个数组里。
检查完你并没在dataStream上接收数据之后,你设置listDataproperty并初始化listEntries property。你将currentOperation property设置为LIST,以此区别GET和LIST正如前面解释的那样,调用CFReadStreamCreateWithFTPURL,使用创建的URL来初始化dataStream。
再次地,设置dataStream的认证信息并将它调度安排并打开。
为了能够解析LIST命令的结果,实现用于解析传入数据流并最终将结果写入listEntries property的三个helper方法。所有数据都被处理以后,directoryListingFinishedWithSuccess方法被调用,该方法返回一个数组,包含在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]; } } 下面的方法来自苹果公司的例子,并且已经清理过。因为从FTP服务端得到的响应并不是100%标准化的,所以你可以使用苹果公司提供的解析示例,该示例在大多数服务端实现上能工作,并呈现给你一个包含返回的文件信息的数组。解析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: (__bridge NSDictionary *) 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]; } }结果数组会提供类似如下的信息:
[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; } 数组中的每个实体包含一个字典,字典中元素含义解释如表8-2所示
为上传一个文件到FTP服务端,你可以使用uploadFileWithFilePath:方法,向其传送一个你想上传的文件的fileath。
你可以使用CFURLCreateCopyAppendingPathComponent在smartURLString后面拼接传入的filePath的lastPathComponent,以形成新的URL。
uploadStream使用filePathPath创建并打开,那样就能使用NSInputStream读取文件。接着调用CFWriteStreamCreateWithFTPURL函数,使用刚才创建的URL创建commandStream,并设置其认证信息。
commandStream调度安排到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:正是流通信魔法发生的地方。该实现首先使用switch语句来为各种可能发生的NSStreamEvent提供不同的处理。
你可以从清单8-12所示的实现中发现,假如eventCode是NSStreamEventHasBytesAvailable时,意味着有可读取的比特,你可以创建一个缓冲区,并从流中读取比特到缓冲区中。一旦所有的比特全都从流中读取完毕,根据当前操作,假如你正在执行listRemoteDirectory,你可以调用parseListData方法来解析结果,或者你正在下载一个文件,你可以调用parseListData方法来将它写到一个文件并存储。
当你想要写入一个NSStream,你需要检查事件是不是NSStreamEventHasSpaceAvailable,该事件仅在NSStream仍有可用空间用于写入时被调用。因为流被当前runloop所调度安排,所以当无可用写入空间时,线程不会锁住。现在你可以简单地从缓冲区向流写入比特。
为使用FTPManager,你要用FTP服务端的IP地址来初始化它,并且在viewDidLoad方法里提供可用的用户名和密码。
你实现你所要求的代理方法,创建你所需要的用户界面,在FTPManager实例中调用适当的函数,走你!
正如你已知的,你不应当在主线程运行网络操作。因此最好的方法是使用DISPATCH_QUEUE_PRIORITY_BACKGROUND在dispatch_queue里创建FTPManager,这样你的用户界面就不会被锁住。
一个实现例子如清单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你可以将创建的FTPManager类用于你第一章创建的应用框架,这样当你开发一个新应用时候它就可以用了。
使用你创建的FTPManager,你可以进行基本的操作,例如下载上传一个文件,在远程服务端端创建一个目录,取回一个目录的清单,就这些。假如你想对FTP过程有更多控制或者想进行本地操作,那你就需要一个不同的方案,该方案在更低层实现FTP协议。在本例子中你将学到如何写一个复杂的FTP客户端,给你FTP操作和应答处理的完全控制。
启动Xcode并创建一个Single ViewApplication Project模板的工程,命名为ComplexFTPClient使用如图8-2所示的选项。
作为第一步,再次添加CFNetwork到你的工程。
创建一个继承自NSObject的新类,命名为YDFTPClient。创建YDFTPClientDelegate协议和公开方法如清单8-15所示。
[objc] view plain copy LISTING 8-15: Chapter8/ComplexFTPClient/YDFTPClient.h #import <Foundation/Foundation.h> @protocol YDFTPClientDelegate <NSObject> -(void)logginFailed; -(void)loggedOn; -(void)serverResponseReceived:(NSString *)lastResponseCode message:(NSString *)lastResponseMessage; -(void)ftpError:(NSString *)err; @end @interface YDFTPClient : NSObject<NSStreamDelegate> @property (nonatomic, strong) id<YDFTPClientDelegate> delegate; @property (readonly) UInt64 numberOfBytesSent; @property (readonly) UInt64 numberOfBytesReceived; - (id)initClient; -(void)sendRAWCommand:(NSString *)command; -(void)connect; -(void)disconnect; @endYDFTPClient.m包括由它的变量定义的私有接口和不同方法的实现。像前面工程那样,YDFTPClient中的关键逻辑是流。你可以在本章的下载中找到完整的YDFTPClient.m实现。这不是所有FTP命令和应答的100%实现,但是是一个你可以通过实现你需要的命令来扩展的骨骼类(skeleton class)。
在YDFTPClient私有接口中创建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让我们来一步步分解该实现,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方法仅仅调用initNet-workCommunication:方法,disconnect方法调用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:方法调用,用于创建inputStream和outputStream,将它们安排调度到当前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:方法也在该实现中,是一个关键方法,包含了从流对象读取和写入的控制逻辑,你可以在清单8-20中看到。在NSStreamEventHasBytesAvailable事件中,你在缓冲区中读取来自流的比特,增加用于网络统计的numberOfBytesReceived property的值,发送读取输出到负责处理的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协议中查阅应答代码。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; } }代码中应注意到,出于安全目的,大多数客户端使用被动连接模式。因这个原因,当发送PASV命令到FTP服务端时,应答会包含将建立的、用于服务端和客户端数据交换的data socket的IP地址和端口号。
acceptDataStreamConfiguration方法负责使用正则表达式解析上述应答结果。前四组数字代表IP地址,后两组用于创建端口号。所以,例如应答是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]; }最后,你需要一些生命周期管理的代码在你的实现里,如清单8-23所示。openDataStream创建一个inputStream和outputStream并且使用scheduleInCurrentThread:方法将他们调度安排到当前runLoop。closeDataStream方法恰当地关闭并且移除数据流。
[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客户端,了解一系列发送的FTP命令和处理相关FTP回应是非常重要的。
你可以在http://www.w3.org/Protocols/rfc959找到完整的RFC说明文档。
当流打开,服务端会回应以220应答(220 response)和某种欢迎消息。假定你的FTP服务端不允许匿名连接,第一个期望的命令是USER命令后加用户名。假如用户名是正确的,服务端会回应331,表示等待回应一个密码。你使用PASS命令后加密码来发送密码。
YDFTPClinet类中的sendUsername和sendPassword方法仅仅是说明发送用户名和密码的简单包装。服务端或回应230登陆成功,或530登录失败。
在登陆之后,直接向服务端发送一个PASV命令是个好习惯,该命令告诉FTP服务端使用一个被动连接,被动连接比主动连接(一直是同样的IP端口)更安全。一个被动连接从服务端配置的可用端口范围中,为数据流选择和使用一个随机端口。
你可以将这个YDFTPClient类添加到你第一章创建的应用框架中。
在本章,你学到两种实现使用流实现FTP客户端的不同方法,使用你所学的技术,你可以:
Ø 创建到FTP客户端的流连接
Ø 从FTP服务端下载或向其上传文件
Ø 发送原生FTP命令到FTP服务端并处理响应