基于WebSocket协议的iOS端即时聊天
好好好久没有在cnblogs上写博客,不过在这里写的最早的一篇博客的时间戳,真是时间久远啊,那时候还没毕业。不在cnblogs的期间,在github pages、简书上写过博客,github pages的markdown还是不错的,不过百度不能检索到文章,也就是通过百度,永远无法导流搜索到我的文章(感动moving),简书感觉更适合抒情鸡汤,可能我不太能融入那个用户群体。不过现在我回来了,那些在github pages上的文章,我暂时也不迁移了,毕竟人生本来就不完美,提醒自己不能有强迫症(内心默念三次)。续上上一篇博客的时间(2016-04-29)继续回到这里,中间的间隔两年多,甚至更长。这段空白时间的大概情况介绍完了~
---------------------脏兮兮的分割线---------------------
言归正传,最近因为公司产品的需要,计划在移动端开发即时聊天的通讯功能。即时聊天的第三方SDK供应商也是非常多的,因为项目高度的自由定制性,数据隐私等方面的考虑,最终Server-Client端都由自己来实现,服务端采用worker man的PHP socket服务器架构。
在长连接双向通信上,选择的是WebSocket协议。开发主要负责iOS Client端的开发,按照开发第三方SDK的标准,将关键的部分封装起来,只留出必要的API供外部调用,将相关代码模块化,方便后期向公司其他项目中移植聊天模块。(不能自己坑自己,遇到移植的需求的可能性是非常大的,所以与其散漫的写代码,不如按照SDK的标准去做开发。)
WebSocket
WebSocket 协议在2008年诞生,2011年成为国际标准。WebSocket 协议本质上是一个基于 TCP 的协议。是建立在 TCP 协议之上的全双工通讯协议,与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器,所以服务器端的实现比较容易。协议标识符是ws,请求地址格式:ws://example.com:80/path
握手过程:
为了建立一个 WebSocket 连接,客户端首先要向服务器发起一个 HTTP 请求,这个请求和通常的 HTTP 请求不同,包含了一些附加头信息。如下所示:
客户端请求Header:
1 --- request header --- 2 GET /chat HTTP/1.1 3 Upgrade: websocket 4 Connection: Upgrade 5 Host: 127.0.0.1:8001 6 Origin: http://127.0.0.1:8001 7 Sec-WebSocket-Key: hj0eNqbhE/A0GkBXDRrYYw== 8 Sec-WebSocket-Version: 13
其中附加头信息"Upgrade: WebSocket"表明这是一个申请协议升级的 HTTP 请求,服务器端解析这些附加的头信息,根据Sec-WebSocket-Key的字符串,通过sha1算法处理,将response信息(sec-Websocket-Accept字符串)返回给客户端,客户端能成功解码字符串,就和服务器端的 WebSocket连接就建立起来了。
服务器的Response:
1 HTTP/1.1 101 Switching Protocols 2 Content-Length: 0 3 Upgrade: websocket 4 Sec-Websocket-Accept: ZEs+c+VBk8Aj01+wJGN7Y15796g= 5 Server: TornadoServer/4.5.1 6 Connection: Upgrade 7 Date: Wed, 21 Jun 2017 03:29:14 GMT
双方就可以通过这个连接通道自由的传递信息,并且这个连接会持续存在直到客户端或者服务器端的某一方主动的关闭连接。
使用封装Websocket的SocketRocket(Objective-C)
上面是WebSocket握手连接通信,而站在巨人的肩膀上,这里使用的是Github上facebook的SocketRocket项目,这是关于WebSocket的Objective-C的封装,提供简单的API,让开发者不用去跟底层协议打交道,而是关注于链路上的数据处理,逻辑层。关于SocketRocket的Features使用等,在Github上有详细介绍,使用起来也非常简单。需要注意SRWebSocketDelegate协议的相关方法:
//当收到服务器的Message时调用,这里的message是id类型,可以是NSString,也可以是NSData。
- (void)webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message;
//当与服务器建立连接时调用
- (void)webSocketDidOpen:(SRWebSocket *)webSocket;
//当发生未知错误的时调用,可能是网络原因等
- (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error;
//当关闭WebSocket时调用
- (void)webSocket:(SRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean;
//接收到服务器的Pong时调用
- (void)webSocket:(SRWebSocket *)webSocket didReceivePong:(NSData *)pongPayload;
//返回YES表示对messages进行转换,以NSString的形式发送,返回NO,表示跳过NSData->NSString的转换,直接以NSData来传递。默认YES
- (BOOL)webSocketShouldConvertTextFrameToString:(SRWebSocket *)webSocket;
使用方式与之前学Java时使用的socket通讯类似,大概流程如下所示:init websocket -> open -> connected -> sendMsg -> handle server response -> close
具体的代码也是非常容易在网上找到的,就不大段的贴代码了。
上述的通道握手建立并且能与服务器简单通信后,就要考虑各种情况的处理,包括断网,信号差等,就需要考虑断线重连,发送心跳包确定是否与服务器保持着连接的状态。
这里心跳包的发送是定时执行的,使用NSTimer的方式。
1 dispatch_main_async_safe(^{ 2 3 [self destoryHeartBeat]; 4 5 __weak typeof(self) weakSelf = self; 6 //心跳设置为3分钟,NAT超时一般为5分钟 7 _heartBeat = [NSTimer scheduledTimerWithTimeInterval:3 * 60 repeats:YES block:^(NSTimer * _Nonnull timer) { 8 NSLog(@"heart"); 9 //和服务端约定好发送什么作为心跳标识,尽可能的减小心跳包大小 10 [weakSelf sendHeartBeatMessage]; 11 }]; 12 [[NSRunLoop currentRunLoop]addTimer:_heartBeat forMode:NSRunLoopCommonModes]; 13 })
错误断网等重连的实现:
1 - (void)reConnect { 2 3 [self stopSocket]; 4 5 if (_connectInterval < 2) { 6 _connectInterval = 2; 7 }else{ 8 _connectInterval = _connectInterval + 2; 9 } 10 11 // 断开连接后每过n+2秒后重新建立一次连接 12 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_connectInterval * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ 13 [self startSocket]; 14 }); 15 }
一个好的与服务器连接的Websocket模块需要细细的打磨,这里展示的都是很粗糙的模块,需要根据以后的需求,出现的问题进行不断的修正,才能有一个好用的Websocket模块。想到了一句话:细节决定成败。所以打磨好生活工作学习中的每一个细节~