基于IOS的FTP详解(一)获取列表

项目中有用到ftp对远程文件进行管理,这里整理一下,主要是通过CFNetwork来实现的。

首先对ftp进行一个小得概述,后面会通过抓包工具进行详细的讲述请求过程:

FTP地址格式:

无密码:

ftp://<服务器地址>

有密码:

ftp://<login>:<password>@<ftpserveraddress>

ftp采用两个TCP连接来传输一个文件,一个是控制连接,一个是数据连接,如下图所示:

基于IOS的FTP详解(一)获取列表_第1张图片

通常Unix实现的ftp客户和服务器使用流方式传输

我们通过终端登录到FTP服务端以后会返回如下信息:

230 Login successful.

Remote system type is UNIX.

Using binary mode to transfer files.


下面我们开始具体的实现:

Downloading a directory listing via FTP is slightly different from downloading or uploading a file. This is because the incoming data has to be parsed. First, set up a read stream to get the directory listing. This should be done as it was for downloading a file: create the stream, register a callback function, schedule the stream with the run loop (if necessary, set up user name, password and proxy information), and finally open the stream. In the following example you do not need both a read and a write stream when retrieving the directory listing, because the incoming data is going to the screen rather than a file.

In the callback function, watch for the kCFStreamEventHasBytesAvailable event.

通过实现来进行讲解:

-(void)start
{
    self.listData = [[NSMutableData alloc]init];
    NSURL *url = [NSURL URLWithString:directoryStr];
    readStream = CFReadStreamCreateWithFTPURL(NULL, (__bridge CFURLRef)url);
    
    CFStreamClientContext clientContext;
    clientContext.version = 0;
    clientContext.info = CFBridgingRetain(self) ;//注解1
    clientContext.retain = nil;
    clientContext.release = nil;
    clientContext.copyDescription = nil;
    if (CFReadStreamSetClient (readStream,
                               kCFStreamEventOpenCompleted |
                               kCFStreamEventHasBytesAvailable |
                               kCFStreamEventCanAcceptBytes |
                               kCFStreamEventErrorOccurred |
                               kCFStreamEventEndEncountered,
                               mySocketReadCallBack,   //注解2
                               &clientContext ) )
    {
        NSLog(@"Set read callBack Succeeded");
        CFReadStreamScheduleWithRunLoop(readStream,    //注解3
                                        CFRunLoopGetCurrent(),
                                        kCFRunLoopCommonModes);
    }
    else
    {
        NSLog(@"Set read callBack Failed");
    }
    
    BOOL success = CFReadStreamOpen(readStream); //注解4
    if (!success) {
        printf("stream open fail\n");
        return;
    }

}


注解1:因为这里是用c来实现的,ARC下面必须用CFBridgingRetain来将引用计数加1,否则会出现僵尸对象;
注解2:对前面一个参数设置的标示事件发生的时候设置的回调函数,后面那个参数是上下文,将你关心的对象传给这个回调函数;
注解3:将输入流加入到当前的运行时循环,这样在所关心的时间发生的时候,就会触发回调;

注解4:打开输入流,这里相当于打开一个TCP连接。


#define BUFSIZE 32768
void mySocketReadCallBack (CFReadStreamRef stream, CFStreamEventType event, void *myPtr)
{
    JUNFTPListRequest* request = (__bridge JUNFTPListRequest *)myPtr;
    
    switch(event)
    {
        case kCFStreamEventHasBytesAvailable:
        {
            UInt8 recvBuffer[BUFSIZE];
            
            CFIndex bytesRead = CFReadStreamRead(stream, recvBuffer, BUFSIZE);

            printf("bytesRead:%ld\n",bytesRead);
            if (bytesRead > 0)
            {
                [request.listData appendBytes:recvBuffer length:bytesRead];
            }
            else if(bytesRead == 0)
            {
                [request parseListData];
                [request stop];
            }
            else
            {
                request.failBlock();
            }
        }
            break;
        case kCFStreamEventErrorOccurred:
        {
            CFStreamError error = CFReadStreamGetError(stream);
            printf("kCFStreamEventErrorOccurred-%d\n",error.error);
            
            [request stop];
            request.failBlock();
        }
            break;
        case kCFStreamEventEndEncountered:
            printf("request finished\n");
            [request stop];
            break;
        default:
            break;
    }
}

当kCFStreamEventHasBytesAvailable事件发生的时候表示套接字有可读的数据到达,因此我们调用CFReadStreamRead方法将数据读到缓冲,当我们读到bytesRead == 0的时候就表示对方文件或者数据已经传送完成,关闭当前连接,因为TCP是字节流,没有任何记录边界,关闭连接的时候会发送一个FIN分节来表示文件结束符(end-of-file)因此我们就可以解析数据了,这里是基于TCP的短连接。


这里要注意最后我们要释放掉通过c语言创建的对象,ARC不负责这个,还要将指针指向空,否则会留下一个野指针;

-(void)stop
{
    CFReadStreamUnscheduleFromRunLoop(readStream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
    CFReadStreamClose(readStream);
    CFRelease(readStream);
    readStream = nil;
    
}


After the data has been read to a buffer, set up a loop to parse the data. The data that is parsed is not necessarily the entire directory listing; it could (and probably will) be chunks of the listing. Create the loop to parse the data using the functionCFFTPCreateParsedResourceListing, which should be passed the buffer of data, the size of the buffer, and a dictionary reference. It returns the number of bytes parsed. As long as this value is greater than zero, continue to loop. The dictionary thatCFFTPCreateParsedResourceListing creates contains all the directory listing information.

It is possible for CFFTPCreateParsedResourceListing to return a positive value, but not create a parse dictionary. For example, if the end of the listing contains information that cannot be parsed, CFFTPCreateParsedResourceListing will return a positive value to tell the caller that data has been consumed. However, CFFTPCreateParsedResourceListing will not create a parse dictionary since it could not understand the data.

-(void)parseListData
{
    CFIndex         bytesConsumed;
    CFIndex         totalBytesConsumed = 0;
    CFDictionaryRef parsedDict;
    NSMutableArray *array = [[NSMutableArray alloc]init];
    
    do
    {
        
        bytesConsumed = CFFTPCreateParsedResourceListing(NULL,
                                                         &((const uint8_t *)self.listData.bytes)[totalBytesConsumed],
                                                         self.listData.length-totalBytesConsumed,
                                                         &parsedDict);
        if (bytesConsumed > 0)
        {
            
            if (parsedDict != NULL) {
                [array addObject:(__bridge id)(parsedDict)];
                CFRelease(parsedDict);
            }
            
            totalBytesConsumed += bytesConsumed;
           
            
        }
        else if (bytesConsumed == 0)
        {
            
            break;
            
        }
        else if (bytesConsumed == -1)
        {
            fprintf(stderr, "CFFTPCreateParsedResourceListing parse failure\n");
            self.failBlock();
            return;
        }
        
    } while (1);
    
    self.finishedBlock([NSArray arrayWithArray:array]);
}

获取列表和下载文件不同,因为文件列表需要解析,它有固定的格式,这里通过 CFFTPCreateParsedResourceListing方法在循环中将列表解析并将每一条数据存放到字典里,每一项数据格式如下:

{

        kCFFTPResourceGroup = ftp;

        kCFFTPResourceLink = "";

        kCFFTPResourceModDate = "2013-12-24 16:00:00 +0000";

        kCFFTPResourceMode = 511;

        kCFFTPResourceName = "\U00c8\U00e6\U00f6\U00c1\U00e9\U2022 - \U00c8\U00a9\U00a8\U00c2\U00d6\U221e\U00cb\U00e4\U00b1.mp3";

        kCFFTPResourceOwner = ftp;

        kCFFTPResourceSize = 5250346;

        kCFFTPResourceType = 8;

 }


关于 kCFFTPResourceType的定义在 sys/dirent.h下其中

#define DT_DIR4

代表目录,那么我们可以根据这个来继续读取下一级列表

这里有个乱码的问题要解决,就是kCFFTPResourceName 获取的文件名,方法如下:

        NSString *      name;
        NSData *        nameData;
        name = [dic objectForKey:(id)kCFFTPResourceName];
        NSLog(@"string = %@",name);
        nameData = [name dataUsingEncoding:NSMacOSRomanStringEncoding];
        if (nameData != nil) {
            name = [[NSString alloc] initWithData:nameData encoding:NSUTF8StringEncoding];
        }
        NSLog(@"string1 = %@",name);


由于好奇我连续获取两个列表,然后用抓包工具查看了一下数据:

基于IOS的FTP详解(一)获取列表_第2张图片

基于IOS的FTP详解(一)获取列表_第3张图片


发现一个问题就是,服务器的21端口只负责控制命令,就像上面图中显示的,发送第一个列表请求的时候首先客户端51664和服务端的21端口通过三次握手建立连接,然后是依次用户名,密码的命令,而每次客户端发送一个控制命令LIST的时候客户端首先会给21端口发送LIST控制命令,然后建立一个新的连接,观察发现端口号在变化,在新连接上面传输数据,数据传输完成以后服务端会关闭连接,下次有数据传输的时候会开启新的连接,51664这个端口和服务器21端口一直保持连接状态,直到我关掉app,51664才会发送FIN标志给服务端。

demo地址:http://download.csdn.net/detail/junjun150013652/7629007


参考:

TCP/IP协议详解

《CFNetwork Programming Guide:Working with FTP Servers》

你可能感兴趣的:(ios,ftp,网络编程,CFNetwork)