CFNetwork框架详细解析(七) —— CFNetwork编程指导之使用FTP服务器(六)

版本记录

版本号 时间
V1.0 2018.06.09

前言

CFNetwork框架访问网络服务并处理网络配置的变化。 建立在网络协议抽象的基础上,可以简化诸如使用BSD套接字,管理HTTP和FTP服务器以及管理Bonjour服务等任务。接下来几篇我们就一起看一下这个框架。感兴趣的可以看上面几篇文章。
1. CFNetwork框架详细解析(一) —— 基本概览
2. CFNetwork框架详细解析(二) —— CFNetwork编程指导之简介(一)
3. CFNetwork框架详细解析(三) —— CFNetwork编程指导之CFNetwork概念(二)
4. CFNetwork框架详细解析(四) —— CFNetwork编程指导之流的处理(三)
5. CFNetwork框架详细解析(五) —— CFNetwork编程指导之与HTTP服务器通信(四)
6. CFNetwork框架详细解析(六) —— CFNetwork编程指导之与验证HTTP服务器通信(五)

Working with FTP Servers - 使用FTP服务器

本章介绍如何使用CFFTP API的一些基本功能。 管理FTP事务是异步执行的,而管理文件传输是同步执行的。


Downloading a File - 下载文件

使用CFFTP与使用CFHTTP非常相似,因为它们都基于CFStream。 与任何其他使用CFStream异步的API一样,使用CFFTP下载文件需要为该文件创建读取流,并为该读取流创建回调函数。 当读取流接收到数据时,回调函数将运行,您将需要适当地下载字节。 此过程通常应分两部分执行:一个用于设置流,一个用作回调函数。

1. Setting Up the FTP Streams - 设置FTP流

首先使用CFReadStreamCreateWithFTPURL函数创建读取流,并将要在远程服务器下载的文件的URL字符串传递给改函数。 一个URL字符串的例子可能是ftp://ftp.example.com/file.txt。 请注意,该字符串包含服务器名称,路径和文件。 接下来,为要下载文件的本地位置创建一个写入流。 这是通过使用CFWriteStreamCreateWithFile函数完成的,将文件将被下载的路径传给它。

由于写入流和读取流需要保持同步,因此创建一个包含所有常用信息的结构(如代理字典,文件大小,写入的字节数,剩下的字节和一个缓冲区。 这个结构可能如Listing 5-1所示

Listing 5-1  A stream structure

typedef struct MyStreamInfo {
 
    CFWriteStreamRef  writeStream;
    CFReadStreamRef   readStream;
    CFDictionaryRef   proxyDict;
    SInt64            fileSize;
    UInt32            totalBytesWritten;
    UInt32            leftOverByteCount;
    UInt8             buffer[kMyBufferSize];
 
} MyStreamInfo;

使用刚刚创建的读取流和写入流来初始化您的结构。然后,您可以定义流客户端上下文(CFStreamClientContext)info字段以指向您的结构。这将在稍后变得有用。

使用CFWriteStreamOpen函数打开您的写入流,以便您可以开始写入本地文件。为了确保流正确打开,调用函数CFWriteStreamGetStatus并检查它是否返回kCFStreamStatusOpenkCFStreamStatusOpening

在写入流打开的情况下,将回调函数与读取流相关联。调用函数CFReadStreamSetClient并传递读取流,回调函数应该接收的网络事件,回调函数的名称和CFStreamClientContext对象。通过早先设置流客户端上下文的info字段,您的结构现在将在运行时发送到您的回调函数。

某些FTP服务器可能需要用户名,有些可能还需要密码。如果您正在访问的服务器需要用户名进行身份验证,请调用CFReadStreamSetProperty函数并传递读取流kCFStreamPropertyFTPUserName作为属性,并引用包含用户名的CFString对象。另外,如果您需要设置密码,请设置kCFStreamPropertyFTPPassword属性。

某些网络配置也可能使用FTP代理。您可以通过不同的方式获取代理信息,具体取决于您的代码是否在OS X或iOS中运行。

这些函数返回一个动态存储引用。您可以使用此值设置读取流的kCFStreamPropertyFTPProxy属性。这将设置代理服务器,指定端口,并返回一个布尔值,指示是否为FTP流实施被动模式。

除了提到的属性之外,还有一些可用于FTP流的其他属性。完整的清单如下。

  • kCFStreamPropertyFTPUserName - 用于登录的用户名(可设置和可检索;不设置匿名FTP连接)
  • kCFStreamPropertyFTPPassword - 用于登录的密码(可设置和可检索;不设置匿名FTP连接)
  • kCFStreamPropertyFTPUsePassiveMode - 是否使用被动模式(可设置和可检索)
  • kCFStreamPropertyFTPResourceSize - 正在下载的项目的预期大小(如果可用)(可检索;仅适用于FTP读取流)
  • kCFStreamPropertyFTPFetchResourceInfo - 开始下载之前是否需要资源信息(如大小)(可设置和可检索);设置此属性可能会影响性能
  • kCFStreamPropertyFTPFileTransferOffset - 开始传输的文件偏移量(可设置和可检索)
  • kCFStreamPropertyFTPAttemptPersistentConnection - 是否尝试重用连接(可设置和可检索)
  • kCFStreamPropertyFTPProxy - 包含代理字典(可设置和可检索)的键值对的CFDictionary类型
  • kCFStreamPropertyFTPProxyHost - FTP代理主机的名称(可设置和可检索)
  • kCFStreamPropertyFTPProxyPort - FTP代理主机的端口号(可设置和可检索)

将正确的属性分配给读取流后,使用CFReadStreamOpen函数打开流。假设这不会返回错误,所有的流都已正确设置。

2. Implementing the Callback Function - 实现回调函数

您的回调函数将接收三个参数:读取流,事件类型和MyStreamInfo结构体。事件的类型决定了必须采取的行动。

最常见的事件是kCFStreamEventHasBytesAvailable,它在读取流从服务器接收到字节时发送。首先,通过调用CFReadStreamRead函数来检查已读取的字节数。确保返回值不小于零(一个错误),或等于零(下载已完成)。如果返回值为正值,则可以开始将读取流中的数据通过写入流写入磁盘。

调用CFWriteStreamWrite函数将数据写入写入流。有时CFWriteStreamWrite可以返回而无需从读取流中写入所有数据。出于这个原因,只要还有数据要写入,就建立一个循环来运行。这个循环的代码在Listing 5-2中,其中info是来自Setting up the StreamsMyStreamInfo结构。这种写入写入流的方法使用阻塞流。您可以通过驱动写入流事件来实现更好的性能,但代码更复杂。

Listing 5-2  Writing data to a write stream from the 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;

只要读取流中有可用的字节,就重复这整个过程。

另外两个需要监听的事件是kCFStreamEventErrorOccurredkCFStreamEventEndEncountered。 如果发生错误,请使用CFReadStreamGetError检索错误,然后退出。 如果文件结束,那么你的下载已经完成,你可以退出。

一切完成后,请确保删除所有流,并且没有其他进程正在使用流。 首先,关闭写入流并将客户端设置为NULL。 然后从运行循环中取消调度流并释放它。 完成后,从运行循环中移除流。


Uploading a File - 上传文件

上传文件与下载文件类似。与下载文件一样,您需要读取流和写入流。但是,上传文件时,读取流将用于本地文件,写入流将用于远程文件。按照Setting up the Streams中的说明进行操作,但无论它指向读取流的任何位置,将代码调整为写入流,反之亦然。

在回调函数中,而不是查找kCFStreamEventHasBytesAvailable事件,现在查找事件kCFStreamEventCanAcceptBytes。首先,使用读取流从文件中读取字节,并将数据放入MyStreamInfo的缓冲区中。然后,运行CFWriteStreamWrite函数将缓冲区中的字节推送到写入流中。CFWriteStreamWrite返回已写入流的字节数。如果写入流的字节数少于从文件读取的字节数,则计算剩余字节数并将其存回缓冲区。在下一个写周期期间,如果有剩余字节,请将它们写入写入流,而不是从读取流中载入新数据。只要写入流可以接受字节(CFWriteStreamCanAcceptBytes),就重复这整个过程。在Listing 5-3的代码中看到这个循环。

Listing 5-3  Writing data to the write stream

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

与下载文件时一样,也要考虑kCFStreamEventErrorOccurredkCFStreamEventEndEncountered事件


Creating a Remote Directory - 创建一个远程目录

要在远程服务器上创建目录,请设置写入流,就好像您要上传文件一样。 但是,为传递给CFWriteStreamCreateWithFTPURL函数的CFURL对象提供目录路径而不是文件。 用正斜杠结束路径。 例如,正确的目录路径是ftp://ftp.example.com/newDirectory/,而不是ftp://ftp.example.com/newDirectory/newFile.txt。 当回调函数由运行循环执行时,它会发送事件kCFStreamEventEndEncountered,这意味着该目录已经创建(或者如果出错,则为kCFStreamEventErrorOccurred)。

每次调用CFWriteStreamCreateWithFTPURL时,只能创建一级目录。 另外,只有在服务器上拥有正确的权限时才会创建目录。


Downloading a Directory Listing - 下载目录列表

通过FTP下载目录列表与下载或上传文件稍有不同。这是因为传入的数据必须被解析。首先,设置一个读取流来获取目录列表。这应该像下载文件一样完成:创建流,注册回调函数,使用运行循环调度流(如有必要,设置用户名,密码和代理信息),最后打开流。在下面的示例中,当检索目录列表时,不需要读取和写入流,因为传入的数据将进入屏幕而不是文件。

在回调函数中,请监听kCFStreamEventHasBytesAvailable事件。在从读取流中加载数据之前,请确保在上次运行回调函数时流中没有剩余数据。加载MyStreamInfo结构的leftOverByteCount字段的偏移量。然后,从流中读取数据,并考虑刚刚计算的偏移量。读取的缓冲区大小和字节数也应计算在内。这一切都在Listing 5-4中完成。

Listing 5-4  Loading data for a directory listing

// 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创建循环来解析数据,该函数应传递数据缓冲区,缓冲区大小和字典引用。它返回解析的字节数。只要这个值大于零,就继续循环。CFFTPCreateParsedResourceListing创建的字典包含所有的目录列表信息;有关密钥的更多信息可在Setting up the Streams中找到。

CFFTPCreateParsedResourceListing可能会返回正值,但不会创建解析字典。例如,如果列表的末尾包含无法分析的信息,则CFFTPCreateParsedResourceListing将返回一个正值以告知调用方数据已被使用。但是,CFFTPCreateParsedResourceListing不会创建解析字典,因为它无法理解数据。

如果创建了解析字典,请重新计算读取的字节数和缓冲区大小,如Listing 5-5所示。

Listing 5-5  Loading the directory listing and parsing it

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

当流没有更多可用字节时,清理所有流并将它们从运行循环中删除。

后记

本篇主要讲述了使用FTP服务器,感兴趣的给个赞或者关注~~~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容