WebSocket不同版本的三種握手方式以及一個Netty實現JAVA類

 

一、WebSocket不同版本的三種握手方式

WebSocket是HTML5中的新特性,應用也是非常的廣泛,特別是用戶WEB端與後臺服務器的消息通訊,如阿里的WEBWW就是使用的WebSocket與後端服務器建立長連接進行的通訊。目前WebSocket還處於發展當中,就目前的發展過程而言,WebSocket現在不同的版本,有三種不同的握手方式:

1、基於Flash的WebSocket通訊,使用場景是IE的多數版本,因爲IE的多數版本不都不支持WebSocket協議,以及FF、CHROME等瀏覽器的低版本,還沒有原生的支持WebSocket,可以使用FLASH的WebSocket實現進行通訊:

瀏覽器請求:

  1. GET /ls HTTP/1.1  
  2. Upgrade: WebSocket  
  3. Connection: Upgrade  
  4. Host: www.xx.com  
  5. Origin: http://www.xx.com  

服務器迴應:

  1. HTTP/1.1 101 Web Socket Protocol Handshake  
  2. Upgrade: WebSocket  
  3. Connection: Upgrade  
  4. WebSocket-Origin: http://www.xx.com  
  5. WebSocket-Location: ws://www.xx.com/ls  

原理:

    如果客戶端沒有發送Origin請求頭,則客戶端不需要返回,如果客戶端沒有發送WebSocket-Protocol請求頭,服務端也不需要返回;服務端唯一需要組裝返回給客戶端做爲校驗的就是WebSocket-Location請求頭,拼裝一個websocket請求的地址就可以了。

  這種方式,是最老的一種方式,連一個安全Key都沒有,服務端也沒有對客戶的請求做加密性校驗。

2、第二種握手方式是帶兩個安全key請求頭的,結果以md5加密,並放在body中返回的方式,參看如下示例:

瀏覽器請求:

  1. GET /demo HTTP/1.1  
  2. Host: example.com  
  3. Connection: Upgrade  
  4. Sec-WebSocket-Key2: 12998 5 Y3 1  .P00  
  5. Sec-WebSocket-Protocol: sample  
  6. Upgrade: WebSocket  
  7. Sec-WebSocket-Key1: 4 @1  46546xW%0l 1 5  
  8. Origin: http://example.com  
  9. ^n:ds[4U  

服務器迴應:

  1. HTTP/1.1 101 WebSocket Protocol Handshake  
  2. Upgrade: WebSocket  
  3. Connection: Upgrade  
  4. Sec-WebSocket-Origin: http://example.com  
  5. Sec-WebSocket-Location: ws://example.com/demo  
  6. Sec-WebSocket-Protocol: sample  
  7. 8jKS’y:G*Co,Wxa-  

原理:

    在請求中的“Sec-WebSocket-Key1”, “Sec-WebSocket-Key2”和最後的“^n:ds[4U”都是隨機的,服務器端會用這些數據來構造出一個16字節的應答。

把第一個Key中的數字除以第一個Key的空白字符的數量,而第二個Key也是如此。然後把這兩個結果與請求最後的8字節字符串連接起來成爲一個字符串,服務器應答正文(“8jKS’y:G*Co,Wxa-”)即這個字符串的MD5 sum。


3、第三種是帶一個安全key的請求,結果是先以“SHA-1”進行加密,再以base64的加密,結果放在Sec-WebSocket-Accept請求頭中返回的方式:

瀏覽器請求:

  1. GET /ls HTTP/1.1  
  2. Upgrade: websocket  
  3. Connection: Upgrade  
  4. Host: www.xx.com  
  5. Sec-WebSocket-Origin: http://www.xx.com  
  6. Sec-WebSocket-Key: 2SCVXUeP9cTjV+0mWB8J6A==  
  7. Sec-WebSocket-Version: 8  

服務器迴應:

  1. HTTP/1.1 101 Switching Protocols  
  2. Upgrade: websocket  
  3. Connection: Upgrade  
  4. Sec-WebSocket-Accept: mLDKNeBNWz6T9SxU+o0Fy/HgeSw=  


原理:

   握手的實現,首先要獲取到請求頭中的Sec-WebSocket-Key的值,再把這一段GUID "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"加到獲取到的Sec-WebSocket-Key的值的後面,然後拿這個字符串做SHA-1 hash計算,然後再把得到的結果通過base64加密,就得到了返回給客戶端的Sec-WebSocket-Accept的http響應頭的值。

    還可以參看我前面專門針對這種協議寫的一篇文章:http://blog.csdn.net/fenglibing/article/details/6852497


二、基於Netty實現JAVA類

爲了支持以上提到的三種不同版本的websocket握手實現,服務端就需要針對這三種情況進行相應的處理,以下是一段基於netty實現的java代碼,一個完整的WebSocketHelper實現:

  1. import java.io.UnsupportedEncodingException;  
  2. import java.security.MessageDigest;  
  3. import java.security.NoSuchAlgorithmException;  
  4.   
  5. import org.jboss.netty.buffer.ChannelBuffer;  
  6. import org.jboss.netty.buffer.ChannelBuffers;  
  7. import org.jboss.netty.handler.codec.http.DefaultHttpResponse;  
  8. import org.jboss.netty.handler.codec.http.HttpHeaders;  
  9. import org.jboss.netty.handler.codec.http.HttpHeaders.Names;  
  10. import org.jboss.netty.handler.codec.http.HttpRequest;  
  11. import org.jboss.netty.handler.codec.http.HttpResponse;  
  12. import org.jboss.netty.handler.codec.http.HttpResponseStatus;  
  13. import org.jboss.netty.handler.codec.http.HttpVersion;  
  14.   
  15. public class WebSocketHelper {  
  16.   
  17.     private final static String SEC_WEBSOCKET_KEY     = "Sec-WebSocket-Key";  
  18.     private final static String SEC_WEBSOCKET_ACCEPT  = "Sec-WebSocket-Accept";  
  19.     /* websocket版本號:草案8到草案12版本號都是8,草案13及以後的版本號都和草案號相同 */  
  20.     private final static String Sec_WebSocket_Version = "Sec-WebSocket-Version";  
  21.   
  22.     /** 
  23.      * 判斷是否是WebSocket請求 
  24.      *  
  25.      * @param req 
  26.      * @return 
  27.      */  
  28.     public boolean supportWebSocket(HttpRequest req) {  
  29.         return (HttpHeaders.Values.UPGRADE.equalsIgnoreCase(req.getHeader(HttpHeaders.Names.CONNECTION)) && HttpHeaders.Values.WEBSOCKET.equalsIgnoreCase(req.getHeader(HttpHeaders.Names.UPGRADE)));  
  30.     }  
  31.   
  32.     /** 
  33.      * 根據WebSocket請求,判斷不同的握手形式,並返回相應版本的握手結果 
  34.      *  
  35.      * @param req 
  36.      * @return 
  37.      */  
  38.     public HttpResponse buildWebSocketRes(HttpRequest req) {  
  39.         String reasonPhrase = "";  
  40.         boolean isThirdTypeHandshake = Boolean.FALSE;  
  41.         int websocketVersion = 0;  
  42.         if (req.getHeader(Sec_WebSocket_Version) != null) {  
  43.             websocketVersion = Integer.parseInt(req.getHeader(Sec_WebSocket_Version));  
  44.         }  
  45.         /** 
  46.          * 在草案13以及其以前,請求源使用http頭是Origin,是草案4到草案10,請求源使用http頭是Sec-WebSocket-Origin,而在草案11及以後使用的請求頭又是Origin了, 
  47.          * 不知道這些制定WEBSOCKET標準的傢伙在搞什麼東東,一個請求頭有必要變名字這樣變來變去的嗎。<br> 
  48.          * 注意,這裏還有一點需要注意的就是"websocketVersion >= 13"這個條件,並不一定適合以後所有的草案,不過這也只是一個預防,有可能會適應後面的草案, 如果不適合還只有升級對應的websocket協議。<br> 
  49.          */  
  50.         if (websocketVersion >= 13  
  51.             || (req.containsHeader(Names.SEC_WEBSOCKET_ORIGIN) && req.containsHeader(SEC_WEBSOCKET_KEY))) {  
  52.             isThirdTypeHandshake = Boolean.TRUE;  
  53.         }  
  54.   
  55.         // websocket協議草案7後面的格式,可以參看wikipedia上面的說明,比較前後版本的不同:http://en.wikipedia.org/wiki/WebSocket   
  56.         if (isThirdTypeHandshake = Boolean.FALSE) {  
  57.             reasonPhrase = "Switching Protocols";  
  58.         } else {  
  59.             reasonPhrase = "Web Socket Protocol Handshake";  
  60.         }  
  61.         HttpResponse res = new DefaultHttpResponse(HttpVersion.HTTP_1_1, new HttpResponseStatus(101, reasonPhrase));  
  62.         res.addHeader(HttpHeaders.Names.UPGRADE, HttpHeaders.Values.WEBSOCKET);  
  63.         res.addHeader(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.UPGRADE);  
  64.         // Fill in the headers and contents depending on handshake method.   
  65.         if (req.containsHeader(Names.SEC_WEBSOCKET_KEY1) && req.containsHeader(Names.SEC_WEBSOCKET_KEY2)) {  
  66.             // New handshake method with a challenge:   
  67.             res.addHeader(Names.SEC_WEBSOCKET_ORIGIN, req.getHeader(Names.ORIGIN));  
  68.             res.addHeader(Names.SEC_WEBSOCKET_LOCATION, getWebSocketLocation(req));  
  69.             String protocol = req.getHeader(Names.SEC_WEBSOCKET_PROTOCOL);  
  70.             if (protocol != null) {  
  71.                 res.addHeader(Names.SEC_WEBSOCKET_PROTOCOL, protocol);  
  72.             }  
  73.             // Calculate the answer of the challenge.   
  74.             String key1 = req.getHeader(Names.SEC_WEBSOCKET_KEY1);  
  75.             String key2 = req.getHeader(Names.SEC_WEBSOCKET_KEY2);  
  76.             int a = (int) (Long.parseLong(getNumeric(key1)) / getSpace(key1).length());  
  77.             int b = (int) (Long.parseLong(getNumeric(key2)) / getSpace(key2).length());  
  78.             long c = req.getContent().readLong();  
  79.             ChannelBuffer input = ChannelBuffers.buffer(16);  
  80.             input.writeInt(a);  
  81.             input.writeInt(b);  
  82.             input.writeLong(c);  
  83.             ChannelBuffer output = null;  
  84.             try {  
  85.                 output = ChannelBuffers.wrappedBuffer(MessageDigest.getInstance("MD5").digest(input.array()));  
  86.             } catch (NoSuchAlgorithmException e) {  
  87.             }  
  88.   
  89.             res.setContent(output);  
  90.         } else if (isThirdTypeHandshake = Boolean.FALSE) {  
  91.             String protocol = req.getHeader(Names.SEC_WEBSOCKET_PROTOCOL);  
  92.             if (protocol != null) {  
  93.                 res.addHeader(Names.SEC_WEBSOCKET_PROTOCOL, protocol);  
  94.             }  
  95.             res.addHeader(SEC_WEBSOCKET_ACCEPT, getSecWebSocketAccept(req));  
  96.         } else {  
  97.             // Old handshake method with no challenge:   
  98.             if (req.getHeader(Names.ORIGIN) != null) {  
  99.                 res.addHeader(Names.WEBSOCKET_ORIGIN, req.getHeader(Names.ORIGIN));  
  100.             }  
  101.             res.addHeader(Names.WEBSOCKET_LOCATION, getWebSocketLocation(req));  
  102.             String protocol = req.getHeader(Names.WEBSOCKET_PROTOCOL);  
  103.             if (protocol != null) {  
  104.                 res.addHeader(Names.WEBSOCKET_PROTOCOL, protocol);  
  105.             }  
  106.         }  
  107.   
  108.         return res;  
  109.     }  
  110.   
  111.     private String getWebSocketLocation(HttpRequest req) {  
  112.         return "ws://" + req.getHeader(HttpHeaders.Names.HOST) + req.getUri();  
  113.     }  
  114.   
  115.     private String getSecWebSocketAccept(HttpRequest req) {  
  116.         // CHROME WEBSOCKET VERSION 8中定義的GUID,詳細文檔地址:http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-10   
  117.         String guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";  
  118.         String key = "";  
  119.         key = req.getHeader(SEC_WEBSOCKET_KEY);  
  120.         key += guid;  
  121.         try {  
  122.             MessageDigest md = MessageDigest.getInstance("SHA-1");  
  123.             md.update(key.getBytes("iso-8859-1"), 0, key.length());  
  124.             byte[] sha1Hash = md.digest();  
  125.             key = base64Encode(sha1Hash);  
  126.         } catch (NoSuchAlgorithmException e) {  
  127.         } catch (UnsupportedEncodingException e) {  
  128.         }  
  129.         return key;  
  130.     }  
  131.   
  132.     String base64Encode(byte[] input) {  
  133.         sun.misc.BASE64Encoder encoder = new sun.misc.BASE64Encoder();  
  134.         String base64 = encoder.encode(input);  
  135.         return base64;  
  136.     }  
  137.   
  138.     // 去掉傳入字符串的所有非數字   
  139.     private String getNumeric(String str) {  
  140.         return str.replaceAll("\\D""");  
  141.     }  
  142.   
  143.     // 返回傳入字符串的空格   
  144.     private String getSpace(String str) {  
  145.         return str.replaceAll("\\S""");  
  146.     }  
  147. }  


三、注意事項

不同版本的WebSocket標準,編碼和解碼的方式還有所不同,在第一種和第二種WebSocket協議標準中,使用Netty自帶的Encoder和Decoder即可:

org.jboss.netty.handler.codec.http.websocket.WebSocketFrameEncoder
org.jboss.netty.handler.codec.http.websocket.WebSocketFrameDecoder

而如果要支持第三種實現標準,Netty目前官方還不支持,可以到github中找到實現的Encoder及Decoder:

https://github.com/joewalnes/webbit/tree/0356ba12f5c21f8a297a5afb433215bb2f738008/src/main/java/org/webbitserver/netty

不過,它的實現有一點問題,就是沒有處理客戶端主動發起的WebSocket請求斷開,既客戶端主動發起opcode爲8的請求,不過它還是有預留的,找到這個類:

Hybi10WebSocketFrameDecoder

的包含這以下內容的行:

} else if (this.opcode == OPCODE_CLOSE) {

在其中插入:

return new DefaultWebSocketFrame(0x08, frame);

然後在你的實現子類中增加如下的代碼判斷即可:

  1. if (frame.getType() == 0x08) {  
  2.         //處理關閉事件的XXX方法   
  3.         return;  
  4. }  
發佈了37 篇原創文章 · 獲贊 3 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章