iOS 开发之CFSocket

前言

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连接需要进行“三次握手”


iOS 开发之CFSocket_第1张图片
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()操作就会产生"四次握手"


iOS 开发之CFSocket_第2张图片
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


iOS 开发之CFSocket_第3张图片
image.png

基于UDP的Socket


iOS 开发之CFSocket_第4张图片
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 
#import 
#import 
#import 

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

客户端
/** 这个端口可以随便设置*/
#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);
        }
    }
    
}

结果如下:
客户端:


iOS 开发之CFSocket_第5张图片
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)