iOS网络编程

作者: FengyunSky | 来源:发表于2020-04-11 23:28 被阅读0次

    CFSocket

    CFSocketBSD socket的抽象和封装,提供了几乎所有socket的功能,并与run loop集成,用来实现多线程网络编程和网络事件监听;基于 CFSocket可以实现各种类型的 socket编程,包括stream-based 的sockets(如tcp)和packet-based 的sockets(如udp)。需要注意的是在iOS中CFSocket接口在需要时不自动激活设备的 cellular modem或on-demand VPN。

    具体的使用接口如下:

    /**** 创建 ******/
    //创建cfsocket对象
    //allocator,分配对象内存的类型,默认NULL或者kCFAllocatorDefault 选择默认分配类型,其他还有kCFAllocatorSystemDefault kCFAllocatorMalloc等
    //protocolFamily, 协议族类型,PF_INET(默认) 或者PF_INET6,如果为0或者负数,则为默认
    //socketType, socket类型,SOCK_STREAM(默认)或者SOCK_DGRAM,负数或者0,则默认
    //protocol, socket协议类型,IPPROTO_TCP(默认)或者IPPROTO_UDP,负数或者0,则默认
    //callBackTypes,回调触发类型,见CFSocketCallBackType,可以使用位或(|)组合类型,如kCFSocketConnectCallBack|kCFSocketAcceptCallBack
    //callout,回调函数
    //context, 保存CFSocket对象上下文信息的结构。函数将信息从结构中复制出来,因此上下文所指向的内存不需要在函数调用之外持久化。可以为空(NULL)
    /*
    typedef struct {
        CFIndex version; //版本号必须位0
        void *  info;//指向程序定义数据的任意指针,该数据可以在创建时与CFSocket对象关联。这个指针被传递给上下文中定义的所有回调
        const void *(*retain)(const void *info);//可为NULL
        void    (*release)(const void *info);//可为NULL
        CFStringRef (*copyDescription)(const void *info);//可为NULL
    } CFSocketContext;
    */
    CFSocketRef CFSocketCreate(CFAllocatorRef allocator, 
                               SInt32 protocolFamily, 
                               SInt32 socketType, 
                               SInt32 protocol, 
                               CFOptionFlags callBackTypes, 
                               CFSocketCallBack callout, 
                               const CFSocketContext *context);
                                                                                                    //返回CFSocketRef对象或者NULL(失败)
    //该接口根据一个包含通讯协议和地址的CFSocketSignature结构来创建一个CFSocket对象
    /*
    和CFSocketCreate()函数类似,只不过使用const CFSocketSignature *signature参数来代替:protocolFamily、socketType、protocol
    typedef struct {
        SInt32  protocolFamily;
        SInt32  socketType;
        SInt32  protocol;
        CFDataRef   address;//用于标示socket的地址,通过sockaddr转换的CFDataRef对象
    } CFSocketSignature
    */
    CFSocketRef CFSocketCreateWithSocketSignature(CFAllocatorRef allocator, const CFSocketSignature *signature, CFOptionFlags callBackTypes, CFSocketCallBack callout, const CFSocketContext *context)
    //该接口在创建一个CFSocket对象的同时还与一个远端主机进行连接
    CFSocketRef CFSocketCreateConnectedToSocketSignature()
    //该接口通过封装一个存在的 BSD socket来创建一个CFSocket对象
    CFSocketRef CFSocketCreateWithNative()
    
    //绑定套接字地址并监听,默认256监听连接数
    CFSocketError   CFSocketSetAddress(CFSocketRef s, CFDataRef address);
    //指定连接服务器并连接,若设定超时时间>0并且创建socket时指定了kCFSocketConnectCallBack,则会以回调的形式返回结果;否则会
    CFSocketError   CFSocketConnectToAddress(CFSocketRef s, CFDataRef address, CFTimeInterval timeout);
    //停止socket数据接收/发送,但不会释放socket对象;若指定了socket source,则会runloop source失效;
    //若创建sokcet时指定CFSocketContext 中的release回调,则会主动调用该回调;
    //默认调用该函数会关闭管理的底层socket,
    //若CFSocketSetSocketFlags函数指定了kCFSocketCloseOnInvalidate,需要手动去关闭socket
    void        CFSocketInvalidate(CFSocketRef s);
    //获取socket是否失效状态
    Boolean CFSocketIsValid(CFSocketRef s);
    //复制本地连接地址
    CFDataRef   CFSocketCopyAddress(CFSocketRef s);
    //复制远端连接地址
    CFDataRef   CFSocketCopyPeerAddress(CFSocketRef s);
    //获取socket上下文
    void        CFSocketGetContext(CFSocketRef s, CFSocketContext *context);
    //获取系统socket描述符
    CFSocketNativeHandle    CFSocketGetNative(CFSocketRef s);
    //创建runloop socket source,需要使用CFRunLoopAddSource添加到runloop中
    CFRunLoopSourceRef  CFSocketCreateRunLoopSource(CFAllocatorRef allocator, CFSocketRef s, CFIndex order);
    //获取socket选项
    CFOptionFlags   CFSocketGetSocketFlags(CFSocketRef s);
    void        CFSocketSetSocketFlags(CFSocketRef s, CFOptionFlags flags);
    void        CFSocketDisableCallBacks(CFSocketRef s, CFOptionFlags callBackTypes);
    void        CFSocketEnableCallBacks(CFSocketRef s, CFOptionFlags callBackTypes);
    //指定连接地址发送数据(若socket已连接,则置为null),超时时间为正数才会生效
    CFSocketError   CFSocketSendData(CFSocketRef s, CFDataRef address, CFDataRef data, CFTimeInterval timeout);
    
    

    注意:kCFSocketAcceptCallBack指定接受连接回调函数,accept返回的关联底层socket为data参数,且该参数需要copy复制,避免野指针;客户端/服务端都需要添加cfsocket runloop source 到runloop中;

    相比底层socketCFSokcet可以实现异步回调的形式读写数据、connect连接、accept接收连接等事件回调;

    CFStream

    读写流,提供一种简单的方法进行媒体数据的交换,与设备无关。你可以为内存中、文件中或者网络中的数据创建流,并且你可以在不把数据加载到内存中的情况下使用流。流是一个字节序列串行传输的通信路径,流是单向的,通常情况下,为了双向通信,需要输入(CFReadStream)、输出流(CFWriteStream)。除了基于文件的流,你不能寻找一个流,一旦数据流被提供或者被消耗,就不能从流中重新取出。

    CFReadStreamRef rdstream = NULL;
    //创建关联socket的读写流
    CFStreamCreatePairWithSocket(kCFAllocatorDefault, sockfd, &rdstream, NULL);
    //打开流
    CFReadStreamOpen(rdstream);
    //异步事件通知读取写流
    CFStreamClientContext context = {0, NULL, NULL, NULL, NULL};//持有上下文信息用于回调,可传递self
    CFReadStreamSetClient(rdstream, kCFStreamEventHasBytesAvailable, _readStream, &context);
    //将流添加到runloop循环中
    CFReadStreamScheduleWithRunLoop(rdstream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
    

    提供了关联socket的读写流,且可通过CFReadStreamSetClient或者CFWriteStreamSetClient异步观察流的回调事件机制,不过需要调用CFReadStreamScheduleWithRunLoop添加到runloop中;

    相比dispatch source观察底层socket读写事件block形式,回调的形式容易使代码不够紧凑,不过流的形式封装了各种事件,如下:

    typedef CF_OPTIONS(CFOptionFlags, CFStreamEventType) {
        kCFStreamEventNone = 0,
        kCFStreamEventOpenCompleted = 1,
        kCFStreamEventHasBytesAvailable = 2,
        kCFStreamEventCanAcceptBytes = 4, 
        kCFStreamEventErrorOccurred = 8,
        kCFStreamEventEndEncountered = 16
    };
    

    iOS Socket(3) —— CFSocket的使用

    CFNetwork

    CFNetworkd网络框架结构层是构建在基于BSD socketsCore Foundataion框架层中的CFSocketCFStream,是一个低级但高性能的网络框架,提供CFSocketStremCFFTPCFHTTPCFHTTPAuthentication高级封装API;

    相比原始的socketCFNetworkd的优势就是被集成到系统级的设置及运行循环中,沿着框架层次越往上就越能获得更好的服务,比如必要时开启蜂窝无线及通过系统范围的VPN进行路由等。

    image.png

    SimplePing

    Apple 的 SimplePing 封装了 ping 的功能,它利用 resolve host,create socket(send & recv data), 解析 ICMP 包验证 checksum 等实现了 ping 功能,并且支持 iPv4 和 iPv6。

    通过CFHost类来异步解析host地址,解析成功后创建数据报ICMP协议的socket,并创建关联该socketCFSocket对象,并将该对象添加到runloop中,以实现异步回调的形式recvfrom读取套接字,并通过sendto发送组装ICMP协议的数据报;

    socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP);//不能使用原始套接字,需要root权限,苹果提供了基于数据报的ICMP协议socket

    核心代码如下:

    //解析host地址
    {
        Boolean             success;
        CFHostClientContext context = {0, (__bridge void *)(self), NULL, NULL, NULL};
        CFStreamError       streamError;
        //创建cfhost
        self.host = (CFHostRef)CFAutorelease(CFHostCreateWithName(NULL, (__bridge CFStringRef) self.hostName));
        if (self.host == NULL) {
            // host NULL; do nothing.
            return;
        }
        //关联cfhost到client context并设置回调
        CFHostSetClient(self.host, HostResolveCallback, &context);
        //添加到runloop中
        CFHostScheduleWithRunLoop(self.host, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
        //解析host地址
        success = CFHostStartInfoResolution(self.host, kCFHostAddresses, &streamError);
        if (!success) {
            [self didFailWithHostStreamError:streamError];
        }
    }
    
    //创建socket及关联的cfsocket对象,观察
    {
        //创建socket
        fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP);
        
        CFSocketContext         context = {0, (__bridge void *)(self), NULL, NULL, NULL};
        CFRunLoopSourceRef      rls;
        id<PingFoundationDelegate>  strongDelegate;
    
        // Wrap it in a CFSocket and schedule it on the runloop.
        self.socket = (CFSocketRef) CFAutorelease( CFSocketCreateWithNative(NULL, fd, kCFSocketReadCallBack, SocketReadCallback, &context) );
    
        // The socket will now take care of cleaning up our file descriptor.
        fd = -1;
        rls = CFSocketCreateRunLoopSource(NULL, self.socket, 0);
        CFRunLoopAddSource(CFRunLoopGetCurrent(), rls, kCFRunLoopDefaultMode);
        CFRelease(rls);
    }
    //读取socket数据
    {
        recvfrom(CFSocketGetNative(self.socket), buffer, kBufferSize, 0, (struct sockaddr *) &addr, &addrLen);
        //校验数据
        [self validatePingResponsePacket:packet sequenceNumber:&sequenceNumber];
        //回调给代理
        [strongDelegate pingFoundation:self didReceivePingResponsePacket:packet sequenceNumber:sequenceNumber];
    }
    //发送数据
    {
      //组装icmp协议数据
      
      //sendto发送
      sendto(CFSocketGetNative(self.socket),packet.bytes,packet.length,0,self.hostAddress.bytes,
        (socklen_t) self.hostAddress.length
      );
    }
    

    参考资料

    CFNetwork Programming Guide

    CFSocket

    CFSokcet.c

    《iOS网络高级编程》

    Demo

    https://github.com/FengyunSky/notes/blob/master/local/code/cfsocketdemo.tar

    相关文章

      网友评论

        本文标题:iOS网络编程

        本文链接:https://www.haomeiwen.com/subject/jlkmmhtx.html