美文网首页
iOS 开发之CFSocket

iOS 开发之CFSocket

作者: 天下林子 | 来源:发表于2018-09-30 14:32 被阅读388次

    前言

    CFSocket是在系统的CFNetwork.framework中,

    Socket接口是TCP/IP网络的API,Socket接口定义了许多函数或例程,程序员可以用它们来开发TCP/IP网络上的应用程序。要学Internet上的TCP/IP网络编程,必须理解Socket接口。

    Socket接口设计者最先是将接口放在Unix操作系统里面的。如果了解Unix系统的输入和输出的话,就很容易了解Socket了。网络的Socket数据传输是一种特殊的I/O,Socket也是一种文件描述符。Socket也具有一个类似于打开文件的函数调用Socket(),该函数返回一个整型的Socket描述符,随后的连接建立、数据传输等操作都是通过该Socket实现的。常用的Socket类型有两种:流式Socket(SOCK_STREAM)和数据报式Socket(SOCK_DGRAM)。流式是一种面向连接的Socket,针对于面向连接的TCP服务应用;数据报式Socket是一种无连接的Socket,对应于无连接的UDP服务应用。

    Socket的通信过程

    每一个应用或服务器都有一个端口,因此需要包含以下的步骤:

    1. 服务端利用Socket监听端口
    2. 客户端发起连接
    3. 服务端返回信息,建立连接,开始通信
    4. 客户端,服务端断开连接

    HTTP和Socket连接的区别

    OSI模型把网络通信分成7层, 由低向高分别是: 物理层,数据链路层,网络层,传输层,会话层,表示层和应用层
    其中HTTP协议是对应应用层,TCP协议对应传输层,IP协议对应网络层,HTTP协议时基于TCP连接。
    TCP/IP是传输层协议,主要解决数据如何在网络中传输,而HTTP是应用层协议,主要解决如何包装数据

    在传输数据时候可以只是用TCP/IP, 但是这样没有应用层,无法识别传输的数据内容,这样是没有意义的,如果想使传输的数据有意义,则必须使用应用层协议,HTTP就是一种,Web使用它,封装HTTP文本信息,然后使用TCP/IP协议传输到网络上

    Socket 实际上是对TCP/IP协议的疯转,本身并不是协议,而是调用一个接口(API), 通过Socket, 我们才能使用TCP/IP 协议

    建立一次TCP连接需要进行“三次握手”


    image.png

    SYN(synchronous),同步标志,ACK(Acknowledgement)即确认标志,seq应该是Sequence Number, 序号的意思,另外还有四次握手的fin, 应该是final,表示结束标志

    第一次握手: 客户端发送一个TCP的SYN标志位置1的包指明链接的服务器端口,以及初始程序号X,保存在包头的序列号(sequence Number)字段里

    第二次握手: 服务器发回确认包(ACK)应答,即SYN标志位和ACK标志位均为1 同时,将确认序号(Acknowledgement Number)设置为客户的序列号加1,即 X + 1

    第三次握手: 客户端再次发送确认包(ACK) SYN标志位为0, ACK标志位为1, 并且吧服务器发来的序号字段+1, 放在确定字段中发送给对方,并且在数据段放写序列号的+1

    只有进行完三次握手后,才能正式传输数据,理想状态下只要建立起链接,在通信双方主动关闭链接之前,TCP连接会一直保持下去。三次握手能够确保对面已经收到自己的同步序列号,这样就可以保证后续数据包的丢失可以被察觉,这也是TCP流式传输的基础。

    断开TCP连接需要发送4个包,客户端和服务端都可以发起这个请求,在socket编程中任何一方执行close()操作就会产生"四次握手"


    image.png

    关闭是4次,是因为当服务端收到客户端的SYN连接请求报文后,可以直接发送SYN+ACK报文,ACK用来回应,SYN用来同步。但是当关闭连接的情况下,接收端收到FIN报文时候,很可能不会立即关闭,所以先发送一个ACK报文告诉发送端我收到了,只有等接受端报文全部发送完了,才能发送FIN报文

    Socekt
    Socket是通信的基石,是支持TCP/IP协议的基本操作单元,包含5种信息:连接使用的协议,本机主机IP地址,本地进程的端口号,远程主机IP地址,远程进程的协议端口。

    应用层通过传输层进行数据传输时候,可能会遇到同一个TCP协议端口传输好几种数据,可以通过socket来区分不同应用程序或者网络连接。

    建立Socket连接的步骤

    1.至少需要一对,一个作用于客户端,一个在服务端

    1. 链接分为三个步骤:服务器监听,客户端请求,链接确认
    2. 服务端监听: 并不对应具体的客户端socket,而是处于等待连接状态,实时监听网络状态,等待客户端连接
    3. 客户端请求:客户端的套接字向服务端套接字发送链接请求,因此需要知道服务端的套接字的地址和端口号,而且需要描述他要连接的服务器的套接字
    4. 连接确认: 当服务端套接字监听到或者接收到客户端的套接字的链接请求,就响应客户端的套接字,建立一个新的链接,把客户端的套接字的描述发给客户端,一旦确认,双方就正式建立连接,而且服务端的套接字仍在监听状态,继续接受其他客户端的套接字

    Socket HTTP TCP区别
    socket 连接可以指定传输层协议, 可以是TCP 或者UDP, 当是TCP协议的时候就是TCP连接。而HTTP连接就是请求->响应的方式, 在请求时候需要先建立连接,然后客户端向服务器发出请求之后,服务器才能回复数据。
    socket一旦建立连接,服务器可以主动将数据传输给客户端,而HTTP则需要客户端先向服务器发送请求之后才能将数据返回给客户端,但实际上socket建立之后因为种种原因,会导致断开连接,其中一个原因就是防火墙会断开长时间处于非活跃状态的链接,因此需要轮询告诉网络,这个连接是活跃的

    应用

    iOS 提供了Socket网络编程接口CFSocket, TCP和UDP的socket是有区别的
    基于TCP的Socket


    image.png

    基于UDP的Socket


    image.png

    常用的socket 分为两种
    流式Socket(SOCKET_STREAM)
    数据报式(SOCKET_DGRAM)
    流式针对面向TCP链接的应用,而数据报式是一种无连接的socket,对应于无连接的UDP服务应用

    iOS官方给出的CFSocket,它是基于BSD Socket进行抽象和封装,CFSocket中包含了少数开销,它几乎可以提供BSD sockets 所具有的一切功能,并且把socket集成进一个“运行循环”当中。CFSocket并不仅仅限于流的sockets(比如TCP),它可以处理任何类型的socket

    你可以利用CFSocketCreate 功能从头开始创建一个CFSocket对象,或者利用CFSocketCreateWithNative函数从BSD socket创建,然后需要利用函数CFSocketCreateRunLoopSource 创建一个“运行循环”源,并利用函数CFRunLoopAddSource把它加入一个运行循环,这样不论CFSocket对象是否接收到信息,CFSocket回调函数都可以运行。
    示例代码:
    客户端和服务器端都需要引入的头文件如下:

    #import <sys/socket.h>
    #import <netinet/in.h>
    #import <arpa/inet.h>
    #import <unistd.h>
    
    

    用两个电脑测试,两个端的端口要一样,两个端的地址一定要相同(设置为服务器端代码所在的电脑的端口),且服务器端要用有线连接网络,客户端用

    客户端
    /** 这个端口可以随便设置*/
    #define TEST_IP_PROT 22235
    /** 替换成你需要连接服务器绑定的IP地址,不能随便输*/
    #define TEST_IP_ADDR "192.168.103.244"
    
    服务端
    /** 这个端口可以随便设置*/
    #define TEST_IP_PROT 22235
    /** 替换成你当前连接的WIFI的IP地址*/
    #define TEST_IP_ADDR "192.168.103.244"
    
    

    客户端

    • 连接服务器
    - (IBAction)connectServer:(id)sender {
        
        if (!_socketRef) {
            
            // 创建socket关联的上下文信息
            
            /*
             typedef struct {
             CFIndex    version; 版本号, 必须为0
             void *    info; 一个指向任意程序定义数据的指针,可以在CFSocket对象刚创建的时候与之关联,被传递给所有在上下文中回调
             const void *(*retain)(const void *info); info 指针中的retain回调,可以为NULL
             void    (*release)(const void *info); info指针中的release回调,可以为NULL
             CFStringRef    (*copyDescription)(const void *info); 回调描述,可以n为NULL
             } CFSocketContext;
             
             */
            
            CFSocketContext sockContext = {0, (__bridge void *)(self), NULL, NULL, NULL};
            
            //创建一个socket
            _socketRef = CFSocketCreate(kCFAllocatorDefault, PF_INET, SOCK_STREAM, IPPROTO_TCF, kCFSocketConnectCallBack, ServerConnectCallBack, &sockContext);
            
            //创建sockadd_in的结构体,改结构体作为socket的地址,IPV6需要改参数
            
            //sockaddr_in
            // sin_len;  长度
            //sin_family;协议簇, 用AF_INET -> 互联网络, TCP,UDP 等等
            //sin_port; 端口号(使用网络字节顺序)htons:将主机的无符号短整形数转成网络字节顺序
            //in_addr sin_addr; 存储IP地址, inet_addr()的功能是将一个点分十进制的IP转换成一个长整型数(u_long类型),若字符串有效则将字符串转换为32位二进制网络字节序的IPV4地址, 否则为IMADDR_NONE
            //sin_zero[8]; 让sockaddr与sockaddr_in 两个数据结构保持大小相同而保留的空字节,无需处理
            
            struct sockaddr_in Socketaddr;
            //memset: 将addr中所有字节用0替代并返回addr,作用是一段内存块中填充某个给定的值,它是对较大的结构体或数组进行清零操作的一种最快方法
            memset(&Socketaddr, 0, sizeof(Socketaddr));
            Socketaddr.sin_len = sizeof(Socketaddr);
            Socketaddr.sin_family = AF_INET;
            Socketaddr.sin_port = htons(TEST_IP_PROT);
            Socketaddr.sin_addr.s_addr = inet_addr(TEST_IP_ADDR);
            
            //将地址转化为CFDataRef
            CFDataRef dataRef = CFDataCreate(kCFAllocatorDefault, (UInt8 *)&Socketaddr, sizeof(Socketaddr));
            
            //连接
            //CFSocketError    CFSocketConnectToAddress(CFSocketRef s, CFDataRef address, CFTimeInterval timeout);
            //第一个参数  连接的socket
            //第二个参数  连接的socket的包含的地址参数
            //第三个参数 连接超时时间,如果为负,则不尝试连接,而是把连接放在后台进行,如果_socket消息类型为kCFSocketConnectCallBack,将会在连接成功或失败的时候在后台触发回调函数
            CFSocketConnectToAddress(_socketRef, dataRef, -1);
            
            //加入循环中
            //获取当前线程的runLoop
            CFRunLoopRef runloopRef = CFRunLoopGetCurrent();
            //把socket包装成CFRunLoopSource, 最后一个参数是指有多个runloopsource通过一个runloop时候顺序,如果只有一个source 通常为0
            CFRunLoopSourceRef sourceRef = CFSocketCreateRunLoopSource(kCFAllocatorDefault, _socketRef, 0);
            
            //加入运行循环
            //第一个参数:运行循环管
            //第二个参数: 增加的运行循环源, 它会被retain一次
            //第三个参数:用什么模式把source加入到run loop里面,使用kCFRunLoopCommonModes可以监视所有通常模式添加source
            CFRunLoopAddSource(runloopRef, sourceRef, kCFRunLoopCommonModes);
            
            //之前被retain一次,这边要释放掉
            CFRelease(sourceRef);
            
        }
        
    }
    
    
    • 发送消息
    - (IBAction)sendMessage:(id)sender {
        
        if (!_socketRef) {
            [[[UIAlertView alloc] initWithTitle:@"对不起" message:@"请先连接服务器" delegate:self cancelButtonTitle:@"确定" otherButtonTitles: nil] show];
            return;
        }
        NSString *stringTosend = [NSString stringWithFormat:@"%@说:%@",self.nameText.text,self.messageText.text];
    
        const char* data       = [stringTosend UTF8String];
    
        /** 成功则返回实际传送出去的字符数, 失败返回-1. 错误原因存于errno*/
        long sendData          = send(CFSocketGetNative(_socketRef), data, strlen(data) + 1, 0);
        
        if (sendData < 0) {
            perror("send");
        }
    }
    
    
    • 读取数据
    
    - (void)_readStreamData
    {
        //定义一个字符型变量
        char buffer[512];
        
        /*
         int recv(SOCKET s, char FAR *buf, int len, int flags);
         
         不论是客户还是服务器应用程序都用recv函数从TCP连接的另一端接收数据
         1. 第一个参数指定接收端套接字描述符
         2.第二个参数指明一个缓冲区,改缓冲区用来存放recv函数接受到的数据
         3. 第三个参数指明buf的长度
         4.第四个参数一般置0
         
         */
        
        long readData;
        //若无错误发生,recv() 返回读入的字节数。如果连接已终止,返回0 如果发生错误,返回-1, 应用程序可通过perror() 获取相应错误信息
        while ((readData = recv(CFSocketGetNative(_socketRef), buffer, sizeof(buffer), 0))) {
            
            NSString *content = [[NSString alloc] initWithBytes:buffer length:readData encoding:NSUTF8StringEncoding];
            
            dispatch_async(dispatch_get_main_queue(), ^{
                self.infoLabel.text = [NSString stringWithFormat:@"%@\n%@", content, self.infoLabel.text];
            });
            
        }
        
        perror("++++++recv+++++++++");
        
    }
    
    
    • 回调函数
    /**
     回调函数
    
     @param s socket对象
     @param callbackType 这个socket对象的活动类型
     @param address socket对象连接的远程地址,CFData对象对应的是socket对象中的protocol family (struct sockaddr_in 或者 struct sockaddr_in6), 除了type 类型 为kCFsocketAcceptCallBack 和kCFSocketDataCallBack ,否则这个值通常是NULL
     @param data 跟回调类型相关的数据指针
     kCFSocketConnectCallBack : 如果失败了, 它指向的就是SINT32的错误代码
     kCFSocketAcceptCallBack : 它指向的就是CFSocketNativeHandle
     kCFSocketDataCallBack : 它指向的就是将要进来的Data
     其他情况就是NULL
    
     @param info 与socket相关的自定义的任意数据
     
     */
    void ServerConnectCallBack (CFSocketRef s, CFSocketCallBackType callbackType, CFDataRef address, const void *data, void *info)
    {
        ViewController *vc = (__bridge ViewController *)(info);
        //判断是不是NULL
        if (data != NULL)
        {
            printf("----->>>>>>连接失败\n");
            
            [vc performSelector:@selector(_releseSocket) withObject:nil];
            
        }
        else
        {
            printf("----->>>>>>连接成功\n");
            [vc performSelectorInBackground:@selector(_readStreamData) withObject:nil];
            
        }
        
    }
    
    • 清空socket
    
    - (void)_releseSocket
    {
        if (_socketRef) {
            CFRelease(_socketRef);
        }
        
        _socketRef = NULL;
        
        self.infoLabel.text = @"----->>>>>>连接失败-----";
        
        
    }
    
    

    服务端

    • 初始化
    - (void)_initSocket
    {
        @autoreleasepool {
            //创建Socket, 指定TCPServerAcceptCallBack
            //作为kCFSocketAcceptCallBack 事件的监听函数
            //参数1: 指定协议族,如果参数为0或者负数,则默认为PF_INET
            //参数2:指定Socket类型,如果协议族为PF_INET,且该参数为0或者负数,则它会默认为SOCK_STREAM,如果要使用UDP协议,则该参数指定为SOCK_DGRAM
            //参数3:指定通讯协议。如果前一个参数为SOCK_STREAM,则默认为使用TCP协议,如果前一个参数为SOCK_DGRAM,则默认使用UDP协议
            //参数4:指定下一个函数所监听的事件类型
            CFSocketRef _socket = CFSocketCreate(kCFAllocatorDefault, PF_INET, SOCK_STREAM, IPPROTO_TCP, kCFSocketAcceptCallBack, TCPServerAcceptCallBack, NULL);
            
            if (_socket == NULL) {
                NSLog(@"————————创建socket 失败");
                return;
            }
            
            BOOL reused = YES;
            
            //设置允许重用本地地址和端口
            setsockopt(CFSocketGetNative(_socket), SOL_SOCKET, SO_REUSEADDR, (const void *)&reused, sizeof(reused));
            
            //定义sockaddr_in类型的变量, 该变量将作为CFSocket的地址
            struct sockaddr_in Socketaddr;
            memset(&Socketaddr, 0, sizeof(Socketaddr));
            Socketaddr.sin_len = sizeof(Socketaddr);
            Socketaddr.sin_family = AF_INET;
            
            //设置服务器监听地址
            Socketaddr.sin_addr.s_addr = inet_addr(TEST_IP_ADDR);
            //设置服务器监听端口
            Socketaddr.sin_port = htons(TEST_IP_PROT);
            
            //将ipv4 的地址转换为CFDataRef
            CFDataRef address = CFDataCreate(kCFAllocatorDefault,  (UInt8 *)&Socketaddr, sizeof(Socketaddr));
            
            //将CFSocket 绑定到指定IP地址
            if (CFSocketSetAddress(_socket, address) != kCFSocketSuccess) {
                
                //如果_socket 不为NULL, 则f释放_socket
                if (_socket) {
                    CFRelease(_socket);
                    exit(1);
                }
                
                _socket = NULL;
            }
            
            //启动h循环箭筒客户链接
            NSLog(@"----启动循环监听客户端连接---");
            //获取当前线程的CFRunloop
            CFRunLoopRef cfrunLoop = CFRunLoopGetCurrent();
            //将_socket包装成CFRunLoopSource
            CFRunLoopSourceRef source = CFSocketCreateRunLoopSource(kCFAllocatorDefault, _socket, 0);
            //为CFRunLoop对象添加source
            CFRunLoopAddSource(cfrunLoop, source, kCFRunLoopCommonModes);
            CFRelease(source);
            //运行当前线程的CFrunLoop
            CFRunLoopRun();
        }
    }
    
    • 读取数据
    void readStream(CFReadStreamRef readStream, CFStreamEventType eventype, void * clientCallBackInfo) {
        
        UInt8 buff[2048];
        
        NSString *aaa = (__bridge NSString *)(clientCallBackInfo);
        NSLog(@"+++++++>>>>>%@", aaa);
        
        //--从可读的数据流中读取数据,返回值是多少字节读到的, 如果为0 就是已经全部结束完毕,如果是-1 则是数据流没有打开或者其他错误发生
        CFIndex hasRead = CFReadStreamRead(readStream, buff, sizeof(buff));
        
        if (hasRead > 0) {
            
            NSLog(@"----->>>>>接受到数据:%s \n", buff);
            const char *str = "test,  test , test \n";
            
            //向客户端输出数据
            CFWriteStreamWrite(writeStreamRef, (UInt8 *)str, strlen(str) + 1);
        }
        
    }
    
    
    • 回调函数
    void TCPServerAcceptCallBack(CFSocketRef socket, CFSocketCallBackType type, CFDataRef address, const void *data, void *info)
    {
        //如果有客户端Socket连接进来
        if (kCFSocketAcceptCallBack == type) {
            
            //获取本地Socket的Handle, 这个回调事件的类型是kCFSocketAcceptCallBack,这个data就是一个CFSocketNativeHandle类型指针
            CFSocketNativeHandle  nativeSocketHandle = *(CFSocketNativeHandle *)data;
            
            //定义一个255数组接收这个新的data转成的socket的地址,SOCK_MAXADDRLEN表示最长的可能的地址
            uint8_t name[SOCK_MAXADDRLEN];
            //这个地址数组的长度
            socklen_t namelen = sizeof(name);
            
            /*
             
             */
            
            //MARK:获取socket信息
            //第一个参数 已经连接的socket
            //第二个参数 用来接受地址信息
            //第三个参数 地址长度
            //getpeername 从已经连接的socket中获的地址信息, 存到参数2中,地址长度放到参数3中,成功返回0, 如果失败了则返回别的数字,对应不同错误码
            
            if (getpeername(nativeSocketHandle, (struct sockaddr *)name, &namelen) != 0) {
                
                perror("++++++++getpeername+++++++");
                
                exit(1);
            }
            
            //获取连接信息
            struct sockaddr_in *addr_in = (struct sockaddr_in *)name;
            
            // inet_ntoa 将网络地址转换成"." 点隔的字符串格式
            NSLog(@"-------->>>>%s===%d--连接进来了", inet_ntoa(addr_in-> sin_addr), addr_in->sin_port);
            
            //创建一组可读/可写的CFStream
            readStreamRef = NULL;
            writeStreamRef = NULL;
            
            //创建一个和Socket对象相关联的读取数据流
            //参数1 :内存分配器
            //参数2 :准备使用输入输出流的socket
            //参数3 :输入流
            //参数4 :输出流
            CFStreamCreatePairWithSocket(kCFAllocatorDefault, nativeSocketHandle, &readStreamRef, &writeStreamRef);
            
            //CFStreamCreatePairWithSocket() 操作成功后,readStreamRef和writeStream都指向有效的地址,因此判断是不是还是之前设置的NULL就可以了
            if (readStreamRef && writeStreamRef) {
                //打开输入流 和输出流
                CFReadStreamOpen(readStreamRef);
                CFWriteStreamRef(writeStreamRef) = NULL;
                
                //一个结构体包含程序定义数据和回调用来配置客户端数据流行为
                NSString *aaa = @" 这 是 一个 测 测试 的 代码";
                
                CFStreamClientContext context = {0, (__bridge void *)(aaa), NULL, NULL };
                
                //指定客户端的数据流, 当特定事件发生的时候, 接受回调
                //参数1 : 需要指定的数据流
                //参数2 : 具体的事件,如果为NULL,当前客户端数据流就会被移除
                //参数3 : 事件发生回调函数,如果为NULL,同上
                //参数4 : 一个为客户端数据流保存上下文信息的结构体,为NULL同上
                //CFReadStreamSetClient  返回值为true 就是数据流支持异步通知, false就是不支持
                if (CFReadStreamSetClient(readStreamRef, kCFStreamEventHasBytesAvailable, readStream, &context)) {
                    
                    exit(1);
                    
                }
                
                //将数据流加入循环
                CFReadStreamScheduleWithRunLoop(readStreamRef, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
                const char *str = "+++welcome++++\n";
                
                //向客户端输出数据
                CFWriteStreamWrite(writeStreamRef, (UInt8 *)str, strlen(str) + 1);
            }
            else
            {
                //如果失败就销毁已经连接的socket
                close(nativeSocketHandle);
            }
        }
        
    }
    
    

    结果如下:
    客户端:


    image.png

    服务端:

    2018-09-30 14:36:19.601959+0800 服务端[11483:942439] ----启动循环监听客户端连接---
    2018-09-30 14:41:01.910275+0800 服务端[11483:942439] 192.168.108.63:38369连接进来了
    2018-09-30 14:41:23.003615+0800 服务端[11483:942439] earth,wind,fire,be my call
    接收到数据:Tom说:hello world
    

    参考链接:
    http://www.cnblogs.com/QianChia/p/6391989.html
    https://blog.csdn.net/potato512/article/details/44001767
    https://blog.csdn.net/chang6520/article/details/7874804
    .......

    相关文章

      网友评论

          本文标题:iOS 开发之CFSocket

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