美文网首页ios端直播业务
CocoaAsyncSocket --Socket学习

CocoaAsyncSocket --Socket学习

作者: A訫飛Flyme | 来源:发表于2019-04-24 09:30 被阅读0次

    Socket理论

    套接字(Socket)概念

    套接字(socket)是通信的基石,是支持TCP/IP协议的网络通信的基本操作单元。
    它是网络通信过程中端点的抽象表示,包含进行网络通信必须的五种信息:连接使用的协议,本地主机的IP地址,本地进程的协议端口,远地主机的IP地址,远地进程的协议端口。

    应用层通过传输层进行数据通信时,TCP会遇到同时为多个应用程序进程提供并发服务的问题。
    多个TCP连接或多个应用程序进程可能需要通过同一个 TCP协议端口传输数据。为了区别不同的应用程序进程和连接,许多计算机操作系统为应用程序与TCP/IP协议交互提供了套接字(Socket)接口。
    应用层可以和传输层通过Socket接口,区分来自不同应用程序进程或网络连接的通信,实现数据传输的并发服务。


    本质

    建立socket连接

    建立Socket连接至少需要一对套接字,其中一个运行于客户端,称为ClientSocket,另一个运行于服务器端,称为ServerSocket
    套接字之间的连接过程分为三个步骤:服务器监听,客户端请求,连接确认

    服务器监听:服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态,等待客户端的连接请求。
    客户端请求:指客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端套接字的地址和端口号,然后就向服务器端套接字提出连接请求。
    连接确认:当服务器端套接字监听到或者说接收到客户端套接字的连接请求时,就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的描述发给客户 端,一旦客户端确认了此描述,双方就正式建立连接。而服务器端套接字继续处于监听状态,继续接收其他客户端套接字的连接请求。

    Socket连接与TCP连接

    创建Socket连接时,可以指定使用的传输层协议,Socket可以支持不同的传输层协议(TCP或UDP),当使用TCP协议进行连接时,该Socket连接就是一个TCP连接。

    Socket连接与HTTP连接

    由于通常情况下Socket连接就是TCP连接,因此Socket连接一旦建立,通信双方即可开始相互发送数据内容,直到双方连接断开。
    但在实际网络应用中,客户端到服务器之间的通信往往需要穿越多个中间节点,例如路由器、网关、防火墙等,大部分防火墙默认会关闭长时间处于非活跃状态的连接而导致 Socket 连接断连,因此需要通过轮询告诉网络,该连接处于活跃状态。
    而HTTP连接使用的是“请求—响应”的方式,不仅在请求时需要先建立连接,而且需要客户端向服务器发出请求后,服务器端才能回复数据。很多情况下,需要服务器端主动向客户端推送数据,保持客户端与服务器数据的实时与同步。此时若双方建立的是Socket连接,服务器就可以直接将数据传送给客户端;若双方建立的是HTTP连接,则服务器需要等到客户端发送一次请求后才能将数据传回给客户端.
    因此,客户端定时向服务器端发送连接请求,不仅可以保持在线,同时也是在“询问”服务器是否有新的数据,如果有就将数据传给客户端。

    连接过程

    TCP三次握手

    TCP 三次握手

    服务器监听到连接请求( 服务端启动之后就在不断地监听连接请求),即收到SYN J包,调用accept函数接收请求向客户端发送SYN K ,ACK J+1,这时accept进入阻塞状态; 客户端收到服务器的SYN K ,ACK J+1之后,这时connect返回,并对SYN K进行确认;服务器收到ACK K+1时,accept返回,至此三次握手完毕,连接建立。

    当客户端调用connect时,
    第一次握手
    建立连接。客户端发送连接请求报文段,将SYN位置为1,Sequence Number为K;然后,客户端SYN_SEND ,connect进入阻塞状态,等待服务器的确认;
    第二次握手
    服务器监听到连接请求( 服务端启动之后就在不断地监听连接请求) ,服务器收到SYN报文段。服务器收到客户端的SYN 报文段,调用accept函数接收请求向客户端发送SYN K ,ACK J+1,这时accept进入阻塞状态,服务器进入SYN_RECV状态;
    (*需要对这个SYN报文段进行确认,设置Acknowledgment Number为K+1(Sequence Number+1);同时,自己自己还要发送SYN请求信息,将SYN位置为1,Sequence Number为y;服务器端将上述所有信息放到一个报文段(即SYN+ACK报文段)中,一并发送给客户端*)
    第三次握手
    客户端收到服务器的SYN+ACK报文段。
    这时connect返回,并对SYN K进行确认,然后将Acknowledgment Number设置为K+1发送,服务器收到ACK K+1时,accept返回。
    客户端和服务器端都进入ESTABLISHED状态,完成TCP三次握手。

    为什么要进行三次握手?
    第一次握手是客户端向服务端发消息,询问你有没有接收消息的能力?确保消息能准确发送出去,告诉服务端我有写的能力(客户端能发消息到服务端);第二次握手是指服务端向客户端回消息,标明我收到了消息并且能给你反馈,也就是服务端有读和写的能力(读到客户端消息,并服务端也发消息给我客户端);第三次握手是客户端给服务端发消息确认建立连接,是告诉服务端我不仅有写的能力,而且我也有读的能力,咱们可以放心通讯了(客户端也能收到服务端消息确认连接)。

    截包数据

    这个三次握手发生在socket的那几个函数中呢?请看下图:


    客户端握手方法都在connect

    四次挥手

    断开连接

    第一次分手
    主机1(可以使客户端,也可以是服务器端),设置Sequence Number,向主机2发送一个FIN报文段;此时,主机1进入FIN_WAIT_1状态;这表示主机1没有数据要发送给主机2了;
    第二次分手
    主机2收到了主机1发送的FIN报文段,向主机1回一个ACK报文段,Acknowledgment Number为Sequence Number加1;主机1进入FIN_WAIT_2状态;主机2告诉主机1,我“同意”你的关闭请求;
    第三次分手
    主机2向主机1发送FIN报文段,请求关闭连接,同时主机2进入LAST_ACK状态;
    第四次分手
    主机1收到主机2发送的FIN报文段,向主机2发送ACK报文段,然后主机1进入TIME_WAIT状态;主机2收到主机1的ACK报文段以后,就关闭连接;此时,主机1等待2 MSL后依然没有收到回复,则证明Server端已正常关闭,那好,主机1也可以关闭连接了。

    为什么要四次分手
    TCP协议是一种面向连接的、可靠的、基于字节流的运输层通信协议。TCP是全双工模式,这就意味着,当主机1发出FIN报文段时,只是表示主机1已经没有数据要发送了,主机1告诉主机2,它的数据已经全部发送完毕了;但是,这个时候主机1还是可以接受来自主机2的数据;当主机2返回ACK报文段时,表示它已经知道主机1没有数据发送了,但是主机2还是可以发送数据到主机1的;当主机2也发送了FIN报文段时,这个时候就表示主机2也没有数据要发送了,就会告诉主机1,我也没有数据要发送了,之后彼此就会愉快的中断这次TCP连接。


    socket方式

    - (void)initScoket
    {
        //初始化
        //每次连接前,先断开连接
        if (_clientScoket != 0) {
            [self disConnect];
            _clientScoket = 0;
        }
        
        //创建客户端socket
        _clientScoket = CreateClinetSocket();
        //服务器Ip
        const char * server_ip="127.0.0.1";
        //服务器端口
        short server_port=6969;
        //等于0说明连接失败
        if (ConnectionToServer(_clientScoket,server_ip, server_port)==0) {
            printf("Connect to server error\n");
            return ;
        }
        //走到这说明连接成功
        printf("Connect to server ok\n");
    }
    
    static int CreateClinetSocket()
    {
        int ClinetSocket = 0;
        //创建一个socket,返回值为Int。(注scoket其实就是Int类型)
        //第一个参数addressFamily IPv4(AF_INET) 或 IPv6(AF_INET6)。
        //第二个参数 type 表示 socket 的类型,通常是流stream(SOCK_STREAM) 或数据报文datagram(SOCK_DGRAM)
        //第三个参数 protocol 参数通常设置为0,以便让系统自动为选择我们合适的协议,对于 stream socket 来说会是 TCP 协议(IPPROTO_TCP),而对于 datagram来说会是 UDP 协议(IPPROTO_UDP)。
        ClinetSocket = socket(AF_INET, SOCK_STREAM, 0);
        return ClinetSocket;
    }
    static int ConnectionToServer(int client_socket,const char * server_ip,unsigned short port)
    {
        //生成一个sockaddr_in类型结构体
        struct sockaddr_in sAddr={0};
        sAddr.sin_len=sizeof(sAddr);
        //设置IPv4
        sAddr.sin_family=AF_INET;
        //inet_aton是一个改进的方法来将一个字符串IP地址转换为一个32位的网络序列IP地址
        //如果这个函数成功,函数的返回值非零,如果输入地址不正确则会返回零。
        inet_aton(server_ip, &sAddr.sin_addr);
        
        //htons是将整型变量从主机字节顺序转变成网络字节顺序,赋值端口号
        sAddr.sin_port=htons(port);
        //用scoket和服务端地址,发起连接。
        //客户端向特定网络地址的服务器发送连接请求,连接成功返回0,失败返回 -1。
        //注意:该接口调用会阻塞当前线程,直到服务器返回。
        if (connect(client_socket, (struct sockaddr *)&sAddr, sizeof(sAddr))==0) {
            return client_socket;
        }
        return 0;
    }
    
    #pragma mark - 对外逻辑
    - (void)connect
    {
        [self initScoket];
    }
    - (void)disConnect
    {
        //关闭连接
        close(self.clientScoket);
    }
    
    //发送消息
    - (void)sendMsg:(NSString *)msg
    {
        const char *send_Message = [msg UTF8String];
        send(self.clientScoket,send_Message,strlen(send_Message)+1,0);
    }
    
    //收取服务端发送的消息
    - (void)recieveAction{
    
        while (1) {
            char recv_Message[1024] = {0};
            recv(self.clientScoket, recv_Message, sizeof(recv_Message), 0);
            printf("%s\n",recv_Message);
        }
    }
    
    #pragma mark - 新线程来接收消息
    - (void)pullMsg
    {
        NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(recieveAction) object:nil];
        [thread start];
    }
    

    CocoaAsyncSocket 实现

    - (void)initSocket
    {
        gcdSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
    }
    
    #pragma mark - 对外的一些接口
    static  NSString * Khost = @"127.0.0.1";
    static const uint16_t Kport = 6969;
    //建立连接
    - (BOOL)connect
    {
        return  [gcdSocket connectToHost:Khost onPort:Kport error:nil];
    }
    //断开连接
    - (void)disConnect
    {
        [gcdSocket disconnect];
    }
    //发送消息
    - (void)sendMsg:(NSString *)msg
    {
        NSData *data  = [msg dataUsingEncoding:NSUTF8StringEncoding];
        //第二个参数,请求超时时间
        [gcdSocket writeData:data withTimeout:-1 tag:110];
    }
    
    #pragma mark - GCDAsyncSocketDelegate
    //连接成功调用
    - (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port
    {
        NSLog(@"连接成功,host:%@,port:%d",host,port);
        [self checkPingPong:110];
        //心跳写在这...
    }
    //断开连接的时候调用
    - (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(nullable NSError *)err
    {
        NSLog(@"断开连接,host:%@,port:%d",sock.localHost,sock.localPort);
        //断线重连写在这...
        if (err) {
            NSLog(@"连接失败");
        }else{
            NSLog(@"正常断开");
        }
        
    //    if ([sock.userData isEqualToString:[NSString stringWithFormat:@"%d",SOCKET_CONNECT_SERVER]])
    //    {
    //        //服务器掉线 重新连接
    //        [self connectToServerWithCommand:@"battery"];
    //    }else{
    //        return;
    //    }
    }
    //写的回调
    - (void)socket:(GCDAsyncSocket*)sock didWriteDataWithTag:(long)tag
    {
        NSLog(@"写的回调,tag:%ld",tag);
        //判断是否成功发送,如果没收到响应,则说明连接断了,则想办法重连
        [self checkPingPong:tag];
    }
    
    - (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag
    {
        NSString *msg = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
        NSLog(@"收到消息:%@",msg);
        [self pullTheMsg:tag];
    }
    
    //监听最新的消息
    - (void)pullTheMsg:(long)tag
    {
        //貌似是分段读数据的方法
        //    [gcdSocket readDataToData:[GCDAsyncSocket CRLFData] withTimeout:10 maxLength:50000 tag:110];
        //监听读数据的代理,只能监听10秒,10秒过后调用代理方法  -1永远监听,不超时,但是只收一次消息,
        //所以每次接受到消息还得调用一次
        [gcdSocket readDataWithTimeout:-1 tag:110];
    }
    //用Pingpong机制来看是否有反馈
    - (void)checkPingPong:(long)tag
    {
        //pingpong设置为3秒,如果3秒内没得到反馈就会自动断开连接
        [gcdSocket readDataWithTimeout:3 tag:110];
    }
    

    SocketRocket(WebSocket)

    心跳监听

    xmppFrameWork: XMPPAutoPing 和 XMPPPing两个类: 心跳监听类;

    数据粘包处理

    //https://www.jb51.net/article/105278.html
    /*
      while (_readBuf.length >= 10)//因为头部固定10个字节,数据长度至少要大于10个字节,我们才能得到完整的消息描述信息 
      { 
        NSData *head = [_readBuf subdataWithRange:NSMakeRange(0, 10)];//取得头部数据 
        NSData *lengthData = [head subdataWithRange:NSMakeRange(6, 4)];//取得长度数据 
        NSInteger length = [[[NSString alloc] initWithData:lengthData encoding:NSUTF8StringEncoding] integerValue];//得出内容长度 
        NSInteger complateDataLength = length + 10;//算出一个包完整的长度(内容长度+头长度) 
        if (_readBuf.length >= complateDataLength)//如果缓存中数据够一个整包的长度 
        { 
          NSData *data = [_readBuf subdataWithRange:NSMakeRange(0, complateDataLength)];//截取一个包的长度(处理粘包) 
          [self handleTcpResponseData:data];//处理包数据 
          //从缓存中截掉处理完的数据,继续循环 
          _readBuf = [NSMutableData dataWithData:[_readBuf subdataWithRange:NSMakeRange(complateDataLength, _readBuf.length - complateDataLength)]]; 
        } 
        else//如果缓存中的数据长度不够一个包的长度,则包不完整(处理半包,继续读取) 
        { 
          [_socket readDataWithTimeout:-1 buffer:_readBuf bufferOffset:_readBuf.length tag:0];//继续读取数据 
          return; 
        } 
      } 
      //缓存中数据都处理完了,继续读取新数据 
      [_socket readDataWithTimeout:-1 buffer:_readBuf bufferOffset:_readBuf.length tag:0];//继续读取数据
    

    END

    GCDAsyncSocket类的使用、心跳、粘包处理
    阳分析VPN包
    TCP建立连接为什么需要三次握手
    iOS Socket 心跳
    网络层之IP协议
    http协议的底层实现

    Question

    1. http底层既然是基于socket抽象层,http 经过socket抽象的TCP连接
      http 短链接基于TCP链接,socket长链接 TCP/UDP ,
      http和 socket 基本属于一个级别,对TCP/UDP的一层封装。
      http属于应用层,
      socket在应用和传输之间。
      TCP/UDP 传输层。
      ip协议:对应与网络层 ,IP是一个载体,TCP,UDP,数据都以IP数据格式传输。

    2. 四次挥手断开问题
      如果网络问题断开会咋样

    3. Socket Scoket?

    相关文章

      网友评论

        本文标题:CocoaAsyncSocket --Socket学习

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