ios端直播业务

CocoaAsyncSocket --Socket学习

2019-04-24  本文已影响0人  A訫飛Flyme

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?

上一篇下一篇

猜你喜欢

热点阅读