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);