基于SRWebSocket的WebSocket长连接

前言

搭建稳定及轻量的长链接能力,应用于业务方的消息提醒、状态更新等即时任务,同时也方便web应用的快速接入,最终选用了WebSocket作为通讯协议。

WebSocket是基于TCP的应用层协议,区别于MQTT、XMPP等聊天的应用协议,它是一个传输通讯协议,有自己的一套连接握手、以及数据传输的规范,WebSocket的数据传输是frame形式传输。Websocket 通过HTTP/1.1 协议的101状态码进行握手,在握手阶段与Http是相同的,握手成功后建立双向的socket通道。由于WebSocket的握手是基于Http协议,所以WebSocket的建联也支持常规的request配置,如请求头(包含cookie)、超时时长。

IOS的WebSocket建联大部分都是使用SRWebSocket-源码解析(facebook开源库)。

应用

pod 'SocketRocket'
通过cocoaPod引入SocketRocket库,或者直接到github下载(下载地址

基于SRWebsocket封装-MKWebSocket(下载地址),支持cookie设置、连接异常 | 网络抖动重连、ping-pong心跳保活。

1、WebSocket初始化

- (SRWebSocket *)webSocket {
    if (!_serverRequest && (!_serverLink || !_serverLink.length)) {
        NSLog(@"serverURL is invalid");
        return nil;
    }
    
    if (_webSocket == nil) {
        if (_serverRequest) {
            _webSocket = [[SRWebSocket alloc] initWithURLRequest:_serverRequest];
        } else {
            _webSocket = [[SRWebSocket alloc] initWithURL:[NSURL URLWithString:_serverLink]];
        }
        [_webSocket setDelegateDispatchQueue:_serailQueue];
        _webSocket.delegate = self;
    }
    return _webSocket;
}

由于websocket是基于http协议的握手,所以支持request的相关配置,可以在建联的同时把登录态也传给服务端。

NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"ws://host:port/path"]];
request.timeoutInterval = 25;
[request setValue:@"token_id=;session_id=;" forHTTPHeaderField:@"Cookie"];

如果只需要带参数,也可以通过url-params直接透传参数

NSString* URLString = [NSURL URLWithString:@"ws://host:port/path?token_id=&session_id="];

 2、WebSocket连接

if (self.socketState != SR_OPEN && elf.socketState != SR_CONNECTING) {
    [self.webSocket open];
}

- (void)webSocketDidOpen:(SRWebSocket *)webSocket {
    self.socketState = SR_OPEN;
}

如果登录态通过url或者request的形式传给服务,后端验证通过并建立链接就已是登录状态。当然登录态校验也可以放在socket建联之后,通过socket通道发送登录态校验包,后端检验ok回包确认,再把当前socket标记为登录状态 。

[self.webSocket sendData:@"{\"action\":\"checklogin\", \"type\":100661, \"data\":{\"token_id\":\"\",\"session_id\":\"\"}}"];

- (void)webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message {
    NSInteger type = [message integerForKey:@"type"];
    if (type == 100661) {
        NSInteger retCode = [message integerForKey:@"retCode"];
        if (retCode == 0) {
            // 登录成功
        }
    }
}

 3、WebSocket前台保活

1)断开重连检测

APP在前台时,由于网络抖动、后台长时间挂起等情况导致socket断开,需要重现连接。在后台会被系统挂起,可以通过APNS的形式接收信息。

/// 链接未连上时,定时检测重连
__weak typeof(self) weakSelf = self;
self.conntectTimer = [[GCDSource alloc] initWithTimeInterval:SOCKET_FAIL_RECONNECT_INTERVAL repeats:YES timerBlock:^{
    __strong typeof(weakSelf) strongSelf = weakSelf;
    [strongSelf reConnect];
}];

/// 网络抖动,检测重连
__weak typeof(self) weakSelf = self;
AFNetworkReachabilityManager *manager = [AFNetworkReachabilityManager sharedManager];
[manager setReachabilityStatusChangeBlock:^(AFNetworkReachabilityStatus status) {
    __strong typeof(weakSelf) strongSelf = weakSelf;
    if (status == AFNetworkReachabilityStatusUnknown || status == AFNetworkReachabilityStatusNotReachable) {
        NSLog(@"网络不可用");
        strongSelf.reachabilityStatus = AFNetworkReachabilityStatusNotReachable;
    } else {
        NSLog(@"网络可用");
        strongSelf.reachabilityStatus = AFNetworkReachabilityStatusReachableViaWWAN;
        [strongSelf reConnect];
    }
}];
[manager startMonitoring];

/// 切回前台后,检测重连
[[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationWillEnterForegroundNotification object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) {
    [self reConnect];
    [self.conntectTimer resumeTimer];
}];
        
[[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidEnterBackgroundNotification object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) {
     [self.conntectTimer pauseTimer];
}];

2)Ping心跳检测

socket建联后,发送ping心跳包,检测服务是否有正常的pong回包。如果ping-pong没有形成回路,则将该链路标记为断开状态,并断开重连。

ping心跳包的作用:①防止网络延迟和设备无响应情况;②防止链接异常断开TCP层无感知的情况。 

WebSocket定义了ping-pong的数据类型,分别为0x9与0xA。SRWebSocket也支持ping包发送,以及pong包的回调。以下是具体的实现。

/// Ping心跳包校验,超过时长:MAX_SOCKET_PING_NUMBER * SOCKET_PING_TIME_INTERVAL 秒
- (void)sendPing:(NSData *)data {
    if (_pingMQ > MAX_SOCKET_PING_NUMBER) {
        [self _open];
    }
    
    if (_socketState == SR_OPEN) {
        if (!data) {
            // ping-pong的标记由前后端协定
            data = [@"SocketTag" dataUsingEncoding:NSASCIIStringEncoding];
        }
        [self.webSocket sendPing:data];
        self.pingMQ++;
    }
}

- (void)webSocket:(SRWebSocket *)webSocket didReceivePong:(NSData *)pongPayload {
    NSString* pongTag = [[NSString alloc] initWithData:pongPayload encoding:NSASCIIStringEncoding];
    // ping-pong的标记由前后端协定
    if ([pongTag isEqualToString:@"SocketTag"]) {
        self.pingMQ = 0; /// 归零,链路正常
    }
}

如果不想通过webSocket的ping-pong能力实现,可以自定义业务ping-pong协定,与上方建联后验证登录类似,多加一种数据类型即可。

4、WebSocket数据收发

前面已经大致讲到了数据的发送与接收,具体方法实现如下。

- (void)sendData:(NSString *)data {
    if (_socketState == SR_OPEN) {
        [self.webSocket send:data];
    }
}

- (void)webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message {
    MKWebSocketMessage* messageItem = [MKWebSocketMessage modelWithMessage:message];
}

5、webSocket异常回调

socket的异常需要处理重连逻辑,并设置最大重连次数。

- (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error {
    self.socketState = SR_CLOSED;
    /// 断开重连,并设置最大重连数
    if (self.reConnectCount < MAX_REPEAT_CONNECT_NUMBER && self.reachabilityStatus == AFNetworkReachabilityStatusReachableAvailable) {
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(REPEAT_CONNECT_INTERVAL * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            //延后重连,防止服务短时间重启的情况
            [self _open];
        });
        self.reConnectCount ++;
    } else {
        self.reConnectCount = 0;
    }
}

6、WebSocket断开

 websocket的断开分为很多种情况,具体code在SRWebSocket中也能看到。

1000为正常断开,也就是主动调用disconnect;1001为服务拒绝,后面的依次是协议错误、无句柄类型、无状态接收、不正常、无效utf8、...,这些都是socket断开的原因。

排除正常断开,同时前后端协议与数据正常的情况,需要对webSocket进行重连。

- (void)webSocket:(SRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean {
    self.socketState = SR_CLOSED;    
    if ( code == SRStatusCodeNormal) {
        /// 主动断开后销毁 Socket & Timer
    } else {
        /// 非主动断开,重新连接 
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(REPEAT_CONNECT_INTERVAL * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            //延后重连
            [self _open];
        });
    }
}

总结 

 本文主要是阐述了SRWebSocket的基本用法,以及常规异常处理,并提供建联与保活的代码库MKWebSocket - 支持模块扩展。由于项目不涉及到IM聊天相关,所以具体实现以及相关前后台协议拟定都相对简单。

猜你喜欢

转载自blog.csdn.net/z119901214/article/details/119658069