WebSocket協議開發

一直以來,網絡在很大程度上都是圍繞着HTTP的請求/響應模式而構建的。客戶端加載一個網頁,而後直到用戶點擊下一頁以前,什麼都不會發生。在2005年左右,Ajax開始讓網絡變得更加動態了。但全部的HTTP通訊仍然是由客戶端控制的,這就須要用戶進行互動或按期輪詢,以便從服務器加載新數據。javascript

長期以來存在着各類技術讓服務器得知有新數據可用時,當即將數據發送到客戶端。這些技術種類繁多,例如「推送」或Comet。最經常使用的一種黑客手段是對服務器發起連接建立假象,被稱爲長輪詢。利用長輪詢,客戶端能夠打開指向服務器的HTTP鏈接,而服務器會一直保持鏈接打開,直到發送響應。服務器只要實際擁有新數據,就會發送響應(其餘技術包括Flash、XHR multipart請求和所謂的HTML Files)。長輪詢和其餘技術都很是好用,在Gmail聊天等應用中會常用它們。html

可是,這些解決方案都存在一個共同的問題:因爲HTTP協議的開銷,致使它們不適用於低延遲應用java

爲了解決這些問題,WebSocket將網絡套接字引入到了客戶端和服務端,瀏覽器和服務器之間能夠經過套接字創建持久的鏈接雙方隨時均可以互發數據給對方,而不是以前由客戶端控制的一請求一應答模式。web

HTTP協議的弊端

將HTTP協議的主要弊端總結以下。bootstrap

(1)HTTP協議爲半雙工協議。半雙工協議指數據能夠在客戶端和服務端兩個方向上傳輸,可是不能同時傳輸。它意味着在同一時刻,只有一個方向上的數據傳送;瀏覽器

(2)HTTP消息冗長而繁瑣。HTTP消息包含消息頭、消息體、換行符等,一般狀況下采用文本方式傳輸,相比於其餘的二進制通訊協議,冗長而繁瑣;安全

(3)針對服務器推送的黑客攻擊。例如長時間輪詢。服務器

如今,不少網站爲了實現消息推送,所用的技術都是輪詢。輪詢是在特定的的時間間隔(如每1秒),由瀏覽器對服務器發出HTTP request,而後由服務器返回最新的數據給客戶端瀏覽器。這種傳統的模式具備很明顯的缺點,即瀏覽器須要不斷地向服務器發出請求,然而HTTP request的header是很是冗長的,裏面包含的可用數據比例可能很是低,這會佔用不少的帶寬和服務器資源。websocket

比較新的一種輪詢技術是Comet,使用了Ajax。這種技術雖然可達到雙向通訊,但依然須要發出請求,並且在Comet中,廣泛採用了長鏈接,這也會大量消耗服務器帶寬和資源。網絡

爲了解決HTTP協議效率低下的問題,HTML5定義了WebSocket協議,能更好地節省服務器資源和帶寬並達到實時通訊。

WebSocket入門

WebSocket是HTML5開始提供的一種瀏覽器與服務器間進行全雙工通訊的網絡技術,WebSocket通訊協議於2011年被IETF定爲標準RFC6455,WebSocket API被W3C定爲標準。

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

下面總結一下WebSocket的特色。

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

WebSocket鏈接創建

創建WebSocket鏈接時,須要經過客戶端或者瀏覽器發出握手請求,請求消息示例如圖:

服務端返回給客戶端的應答消息如圖:

爲了創建一個WebSocket鏈接,客戶端瀏覽器首先要向服務器發起一個HTTP請求,這個請求和一般的HTTP請求不一樣,包含了一些附加頭信息,其中附加頭信息「Upgrade: WebSocket」代表這是一個申請協議升級的HTTP請求。服務器端解析這些附加的頭信息,而後生成應答信息返回給客戶端,客戶端和服務器端的WebSocket鏈接就創建起來了,雙方能夠經過這個鏈接通道自由地傳遞信息,而且這個鏈接會持續存在直到客戶端或者服務器端的某一方主動關閉鏈接。

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

WebSocket生命週期 

握手成功以後,服務端和客戶端就能夠經過「messages」的方式進行通訊了,一個消息由一個或者多個幀組成,WebSocket的消息並不必定對應一個特定網絡層的幀,它能夠被分割成多個幀或者被合併。

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

WebSocket鏈接生命週期示意圖如圖:

WebSocket鏈接關閉

爲關閉WebSocket鏈接,客戶端和服務端須要經過一個安全的方法關閉底層TCP鏈接以及TLS會話。若是合適,丟棄任何可能已經接收的字節;必要時(好比受到攻擊),能夠經過任何可用的手段關閉鏈接。

底層的TCP鏈接,在正常狀況下,應該首先由服務器關閉。在異常狀況下(例如在一個合理的時間週期後沒有接收到服務器的TCP Close),客戶端能夠發起TCP Close。所以,當服務器被指示關閉WebSocket鏈接時,它應該當即發起一個TCP Close操做;客戶端應該等待服務器的TCP Close。

WebSocket的握手關閉消息帶有一個狀態碼和一個可選的關閉緣由,它必須按照協議要求發送一個Close控制幀,當對端接收到關閉控制幀指令時,須要主動關閉WebSocket鏈接。

Netty WebSocket協議開發

Netty基於HTTP協議棧開發了WebSocket協議棧,利用Netty的WebSocket協議棧能夠很是方便地開發出WebSocket客戶端和服務端。

場景設計

WebSocket服務端的功能以下:支持WebSocket的瀏覽器經過WebSocket協議發送請求消息給服務端,服務端對請求消息進行判斷,若是是合法的WebSocket請求,則獲取請求消息體(文本),並在後面追加字符串「歡迎使用Netty WebSocket服務,如今時刻:系統時間」。

客戶端HTML經過內嵌的JS腳本建立WebSocket鏈接,若是握手成功,在文本框中打印「打開WebSocket服務正常,瀏覽器支持WebSocket!」。

服務端代碼示例:

首先對WebSocket服務端的功能進行簡單地講解。WebSocket服務端接收到請求消息以後,先對消息的類型進行判斷,若是不是WebSocket握手請求消息,則返回 HTTP 400 BAD REQUEST 響應給客戶端。

服務端對握手請求消息進行處理,構造握手響應返回,雙方的Socket鏈接正式創建,服務端返回的握手應答消息:

鏈接創建成功後,到被關閉以前,雙方均可以主動向對方發送消息,這點跟HTTP的一請求一應答模式存在很大的差異。相比於HTTP,它的網絡利用率更高,能夠經過全雙工的方式進行消息發送和接收。

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.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() {
                        @Override
                        protected void initChannel(Channel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();
                            //首先添加HttpServerCodec,將請求和應答消息編碼或者解碼爲HTTP消息;
                            pipeline.addLast("http-codec", new HttpServerCodec());
                            //增長HttpObjectAggregator,它的目的是將HTTP消息的多個部分組合成一條完整的HTTP消息;
                            pipeline.addLast("aggregator", new HttpObjectAggregator(65536));
                            //添加ChunkedWriteHandler,來向客戶端發送HTML5文件,
                            //它主要用於支持瀏覽器和服務端進行WebSocket通訊;
                            ch.pipeline().addLast("http-chunked", new ChunkedWriteHandler());
                            //增長WebSocket服務端handler。
                            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);
    }
}

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.websocketx.*;
import io.netty.util.CharsetUtil;


import static io.netty.handler.codec.http.HttpResponseStatus.*;
import static io.netty.handler.codec.http.HttpHeaders.*;
import static io.netty.handler.codec.http.HttpVersion.*;

public class WebSocketServerHandler extends SimpleChannelInboundHandler {

    private WebSocketServerHandshaker handshaker;

    @Override
    public void messageReceived(ChannelHandlerContext ctx, Object msg)
            throws Exception {
        // 傳統的HTTP接入
        //第一次握手請求消息由HTTP協議承載,因此它是一個HTTP消息,執行handleHttpRequest方法來處理WebSocket握手請求。
        if (msg instanceof FullHttpRequest) {
            handleHttpRequest(ctx, (FullHttpRequest) msg);
        }
        // WebSocket接入
        // 客戶端經過文本框提交請求消息給服務端,WebSocketServerHandler接收到的是已經解碼後的WebSocketFrame消息。
        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解碼失敗,返回HTTP異常
        // 首先對握手請求消息進行判斷,若是消息頭中沒有包含Upgrade字段或者它的值不是websocket,則返回HTTP 400響應。
        if (!req.getDecoderResult().isSuccess() || (!"websocket".equals(req.headers().get("Upgrade")))) {
            sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1,BAD_REQUEST));
            return;
        }
        // 構造握手響應返回,本機測試
        // 握手請求簡單校驗經過以後,開始構造握手工廠,
        // 建立握手處理類WebSocketServerHandshaker,
        WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(
              "ws://localhost:8080/websocket", null, false); handshaker = wsFactory.newHandshaker(req); if (handshaker == null) { WebSocketServerHandshakerFactory.sendUnsupportedWebSocketVersionResponse(ctx.channel()); } else { // 經過它構造握手響應消息返回給客戶端, // 同時將WebSocket相關的編碼和解碼類動態添加到ChannelPipeline中,用於WebSocket消息的編解碼, // 添加WebSocketEncoder和WebSocketDecoder以後,服務端就能夠自動對WebSocket消息進行編解碼了 handshaker.handshake(ctx.channel(), req); } } private void handleWebSocketFrame(ChannelHandlerContext ctx,WebSocketFrame frame) { // 判斷是不是關閉鏈路的指令 // 首先須要對控制幀進行判斷,若是是關閉鏈路的控制消息 // 就調用WebSocketServerHandshaker的close方法關閉WebSocket鏈接; if (frame instanceof CloseWebSocketFrame) { handshaker.close(ctx.channel(),(CloseWebSocketFrame) frame.retain()); return; } // 判斷是不是Ping消息 // 若是是維持鏈路的Ping消息,則構造Pong消息返回。 if (frame instanceof PingWebSocketFrame) { ctx.channel().write(new PongWebSocketFrame(frame.content().retain())); return; } // 本例程僅支持文本消息,不支持二進制消息 // WebSocket通訊雙方使用的都是文本消息,因此對請求消息的類型進行判斷,不是文本的拋出異常。 if (!(frame instanceof TextWebSocketFrame)) { throw new UnsupportedOperationException( String.format("%s frame types not supported", frame.getClass().getName())); } // 返回應答消息 // 從TextWebSocketFrame中獲取請求消息字符串, String request = ((TextWebSocketFrame) frame).text(); // 對它處理後經過構造新的TextWebSocketFrame消息返回給客戶端, // 因爲握手應答時動態增長了TextWebSocketFrame的編碼類,因此,能夠直接發送TextWebSocketFrame對象。 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(); 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(); } }

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服務正常,瀏覽器支持";
        };
        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>
    <input type="text" name="message" value="Netty WebSocket"/>
    <br><br>
    <input type="button" value="發送消息" onclick="send(this.form.message.value)">
    <hr color="blue"/>
    <h3>服務器返回的應答消息</h3>
    <textarea id="responseText" style="width: 500px;height: 300px;"></textarea>
</form>
</body>
</html>
相關文章
相關標籤/搜索