Netty WebSocket 協議

HTTP 協議的弊端

  1. HTTP 協議爲半雙工協議. 半雙工協議指數據能夠在客戶端和服務端兩個方向上傳輸, 可是不能同時傳輸. 它意味這同一時刻, 只有一個方向上的數據傳輸; 客戶端發送請求, 服務器等待, 直到收到完整的請求. 而後發送迴應, 客戶端和服務器沒法同時發送.
  2. HTTP 消息冗長而繁瑣. HTTP 消息包含消息頭、消息頭、換行符等, 一般狀況下采用文本方式傳輸, 相比於其餘的二進制通訊協議, 冗長而繁瑣;
  3. 針對服務器推送的黑客攻擊. 例如長時間輪詢.

WebSocket 入門

webSocket 是 HTML5 開始提供的一種瀏覽器於服務器間進行全雙工通訊的技術.java

在 WebSocket API 中, 瀏覽器和服務器只須要作一個握手的動做, 而後, 瀏覽器和服務器之間就造成了一條快速通道, 二者就能夠直接相互傳送數據了. WebSocket 基於 TCP 雙向全雙工進行消息傳遞, 在同一時刻, 既能夠發送消息, 也能夠接收消息, 相比 HTTP 的半雙工協議, 性能獲得很大提高.web

WebSocket 的特色:瀏覽器

  • 單一的 TCP 鏈接, 採用全雙工模式通訊;
  • 對代理、防火牆和路由器透明;
  • 無頭部信息、Cookie和身份驗證;
  • 無安全開銷;
  • 經過 ping/pong 幀保持鏈路激活;
  • 服務器能夠主動傳遞消息給客戶端, 再也不須要客戶端輪詢.

WebSocket 鏈接創建

創建 webSocket 鏈接時, 須要經過客戶端或瀏覽器發出握手請求, 相似下面的 http 報文.安全

clipboard.png

這個請求和一般的 HTTP 請求不一樣, 包含了一些附加頭信息, 其中附加頭信息 Upgrade:WebSocket 代表這是一個申請協議升級的 HTTP 請求.服務器

服務器解析這些附加的頭信息, 而後生成應答信息返回給客戶端, 客戶端和服務端的 WebSocket 鏈接就創建起來了, 雙方能夠經過這個鏈接通道自由的傳遞信息, 而且這個鏈接會持續存在直到客戶端或服務端的某一方主動關閉鏈接.websocket

服務端返回給客戶端的應答消息, 相似以下報文socket

clipboard.png

請求消息中的 Sec-WebSocket-Key 是隨機的, 服務端會用這些數據來構造出一個 SHA-1 的信息摘要, 把 Sec-WebSocket-Key 加上一個魔幻字符串 258EAFA5-E914-47DA-95CA-C5AB0DC85B11. 使用 SHA-1 加密, 而後進行 BASE-64 編碼, 將結果作爲 Sec-WebSocket-Accept 頭的值, 返回給客戶端.ide

WebSocket 生命週期

握手成功以後, 服務端和客戶端就能夠經過 messages 的方式進行通信, 一個消息由一個或多個幀組成.oop

幀都有本身對應的類型, 屬於同一個消息的多個幀具備相同類型的數據. 從廣義上講, 數據類型能夠是文本數據(UTF-8文字)、二進制數據和控制幀(協議級信令, 例如信號).性能

WebSocket 鏈接生命週期以下:

clipboard.png

Netty WebSocket 協議開發

示例代碼

public class TimeServer {

    public void bind(int port) throws Exception {

        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG, 1024)
                    .handler(new LoggingHandler(LogLevel.DEBUG))
                    .childHandler(new ChildChannelHandler());

            // 綁定端口, 同步等待成功
            ChannelFuture f = b.bind(port).sync();

            // 等待服務端監聽端口關閉
            f.channel().closeFuture().sync();
        } finally {
            System.out.println("shutdownGracefully");
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

    private class ChildChannelHandler extends ChannelInitializer<SocketChannel> {
        @Override
        protected void initChannel(SocketChannel ch) {
            ch.pipeline().addLast("http-codec", new HttpServerCodec());

            ch.pipeline().addLast("aggregator", new HttpObjectAggregator(65536));

            ch.pipeline().addLast("http-chunked", new ChunkedWriteHandler());
            ch.pipeline().addLast("handler", new WebSOcketServerHandler());
        }
    }

    private class WebSOcketServerHandler extends SimpleChannelInboundHandler<Object> {

        private WebSocketServerHandshaker handshaker;

        @Override
        protected void messageReceived(ChannelHandlerContext ctx, Object msg) throws Exception {
            // 傳統的 HTTP 接入
            if (msg instanceof FullHttpRequest) {
                System.out.println("傳統的 HTTP 接入");
                handleHttpRequest(ctx, (FullHttpRequest) msg);
            }
            // WebSocket 接入
            else if (msg instanceof WebSocketFrame) {
                System.out.println("WebSocket 接入");
                handleWebSocketFrame(ctx, (WebSocketFrame) msg);
            }
        }

        private void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest req) throws Exception {
            // 若是 HTTP 解碼失敗, 返回HTTP異常
            if (!req.getDecoderResult().isSuccess() || (!"websocket".equalsIgnoreCase(req.headers().get("Upgrade")))) {
                sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST));
                return;
            }

            // 構造握手響應返回, 本機測試
            WebSocketServerHandshakerFactory wsFactory =
                    new WebSocketServerHandshakerFactory("ws://localhost:8080/websocket", null, false);

            handshaker = wsFactory.newHandshaker(req);
            if (handshaker == null) {
                WebSocketServerHandshakerFactory.sendUnsupportedWebSocketVersionResponse(ctx.channel());
            } else {
                handshaker.handshake(ctx.channel(), req);
            }
        }

        private void handleWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) {
            // 判斷是不是關閉鏈路的指令
            if (frame instanceof CloseWebSocketFrame) {
                handshaker.close(ctx.channel(), (CloseWebSocketFrame) frame.retain());
                return;
            }

            // 判斷是不是 ping 信息
            if (frame instanceof PingWebSocketFrame) {
                ctx.channel().write(new PongWebSocketFrame(frame.content().retain()));
                return;
            }

            // 本例程僅支持文本消息, 不支持二進制消息
            if (!(frame instanceof TextWebSocketFrame)) {
                throw new UnsupportedOperationException(String.format("%s frame types not supported", frame.getClass().getName()));
            }

            // 返回應答信息
            String request = ((TextWebSocketFrame) frame).text();
            ctx.channel().write(new TextWebSocketFrame(request + " , 歡迎使用 Netty WebSocket 服務, 如今時刻: "
                    + new java.util.Date().toString()));

        }

        private void sendHttpResponse(ChannelHandlerContext ctx, FullHttpRequest req, FullHttpResponse res) {
            if (res.getStatus().code() != 200) {
                ByteBuf buf = Unpooled.copiedBuffer(res.getStatus().toString(), CharsetUtil.UTF_8);
                res.content().writeBytes(buf);
                buf.release();
                setContentLength(res, res.content().readableBytes());
            }

            // 若是是非 Keep-Alive, 關閉鏈接
            ChannelFuture f = ctx.channel().writeAndFlush(res);
            if (!isKeepAlive(req) || res.getStatus().code() != 200) {
                f.addListener(ChannelFutureListener.CLOSE);
            }
        }

        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            cause.printStackTrace();
            ctx.close();
        }

        @Override
        public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
            ctx.flush();
        }
    }

}

HttpServerCodec: 將請求和應答消息編碼或解碼爲 HTTP 消息.
HttpObjectAggregator: 它的目的是將 HTTP 消息的多個部分組合成一條完整的 HTTP 消息. Netty 能夠聚合 HTTP 消息, 使用 FullHttpResponseFullHttpRequestChannelPipeline 中的下一個 ChannelHandler, 這就消除了斷裂消息, 保證了消息的完整.
ChunkedWriteHandler: 來向客戶端發送 HTML5 文件, 主要用於支持瀏覽器和服務端進行 WebSocket 通訊.

第一次握手請求消息由 HTTP 協議承載, 因此它是一個 HTTP 消息, 執行 handleHttpRequest 方法來處理 WebSocket 握手請求. 經過判斷請求消息判斷是否包含 Upgrade 字段或它的值不是 websocket, 則返回 HTTP 400 響應.

握手請求校驗經過以後, 開始構造握手工廠, 建立握手處理類 WebSocketServerHandshaker, 經過它構造握手響應消息返回給客戶端.

添加 WebSocket Encoder 和 WebSocket Decoder 以後, 服務端就能夠自動對 WebSocket 消息進行編解碼了, 後面的 handler 能夠直接對 WebSocket 對象進行操做.

handleWebSocketFrame 對消息進行判斷, 首先判斷是不是控制幀, 若是是就關閉鏈路. 若是是維持鏈路的 Ping 消息, 則構造 Pong 消息返回. 因爲本例程的 WebSocket 通訊雙方使用的都是文本消息, 因此對請求新消息的類型進行判斷, 而不是文本的拋出異常.

最後, 從 TextWebSocketFrame 中獲取請求消息字符串, 對它處理後經過構造新的 TextWebSocketFrame 消息返回給客戶端, 因爲握手應答時, 動態增長了 TextWebSocketFrame 的編碼類, 因此能夠直接發送 TextWebSocketFrame 對象.

相關文章
相關標籤/搜索