【图+文基于WebSocket协议实现前后端全双工通信+子协议传值(Netty+WebSocket API库)

2023年 10月 8日 67.1k 0

由于在项目开发过程中,需要用到WebSocket协议(建立一个在单次TCP连接上实现全双工通信)实现前后端稳定连接并实现高效通讯。我做了相关实践因此做此分享。
WebSocket优势:

  • WebSocket协议支持双向实时通信
  • WebSocket连接是持久性的不需要频繁建立和关闭连接,因此可以减少网络开销和资源消耗。
  • 由于WebSocket连接是持久性的,可以极大减少HTTP协议带来的延迟
  • 较少的数据开销及较少的服务器负担等等。
    全双工通信:通信的两端可以同时发送和接收数据。
  • 使用WebSocket协议,在前端和后端之间实现全双工通信。在后端,使用Netty框架和WebSocket API库来处理WebSocket连接和数据传输。这种模型通常用于实时聊天、实时协作应用、实时数据传输等需要双向通信的应用程序。

    后端

    这里使用Netty框架以便于实现对应操作。

    代码

    public class WebSocketServer {
    
        public static void start() {
            EventLoopGroup bossGroup = new NioEventLoopGroup();
            EventLoopGroup workerGroup = new NioEventLoopGroup();
            try {
                ServerBootstrap serverBootstrap = new ServerBootstrap();
                serverBootstrap.group(bossGroup, workerGroup)
                        .channel(NioServerSocketChannel.class)
                        .childHandler(new ChannelInitializer() {
                            @Override
                            protected void initChannel(SocketChannel ch){
                                ch.pipeline().addLast(new HttpServerCodec());
                                ch.pipeline().addLast(new ChunkedWriteHandler());
                                ch.pipeline().addLast(new HttpObjectAggregator(65536));
                                ch.pipeline().addLast(new WebSocketServerProtocolHandler("/websocket"));
                                ch.pipeline().addLast(new WebSocketHandler());
    //                            ch.pipeline().addLast(new LoggingHandler(LogLevel.WARN));
                            }
                        });
                ChannelFuture channelFuture = serverBootstrap.bind(8083).sync();
                channelFuture.channel().closeFuture().sync();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                bossGroup.shutdownGracefully();
                workerGroup.shutdownGracefully();
            }
        }
    }
    

    代码解析: 没学过netty不要被吓到了,这段代码说简单点就是实现了对 localhost:8083 端口进行WebSocket协议的监听,同时指定了websocket路径为/websocket。

    完成监听之后,我们需要在对应的Handler中实现对websocket协议的接受与处理操作。

    public class WebSocketHandler extends SimpleChannelInboundHandler {
        @Override
        protected void channelRead0(ChannelHandlerContext channelHandlerContext, WebSocketFrame frame) throws Exception {
            //如果接受的是文本信息
            if (frame instanceof TextWebSocketFrame) {
                // 接收前端发送的文本消息
                String message = ((TextWebSocketFrame) frame).text();
                if(message==null){
                    throw new BusinessException(ExceptionInfo.PARAMS_ERROR,"失败,未接收参数!");
                }
            }
        }
    }
    

    完成后等待websocket的连接。

    前端

    这里采用WebSocket API库(HTML5原生库)

    代码

    建立连接:

        核心步骤:
        this.socket = new WebSocket(`ws://localhost:8083/websocket`)
        // 处理连接成功事件
        const socket = this.socket
        事件监听器:
        socket.addEventListener('message', (event) => {
          console.log('收到服务器的消息:', event.data)
          this.message = event.data
        })
        // 处理连接关闭事件
        socket.addEventListener('close', (event) => {
          console.log('WebSocket 连接已关闭')
        })
        socket.addEventListener('open', (event) => {
          console.log('WebSocket 连接已建立')
        })
    
        // 处理连接错误事件
        socket.addEventListener('error', (event) => {
          console.error('WebSocket 连接错误:', event)
        })
    
    
    将变量socket关闭或发送信息
    stopws(socket) {
      socket.close()
    },
    sendws(socket) {
      socket.send("hello world")
    }
    

    URL解析(ws://localhost:8083/websocket ):ws://localhost:8083/这里ws代表websocket协议,localhost:8083 代表对应的websocket服务器监听的地址及端口, /websocket 代表请求的路径

    代码解释:在这里先创建了一个websocket对象并尝试连接对应的端口路径,连接完成后将该socket返回给变量以便后续操作。
    stopws(socket)和sendws(socket)方法分别是获取刚刚获得的socket对象后进行关闭或者发送信息操作.

    如图所示:

    图片.png

    测试

    可以看到控制台已经收到了websocket建立的消息
    图片.png

    拓展1

    需求:要在建立websocket连接时,就传递参数至服务器以实现如加密通信,安全校验等功能。
    需求分析:websocket不同于HTTP,它不支持请求头传值,因此无法以类似于

    request.setHeader('x','x')

    的方式在建立连接时就传递参数,因此websocket也不建议在此时传值(这与websocket协议设计有关)。
    解决方案:

  • 可以直接类似于http的方式传值:ws://localhost:8083/websocket?test=1 较为简单,在此不讨论
  • 可以采用类似子协议传值的方式进行参数传递,及在建立连接时,加上对应的参数。
  • 代码(子协议传值)

    后端
    public class WebSocketHandler extends SimpleChannelInboundHandler {
        @Override
        public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
            if (evt instanceof WebSocketServerProtocolHandler.HandshakeComplete) {
                WebSocketServerProtocolHandler.HandshakeComplete handshake = (WebSocketServerProtocolHandler.HandshakeComplete) evt;
    
                // 获取WebSocket连接的子协议
                String subprotocol = handshake.requestHeaders().get("Sec-WebSocket-Protocol");
                System.out.println("WebSocket子协议: " + subprotocol);
    
                // 解析子协议中的参数,这部分逻辑根据子协议的格式进行自定义
                // 例如,如果子协议是 "your-subprotocol:param1=value1,param2=value2",
                // 您可以解析参数并处理它们。
            }
        }
    }
    
    前端
    test: 'helloworld'
    this.socket = new WebSocket(`ws://localhost:8083/websocket`, this.test)
    
    输出

    图片.png
    避坑:子协议不能携带一些特定字符(包括但不限于":",中文等)

    拓展2

    需求:需要在该websocket连接进行通信
    需求分析: 我们已经建立了websocket连接并拿到了socket对象,此时我们只需要利用代码:

    socket.send(data)
    

    完整代码(Vue+js)

    
      
        
          建立WS连接
        
        
          关闭WS连接
        
        
        
          
          
          
            
              发送信息
            
          
        
      
    
    
    
    export default {
      data() {
        return {
          message: '',
          socket: '',
          // token: 'JWTToken-sx',
          // data: {
          //   userId: '123',
          //   token: this.token
          // },
          dataArray: 'protocol: ,operation: [],deviceId:',
          test: 'helloworld'
        }
      },
      methods: {
        ws() {
          this.socket = new WebSocket(`ws://localhost:8083/websocket`, this.test)
          // 处理连接成功事件
          const socket = this.socket
          // 处理消息接收事件
          // socket.addEventListener('beforeSend', function(event) {
          //   event.target.setRequestHeader('Authorization', 'Bearer ' + this.token)
          //   event.target.setRequestHeader('Custom-Header', 'value')
          // })
          socket.addEventListener('message', (event) => {
            console.log('收到服务器的消息:', event.data)
            this.message = event.data
          })
          // 处理连接关闭事件
          socket.addEventListener('close', (event) => {
            console.log('WebSocket 连接已关闭')
          })
          socket.addEventListener('open', (event) => {
            console.log('WebSocket 连接已建立')
          })
    
          // 处理连接错误事件
          socket.addEventListener('error', (event) => {
            console.error('WebSocket 连接错误:', event)
          })
        },
        stopws(socket) {
          socket.close()
        },
        sendws(socket) {
          socket.send(this.dataArray)
        }
      }
    
    }
    
    

    图片.png
    后端代码

    public class WebSocketHandler extends SimpleChannelInboundHandler {
        @Override
        public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
            if (evt instanceof WebSocketServerProtocolHandler.HandshakeComplete) {
                WebSocketServerProtocolHandler.HandshakeComplete handshake = (WebSocketServerProtocolHandler.HandshakeComplete) evt;
                // 获取WebSocket连接的子协议
                String subprotocol = handshake.requestHeaders().get("Sec-WebSocket-Protocol");
                System.out.println("WebSocket子协议: " + subprotocol);
                // 解析子协议中的参数,这部分逻辑根据子协议的格式进行自定义
                // 例如,如果子协议是 "your-subprotocol:param1=value1,param2=value2",
                // 你可以解析参数并处理它们。
            }
        }
    
        @Override
        protected void channelRead0(ChannelHandlerContext channelHandlerContext, WebSocketFrame frame) throws Exception {
            //如果接受文本信息
            if (frame instanceof TextWebSocketFrame) {
                // 接收前端发送的文本消息
                String message = ((TextWebSocketFrame) frame).text();
                System.out.println(message);
                // 使用正则表达式(根据约定的信息格式)提取对应编号、操作和协议信息
                String cameraId = MessageParserUtil.extractCameraId(message);
                String operation = MessageParserUtil.extractOperation(message);
                String protocol = MessageParserUtil.extractProtocol(message);
                if(operation==null){
                    throw new BusinessException(ExceptionInfo.PARAMS_ERROR,"操作失败,未接收该操作参数!");
                }
                System.out.println("cameraId:"+cameraId);
                System.out.println("operation:"+operation);
                System.out.println("protocol:"+protocol);
            }
        }
    }
    

    输出

    图片.png

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论