netty 與 webSocket

来源:https://www.cnblogs.com/xisha/archive/2018/10/31/netty_and_websocket.html
-Advertisement-
Play Games

netty 與 webSocket 起因 有個需求需要用到 ,然後最近又正好在學 ,然後合起來走一波。寫篇文章記錄一下,做一個念想。 協議格式 開始 我們先寫一個什麼都不加的 熱熱手,話不多說,代碼如下 常規的netty入門示例,加了個String的編碼和解碼器,還加了一個列印消息的 ,並不是什麼太 ...


netty 與 webSocket

起因

有個需求需要用到webSocket ,然後最近又正好在學netty,然後合起來走一波。寫篇文章記錄一下,做一個念想。

協議格式

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 ...                |
 +---------------------------------------------------------------+
 
 具體每一bit的意思
FIN      1bit 表示信息的最後一幀
RSV 1-3  1bit each 以後備用的 預設都為 0
Opcode   4bit 幀類型,稍後細說
Mask     1bit 掩碼,是否加密數據,預設必須置為1
Payload  7bit 數據的長度
Masking-key      1 or 4 bit 掩碼
Payload data     (x + y) bytes 數據
Extension data   x bytes  擴展數據
Application data y bytes  程式數據
OPCODE:4位
解釋PayloadData,如果接收到未知的opcode,接收端必須關閉連接。
0x0表示附加數據幀
0x1表示文本數據幀
0x2表示二進位數據幀
0x3-7暫時無定義,為以後的非控制幀保留
0x8表示連接關閉
0x9表示ping
0xA表示pong
0xB-F暫時無定義,為以後的控制幀保留

開始

我們先寫一個什麼都不加的 service 熱熱手,話不多說,代碼如下

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;

/**
 * @author Sean Wu
 */
public class ServiceMain {


    public static void main(String[] args) throws Exception {

        NioEventLoopGroup boss = new NioEventLoopGroup(1);
        NioEventLoopGroup worker = new NioEventLoopGroup();
        ServerBootstrap b = new ServerBootstrap();
        b.group(boss, worker)
                .channel(NioServerSocketChannel.class)
                .handler(new LoggingHandler(LogLevel.DEBUG))
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    protected void initChannel(SocketChannel ch) throws Exception {

                        ChannelPipeline p = ch.pipeline();
                        p.addLast(new StringEncoder()).addLast(new StringDecoder()).addLast(new ChannelInboundHandlerAdapter() {
                            @Override
                            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                                super.channelRead(ctx, msg);
                                System.out.println(msg.toString());
                            }
                        });
                    }
                });
        ChannelFuture f = b.bind(8866).sync();
        f.channel().closeFuture().sync();
        boss.shutdownGracefully();
        worker.shutdownGracefully();
    }

}

常規的netty入門示例,加了個String的編碼和解碼器,還加了一個列印消息的 Handler,並不是什麼太複雜的代碼。

添加Http的支持

websocket 協議作為 http 協議的一種升級,最好麽我們先順手添加一下對 Http 協議的支持。首先我們寫一個 HTTPRequestHandler,話不多說,代碼如下

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.DefaultHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpResponseStatus;

/**
 * @author Sean Wu
 */
public class HTTPRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest> {

    protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest msg) throws Exception {

        // 創建要返回的內容
        byte[] retBytes = "this a simple http response".getBytes();
        ByteBuf byteBuf = Unpooled.copiedBuffer(retBytes);
        // 由於http並不是我們關心的重點,我們就直接返回好了
        DefaultHttpResponse response = new DefaultHttpResponse(msg.protocolVersion(), HttpResponseStatus.OK);
        response.headers().set(HttpHeaderNames.CONTENT_LENGTH, retBytes.length);
        ctx.writeAndFlush(response);
        ctx.writeAndFlush(byteBuf);

    }
}

這個 Handler 對 http 協議做了一個最簡單的支持,就是不管客戶端傳啥都返回一個 this a simple http response。什麼keep-alive,Expect:100-Continue都先不管好了,跟我們這次要講的websocket 並沒有什麼關係的說。然後我們改一下我們上面的 ServiceMain 這個類,在Channel里添加對http的支持。代碼如下。


import com.jiuyan.xisha.websocket.handler.HTTPRequestHandler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
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.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;

/**
 * @author Sean Wu
 */
public class ServiceMain {
    public static void main(String[] args) throws Exception {
        NioEventLoopGroup boss = new NioEventLoopGroup(1);
        NioEventLoopGroup worker = new NioEventLoopGroup();
        ServerBootstrap b = new ServerBootstrap();
        b.group(boss, worker)
                .channel(NioServerSocketChannel.class)
                .handler(new LoggingHandler(LogLevel.DEBUG))
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    protected void initChannel(SocketChannel ch) throws Exception {
                        ChannelPipeline p = ch.pipeline();
                        p.addLast(new HttpServerCodec())
                                .addLast(new HttpObjectAggregator(65536))
                                .addLast(new HTTPRequestHandler());
                    }
                });
        ChannelFuture f = b.bind(8866).sync();
        f.channel().closeFuture().sync();
        boss.shutdownGracefully();
        worker.shutdownGracefully();
    }
}

可以看到也非常的簡單,介紹下我們這裡用到的幾個Handler

ChannelHandler 作用
HttpServerCodec 對位元組碼根據http協議進行編碼和解碼,
HttpObjectAggregator 將一個 HttpMessage 和跟隨它的多個 HttpContent 聚合

為單個 FullHttpRequest 或者 FullHttpResponse (取
決於它是被用來處理請求還是響應)。安裝了這個之後,
ChannelPipeline 中的下一個 ChannelHandler 將只會
收到完整的 HTTP 請求或響應
HTTPRequestHandler | 處理 HttpObjectAggregator 送過來的 FullHttpRequest 請求

然後我們運行一下 ServiceMain 然後用瀏覽器訪問一下,正常的話,如圖所示。
正常返回

添加對 websocket 的支持

首先,我們在剛纔的 HTTPRequestHandler 的 channelRead0 方法里添加對 websocket 介面的特殊處理。修改後的代碼如下

/**
 * @author Sean Wu
 */
public class HTTPRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest> {



    protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest msg) throws Exception {

        System.out.println(msg.uri());
        // 如果尾碼為 ws 的請求,則增加引用計數,將他傳給下一個 ChannelInboundHandler
        if ("/ws".equalsIgnoreCase(msg.uri())) {
            ctx.fireChannelRead(msg.retain());
            return;
        }

        // 之前的代碼

    }
}

然後我們要加一個處理 websocket 協議的 handler
根據WebSocket 協議,netty 定義瞭如下六種幀

幀類型 秒速
BinaryWebSocketFrame 充滿了二進位數據流的一個幀,大多是多媒體文件
TextWebSocketFrame 充滿了文本的一個幀
CloseWebSocketFrame 用來關閉websocket的幀
PingWebSocketFrame 用來探活的的一個幀
PongWebSocketFrame 用來表示自己還活著的一個幀

Netty 里提供了一個叫 WebSocketServerProtocolHandler 的類,他會幫你處理 Ping,Pong,Close之類的服務狀態的幀。這裡我們只需要簡單的用下TextWebSocketFramce就好了。

/**
 * @author Sean Wu
 */
public class TextWebSocketFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {


    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt == WebSocketServerProtocolHandler.ServerHandshakeStateEvent.HANDSHAKE_COMPLETE) {
            ctx.writeAndFlush(new TextWebSocketFrame("client " + ctx.channel() + "join"));
        }
        super.userEventTriggered(ctx, evt);
    }

    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
        System.out.println(msg.text());
        ctx.writeAndFlush(new TextWebSocketFrame("hello" + msg.text()));
    }
}

這裡我們的例子非常的簡單,可以說是網上所有 netty-websocket 的例子里最簡單的了。我們只是在收到了客戶端的消息之後列印了一下然後原封不動的加個 hello 返回回去。

再然後,我們要改一下我們之前的 ChannelPipeline。添加對 websocket 的支持。改完之後的代碼如下

        NioEventLoopGroup boss = new NioEventLoopGroup(1);
        NioEventLoopGroup worker = new NioEventLoopGroup();
        ServerBootstrap b = new ServerBootstrap();
        b.group(boss, worker)
                .channel(NioServerSocketChannel.class)
                .handler(new LoggingHandler(LogLevel.DEBUG))
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    protected void initChannel(SocketChannel ch) throws Exception {
                        ChannelPipeline p = ch.pipeline();
                        p.addLast(new HttpServerCodec())
                                .addLast(new HttpObjectAggregator(65536))
                                .addLast(new HTTPRequestHandler())
                                .addLast(new WebSocketServerProtocolHandler("/ws"))
                                .addLast(new TextWebSocketFrameHandler());
                    }
                });
        ChannelFuture f = b.bind(8866).sync();
        f.channel().closeFuture().sync();
        boss.shutdownGracefully();
        worker.shutdownGracefully();
    }

運行示例

首先,啟動我們的伺服器。然後打開剛纔的那個頁面(http://127.0.0.1:8866/),打開調試模式(f12)。
然後輸入如下 js 代碼

var ws = new WebSocket("ws://127.0.0.1:8866/ws");
ws.onopen = function(evt) {
    console.log("鏈接建立了 ...");
};
ws.onmessage = function(evt) {
    console.log( "收到了消息: " + evt.data);
};

image
可以看到,很完美。然後我們再試著用 ws.send("xisha")發些消息看。發消息的js代碼和結果如下。
image
我們也可以打開網路面板查看我們的消息內容。
image
可以看到,只有一個鏈接。

總結

還有很多沒講到,恩。。。問題不大。下次有機會再說。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 問題: 由於公司業務擴大,各個子系統陸續遷移和部署在不同的數據源上,這樣方便擴容,但是因此引出了一些問題。 舉個例子:在查詢"訂單"(位於訂單子系統)列表時,同時需要查詢出所關聯的"用戶"(位於賬戶子系統)的姓名,而這時由於數據存儲在不同的數據源上,沒有辦法通過一條連表的sql獲取到全部的數據,而是 ...
  • laravel里所謂的provider服務提供者,其實是對某一類功能進行整合,與做一些使用前的初始化引導工作。laravel里的服務提供者也分為,系統核心服務提供者、與一般系統服務提供者。例如上一篇博文里介紹的,最早在application中進行註冊的event、log、routing這些就是系統的 ...
  • 開篇:做了這麼多年的軟體,第一次使用博客的方式記錄學習過程,之前都是筆記本(都有一摞了),因為之前一直從事的都是.NET的開發工作,對C++知之甚少,但一直想瞭解C++這門鼻祖級的語言,現在終於下定決心、騰出時間,系統的學習一下,因為有了豐富的編程經驗,所以不再記錄安裝編程環境之類的事項直接進行編程 ...
  • 認識進程與線程(python) 一段時間沒有更新博客了,今天和大家講講關於 python 進程和線程的知識點。(個人心得,多多指教!) 階段一:併發與並行的深入理解 ​ 並行一定是併發,但併發不一定是並行。 ​ 並行是相對的,並行是絕對的。 問題一: 電腦是如何執行程式指令的? 問題二: 電腦如 ...
  • 1. fgetss函數php官網的解釋是: (PHP 4, PHP 5, PHP 7) fgetss — 從文件指針中讀取一行並過濾掉 HTML 標記 2. 測試後出現的問題是: 當文本中有一行數據出現 < 左尖括弧字元時,會把下麵的數據全部替換成空白行 ,每行讀取到的數據都是空白 ...
  • 前面提到條件語句的標準格式為“if (條件) { /* 條件成立時的操作代碼 */ } else { /* 條件不成立時的操作代碼 */ }”,乍看之下仿佛只有兩個分支,一個是條件成立時的分支,另一個是條件不成立時的分支。很明顯僅僅兩個分支是不能滿足複雜的業務需求的,自然Java代碼也不會這麼傻瓜到 ...
  • 1、使用IDEA新建工程引導方式,創建消息生產工程 springboot-kafka-producer。 工程POM文件代碼如下: 註釋部分為手動添加的 gson、lombok 依賴。 2、創建消息實體類 3、創建消息生產類 4、編輯資源配置文件 application.properties 5、啟 ...
  • 創建Django項目: 命令行創建:python manage.py startproject 項目名 啟動Django項目: 根目錄下(有manage.py的目錄) python manage.py runserver IP:埠(可直接寫埠或預設在本地的8000埠下) 創建APP: 命令行創 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...