WebSocket不同版本的三种握手方式以及一个Netty实现JAVA类

本文深入探讨WebSocket的不同握手方式,包括基于Flash、带安全密钥的两种方式及带一个安全密钥的实现,并提供了基于Netty的Java代码实现。文章详细解释了握手流程及Netty中对三种握手方式的处理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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

WebSocket是HTML5中的新特性,应用也是非常的广泛,特别是用户WEB端与后台服务器的消息通讯,如阿里的WEBWW就是使用的WebSocket与后端服务器建立长连接进行的通讯。目前WebSocket还处于发展当中,就目前的发展过程而言,WebSocket现在不同的版本,有三种不同的握手方式:

1、基于Flash的WebSocket通讯,使用场景是IE的多数版本,因为IE的多数版本不都不支持WebSocket协议,以及FF、CHROME等浏览器的低版本,还没有原生的支持WebSocket,可以使用FLASH的WebSocket实现进行通讯:

浏览器请求:

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

服务器回应:

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

原理:

如果客户端没有发送Origin请求头,则客户端不需要返回,如果客户端没有发送WebSocket-Protocol请求头,服务端也不需要返回;服务端唯一需要组装返回给客户端做为校验的就是WebSocket-Location请求头,拼装一个websocket请求的地址就可以了。

这种方式,是最老的一种方式,连一个安全Key都没有,服务端也没有对客户的请求做加密性校验。

2、第二种握手方式是带两个安全key请求头的,结果以md5加密,并放在body中返回的方式,参看如下示例:

浏览器请求:

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

服务器回应:

  1. HTTP/1.1101WebSocketProtocolHandshake
  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-
HTTP/1.1 101 WebSocket Protocol Handshake Upgrade: WebSocket Connection: Upgrade Sec-WebSocket-Origin: http://example.com Sec-WebSocket-Location: ws://example.com/demo Sec-WebSocket-Protocol: sample 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/lsHTTP/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
GET /ls HTTP/1.1 Upgrade: websocket Connection: Upgrade Host: www.xx.com Sec-WebSocket-Origin: http://www.xx.com Sec-WebSocket-Key: 2SCVXUeP9cTjV+0mWB8J6A== Sec-WebSocket-Version: 8
服务器回应:

  1. HTTP/1.1101SwitchingProtocols
  2. Upgrade:websocket
  3. Connection:Upgrade
  4. Sec-WebSocket-Accept:mLDKNeBNWz6T9SxU+o0Fy/HgeSw=
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade 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.youkuaiyun.com/fenglibing/article/details/6852497


二、基于Netty实现JAVA类

为了支持以上提到的三种不同版本的websocket握手实现,服务端就需要针对这三种情况进行相应的处理,以下是一段基于netty实现的java代码,一个完整的WebSocketHelper实现:

  1. importjava.io.UnsupportedEncodingException;
  2. importjava.security.MessageDigest;
  3. importjava.security.NoSuchAlgorithmException;
  4. importorg.jboss.netty.buffer.ChannelBuffer;
  5. importorg.jboss.netty.buffer.ChannelBuffers;
  6. importorg.jboss.netty.handler.codec.http.DefaultHttpResponse;
  7. importorg.jboss.netty.handler.codec.http.HttpHeaders;
  8. importorg.jboss.netty.handler.codec.http.HttpHeaders.Names;
  9. importorg.jboss.netty.handler.codec.http.HttpRequest;
  10. importorg.jboss.netty.handler.codec.http.HttpResponse;
  11. importorg.jboss.netty.handler.codec.http.HttpResponseStatus;
  12. importorg.jboss.netty.handler.codec.http.HttpVersion;
  13. publicclassWebSocketHelper{
  14. privatefinalstaticStringSEC_WEBSOCKET_KEY="Sec-WebSocket-Key";
  15. privatefinalstaticStringSEC_WEBSOCKET_ACCEPT="Sec-WebSocket-Accept";
  16. /*websocket版本号:草案8到草案12版本号都是8,草案13及以后的版本号都和草案号相同*/
  17. privatefinalstaticStringSec_WebSocket_Version="Sec-WebSocket-Version";
  18. /**
  19. *判断是否是WebSocket请求
  20. *
  21. *@paramreq
  22. *@return
  23. */
  24. publicbooleansupportWebSocket(HttpRequestreq){
  25. return(HttpHeaders.Values.UPGRADE.equalsIgnoreCase(req.getHeader(HttpHeaders.Names.CONNECTION))&&HttpHeaders.Values.WEBSOCKET.equalsIgnoreCase(req.getHeader(HttpHeaders.Names.UPGRADE)));
  26. }
  27. /**
  28. *根据WebSocket请求,判断不同的握手形式,并返回相应版本的握手结果
  29. *
  30. *@paramreq
  31. *@return
  32. */
  33. publicHttpResponsebuildWebSocketRes(HttpRequestreq){
  34. StringreasonPhrase="";
  35. booleanisThirdTypeHandshake=Boolean.FALSE;
  36. intwebsocketVersion=0;
  37. if(req.getHeader(Sec_WebSocket_Version)!=null){
  38. websocketVersion=Integer.parseInt(req.getHeader(Sec_WebSocket_Version));
  39. }
  40. /**
  41. *在草案13以及其以前,请求源使用http头是Origin,是草案4到草案10,请求源使用http头是Sec-WebSocket-Origin,而在草案11及以后使用的请求头又是Origin了,
  42. *不知道这些制定WEBSOCKET标准的家伙在搞什么东东,一个请求头有必要变名字这样变来变去的吗。<br>
  43. *注意,这里还有一点需要注意的就是"websocketVersion>=13"这个条件,并不一定适合以后所有的草案,不过这也只是一个预防,有可能会适应后面的草案,如果不适合还只有升级对应的websocket协议。<br>
  44. */
  45. if(websocketVersion>=13
  46. ||(req.containsHeader(Names.SEC_WEBSOCKET_ORIGIN)&&req.containsHeader(SEC_WEBSOCKET_KEY))){
  47. isThirdTypeHandshake=Boolean.TRUE;
  48. }
  49. //websocket协议草案7后面的格式,可以参看wikipedia上面的说明,比较前后版本的不同:http://en.wikipedia.org/wiki/WebSocket
  50. if(isThirdTypeHandshake=Boolean.FALSE){
  51. reasonPhrase="SwitchingProtocols";
  52. }else{
  53. reasonPhrase="WebSocketProtocolHandshake";
  54. }
  55. HttpResponseres=newDefaultHttpResponse(HttpVersion.HTTP_1_1,newHttpResponseStatus(101,reasonPhrase));
  56. res.addHeader(HttpHeaders.Names.UPGRADE,HttpHeaders.Values.WEBSOCKET);
  57. res.addHeader(HttpHeaders.Names.CONNECTION,HttpHeaders.Values.UPGRADE);
  58. //Fillintheheadersandcontentsdependingonhandshakemethod.
  59. if(req.containsHeader(Names.SEC_WEBSOCKET_KEY1)&&req.containsHeader(Names.SEC_WEBSOCKET_KEY2)){
  60. //Newhandshakemethodwithachallenge:
  61. res.addHeader(Names.SEC_WEBSOCKET_ORIGIN,req.getHeader(Names.ORIGIN));
  62. res.addHeader(Names.SEC_WEBSOCKET_LOCATION,getWebSocketLocation(req));
  63. Stringprotocol=req.getHeader(Names.SEC_WEBSOCKET_PROTOCOL);
  64. if(protocol!=null){
  65. res.addHeader(Names.SEC_WEBSOCKET_PROTOCOL,protocol);
  66. }
  67. //Calculatetheanswerofthechallenge.
  68. Stringkey1=req.getHeader(Names.SEC_WEBSOCKET_KEY1);
  69. Stringkey2=req.getHeader(Names.SEC_WEBSOCKET_KEY2);
  70. inta=(int)(Long.parseLong(getNumeric(key1))/getSpace(key1).length());
  71. intb=(int)(Long.parseLong(getNumeric(key2))/getSpace(key2).length());
  72. longc=req.getContent().readLong();
  73. ChannelBufferinput=ChannelBuffers.buffer(16);
  74. input.writeInt(a);
  75. input.writeInt(b);
  76. input.writeLong(c);
  77. ChannelBufferoutput=null;
  78. try{
  79. output=ChannelBuffers.wrappedBuffer(MessageDigest.getInstance("MD5").digest(input.array()));
  80. }catch(NoSuchAlgorithmExceptione){
  81. }
  82. res.setContent(output);
  83. }elseif(isThirdTypeHandshake=Boolean.FALSE){
  84. Stringprotocol=req.getHeader(Names.SEC_WEBSOCKET_PROTOCOL);
  85. if(protocol!=null){
  86. res.addHeader(Names.SEC_WEBSOCKET_PROTOCOL,protocol);
  87. }
  88. res.addHeader(SEC_WEBSOCKET_ACCEPT,getSecWebSocketAccept(req));
  89. }else{
  90. //Oldhandshakemethodwithnochallenge:
  91. if(req.getHeader(Names.ORIGIN)!=null){
  92. res.addHeader(Names.WEBSOCKET_ORIGIN,req.getHeader(Names.ORIGIN));
  93. }
  94. res.addHeader(Names.WEBSOCKET_LOCATION,getWebSocketLocation(req));
  95. Stringprotocol=req.getHeader(Names.WEBSOCKET_PROTOCOL);
  96. if(protocol!=null){
  97. res.addHeader(Names.WEBSOCKET_PROTOCOL,protocol);
  98. }
  99. }
  100. returnres;
  101. }
  102. privateStringgetWebSocketLocation(HttpRequestreq){
  103. return"ws://"+req.getHeader(HttpHeaders.Names.HOST)+req.getUri();
  104. }
  105. privateStringgetSecWebSocketAccept(HttpRequestreq){
  106. //CHROMEWEBSOCKETVERSION8中定义的GUID,详细文档地址:http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-10
  107. Stringguid="258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
  108. Stringkey="";
  109. key=req.getHeader(SEC_WEBSOCKET_KEY);
  110. key+=guid;
  111. try{
  112. MessageDigestmd=MessageDigest.getInstance("SHA-1");
  113. md.update(key.getBytes("iso-8859-1"),0,key.length());
  114. byte[]sha1Hash=md.digest();
  115. key=base64Encode(sha1Hash);
  116. }catch(NoSuchAlgorithmExceptione){
  117. }catch(UnsupportedEncodingExceptione){
  118. }
  119. returnkey;
  120. }
  121. Stringbase64Encode(byte[]input){
  122. sun.misc.BASE64Encoderencoder=newsun.misc.BASE64Encoder();
  123. Stringbase64=encoder.encode(input);
  124. returnbase64;
  125. }
  126. //去掉传入字符串的所有非数字
  127. privateStringgetNumeric(Stringstr){
  128. returnstr.replaceAll("\\D","");
  129. }
  130. //返回传入字符串的空格
  131. privateStringgetSpace(Stringstr){
  132. returnstr.replaceAll("\\S","");
  133. }
  134. }
import java.io.UnsupportedEncodingException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import org.jboss.netty.buffer.ChannelBuffer; import org.jboss.netty.buffer.ChannelBuffers; import org.jboss.netty.handler.codec.http.DefaultHttpResponse; import org.jboss.netty.handler.codec.http.HttpHeaders; import org.jboss.netty.handler.codec.http.HttpHeaders.Names; import org.jboss.netty.handler.codec.http.HttpRequest; import org.jboss.netty.handler.codec.http.HttpResponse; import org.jboss.netty.handler.codec.http.HttpResponseStatus; import org.jboss.netty.handler.codec.http.HttpVersion; public class WebSocketHelper { private final static String SEC_WEBSOCKET_KEY = "Sec-WebSocket-Key"; private final static String SEC_WEBSOCKET_ACCEPT = "Sec-WebSocket-Accept"; /* websocket版本号:草案8到草案12版本号都是8,草案13及以后的版本号都和草案号相同 */ private final static String Sec_WebSocket_Version = "Sec-WebSocket-Version"; /** * 判断是否是WebSocket请求 * * @param req * @return */ public boolean supportWebSocket(HttpRequest req) { return (HttpHeaders.Values.UPGRADE.equalsIgnoreCase(req.getHeader(HttpHeaders.Names.CONNECTION)) && HttpHeaders.Values.WEBSOCKET.equalsIgnoreCase(req.getHeader(HttpHeaders.Names.UPGRADE))); } /** * 根据WebSocket请求,判断不同的握手形式,并返回相应版本的握手结果 * * @param req * @return */ public HttpResponse buildWebSocketRes(HttpRequest req) { String reasonPhrase = ""; boolean isThirdTypeHandshake = Boolean.FALSE; int websocketVersion = 0; if (req.getHeader(Sec_WebSocket_Version) != null) { websocketVersion = Integer.parseInt(req.getHeader(Sec_WebSocket_Version)); } /** * 在草案13以及其以前,请求源使用http头是Origin,是草案4到草案10,请求源使用http头是Sec-WebSocket-Origin,而在草案11及以后使用的请求头又是Origin了, * 不知道这些制定WEBSOCKET标准的家伙在搞什么东东,一个请求头有必要变名字这样变来变去的吗。<br> * 注意,这里还有一点需要注意的就是"websocketVersion >= 13"这个条件,并不一定适合以后所有的草案,不过这也只是一个预防,有可能会适应后面的草案, 如果不适合还只有升级对应的websocket协议。<br> */ if (websocketVersion >= 13 || (req.containsHeader(Names.SEC_WEBSOCKET_ORIGIN) && req.containsHeader(SEC_WEBSOCKET_KEY))) { isThirdTypeHandshake = Boolean.TRUE; } // websocket协议草案7后面的格式,可以参看wikipedia上面的说明,比较前后版本的不同:http://en.wikipedia.org/wiki/WebSocket if (isThirdTypeHandshake = Boolean.FALSE) { reasonPhrase = "Switching Protocols"; } else { reasonPhrase = "Web Socket Protocol Handshake"; } HttpResponse res = new DefaultHttpResponse(HttpVersion.HTTP_1_1, new HttpResponseStatus(101, reasonPhrase)); res.addHeader(HttpHeaders.Names.UPGRADE, HttpHeaders.Values.WEBSOCKET); res.addHeader(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.UPGRADE); // Fill in the headers and contents depending on handshake method. if (req.containsHeader(Names.SEC_WEBSOCKET_KEY1) && req.containsHeader(Names.SEC_WEBSOCKET_KEY2)) { // New handshake method with a challenge: res.addHeader(Names.SEC_WEBSOCKET_ORIGIN, req.getHeader(Names.ORIGIN)); res.addHeader(Names.SEC_WEBSOCKET_LOCATION, getWebSocketLocation(req)); String protocol = req.getHeader(Names.SEC_WEBSOCKET_PROTOCOL); if (protocol != null) { res.addHeader(Names.SEC_WEBSOCKET_PROTOCOL, protocol); } // Calculate the answer of the challenge. String key1 = req.getHeader(Names.SEC_WEBSOCKET_KEY1); String key2 = req.getHeader(Names.SEC_WEBSOCKET_KEY2); int a = (int) (Long.parseLong(getNumeric(key1)) / getSpace(key1).length()); int b = (int) (Long.parseLong(getNumeric(key2)) / getSpace(key2).length()); long c = req.getContent().readLong(); ChannelBuffer input = ChannelBuffers.buffer(16); input.writeInt(a); input.writeInt(b); input.writeLong(c); ChannelBuffer output = null; try { output = ChannelBuffers.wrappedBuffer(MessageDigest.getInstance("MD5").digest(input.array())); } catch (NoSuchAlgorithmException e) { } res.setContent(output); } else if (isThirdTypeHandshake = Boolean.FALSE) { 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)); } else { // Old handshake method with no challenge: if (req.getHeader(Names.ORIGIN) != null) { res.addHeader(Names.WEBSOCKET_ORIGIN, req.getHeader(Names.ORIGIN)); } res.addHeader(Names.WEBSOCKET_LOCATION, getWebSocketLocation(req)); String protocol = req.getHeader(Names.WEBSOCKET_PROTOCOL); if (protocol != null) { res.addHeader(Names.WEBSOCKET_PROTOCOL, protocol); } } return res; } private String getWebSocketLocation(HttpRequest req) { return "ws://" + req.getHeader(HttpHeaders.Names.HOST) + req.getUri(); } 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) { } catch (UnsupportedEncodingException e) { } return key; } String base64Encode(byte[] input) { sun.misc.BASE64Encoder encoder = new sun.misc.BASE64Encoder(); String base64 = encoder.encode(input); return base64; } // 去掉传入字符串的所有非数字 private String getNumeric(String str) { return str.replaceAll("\\D", ""); } // 返回传入字符串的空格 private String getSpace(String str) { return str.replaceAll("\\S", ""); } }


三、注意事项

不同版本的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. }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值