万人都要将火熄灭,我一人独将此火高高举起。——海子

uniapp-x

utils/device.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* 获取设备唯一标识符
* @returns {string} 唯一设备标识符
*/
export function getUniqueDeviceId(): string {
let deviceId: string | null = uni.getStorageSync('deviceId'); // 从本地缓存获取
if (!deviceId) {
// 如果不存在,生成新的 UUID
deviceId = generateUUID();
uni.setStorageSync('deviceId', deviceId); // 存储到本地
}
console.log('设备唯一标识: ', deviceId);
return deviceId;
}

/**
* 生成随机 UUID
* @returns {string} UUID 字符串
*/
export function generateUUID(): string {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(
/[xy]/g,
(substring: string, ...args: any[]): string => {
const random: number = (Math.random() * 16) | 0;
const value: number = substring === 'x' ? random : (random & 0x3) | 0x8;
return value.toString(16);
}
);
}

pages/index/index.uvue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
<template>
<page-head title="websocket通讯示例"></page-head>
<view class="uni-padding-wrap">
<view class="uni-btn-v">
<text class="websocket-msg">{{ showMsg }}</text>
<button class="uni-btn-v" type="primary" @click="connect">
连接websocket服务
</button>
<button class="uni-btn-v" v-show="connected" type="primary" @click="send">
发送一条消息
</button>
<button class="uni-btn-v" type="primary" @click="close">
断开websocket服务
</button>
<text class="websocket-tips">发送消息后会收到一条服务器返回的消息(与发送的消息内容一致)</text>
<button class="uni-btn-v" type="primary" @click="goSocketTask">
跳转 socketTask 示例
</button>
</view>
</view>
</template>

<script>
import { getUniqueDeviceId } from '@/utils/device';
export default {
data() {
return {
connected: false,
connecting: false,
msg: '',
roomId: '',
platform: '',
deviceId: ''
}
},
computed: {
showMsg() : string {
if (this.connected) {
if (this.msg.length > 0) {
return '收到消息:' + this.msg
} else {
return '等待接收消息'
}
} else {
return '尚未连接'
}
},
},
onLoad() {
this.platform = uni.getSystemInfoSync().platform
},
onUnload() {
uni.closeSocket({
code: 1000,
reason: 'close reason from client',
success: (res : any) => {
console.log('uni.closeSocket success', res)
},
fail: (err : any) => {
console.log('uni.closeSocket fail', err)
},
} as CloseSocketOptions)
uni.hideLoading()
},
methods: {
connect() {
if (this.connected || this.connecting) {
uni.showModal({
content: '正在连接或者已经连接,请勿重复连接',
showCancel: false,
})
return
}
this.connecting = true
uni.showLoading({
title: '连接中...',
})
this.deviceId = getUniqueDeviceId();
const url = `ws://192.168.1.27:8080/ws?deviceId=${encodeURIComponent(this.deviceId)}`;
setTimeout(() => {
if (this.connected) {
return
}
this.connecting = false
uni.hideLoading();
uni.showToast({
icon: 'none',
title: '连接超时',
})
}, 10000)
uni.connectSocket({
url,
header: null,
protocols: null,
success: (res : any) => {
// 这里是接口调用成功的回调,不是连接成功的回调,请注意
console.log('uni.connectSocket success', { url, res })
},
fail: (err : any) => {
// 这里是接口调用失败的回调,不是连接失败的回调,请注意
console.log('uni.connectSocket fail', err)
},
})
uni.onSocketOpen((res) => {
this.connecting = false
this.connected = true
uni.hideLoading()

uni.showToast({
icon: 'none',
title: '连接成功',
})
console.log('onOpen', res)
})
uni.onSocketError((err) => {
this.connecting = false
this.connected = false
uni.hideLoading()

uni.showModal({
content: '连接失败,可能是websocket服务不可用,请稍后再试',
showCancel: false,
})
console.log('onError', err)
})
uni.onSocketMessage((res) => {
this.msg = res.data as string
console.log('onMessage', res)
})
uni.onSocketClose((res) => {
this.connected = false
this.msg = ''
console.log('onClose', res)
})
},
send() {
uni.sendSocketMessage({
data: JSON.stringify({ deviceId: this.deviceId }),
success: (res : any) => {
console.log(res)
},
fail: (err : any) => {
console.log(err)
},
} as SendSocketMessageOptions)
},
close() {
uni.closeSocket({
code: 1000,
reason: 'close reason from client',
success: (res : any) => {
console.log('uni.closeSocket success', res)
},
fail: (err : any) => {
console.log('uni.closeSocket fail', err)
},
} as CloseSocketOptions)
},
goSocketTask() {
uni.navigateTo({
url: '/pages/API/websocket/socketTask',
})
}
},
}
</script>

<style>
.uni-btn-v {
padding: 5px 0;
}

.uni-btn-v {
margin: 10px 0;
}

.websocket-msg {
padding: 40px 0px;
text-align: center;
font-size: 14px;
line-height: 40px;
color: #666666;
}

.websocket-tips {
padding: 40px 0px;
text-align: center;
font-size: 14px;
line-height: 24px;
color: #666666;
}
</style>

springboot

interceptor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
package com.ruben.videochatserver.rubenvideochatserver.interceptor;

import org.dromara.hutool.core.lang.Opt;
import org.dromara.hutool.core.net.url.UrlQueryUtil;
import org.dromara.hutool.core.text.StrUtil;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;

import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.Map;

/**
* WebSocket 拦截器
*/
@Component
public class WebSocketInterceptor implements HandshakeInterceptor {

/**
* 在握手之前拦截连接请求
*
* @param request ServerHttpRequest 对象
* @param response ServerHttpResponse 对象
* @param wsHandler WebSocketHandler 对象
* @param attributes 存储 WebSocket 会话属性的 Map
* @return 是否允许握手
* @throws Exception 如果发生异常
*/
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
// 获取请求的 URI
URI uri = request.getURI();
String query = uri.getQuery();

if (StrUtil.isNotBlank(query)) {
// 使用 Hutool 工具解析查询参数
var queryParams = UrlQueryUtil.decodeQueryList(query, StandardCharsets.UTF_8);

String deviceId = Opt.ofEmptyAble(queryParams.get("deviceId")).map(l -> l.get(0)).get();
if (StrUtil.isNotBlank(deviceId)) {
attributes.put("deviceId", deviceId);
return true; // 允许握手
}
}

// 如果没有 deviceId 参数,拒绝握手
return false;
}

/**
* 握手完成后的回调方法
*
* @param request ServerHttpRequest 对象
* @param response ServerHttpResponse 对象
* @param wsHandler WebSocketHandler 对象
* @param exception 异常信息
*/
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Exception exception) {
// 可在此处进行日志记录或其他处理
}
}

handler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package com.ruben.videochatserverrubenwvideochatserver.handler;

import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
* 设备 WebSocket 处理器
*/
@Component
public class DeviceWebSocketHandler extends TextWebSocketHandler {

/**
* 设备ID到会话的映射关系
*/
private final Map<String, WebSocketSession> deviceSessionMap = new ConcurrentHashMap<>();

@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
String deviceId = (String) session.getAttributes().get("deviceId");
if (deviceId == null || deviceId.isEmpty()) {
session.close(CloseStatus.NOT_ACCEPTABLE.withReason("Missing deviceId"));
return;
}
deviceSessionMap.put(deviceId, session);
session.sendMessage(new TextMessage("连接成功,设备ID: " + deviceId));
}

@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
String payload = message.getPayload();
session.sendMessage(new TextMessage("收到消息: " + payload));
}

@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
deviceSessionMap.entrySet().removeIf(entry -> entry.getValue().equals(session));
}

/**
* 定向发送消息到指定设备
*
* @param deviceId 设备ID
* @param message 消息内容
*/
public void sendMessageToDevice(String deviceId, String message) {
WebSocketSession session = deviceSessionMap.get(deviceId);
if (session != null && session.isOpen()) {
try {
session.sendMessage(new TextMessage(message));
} catch (Exception e) {
throw new RuntimeException("发送消息失败: " + e.getMessage(), e);
}
} else {
throw new IllegalStateException("设备未连接或连接已关闭: " + deviceId);
}
}
}

config

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package com.ruben.videochatserverrubenwvideochatserver.config;

import corubensw.videochatservrubensswvideochatserver.handler.DeviceWebSocketHandler;
import rubenruben.videochatseruben.rubenvideochatserver.interceptor.WebSocketInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

/**
* WebSocket 配置类
*/
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

private final DeviceWebSocketHandler deviceWebSocketHandler;
private final WebSocketInterceptor webSocketInterceptor;

public WebSocketConfig(DeviceWebSocketHandler deviceWebSocketHandler,
WebSocketInterceptor webSocketInterceptor) {
this.deviceWebSocketHandler = deviceWebSocketHandler;
this.webSocketInterceptor = webSocketInterceptor;
}

@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(deviceWebSocketHandler, "/ws")
.setAllowedOrigins("*")
.addInterceptors(webSocketInterceptor);
}
}

真机运行日志输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
[广告] 19:05:01.089 uni-cdn,比主流云厂商便宜30%,更具性价比!详情
19:05:01.092 项目 ruben-hexiaoshi 开始编译
19:05:02.200 请注意运行模式下,因日志输出、sourcemap 以及未压缩源码等原因,性能和包体积,均不及发行模式。
19:05:02.200 编译器版本:4.36(uni-app x)
19:05:02.200 正在编译中...
19:05:06.845 项目rubenw-hexiaoshi 编译成功。
19:05:08.209 ready in 6526ms.
19:05:08.284 正在建立手机连接...
19:05:08.394 正在安装手机端uni-app x调试基座...
19:05:12.943 安装uni-app x调试基座完成
19:05:12.943 正在同步手机端程序文件...
19:05:15.245 同步手机端程序文件完成
19:05:15.246 联机调试并非打包,调试基座 uni-app x 是默认的测试包,权限、图标都不可自定义。只有在点菜单"发行-发行为原生安装包"时才能自定义这些设置
19:05:15.337 项目rubensw-hexiaoshi] 已启动。请点击手机/模拟器的运行基座App(uni-app x)查看效果。如应用未更新,请在手机上杀掉基座进程重启。
19:05:16.400 App Launch at App.uvue:5
19:05:16.401 App Show at App.uvue:8
19:05:16.401 [Vue warn]: Failed to resolve component: page-head
19:05:16.401 If this is a native custom element, make sure to exclude it from component resolution via compilerOptions.isCustomElement.
19:05:16.401 at <Index __pageId=1 __pagePath="pages/index/index" __pageQuery= ... >
19:05:18.424 设备唯一标识: , f1c7499e-cfcb-4772-ae27-6dc44bb0cedf at utils/device.ts:12
19:05:18.424 uni.connectSocket success, [Object] {"url":"ws://192.168.1.27:8080/ws?deviceId=f1c7499e-cfcb-4772-ae27-6dc44bb0cedf","res":{"er...} at pages/index/index.uvue:97
19:05:20.441 onOpen, [Object] {"header":{"Upgrade":"websocket","Date":"Tue, 03 Dec 2024 11:05:20 GMT","Sec-WebSocket-Acce...} at pages/index/index.uvue:113
19:05:20.441 onMessage, [Object] {"data":"连接成功,设备ID: f1c7499e-cfcb-4772-ae27-6dc44bb0cedf"} at pages/index/index.uvue:128
19:05:20.441 [Vue warn]: Failed to resolve component: page-head
19:05:20.441 If this is a native custom element, make sure to exclude it from component resolution via compilerOptions.isCustomElement.
19:05:20.441 at <Index __pageId=1 __pagePath="pages/index/index" __pageQuery= ... >
19:05:24.472 [Object] {"errMsg":"sendSocketMessage:ok"} at pages/index/index.uvue:140
19:05:24.473 onMessage, [Object] {"data":"收到消息: {\"deviceId\":\"f1c7499e-cfcb-4772-ae27-6dc44bb0cedf\"}"} at pages/index/index.uvue:128
19:05:24.473 [Vue warn]: Failed to resolve component: page-head
19:05:24.473 If this is a native custom element, make sure to exclude it from component resolution via compilerOptions.isCustomElement.
19:05:24.473 at <Index __pageId=1 __pagePath="pages/index/index" __pageQuery= ... >
19:05:27.500 [Object] {"errMsg":"sendSocketMessage:ok"} at pages/index/index.uvue:140
19:05:27.500 onMessage, [Object] {"data":"收到消息: {\"deviceId\":\"f1c7499e-cfcb-4772-ae27-6dc44bb0cedf\"}"} at pages/index/index.uvue:128
19:05:29.522 [Object] {"errMsg":"sendSocketMessage:ok"} at pages/index/index.uvue:140
19:05:30.541 onMessage, [Object] {"data":"收到消息: {\"deviceId\":\"f1c7499e-cfcb-4772-ae27-6dc44bb0cedf\"}"} at pages/index/index.uvue:128
19:07:32.307 App Hide at App.uvue:11