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

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

 

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

 

需求:

    一、使用 netty 實現 websocket 服務器前端

    二、實現 文本信息 的傳遞java

    三、實現 二進制 信息的傳遞,若是是圖片則傳遞到後臺後在前臺直接顯示,非圖片提示。(此處的圖片和非圖片是前端傳遞到後臺的二進制數據而後後端在原封不動的直接返回到前臺)git

    四、只須要考慮 websocket 協議,不用處理http請求web

 

實現細節:

    一、netty中對websocket加強的處理器

          WebSocketServerProtocolHandler bootstrap

              >> 此處理器能夠處理了 webSocket 協議的握手請求處理,以及 ClosePingPong控制幀的處理。對於文本和二進制的數據幀須要咱們本身處理後端

              >> 若是咱們須要攔截 webSocket 協議握手完成後的處理,能夠實現ChannelInboundHandler#userEventTriggered方法,並判斷是不是 HandshakeComplete 事件。數組

              >> 參數:websocketPath 表示 webSocket 的路徑服務器

              >> 參數:maxFrameSize 表示最大的幀,若是上傳大文件時須要將此值調大

    二、文本消息的處理

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

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

    三、二進制消息的處理

               客戶端:向後臺傳遞一個 blob 對象便可,若是咱們須要傳遞額外的信息,那麼能夠在 blob 對象中進行添加,此例中自定義前4個字節表示數據的類型。

               服務端:處理 BinaryWebSocketFrame 幀,並獲取前4個字節,判斷是不是圖片,而後返回 BinaryWebSocketFrame對象給前臺。

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

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

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

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

     五、js中處理二進制數據

           見 webSocket.html 文件中的處理。

 

實現步驟:

一、主要的依賴

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

 

二、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. }  

   注意:

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

          二、對於 webSocket 的握手、Close、Ping、Pong等的處理,由 WebSocketServerProtocolHandler 已經處理了,咱們本身只須要處理 Text和Binary等數據幀的處理。

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

 

三、自定義處理器握手後和文本消息

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. }  

注意:

           一、此處只處理文本消息,所以 SimpleChannelInboundHandler 中的範型寫 TextWebSocketFrame

           二、發送文本消息給客戶端,須要發送 TextWebSocketFrame 對象,不然客戶端接收不到。

           三、處理 握手後 的處理,判斷是不是 HandshakeComplete 事件。

 

四、處理二進制消息

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. }  

   注意:

        一、此處只處理二進制消息,所以泛型中寫 BinaryWebSocketFrame

 

五、客戶端的寫法

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>  

   注意:

          一、此處須要注意發送二進制數據時,若是在二進制數據前面加一個字節數據

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

 

六、實現效果


 

完成代碼:

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

相關文章
相關標籤/搜索