前言 
借助一些工具库,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协议—从顶层到底层的实现原理(修订版)