netty實現websocket發送文本和二進制數據


原文:https://huan1993.iteye.com/blog/2433552

 

  最近在學習netty相關的知識,看到netty可以實現 websoket,因此記錄一下在netty中實現websocket的步驟,主要實現傳遞文本消息傳遞二進制消息,傳遞二進制消息由於需要傳遞額外信息,因此使用自定義消息協議。

 

需求:

    1、使用 netty 實現 websocket 服務器

    2、實現 文本信息 的傳遞

    3、實現 二進制 信息的傳遞,如果是圖片則傳遞到后台后在前台直接顯示,非圖片提示。(此處的圖片和非圖片是前端傳遞到后台的二進制數據然后后端在原封不動的直接返回到前台)

    4、只需要考慮 websocket 協議,不用處理http請求

 

實現細節:

    1、netty中對websocket增強的處理器

          WebSocketServerProtocolHandler 

              >> 此處理器可以處理了 webSocket 協議的握手請求處理,以及 ClosePingPong控制幀的處理。對於文本和二進制的數據幀需要我們自己處理

              >> 如果我們需要攔截 webSocket 協議握手完成后的處理,可以實現ChannelInboundHandler#userEventTriggered方法,並判斷是否是 HandshakeComplete 事件。

              >> 參數:websocketPath 表示 webSocket 的路徑

              >> 參數:maxFrameSize 表示最大的幀,如果上傳大文件時需要將此值調大

    2、文本消息的處理

               客戶端: 直接發送一個字符串即可

               服務端: 服務端給客戶端響應文本數據,需要返回  TextWebSocketFrame 對象,否則客戶端接收不到。

    3、二進制消息的處理

               客戶端:向后台傳遞一個 blob 對象即可,如果我們需要傳遞額外的信息,那么可以在 blob 對象中進行添加,此例中自定義前4個字節表示數據的類型。

               服務端:處理 BinaryWebSocketFrame 幀,並獲取前4個字節,判斷是否是圖片,然后返回 BinaryWebSocketFrame對象給前台。

    4、針對二進制消息的自定義協議如下:(此處實現比較簡單)

          前四個字節表示文件類型,后面的字節表示具體的數據。

          在java中一個int是4個字節,在js中使用Int32表示

          此協議主要是判斷前端是否傳遞的是 圖片,如果是圖片就直接傳遞到后台,然后后台在返回二進制數據到前台直接顯示這個圖片。非圖片不用處理。

     5、js中處理二進制數據

           見 webSocket.html 文件中的處理。

 

實現步驟:

1、主要的依賴

Java代碼   收藏代碼
  1. <dependency>  
  2.       <groupId>io.netty</groupId>  
  3.       <artifactId>netty-all</artifactId>  
  4.       <version>4.1.31.Final</version>  
  5. </dependency>  

 

2、webSocket服務端編寫

Java代碼   收藏代碼
  1. @Slf4j  
  2. public class WebSocketServer {  
  3.   
  4.     public static void main(String[] args) throws InterruptedException {  
  5.         EventLoopGroup bossGroup = new NioEventLoopGroup();  
  6.         EventLoopGroup workGroup = new NioEventLoopGroup();  
  7.         try {  
  8.             ServerBootstrap bootstrap = new ServerBootstrap();  
  9.             bootstrap.group(bossGroup, workGroup)  
  10.                     .option(ChannelOption.SO_BACKLOG, 128)  
  11.                     .childOption(ChannelOption.TCP_NODELAY, true)  
  12.                     .childOption(ChannelOption.SO_KEEPALIVE, true)  
  13.                     .handler(new LoggingHandler(LogLevel.TRACE))  
  14.                     .channel(NioServerSocketChannel.class)  
  15.                     .childHandler(new ChannelInitializer<SocketChannel>() {  
  16.                         @Override  
  17.                         protected void initChannel(SocketChannel ch) throws Exception {  
  18.                             ch.pipeline()  
  19.                                     .addLast(new LoggingHandler(LogLevel.TRACE))  
  20.                                     // HttpRequestDecoder和HttpResponseEncoder的一個組合,針對http協議進行編解碼  
  21.                                     .addLast(new HttpServerCodec())  
  22.                                     // 分塊向客戶端寫數據,防止發送大文件時導致內存溢出, channel.write(new ChunkedFile(new File("bigFile.mkv")))  
  23.                                     .addLast(new ChunkedWriteHandler())  
  24.                                     // 將HttpMessage和HttpContents聚合到一個完成的 FullHttpRequest或FullHttpResponse中,具體是FullHttpRequest對象還是FullHttpResponse對象取決於是請求還是響應  
  25.                                     // 需要放到HttpServerCodec這個處理器后面  
  26.                                     .addLast(new HttpObjectAggregator(10240))  
  27.                                     // webSocket 數據壓縮擴展,當添加這個的時候WebSocketServerProtocolHandler的第三個參數需要設置成true  
  28.                                     .addLast(new WebSocketServerCompressionHandler())  
  29.                                     // 服務器端向外暴露的 web socket 端點,當客戶端傳遞比較大的對象時,maxFrameSize參數的值需要調大  
  30.                                     .addLast(new WebSocketServerProtocolHandler("/chat", null, true, 10485760))  
  31.                                     // 自定義處理器 - 處理 web socket 文本消息  
  32.                                     .addLast(new TextWebSocketHandler())  
  33.                                     // 自定義處理器 - 處理 web socket 二進制消息  
  34.                                     .addLast(new BinaryWebSocketFrameHandler());  
  35.                         }  
  36.                     });  
  37.             ChannelFuture channelFuture = bootstrap.bind(9898).sync();  
  38.             log.info("webSocket server listen on port : [{}]", 9898);  
  39.             channelFuture.channel().closeFuture().sync();  
  40.         } finally {  
  41.             bossGroup.shutdownGracefully();  
  42.             workGroup.shutdownGracefully();  
  43.         }  
  44.     }  
  45. }  

   注意:

          1、看一下上方依次引入了哪些處理器

          2、對於 webSocket 的握手、Close、Ping、Pong等的處理,由 WebSocketServerProtocolHandler 已經處理了,我們自己只需要處理 Text和Binary等數據幀的處理。

          3、對於傳遞比較大的文件,需要修改 maxFrameSize 參數。

 

3、自定義處理器握手后和文本消息

Java代碼   收藏代碼
 
  1. @Slf4j  
  2. public class TextWebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {  
  3.   
  4.     @Override  
  5.     protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) {  
  6.         log.info("接收到客戶端的消息:[{}]", msg.text());  
  7.         // 如果是向客戶端發送文本消息,則需要發送 TextWebSocketFrame 消息  
  8.         InetSocketAddress inetSocketAddress = (InetSocketAddress) ctx.channel().remoteAddress();  
  9.         String ip = inetSocketAddress.getHostName();  
  10.         String txtMsg = "[" + ip + "][" + LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")) + "] ==> " + msg.text();  
  11.         ctx.channel().writeAndFlush(new TextWebSocketFrame(txtMsg));  
  12.     }  
  13.   
  14.     @Override  
  15.     public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {  
  16.         ctx.close();  
  17.         log.error("服務器發生了異常:", cause);  
  18.     }  
  19.   
  20.     @Override  
  21.     public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {  
  22.         if (evt instanceof WebSocketServerProtocolHandler.HandshakeComplete) {  
  23.             log.info("web socket 握手成功。");  
  24.             WebSocketServerProtocolHandler.HandshakeComplete handshakeComplete = (WebSocketServerProtocolHandler.HandshakeComplete) evt;  
  25.             String requestUri = handshakeComplete.requestUri();  
  26.             log.info("requestUri:[{}]", requestUri);  
  27.             String subproTocol = handshakeComplete.selectedSubprotocol();  
  28.             log.info("subproTocol:[{}]", subproTocol);  
  29.             handshakeComplete.requestHeaders().forEach(entry -> log.info("header key:[{}] value:[{}]", entry.getKey(), entry.getValue()));  
  30.         } else {  
  31.             super.userEventTriggered(ctx, evt);  
  32.         }  
  33.     }  
  34. }  

注意:

           1、此處只處理文本消息,因此 SimpleChannelInboundHandler 中的范型寫 TextWebSocketFrame

           2、發送文本消息給客戶端,需要發送 TextWebSocketFrame 對象,否則客戶端接收不到。

           3、處理 握手后 的處理,判斷是否是 HandshakeComplete 事件。

 

4、處理二進制消息

Java代碼   收藏代碼
  1. @Slf4j  
  2. public class BinaryWebSocketFrameHandler extends SimpleChannelInboundHandler<BinaryWebSocketFrame> {  
  3.   
  4.     @Override  
  5.     protected void channelRead0(ChannelHandlerContext ctx, BinaryWebSocketFrame msg) throws Exception {  
  6.         log.info("服務器接收到二進制消息.");  
  7.         ByteBuf content = msg.content();  
  8.         content.markReaderIndex();  
  9.         int flag = content.readInt();  
  10.         log.info("標志位:[{}]", flag);  
  11.         content.resetReaderIndex();  
  12.   
  13.         ByteBuf byteBuf = Unpooled.directBuffer(msg.content().capacity());  
  14.         byteBuf.writeBytes(msg.content());  
  15.   
  16.         ctx.writeAndFlush(new BinaryWebSocketFrame(byteBuf));  
  17.     }  
  18. }  

   注意:

        1、此處只處理二進制消息,因此泛型中寫 BinaryWebSocketFrame

 

5、客戶端的寫法

Java代碼   收藏代碼
  1. <!DOCTYPE html>  
  2. <html lang="en">  
  3. <head>  
  4.     <meta charset="UTF-8">  
  5.     <title>web socket 測試</title>  
  6. </head>  
  7. <body>  
  8.   
  9. <div style="width: 600px;height: 400px;">  
  10.     <p>服務器輸出:</p>  
  11.     <div style="border: 1px solid #CCC;height: 300px;overflow: scroll" id="server-msg-container">  
  12.   
  13.     </div>  
  14.     <p>  
  15.         <textarea id="inp-msg" style="height: 50px;width: 500px"></textarea><input type="button" value="發送" id="send"><br/>  
  16.         選擇圖片: <input type="file" id="send-pic">  
  17.     </p>  
  18. </div>  
  19.   
  20. <script type="application/javascript">  
  21.     var ws = new WebSocket("ws://192.168.100.215:9898/chat");  
  22.     ws.onopen = function (ev) {  
  23.   
  24.     };  
  25.     ws.onmessage = function (ev) {  
  26.         console.info("onmessage", ev);  
  27.         var inpMsg = document.getElementById("server-msg-container");  
  28.         if (typeof  ev.data === "string") {  
  29.             inpMsg.innerHTML += ev.data + "<br/>";  
  30.         } else {  
  31.             var result = ev.data;  
  32.             var flagReader = new FileReader();  
  33.             flagReader.readAsArrayBuffer(result.slice(0, 4));  
  34.             flagReader.onload = function (flag) {  
  35.                 if (new DataView(flag.target.result).getInt32(0) === 10) {  
  36.                     var imageReader = new FileReader();  
  37.                     imageReader.readAsDataURL(result.slice(4));  
  38.                     imageReader.onload = function (img) {  
  39.                         var imgHtml = "<img src='" + img.target.result + "' style='width: 100px;height: 100px;'>";  
  40.                         inpMsg.innerHTML += imgHtml.replace("data:application/octet-stream;", "data:image/png;") + "<br />";  
  41.                     }  
  42.                 } else {  
  43.                     alert("后端返回的是非圖片類型數據,無法顯示。");  
  44.                 }  
  45.             }  
  46.         }  
  47.     };  
  48.     ws.onerror = function () {  
  49.         var inpMsg = document.getElementById("server-msg-container");  
  50.         inpMsg.innerHTML += "發生異常" + "<br/>";  
  51.     };  
  52.     ws.onclose = function () {  
  53.         var inpMsg = document.getElementById("server-msg-container");  
  54.         inpMsg.innerHTML += "webSocket 關閉" + "<br/>";  
  55.     };  
  56.   
  57.     // 發送文字消息  
  58.     document.getElementById("send").addEventListener("click", function () {  
  59.         ws.send(document.getElementById("inp-msg").value);  
  60.     }, false);  
  61.   
  62.     // 發送圖片  
  63.     document.querySelector('#send-pic').addEventListener('change', function (ev) {  
  64.         var files = this.files;  
  65.         if (files && files.length) {  
  66.             var file = files[0];  
  67.             var fileType = file.type;  
  68.             // 表示傳遞的是 非圖片  
  69.             var dataType = 20;  
  70.             if (!/^image/.test(fileType)) {  
  71.                 // 表示傳遞的是 圖片  
  72.                 dataType = 10;  
  73.                 return;  
  74.             }  
  75.             var fileReader = new FileReader();  
  76.             fileReader.readAsArrayBuffer(file);  
  77.             fileReader.onload = function (e) {  
  78.                 // 獲取到文件對象  
  79.                 var result = e.target.result;  
  80.                 // 創建一個 4個 字節的數組緩沖區  
  81.                 var arrayBuffer = new ArrayBuffer(4);  
  82.                 var dataView = new DataView(arrayBuffer);  
  83.                 // 從第0個字節開始,寫一個 int 類型的數據(dataType),占4個字節  
  84.                 dataView.setInt32(0, dataType);  
  85.                 // 組裝成 blob 對象  
  86.                 var blob = new Blob([arrayBuffer, result]);  
  87.                 // 發送到 webSocket 服務器端  
  88.                 ws.send(blob);  
  89.             }  
  90.         }  
  91.     }, false);  
  92. </script>  
  93.   
  94. </body>  
  95. </html>  

   注意:

          1、此處需要注意發送二進制數據時,如果在二進制數據前面加一個字節數據

          2、如何處理后端返回的二進制數據。

 

6、實現效果


 

完成代碼:

 代碼如下:https://gitee.com/huan1993/netty-study/tree/master/src/main/java/com/huan/netty/websocket


免責聲明!

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



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