zoukankan      html  css  js  c++  java
  • netty(4)高级篇-Websocket协议开发

    一、HTTP协议的弊端

    将HTTP协议的主要弊端总结如下:

    • (1) 半双工协议:可以在客户端和服务端2个方向上传输,但是不能同时传输。同一时刻,只能在一个方向上传输。
    • (2) HTTP消息冗长:相比于其他二进制协议,有点繁琐。
    • (3) 针对服务器推送的黑客攻击,例如长时间轮询。

    现在很多网站的消息推送都是使用轮询,即客户端每隔1S或者其他时间给服务器发送请求,然后服务器返回最新的数据给客户端。HTTP协议中的Header非常冗长,因此会占用很多的带宽和服务器资源。

    比较新的技术是Comet,使用了AJAX。虽然可以双向通信,但是依然需要发送请求,而且在Comet中,普遍采用了长连接,也会大量消耗服务器的带宽和资源。

    为了解决这个问题,HTML5定义的WebSocket协议。

    二、WebSocket协议介绍

    在WebSocket API中,浏览器和服务器只需要一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道,两者就可以直接互相传送数据了。

    WebSocket基于TCP双向全双工协议,即在同一时刻,即可以发送消息,也可以接收消息,相比于HTTP协议,是一个性能上的提升。

    特点:

    •   单一的TCP连接,全双工;
    •   对代理、防火墙和路由器透明;
    •   无头部信息、Cookie和身份验证;
    •   无安全开销;
    •   通过"ping/pong"帧保持链路激活;
    •   服务器可以主动传递消息给客户端,不再需要客户端轮询;

    拥有以上特点的WebSocket就是为了取代轮询和Comet技术,使得客户端浏览器具备像C/S架构下桌面系统一样的实时能力。

    浏览器通过js建立一个WebSocket的请求,连接建立后,客户端和服务器端可以通过TCP直接交换数据。

    因为WebSocket本质上是一个TCP连接,稳定,所以在Comet和轮询比拥有性能优势,如图所示:

    三、WebSocket连接

    3.1 连接建立

    client端发送握手请求,请求消息如图所示:

    • 这个请求和普通的HTTP请求不同,包含了一些附加头信息,其中附加头信息"Upgrade: Websocket"表明这是一个申请协议升级的HTTP请求。
    • 服务器尝试解析这个信息,然后返回应答信息给客户端,因此客户端和服务器端的WebSocket连接就建立起来了,双方可以通过这个连接通道自由的传递信息。
    • 这个连接会持续到某一方主动断开连接。

    服务端的应答请求如图所示:

     client消息中的"Sec-WebSocket-Key"是随机的,服务器端会用这些数据来构造一个"SHA-1"的信息摘要,把"Sec-WebSocket-Key"加上一个魔幻字符串。使用"SHA-1"加密,然后进行BASE64编码,将结果作为"Sec-Webscoket-Accept"头的值。

    3.2 生命周期

    • 握手成功,连接建立后,以"Messages"的方式通信。
    • 一个消息由一个或者多个""组成。
    • 帧都有自己的类型,同一消息的多个帧类型相同。
    • 广义上,类型可以是文本、二进制、控制帧如信号。

    3.3 连接关闭

    • 安全方法是关闭底层TCP连接以及TLS会话。
    • 底层的TCP连接,正常情况下,应该由服务器先关闭。
    • 异常时(比如合理的时间内没有接收到服务器的TCP Close),可以由客户端发起TCP Close。因此,在client发起TCP Close时,服务器应该立即发起一个TCP Close操作;客户端则等待服务器的TCP Close;
    • 关闭消息带有一个状态码和可选的关闭原因,它必须按照协议要求发送一个Close控制帧。

    四、协议开发

    官方demo: http://netty.io/4.1/xref/io/netty/example/http/websocketx/server/package-summary.html

    功能介绍:

    服务器端开发:

    import io.netty.bootstrap.ServerBootstrap;
    import io.netty.channel.Channel;
    import io.netty.channel.ChannelInitializer;
    import io.netty.channel.ChannelPipeline;
    import io.netty.channel.EventLoopGroup;
    import io.netty.channel.nio.NioEventLoopGroup;
    import io.netty.channel.socket.SocketChannel;
    import io.netty.channel.socket.nio.NioServerSocketChannel;
    import io.netty.handler.codec.http.HttpObjectAggregator;
    import io.netty.handler.codec.http.HttpServerCodec;
    import io.netty.handler.stream.ChunkedWriteHandler;
    
    /**
     * @author lilinfeng
     * @version 1.0
     * @date 2014年2月14日
     */
    public class WebSocketServer {
        public void run(int port) throws Exception {
            EventLoopGroup bossGroup = new NioEventLoopGroup();
            EventLoopGroup workerGroup = new NioEventLoopGroup();
            try {
                ServerBootstrap b = new ServerBootstrap();
                b.group(bossGroup, workerGroup)
                        .channel(NioServerSocketChannel.class)
                        .childHandler(new ChannelInitializer<SocketChannel>() {
    
                            @Override
                            protected void initChannel(SocketChannel ch)
                                    throws Exception {
                                ChannelPipeline pipeline = ch.pipeline();
                                pipeline.addLast("http-codec",
                                        new HttpServerCodec());
                                pipeline.addLast("aggregator",
                                        new HttpObjectAggregator(65536));
                                ch.pipeline().addLast("http-chunked",
                                        new ChunkedWriteHandler());
                                pipeline.addLast("handler",
                                        new WebSocketServerHandler());
                            }
                        });
    
                Channel ch = b.bind(port).sync().channel();
                System.out.println("Web socket server started at port " + port
                        + '.');
                System.out
                        .println("Open your browser and navigate to http://localhost:"
                                + port + '/');
    
                ch.closeFuture().sync();
            } finally {
                bossGroup.shutdownGracefully();
                workerGroup.shutdownGracefully();
            }
        }
    
        public static void main(String[] args) throws Exception {
            int port = 8080;
            if (args.length > 0) {
                try {
                    port = Integer.parseInt(args[0]);
                } catch (NumberFormatException e) {
                    e.printStackTrace();
                }
            }
            new WebSocketServer().run(port);
        }
    }

    HttpServerCodec:将请求和应答消息解码为HTTP消息

    HttpObjectAggregator:将HTTP消息的多个部分合成一条完整的HTTP消息

    ChunkedWriteHandler:向客户端发送HTML5文件

    看上去和HTTP协议的非常类似,下面从Handler中来寻找答案:

      1 import io.netty.buffer.ByteBuf;
      2 import io.netty.buffer.Unpooled;
      3 import io.netty.channel.ChannelFuture;
      4 import io.netty.channel.ChannelFutureListener;
      5 import io.netty.channel.ChannelHandlerContext;
      6 import io.netty.channel.SimpleChannelInboundHandler;
      7 import io.netty.handler.codec.http.DefaultFullHttpResponse;
      8 import io.netty.handler.codec.http.FullHttpRequest;
      9 import io.netty.handler.codec.http.FullHttpResponse;
     10 import io.netty.handler.codec.http.HttpUtil;
     11 import io.netty.handler.codec.http.websocketx.*;
     12 import io.netty.util.CharsetUtil;
     13 
     14 import java.util.logging.Level;
     15 import java.util.logging.Logger;
     16 
     17 import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
     18 import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
     19 
     20 /**
     21  * @author lilinfeng
     22  * @version 1.0
     23  * @date 2014年2月14日
     24  */
     25 public class WebSocketServerHandler extends SimpleChannelInboundHandler<Object> {
     26     private static final Logger logger = Logger
     27             .getLogger(WebSocketServerHandler.class.getName());
     28 
     29     private WebSocketServerHandshaker handshaker;
     30 
     31 
     32     @Override
     33     public void channelRead0(ChannelHandlerContext ctx, Object msg)
     34             throws Exception {
     35         // 传统的HTTP接入
     36         if (msg instanceof FullHttpRequest) {
     37             handleHttpRequest(ctx, (FullHttpRequest) msg);
     38         }
     39         // WebSocket接入
     40         else if (msg instanceof WebSocketFrame) {
     41             handleWebSocketFrame(ctx, (WebSocketFrame) msg);
     42         }
     43     }
     44 
     45     @Override
     46     public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
     47         ctx.flush();
     48     }
     49 
     50     private void handleHttpRequest(ChannelHandlerContext ctx,
     51                                    FullHttpRequest req) throws Exception {
     52 
     53         // 如果HTTP解码失败,返回HHTP异常
     54         if (!req.decoderResult().isSuccess()
     55                 || (!"websocket".equals(req.headers().get("Upgrade")))) {
     56             sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1,
     57                     BAD_REQUEST));
     58             return;
     59         }
     60 
     61         // 构造握手响应返回,本机测试
     62         WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(
     63                 "ws://localhost:8080/websocket", null, false);
     64         handshaker = wsFactory.newHandshaker(req);
     65         if (handshaker == null) {
     66             WebSocketServerHandshakerFactory
     67                     .sendUnsupportedVersionResponse(ctx.channel());
     68         } else {
     69             handshaker.handshake(ctx.channel(), req);
     70         }
     71     }
     72 
     73     private void handleWebSocketFrame(ChannelHandlerContext ctx,
     74                                       WebSocketFrame frame) {
     75 
     76         // 判断是否是关闭链路的指令
     77         if (frame instanceof CloseWebSocketFrame) {
     78             handshaker.close(ctx.channel(),
     79                     (CloseWebSocketFrame) frame.retain());
     80             return;
     81         }
     82         // 判断是否是Ping消息
     83         if (frame instanceof PingWebSocketFrame) {
     84             ctx.channel().write(
     85                     new PongWebSocketFrame(frame.content().retain()));
     86             return;
     87         }
     88         // 本例程仅支持文本消息,不支持二进制消息
     89         if (!(frame instanceof TextWebSocketFrame)) {
     90             throw new UnsupportedOperationException(String.format(
     91                     "%s frame types not supported", frame.getClass().getName()));
     92         }
     93 
     94         // 返回应答消息
     95         String request = ((TextWebSocketFrame) frame).text();
     96         if (logger.isLoggable(Level.FINE)) {
     97             logger.fine(String.format("%s received %s", ctx.channel(), request));
     98         }
     99         ctx.channel().write(
    100                 new TextWebSocketFrame(request
    101                         + " , 欢迎使用Netty WebSocket服务,现在时刻:"
    102                         + new java.util.Date().toString()));
    103     }
    104 
    105     private static void sendHttpResponse(ChannelHandlerContext ctx,
    106                                          FullHttpRequest req, FullHttpResponse res) {
    107         // 返回应答给客户端
    108         if (res.getStatus().code() != 200) {
    109             ByteBuf buf = Unpooled.copiedBuffer(res.getStatus().toString(),
    110                     CharsetUtil.UTF_8);
    111             res.content().writeBytes(buf);
    112             buf.release();
    113             HttpUtil.setContentLength(res, res.content().readableBytes());
    114         }
    115 
    116         // 如果是非Keep-Alive,关闭连接
    117         ChannelFuture f = ctx.channel().writeAndFlush(res);
    118         if (!HttpUtil.isKeepAlive(req) || res.status().code() != 200) {
    119             f.addListener(ChannelFutureListener.CLOSE);
    120         }
    121     }
    122 
    123     @Override
    124     public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
    125             throws Exception {
    126         cause.printStackTrace();
    127         ctx.close();
    128     }
    129 }

    (1) 第一次握手由HTTP协议承载,所以是一个HTTP消息,根据消息头中是否包含"Upgrade"字段来判断是否是websocket。

    (2) 通过校验后,构造WebSocketServerHandshaker,通过它构造握手响应信息返回给客户端,同时将WebSocket相关的编码和解码类动态添加到ChannelPipeline中。

    下面分析链路建立之后的操作:

    (1) 客户端通过文本框提交请求给服务端,Handler收到之后已经解码之后的WebSocketFrame消息。

    (2) 如果是关闭按链路的指令就关闭链路

    (3) 如果是维持链路的ping消息就返回Pong消息。

    (4) 否则就返回应答消息

    五、客户端以及测试

    html5中的JS代码:

    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="UTF-8">
        Netty WebSocket 时间服务器
    </head>
    <br>
    <body>
    <br>
    <script type="text/javascript">
        var socket;
        if (!window.WebSocket) {
            window.WebSocket = window.MozWebSocket;
        }
        if (window.WebSocket) {
            socket = new WebSocket("ws://localhost:8080/websocket");
            socket.onmessage = function (event) {
                var ta = document.getElementById('responseText');
                ta.value = "";
                ta.value = event.data
            };
            socket.onopen = function (event) {
                var ta = document.getElementById('responseText');
                ta.value = "打开WebSocket服务正常,浏览器支持WebSocket!";
            };
            socket.onclose = function (event) {
                var ta = document.getElementById('responseText');
                ta.value = "";
                ta.value = "WebSocket 关闭!";
            };
        }
        else {
            alert("抱歉,您的浏览器不支持WebSocket协议!");
        }
    
        function send(message) {
            if (!window.WebSocket) {
                return;
            }
            if (socket.readyState == WebSocket.OPEN) {
                socket.send(message);
            }
            else {
                alert("WebSocket连接没有建立成功!");
            }
        }
    </script>
    <form onsubmit="return false;">
        <input type="text" name="message" value="Netty最佳实践"/>
        <br><br>
        <input type="button" value="发送WebSocket请求消息" onclick="send(this.form.message.value)"/>
        <hr color="blue"/>
        <h3>服务端返回的应答消息</h3>
        <textarea id="responseText" style="500px;height:300px;"></textarea>
    </form>
    </body>
    </html>

    演示效果大致如下:

    这里只是对WebSocket协议最基本的演示,WebSocket支持多种协议,文本,二进制,控制帧。

  • 相关阅读:
    html5 canvas 小例子 旋转的时钟
    用深度学习(CNN RNN Attention)解决大规模文本分类问题
    生成式对抗网络GAN 的研究进展与展望
    linux 系统信息查看
    cmd 更改字体
    查看sbt版本
    机器学习算法汇总
    spark 大数据 LR测试
    spark
    hadoop生态圈介绍
  • 原文地址:https://www.cnblogs.com/carl10086/p/6188808.html
Copyright © 2011-2022 走看看