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

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

WebSocket优势:

  1. WebSocket协议支持双向实时通信
  2. WebSocket连接是持久性的不需要频繁建立和关闭连接,因此可以减少网络开销和资源消耗。
  3. 由于WebSocket连接是持久性的,可以极大减少HTTP协议带来的延迟
  4. 较少的数据开销及较少的服务器负担等等。
    全双工通信:通信的两端可以同时发送和接收数据。

使用WebSocket协议,在前端和后端之间实现全双工通信。在后端,使用Netty 框架和WebSocket API 库来处理WebSocket连接和数据传输。这种模型通常用于实时聊天、实时协作应用、实时数据传输等需要双向通信的应用程序。

后端

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

代码

java 复制代码
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<SocketChannel>() {
                        @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协议的接受与处理操作。

java 复制代码
public class WebSocketHandler extends SimpleChannelInboundHandler<WebSocketFrame> {
    @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原生库)

代码

建立连接:

js 复制代码
    核心步骤:
    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)
    })
js 复制代码
将变量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对象后进行关闭或者发送信息操作.

如图所示:

测试

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

拓展1

需求:要在建立websocket连接时,就传递参数至服务器以实现如加密通信,安全校验等功能。

需求分析:websocket不同于HTTP,它不支持请求头传值,因此无法以类似于

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

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

解决方案:

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

代码(子协议传值)

后端
java 复制代码
public class WebSocketHandler extends SimpleChannelInboundHandler<WebSocketFrame> {
    @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",
            // 您可以解析参数并处理它们。
        }
    }
}
前端
js 复制代码
test: 'helloworld'
this.socket = new WebSocket(`ws://localhost:8083/websocket`, this.test)
输出

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

拓展2

需求:需要在该websocket连接进行通信

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

js 复制代码
socket.send(data)

完整代码(Vue+js)

js 复制代码
<template>
  <div style="margin-left: 600px;margin-top: 300px">
    <el-button @click="ws" style="width: 140px">
      建立WS连接
    </el-button>
    <el-button @click="stopws(socket)" style="width: 150px">
      关闭WS连接
    </el-button>
    <br>
    <div style="margin-top: 10px">
      <el-input v-model="dataArray" style="max-width: 300px"/>
      <br>
      <div style="margin-top: 10px">
        <el-button @click="sendws(socket)" style="width: 300px">
          发送信息
        </el-button>
      </div>
    </div>
  </div>
</template>

<script>
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)
    }
  }

}
</script>

后端代码

java 复制代码
public class WebSocketHandler extends SimpleChannelInboundHandler<WebSocketFrame> {
    @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);
        }
    }
}

输出

相关推荐
NiNg_1_2341 小时前
SpringBoot整合SpringSecurity实现密码加密解密、登录认证退出功能
java·spring boot·后端
Chrikk2 小时前
Go-性能调优实战案例
开发语言·后端·golang
幼儿园老大*2 小时前
Go的环境搭建以及GoLand安装教程
开发语言·经验分享·后端·golang·go
canyuemanyue2 小时前
go语言连续监控事件并回调处理
开发语言·后端·golang
杜杜的man2 小时前
【go从零单排】go语言中的指针
开发语言·后端·golang
customer084 小时前
【开源免费】基于SpringBoot+Vue.JS周边产品销售网站(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·java-ee·开源
Yaml45 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
小码编匠6 小时前
一款 C# 编写的神经网络计算图框架
后端·神经网络·c#