iOS网络(四)socket简单应用

2,029 阅读6分钟

一、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