一 302012 今天@julyclyde 在微博上問我websocket的細(xì)節(jié),。但是這個用70個字是無法說清楚的,所以就整理在這里吧,。恰好我最近要重構(gòu)年前寫的websocket的代碼,。 眾所周知,HTTP是一種基于消息(message)的請求(request )/應(yīng)答(response)協(xié)議,。當(dāng)我們在網(wǎng)頁中點擊一條鏈接(或者提交一個表單)的時候,,瀏覽器給服務(wù)器發(fā)一個request message,然后服務(wù)器算啊算,,答復(fù)一條response message,。主動發(fā)起TCP連接的是client,,接受TCP連接的是server,。HTTP消息只有兩種:request和response。client只能發(fā)送request message,,server只能發(fā)送response message,。一問一答,因此按HTTP協(xié)議本身的設(shè)計,,服務(wù)器不能主動的把消息推給客戶端,。而像微博、網(wǎng)頁聊天,、網(wǎng)頁游戲等都需要服務(wù)器主動給客戶端推東西,,現(xiàn)在只能用long polling等方式模擬,,很不方便。
OK,,來看看internet的另一邊,,網(wǎng)絡(luò)游戲是怎么工作的? 我之前在一個游戲公司工作,。我們做游戲的時候,,普遍采用的模式是雙向、異步消息模式,。 首先通信的最基本單元是message,。(這點和HTTP一樣) 其次,是雙向的,。client和server都可以給對方發(fā)消息(這點和HTTP不一樣) 最后,,消息是異步的。我給服務(wù)器發(fā)一條消息出去,,然后可能有一條答復(fù),,也可能有多條答復(fù),也可能根本沒有答復(fù),。無論如何,,調(diào)用完send方法我就不管了,我不會傻乎乎的在這里等答復(fù),。服務(wù)器和客戶端都會有一個線程專門負(fù)責(zé)read,,以及一個大大的switch… case,根據(jù)message id做相應(yīng)的action,。 while( msg=myconnection.readMessage()){ switch(msg.id()){ case LOGIN: do_login(); break; } } Websocket就是把這樣一種模式,,搬入到HTTP/WEB的框架內(nèi)。它主要解決兩個問題:
websocket協(xié)議在RFC 6455中定義,,這個RFC在上個月(2011年12月)才終于定稿、提交,。所以目前沒有任何一個瀏覽器是能完全符合這個RFC的最終版的,。Google是websocket協(xié)議的主力支持者,目前主流的瀏覽器中,,對websocket支持最好的就是chrome,。chrome目前的最新版本是16,支持的是RFC 6455的draft 13,http://tools./html/draft-ietf-hybi-thewebsocketprotocol-13 ,。IE9則是完全不支持websocket,。而server端,只有jetty和Node.js對websocket的支持比較好,。
Websocket協(xié)議可以分為兩個階段,,一個是握手階段,一個是數(shù)據(jù)傳輸階段,。 在建立TCP連接之后,,首先是websocket層的握手。這階段很簡單,,client給server發(fā)一個http request,,server給client一個http response。這個階段,,所有數(shù)據(jù)傳輸都是基于文本的,,和現(xiàn)有的HTTP/1.1協(xié)議保持兼容。 這是一個請求的例子:
(其中Host和Origin不是必須的) Connection是HTTP/1.1中常用的一個header,,以前經(jīng)常填的是keepalive或close,。這里填的是Upgrade。在設(shè)計HTTP/1.1的時候,,委員們就想到一個問題,,假如以后出HTTP 2.0了,那么現(xiàn)有的這套東西怎么辦呢,?所以HTTP協(xié)議中就預(yù)定義了一個header叫Upgrade,。如果客戶端發(fā)來的請求中有這個,那么意思就是說,,我支持某某協(xié)議,,并且我更偏向于用這個協(xié)議,你看你是不是也支持,?你要是支持,,咱們就換新協(xié)議吧! 然后就是websocket協(xié)議中所定義的兩個特殊的header,,Sec-WebSocket-Key和Sec-WebSocket-Version,。 其中Sec-WebSocket-Key是客戶端生的一串隨機(jī)數(shù),然后base64之后填進(jìn)去的,。Sec-WebSocket-Version是指協(xié)議的版本號,。這里的13,代表draft 13,。下面給出,我年前寫的發(fā)送握手請求的JAVA代碼: // 生一個隨機(jī)字符串,,作為Sec-WebSocket-Key View Code JAVA
服務(wù)器在收到握手請求之后需要做相應(yīng)的答復(fù),。消息的例子如下: HTTP/1.1 101 Switching Protocols Connection:Upgrade Date:Sun, 29 Jan 2012 18:05:49 GMT Sec-WebSocket-Accept:7vI97qQ5QRxq6lD6E5RRX36mOBc= Server:jetty Upgrade:websocket (其中Date,、Server都不是必須的) 第一行是HTTP的Status-Line。注意,,這里的Status Code是101,。很少見吧!Sec-WebSocket-Accept字段是一個根據(jù)client發(fā)來的Sec-WebSocket-Key得到的計算結(jié)果,。 算法為: 把客戶端發(fā)來的key作為字符串,,與” 258EAFA5-E914-47DA-95CA-C5AB0DC85B11″這個字符串連接起來,然后做sha1 Hash,,將計算結(jié)果base64編碼,。注意,用來和” 258EAFA5-E914-47DA-95CA-C5AB0DC85B11″做連接操作的字符串,,是base64編碼后的,。也就是說,客戶端發(fā)來什么樣就是什么樣,,不要試圖去做base64解碼,。 示例代碼如下: View Code CPP
握手成功后,進(jìn)入數(shù)據(jù)流階段,。這個階段就和http協(xié)議沒什么關(guān)系了,。是在TCP流的基礎(chǔ)上,把數(shù)據(jù)分成frame而已,。首先,,websocket的一個message,可以被分成多個frame,。從邏輯上來看,,frame的格式如下
isFinal: 每個frame的第一個字節(jié)的最高位,,代表這個frame是不是該message的最后一個frame,。1代表是最后一個,0代表后面還有,。 opcode: 指明這個frame的類型,。目前定義了這么幾類continuation、text ,、binary ,、connection close、ping,、pong,。對于應(yīng)用而言,最關(guān)心的就是,這個message是binary的呢,,還是text的,?因為html5中,對于這兩種message的接口有細(xì)微不一樣,。 isMasked: 客戶端發(fā)給服務(wù)器的消息,,要求加擾之后發(fā)送。加擾的方式是:客戶端生一個32位整數(shù)(mask key),,然后把data和這32位整數(shù)做異或,。 mask key: 前面已經(jīng)說過了,就是用來做異或的隨機(jī)數(shù),。 Data: 這才是我們真正要傳輸?shù)臄?shù)據(jù)?。?! 發(fā)送frame時加擾的代碼如下: java.util.Random rand ; ByteBuffer buffer; byte[] dataToSend; …
byte[] mask = new byte[4]; rand.nextBytes(mask); buffer.put(mask); int oldpos = buffer.position(); buffer.put(data); int newpos = buffer.position(); // 按位異或 for (int i = oldpos; i != newpos; ++i) { int maskIndex = (i – oldpos) % mask.length; buffer.put(i, (byte) (buffer.get(i) ^ (byte) mask[maskIndex])); } 下面討論一下這個協(xié)議的某些設(shè)計: 為什么要做這個異或操作呢,? 說來話長。首先從Connection:Upgrade這個header講起,。本來它是留給TLS用的,。就是,假如我從80端口去連接一個服務(wù)器,,然后通過發(fā)送Connection:Upgrade,,仿照上面所說的流程,把http協(xié)議”升級”成https協(xié)議,。但是實際上根本沒人這么用,。你要用https,你就去連接443,。我80端口根本不處理https,。由于這個header只是出現(xiàn)在rfc中,并未實際使用,,于是大多數(shù)cache server看不懂這個header,。這樣的結(jié)果是,cache server可能以為后面的傳輸數(shù)據(jù)依然是普通的http協(xié)議,,然后按照原來的規(guī)則做cache,。那么,如果這個client和server都已經(jīng)被黑客很好的操控,,他就可以往這個cache server上投毒,。比如,從client發(fā)送一個websocket frame,,但是偽裝成普通的http GET請求,,指向一個JS文件,。但是這個GET請求的目的地未必是之前那個websocket server,可能是另外一臺web server,。然后他再去操控這個web server,,做好配合,,給一個看起來像http response的答復(fù)(實際是websocket frame),,里面放的是被修改過的js文件。然后cache server就會把這個js文件錯誤的緩存下來,,以后發(fā)給其他人,。 首先,client是誰,?是瀏覽器,。它在一個不很安全的環(huán)境中,很容易受到XSS或者流氓插件的攻擊,。假如我們的頁面遭到了xss,,使得攻擊者可以利用JS從受害者的頁面上發(fā)送任意字符串給服務(wù)器,如果沒有這個異或操作,,那么他就可以控制什么樣的二進(jìn)制數(shù)據(jù)出現(xiàn)在信道上,,從而實現(xiàn)上述攻擊。但是我還是覺得有點問題,。proxy server一般都會對目的地做嚴(yán)格的限制,,比如,sina的squid肯定不會幫new.163.com做cache,。那么既然你已經(jīng)控制了一個web server,,為什么不讓js直接這么做呢?那篇paper的名字叫《Talking to Yourself for Fun and Pro?t》,,有空我繼續(xù)看,。貌似是中國人寫的。 還有,,為什么要把message分成frame呢,? 因為HTTP協(xié)議有chunk功能,可以讓服務(wù)器一邊生數(shù)據(jù),,一邊發(fā),。而websocket協(xié)議也考慮到了這點。如果沒有framing功能,,那么我必須知道整個message的長度之后,,才能開始發(fā)送message的data。 |
|