前言
借助一些工具库,iOS 设备可以配置成为服务器,在此之上可以做许多有意思的事情。例如,HttpServerDebug 基于这种能力,提供了现场调试 iOS App 的能力。
CocoaHTTPServer 是比较早期的提供服务器能力的库,(基础 Socket 通信能力由 CocoaAsyncSocket 提供,)现在已不再维护。
GCDWebServer 也是一个提供服务器能力的工具,基于 GCD 实现,当前仍在维护中。
HSD(HttpServerDebug) 基于 GCDWebServer。HSD 希望能够主动的把信息发送到前端,这依赖于 WebSocket 协议,但是 GCDWebServer 不支持。
HSD 对 GCDWebServer 进行二次开发,增加 WebSocket 能力,并且不再同步原始 GCDWebServer 库。这是支持 WebSocket 协议的一次实践,并没有完备的支持 WebSocket 的所有方面,如新版本的 WebSochet 协议、加密数据传输等。
Socket 实现
Http 和 WebSocket 都是应用层的协议,其底层都依赖 Socket 进行通信。
GCDWebServer 中,Socket 的建立使用 POSIX C 函数实现。生成的核心数据对象是一对 Socket 文件描述符,分别代表服务端和客户端。对 Socket 文件描述符的监听、读取、写入均使用 GCD 函数进行了封装。
WebSocket 协议
WebSocket 协议是借用 HTTP 101 switch protocol 来完成协议转换,从 HTTP 协议切换成 WebSocket 通信协议。一个典型的建立连接请求和响应头如下,具体含义见参考文献链接。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 Request Headers GET ws://localhost:5555/ HTTP/1.1 Host: localhost:5555 Connection: Upgrade Pragma: no-cache Cache-Control: no-cache Upgrade: websocket Origin: http://localhost:5555 Sec-WebSocket-Version: 13 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36 Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9,en;q=0.8 Cookie: locale=zh-cn Sec-WebSocket-Key: qtAGynUVRNU3JdTH1dsQiA== Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits Response Headers HTTP/1.1 101 Web Socket Protocol Handshake WebSocket-Location: ws://localhost:5555/ Sec-WebSocket-Accept: We1qmJgFvf8w3cDqTuUO5B6lrNA= Upgrade: WebSocket Connection: Upgrade WebSocket-Origin: http://localhost:5555
WebSocket 协议传输的数据以 Frame 为单位,每个 Frame 都有严格的数据结构,如下表所示。其中每个位以字节流形式考察,具体含义见参考文献链接。
0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 |
+ - - - - - - - - - - - - - - - +-------------------------------+
| |Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---------------------------------------------------------------+
持有关系
如上图所示,增加 WebSocket 协议支持后,对象实例的持有关系有一些变化。GCDWebServer 表示服务器实例,GCDWebServerConnection 表示一次通信连接实例。
GCDWebServer 实例和 GCDWebServerConnection 实例的对应关系是 1 : n,这在修改前后都是一样的。修改前,GCDWebServerConnection 实例没有显式的声明被某个对象持有,其内部实现中,dispatch_read 和 dispatch_write 的 block 持有其自身,并在 block 执行结束后释放。
修改后,明确了 GCDWebServerConnection 实例的持有关系。GCDWebServer 实例接收到请求并实例出 GCDWebServerConnection 对象后,显示持有该对象。
HSDGWebSocket 是新增的负责处理 WebSocket 协议的类。GCDWebServerConnection 检测到当前请求为 WebSocket 请求时,实例出 HSDGWebSocket 对象,并显示持有。他们的对应关系是 1 : 1。
建立连接
1 2 3 4 5 6 7 8 9 10 11 12 13 14 + (BOOL )isWebSocketRequest:(NSDictionary *)requestHeaders { NSString *connectionHeaderValue = [requestHeaders objectForKey:@"Connection" ]; NSString *upgradeHeaderValue = [requestHeaders objectForKey:@"Upgrade" ]; BOOL isWebSocket = YES ; if (!upgradeHeaderValue || !connectionHeaderValue) { isWebSocket = NO ; } else if ([upgradeHeaderValue caseInsensitiveCompare:@"WebSocket" ] != NSOrderedSame ) { isWebSocket = NO ; } else if ([connectionHeaderValue rangeOfString:@"Upgrade" options:NSCaseInsensitiveSearch ].location == NSNotFound ) { isWebSocket = NO ; } return isWebSocket; }
如上代码所示,接收到请求后,根据请求头判断是否是 WebSocket 请求。如果是 WekSocket 请求,则发送对应的响应头,如下代码所示。请求和响应的格式和值见 WebSocket 协议的定义。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 - (void )sendResponseHeaders { NSDictionary *requestHeaders = CFBridgingRelease (CFHTTPMessageCopyAllHeaderFields (self .requestMessage)); NSString *origin = [requestHeaders objectForKey:@"Origin" ]; NSString *host = [requestHeaders objectForKey:@"Host" ]; NSString *secWebSocketKey = [requestHeaders objectForKey:@"Sec-WebSocket-Key" ]; NSURL *requestURL = CFBridgingRelease (CFHTTPMessageCopyRequestURL (self .requestMessage)); NSString *relativeString = [requestURL relativeString]; CFHTTPMessageRef responseMessage = CFHTTPMessageCreateResponse (kCFAllocatorDefault, 101 , CFSTR ("Web Socket Protocol Handshake" ), kCFHTTPVersion1_1); CFHTTPMessageSetHeaderFieldValue (responseMessage, CFSTR ("Connection" ), CFSTR ("Upgrade" )); CFHTTPMessageSetHeaderFieldValue (responseMessage, CFSTR ("Upgrade" ), CFSTR ("WebSocket" )); CFHTTPMessageSetHeaderFieldValue (responseMessage, CFSTR ("WebSocket-Origin" ), (__bridge CFStringRef )origin); NSString *locationValue = [NSString stringWithFormat:@"ws://%@%@" , host, relativeString]; CFHTTPMessageSetHeaderFieldValue (responseMessage, CFSTR ("WebSocket-Location" ), (__bridge CFStringRef )locationValue); NSString *guid = @"258EAFA5-E914-47DA-95CA-C5AB0DC85B11" ; NSString *acceptValue = [[secWebSocketKey stringByAppendingString:guid] dataUsingEncoding: NSUTF8StringEncoding ].sha1Digest.base64Encoded; if (acceptValue.length > 0 ) { CFHTTPMessageSetHeaderFieldValue (responseMessage, CFSTR ("Sec-WebSocket-Accept" ), (__bridge CFStringRef )acceptValue); } CFDataRef data = CFHTTPMessageCopySerializedMessage (responseMessage); [self writeData:(__bridge NSData *)data withCompletionBlock:^(BOOL sucess) {}]; CFRelease (data); }
发送和接收信息
下面两段代码分别是,从服务端发送信息到前端和服务端接收前端发来的信息。其中关于字节流的处理见 WebSocket 协议 Frame 的定义。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 - (void )sendMessage:(NSString *)msg { NSData *msgData = [msg dataUsingEncoding:NSUTF8StringEncoding ]; NSMutableData *data = nil ; NSUInteger length = msgData.length; if (length <= 125 ) { data = [NSMutableData dataWithCapacity:(length + 2 )]; [data appendBytes:"\x81" length:1 ]; UInt8 len = (UInt8 )length; [data appendBytes:&len length:1 ]; [data appendData:msgData]; } else if (length <= 0xFFFF ) { data = [NSMutableData dataWithCapacity:(length + 4 )]; [data appendBytes:"\x81\x7E" length:2 ]; UInt16 len = (UInt16 )length; [data appendBytes:(UInt8 []){len >> 8 , len & 0xFF } length:2 ]; [data appendData:msgData]; } else { data = [NSMutableData dataWithCapacity:(length + 10 )]; [data appendBytes:"\x81\x7F" length:2 ]; [data appendBytes:(UInt8 []){0 , 0 , 0 , 0 , (UInt8 )(length >> 24 ), (UInt8 )(length >> 16 ), (UInt8 )(length >> 8 ), length & 0xFF } length:8 ]; [data appendData:msgData]; } [self writeData:data withCompletionBlock:^(BOOL success) {}]; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 - (void )handleReceivedData:(NSData *)data { NSUInteger curPointPos = 0 ; NSUInteger msgLength; NSUInteger opCode; BOOL frameMasked; NSData *maskingKey; NSData *tmp = [[NSData alloc] initWithBytes:(UInt8 *)[data bytes] length:1 ]; curPointPos++; UInt8 frame = *(UInt8 *)[tmp bytes]; if ([self isValidWebSocketFrame:frame]) { opCode = frame & 0x0F ; } else { [self closeWebSocket]; return ; } tmp = [[NSData alloc] initWithBytes:((UInt8 *)[data bytes] + curPointPos) length:1 ]; curPointPos++; frame = *(UInt8 *)[tmp bytes]; frameMasked = WS_PAYLOAD_IS_MASKED(frame); NSUInteger length = WS_PAYLOAD_LENGTH(frame); if (length <= 125 ) { if (frameMasked) { maskingKey = [[NSData alloc] initWithBytes:((UInt8 *)[data bytes] + curPointPos) length:4 ]; curPointPos += 4 ; } msgLength = length; } else if (length == 126 ) { tmp = [[NSData alloc] initWithBytes:((UInt8 *)[data bytes] + curPointPos) length:2 ]; curPointPos += 2 ; UInt8 *pFrame = (UInt8 *)[tmp bytes]; NSUInteger length = ((NSUInteger )pFrame[0 ] << 8 ) | (NSUInteger )pFrame[1 ]; if (frameMasked) { maskingKey = [[NSData alloc] initWithBytes:((UInt8 *)[data bytes] + curPointPos) length:4 ]; curPointPos += 4 ; } msgLength = length; } else { tmp = [[NSData alloc] initWithBytes:((UInt8 *)[data bytes] + curPointPos) length:8 ]; curPointPos += 8 ; [self closeWebSocket]; return ; } NSData *remainingData = [[NSData alloc] initWithBytes:((UInt8 *)[data bytes] + curPointPos) length:msgLength]; if (frameMasked && maskingKey) { NSMutableData *masked = [remainingData mutableCopy]; UInt8 *pData = (UInt8 *)masked.mutableBytes; UInt8 *pMask = (UInt8 *)maskingKey.bytes; for (NSUInteger i = 0 ; i < msgLength; i++) { pData[i] = pData[i] ^ pMask[i % 4 ]; } remainingData = masked; } if (opCode == WS_OP_TEXT_FRAME) { NSString *msg = [[NSString alloc] initWithBytes:[remainingData bytes] length:msgLength encoding:NSUTF8StringEncoding ]; [self didReceiveMessage:msg]; if (self .isReadSourceSuspended) { dispatch_resume(self .readSource); self .socketFDBytesAvailable = 0 ; } } else { [self closeWebSocket]; } }
遇到的问题
HSDGWebSocket 实例无法释放问题
HSDGWebSocket 使用 dispatch_source 监听客户端 Socket 文件描述符。如果有数据可以读取,则 event_handler 会读取并解析数据。
最初的实现中设置了 dispatch_source_set_cancel_handler,如果接收到 WebSocket 关闭的信息,调用 dispatch_source_cancel,cancel_handler 会负责执行清理的工作,比如关闭 Socket 文件描述符。但是,测试下来发现,调用 dispatch_source_cancel 并没有触发 cancel_handler 的执行,并且,event_handler 的 block 实例不会释放,而且一直持有着 HSDGWebSocket 实例。
使用另外一种方法解决了这个问题。不再设置 dispatch_source_set_cancel_handler,如果接收到 WebSocket 关闭的信息,主动去执行清理的工作。并且 dispatch_source_set_event_handler 的 block 弱引用 HSDGWebSocket,解决 HSDGWebSocket 无法释放的问题。
dispatch_source_cancel 没有触发 cancel_handler 的原因还不知道。当前的做法可能仍然存在 dispatch_source_set_event_handler 的 block 实例一直未释放的问题。
参考文献:
学习WebSocket协议—从顶层到底层的实现原理(修订版)