1. 程式人生 > IOS開發 >iOS網路(四)socket簡單應用

iOS網路(四)socket簡單應用

一、Socket概覽

  • Socket就是為網路服務提供的一種機制

  • 通訊的兩端都是socket

  • 網路通訊其實就是socket間的通訊

  • 資料在兩個socket間通過IO傳輸

  • socket是純c語言的,是跨平臺的

  • 雙工:A←→B雙向傳輸

    半雙工:雙工中新增開關,若1開關開啟則A→B,若2開關開啟則B→A

  • socket牛逼之處

    主動傳送請求 → 提高速度、節省頻寬、創造及時性 → 即時通訊

二、客戶端實現

1、建立socketID

/**
     1: 建立socket
     引數
     domain:協議域,又稱協議族(family)。常用的協議族有AF_INET、AF_INET6、AF_LOCAL(或稱AF_UNIX,Unix域Socket)、AF_ROUTE等。協議族決定了socket的地址型別,在通訊中必須採用對應的地址,如AF_INET決定了要用ipv4地址(32位的)與埠號(16位的)的組合、AF_UNIX決定了要用一個絕對路徑名作為地址。
     type:指定Socket型別。常用的socket型別有SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等。流式Socket(SOCK_STREAM)是一種面向連線的Socket,針對於面向連線的TCP服務應用。資料報式Socket(SOCK_DGRAM)是一種無連線的Socket,對應於無連線的UDP服務應用。
     protocol:指定協議。常用協議有IPPROTO_TCP、IPPROTO_UDP、IPPROTO_STCP、IPPROTO_TIPC等,分別對應TCP傳輸協議、UDP傳輸協議、STCP傳輸協議、TIPC傳輸協議。
     注意:1.type和protocol不可以隨意組合,如SOCK_STREAM不可以跟IPPROTO_UDP組合。當第三個引數為0時,會自動選擇第二個引數型別對應的預設協議。
     返回值:
     如果呼叫成功就返回新建立的套接字的描述符,如果失敗就返回INVALID_SOCKET(Linux下失敗返回-1)
     */
    
    int socketID = socket(AF_INET,SOCK_STREAM,0);
		self.clinenId= socketID;
    if (socketID == -1)
    {
        NSLog(@"建立socket 失敗");
        return;
    }
複製程式碼

2、建立連線

//htons : 將一個無符號短整型的主機數值轉換為網路位元組順序,不同cpu 是不同的順序 (big-endian大尾順序,little-endian小尾順序)
#define SocketPort htons(8040)
//inet_addr是一個計算機函式,功能是將一個點分十進位制的IP轉換成一個長整數型數
#define SocketIP   inet_addr("127.0.0.1")


/**
     __uint8_t    sin_len;          假如沒有這個成員,其所佔的一個位元組被併入到sin_family成員中
     sa_family_t    sin_family;     一般來說AF_INET(地址族)PF_INET(協議族)
     in_port_t    sin_port;         // 埠
     struct    in_addr sin_addr;    // ip
     char        sin_zero[8];       沒有實際意義,只是為了 跟SOCKADDR結構在記憶體中對齊
     */
 
    struct sockaddr_in socketAddr;
    socketAddr.sin_family = AF_INET;
    socketAddr.sin_port   = SocketPort;
    struct in_addr socketIn_addr;
    socketIn_addr.s_addr  = SocketIP;
    socketAddr.sin_addr   = socketIn_addr;

/**
     引數
     引數一:套接字描述符
     引數二:指向資料結構sockaddr的指標,其中包括目的埠和IP地址
     引數三:引數二sockaddr的長度,可以通過sizeof(struct sockaddr)獲得
     返回值
     成功則返回0,失敗返回非0,錯誤碼GetLastError()。
     */
    // ip
    int result = connect(socketID,(const struct sockaddr *)&socketAddr,sizeof(socketAddr));

    if (result != 0) 
    {
        NSLog(@"連結失敗");
        return;
    }
    NSLog(@"連結成功");
複製程式碼

3、傳送資料

#pragma mark - 傳送訊息

- (IBAction)sendMsgAction:(id)sender 
{
    /**
     3: 傳送訊息
     s:一個用於標識已連線套介面的描述字。
     buf:包含待發送資料的緩衝區。
     len:緩衝區中資料的長度。
     flags:呼叫執行方式。
     
     返回值
     如果成功,則返回傳送的位元組數,失敗則返回SOCKET_ERROR
     一箇中文對應 3 個位元組!UTF8 編碼!
     */
    if (self.sendMsgContent_tf.text.length==0) 
    {
        NSLog(@"訊息為空,無法傳送");
        return;
    }
    const char *msg = self.sendMsgContent_tf.text.UTF8String;
    ssize_t sendLen = send(self.clinenId,msg,strlen(msg),0);
    NSLog(@"傳送了:%ld位元組",sendLen);
    [self showMsg:self.sendMsgContent_tf.text msgType:0];
    self.sendMsgContent_tf.text = @"";
}
複製程式碼

4、監聽接收資料

#pragma mark - 接受資料

dispatch_async(dispatch_get_global_queue(0,0),^{
        [self recvMsg];
    });

- (void)recvMsg
{
    // 4. 接收資料
    /**
     引數
     1> 客戶端socket
     2> 接收內容緩衝區地址
     3> 接收內容快取區長度
     4> 接收方式,0表示阻塞,必須等待伺服器返回資料
     
     返回值
     如果成功,則返回讀入的位元組數,失敗則返回SOCKET_ERROR
     */
    
    while (1) 
    {
        uint8_t buffer[1024];
        ssize_t recvLen = recv(self.clinenId,buffer,sizeof(buffer),0);
        NSLog(@"接收到了:%ld位元組",recvLen);
        // 判斷如果 0  下面會奔潰
        if (recvLen==0) 
        {
            self.restartId ++;
            if (self.restartId > 3)
            {
                self.restartId = 0;
                return;
            }
            NSLog(@"此次傳輸長度為0 如果下次還為0 請檢查連線");
            continue;
        }
        
        // 接收到的資料轉換
        NSData *recvData  = [NSData dataWithBytes:buffer length:recvLen];
        NSString *recvStr = [[NSString alloc] initWithData:recvData encoding:NSUTF8StringEncoding];
        NSLog(@"%@",recvStr);
        self.restartId = 0;

        dispatch_async(dispatch_get_main_queue(),^{
            [self showMsg:recvStr msgType:1];
        });
    }
}
複製程式碼

5、關閉

close(self.clinenId);

    if (self.clinenId) 
    {
        // 7: 關閉socket連線
        int close_result = close(self.clinenId);
        
        if (close_result == -1) 
        {
            NSLog(@"socket 關閉失敗");
            return;
        }
      	else
      	{
            NSLog(@"socket 關閉成功");
        }
    }
複製程式碼

三、GCDAsySocket應用

1、連結

#pragma mark - 連線socket
- (IBAction)didClickConnectSocket:(id)sender
{
    // 建立socket
    if (self.socket == nil)
      	// 要自己寫併發佇列
      	// 其內部為同步
        self.socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_global_queue(0,0)];
    // 連線socket
    if (!self.socket.isConnected)
    {
        NSError *error;
        [self.socket connectToHost:@"127.0.0.1" onPort:8090 withTimeout:-1 error:&error];
        if (error) NSLog(@"%@",error);
    }
}
複製程式碼

2、傳送

#pragma mark - 傳送
- (IBAction)didClickSendAction:(id)sender 
{
    NSData *data = [self.contentTF.text dataUsingEncoding:NSUTF8StringEncoding];
    [self.socket writeData:data withTimeout:-1 tag:10086];
}
複製程式碼

3、關閉

#pragma mark - 關閉socket
- (IBAction)didClickCloseAction:(id)sender 
{
    [self.socket disconnect];
    self.socket = nil;
}
複製程式碼

4、代理

#pragma mark - GCDAsyncSocketDelegate

// 需要二次封裝block
// socketmanager直接呼叫資料


//已經連線到伺服器
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(nonnull NSString *)host port:(uint16_t)port
{
    NSLog(@"連線成功 : %@---%d",host,port);
    [self.socket readDataWithTimeout:-1 tag:10086];
  	// -1 代表永久監聽不失效
}

// 連線斷開
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err
{
    NSLog(@"斷開 socket連線 原因:%@",err);
}

//已經接收伺服器返回來的資料
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag
{
    NSLog(@"接收到tag = %ld : %ld 長度的資料",tag,data.length);
    [self.socket readDataWithTimeout:-1 tag:10086]; // 收到後要標記,不然就是一次性
}

//訊息傳送成功 代理函式 向伺服器 傳送訊息
- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag
{
    NSLog(@"%ld 傳送資料成功",tag);
}
複製程式碼

5、斷線重連

#pragma mark - 重連
- (IBAction)didClickReconnectAction:(id)sender 
{
    // 建立socket
    if (self.socket == nil)
        self.socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_global_queue(0,0)];
    // 連線socket
    if (!self.socket.isConnected){
        NSError *error;
        [self.socket connectToHost:@"127.0.0.1" onPort:8090 withTimeout:-1 error:&error];
        if (error) NSLog(@"%@",error);
    }
}
複製程式碼

四、粘包與拆包

1、概念

當資料太大時,因為頻寬限制,需要對資料進行分段處理

比如頻寬是1000,你要的東西的大小是1800,第一次給你傳1000,第二次又給你傳1000,多出來的200怎麼區分?

  • 做標識<資料段1><資料段2>通過分隔符實現,使資料按照規則展示

  • 通過傳送一個數據的長度+資料的型別+資料

    #pragma mark - 傳送資料格式化
    - (void)sendData:(NSData *)data dataType:(unsigned int)dataType
    {
        NSMutableData *mData = [NSMutableData data];
        // 計算資料總長度 data
        unsigned int dataLength = 4+4+(int)data.length;// 資料長度+資料型別+原資料長度=總資料長度
        NSData *lengthData = [NSData dataWithBytes:&dataLength length:4];
        [mData appendData:lengthData];// 將資料長度拼接入資料
        
        // 資料型別 data
        // 2.拼接指令型別(4~7:指令)
        NSData *typeData = [NSData dataWithBytes:&dataType length:4];
        [mData appendData:typeData];
        
        // 最後拼接資料
        [mData appendData:data];
        NSLog(@"傳送資料的總位元組大小:%ld",mData.length);
        
        // 發資料
        [self.socket writeData:mData withTimeout:-1 tag:10086];
    }
    複製程式碼
  • 心跳 - 反向心跳

    有時候socket斷開是監聽不到的,比如負載的時候

    保證彼此的連結-防止資料丟包

    間隔時間不能太短或太長

  • 重連機制

    一般在websocket