SocketRocket简单使用与源码解析

SocketRocket简单使用与源码解析

IOS小彩虹2021-08-20 9:33:51180A+A-

文章由两部分组成,第一部分通过对API的讲解来对SocketRocket的使用有个简单的了解。如果对SocketRocket的实现感兴趣的同学可以着重看看第二部分,在源码解析的过程中会穿插着Websocket协议的讲解,干货满满。

第一部分 - 简单使用

初始化

如果没有特殊需求,以下就能进行初始化并连接websocket

- (void)connectWithUrl:(NSString *)url {
    NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"%@",url]]];
    self.socket = [[SRWebSocket alloc] initWithURLRequest:request];
    self.socket.delegate = self;
    [self.socket open];
}

如果需要超时时间,需要在执行open方法前使用以下设置

NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"%@",url]]];
request.timeoutInterval = 30;

如果设置了超时时间,SRWebSocketopen方法中会起一个延迟任务,时间为timeoutInterval,当触发延迟任务时,webSocket还未连接完成则会报超时错误。

发送消息

通过send发送消息

[self.socket send:@"Hello!"];
  1. 如果是文本消息,需要传递NSString格式的数据。
  2. 如果是二进制消息,需要传递NSSData格式的数据。
  3. 如果传nil则内部会转成文本消息。
  4. 其他类型的消息,SRWebSocket不会进行处理。

代理回调

接收到服务端消息时的回调
- (void)webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message {
    NSLog(@"接收到消息 - %@", message);
}
  1. messageNSString类型或者NSData类型。
  2. 当为文字消息时,默认情况下SRWebSocket会将其转化为NSString
  3. 可以通过webSocketShouldConvertTextFrameToString代理修改是否自动转化为NSSting。
webSocket连接成功
- (void)webSocketDidOpen:(SRWebSocket *)webSocket {
    NSLog(@"webSocket已经打开");
}

触发此回调时表明webSocket已经连接成功,可以发送和接收消息了。

webSocket连接,发送或接收消息时失败
- (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error {
    NSLog(@"didFailWithError - %@", error);
}

webSocket连接,发送或接收消息时失败
- (void)webSocket:(SRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean {
    NSLog(@"关闭webSocket完成 Code:%ld  reason:%@",code,reason);
}
收到Pong的回调
- (void)webSocket:(SRWebSocket *)webSocket didReceivePong:(NSData *)pongPayload {
    NSLog(@"didReceivePong");
}
是否需要将内容转化为字符串
- (BOOL)webSocketShouldConvertTextFrameToString:(SRWebSocket *)webSocket {
    return YES;
}
  1. 默认是YES
  2. YES时将服务端发来的文本消息自动转化为NSString

第二部分 - 源码解析

WebSocket建立连接的过程

sequenceDiagram
Client->>Service: Input/OutputStream Socket Connection
Client->>Service: HTTP Request(Connection: Upgrade Upgrade: websocket 发起协议升级请求)
Service-->>Client: HTTP Response (101 Switching Protocols 完成协议升级)
Client-)Service: Websocket Data
Client-)Service: Websocket Data
Service-->>Client: Websocket Data

如上面所描述的WebSocket建立连接有以下几个过程:

  1. 客户端绑定数据输入/输出流并与服务端进行Socket连接。
  2. 首先客户端会向服务端发起HTTP请求,Request Header中会有两个字段,Connection: Upgrade表示需要将协议进行升级,Upgrade: websocket表明要升级为websocket。
  3. 服务端响应HTTP请求,返回状态码101表明协议升级完成。
  4. 之后客户端和服务器通过WebSocket协议进行通讯。

接下来我们可以通过以下几个过程为抓手去解读SRWebSocket源码,其中4和6放在一起来讲。

  1. SRWebSocket的初始化
  2. 数据输入输出流绑定Socket并进行连接
  3. 发起请求HTTP
  4. 接收HTTP请求服务端的响应
  5. 发送WebSocket Data
  6. 接收WebSocket Data

一. SRWebSocket的初始化

初始化过程如下

graph TD
initWithURLRequest: --> initWithURLRequest:protocols: --> initWithURLRequest:protocols:allowsUntrustedSSLCertificates: --> _SR_commonInit --> _initializeStreams

构造函数

- (id)initWithURLRequest:(NSURLRequest *)request protocols:(NSArray *)protocols allowsUntrustedSSLCertificates:(BOOL)allowsUntrustedSSLCertificates;
- (id)initWithURLRequest:(NSURLRequest *)request protocols:(NSArray *)protocols;
- (id)initWithURLRequest:(NSURLRequest *)request;

// Some helper constructors.
- (id)initWithURL:(NSURL *)url protocols:(NSArray *)protocols allowsUntrustedSSLCertificates:(BOOL)allowsUntrustedSSLCertificates;
- (id)initWithURL:(NSURL *)url protocols:(NSArray *)protocols;
- (id)initWithURL:(NSURL *)url;

所有的构造函数最终都走到initWithURLRequest:protocols:allowsUntrustedSSLCertificates:中,且其内部只是做一些赋值操作,并且调用_SR_commonInit函数。

_SR_commonInit函数

_SR_commonInit函数由以下几个部分组成:

// 获取协议头
NSString *scheme = _url.scheme.lowercaseString;
// 过滤协议头
assert([scheme isEqualToString:@"ws"] || [scheme isEqualToString:@"http"] || [scheme isEqualToString:@"wss"] || [scheme isEqualToString:@"https"]);
// 如果是wss和https则表识需要使用SSL加密
if ([scheme isEqualToString:@"wss"] || [scheme isEqualToString:@"https"]) {
    _secure = YES;
}

可以注意到,SRWebSocket并没有过虑掉http和https,在websocket连接时首先会进行一个http请求,然后再将http升级到websocket,所以此时使用ws或者http是没有区别的。

// 初始化准备状态
_readyState = SR_CONNECTING;
_consumerStopped = YES;
// 设置webSocket的版本
_webSocketVersion = 13;
// 创建串行工作队列
_workQueue = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL);
dispatch_queue_set_specific(_workQueue, (__bridge void *)self, maybe_bridge(_workQueue), NULL);

- (void)assertOnWorkQueue {
    assert(dispatch_get_specific((__bridge void *)self) == maybe_bridge(_workQueue));
}

使用dispatch_queue_set_specific_workQueue和当前对象进行绑定,然后通过在assertOnWorkQueue中 去判断当前执行的任务是否包含在当前工作队列中。

初始化成员变量:

_readBuffer = [[NSMutableData alloc] init];
_outputBuffer = [[NSMutableData alloc] init];
_currentFrameData = [[NSMutableData alloc] init];
_consumers = [[NSMutableArray alloc] init];
_consumerPool = [[SRIOConsumerPool alloc] init];
_scheduledRunloops = [[NSMutableSet alloc] init];
[self _initializeStreams];

_initializeStreams

- (void)_initializeStreams {
    // 判断端口号是否合法
    assert(_url.port.unsignedIntValue <= UINT32_MAX);
    uint32_t port = _url.port.unsignedIntValue;
    if (port == 0) {
    	// 如果端口号未进行赋值则进行默认设置
        if (!_secure) {
            // 如果未加密默认端口号为80,否则为443
            port = 80;
        } else {
            port = 443;
        }
    }
    NSString *host = _url.host;
    
    CFReadStreamRef readStream = NULL;
    CFWriteStreamRef writeStream = NULL;
    // 创建socket连接并且绑定输入输出流
    CFStreamCreatePairWithSocketToHost(NULL, (__bridge CFStringRef)host, port, &readStream, &writeStream);
    // 把非OC的指针指向OC并且转换成ARC来管理生命周期
    _outputStream = CFBridgingRelease(writeStream);
    _inputStream = CFBridgingRelease(readStream);
    // 输入输出流设置代理
    _inputStream.delegate = self;
    _outputStream.delegate = self;
}

二. 数据输入输出流绑定Socket并进行连接

graph TD
open --> openConnection --> _updateSecureStreamOptions
openConnection --> scheduleInRunLoop:forMode:

open函数

- (void)open;
{
    assert(_url);
    // 如果是正在连接的状态则断言报错,表明之前已经调用过 open 
    NSAssert(_readyState == SR_CONNECTING, @"Cannot call -(void)open on SRWebSocket more than once");
    // 防止自身被提前释放
    _selfRetain = self;
    // 如果设置超时时间则起一个定时任务
    if (_urlRequest.timeoutInterval > 0) {
        dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, _urlRequest.timeoutInterval * NSEC_PER_SEC);
        dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
            // 当延迟任务出发时还未连接成功则报超时错误
            if (self.readyState == SR_CONNECTING)
                [self _failWithError:[NSError errorWithDomain:@"com.squareup.SocketRocket" code:504 userInfo:@{NSLocalizedDescriptionKey: @"Timeout Connecting to Server"}]];
        });
    }
    [self openConnection];
}

_updateSecureStreamOptions

// 如果设置加密
if (_secure) {
    NSMutableDictionary *SSLOptions = [[NSMutableDictionary alloc] init];
    // kCFStreamPropertySocketSecurityLevel 套接字安全级别属性键
    // kCFStreamSocketSecurityLevelNegotiatedSSL 指定将可协商的最高级别安全协议设置为套接字流的安全协议。
    [_outputStream setProperty:(__bridge id)kCFStreamSocketSecurityLevelNegotiatedSSL forKey:(__bridge id)kCFStreamPropertySocketSecurityLevel];   
    // 如果使用了SSLPinned则设置不需要验证证书,SSLPinne需要再APP中内置证书
    if ([_urlRequest SR_SSLPinnedCertificates].count) {
        [SSLOptions setValue:@NO forKey:(__bridge id)kCFStreamSSLValidatesCertificateChain];
    }
    [_outputStream setProperty:SSLOptions
                        forKey:(__bridge id)kCFStreamPropertySSLSettings];
}

openConnection

- (void)openConnection {
    // 更新流的配置
    [self _updateSecureStreamOptions];
    if (!_scheduledRunloops.count) {
        [self scheduleInRunLoop:[NSRunLoop SR_networkRunLoop] forMode:NSDefaultRunLoopMode];
    }
    // 打开输入输出流
    [_outputStream open];
    [_inputStream open];
}

当打开流之后,输入输出流会和指定的host建立连接,当连接成功后Stream会回调代理stream:handleEvent:,并且此时eventCode的状态为NSStreamEventOpenCompleted

三. 发起HTTP请求

stream:handleEvent:中当eventCode的状态为NSStreamEventOpenCompleted时会执行didConnect操作。

stateDiagram-v2
OpenCompleted --> didConnect
OpenCompleted --> _pumpWriting
OpenCompleted --> _pumpScanner
didConnect--> _readHTTPHeader
_readHTTPHeader --> _readUntilHeaderCompleteWithCallback
_readUntilHeaderCompleteWithCallback --> _readUntilByteslengthcallback
_readUntilByteslengthcallback --> _addConsumerWithScannercallbackdataLength
_addConsumerWithScannercallbackdataLength --> _pumpScanner
_pumpScanner --> _innerPumpScanner
didConnect--> _writeData
_writeData--> _pumpWriting

didConnect解析:

1.设置Header

Websocket连接时首次发送HTTP请求时Header规范如下:

GET / HTTP/1.1
Host: localhost:8080
Origin: http://127.0.0.1:3000
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: 34v76lq3RNcgctw==O6xFTi3

其中头部字端意义如下:

  • Connection: Upgrade 表示要升级协议
  • Upgrade: websocket 表示要升级到websocket
  • Sec-WebSocket-Version: 13 表示websocket的版本,如果服务端不支持该版本,需要返回一个Sec-WebSocket-Version: header,里面包含服务端支持的版本号。
  • Sec-WebSocket-Key 对称密钥

2.设置Cookie

NSDictionary * cookies = [NSHTTPCookie requestHeaderFieldsWithCookies:[self requestCookies]];
for (NSString * cookieKey in cookies) {
    NSString * cookieValue = [cookies objectForKey:cookieKey];
    if ([cookieKey length] && [cookieValue length]) {
        CFHTTPMessageSetHeaderFieldValue(request, (__bridge CFStringRef)cookieKey, (__bridge CFStringRef)cookieValue);
    }
}

3.发送请求

//将request序列化为 NSData 
NSData *message = CFBridgingRelease(CFHTTPMessageCopySerializedMessage(request));
//发送 message 消息
[self _writeData:message];

3.读取响应的http的头部

[self _readHTTPHeader];

_writeData解析

- (void)_writeData:(NSData *)data {    
    [self assertOnWorkQueue];
    // 如果连接已经关闭则直接返回
    if (_closeWhenFinishedWriting) {
    	return;
    }
    // 输出缓存拼接需要发送的数据
    [_outputBuffer appendData:data];
    [self _pumpWriting];
}

_outputBuffer是存储需要发送数据的缓存区,_outputBufferOffset是用来记录已发送数据的位置。

_pumpWriting解析

1.发送数据

// 获取发将要发送数据的长度
NSUInteger dataLength = _outputBuffer.length;
// _outputBufferOffset 记录已经发送数据的偏移量 且 输出流可以被写入
if (dataLength - _outputBufferOffset > 0 && _outputStream.hasSpaceAvailable) {
    // write:maxLength:执行后会返回已被写入数据的大小,被写入的部分会发送给服务器
    NSInteger bytesWritten = [_outputStream write:_outputBuffer.bytes + _outputBufferOffset maxLength:dataLength - _outputBufferOffset];
    // 当写入大小为-1则表明写入数据失败
    if (bytesWritten == -1) {
        [self _failWithError:[NSError errorWithDomain:SRWebSocketErrorDomain code:2145 userInfo:[NSDictionary dictionaryWithObject:@"Error writing to stream" forKey:NSLocalizedDescriptionKey]]];
         return;
    }
    // 记录被加入的数据
    _outputBufferOffset += bytesWritten;
    // 如果被写入的数据大于4096 且 大于数据总长度的一半 则清除已写入的数据
    if (_outputBufferOffset > 4096 && _outputBufferOffset > (_outputBuffer.length >> 1)) {
        // 重置输出缓存区,释放无用内存
        _outputBuffer = [[NSMutableData alloc] initWithBytes:(char *)_outputBuffer.bytes + _outputBufferOffset length:_outputBuffer.length - _outputBufferOffset];
        _outputBufferOffset = 0;
    }
}

2.当同时满足以下四个条件时会关闭:

  • 标记为写完成关闭
  • 输出的bufeer - 偏移量 = 0
  • 输入流正在打开
  • 首次关闭发送
if (_closeWhenFinishedWriting &&
    (dispatch_data_get_size(_outputBuffer) - _outputBufferOffset) == 0 &&
    (_inputStream.streamStatus != NSStreamStatusNotOpen &&
    _inputStream.streamStatus != NSStreamStatusClosed) &&
    !_sentClose) {
    _sentClose = YES;

    @synchronized(self) {
    	// 关闭输入输出流
        [_outputStream close];
        [_inputStream close];
		
		// 移除RunLoop
        for (NSArray *runLoop in [_scheduledRunloops copy]) {
            [self unscheduleFromRunLoop:[runLoop objectAtIndex:0] forMode:[runLoop objectAtIndex:1]];
        }
    }

    if (!_failed) {
    // 触发关闭websocket的回调
        [self.delegateController performDelegateBlock:^(id<SRWebSocketDelegate>  _Nullable delegate, SRDelegateAvailableMethods availableMethods) {
            if (availableMethods.didCloseWithCode) {
                [delegate webSocket:self didCloseWithCode:_closeCode reason:_closeReason wasClean:YES];
            }
        }];
    }
	// 关闭定时器
    [self _scheduleCleanup];
}

四. 接收服务端响应

InputStream接收到服务端发来的数据时其代理stream:handleEvent:中的eventCodeNSStreamEventHasBytesAvailable

graph TD
HasBytesAvailable --> _pumpScanner
_pumpScanner --> _innerPumpScanner
_innerPumpScanner --> consumer.consumer
consumer.consumer --> _readUntilByteslengthcallback
_innerPumpScanner --> consumer.handler
consumer.handler --> _HTTPHeadersDidFinish
_HTTPHeadersDidFinish --> _readFrameNew
_readFrameNew --> _readFrameContinue
_readFrameContinue --> _handleFrameHeader.curData
_handleFrameHeader.curData --> _handleFrameWithData.opCode
_handleFrameHeader.curData --> _readFrameContinue
_handleFrameWithData.opCode --> _readFrameContinue

stream:handleEvent:eventCodeNSStreamEventHasBytesAvailable时代码如下:

const int bufferSize = 2048;
uint8_t buffer[bufferSize];
//如果有可读字节
while (_inputStream.hasBytesAvailable) {
    // 读取数据,读取2048
    NSInteger bytes_read = [_inputStream read:buffer maxLength:bufferSize];
    // bytes_read为真正读取数据的大小
    if (bytes_read > 0) {
        // 拼接数据
        [_readBuffer appendBytes:buffer length:bytes_read];
    } else if (bytes_read < 0) {
        // bytes_read为-1时代表读取失败
        [self _failWithError:_inputStream.streamError];
    }
    //如果读取的不等于最大的,说明读完了,跳出循环
    if (bytes_read != bufferSize) {
        break;
    }
};
//开始扫描,看消费者什么时候消费数据
[self _pumpScanner];

_pumpScanner解析

- (void)_pumpScanner {
    [self assertOnWorkQueue];
    // 如果正在进行扫描则直接返回
    if (!_isPumping) {
        _isPumping = YES;
    } else {
        return;
    }
    // 当无消费者,且读缓存区已经无数据时结束循环
    while ([self _innerPumpScanner]) {
    }
    _isPumping = NO;
}

_innerPumpScanner解析

1.判断当前读缓存区里的数据是否已经处理完成,如果处理完成则直接返回。

BOOL didWork = NO;
// 当处于关闭关闭状态时直接返回NO
if (self.readyState >= SR_CLOSED) {
    return didWork;
}

// 如果消费者为空直接返回NO
if (!_consumers.count) {
    return didWork;
}
// 获取读缓冲区的大小
size_t readBufferSize = dispatch_data_get_size(_readBuffer);
// 如果缓冲区无数据则直接返回NO
size_t curSize = readBufferSize - _readBufferOffset;
if (!curSize) {
    return didWork;
}

2.从读缓存中获取本次需要处理的数据

// 获取第一个消费者
SRIOConsumer *consumer = [_consumers objectAtIndex:0];
size_t bytesNeeded = consumer.bytesNeeded;
size_t foundSize = 0;
// 如果消费者的consumer回调存在则触发consumer回调获取匹配数据的大小
if (consumer.consumer) {
    // 获取还未读取的数据并将其转化为NSData
    NSData *subdata = (NSData *)dispatch_data_create_subrange(_readBuffer, _readBufferOffset, readBufferSize - _readBufferOffset);
    // 获取获取匹配的数据大小
    foundSize = consumer.consumer(subdata);
} else {
    assert(consumer.bytesNeeded);
    // 如果当前剩余的数据大于消费者所需要的字节数,则此次读取消费者所需要的字节数的数据
    if (curSize >= bytesNeeded) {
        foundSize = bytesNeeded;
    } else if (consumer.readToCurrentFrame) {
        // 如果当前剩余的数据小于消费者所需要的字节数的数据
        foundSize = curSize;
    }
}

SRWebSocket中只有注册header的消费者时会在consumer.consumer添加回调函数,其具体代码在下面_readUntilBytes:length:callback:中。

3.从读缓存中读取本次需要处理的数据

//当消费者读取到当前帧或者找到Size
if (consumer.readToCurrentFrame || foundSize) {
    // 获取未处理的切片数据
    dispatch_data_t slice = dispatch_data_create_subrange(_readBuffer, _readBufferOffset, foundSize);
    // 记录读取到的数据
    _readBufferOffset += foundSize;
    // 当已处理的数据大于默认Buffer的阈值且大于已读取数据的一半
    if (_readBufferOffset > SRDefaultBufferSize() && _readBufferOffset > readBufferSize / 2) {
        // 重新创建已读数据,释放已处理的数据
        _readBuffer = dispatch_data_create_subrange(_readBuffer, _readBufferOffset, readBufferSize - _readBufferOffset);
        _readBufferOffset = 0;
    }
    // 是否要对数据载荷进行反掩码操作
    if (consumer.unmaskBytes) {
        // 复制当前切片数据
        __block NSMutableData *mutableSlice = [slice mutableCopy];
        NSUInteger len = mutableSlice.length;
        uint8_t *bytes = mutableSlice.mutableBytes;
        // 对当前数据的每一个字节进行反掩码操作
        for (NSUInteger i = 0; i < len; i++) {
            bytes[i] = bytes[i] ^ _currentReadMaskKey[_currentReadMaskOffset % sizeof(_currentReadMaskKey)];
            _currentReadMaskOffset += 1;
        }
        // 重置切片数据,清除原始数据
        slice = dispatch_data_create(bytes, len, nil, ^{
            mutableSlice = nil;
        });
    }
    // 是否读取当前帧
    if (consumer.readToCurrentFrame) {
        // 遍历切片数据对象的各个内存
        dispatch_data_apply(slice, ^bool(dispatch_data_t region, size_t offset, const void *buffer, size_t size) {
            // 将切片中的数据拼接在当前帧数据上
            [_currentFrameData appendBytes:buffer length:size];
            return true;
        });
        //在SRWebSocket中未发现具体使用
        _readOpCount += 1;
        // 如果当前帧是文本格式数据
        if (_currentFrameOpcode == SROpCodeTextFrame) {
            // 验证数据是否是UTF8
            size_t currentDataSize = _currentFrameData.length;
            if (_currentFrameOpcode == SROpCodeTextFrame && currentDataSize > 0) {
                size_t scanSize = currentDataSize - _currentStringScanPosition;
                NSData *scan_data = [_currentFrameData subdataWithRange:NSMakeRange(_currentStringScanPosition, scanSize)];
                int32_t valid_utf8_size = validate_dispatch_data_partial_string(scan_data);

                if (valid_utf8_size == -1) {
                    [self closeWithCode:SRStatusCodeInvalidUTF8 reason:@"Text frames must be valid UTF-8"];
                    dispatch_async(_workQueue, ^{
                        [self closeConnection];
                    });
                    return didWork;
                } else {
                    _currentStringScanPosition += valid_utf8_size;
                }
            }
        }
        // 消费者需要处理的数据减去此次处理的数据
        consumer.bytesNeeded -= foundSize;
        // 触发回调函数handler
        // 当消费者处理完成数据则移除消费者数据并将其放入消费者缓存池
        if (consumer.bytesNeeded == 0) {
            [_consumers removeObjectAtIndex:0];
            consumer.handler(self, nil);
            [_consumerPool returnConsumer:consumer];
            didWork = YES;
        }
    } else if (foundSize) {
        // 如果没有读取当前帧
        // 触发回调函数handler
        // 当消费者处理完成数据则移除消费者数据并将其放入消费者缓存池
        [_consumers removeObjectAtIndex:0];
        consumer.handler(self, (NSData *)slice);
        [_consumerPool returnConsumer:consumer];
        didWork = YES;
    }
}

_readUntilBytes:length:callback:解析

注册consumer回调,此回调的作用是扫描接收的data数据,识别header并将其大小返回。 因为每个header都以\r\n结尾,并且最后一行加上一个额外的空行\r\n,所以只要匹配了{'\r', '\n', '\r', '\n'}则就代表是响应头。

static const char CRLFCRLFBytes[] = {'\r', '\n', '\r', '\n'};
- (void)_readUntilHeaderCompleteWithCallback:(data_callback)dataHandler {
    [self _readUntilBytes:CRLFCRLFBytes length:sizeof(CRLFCRLFBytes) callback:dataHandler];
}

- (void)_readUntilBytes:(const void *)bytes length:(size_t)length callback:(data_callback)dataHandler {
    // bytes = CRLFCRLFBytes length = CRLFCRLFBytes中字节的数量
    stream_scanner consumer = ^size_t(NSData *data) {
        __block size_t found_size = 0;
        __block size_t match_count = 0;
        // 获取传来数据的长度
        size_t size = data.length;
        const unsigned char *buffer = data.bytes;
        // 将data与边界符 CRLFCRLFBytes进行匹配,将完全匹配后找到的数据大小返回
        // 遍历data中所有的字节
        for (size_t i = 0; i < size; i++ ) {
            // 如果data当前字节和CRLFCRLFBytes的当前字节相同则匹配度加1
            if (((const unsigned char *)buffer)[i] == ((const unsigned char *)bytes)[match_count]) {
                // 匹配度加1
                match_count += 1;
                // 如果匹配度等于CRLFCRLFBytes的长度则返回
                if (match_count == length) {
                    found_size = i + 1;
                    break;
                }
            } else {
                // 如果不相同则匹配度清0重新匹配
                match_count = 0;
            }
        }
        return found_size;
    };
    [self _addConsumerWithScanner:consumer callback:dataHandler];
}

_HTTPHeadersDidFinish通过HTTP Response Header来判定升级为WebSocket协议是否成功,如果成功则通过_readFrameContinue解析数据。

_readFrameContinue解析

_readFrameContinue中首会添加一个consumer来处理WebSocket数据中的frame_header。

在解析源码前先简单了解一下WebSocket协议。

 0                   1                   2                   3
  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
 +-+-+-+-+-------+-+-------------+-------------------------------+
 |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 ...                |
 +---------------------------------------------------------------+

Fin: 1个比特。

  • 等于1表明这是消息的最后一个分片
  • 等于0表明这不是消息的最后一个分片

Opcode: 4个比特。Opcode的值决定了应该如何解析后续的数据。如果操作代码是不认识的,那么接收端应该断开连接。

  • %x0:表示一个延续帧。当Opcode为0时,表示本次数据传输采用了数据分片,当前收到的数据帧为其中一个数据分片。
  • %x1:表示这是一个文本帧
  • %x2:表示这是一个二进制帧(
  • %x3-7:保留的操作代码,用于后续定义的非控制帧。
  • %x8:表示连接断开。
  • %x9:表示这是一个ping操作。
  • %xA:表示这是一个pong操作。
  • %xB-F:保留的操作代码,用于后续定义的控制帧。

Mask:标识是否对数据进行掩码操作。从客户端向服务端发送消息需要掩码操作,服务端向客户端发送消息不需要掩码操作,当服务端接收到消息如果没有进行掩码操作,服务端需要断开连接。在进行掩码操作操作时在Masking-key中会定义一个掩码键,并通过掩码键来对数据进行反掩码。

Payload length:数据的长度

Masking-key:掩码键,32位的随机数

[self _addConsumerWithDataLength:2 callback:^(SRWebSocket *self, NSData *data) {
    // 初始化 frame_header
    __block frame_header header = {0};
    const uint8_t *headerBuffer = data.bytes;
    assert(data.length >= 2);

    // 如果服务端使用了RSV则报错
    if (headerBuffer[0] & SRRsvMask) {
        [self _closeWithProtocolError:@"Server used RSV bits"];
        return;
    }
    // 获取Opcode
    uint8_t receivedOpcode = (SROpCodeMask & headerBuffer[0]);
    // 判断帧类型,是否是指定的控制帧
    BOOL isControlFrame = (receivedOpcode == SROpCodePing || receivedOpcode == SROpCodePong || receivedOpcode == SROpCodeConnectionClose);
    // 当Opcode为0时,表示本次数据传输采用了数据分片,当前收到的数据帧为其中一个数据分片。
    // 如果不是指定帧,且未采用数据分片,而且_currentFrameCount消息帧大于0,错误关闭
    if (!isControlFrame && receivedOpcode != 0 && self->_currentFrameCount > 0) {
        [self _closeWithProtocolError:@"all data frames after the initial data frame must have opcode 0"];
        return;
    }
    // 如果采用数据分片 且 _currentFrameCount数量等于0则报错
    if (receivedOpcode == 0 && self->_currentFrameCount == 0) {
        [self _closeWithProtocolError:@"cannot continue a message"];
        return;
    }

    // 如果采用数据分片,继续使用上一个切片的opcode
    header.opcode = receivedOpcode == 0 ? self->_currentFrameOpcode : receivedOpcode;
    // 得到fin
    header.fin = !!(SRFinMask & headerBuffer[0]);
    // 得到Mask
    header.masked = !!(SRMaskMask & headerBuffer[1]);
    // 得到数据长度
    header.payload_length = SRPayloadLenMask & headerBuffer[1];

    headerBuffer = NULL;
    // 如果是带掩码的,则报错
    if (header.masked) {
        [self _closeWithProtocolError:@"Client must receive unmasked data"];
    }
    // 如果需要反则加上MaskKey的长度
    size_t extra_bytes_needed = header.masked ? sizeof(_currentReadMaskKey) : 0;
    // 累加 payload_data的长度
    if (header.payload_length == 126) {
        extra_bytes_needed += sizeof(uint16_t);
    } else if (header.payload_length == 127) {
        extra_bytes_needed += sizeof(uint64_t);
    }
    // 如果如果‘扩展数据长度’不为空则需要通过计算‘扩展数据长度’的大小来判断Masking-key的位置
    if (extra_bytes_needed == 0) {
        [self _handleFrameHeader:header curData:self->_currentFrameData];
    } else {
        // 注册消费者 获取 Masking-key
        [self _addConsumerWithDataLength:extra_bytes_needed callback:^(SRWebSocket *self, NSData *data) {
            // ...
            size_t mapped_size = data.length;
            #pragma unused (mapped_size)
            const void *mapped_buffer = data.bytes;
            size_t offset = 0;
            // 获取‘扩展数据长度’的大小
            if (header.payload_length == 126) {
                // 如果payload_length为126,则mapped_buffer代表一个16位的无符号整数
                assert(mapped_size >= sizeof(uint16_t));
                uint16_t newLen = EndianU16_BtoN(*(uint16_t *)(mapped_buffer));
                header.payload_length = newLen;
                offset += sizeof(uint16_t);
            } else if (header.payload_length == 127) {
                // 如果payload_length为127,则mapped_buffer代表一个64位的无符号整数
                assert(mapped_size >= sizeof(uint64_t));
                header.payload_length = EndianU64_BtoN(*(uint64_t *)(mapped_buffer));
                offset += sizeof(uint64_t);
            } else {
                assert(header.payload_length < 126 && header.payload_length >= 0);
            }
            // 如果带有掩码则需要进行反掩码
            if (header.masked) {
                assert(mapped_size >= sizeof(_currentReadMaskOffset) + offset);
                memcpy(self->_currentReadMaskKey, ((uint8_t *)mapped_buffer) + offset, sizeof(self->_currentReadMaskKey));
            }
            [self _handleFrameHeader:header curData:self->_currentFrameData];
        } readToCurrentFrame:NO unmaskBytes:NO];
    }
} readToCurrentFrame:NO unmaskBytes:NO];

_handleFrameHeader:curData: 处理帧数据的Header

...
// 如果不是控制帧
if (!isControlFrame) {
    // 记录当前帧的opcode
    _currentFrameOpcode = frame_header.opcode;
    // 累加帧数量
    _currentFrameCount += 1;
}
// 如果数据长度为0
if (frame_header.payload_length == 0) {
    if (isControlFrame) {
        // 如果是控制帧则处理帧数据
        [self _handleFrameWithData:curData opCode:frame_header.opcode];
    } else {
        // 如果当前帧是最后一个切片则处理帧数据
        if (frame_header.fin) {
            [self _handleFrameWithData:_currentFrameData opCode:frame_header.opcode];
        } else {
            // 如果当前帧不是最后一个切片则继续读取数据
            [self _readFrameContinue];
        }
    }
} else {
    // 添加consumer,去读取Payload Data
    [self _addConsumerWithDataLength:(size_t)frame_header.payload_length callback:^(SRWebSocket *self, NSData *newData) {
        if (isControlFrame) {
            [self _handleFrameWithData:newData opCode:frame_header.opcode];
        } else {
            if (frame_header.fin) {
                [self _handleFrameWithData:self->_currentFrameData opCode:frame_header.opcode];
            } else {
                [self _readFrameContinue];
            }
        }
    } readToCurrentFrame:!isControlFrame unmaskBytes:frame_header.masked];
}

_handleFrameWithData:opCode:只是对数据的处理,逻辑比较简单,在这里就不多做描述了

五. 发送WebSocket Data

graph TD
send: --> sendData: --> sendDataNoCopy:--> _sendFrameWithOpcode:data:
send: --> sendString: --> _sendFrameWithOpcode:data: --> _writeData: --> _pumpWriting

_sendFrameWithOpcode 发送帧数据

// 预留32个字节的空间填充WebSocket协议中需要的字段
static const size_t SRFrameHeaderOverhead = 32;

size_t payloadLength = data.length;
// 创建帧数据 
NSMutableData *frameData = [[NSMutableData alloc] initWithLength:payloadLength + SRFrameHeaderOverhead];
 // 当内存不足时会创建失败
if (!frameData) {
    // 创建失败后触发失败回调
    [self closeWithCode:SRStatusCodeMessageTooBig reason:@"Message too big"];
    return;
}
// 获取数据指针
uint8_t *frameBuffer = (uint8_t *)frameData.mutableBytes;
// 设置 fin 和 opcode
frameBuffer[0] = SRFinMask | opCode;
// 对数据载荷进行掩码操作
frameBuffer[1] |= SRMaskMask;

size_t frameBufferSize = 2;
// 数据载荷的长度小于126时直接赋值
if (payloadLength < 126) {
    frameBuffer[1] |= payloadLength;
} else {
    uint64_t declaredPayloadLength = 0;
    size_t declaredPayloadLengthSize = 0;
    if (payloadLength <= UINT16_MAX) {
        frameBuffer[1] |= 126;
        // 后续2个字节代表一个16位的无符号整数,该无符号整数的值为数据的长度。
        declaredPayloadLength = CFSwapInt16BigToHost((uint16_t)payloadLength);
        declaredPayloadLengthSize = sizeof(uint16_t);
    } else {
        frameBuffer[1] |= 127;
        // 后续8个字节代表一个64位的无符号整数(最高位为0),该无符号整数的值为数据的长度。
        declaredPayloadLength = CFSwapInt64BigToHost((uint64_t)payloadLength);
        declaredPayloadLengthSize = sizeof(uint64_t);
    }
    // 将declaredPayloadLength拷贝在frameBuffer的后两位的内存中
    memcpy((frameBuffer + frameBufferSize), &declaredPayloadLength, declaredPayloadLengthSize);
    frameBufferSize += declaredPayloadLengthSize;
}

const uint8_t *unmaskedPayloadBuffer = (uint8_t *)data.bytes;
// 创建maskKey(32位的随机数)
uint8_t *maskKey = frameBuffer + frameBufferSize;
size_t randomBytesSize = sizeof(uint32_t);
int result = SecRandomCopyBytes(kSecRandomDefault, randomBytesSize, maskKey);
if (result != 0) {
}
frameBufferSize += randomBytesSize;

// 将数据进行掩码操作
uint8_t *frameBufferPayloadPointer = frameBuffer + frameBufferSize;
memcpy(frameBufferPayloadPointer, unmaskedPayloadBuffer, payloadLength);
SRMaskBytesSIMD(frameBufferPayloadPointer, payloadLength, maskKey);
frameBufferSize += payloadLength;

assert(frameBufferSize <= frameData.length);
frameData.length = frameBufferSize;

[self _writeData:frameData];

_sendFrameWithOpcode 发送帧数据

// 预留32个字节的空间填充WebSocket协议中需要的字段
static const size_t SRFrameHeaderOverhead = 32;

size_t payloadLength = data.length;
// 创建帧数据 
NSMutableData *frameData = [[NSMutableData alloc] initWithLength:payloadLength + SRFrameHeaderOverhead];
 // 当内存不足时会创建失败
if (!frameData) {
    // 创建失败后触发失败回调
    [self closeWithCode:SRStatusCodeMessageTooBig reason:@"Message too big"];
    return;
}
// 获取数据指针
uint8_t *frameBuffer = (uint8_t *)frameData.mutableBytes;
// 设置 fin 和 opcode
frameBuffer[0] = SRFinMask | opCode;
// 对数据载荷进行掩码操作
frameBuffer[1] |= SRMaskMask;

size_t frameBufferSize = 2;
// 数据载荷的长度小于126时直接赋值
if (payloadLength < 126) {
    frameBuffer[1] |= payloadLength;
} else {
    uint64_t declaredPayloadLength = 0;
    size_t declaredPayloadLengthSize = 0;
    if (payloadLength <= UINT16_MAX) {
        frameBuffer[1] |= 126;
        // 后续2个字节代表一个16位的无符号整数,该无符号整数的值为数据的长度。
        declaredPayloadLength = CFSwapInt16BigToHost((uint16_t)payloadLength);
        declaredPayloadLengthSize = sizeof(uint16_t);
    } else {
        frameBuffer[1] |= 127;
        // 后续8个字节代表一个64位的无符号整数(最高位为0),该无符号整数的值为数据的长度。
        declaredPayloadLength = CFSwapInt64BigToHost((uint64_t)payloadLength);
        declaredPayloadLengthSize = sizeof(uint64_t);
    }
    // 将declaredPayloadLength拷贝在frameBuffer的后两位的内存中
    memcpy((frameBuffer + frameBufferSize), &declaredPayloadLength, declaredPayloadLengthSize);
    frameBufferSize += declaredPayloadLengthSize;
}

const uint8_t *unmaskedPayloadBuffer = (uint8_t *)data.bytes;
// 创建maskKey(32位的随机数)
uint8_t *maskKey = frameBuffer + frameBufferSize;
size_t randomBytesSize = sizeof(uint32_t);
int result = SecRandomCopyBytes(kSecRandomDefault, randomBytesSize, maskKey);
if (result != 0) {
}
frameBufferSize += randomBytesSize;

// 将数据进行掩码操作
uint8_t *frameBufferPayloadPointer = frameBuffer + frameBufferSize;
memcpy(frameBufferPayloadPointer, unmaskedPayloadBuffer, payloadLength);
SRMaskBytesSIMD(frameBufferPayloadPointer, payloadLength, maskKey);
frameBufferSize += payloadLength;

assert(frameBufferSize <= frameData.length);
frameData.length = frameBufferSize;

[self _writeData:frameData];

Other

常驻线程

为了防止影响主线程的性能,SRWebSocketInput/OutputStream注册到子线程的RunLoop中。

+ (NSRunLoop *)SR_networkRunLoop {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // 开辟一个子线程
        networkThread = [[_SRRunLoopThread alloc] init];
        networkThread.name = @"com.squareup.SocketRocket.NetworkThread";
        [networkThread start];
        // 获取个子线程的Runloop
        networkRunLoop = networkThread.runLoop;
    });
    return networkRunLoop;
}
- (id)init {
    self = [super init];
    if (self) {
        // 创建一个group
        _waitGroup = dispatch_group_create();
        // 等待的group里 + 1
        dispatch_group_enter(_waitGroup);
    }
    return self;
}

// 线程的执行
- (void)main {
    @autoreleasepool {
        // 获取当前线程的runloop
        _runLoop = [NSRunLoop currentRunLoop];
        // 开始执行,group - 1
        dispatch_group_leave(_waitGroup)
        // 创建一个空的runloop SourceContext,防止runloop退出
        CFRunLoopSourceContext sourceCtx = {
            .version = 0,
            .info = NULL,
            .retain = NULL,
            .release = NULL,
            .copyDescription = NULL,
            .equal = NULL,
            .hash = NULL,
            .schedule = NULL,
            .cancel = NULL,
            .perform = NULL
        };
        // 转化为source
        CFRunLoopSourceRef source = CFRunLoopSourceCreate(NULL, 0, &sourceCtx);
        // 添加source
        CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
        CFRelease(source);
        // 一直让runloop运行在 NSDefaultRunLoopMode模式下
        while ([_runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]) {
            
        }
        assert(NO);
    }
}

- (NSRunLoop *)runLoop;
{
    // 阻塞,直到main中获取到当前RunLoop
    dispatch_group_wait(_waitGroup, DISPATCH_TIME_FOREVER);
    return _runLoop;
}

点击这里复制本文地址 以上内容由权冠洲的博客整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!

支持Ctrl+Enter提交

联系我们| 本站介绍| 留言建议 | 交换友链 | 域名展示
本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除

权冠洲的博客 © All Rights Reserved.  Copyright quanguanzhou.top All Rights Reserved
苏公网安备 32030302000848号   苏ICP备20033101号-1
本网站由 提供CDN/云存储服务

联系我们