由于在项目开发过程中,需要用到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对象后进行关闭或者发送信息操作.
如图所示:
测试
可以看到控制台已经收到了websocket建立的消息
拓展1
需求:要在建立websocket连接时,就传递参数至服务器以实现如加密通信,安全校验等功能。
需求分析:websocket不同于HTTP,它不支持请求头传值,因此无法以类似于
request.setHeader('x','x')
的方式在建立连接时就传递参数,因此websocket也不建议在此时传值(这与websocket协议设计有关)。
解决方案:
代码(子协议传值)
后端
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)
输出
避坑:子协议不能携带一些特定字符(包括但不限于":",中文等)
拓展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)
}
}
}
后端代码
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);
}
}
}