iOS开发——socket编程和GCDAsyncSocket

一、socket

1.网络体系结构和网络协议

在说socket之前,先要简单说一说网络体系结构。OSI(Open System Interconnection Reference, 开放式系统互联通信参考)将计算机网络体系结构划分为以下七层:

iOS开发——socket编程和GCDAsyncSocket_第1张图片
网络七层

其中媒体层是网络工程师所研究的对象,主机层则是用户所面向和关心的内容。

常应用到的传输协议有http协议、tcp/udp协议、ip协议分别对应于应用层、传输层、网络层。TCP/IP是传输层协议,主要解决数据如何在网络中传输;而HTTP是应用层协议,主要解决如何包装数据。

我们在传输数据时,可以只使用传输层(TCP/IP),那样的话由于没有应用层,便无法识别数据内容,如果想要使传输的数据有意义,则必须使用应用层协议,应用层协议很多,有HTTP、FTP、TELNET等等,也可以自己定义应用层协议。WEB使用HTTP作传输层协议,以封装HTTP文本信息,然 后使用TCP/IP做传输层协议将它发送到网络上。Socket是对TCP/IP协议的封装,Socket本身并不是协议,而是一个调用接口(API),通过Socket,我们才能使用TCP/IP协议。

2.Http和Socket连接区别

2.1 socket连接:

建立起一个TCP连接需要经过“三次握手”:
第一次握手:客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认;
第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;
第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。
握手过程中传送的包里不包含数据,三次握手完毕后,客户端与服务器才正式开始传送数据。断开连接时服务器和客户端均可以主动发起断开TCP连接的请求,断开过程需要经过“四次握手”。

socket连接就是所谓的长连接,理论上客户端和服务器端一旦加你其连接将不会主动断掉;但是由于各种环境因素可能会连接断开,比如说:服务器端或客户端主机down了,网络故障,或者两者之间长时间没有传输数据,网络防火墙可能会断开该连接以释放网络资源。所以当一个socket连接中没有数据传输的时候,那么为了维持连接需要发送心跳消息,具体心跳消息格式是开发者自己定义的。

2.2 HTTP连接

HTTP协议即超文本传送协议(HypertextTransfer Protocol ),是Web联网的基础,也是手机联网常用的协议之一,HTTP协议是建立在TCP协议之上的一种应用。
HTTP连接最显著的特点是客户端发送的每次请求都需要服务器回送响应,在请求结束后,会主动释放连接。从建立连接到关闭连接的过程称为“一次连接”。
1)在HTTP 1.0中,客户端的每次请求都要求建立一次单独的连接,在处理完本次请求后,就自动释放连接。
2)在HTTP 1.1中则可以在一次连接中处理多个请求,并且多个请求可以重叠进行,不需要等待一个请求结束后再发送下一个请求。
由于HTTP在每次请求结束后都会主动释放连接,因此HTTP连接是一种“短连接”,要保持客户端程序的在线状态,需要不断地向服务器发起连接请求。通常的 做法是即时不需要获得任何数据,客户端也保持每隔一段固定的时间向服务器发送一次“保持连接”的请求,服务器在收到该请求后对客户端进行回复,表明知道客户端“在线”。若服务器长时间无法收到客户端的请求,则认为客户端“下线”,若客户端长时间无法收到服务器的回复,则认为网络已经断开。

二.GCDAsyncSocket

在iOS开发中使用socket,一般都是用第三方库GCDAsyncSocket(虽然也有原生CFSocket)。
GCDAsyncSocket 下载地址: GCDAsyncSocket

使用之前需要先在项目引入ASyncSocket库:

  1. 把ASyncSocket库源码加入项目:只需要增加RunLoop目录中的AsyncSocket.h、AsyncSocket.m、AsyncUdpSocket.h和AsyncUdpSocket.m四个文件。
  2. 在项目增加CFNetwork框架:在Framework目录右健,选择Add-->Existing Files... , 选择 CFNetwork.framework

下面开始介绍一下如何使用ASyncSocket:

一般来说,一个用户只需要建立一个socket长连接,所以可以用单例类方便使用。

单例方法

// 创建单例
+ (Singleton *) sharedInstance
{
    static Singleton *sharedInstace = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedInstace = [[self alloc] initPrivate];
    });
    return sharedInstace;
}

// 私有创建方法,不公开
- (instancetype)initPrivate {
    if (self = [super init]) {
        _lockStr = @"1234";
    }
    return self;
}

// 废除init创建方法
- (instancetype)init {
    @throw [NSException exceptionWithName:@"初始化异常" reason:@"不允许通过init方法创建对象" userInfo:nil];
}

建立socket长连接

#define TIME_OUT 20

// 建立socket连接
-(void)socketConnectHost{
    _socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)];
    NSLog(@"连接服务器");
    NSError *error = nil;
    [_socket connectToHost:_socketHost onPort:_socketPort withTimeout:TIME_OUT error:&error];
}

// socket成功连接回调
-(void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port {
    NSLog(@"成功连接到%@:%d",host,port);
    _bufferData = [[NSMutableData alloc] init]; // 存储接收数据的缓存区
    [_socket readDataWithTimeout:-1 tag:99];
}

心跳###

@property (nonatomic, retain) NSTimer             *heartTimer;   // 心跳计时器

在连接成功的回调方法里,启动定时器,每隔2秒向服务器发送固定的消息来检测长连接。

// 心跳连接
-(void)longConnectToSocket{
    根据服务器要求发送固定格式的数据,假设为指令@"longConnect",但是一般不会是这么简单的指令
    NSString *longConnect = @"longConnect";
    NSData   *dataStream  = [longConnect dataUsingEncoding:NSUTF8StringEncoding];
    [_socket writeData:dataStream withTimeout:1 tag:1];
}

断开连接###

  • 主动断开
- (void)cutOffSocket {
    [_socket disconnect];
    _socket.userData =  @(SocketOfflineByUser);
    NSLog(@"断开连接");
}
  • 被动断开
-(void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err {
    if (err.code == 57) {
        _socket.userData = @(SocketOfflineByWifiCut); // wifi断开
    }
    else {
        _socket.userData =  @(SocketOfflineByServer);  // 服务器掉线
    }
    NSLog(@"断开连接,错误:%@",err);
}

错误码请见 sys/errno.h

发送消息

// 发消息
- (void)sendMessage:(NSData *)data {
    [_socket writeData:data withTimeout:TIME_OUT tag:10];
}

// wirte成功
-(void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag
{
    // 持续接收数据
    // 超时设置为附属,表示不会使用超时
    [_socket readDataWithTimeout:-1 tag:tag];
}

接收消息

-(void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
    // 在这里处理消息
    [self disposeBufferData:data];

    //持续接收服务端的数据
    [sock readDataWithTimeout:-1 tag:tag];
}

粘包

最后说说战报的问题和相关处理。粘包是指发送方发送的若干包数据到接收方接收时粘成一包,TCP传输往往会出现粘包。出现粘包现象的原因是多方面的,它既可能由发送方造成,也可能由接收方造成。发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一包数据。若连续几次发送的数据都很少,通常TCP会根据优化算法把这些数据合成一包后一次发送出去,这样接收方就收到了粘包数据。接收方引起的粘包是由于接收方用户进程不及时接收数据,从而导致粘包现象。这是因为接收方先把收到的数据放在系统接收缓冲区,用户进程从该缓冲区取数据,若下一包数据到达时前一包数据尚未被用户进程取走,则下一包数据放到系统接收缓冲区时就接到前一包数据之后,而用户进程根据预先设定的缓冲区大小从系统接收缓冲区取数据,这样就一次取到了多包数据。

为了避免粘包现象,可采取以下几种措施。一是对于发送方引起的粘包现象,用户可通过编程设置来避免,TCP提供了强制数据立即传送的操作指令push,TCP软件收到该操作指令后,就立即将本段数据发送出去,而不必等待发送缓冲区满;二是对于接收方引起的粘包,则可通过优化程序设计、精简接收进程工作量、提高接收进程优先级等措施,使其及时接收数据,从而尽量避免出现粘包现象;三是由接收方控制,将一包数据按结构字段,人为控制分多次接收,然后合并,通过这种手段来避免粘包。

以上提到的三种措施,都有其不足之处。第一种编程设置方法虽然可以避免发送方引起的粘包,但它关闭了优化算法,降低了网络发送效率,影响应用程序的性能,一般不建议使用。第二种方法只能减少出现粘包的可能性,但并不能完全避免粘包,当发送频率较高时,或由于网络突发可能使某个时间段数据包到达接收方较快,接收方还是有可能来不及接收,从而导致粘包。第三种方法虽然避免了粘包,但应用程序的效率较低,对实时应用的场合不适合。

一种比较周全的对策是:接收方创建一预处理线程,对接收到的数据包进行预处理,将粘连的包分开。具体的方法就是在发送数据是在数据前加入包头,接收时首先将待处理的接收数据流(长度为m)强行转换成预定的结构数据形式,并从中取出结构数据长度字段n,而后根据n计算得到第一包数据长度。
1)若n
2)若n=m,则表明数据流内容恰好是一完整结构数据,直接将其存入临时缓冲区即可。
3)若n>m,则表明数据流内容尚不够构成一完整结构数据,需留待与下一包数据合并后再行处理。

下面是我和服务器约定好的包头和包的类型

// 定义包头
typedef struct tagNetPacketHead
{
    int version;                      //版本
    int eMainType;                  //包类型主协议
    int eSubType;                    //包类型子协议
    unsigned int nLen;              //包体长度
} NetPacketHead;

// 定义发包类型
typedef struct tagNetPacket
{
    NetPacketHead netPacketHead;      //包头
    unsigned char *packetBody;      //包体
} NetPacket;

收到数据时先将收到的数据放到缓存中,然后进行上述判断。

- (void)disposeBufferData:(NSData *)data {
    @synchronized (self.lockStr) {
        [_bufferData appendData:data];
        while (_bufferData.length >= 16) {
            struct tagNetPacketHead head;

            [_bufferData getBytes:&head range:NSMakeRange(0, 16)];
            while (_bufferData.length >= 16 && !(head.version == 1 && head.eMainType > -10 && head.eMainType < 1000 && head.eSubType > - 10 && head.eSubType < 1000)) {
                int a = (int)_bufferData.length - 1;
                _bufferData = [_bufferData subdataWithRange:NSMakeRange(1, a)].mutableCopy;
                if (_bufferData.length >= 16) {
                    [_bufferData getBytes:&head range:NSMakeRange(0, 16)];
                }
            }
            
            BOOL isIn = !(head.nLen > (_bufferData.length - 16));
            if (isIn && _bufferData.length >= 16) {
                NSMutableData *pendingData = [NSMutableData data];
                if (head.eSubType == -1) {
                    pendingData = [_bufferData subdataWithRange:NSMakeRange(4, 4)].mutableCopy;
                    [pendingData appendData:[_bufferData subdataWithRange:NSMakeRange(16, head.nLen)]];
                }
                else {
                    pendingData = [_bufferData subdataWithRange:NSMakeRange(4, 8)].mutableCopy;
                    NSLog(@"%d", head.nLen);
                    [pendingData appendData:[_bufferData subdataWithRange:NSMakeRange(16, head.nLen)]];
                }

                [DisposeManager disposeData:pendingData num:head.eMainType];
                int totalLen = _bufferData.length;
                _bufferData = [_bufferData subdataWithRange:NSMakeRange(16 + head.nLen, totalLen - 16 - head.nLen)].mutableCopy;
            }
        }
    }
}

注意:在这里加入线程锁@synchronized (self.lockStr),防止缓冲区同时被多个线程访问发生缓冲区数据混乱。

发送数据时则需要将数据按照约定的结构进行处理,在前边加上包头。

- (NSMutableData *)linkDataWithVersion:(NSData *)versionData mainType:(int)mainType subType:(int)subType packetBody:(NSData *)packetBody{
    if (!versionData) {
        int version = 1;
        versionData = [NSMutableData dataWithBytes:&version length:sizeof(version)];
    }
    NSMutableData *mainTypeData = [NSMutableData dataWithBytes:&mainType length:sizeof(mainType)];

    NSMutableData *subTypeData = [NSMutableData dataWithBytes:&subType length:sizeof(subType)];

    unsigned int len;
    if (packetBody) {
        len = packetBody.length;
    }
    else {
        len = 0;
    }

    NSMutableData *lenData = [NSMutableData dataWithBytes:&len length:sizeof(len)];

    NSMutableData *sendData = [[NSMutableData alloc] init];
    [sendData appendData:versionData];
    [sendData appendData:mainTypeData];
    [sendData appendData:subTypeData];
    [sendData appendData:lenData];
    [sendData appendData:packetBody];

    return sendData.mutableCopy;
}

注:这里的mainTypeData和subTypeData只是和本工程相关的主协议和子协议,并不具备普遍性。
注:version和服务器端约定好是1。

你可能感兴趣的:(iOS开发——socket编程和GCDAsyncSocket)