netty系列之:使用netty搭建websocket服務器

簡介

websocket是一個優秀的協議,它是建立在TCP基礎之上的,兼容HTTP的網絡協議。通過Websocket我們可以實現客戶端和服務器端的即時通訊,免除了客戶端多次輪循帶來的性能損耗。

既然websocket這麼優秀,那麼怎麼在netty中使用websocket呢?

netty中的websocket

雖然websocket是一個單獨的和HTTP協議完全不同的協議,但是在netty中還是將其放到了http包中。我們回想一下netty中對於各種協議的支持。如果要支持這種協議,肯定需要一個decoder和encoder編碼和解碼器用於對協議進行編解碼。將傳輸的數據從ByteBuf轉換到協議類型,或者將協議類型轉換成爲ByteBuf。

這是netty的工作核心原理,也是後續自定義netty擴展的基礎。

那麼對於websocket來說,是怎麼樣的呢?

websocket的版本

WebSocket作爲一種協議,自然不是憑空而來的,通過不斷的發展纔到了今天的WebSocket協議。具體的webSocket的發展史我們就不去深究了。我們先看下netty提供的各種WebSocket的版本。

在WebSocketVersion類中,我們可以看到:

UNKNOWN(AsciiString.cached(StringUtil.EMPTY_STRING)),

    V00(AsciiString.cached("0")),

    V07(AsciiString.cached("7")),

    V08(AsciiString.cached("8")),

    V13(AsciiString.cached("13"));

WebSocketVersion是一個枚舉類型,它裏面定義了websocket的4個版本,除了UNKNOWN之外,我們可以看到websocket的版本有0,7,8,13這幾個。

FrameDecoder和FrameEncoder

我們知道websocket的消息是通過frame來傳遞的,因爲不同websocket的版本影響到的是frame的格式的不同。所以我們需要不同的FrameDecoder和FrameEncoder來在WebSocketFrame和ByteBuf之間進行轉換。

既然websocket有四個版本,那麼相對應的就有4個版本的decoder和encoder:

WebSocket00FrameDecoder
WebSocket00FrameEncoder
WebSocket07FrameDecoder
WebSocket07FrameEncoder
WebSocket08FrameDecoder
WebSocket08FrameEncoder
WebSocket13FrameDecoder
WebSocket13FrameEncoder

至於每個版本之間的frame有什麼區別,我們這裏就不細講了,感興趣的朋友可以關注我的後續文章。

熟悉netty的朋友應該都知道,不管是encoder還是decoder都是作用在channel中對消息進行轉換的。那麼在netty中對websocket的支持是怎麼樣的呢?

WebSocketServerHandshaker

netty提供了一個WebSocketServerHandshaker類來統一使用encoder和decoder的使用。netty提供一個工廠類WebSocketServerHandshakerFactory根據客戶端請求header的websocket版本不同,來返回不同的WebSocketServerHandshaker。

public WebSocketServerHandshaker newHandshaker(HttpRequest req) {

        CharSequence version = req.headers().get(HttpHeaderNames.SEC_WEBSOCKET_VERSION);
        if (version != null) {
            if (version.equals(WebSocketVersion.V13.toHttpHeaderValue())) {
                // Version 13 of the wire protocol - RFC 6455 (version 17 of the draft hybi specification).
                return new WebSocketServerHandshaker13(
                        webSocketURL, subprotocols, decoderConfig);
            } else if (version.equals(WebSocketVersion.V08.toHttpHeaderValue())) {
                // Version 8 of the wire protocol - version 10 of the draft hybi specification.
                return new WebSocketServerHandshaker08(
                        webSocketURL, subprotocols, decoderConfig);
            } else if (version.equals(WebSocketVersion.V07.toHttpHeaderValue())) {
                // Version 8 of the wire protocol - version 07 of the draft hybi specification.
                return new WebSocketServerHandshaker07(
                        webSocketURL, subprotocols, decoderConfig);
            } else {
                return null;
            }
        } else {
            // Assume version 00 where version header was not specified
            return new WebSocketServerHandshaker00(webSocketURL, subprotocols, decoderConfig);
        }
    }

同樣的, 我們可以看到,netty爲websocket也定義了4種不同的WebSocketServerHandshaker。

WebSocketServerHandshaker中定義了handleshake方法,通過傳入channel,並向其添加encoder和decoder

public final ChannelFuture handshake(Channel channel, FullHttpRequest req,
                                            HttpHeaders responseHeaders, final ChannelPromise promise) 

            p.addBefore(ctx.name(), "wsencoder", newWebSocketEncoder());
            p.addBefore(ctx.name(), "wsdecoder", newWebsocketDecoder());

而添加的這兩個newWebSocketEncoder和newWebsocketDecoder就是各個WebSocketServerHandshaker的具體實現中定義的。

WebSocketFrame

所有的ecode和decode都是在WebSocketFrame和ByteBuf中進行轉換。WebSocketFrame繼承自DefaultByteBufHolder,表示它是一個ByteBuf的容器。除了保存有ByteBuf之外,它還有兩個額外的屬性,分別是finalFragment和rsv。

finalFragment表示該frame是不是最後一個Frame。對於大數據量的消息來說,會將消息拆分成爲不同的frame,這個屬性特別有用。

我們再看一下websocket協議消息的格式:

      0                   1                   2                   3
      0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
     +-+-+-+-+-------+-+-------------+-------------------------------+
     |F|R|R|R| opcode|M| Payload len |    Extended payload length    |
     |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
     |N|V|V|V|       |S|             |   (if payload len==126/127)   |
     | |1|2|3|       |K|             |                               |
     +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
     |     Extended payload length continued, if payload len == 127  |
     + - - - - - - - - - - - - - - - +-------------------------------+
     |                               |Masking-key, if MASK set to 1  |
     +-------------------------------+-------------------------------+
     | Masking-key (continued)       |          Payload Data         |
     +-------------------------------- - - - - - - - - - - - - - - - +
     :                     Payload Data continued ...                :
     + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
     |                     Payload Data continued ...                |
     +---------------------------------------------------------------+

rsv代表的是消息中的擴展字段,也就是RSV1,RSV2和RSV3。

除此之外就是ByteBuf的一些基本操作了。

WebSocketFrame是一個抽象類,它的具體實現類有下面幾種:

BinaryWebSocketFrame
CloseWebSocketFrame
ContinuationWebSocketFrame
PingWebSocketFrame
PongWebSocketFrame
TextWebSocketFrame

BinaryWebSocketFrame和TextWebSocketFrame很好理解,他們代表消息傳輸的兩種方式。

CloseWebSocketFrame是代表關閉連接的frame。ContinuationWebSocketFrame表示消息中多於一個frame的表示。

而PingWebSocketFrame和PongWebSocketFrame是兩個特殊的frame,他們主要用來做服務器和客戶端的探測。

這些frame都是跟Websocket的消息類型一一對應的,理解了websocket的消息類型,對應理解這些frame類還是很有幫助的。

netty中使用websocket

講了這麼多websocket的原理和實現類,接下來就是實戰了。

在這個例子中,我們使用netty創建一個websocket server,然後使用瀏覽器客戶端來對server進行訪問。

創建websocket server和普通netty服務器的過程沒有什麼兩樣。只是在ChannelPipeline中,需要加入自定義的WebSocketServerHandler:

pipeline.addLast(new WebSocketServerHandler());

這個WebSocketServerHandler需要做什麼事情呢?

它需要同時處理普通的HTTP請求和webSocket請求。

這兩種請求可以通過接收到的msg類型的不同來進行判斷:

    public void channelRead0(ChannelHandlerContext ctx, Object msg) throws IOException {
        //根據消息類型,處理兩種不同的消息
        if (msg instanceof FullHttpRequest) {
            handleHttpRequest(ctx, (FullHttpRequest) msg);
        } else if (msg instanceof WebSocketFrame) {
            handleWebSocketFrame(ctx, (WebSocketFrame) msg);
        }
    }

在客戶端進行websocket連接之前,需要借用當前的channel通道,開啓handleshake:

        // websocket握手
        WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(
                getWebSocketLocation(req), null, true, 5 * 1024 * 1024);
        handshaker = wsFactory.newHandshaker(req);
        if (handshaker == null) {
            WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());
        } else {
            handshaker.handshake(ctx.channel(), req);
        }

我們得到handshaker之後,就可以對後續的WebSocketFrame進行處理:

private void handleWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) {

        // 處理各種websocket的frame信息
        if (frame instanceof CloseWebSocketFrame) {
            handshaker.close(ctx, (CloseWebSocketFrame) frame.retain());
            return;
        }
        if (frame instanceof PingWebSocketFrame) {
            ctx.write(new PongWebSocketFrame(frame.content().retain()));
            return;
        }
        if (frame instanceof TextWebSocketFrame) {
            // 直接返回
            ctx.write(frame.retain());
            return;
        }
        if (frame instanceof BinaryWebSocketFrame) {
            // 直接返回
            ctx.write(frame.retain());
        }
    }

這裏我們只是機械的返回消息,大家可以根據自己業務邏輯的不同,對消息進行解析。

有了服務器端,客戶端該怎麼連接呢?很簡單首選構造WebSocket對象,然後處理各種回調即可:

socket = new WebSocket("ws://127.0.0.1:8000/websocket");
socket.onmessage = function (event) { 

}
socket.onopen = function(event) {
        };
socket.onclose = function(event) {
        };

總結

以上就是使用netty搭建websocket服務器的完整流程,本文中的服務器可以同時處理普通HTTP請求和webSocket請求,但是稍顯複雜,有沒有更加簡單的方式呢?敬請期待。

本文的例子可以參考:learn-netty4

本文已收錄於 http://www.flydean.com/23-netty-websocket-server/

最通俗的解讀,最深刻的乾貨,最簡潔的教程,衆多你不知道的小技巧等你來發現!

歡迎關注我的公衆號:「程序那些事」,懂技術,更懂你!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章