基於Websocket草案10協議的升級及基於Netty的握手實現

來源:互聯網
上載者:User

    最近發現,WEBWW在chrome14及FF6.5中沒法與後台建立串連了,後面經過尋找原因,是chrome14中使用最新的websocket協議草案,而chrome12中使用的websocket協議標準還是草案7.5、7.6的標準;現在草案的最新版本是草案10,草案的連結地址為:http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-10,本次協議變更比較大,主要體現在安全性和可擴充性上:

    1、握手的標準:

1)、最老的websocket草案標準中是沒有安全key,草案7.5、7.6中有兩個安全key,而現在的草案10中只有一個安全key,即將7.5、7.6中http頭中的"Sec-WebSocket-Key1"與"Sec-WebSocket-Key2"合并為了一個"Sec-WebSocket-Key"

2)、把http頭中Upgrade的值由"WebSocket"修改為了"websocket";

3)、把http頭中的"-Origin"修改為了"Sec-WebSocket-Origin";

4)、增加了http頭"Sec-WebSocket-Accept",用來返回原來草案7.5、7.6伺服器返回給用戶端的握手驗證,原來是以內容的形式返回,現在是放到了http頭中;另外伺服器返回用戶端的驗證方式也變了,後面會有介紹。

    2、資料轉送的格式:

以下是一個格式標準圖:

FIN:1位,用來表明這是一個訊息的最後的訊息片斷,當然第一個訊息片斷也可能是最後的一個訊息片斷;

RSV1, RSV2, RSV3: 分別都是1位,如果雙方之間沒有約定自訂協議,那麼這幾位的值都必須為0,否則必須斷掉WebSocket串連;

Opcode:4位作業碼,定義承載資料,如果收到了一個未知的作業碼,串連也必須斷掉,以下是定義的作業碼:
      *  %x0 表示連續訊息片斷
      *  %x1 表示簡訊片斷
      *  %x2 表未二進位訊息片斷
      *  %x3-7 為將來的非控制訊息片斷保留的作業碼
      *  %x8 表示串連關閉
      *  %x9 表示心跳檢查的ping
      *  %xA 表示心跳檢查的pong
      *  %xB-F 為將來的控制訊息片斷的保留作業碼

Mask:1位,定義傳輸的資料是否有加掩碼,如果設定為1,掩碼鍵必須放在masking-key地區,用戶端發送給服務端的所有訊息,此位的值都是1;

Payload length: 傳輸資料的長度,以位元組的形式表示:7位、7+16位、或者7+64位。如果這個值以位元組表示是0-125這個範圍,那這個值就表示傳輸資料的長度;如果這個值是126,則隨後的兩個位元組表示的是一個16進位無符號數,用來表示傳輸資料的長度;如果這個值是127,則隨後的是8個位元組表示的一個64位無符合數,這個數用來表示傳輸資料的長度。多位元組長度的數量是以網路位元組的順序表示。負載資料的長度為擴充資料及應用資料之和,擴充資料的長度可能為0,因而此時負載資料的長度就為應用資料的長度。

Masking-key:0或4個位元組,用戶端發送給服務端的資料,都是通過內嵌的一個32位值作為掩碼的;掩碼鍵只有在掩碼位設定為1的時候存在。
Payload data:  (x+y)位,負載資料為擴充資料及應用資料長度之和。
Extension data:x位,如果用戶端與服務端之間沒有特殊約定,那麼擴充資料的長度始終為0,任何的擴充都必須指定擴充資料的長度,或者長度的計算方式,以及在握手時如何確定正確的握手方式。如果存在擴充資料,則擴充資料就會包括在負載資料的長度之內。
Application data:y位,任意的應用資料,放在擴充資料之後,應用資料的長度=負載資料的長度-擴充資料的長度。

    資料幀協議是按照擴充的巴科斯範式(ANBF:Augmented Backus-Naur Form
RFC5234)組成的:       

   ws-frame                = frame-fin                             frame-rsv1                             frame-rsv2                             frame-rsv3                             frame-opcode                             frame-masked                             frame-payload-length                             [ frame-masking-key ]                             frame-payload-data   frame-fin               = %x0 ; 表示這不是當前訊息的最後一幀,後面還有訊息                           / %x1 ; 表示這是當前訊息的最後一幀   frame-rsv1              = %x0                             ; 1 bit, 如果沒有擴充約定,該值必須為0   frame-rsv2              = %x0                             ; 1 bit, 如果沒有擴充約定,該值必須為0   frame-rsv3              = %x0                             ; 1 bit, 如果沒有擴充約定,該值必須為0   frame-opcode            = %x0 ; 表示這是一個連續幀訊息                           / %x1 ; 表示簡訊                           / %x2 ; 表示二進位訊息                           / %x3-7 ; 保留                           / %x8 ; 表示用戶端發起的關閉                           / %x9 ; ping(用於心跳)                           / %xA ; pong(用於心跳)                           / %xB-F ; 保留   frame-masked            = %x0 ; 資料幀沒有加掩碼,後面沒有掩碼key                           / %x1 ; 資料幀加了掩碼,後面有掩碼key   frame-payload-length    = %x00-7D                           / %x7E frame-payload-length-16                           / %x7F frame-payload-length-63   ; 表示資料幀的長度   frame-payload-length-16 = %x0000-FFFF   ; 表示資料幀的長度   frame-payload-length-63 = %x0000000000000000-7FFFFFFFFFFFFFFF   ; 表示資料幀的長度   frame-masking-key       = 4( %0x00-FF ) ; 掩碼key,只有當掩碼位為1時出現   frame-payload-data      = (frame-masked-extension-data                              frame-masked-application-data)   ; 當掩碼位為1時,這裡的資料為帶掩碼的資料,擴充資料及應用資料都帶掩碼                           / (frame-unmasked-extension-data                              frame-unmasked-application-data) ; 當掩碼位為0時,這裡的資料為不帶掩碼的資料,擴充資料及應用資料都不帶掩碼   frame-masked-extension-data     = *( %x00-FF ) ; 目前保留,以後定義   frame-masked-application-data   = *( %x00-FF )   frame-unmasked-extension-data   = *( %x00-FF ) ; 目前保留,以後定義   frame-unmasked-application-data = *( %x00-FF )

    還有一些其它的變更,詳細的可以查看查案文檔了,主要的應該就是上面提到的兩大塊。

    後台Server使用的是Netty NIO架構,目前Netty官方還沒有對websocket草案10的實現,只支援到草案7.6,自己根據草案標準實現netty的encode及decoder也不難,不過在非官方還是找到了Netty支援最新草案10的實現:https://github.com/joewalnes/webbit/tree/0356ba12f5c21f8a297a5afb433215bb2f738008/src/main/java/org/webbitserver/netty,呵,有現成的就用現成的了。

    在後台只需要判斷是最新的草案10,將Encoder及Decoder分別換成Hybi10WebSocketFrameDecoder()及Hybi10WebSocketFrameEncoder(),判斷是否最新草案10的websocket協議,只需要判斷要求標頭中是否同時包括了這兩個要求標頭:Sec-WebSocket-Origin及Sec-WebSocket-Key,如果包含了那說明是草案10標準,如果不是再判斷是否草案76或最老的草案標準,進行相應的處理。

    握手的實現,首先要擷取到要求標頭中的Sec-WebSocket-Key的值,再把這一段GUID "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"加到擷取到的Sec-WebSocket-Key的值的後面,然後拿這個字串做SHA-1 hash計算,然後再把得到的結果通過base64加密,就得到了返回給用戶端的Sec-WebSocket-Accept的http回應標頭的值,以下是基於Netty的JAVA實現:    

    private HttpResponse buildWebSocketRes(HttpRequest req, boolean isWebSocketProtocolAfterDraft7) {        String reasonPhrase = "";        // websocket協議草案7後面的格式,可以參看wikipedia上面的說明,比較前後版本的不同:http://en.wikipedia.org/wiki/WebSocket        reasonPhrase = "Switching Protocols";        HttpResponse res = new DefaultHttpResponse(HttpVersion.HTTP_1_1, new HttpResponseStatus(101, reasonPhrase));        res.addHeader(HttpHeaders.Names.UPGRADE, "websocket");        res.addHeader(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.UPGRADE);String protocol = req.getHeader(Names.SEC_WEBSOCKET_PROTOCOL);if (protocol != null) {res.addHeader(Names.SEC_WEBSOCKET_PROTOCOL, protocol);}res.addHeader(SEC_WEBSOCKET_ACCEPT, getSecWebSocketAccept(req));        return res;    }private String getSecWebSocketAccept(HttpRequest req) {        // CHROME WEBSOCKET VERSION 8中定義的GUID,詳細文檔地址:http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-10        String guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";        String key = "";        key = req.getHeader(SEC_WEBSOCKET_KEY);        key += guid;        try {            MessageDigest md = MessageDigest.getInstance("SHA-1");            md.update(key.getBytes("iso-8859-1"), 0, key.length());            byte[] sha1Hash = md.digest();            key = base64Encode(sha1Hash);        } catch (NoSuchAlgorithmException e) {            if (logger.isErrorEnabled()) {                logger.error("NoSuchAlgorithmException:" + e.getMessage(), e);            }        } catch (UnsupportedEncodingException e) {            if (logger.isErrorEnabled()) {                logger.error("UnsupportedEncodingException:" + e.getMessage(), e);            }        }        return key;    }/**     * 將輸入位元組數組進行base64編碼,再返回編碼後的字串     *      * @param input     * @return     */    public static String base64Encode(byte[] input) {        BASE64Encoder encoder = new BASE64Encoder();        String base64 = encoder.encode(input);        return base64;    }

    另外,最老版本的無安全Key的WebSocket的握手實現以及基於草案76以前的握手實現,可以參看我的另外一篇文章:http://blog.csdn.net/fenglibing/article/details/6699154。

本文出自:馮立彬的部落格

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.