一、WebSocket不同版本的三种握手方式
WebSocket是HTML5中的新特性,应用也是非常的广泛,特别是用户WEB端与后台服务器的消息通讯,如阿里的WEBWW就是使用的WebSocket与后端服务器建立长连接进行的通讯。目前WebSocket还处于发展当中,就目前的发展过程而言,WebSocket现在不同的版本,有三种不同的握手方式:
1、基于Flash的WebSocket通讯,使用场景是IE的多数版本,因为IE的多数版本不都不支持WebSocket协议,以及FF、CHROME等浏览器的低版本,还没有原生的支持WebSocket,可以使用FLASH的WebSocket实现进行通讯:
浏览器请求:
- GET/lsHTTP/1.1
- Upgrade:WebSocket
- Connection:Upgrade
- Host:www.xx.com
- Origin:http://www.xx.com
服务器回应:
- HTTP/1.1101WebSocketProtocolHandshake
- 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中返回的方式,参看如下示例:
浏览器请求:
- GET/demoHTTP/1.1
- Host:example.com
- Connection:Upgrade
- Sec-WebSocket-Key2:129985Y31.P00
- Sec-WebSocket-Protocol:sample
- Upgrade:WebSocket
- Sec-WebSocket-Key1:4@146546xW%0l15
- Origin:http://example.com
- ^n:ds[4U
服务器回应:
- HTTP/1.1101WebSocketProtocolHandshake
- 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请求头中返回的方式:
浏览器请求:
- GET/lsHTTP/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
服务器回应:
- HTTP/1.1101SwitchingProtocols
- 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实现:
- importjava.io.UnsupportedEncodingException;
- importjava.security.MessageDigest;
- importjava.security.NoSuchAlgorithmException;
- importorg.jboss.netty.buffer.ChannelBuffer;
- importorg.jboss.netty.buffer.ChannelBuffers;
- importorg.jboss.netty.handler.codec.http.DefaultHttpResponse;
- importorg.jboss.netty.handler.codec.http.HttpHeaders;
- importorg.jboss.netty.handler.codec.http.HttpHeaders.Names;
- importorg.jboss.netty.handler.codec.http.HttpRequest;
- importorg.jboss.netty.handler.codec.http.HttpResponse;
- importorg.jboss.netty.handler.codec.http.HttpResponseStatus;
- importorg.jboss.netty.handler.codec.http.HttpVersion;
- publicclassWebSocketHelper{
- privatefinalstaticStringSEC_WEBSOCKET_KEY="Sec-WebSocket-Key";
- privatefinalstaticStringSEC_WEBSOCKET_ACCEPT="Sec-WebSocket-Accept";
- /*websocket版本号:草案8到草案12版本号都是8,草案13及以后的版本号都和草案号相同*/
- privatefinalstaticStringSec_WebSocket_Version="Sec-WebSocket-Version";
- /**
- *判断是否是WebSocket请求
- *
- *@paramreq
- *@return
- */
- publicbooleansupportWebSocket(HttpRequestreq){
- return(HttpHeaders.Values.UPGRADE.equalsIgnoreCase(req.getHeader(HttpHeaders.Names.CONNECTION))&&HttpHeaders.Values.WEBSOCKET.equalsIgnoreCase(req.getHeader(HttpHeaders.Names.UPGRADE)));
- }
- /**
- *根据WebSocket请求,判断不同的握手形式,并返回相应版本的握手结果
- *
- *@paramreq
- *@return
- */
- publicHttpResponsebuildWebSocketRes(HttpRequestreq){
- StringreasonPhrase="";
- booleanisThirdTypeHandshake=Boolean.FALSE;
- intwebsocketVersion=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="SwitchingProtocols";
- }else{
- reasonPhrase="WebSocketProtocolHandshake";
- }
- HttpResponseres=newDefaultHttpResponse(HttpVersion.HTTP_1_1,newHttpResponseStatus(101,reasonPhrase));
- res.addHeader(HttpHeaders.Names.UPGRADE,HttpHeaders.Values.WEBSOCKET);
- res.addHeader(HttpHeaders.Names.CONNECTION,HttpHeaders.Values.UPGRADE);
- //Fillintheheadersandcontentsdependingonhandshakemethod.
- if(req.containsHeader(Names.SEC_WEBSOCKET_KEY1)&&req.containsHeader(Names.SEC_WEBSOCKET_KEY2)){
- //Newhandshakemethodwithachallenge:
- res.addHeader(Names.SEC_WEBSOCKET_ORIGIN,req.getHeader(Names.ORIGIN));
- res.addHeader(Names.SEC_WEBSOCKET_LOCATION,getWebSocketLocation(req));
- Stringprotocol=req.getHeader(Names.SEC_WEBSOCKET_PROTOCOL);
- if(protocol!=null){
- res.addHeader(Names.SEC_WEBSOCKET_PROTOCOL,protocol);
- }
- //Calculatetheanswerofthechallenge.
- Stringkey1=req.getHeader(Names.SEC_WEBSOCKET_KEY1);
- Stringkey2=req.getHeader(Names.SEC_WEBSOCKET_KEY2);
- inta=(int)(Long.parseLong(getNumeric(key1))/getSpace(key1).length());
- intb=(int)(Long.parseLong(getNumeric(key2))/getSpace(key2).length());
- longc=req.getContent().readLong();
- ChannelBufferinput=ChannelBuffers.buffer(16);
- input.writeInt(a);
- input.writeInt(b);
- input.writeLong(c);
- ChannelBufferoutput=null;
- try{
- output=ChannelBuffers.wrappedBuffer(MessageDigest.getInstance("MD5").digest(input.array()));
- }catch(NoSuchAlgorithmExceptione){
- }
- res.setContent(output);
- }elseif(isThirdTypeHandshake=Boolean.FALSE){
- Stringprotocol=req.getHeader(Names.SEC_WEBSOCKET_PROTOCOL);
- if(protocol!=null){
- res.addHeader(Names.SEC_WEBSOCKET_PROTOCOL,protocol);
- }
- res.addHeader(SEC_WEBSOCKET_ACCEPT,getSecWebSocketAccept(req));
- }else{
- //Oldhandshakemethodwithnochallenge:
- if(req.getHeader(Names.ORIGIN)!=null){
- res.addHeader(Names.WEBSOCKET_ORIGIN,req.getHeader(Names.ORIGIN));
- }
- res.addHeader(Names.WEBSOCKET_LOCATION,getWebSocketLocation(req));
- Stringprotocol=req.getHeader(Names.WEBSOCKET_PROTOCOL);
- if(protocol!=null){
- res.addHeader(Names.WEBSOCKET_PROTOCOL,protocol);
- }
- }
- returnres;
- }
- privateStringgetWebSocketLocation(HttpRequestreq){
- return"ws://"+req.getHeader(HttpHeaders.Names.HOST)+req.getUri();
- }
- privateStringgetSecWebSocketAccept(HttpRequestreq){
- //CHROMEWEBSOCKETVERSION8中定义的GUID,详细文档地址:http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-10
- Stringguid="258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
- Stringkey="";
- key=req.getHeader(SEC_WEBSOCKET_KEY);
- key+=guid;
- try{
- MessageDigestmd=MessageDigest.getInstance("SHA-1");
- md.update(key.getBytes("iso-8859-1"),0,key.length());
- byte[]sha1Hash=md.digest();
- key=base64Encode(sha1Hash);
- }catch(NoSuchAlgorithmExceptione){
- }catch(UnsupportedEncodingExceptione){
- }
- returnkey;
- }
- Stringbase64Encode(byte[]input){
- sun.misc.BASE64Encoderencoder=newsun.misc.BASE64Encoder();
- Stringbase64=encoder.encode(input);
- returnbase64;
- }
- //去掉传入字符串的所有非数字
- privateStringgetNumeric(Stringstr){
- returnstr.replaceAll("\\D","");
- }
- //返回传入字符串的空格
- privateStringgetSpace(Stringstr){
- returnstr.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:
不过,它的实现有一点问题,就是没有处理客户端主动发起的WebSocket请求断开,既客户端主动发起opcode为8的请求,不过它还是有预留的,找到这个类:
Hybi10WebSocketFrameDecoder
的包含这以下内容的行:
} else if (this.opcode == OPCODE_CLOSE) {
在其中插入:
return new DefaultWebSocketFrame(0x08, frame);
然后在你的实现子类中增加如下的代码判断即可:
- if(frame.getType()==0x08){
- //处理关闭事件的XXX方法
- return;
- }