WebSocket協議開發


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

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

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

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

HTTP協議的弊端

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

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

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

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

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

比較新的一種輪詢技術是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>

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM