前言
搭建稳定及轻量的长链接能力,应用于业务方的消息提醒、状态更新等即时任务,同时也方便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聊天相关,所以具体实现以及相关前后台协议拟定都相对简单。