【轉載】神馬是websocket

HTML5 WebSockets Architecture

衆所周知,HTTP是一種基於消息(message)的請求(request )/應答(response)協議。當我們在網頁中點擊一條鏈接(或者提交一個表單)的時候,瀏覽器給服務器發一個request message,然後服務器算啊算,答覆一條response message。主動發起TCP連接的是client,接受TCP連接的是server。HTTP消息只有兩種:request和response。client只能發送request message,server只能發送response message。一問一答,因此按HTTP協議本身的設計,服務器不能主動的把消息推給客戶端。而像微博、網頁聊天、網頁遊戲等都需要服務器主動給客戶端推東西,現在只能用long polling等方式模擬,很不方便。

 

OK,來看看internet的另一邊,網絡遊戲是怎麼工作的?

我之前在一個遊戲公司工作。我們做遊戲的時候,普遍採用的模式是雙向、異步消息模式。

首先通信的最基本單元是message。(這點和HTTP一樣)

其次,是雙向的。client和server都可以給對方發消息(這點和HTTP不一樣)

最後,消息是異步的。我給服務器發一條消息出去,然後可能有一條答覆,也可能有多條答覆,也可能根本沒有答覆。無論如何,調用完send方法我就不管了,我不會傻乎乎的在這裏等答覆。服務器和客戶端都會有一個線程專門負責read,以及一個大大的switch… case,根據message id做相應的action。

while( msg=myconnection.readMessage()){

switch(msg.id()){

case LOGIN: do_login(); break; 
case TALK: do_talk(); break; 

}

}

Websocket就是把這樣一種模式,搬入到HTTP/WEB的框架內。它主要解決兩個問題:

  1. 從服務器給客戶端主動推東西。
  2. HTTP協議傳輸效率低下的問題。這一點在web service中尤爲突出。每個請求和應答都得帶上很長的http header!

websocket協議在RFC 6455中定義,這個RFC在上個月(2011年12月)才終於定稿、提交。所以目前沒有任何一個瀏覽器是能完全符合這個RFC的最終版的。Google是websocket協議的主力支持者,目前主流的瀏覽器中,對websocket支持最好的就是chrome。chrome目前的最新版本是16,支持的是RFC 6455的draft 13,http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-13。IE9則是完全不支持websocket。而server端,只有jetty和Node.js對websocket的支持比較好。

 

Websocket協議可以分爲兩個階段,一個是握手階段,一個是數據傳輸階段。

在建立TCP連接之後,首先是websocket層的握手。這階段很簡單,client給server發一個http request,server給client一個http response。這個階段,所有數據傳輸都是基於文本的,和現有的HTTP/1.1協議保持兼容。

這是一個請求的例子: 
Connection:Upgrade 

Host:echo.websocket.org 

Origin:http://websocket.org 

Sec-WebSocket-Key:ov0xgaSDKDbFH7uZ1o+nSw== 

Sec-WebSocket-Version:13 

Upgrade:websocket 

 

(其中Host和Origin不是必須的)

Connection是HTTP/1.1中常用的一個header,以前經常填的是keepalive或close。這裏填的是Upgrade。在設計HTTP/1.1的時候,委員們就想到一個問題,假如以後出HTTP 2.0了,那麼現有的這套東西怎麼辦呢?所以HTTP協議中就預定義了一個header叫Upgrade。如果客戶端發來的請求中有這個,那麼意思就是說,我支持某某協議,並且我更偏向於用這個協議,你看你是不是也支持?你要是支持,咱們就換新協議吧!

然後就是websocket協議中所定義的兩個特殊的header,Sec-WebSocket-Key和Sec-WebSocket-Version。

其中Sec-WebSocket-Key是客戶端生的一串隨機數,然後base64之後填進去的。Sec-WebSocket-Version是指協議的版本號。這裏的13,代表draft 13。下面給出,我年前寫的發送握手請求的JAVA代碼:

// 生一個隨機字符串,作爲Sec-WebSocket-Key

?View CodeJAVA
 
        byte[] nonce = new byte[16];
        rand.nextBytes(nonce);
        BASE64Encoder encode = new BASE64Encoder();
        String chan = encode.encode(nonce);
 
        HttpRequest request = new BasicHttpRequest("GET", "/someurl");
        request.addHeader("Host", host);
        request.addHeader("Upgrade", "websocket");
        request.addHeader("Connection", "Upgrade");
        request.addHeader("Sec-WebSocket-Key", chan); // 生的隨機串
         request.addHeader("Sec-WebSocket-Version", "13");
        HttpResponse response;
        try {
            conn.sendRequestHeader(request);
            conn.flush();
            request.toString();
            response = conn.receiveResponseHeader();
        } catch (HttpException ex) {
            throw new RuntimeException("handshake fail", ex);
        }

 

服務器在收到握手請求之後需要做相應的答覆。消息的例子如下:

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字段是一個根據client發來的Sec-WebSocket-Key得到的計算結果。

算法爲:

把客戶端發來的key作爲字符串,與” 258EAFA5-E914-47DA-95CA-C5AB0DC85B11″這個字符串連接起來,然後做sha1 Hash,將計算結果base64編碼。注意,用來和” 258EAFA5-E914-47DA-95CA-C5AB0DC85B11″做連接操作的字符串,是base64編碼後的。也就是說,客戶端發來什麼樣就是什麼樣,不要試圖去做base64解碼。

示例代碼如下:

?View CodeCPP
 
    std::string value=request.get("Sec-WebSocket-Key");
    value+="258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
    unsigned char hash[20];
    sha1::calc(value.c_str(),value.length(),hash);
    std::string res=base64_encode(hash,sizeof(hash));
    std::ostringstream oss;
    oss<<"HTTP/1.1 101 Switching Protocols\r\n"
        "Upgrade: websocket\r\n"
        "Connection: Upgrade\r\n"
        "Sec-WebSocket-Accept: "<<res<<"\r\n";
               connection.send(oss.str());

握手成功後,進入數據流階段。這個階段就和http協議沒什麼關係了。是在TCP流的基礎上,把數據分成frame而已。首先,websocket的一個message,可以被分成多個frame。從邏輯上來看,frame的格式如下

isFinal

opcode

isMasked

Data length

Mask key

Data(可以是文本,也可以是二進制)

 

isFinal:

每個frame的第一個字節的最高位,代表這個frame是不是該message的最後一個frame。1代表是最後一個,0代表後面還有。

opcode:

指明這個frame的類型。目前定義了這麼幾類continuation、text 、binary 、connection close、ping、pong。對於應用而言,最關心的就是,這個message是binary的呢,還是text的?因爲html5中,對於這兩種message的接口有細微不一樣。

isMasked:

客戶端發給服務器的消息,要求加擾之後發送。加擾的方式是:客戶端生一個32位整數(mask key),然後把data和這32位整數做異或。

mask key:

前面已經說過了,就是用來做異或的隨機數。

Data:

這纔是我們真正要傳輸的數據啊!!

發送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]));

        }

下面討論一下這個協議的某些設計:

爲什麼要做這個異或操作呢?

說來話長。首先從Connection:Upgrade這個header講起。本來它是留給TLS用的。就是,假如我從80端口去連接一個服務器,然後通過發送Connection:Upgrade,仿照上面所說的流程,把http協議”升級”成https協議。但是實際上根本沒人這麼用。你要用https,你就去連接443。我80端口根本不處理https。由於這個header只是出現在rfc中,並未實際使用,於是大多數cache server看不懂這個header。這樣的結果是,cache server可能以爲後面的傳輸數據依然是普通的http協議,然後按照原來的規則做cache。那麼,如果這個client和server都已經被黑客很好的操控,他就可以往這個cache server上投毒。比如,從client發送一個websocket frame,但是僞裝成普通的http GET請求,指向一個JS文件。但是這個GET請求的目的地未必是之前那個websocket server,可能是另外一臺web server。然後他再去操控這個web server,做好配合,給一個看起來像http response的答覆(實際是websocket frame),裏面放的是被修改過的js文件。然後cache server就會把這個js文件錯誤的緩存下來,以後發給其他人。

首先,client是誰?是瀏覽器。它在一個不很安全的環境中,很容易受到XSS或者流氓插件的攻擊。假如我們的頁面遭到了xss,使得攻擊者可以利用JS從受害者的頁面上發送任意字符串給服務器,如果沒有這個異或操作,那麼他就可以控制什麼樣的二進制數據出現在信道上,從而實現上述攻擊。但是我還是覺得有點問題。proxy server一般都會對目的地做嚴格的限制,比如,sina的squid肯定不會幫new.163.com做cache。那麼既然你已經控制了一個web server,爲什麼不讓js直接這麼做呢?那篇paper的名字叫《Talking to Yourself for Fun and Profit》,有空我繼續看。貌似是中國人寫的。

還有,爲什麼要把message分成frame呢? 因爲HTTP協議有chunk功能,可以讓服務器一邊生數據,一邊發。而websocket協議也考慮到了這點。如果沒有framing功能,那麼我必須知道整個message的長度之後,才能開始發送message的data。

原文鏈接: http://www.udpwork.com/redirect/6758

websocket官網: http://www.websocket.org/aboutwebsocket.html

發佈了98 篇原創文章 · 獲贊 3 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章