最近發現,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。
本文出自:馮立彬的部落格