zoukankan      html  css  js  c++  java
  • WebSocket不同版本的三种握手方式以及一个Netty实现JAVA类

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

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

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

    浏览器请求:

    GET /ls HTTP/1.1
    Upgrade: WebSocket
    Connection: Upgrade
    Host: www.xx.com
    Origin: http://www.xx.com

    服务器回应:

    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中返回的方式,参看如下示例:

    浏览器请求:

    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

    服务器回应:

    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请求头中返回的方式:

    浏览器请求:

    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

    服务器回应:
    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.csdn.net/fenglibing/article/details/6852497

    二、基于Netty实现JAVA类

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

    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);

    然后在你的实现子类中增加如下的代码判断即可:

    if (frame.getType() == 0x08) {
            //处理关闭事件的XXX方法
            return;
    }

    本文出自:冯立彬的博客


    再分享一下我老师大神的人工智能教程吧。零基础!通俗易懂!风趣幽默!还带黄段子!希望你也加入到我们人工智能的队伍中来!https://blog.csdn.net/jiangjunshow

  • 相关阅读:
    1203正规式转换为有穷自动机
    访问本班同学的博客
    0312复利计算3
    操作系统
    0311复利计算2
    0309简单的复利计算
    0302IT行业虽吃香,能完全享受这块“香"的也很难
    12.30递归下降语义分析
    1203正规式转换为有穷自动机
    对10位同学的文法解释和语法树的评论
  • 原文地址:https://www.cnblogs.com/skiwdhwhssh/p/10340702.html
Copyright © 2011-2022 走看看