MixPanel代碼閱讀筆記-websocket

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;


? ? ? ? }


? ? });


}


?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末梧田,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子侧蘸,更是在濱河造成了極大的恐慌裁眯,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,084評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件讳癌,死亡現(xiàn)場離奇詭異穿稳,居然都是意外死亡,警方通過查閱死者的電腦和手機晌坤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,623評論 3 392
  • 文/潘曉璐 我一進店門司草,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人泡仗,你說我怎么就攤上這事埋虹。” “怎么了娩怎?”我有些...
    開封第一講書人閱讀 163,450評論 0 353
  • 文/不壞的土叔 我叫張陵搔课,是天一觀的道長。 經常有香客問我截亦,道長爬泥,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,322評論 1 293
  • 正文 為了忘掉前任崩瓤,我火速辦了婚禮袍啡,結果婚禮上,老公的妹妹穿的比我還像新娘却桶。我一直安慰自己境输,他們只是感情好蔗牡,可當我...
    茶點故事閱讀 67,370評論 6 390
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著嗅剖,像睡著了一般辩越。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上信粮,一...
    開封第一講書人閱讀 51,274評論 1 300
  • 那天黔攒,我揣著相機與錄音,去河邊找鬼强缘。 笑死督惰,一個胖子當著我的面吹牛,可吹牛的內容都是我干的旅掂。 我是一名探鬼主播赏胚,決...
    沈念sama閱讀 40,126評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼辞友!你這毒婦竟也來了?” 一聲冷哼從身側響起震肮,我...
    開封第一講書人閱讀 38,980評論 0 275
  • 序言:老撾萬榮一對情侶失蹤称龙,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后戳晌,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體鲫尊,經...
    沈念sama閱讀 45,414評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,599評論 3 334
  • 正文 我和宋清朗相戀三年沦偎,在試婚紗的時候發(fā)現(xiàn)自己被綠了疫向。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,773評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡豪嚎,死狀恐怖搔驼,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情侈询,我是刑警寧澤舌涨,帶...
    沈念sama閱讀 35,470評論 5 344
  • 正文 年R本政府宣布,位于F島的核電站扔字,受9級特大地震影響囊嘉,放射性物質發(fā)生泄漏。R本人自食惡果不足惜革为,卻給世界環(huán)境...
    茶點故事閱讀 41,080評論 3 327
  • 文/蒙蒙 一扭粱、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧震檩,春花似錦琢蛤、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,713評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽俩滥。三九已至,卻和暖如春贺奠,著一層夾襖步出監(jiān)牢的瞬間霜旧,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,852評論 1 269
  • 我被黑心中介騙來泰國打工儡率, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留挂据,地道東北人。 一個月前我還...
    沈念sama閱讀 47,865評論 2 370
  • 正文 我出身青樓儿普,卻偏偏與公主長得像崎逃,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子眉孩,可洞房花燭夜當晚...
    茶點故事閱讀 44,689評論 2 354

推薦閱讀更多精彩內容