1. 程式人生 > >Netty——WebSocket之事

Netty——WebSocket之事

        在說WebSocket以前,我們再來看看HTTP協議,HTTP1.0,HTTP1.1,HTTP2.0每個版本的更新帶來更高效的更豐富的功能:短連線、長連線、快取處理的豐富、斷點續傳、錯誤通知的豐富、多路複用、請求優先順序、header壓縮……更多的我們來看這篇文章:HTTP1.0、HTTP1.1和HTTP2.0的區別https://www.cnblogs.com/zhangyfr/p/8662673.html)

        HTTP解決了我們很多問題,而且也非常強大,但是它有哪些不足之處呢?我們來看下:1,HTTP協議為半雙工協議,可以在 client和server兩個方向上傳輸,但是不能同時傳輸,意味著同一時刻,只能一個方向上傳送;    2,HTTP訊息冗長而複雜,包含訊息頭、訊息體、換行符,雖然功能強大,但是在某些方面也成了不足。              例如我們需要server向client端推送資料利用HTTP就很難,例如一些監控系統,隨著後臺資料的變化,前邊也跟著相應的變化,隨後後來的輪詢機制等可以解決這個問題,但是這樣採用了長連線,大量消耗了伺服器的寬頻和資源。也就是在client和server期望實時通訊的情況下,Http就有些頭疼了。亦是HTML5定義了WebSocket協議,更好的節省伺服器資源和寬頻,並達到實時通訊的目的。好下邊我們來看WebSocket的相關內容:

       WebSocket,瀏覽器和伺服器只需要做一個握手動作(非HTTP三次握手),然後之間就會形成一條快速通道,兩者就可以相互傳送資料了,是基於TCP雙向全雙工進行訊息傳遞,其特點包括:1,單一的TCP連線,採用全雙工模式通訊;    2,對代理、防火牆和路由器透明;     3,無頭部資訊、Cookie和身份驗證;     4,無安全開銷;     5,通過“ping/pong”幀保持鏈路啟用;     6,伺服器可以主動傳遞訊息給客戶端,不再需要客戶端輪詢。

      好了,講了WebSocket,我們來看下,利用Netty如何方便快速的開發WebSocket的服務端,下邊通過一個實時傳輸文字的例子來看,服務端程式碼:

public class WebSocketServer {


    public void run(final int port) {
        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));
                    pipeline.addLast("http-chunked",new ChunkedWriteHandler());
                    //業務處理
                    ch.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();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }


    public static void main(String[] args) {
        int port = 8091;
        if (args != null && args.length > 0) {
            try {
                port = Integer.valueOf(args[0]);
            } catch (NumberFormatException e) {

            }
        }

        new WebSocketServer().run(port);
    }
}




public class WebSocketServerHandler extends SimpleChannelInboundHandler<Object> {

    private static final Logger logger = Logger.getLogger(WebSocketServerHandler.class.getName());

    private WebSocketServerHandshaker handshaker;

    /**
     * 接受到訊息處理
     * @param ctx
     * @param msg
     * @throws Exception
     */
    @Override
    protected void messageReceived(ChannelHandlerContext ctx, Object msg) throws Exception {
        if(msg instanceof FullHttpRequest){
            //傳統的http接入
            handlerHttpRequest(ctx,(FullHttpRequest)msg);
        }else if(msg instanceof WebSocketFrame){
            //WebSocket接入
            handlerWebSocketFrame(ctx,(WebSocketFrame)msg);
        }
    }

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

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


    /**
     * 處理正常請求
     * @param ctx
     * @param req
     */
    private void handlerHttpRequest(ChannelHandlerContext ctx,FullHttpRequest req){
        //如果HTTP解碼失敗,返回http異常
        if(!req.decoderResult().isSuccess() || (!"websocket".equals(req.headers().get("Upgrade")))){
            sendHttpResponse(ctx,req,new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,HttpResponseStatus.BAD_REQUEST));
            return;
        }

        //構造握手響應返回,本機測試
        WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory("ws://localhost:8091/websocket",null,false);
        handshaker = wsFactory.newHandshaker(req);
        if(handshaker ==null){
            WebSocketServerHandshakerFactory .sendUnsupportedVersionResponse(ctx.channel());
        }else{
            handshaker.handshake(ctx.channel(),req);
        }
    }


    /**
     * 處理websocket請求
     * @param ctx
     * @param frame
     */
    private void handlerWebSocketFrame(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));
        }
        System.out.println("server 接受到的資訊:" + request);
        ctx.channel().write(new TextWebSocketFrame(request + " ,歡迎使用netty websocket服務,現在時刻:" + new Date().toString()));
    }

    /**
     * 響應客戶端訊息
     * @param ctx
     * @param req
     * @param res
     */
    private static void sendHttpResponse(ChannelHandlerContext ctx , FullHttpRequest req, FullHttpResponse res){
        if(res.status().code() !=200){
            ByteBuf buf = Unpooled.copiedBuffer(res.status().toString(), CharsetUtil.UTF_8);
            res.content().writeBytes(buf);
            buf.release();
            HttpHeaderUtil.setContentLength(res,res.content().readableBytes());
        }

        ChannelFuture f = ctx.channel().writeAndFlush(res);
        if(!HttpHeaderUtil.isKeepAlive(req) || res.status().code()!=200){
            f.addListener(ChannelFutureListener.CLOSE);
        }
    }

}

         前端頁面:

<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:8091/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("sorry,您的瀏覽器不支援websocket協議!");
            }

            function send(message) {
                if(!window.WebSocket){
                    return;
                }
                if(socket.readyState = WebSocket.OPEN){
                    socket.send(message);
                    socket.send("ljh study netty")
                }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>

 

          Netty對WebSocket協議棧的支援還是非常棒的,更多的我們需要了解可以到WebSocket官網http://www.websocket.org/index.html)