WebSocket介紹
WebSocket protocol 是HTML5一種新的協(xié)議但壮。它實現(xiàn)了瀏覽器與服務器全雙工通信(full-duplex)卷谈。一開始的握手需要借助HTTP請求完成趁冈。?
WebSocket和HTTP掉弛、FTP一樣也是應用層的協(xié)議搅幅,但是它是一種雙向通信協(xié)議咽斧,是建立在TCP/IP之上的堪置。?
連接的過程是:?
首先,客戶端發(fā)起http請求,http請求里存放WebSocket支持的版本號等信息张惹,如:Upgrade舀锨、Connection、WebSocket-Version等宛逗;?
然后坎匿,服務器收到客戶端的握手請求后,同樣采用HTTP協(xié)議回饋數(shù)據(jù);?
最后替蔬,客戶端收到連接成功的消息后告私,開始借助于TCP傳輸信道進行全雙工通信。
MPWebsocket
mixpanel的websocket代碼和facebook的socketRocket類似承桥,而SRWebsocket代碼也是開源的德挣。。
它主要的功能api有:
- (void)webSocket:(MPWebSocket *)webSocket didReceiveMessage:(id)message;
- (void)webSocketDidOpen:(MPWebSocket *)webSocket;
- (void)webSocket:(MPWebSocket *)webSocket didFailWithError:(NSError *)error;
- (void)webSocket:(MPWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean;
使用起來比較簡單快毛,我們用這幾個代理方法就能實現(xiàn)我們大部分的功能了。
初始化流程
包括對schem進行斷言番挺,只支持ws/wss/http/https四種唠帝。
當前socket狀態(tài),是正在連接玄柏,還是已連接襟衰、斷開等等。
初始化工作隊列粪摘,以及流回調線程等等瀑晒。
初始化讀寫緩沖區(qū):_readBuffer、_outputBuffer徘意。
- (void)_MP_commonInit;
{
? ? NSString *scheme = _url.scheme.lowercaseString;
assert([scheme isEqualToString:@"ws"] || [scheme isEqualToString:@"http"] || [scheme isEqualToString:@"wss"] || [scheme isEqualToString:@"https"]);?
? ? if ([scheme isEqualToString:@"wss"] || [scheme isEqualToString:@"https"]) {
? ? ? ? _secure = YES;
? ? }
? ? _readyState = MPWebSocketStateConnecting;
? ? _consumerStopped = YES;
? ? _webSocketVersion = 13;
? ? _workQueue = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL);
? ? // Going to set a specific on the queue so we can validate we're on the work queue
? ? dispatch_queue_set_specific(_workQueue, (__bridge void *)self, maybe_bridge(_workQueue), NULL);
? ? _delegateDispatchQueue = dispatch_get_main_queue();
? ? mp_dispatch_retain(_delegateDispatchQueue);
? ? _readBuffer = [[NSMutableData alloc] init];
? ? _outputBuffer = [[NSMutableData alloc] init];
? ? _currentFrameData = [[NSMutableData alloc] init];
? ? _consumers = [NSMutableArray array];
? ? _consumerPool = [[MPIOConsumerPool alloc] init];
? ? _scheduledRunloops = [NSMutableSet set];
? ? [self _initializeStreams];
? ? // default handlers
}
?開啟連接 (給輸入輸出流綁定一個runloop)
- (void)_connect
{
? ? if (!_scheduledRunloops.count) { // 判斷有沒有runloop,
? ? ? ? [self scheduleInRunLoop:[NSRunLoop mp_networkRunLoop] forMode:NSDefaultRunLoopMode];
? ? ? ? // 創(chuàng)建一個帶有runloop的常駐線程苔悦,模式為 NSDefaultRunLoopMode
? ? }
//? ? 開啟輸入輸出流
? ? [_outputStream open];
? ? [_inputStream open];
}
?把socket放入run loop中 并打開流
- (void)scheduleInRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode;
{
? ? [_outputStream scheduleInRunLoop:aRunLoop forMode:mode];
? ? [_inputStream scheduleInRunLoop:aRunLoop forMode:mode];
? ? [_scheduledRunloops addObject:@[aRunLoop, mode]];
}
?打開成功后會發(fā)送一個http請求 GET,作為第一次握手椎咧,WebSocket建立連接前玖详,都會以http請求作為握手的方式,這個方法就是在構造http的請求頭
- (void)didConnect
{
? ? MPLogInfo(@"Connected");
? ? CFHTTPMessageRef request = CFHTTPMessageCreateRequest(NULL, CFSTR("GET"), (__bridge CFURLRef)_url, kCFHTTPVersion1_1);
? ? // Set host first so it defaults
? ? CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Host"), (__bridge CFStringRef)(_url.port ? [NSString stringWithFormat:@"%@:%@", _url.host, _url.port] : _url.host));
? ? // 生成密鑰勤讽,
? ? NSMutableData *keyBytes = [[NSMutableData alloc] initWithLength:16];
? ? int result = SecRandomCopyBytes(kSecRandomDefault, keyBytes.length, keyBytes.mutableBytes);
? ? if (result != 0) {
? ? ? ? MPLogError(@"Failed to generate random bytes with status: %d", result);
? ? }
? ? // 用base64轉碼
? ? _secKey = [keyBytes base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength];
? ? // 斷言編碼后長度24
? ? assert(_secKey.length == 24);
? ? //web socket規(guī)范head
? ? CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Upgrade"), CFSTR("websocket"));
? ? CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Connection"), CFSTR("Upgrade"));
? ? CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Sec-WebSocket-Key"), (__bridge CFStringRef)_secKey);
? ? CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Sec-WebSocket-Version"), (__bridge CFStringRef)[NSString stringWithFormat:@"%ld", (long)_webSocketVersion]);
? ? CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Origin"), (__bridge CFStringRef)_url.mp_origin);
? ? if (_requestedProtocols) {//用戶初始化的協(xié)議數(shù)組蟋座,可以約束websocket的一些行為
? ? ? ? CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Sec-WebSocket-Protocol"), (__bridge CFStringRef)[_requestedProtocols componentsJoinedByString:@", "]);
? ? }
//? ? 吧 _urlRequest中原有的head 設置到request中去
? ? [_urlRequest.allHTTPHeaderFields enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
? ? ? ? CFHTTPMessageSetHeaderFieldValue(request, (__bridge CFStringRef)key, (__bridge CFStringRef)obj);
? ? }];
//返回一個序列化 , CFBridgingRelease和 __bridge transfer一個意思, CFHTTPMessageCopySerializedMessage copy一份新的并且序列化脚牍,返回CFDataRef
? ? NSData *message = CFBridgingRelease(CFHTTPMessageCopySerializedMessage(request));
//釋放request
? ? CFRelease(request);
//把這個request當成data去寫
? ? [self _writeData:message];
? ? //讀取http的頭部
? ? [self _readHTTPHeader];
}
處理消息幀方法
數(shù)據(jù)是通過CFStream流的方式回調回來的向臀,每次拿到流數(shù)據(jù),都是先放在數(shù)據(jù)緩沖區(qū)中诸狭,然后去讀當前消息幀的頭部券膀,得到當前數(shù)據(jù)包的大小,然后再去創(chuàng)建消費者對象consumer驯遇,去讀取緩沖區(qū)指定數(shù)據(jù)包大小的內容三娩,讀完才會回調給我們上層用戶,所以妹懒,我們如果用SRWebSocket完全不需要考慮數(shù)據(jù)斷包雀监、粘包的問題,每次到達的數(shù)據(jù),都是一條完整的數(shù)據(jù)
- (void)_readHTTPHeader;
{
? ? if (_receivedHTTPHeaders == NULL) {
? ? ? ? //序列化的http消息
? ? ? ? _receivedHTTPHeaders = CFHTTPMessageCreateEmpty(NULL, NO);
? ? }
? ? //不停的add consumer去讀數(shù)據(jù)
? ? [self _readUntilHeaderCompleteWithCallback:^(MPWebSocket *websocket,? NSData *data) {
? ? ? ? //拼接數(shù)據(jù)会前,拼到頭部
? ? ? ? CFHTTPMessageAppendBytes(websocket->_receivedHTTPHeaders, (const UInt8 *)data.bytes, (CFIndex)data.length);
//? ? ? ? 持續(xù)進行反序列化直到CFHTTPMessageIsHeaderComplete 返回TRUE好乐。如果不去檢測CFHTTPMessageIsHeaderComplete返回TRUE,這個消息可能不完整和不可靠瓦宜。
? ? ? ? //判斷是否接受完
? ? ? ? if (CFHTTPMessageIsHeaderComplete(websocket->_receivedHTTPHeaders)) {
? ? ? ? ? ? MPLogDebug(@"Finished reading headers %@", CFBridgingRelease(CFHTTPMessageCopyAllHeaderFields(websocket->_receivedHTTPHeaders)));
? ? ? ? ? ? [websocket _HTTPHeadersDidFinish]; //
? ? ? ? } else {
? ? ? ? ? ? //沒讀完遞歸調
? ? ? ? ? ? [websocket _readHTTPHeader];
? ? ? ? }
? ? }];
}
?檢查握手信息
//我們發(fā)出這個http請求后蔚万,得到服務端的響應頭,去按照服務端的方式加密Sec-WebSocket-Key临庇,判斷與Sec-WebSocket-Accept是否相同反璃,相同則表明握手成功,否則失敗處理假夺。
- (BOOL)_checkHandshake:(CFHTTPMessageRef)httpMessage;
{
? ? // 是不是允許的header
? ? NSString *acceptHeader = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(httpMessage, CFSTR("Sec-WebSocket-Accept")));
? ? // 為nil被服務器拒絕
? ? if (acceptHeader == nil) {
? ? ? ? return NO;
? ? }
? ? //得到
? ? NSString *concatenatedString = [_secKey stringByAppendingString:MPWebSocketAppendToSecKeyString];
? ? ? //期待accept的字符串
? ? NSString *expectedAccept = [concatenatedString stringBySHA1ThenBase64Encoding];
//判斷是否相同淮蜈,相同就握手信息對了
? ? return [acceptHeader isEqualToString:expectedAccept];
}
至此都成功的話,一個WebSocket連接建立完畢已卷。
MPWebsocket基于NSStreamDelegate來完成數(shù)據(jù)流的讀寫
- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode;
{
? ? if (_secure && !_pinnedCertFound && (eventCode == NSStreamEventHasBytesAvailable || eventCode == NSStreamEventHasSpaceAvailable)) {
? ? ? ? NSArray *sslCerts = [_urlRequest mp_SSLPinnedCertificates];
? ? ? ? if (sslCerts) {
? ? ? ? ? ? SecTrustRef secTrust = (__bridge SecTrustRef)[aStream propertyForKey:(__bridge id)kCFStreamPropertySSLPeerTrust];
? ? ? ? ? ? if (secTrust) {
? ? ? ? ? ? ? ? NSInteger numCerts = SecTrustGetCertificateCount(secTrust);
? ? ? ? ? ? ? ? for (NSInteger i = 0; i < numCerts && !_pinnedCertFound; i++) {
? ? ? ? ? ? ? ? ? ? SecCertificateRef cert = SecTrustGetCertificateAtIndex(secTrust, i);
? ? ? ? ? ? ? ? ? ? NSData *certData = CFBridgingRelease(SecCertificateCopyData(cert));
? ? ? ? ? ? ? ? ? ? for (id ref in sslCerts) {
? ? ? ? ? ? ? ? ? ? ? ? SecCertificateRef trustedCert = (__bridge SecCertificateRef)ref;
? ? ? ? ? ? ? ? ? ? ? ? NSData *trustedCertData = CFBridgingRelease(SecCertificateCopyData(trustedCert));
? ? ? ? ? ? ? ? ? ? ? ? if ([trustedCertData isEqualToData:certData]) {
? ? ? ? ? ? ? ? ? ? ? ? ? ? _pinnedCertFound = YES;
? ? ? ? ? ? ? ? ? ? ? ? ? ? break;
? ? ? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? }
? ? ? ? ? ? }
? ? ? ? ? ? if (!_pinnedCertFound) {
? ? ? ? ? ? ? ? dispatch_async(_workQueue, ^{
? ? ? ? ? ? ? ? ? ? [self _failWithError:[NSError errorWithDomain:@"org.lolrus.SocketRocket" code:23556 userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Invalid server cert"]}]];
? ? ? ? ? ? ? ? });
? ? ? ? ? ? ? ? return;
? ? ? ? ? ? }
? ? ? ? }
? ? }
? ? dispatch_async(_workQueue, ^{
? ? ? ? switch (eventCode) {
? ? ? ? ? ? case NSStreamEventOpenCompleted: {
? ? ? ? ? ? ? ? MPLogDebug(@"NSStreamEventOpenCompleted %@", aStream);
? ? ? ? ? ? ? ? if (self.readyState >= MPWebSocketStateClosing) {
? ? ? ? ? ? ? ? ? ? return;
? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? assert(self->_readBuffer);
? ? ? ? ? ? ? ? if (self.readyState == MPWebSocketStateConnecting && aStream == self->_inputStream) {
? ? ? ? ? ? ? ? ? ? [self didConnect];
? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? [self _pumpWriting];
? ? ? ? ? ? ? ? [self _pumpScanner];
? ? ? ? ? ? ? ? break;
? ? ? ? ? ? }
? ? ? ? ? ? case NSStreamEventErrorOccurred: {
? ? ? ? ? ? ? ? MPLogError(@"NSStreamEventErrorOccurred %@ %@", aStream, [[aStream streamError] copy]);
? ? ? ? ? ? ? ? /// TODO specify error better!
? ? ? ? ? ? ? ? [self _failWithError:aStream.streamError];
? ? ? ? ? ? ? ? self->_readBufferOffset = 0;
? ? ? ? ? ? ? ? [self->_readBuffer setLength:0];
? ? ? ? ? ? ? ? break;
? ? ? ? ? ? }
? ? ? ? ? ? case NSStreamEventEndEncountered: {
? ? ? ? ? ? ? ? [self _pumpScanner];
? ? ? ? ? ? ? ? MPLogDebug(@"NSStreamEventEndEncountered %@", aStream);
? ? ? ? ? ? ? ? if (aStream.streamError) {
? ? ? ? ? ? ? ? ? ? [self _failWithError:aStream.streamError];
? ? ? ? ? ? ? ? } else {
? ? ? ? ? ? ? ? ? ? if (self.readyState != MPWebSocketStateClosed) {
? ? ? ? ? ? ? ? ? ? ? ? self.readyState = MPWebSocketStateClosed;
? ? ? ? ? ? ? ? ? ? ? ? [self _scheduleCleanup];
? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? ? ? if (!self->_sentClose && !self->_failed) {
? ? ? ? ? ? ? ? ? ? ? ? self->_sentClose = YES;
? ? ? ? ? ? ? ? ? ? ? ? // If we get closed in this state it's probably not clean because we should be sending this when we send messages
? ? ? ? ? ? ? ? ? ? ? ? [self _performDelegateBlock:^{
? ? ? ? ? ? ? ? ? ? ? ? ? ? if ([self.delegate respondsToSelector:@selector(webSocket:didCloseWithCode:reason:wasClean:)]) {
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? [self.delegate webSocket:self didCloseWithCode:MPStatusCodeGoingAway reason:@"Stream end encountered" wasClean:NO];
? ? ? ? ? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? ? ? ? ? }];
? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? break;
? ? ? ? ? ? }
? ? ? ? ? ? case NSStreamEventHasBytesAvailable: {
? ? ? ? ? ? ? ? MPLogDebug(@"NSStreamEventHasBytesAvailable %@", aStream);
? ? ? ? ? ? ? ? const int bufferSize = 2048;
? ? ? ? ? ? ? ? uint8_t buffer[bufferSize];
? ? ? ? ? ? ? ? while (self->_inputStream.hasBytesAvailable) {
? ? ? ? ? ? ? ? ? ? NSInteger bytes_read = [self->_inputStream read:buffer maxLength:bufferSize];
? ? ? ? ? ? ? ? ? ? if (bytes_read > 0) {
? ? ? ? ? ? ? ? ? ? ? ? [self->_readBuffer appendBytes:buffer length:(NSUInteger)bytes_read];
? ? ? ? ? ? ? ? ? ? } else if (bytes_read < 0) {
? ? ? ? ? ? ? ? ? ? ? ? [self _failWithError:self->_inputStream.streamError];
? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? ? ? if (bytes_read != bufferSize) {
? ? ? ? ? ? ? ? ? ? ? ? break;
? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? [self _pumpScanner];
? ? ? ? ? ? ? ? break;
? ? ? ? ? ? }
? ? ? ? ? ? case NSStreamEventHasSpaceAvailable: {
? ? ? ? ? ? ? ? MPLogDebug(@"NSStreamEventHasSpaceAvailable %@", aStream);
? ? ? ? ? ? ? ? [self _pumpWriting];
? ? ? ? ? ? ? ? break;
? ? ? ? ? ? }
? ? ? ? ? ? default:
? ? ? ? ? ? ? ? MPLogDebug(@"(default) %@", aStream);
? ? ? ? ? ? ? ? break;
? ? ? ? }
? ? });
}