前言
關(guān)于我和WebSocket的緣:我從大二在計(jì)算機(jī)網(wǎng)絡(luò)課上聽(tīng)老師講過(guò)之后,第一次使用就到了畢業(yè)之后的第一份工作。直到最近換了工作,到了一家是含有IM社交聊天功能的app的時(shí)候昆箕,我覺(jué)得我現(xiàn)在可以談?wù)勎覍?duì)WebSocket/Socket的一些看法了。要想做IM聊天app租冠,就不得不理解WebSocket和Socket的原理了鹏倘,聽(tīng)我一一道來(lái)。
目錄
- 1.WebSocket使用場(chǎng)景
- 2.WebSocket誕生由來(lái)
- 3.談?wù)刉ebSocket協(xié)議原理
- 4.WebSocket 和 Socket的區(qū)別與聯(lián)系
- 5.iOS平臺(tái)有哪些WebSocket和Socket的開源框架
- 6.iOS平臺(tái)如何實(shí)現(xiàn)WebSocket協(xié)議
一.WebSocket的使用場(chǎng)景
1.社交聊天
最著名的就是微信肺稀,QQ第股,這一類社交聊天的app应民。這一類聊天app的特點(diǎn)是低延遲话原,高即時(shí)。即時(shí)是這里面要求最高的诲锹,如果有一個(gè)緊急的事情繁仁,通過(guò)IM軟件通知你,假設(shè)網(wǎng)絡(luò)環(huán)境良好的情況下归园,這條message還無(wú)法立即送達(dá)到你的客戶端上黄虱,緊急的事情都結(jié)束了,你才收到消息庸诱,那么這個(gè)軟件肯定是失敗的捻浦。
2.彈幕
說(shuō)到這里,大家一定里面想到了A站和B站了桥爽。確實(shí)朱灿,他們的彈幕一直是一種特色。而且彈幕對(duì)于一個(gè)視頻來(lái)說(shuō)钠四,很可能彈幕才是精華盗扒。發(fā)彈幕需要實(shí)時(shí)顯示,也需要和聊天一樣,需要即時(shí)侣灶。
3.多玩家游戲
4.協(xié)同編輯
現(xiàn)在很多開源項(xiàng)目都是分散在世界各地的開發(fā)者一起協(xié)同開發(fā)甸祭,此時(shí)就會(huì)用到版本控制系統(tǒng),比如Git褥影,SVN去合并沖突池户。但是如果有一份文檔,支持多人實(shí)時(shí)在線協(xié)同編輯伪阶,那么此時(shí)就會(huì)用到比如WebSocket了煞檩,它可以保證各個(gè)編輯者都在編輯同一個(gè)文檔,此時(shí)不需要用到Git栅贴,SVN這些版本控制斟湃,因?yàn)樵趨f(xié)同編輯界面就會(huì)實(shí)時(shí)看到對(duì)方編輯了什么,誰(shuí)在修改哪些段落和文字檐薯。
5.股票基金實(shí)時(shí)報(bào)價(jià)
金融界瞬息萬(wàn)變——幾乎是每毫秒都在變化凝赛。如果采用的網(wǎng)絡(luò)架構(gòu)無(wú)法滿足實(shí)時(shí)性,那么就會(huì)給客戶帶來(lái)巨大的損失坛缕。幾毫秒錢股票開始大跌墓猎,幾秒以后才刷新數(shù)據(jù),一秒鐘的時(shí)間內(nèi)赚楚,很可能用戶就已經(jīng)損失巨大財(cái)產(chǎn)了毙沾。
6.體育實(shí)況更新
全世界的球迷,體育愛(ài)好者特別多宠页,當(dāng)然大家在關(guān)心自己喜歡的體育活動(dòng)的時(shí)候左胞,比賽實(shí)時(shí)的賽況是他們最最關(guān)心的事情。這類新聞中最好的體驗(yàn)就是利用Websocket達(dá)到實(shí)時(shí)的更新举户!
7.視頻會(huì)議/聊天
視頻會(huì)議并不能代替和真人相見(jiàn)烤宙,但是他能讓分布在全球天涯海角的人聚在電腦前一起開會(huì)。既能節(jié)省大家聚在一起路上花費(fèi)的時(shí)間俭嘁,討論聚會(huì)地點(diǎn)的糾結(jié)躺枕,還能隨時(shí)隨地,只要有網(wǎng)絡(luò)就可以開會(huì)供填。
8.基于位置的應(yīng)用
越來(lái)越多的開發(fā)者借用移動(dòng)設(shè)備的GPS功能來(lái)實(shí)現(xiàn)他們基于位置的網(wǎng)絡(luò)應(yīng)用拐云。如果你一直記錄用戶的位置(比如運(yùn)行應(yīng)用來(lái)記錄運(yùn)動(dòng)軌跡),你可以收集到更加細(xì)致化的數(shù)據(jù)近她。
9.在線教育
在線教育近幾年也發(fā)展迅速叉瘩。優(yōu)點(diǎn)很多,免去了場(chǎng)地的限制泄私,能讓名師的資源合理的分配給全國(guó)各地想要學(xué)習(xí)知識(shí)的同學(xué)手上房揭,Websocket是個(gè)不錯(cuò)的選擇备闲,可以視頻聊天、即時(shí)聊天以及其與別人合作一起在網(wǎng)上討論問(wèn)題...
10.智能家居
這也是我一畢業(yè)加入的一個(gè)偉大的物聯(lián)網(wǎng)智能家居的公司捅暴√裆埃考慮到家里的智能設(shè)備的狀態(tài)必須需要實(shí)時(shí)的展現(xiàn)在手機(jī)app客戶端上,毫無(wú)疑問(wèn)選擇了Websocket蓬痒。
11.總結(jié)
從上面我列舉的這些場(chǎng)景來(lái)看泻骤,一個(gè)共同點(diǎn)就是,高實(shí)時(shí)性梧奢!
二.WebSocket誕生由來(lái)
1.最開始的輪詢Polling階段
這種方式下狱掂,是不適合獲取實(shí)時(shí)信息的,客戶端和服務(wù)器之間會(huì)一直進(jìn)行連接亲轨,每隔一段時(shí)間就詢問(wèn)一次趋惨。客戶端會(huì)輪詢惦蚊,有沒(méi)有新消息器虾。這種方式連接數(shù)會(huì)很多,一個(gè)接受蹦锋,一個(gè)發(fā)送兆沙。而且每次發(fā)送請(qǐng)求都會(huì)有Http的Header,會(huì)很耗流量莉掂,也會(huì)消耗CPU的利用率葛圃。
2.改進(jìn)版的長(zhǎng)輪詢Long polling階段
長(zhǎng)輪詢是對(duì)輪詢的改進(jìn)版,客戶端發(fā)送HTTP給服務(wù)器之后憎妙,有沒(méi)有新消息库正,如果沒(méi)有新消息,就一直等待尚氛。當(dāng)有新消息的時(shí)候诀诊,才會(huì)返回給客戶端洞渤。在某種程度上減小了網(wǎng)絡(luò)帶寬和CPU利用率等問(wèn)題阅嘶。但是這種方式還是有一種弊端:例如假設(shè)服務(wù)器端的數(shù)據(jù)更新速度很快,服務(wù)器在傳送一個(gè)數(shù)據(jù)包給客戶端后必須等待客戶端的下一個(gè)Get請(qǐng)求到來(lái)载迄,才能傳遞第二個(gè)更新的數(shù)據(jù)包給客戶端讯柔,那么這樣的話,客戶端顯示實(shí)時(shí)數(shù)據(jù)最快的時(shí)間為2×RTT(往返時(shí)間)护昧,而且如果在網(wǎng)絡(luò)擁塞的情況下魂迄,這個(gè)時(shí)間用戶是不能接受的,比如在股市的的報(bào)價(jià)上惋耙。另外捣炬,由于http數(shù)據(jù)包的頭部數(shù)據(jù)量往往很大(通常有400多個(gè)字節(jié))熊昌,但是真正被服務(wù)器需要的數(shù)據(jù)卻很少(有時(shí)只有10個(gè)字節(jié)左右),這樣的數(shù)據(jù)包在網(wǎng)絡(luò)上周期性的傳輸湿酸,難免對(duì)網(wǎng)絡(luò)帶寬是一種浪費(fèi)婿屹。
3.WebSocket誕生
現(xiàn)在急需的需求是能支持客戶端和服務(wù)器端的雙向通信,而且協(xié)議的頭部又沒(méi)有HTTP的Header那么大推溃,于是昂利,Websocket就誕生了!
上圖就是Websocket和Polling的區(qū)別铁坎,從圖中可以看到Polling里面客戶端發(fā)送了好多Request蜂奸,而下圖,只有一個(gè)Upgrade硬萍,非常簡(jiǎn)潔高效扩所。至于消耗方面的比較就要看下圖了
上圖中,我們先看藍(lán)色的柱狀圖朴乖,是Polling輪詢消耗的流量碌奉,這次測(cè)試,HTTP請(qǐng)求和響應(yīng)頭信息開銷總共包括871字節(jié)寒砖。當(dāng)然每次測(cè)試不同的請(qǐng)求赐劣,頭的開銷不同。這次測(cè)試都以871字節(jié)的請(qǐng)求來(lái)測(cè)試哩都。
**Use case A: **1,000 clients polling every second: Network throughput is (871 x 1,000) = 871,000 bytes = 6,968,000 bits per second (6.6 Mbps)
**Use case B: **10,000 clients polling every second: Network throughput is (871 x 10,000) = 8,710,000 bytes = 69,680,000 bits per second (66 Mbps)
**Use case C: **100,000 clients polling every 1 second: Network throughput is (871 x 100,000) = 87,100,000 bytes = 696,800,000 bits per second (665 Mbps)
而Websocket的Frame是 just two bytes of overhead instead of 871魁兼,僅僅用2個(gè)字節(jié)就代替了輪詢的871字節(jié)!
**Use case A: **1,000 clients receive 1 message per second: Network throughput is (2 x 1,000) = 2,000 bytes = 16,000 bits per second (0.015 Mbps)
**Use case B: **10,000 clients receive 1 message per second: Network throughput is (2 x 10,000) = 20,000 bytes = 160,000 bits per second (0.153 Mbps)
**Use case C: **100,000 clients receive 1 message per second: Network throughput is (2 x 100,000) = 200,000 bytes = 1,600,000 bits per second (1.526 Mbps)
相同的每秒客戶端輪詢的次數(shù)漠嵌,當(dāng)次數(shù)高達(dá)10W/s的高頻率次數(shù)的時(shí)候咐汞,Polling輪詢需要消耗665Mbps,而Websocket僅僅只花費(fèi)了1.526Mbps儒鹿,將近435倍;骸!
三.談?wù)刉ebSocket協(xié)議原理
Websocket是應(yīng)用層第七層上的一個(gè)應(yīng)用層協(xié)議约炎,它必須依賴 HTTP 協(xié)議進(jìn)行一次握手 植阴,握手成功后,數(shù)據(jù)就直接從 TCP 通道傳輸圾浅,與 HTTP 無(wú)關(guān)了掠手。
Websocket的數(shù)據(jù)傳輸是frame形式傳輸?shù)模热鐣?huì)將一條消息分為幾個(gè)frame狸捕,按照先后順序傳輸出去喷鸽。這樣做會(huì)有幾個(gè)好處:
1 大數(shù)據(jù)的傳輸可以分片傳輸,不用考慮到數(shù)據(jù)大小導(dǎo)致的長(zhǎng)度標(biāo)志位不足夠的情況灸拍。
2 和http的chunk一樣做祝,可以邊生成數(shù)據(jù)邊傳遞消息砾省,即提高傳輸效率。
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 1bit 表示信息的最后一幀混槐,flag纯蛾,也就是標(biāo)記符
RSV 1-3 1bit each 以后備用的 默認(rèn)都為 0
Opcode 4bit 幀類型,稍后細(xì)說(shuō)
Mask 1bit 掩碼纵隔,是否加密數(shù)據(jù)翻诉,默認(rèn)必須置為1 (這里很蛋疼)
Payload 7bit 數(shù)據(jù)的長(zhǎng)度
Masking-key 1 or 4 bit 掩碼
Payload data (x + y) bytes 數(shù)據(jù)
Extension data x bytes 擴(kuò)展數(shù)據(jù)
Application data y bytes 程序數(shù)據(jù)
具體的規(guī)范,還請(qǐng)看官網(wǎng)的RFC 6455文檔給出的詳細(xì)定義捌刮。這里還有一個(gè)翻譯版本
四.WebSocket 和 Socket的區(qū)別與聯(lián)系
首先碰煌,
Socket 其實(shí)并不是一個(gè)協(xié)議。它工作在 OSI 模型會(huì)話層(第5層)绅作,是為了方便大家直接使用更底層協(xié)議(一般是 TCP 或 UDP )而存在的一個(gè)抽象層芦圾。Socket是對(duì)TCP/IP協(xié)議的封裝,Socket本身并不是協(xié)議俄认,而是一個(gè)調(diào)用接口(API)个少。
Socket通常也稱作”套接字”,用于描述IP地址和端口眯杏,是一個(gè)通信鏈的句柄夜焦。網(wǎng)絡(luò)上的兩個(gè)程序通過(guò)一個(gè)雙向的通訊連接實(shí)現(xiàn)數(shù)據(jù)的交換,這個(gè)雙向鏈路的一端稱為一個(gè)Socket岂贩,一個(gè)Socket由一個(gè)IP地址和一個(gè)端口號(hào)唯一確定茫经。應(yīng)用程序通常通過(guò)”套接字”向網(wǎng)絡(luò)發(fā)出請(qǐng)求或者應(yīng)答網(wǎng)絡(luò)請(qǐng)求。
Socket在通訊過(guò)程中萎津,服務(wù)端監(jiān)聽(tīng)某個(gè)端口是否有連接請(qǐng)求卸伞,客戶端向服務(wù)端發(fā)送連接請(qǐng)求,服務(wù)端收到連接請(qǐng)求向客戶端發(fā)出接收消息锉屈,這樣一個(gè)連接就建立起來(lái)了荤傲。客戶端和服務(wù)端也都可以相互發(fā)送消息與對(duì)方進(jìn)行通訊颈渊,直到雙方連接斷開遂黍。
所以基于WebSocket和基于Socket都可以開發(fā)出IM社交聊天類的app
五.iOS平臺(tái)有哪些WebSocket和Socket的開源框架
Socket開源框架有:CocoaAsyncSocket,socketio/socket.io-client-swift
WebSocket開源框架有:facebook/SocketRocket儡炼,tidwall/SwiftWebSocket
六.iOS平臺(tái)如何實(shí)現(xiàn)WebSocket協(xié)議
Talk is cheap妓湘。Show me the code ——Linus Torvalds
我們今天來(lái)看看facebook/SocketRocket的實(shí)現(xiàn)方法
首先這是SRWebSocket定義的一些成員變量
@property (nonatomic, weak) id <SRWebSocketDelegate> delegate;
/**
A dispatch queue for scheduling the delegate calls. The queue doesn't need be a serial queue.
If `nil` and `delegateOperationQueue` is `nil`, the socket uses main queue for performing all delegate method calls.
*/
@property (nonatomic, strong) dispatch_queue_t delegateDispatchQueue;
/**
An operation queue for scheduling the delegate calls.
If `nil` and `delegateOperationQueue` is `nil`, the socket uses main queue for performing all delegate method calls.
*/
@property (nonatomic, strong) NSOperationQueue *delegateOperationQueue;
@property (nonatomic, readonly) SRReadyState readyState;
@property (nonatomic, readonly, retain) NSURL *url;
@property (nonatomic, readonly) CFHTTPMessageRef receivedHTTPHeaders;
// Optional array of cookies (NSHTTPCookie objects) to apply to the connections
@property (nonatomic, copy) NSArray<NSHTTPCookie *> *requestCookies;
// This returns the negotiated protocol.
// It will be nil until after the handshake completes.
@property (nonatomic, readonly, copy) NSString *protocol;
下面這些是SRWebSocket的一些方法
// Protocols should be an array of strings that turn into Sec-WebSocket-Protocol.
- (instancetype)initWithURLRequest:(NSURLRequest *)request;
- (instancetype)initWithURLRequest:(NSURLRequest *)request protocols:(NSArray<NSString *> *)protocols;
- (instancetype)initWithURLRequest:(NSURLRequest *)request protocols:(NSArray<NSString *> *)protocols allowsUntrustedSSLCertificates:(BOOL)allowsUntrustedSSLCertificates;
// Some helper constructors.
- (instancetype)initWithURL:(NSURL *)url;
- (instancetype)initWithURL:(NSURL *)url protocols:(NSArray<NSString *> *)protocols;
- (instancetype)initWithURL:(NSURL *)url protocols:(NSArray<NSString *> *)protocols allowsUntrustedSSLCertificates:(BOOL)allowsUntrustedSSLCertificates;
// By default, it will schedule itself on +[NSRunLoop SR_networkRunLoop] using defaultModes.
- (void)scheduleInRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode;
- (void)unscheduleFromRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode;
// SRWebSockets are intended for one-time-use only. Open should be called once and only once.
- (void)open;
- (void)close;
- (void)closeWithCode:(NSInteger)code reason:(NSString *)reason;
///--------------------------------------
#pragma mark Send
///--------------------------------------
//下面是4個(gè)發(fā)送的方法
/**
Send a UTF-8 string or binary data to the server.
@param message UTF-8 String or Data to send.
@deprecated Please use `sendString:` or `sendData` instead.
*/
- (void)send:(id)message __attribute__((deprecated("Please use `sendString:` or `sendData` instead.")));
- (void)sendString:(NSString *)string;
- (void)sendData:(NSData *)data;
- (void)sendPing:(NSData *)data;
@end
對(duì)應(yīng)5種狀態(tài)的代理方法
///--------------------------------------
#pragma mark - SRWebSocketDelegate
///--------------------------------------
@protocol SRWebSocketDelegate <NSObject>
- (void)webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message;
@optional
- (void)webSocketDidOpen:(SRWebSocket *)webSocket;
- (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error;
- (void)webSocket:(SRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean;
- (void)webSocket:(SRWebSocket *)webSocket didReceivePong:(NSData *)pongPayload;
// Return YES to convert messages sent as Text to an NSString. Return NO to skip NSData -> NSString conversion for Text messages. Defaults to YES.
- (BOOL)webSocketShouldConvertTextFrameToString:(SRWebSocket *)webSocket;
@end
didReceiveMessage方法是必須實(shí)現(xiàn)的查蓉,用來(lái)接收消息的乌询。
下面4個(gè)did方法分別對(duì)應(yīng)著Open,F(xiàn)ail豌研,Close妹田,ReceivePong不同狀態(tài)的代理方法
方法就上面這些了唬党,我們實(shí)際來(lái)看看代碼怎么寫
先是初始化Websocket連接,注意此處ws://或者wss://連接有且最多只能有一個(gè)鬼佣,這個(gè)是Websocket協(xié)議規(guī)定的
self.ws = [[SRWebSocket alloc] initWithURLRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"%@://%@:%zd/ws", serverProto, serverIP, serverPort]]]];
self.ws.delegate = delegate;
[self.ws open];
發(fā)送消息
[self.ws send:message];
接收消息以及其他3個(gè)代理方法
//這個(gè)就是接受消息的代理方法了驶拱,這里接受服務(wù)器返回的數(shù)據(jù),方法里面就應(yīng)該寫處理數(shù)據(jù)晶衷,存儲(chǔ)數(shù)據(jù)的方法了蓝纲。
- (void)webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message
{
NSDictionary *data = [NetworkUtils decodeData:message];
if (!data)
return;
}
//這里是Websocket剛剛Open之后的代理方法。就想微信剛剛連接中晌纫,會(huì)顯示連接中税迷,當(dāng)連接上了,就不顯示連接中了锹漱,取消顯示連接的方法就應(yīng)該寫在這里面
- (void)webSocketDidOpen:(SRWebSocket *)webSocket
{
// Open = silent ping
[self.ws receivedPing];
}
//這是關(guān)閉Websocket的代理方法
- (void)webSocket:(SRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean
{
[self failedConnection:NSLS(Disconnected)];
}
//這里是連接Websocket失敗的方法箭养,這里面一般都會(huì)寫重連的方法
- (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error
{
[self failedConnection:NSLS(Disconnected)];
}
最后
以上就是我想分享的一些關(guān)于Websocket的心得,文中如果有錯(cuò)誤的地方哥牍,歡迎大家指點(diǎn)毕泌!一般沒(méi)有微信QQ那么大用戶量的app,用Websocket應(yīng)該都可以完成IM社交聊天的任務(wù)嗅辣。當(dāng)用戶達(dá)到億級(jí)別撼泛,應(yīng)該還有很多需要優(yōu)化,優(yōu)化性能各種的吧澡谭。
最后坎弯,微信和QQ的實(shí)現(xiàn)方法也許并不是只用Websocket和Socket這么簡(jiǎn)單,也許是他們自己開發(fā)的一套能支持這么大用戶译暂,大數(shù)據(jù)的抠忘,各方面也都優(yōu)化都最優(yōu)的方法。如果有開發(fā)和微信和QQ的大神看到這篇文章外永,可以留言說(shuō)說(shuō)看你們用什么方式實(shí)現(xiàn)的崎脉,也可以和我們一起分享,我們一起學(xué)習(xí)伯顶!我先謝謝大神們的指點(diǎn)了囚灼!