Working with FTP Servers
这篇文章介绍如何使用CFFTP API的一些基本特点来进行管理FTP异步传输、同时管理文件的同步传输。
1、下载一个文件(Downloading a File)
使用CFFTP和CFHTTP是非常相似的,因为他们都是基于CFStream。用CFFTP进行文件下载请求你需要创建一个read stream和回调一个关于read stream的方法。当创建的read stream接受到数据,这个回调函数将会被执行下载字节。这个步骤通常由两个函数完成:一个设置stream,一个作为回调方法。
1.1、设置FTP流(Setting Up the FTP Streams)
用CFReadStreamCreateWithFTPURL方法创建一个read stream,传一个要从远程服务器下载的文件URL 字符串。例如URL 字符串:ftp://ftp.example.com/file.txt. 。注意这个字符串包含了服务器名、路径、文件。接下来在本地为下载的文件创建一个wirte stream,这步用CFWriteStreamCreateWithFile 方法,传被下载文件存放的路径。
由于write stream和read stream需要保持同步,有个好的办法是创建一个包涵所有共同信息的数据结构,如代理字典,文件大小,写入字节数,剩下的字节数,和一个缓冲区。如下:
typedef struct MyStreamInfo {
CFWriteStreamRef writeStream;
CFReadStreamRef readStream;
CFDictionaryRef proxyDict;
SInt64 fileSize;
UInt32 totalBytesWritten;
UInt32 leftOverByteCount;
UInt8 buffer[kMyBufferSize];
} MyStreamInfo;
在创建这个结构的时候用read stream和write stream来初始化,然后你可以定义你的数据流的客户端上下文信息字段(CFStreamClientContext)指向你的结构。这将成为有用的后。
用CFWriteStreamOpen函数打开write stream,你可以将它写到本地文件。为了确保这个stream能够正确的打开,调用CFWriteStreamGetStatus函数检查是否返回kCFStreamStatusOpen 或 kCFStreamStatusOpening。
随着write stream打开,将一个回调函数与读流关联起来。通过read stream调用CFReadStreamSetClient方法,网络事件的回调函数应该接受回调函数的名称和CFStreamClientcontext对象,通过较早设置数据流客户端上下文的信息字段,您的结构将在运行时发送给您的回调函数。
一些FTP服务器可能需要用户名和密码,如果你访问的服务器需要用户名来授权,通过read stream调用CFReadStreamSetProperty方法,为kCFStreamPropertyFTPUserName属性设置一个包涵了用户名的CFString类型对象。除此之外,如果你需要设置密码,需设置kCFStreamPropertyFTPPassword属性。
一些网络配置可能用了FTP代理,你用不同的方式获取代理信息取决于你的代码运行在OS X还是iOS之上。
在OS X上,你可以通过给SCDynamicStoreCopyProxies方法传递NULL,在返回的字典里面检索代理设置。
在iOS,你可以通过调用CFNetworkCopyProxiesForURL方法来检索代理设置。
这些函数返回一个动态存储参考。你可以用这个值来设置read stream的kCFStreamPropertyFTPProxy属性。这台代理服务器,指定端口,并返回一个布尔值,指示被动模式是否被强制执行FTP流。
除了提到的属性之外,下面是一些对于FTP流很有用的其它属性。完整的列表如下:
• kCFStreamPropertyFTPUserName — 用户登录名 (可以设置和检索,不要为匿名FTP连接设置)
• kCFStreamPropertyFTPPassword — 用户密码 (可以设置和检索,不要为匿名FTP连接设置)
• kCFStreamPropertyFTPUsePassiveMode — 是否设置被动模式 (可以设置和检索)
• kCFStreamPropertyFTPResourceSize — 希望被下载的文件大小(可以检索,只对FTP读数据流有效)
• kCFStreamPropertyFTPFetchResourceInfo — whether to require that resource information, such as size, be required before starting a download (settable and retrievable); setting this property may impact performance
• kCFStreamPropertyFTPFileTransferOffset — file offset at which to start a transfer (settable and retrievable)
• kCFStreamPropertyFTPAttemptPersistentConnection — whether to try to reuse connections (settable and retrievable)
• kCFStreamPropertyFTPProxy — CFDictionary类型的键值对
• kCFStreamPropertyFTPProxyHost — FTP代理主机名
• kCFStreamPropertyFTPProxyPort — FTP代理主机端口号
正确的属性被分配到读取流,使用CFReadstreamopen函数打开流。如果不返回一个错误,那么所有的流都被正确地设置了。
1.2、实现回调方法(Implementing the Callback Function)
你的回调方法接收三个参数:read stream ,事件类型,上述的MyStreamInfo结构,事件类型决定了做什么事情,
最基本的事件kCFStreamEventHasBytesAvailable,当read stream接收到来自服务器的字节时被触发。首先,调用CFReadStreamRead方法检查要读的字节数大小,确保返回值不小于(错误)或者等于(下载完成)0,如果返回一个正数,你可以开始把在read stream里面的数据通过write stream写进磁盘。
调用CFWriteStreamWrite方法写数据到write stream,有时CFWriteStreamWrite可能没有把read stream里面的数据全部写入就返回了,鉴于这个原因,设置一个循环,只要数据还在被写入就运行,循环代码如下:
bytesRead = CFReadStreamRead(info->readStream, info->buffer, kMyBufferSize);
//...make sure bytesRead > 0 ...
bytesWritten = 0;
while (bytesWritten < bytesRead) {
CFIndex result;
result = CFWriteStreamWrite(info->writeStream, info->buffer + bytesWritten, bytesRead - bytesWritten);
if (result <= 0) {
fprintf(stderr, "CFWriteStreamWrite returned %ld\n", result);
goto exit;
}
bytesWritten += result;
}
info->totalBytesWritten += bytesWritten;
这个方法用了block stream来写入write stream,你可以通过做write stream的事件驱动实现更好的性能,但是代码更复杂。重复整个流程只要在read stream的字节是有效的。
其它的有两个事件你需要注意kCFStreamEventErrorOccurred 和kCFStreamEventEndEncountered.如果一个错误出现了,用CFReadStreamGetError方法来检测这个错误然后退出,如果出现在文件的末尾,那么你的下载已经完成了,你可以退出。
确保在所有事情完成和没有其它进程使用stream之后移除所有的stream,首先关闭write stream和设置客户端为null,然后从run loop取消预定stream并释放它,移除stream从run loop。
2、上传文件(Uploading a File)
上传文件和下载文件是相似的,和下载文件一样,你需要一个read stream和write stream,然而,当上传一个文件的时候,read stream对应的是本地文件,write stream对应远程文件。
在回调函数中,不是查看kCFStreamEventHasBytesAvailable事件,现在观察的是kCFStreamEventCanAcceptBytes事件。首先,用read stream从一个本地文件读取字节,然后把数据放在MyStreamInfo的缓冲里面,运行CFWriteStreamWrite方法把缓冲里面的数据放到write stream,CFWriteStreamWrite方法返回已经被写入到stream的字节数,如果被写的字节数小于从文件中读取的,计算剩余字节并将它们存回缓冲区,在下一个写循环期间,如果有剩余的字节,将他们写入到write stream而不是从read stream里面载入新的数据。只要write stream能够接受字节(aCFWriteStreamCanAcceptBytes)就重复整个流程,下面是循环代码:
do {
// Check for leftover data
if (info->leftOverByteCount > 0) {
bytesRead = info->leftOverByteCount;
} else {
// Make sure there is no error reading from the file
bytesRead = CFReadStreamRead(info->readStream, info->buffer,
kMyBufferSize);
if (bytesRead < 0) {
fprintf(stderr, "CFReadStreamRead returned %ld\n", bytesRead);
goto exit;
}
totalBytesRead += bytesRead;
}
// Write the data to the write stream
bytesWritten = CFWriteStreamWrite(info->writeStream, info->buffer, bytesRead);
if (bytesWritten > 0) {
info->totalBytesWritten += bytesWritten;
// Store leftover data until kCFStreamEventCanAcceptBytes event occurs again
if (bytesWritten < bytesRead) {
info->leftOverByteCount = bytesRead - bytesWritten;
memmove(info->buffer, info->buffer + bytesWritten,
info->leftOverByteCount);
} else {
info->leftOverByteCount = 0;
}
} else {
if (bytesWritten < 0)
fprintf(stderr, "CFWriteStreamWrite returned %ld\n", bytesWritten);
break;
}
} while (CFWriteStreamCanAcceptBytes(info->writeStream));
还要考虑到kCFStreamEventErrorOccurred 和 kCFStreamEventEndEncountered事件,就像下载文件时一样。
3、创建一个远程目录(Creating a Remote Directory)
为了在远程服务器上创建一个目录,设置write stream就像要上传文件一样,然而,需要提供一个目录路径,不是一个文件,将一个CFURL对象传入到CFWriteStreamCreateWithFTPURL方法,以正斜杠结束路径,例如,一个正确的目录路径是:ftp://ftp.example.com/newDirectory/ 而不是ftp://ftp.example.com/newDirectory/newFile.txt。当回调函数被run loop执行的时候,它发送kCFStreamEventEndEncountered事件,意味着这个目录已经被创建了。
每次调用CFWriteStreamWithFTPURL时,只有一级目录能够被创建。 此外,只有在服务器上你有正确的权限才能创建目录.
4、下载目录列表(Downloading a Directory Listing)
通过FTP下载一个目录列表和上传、下载文件稍微的不同,这是因为传入的数据不得不解析。首先,为了获取目录列表设置一个read stream,下载文件应该要做的:创建一个stream,注册回调方法,把stream放入run loop(如果必要,设置用户名和密码,代理信息),最后打开stream,在下面的示例中,当检索目录列表时,不需要read stream和write stream,因为传入的数据将要在屏幕上,而不是一个文件。
在回调方法里面,检测kCFStreamEventHasBytesAvailable事件,在从read stream载入数据之前,确保上一次运行的回调函数中的stream没有剩余的数据。从MyStreamInfo结构里的leftoverbytecount加载偏移,然后从stream读取数据,考虑到你刚才计算的偏移量,缓冲去的大小和读取的字节数也需要重新计算,计算代码:
// If previous call had unloaded data
int offset = info->leftOverByteCount;
// Load data from the read stream, accounting for the offset
bytesRead = CFReadStreamRead(info->readStream, info->buffer + offset,
kMyBufferSize - offset);
if (bytesRead < 0) {
fprintf(stderr, "CFReadStreamRead returned %ld\n", bytesRead);
break;
} else if (bytesRead == 0) {
break;
}
bufSize = bytesRead + offset;
totalBytesRead += bufSize;
在读取了一个缓冲的数据,设置一个循环来解析数据,解析的数据不一定是整个目录列表,可能是列表的大部分,创建一个循环利用CFFTPCreateParsedResourceListing方法解析数据,需要传入缓冲数据,缓冲大小,一个参考目录,它返回被解析的字节数,只要这个值大于0就继续循环,CFFTPCreateParsedResourceListing创建的目录包含了所有的列表信息。
可能CFFTPCreateParsedResourceListing函数返回了一个正数,但是不能创建解析目录,例如,如果列表的结尾包含无法解析的信息,CFFTP CreateParsedResourceListing将返回一个正值,告诉调用者数据已被解析完成。然而,CFFTPCreateParsedResourceListing不能创建一个解析路径因为它不能明白这个数据。
如果解析路径被创建,重新计算读的字节数和缓冲的大小:
do
{
bufRemaining = info->buffer + totalBytesConsumed;
bytesConsumed = CFFTPCreateParsedResourceListing(NULL, bufRemaining,
bufSize, &parsedDict);
if (bytesConsumed > 0) {
// Make sure CFFTPCreateParsedResourceListing was able to properly
// parse the incoming data
if (parsedDict != NULL) {
// ...Print out data from parsedDict...
CFRelease(parsedDict);
}
totalBytesConsumed += bytesConsumed;
bufSize -= bytesConsumed;
info->leftOverByteCount = bufSize;
} else if (bytesConsumed == 0) {
// This is just in case. It should never happen due to the large buffer size
info->leftOverByteCount = bufSize;
totalBytesRead -= info->leftOverByteCount;
memmove(info->buffer, bufRemaining, info->leftOverByteCount);
} else if (bytesConsumed == -1) {
fprintf(stderr, "CFFTPCreateParsedResourceListing parse failure\n");
// ...Break loop and cleanup...
}
} while (bytesConsumed > 0);