1. 程式人生 > >web Socket與netty框架支援高併發

web Socket與netty框架支援高併發

ONE、分析HTTP與WEB SOCKET的優缺點:

一、HTTP協議的弊端
將HTTP協議的主要弊端總結如下:

(1)半雙工協議:可以在客戶端和服務端2個方向上傳輸,但是不能同時傳輸。同一時刻,只能在一個方向上傳輸。
(2) HTTP訊息冗長:相比於其他二進位制協議,有點繁瑣。
(3) 針對伺服器推送的黑客攻擊,例如長時間輪詢。

現在很多網站的訊息推送都是使用輪詢,即客戶端每隔1S或者其他時間給伺服器傳送請求,然後伺服器返回最新的資料給客戶端。HTTP協議中的Header非常冗長,因此會佔用很多的頻寬和伺服器資源。
比較新的技術是Comet,使用了AJAX輪詢。雖然可以雙向通訊,但是依然需要傳送請求,而且在Comet中,普遍採用了長連線,也會大量消耗伺服器的頻寬和資源。

但是為了解決這個問題,HTML5定義的WebSocket協議。

TWO、WebSocket協議介紹:

在WebSocket API中,瀏覽器和伺服器只需要一個握手的動作,然後,瀏覽器和伺服器之間就形成了一條快速通道,兩者就可以直接互相傳送資料了。
WebSocket基於TCP雙向全雙工協議,即在同一時刻,即可以傳送訊息,也可以接收訊息,相比於HTTP協議,是一個性能上的提升。
特點:

單一的TCP連線,全雙工;
對代理、防火牆和路由器透明;
無安全開銷;
通過”ping/pong”幀保持鏈路啟用;
伺服器可以主動傳遞訊息給客戶端,不再需要客戶端輪詢;
無頭部資訊、Cookie和身份驗證;

擁有以上特點的WebSocket就是為了取代輪詢和Comet技術,使得客戶端瀏覽器具備像C/S架構下桌面系統一樣的實時能力。
瀏覽器通過js建立一個WebSocket的請求,連線建立後,客戶端和伺服器端可以通過TCP直接交換資料。
因為WebSocket本質上是一個TCP連線,穩定,所以在Comet和輪詢比擁有效能優勢。

THREE、WebSocket連線

1、client連線建立:
client端傳送握手請求,這個請求和普通的HTTP請求不同,包含了一些附加頭資訊,其中附加頭資訊”Upgrade: Websocket”表明這是一個申請協議升級的HTTP請求。伺服器嘗試解析這個資訊,然後返回應答資訊給客戶端,因此客戶端和伺服器端的WebSocket連線就建立起來了,雙方可以通過這個連線通道自由的傳遞資訊。這個連線會持續到某一方主動斷開連線。
2、服務端的應答請求:


client訊息中的”Sec-WebSocket-Key”是隨機的,伺服器端會用這些資料來構造一個”SHA-1”的資訊摘要,把”Sec-WebSocket-Key”加上一個魔幻字串。使用”SHA-1”加密,然後進行BASE64編碼,將結果作為”Sec-Webscoket-Accept”頭的值。
3、生命週期:
握手成功,連線建立後,以”Messages”的方式通訊。一個訊息由一個或者多個”幀”組成。幀都有自己的型別,同一訊息的多個幀型別相同。廣義上,型別可以是文字、二進位制、控制幀如訊號。
4、連線關閉:
安全方法是關閉底層TCP連線以及TLS會話。底層的TCP連線,正常情況下,應該由伺服器先關閉。異常時(比如合理的時間內沒有接收到伺服器的TCP Close),可以由客戶端發起TCP Close。因此,在client發起TCP Close時,伺服器應該立即發起一個TCP Close操作;客戶端則等待伺服器的TCP Close;關閉訊息帶有一個狀態碼和可選的關閉原因,它必須按照協議要求傳送一個Close控制幀。

FOUR、協議開發

官方demo:

服務端:

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;

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檔案

netty框架應用:

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.websocketx.*;
import io.netty.util.CharsetUtil;

import java.util.logging.Level;
import java.util.logging.Logger;

import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;

public class WebSocketServerHandler extends SimpleChannelInboundHandler<Object> {
    private static final Logger logger = Logger
            .getLogger(WebSocketServerHandler.class.getName());

    private WebSocketServerHandshaker handshaker;


    @Override
    public void channelRead0(ChannelHandlerContext ctx, Object msg)
            throws Exception {
        // 傳統的HTTP接入
        if (msg instanceof FullHttpRequest) {
            handleHttpRequest(ctx, (FullHttpRequest) msg);
        }
        // WebSocket接入
        else if (msg instanceof WebSocketFrame) {
            handleWebSocketFrame(ctx, (WebSocketFrame) msg);
        }
    }

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

    private void handleHttpRequest(ChannelHandlerContext ctx,
                                   FullHttpRequest req) throws Exception {

        // 如果HTTP解碼失敗,返回HHTP異常
        if (!req.decoderResult().isSuccess()
                || (!"websocket".equals(req.headers().get("Upgrade")))) {
            sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1,
                    BAD_REQUEST));
            return;
        }

        // 構造握手響應返回,本機測試
        WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(
                "ws://localhost:8080/websocket", null, false);
        handshaker = wsFactory.newHandshaker(req);
        if (handshaker == null) {
            WebSocketServerHandshakerFactory
                    .sendUnsupportedVersionResponse(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();
        if (logger.isLoggable(Level.FINE)) {
            logger.fine(String.format("%s received %s", ctx.channel(), request));
        }
        ctx.channel().write(
                new TextWebSocketFrame(request
                        + " , 歡迎使用Netty WebSocket服務,現在時刻:"
                        + new java.util.Date().toString()));
    }

    private static 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();
            HttpUtil.setContentLength(res, res.content().readableBytes());
        }

        // 如果是非Keep-Alive,關閉連線
        ChannelFuture f = ctx.channel().writeAndFlush(res);
        if (!HttpUtil.isKeepAlive(req) || res.status().code() != 200) {
            f.addListener(ChannelFutureListener.CLOSE);
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
            throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}
(1) 第一次握手由HTTP協議承載,所以是一個HTTP訊息,根據訊息頭中是否包含"Upgrade"欄位來判斷是否是websocket。
(2) 通過校驗後,構造WebSocketServerHandshaker,通過它構造握手響應資訊返回給客戶端,同時將WebSocket相關的編碼和解碼類動態新增到ChannelPipeline中。
下面分析鏈路建立之後的操作:
(1) 客戶端通過文字框提交請求給服務端,Handler收到之後已經解碼之後的WebSocketFrame訊息。
(2) 如果是關閉按鏈路的指令就關閉鏈路
(3) 如果是維持鏈路的ping訊息就返回Pong訊息。
(4) 否則就返回應答訊息

HTML測試:

<!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="width:500px;height:300px;"></textarea>
</form>
</body>
</html>

這裡寫圖片描述