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=" 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; 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=' 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

原文地址:https://www.cnblogs.com/shihaiming/p/11192940.html