websocket.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  1. /**
  2. * WebSocket聊天管理工具类
  3. * 实现了心跳机制、断线重连、消息队列等功能
  4. */
  5. const DEBUG_WS = false;
  6. class WebSocketManager {
  7. constructor() {
  8. // 心跳相关
  9. this.heartbeatTimer = null;
  10. this.heartbeatInterval = 30000; // 30秒
  11. // 重连相关
  12. this.reconnectTimer = null;
  13. this.reconnectCount = 0;
  14. this.maxReconnectCount = 10;
  15. this.reconnectDelay = 1000; // 初始1秒
  16. // 消息队列(断线时暂存)
  17. this.messageQueue = [];
  18. this.maxQueueSize = 100;
  19. // 回调函数
  20. this.onMessageCallback = null;
  21. this.onConnectCallback = null;
  22. this.onDisconnectCallback = null;
  23. this.onErrorCallback = null;
  24. }
  25. /**
  26. * 连接WebSocket
  27. * @param {Number} userId 用户ID
  28. * @param {String} baseUrl WebSocket服务地址
  29. */
  30. connect(userId, baseUrl = 'ws://localhost:8083') {
  31. if (this.isConnected) {
  32. if (DEBUG_WS) console.log('WebSocket已连接');
  33. return;
  34. }
  35. this.userId = userId;
  36. this.url = `${baseUrl}/ws/chat?userId=${userId}`;
  37. if (DEBUG_WS) console.log('正在连接WebSocket...', this.url);
  38. this.socket = uni.connectSocket({
  39. url: this.url,
  40. success: () => {
  41. if (DEBUG_WS) console.log('WebSocket连接请求已发送');
  42. },
  43. fail: (err) => {
  44. console.error('WebSocket连接失败', err);
  45. this.onError(err);
  46. }
  47. });
  48. // 监听连接打开
  49. uni.onSocketOpen((res) => {
  50. if (DEBUG_WS) console.log('WebSocket连接成功');
  51. this.isConnected = true;
  52. this.reconnectCount = 0;
  53. // 启动心跳
  54. this.startHeartbeat();
  55. // 发送队列中的消息
  56. this.flushMessageQueue();
  57. // 触发连接回调
  58. if (this.onConnectCallback) {
  59. this.onConnectCallback();
  60. }
  61. });
  62. // 监听消息接收
  63. uni.onSocketMessage((res) => {
  64. try {
  65. if (DEBUG_WS) console.log('🔵 收到原始WebSocket消息:', res.data);
  66. const message = JSON.parse(res.data);
  67. if (DEBUG_WS) console.log('🔵 解析后的消息对象:', JSON.stringify(message, null, 2));
  68. this.handleMessage(message);
  69. } catch (e) {
  70. console.error('❌ 解析消息失败', e);
  71. console.error(' 原始数据:', res.data);
  72. }
  73. });
  74. // 监听连接关闭
  75. uni.onSocketClose((res) => {
  76. if (DEBUG_WS) console.log('WebSocket连接关闭', res);
  77. this.isConnected = false;
  78. this.stopHeartbeat();
  79. // 触发断开回调
  80. if (this.onDisconnectCallback) {
  81. this.onDisconnectCallback();
  82. }
  83. // 尝试重连
  84. this.reconnect();
  85. });
  86. // 监听错误
  87. uni.onSocketError((err) => {
  88. console.error('WebSocket错误', err);
  89. this.isConnected = false;
  90. this.onError(err);
  91. });
  92. }
  93. /**
  94. * 处理接收到的消息
  95. */
  96. handleMessage(message) {
  97. if (DEBUG_WS) console.log('收到消息', message);
  98. switch (message.type) {
  99. case 'pong':
  100. // 心跳响应
  101. console.log('收到pong');
  102. break;
  103. case 'chat':
  104. // 聊天消息
  105. if (this.onMessageCallback) {
  106. this.onMessageCallback(message);
  107. }
  108. // 自动发送ACK确认
  109. this.sendAck(message.messageId);
  110. break;
  111. case 'ack':
  112. // 消息确认
  113. if (DEBUG_WS) {
  114. console.log('✅ 收到ACK消息!');
  115. console.log(' messageId:', message.messageId);
  116. console.log(' 完整ACK消息:', JSON.stringify(message));
  117. }
  118. if (this.onMessageCallback) {
  119. console.log(' 正在调用onMessageCallback...');
  120. this.onMessageCallback(message);
  121. } else {
  122. console.warn(' ⚠️ onMessageCallback未设置!');
  123. }
  124. break;
  125. case 'recall':
  126. // 消息撤回
  127. if (this.onMessageCallback) {
  128. this.onMessageCallback(message);
  129. }
  130. break;
  131. case 'typing':
  132. // 正在输入
  133. if (this.onMessageCallback) {
  134. this.onMessageCallback(message);
  135. }
  136. break;
  137. case 'read':
  138. // 已读回执
  139. if (this.onMessageCallback) {
  140. this.onMessageCallback(message);
  141. }
  142. break;
  143. case 'online':
  144. // 上线通知
  145. if (DEBUG_WS) console.log('用户上线', message.fromUserId);
  146. break;
  147. case 'error':
  148. // 错误消息
  149. console.error('服务器错误', message.errorMessage);
  150. uni.showToast({
  151. title: message.errorMessage,
  152. icon: 'none'
  153. });
  154. break;
  155. default:
  156. if (DEBUG_WS) console.log('未知消息类型', message.type);
  157. }
  158. }
  159. /**
  160. * 发送聊天消息
  161. */
  162. sendMessage(toUserId, content, messageType = 1, extraData = {}) {
  163. const message = {
  164. type: 'chat',
  165. messageId: this.generateMessageId(),
  166. fromUserId: this.userId,
  167. toUserId: toUserId,
  168. messageType: messageType,
  169. content: content,
  170. timestamp: Date.now(),
  171. extraData: extraData
  172. };
  173. if (messageType === 2) {
  174. // 图片消息
  175. message.mediaUrl = extraData.mediaUrl;
  176. message.thumbnailUrl = extraData.thumbnailUrl;
  177. } else if (messageType === 3 || messageType === 4) {
  178. // 语音或视频消息
  179. message.mediaUrl = extraData.mediaUrl;
  180. message.duration = extraData.duration;
  181. message.mediaSize = extraData.mediaSize;
  182. }
  183. this.send(message);
  184. return message.messageId;
  185. }
  186. /**
  187. * 发送消息ACK确认
  188. */
  189. sendAck(messageId) {
  190. this.send({
  191. type: 'ack',
  192. messageId: messageId,
  193. timestamp: Date.now()
  194. });
  195. }
  196. /**
  197. * 撤回消息
  198. */
  199. recallMessage(messageId) {
  200. this.send({
  201. type: 'recall',
  202. messageId: messageId,
  203. timestamp: Date.now()
  204. });
  205. }
  206. /**
  207. * 发送正在输入状态
  208. */
  209. sendTyping(toUserId) {
  210. this.send({
  211. type: 'typing',
  212. toUserId: toUserId,
  213. timestamp: Date.now()
  214. });
  215. }
  216. /**
  217. * 发送已读回执
  218. */
  219. sendReadReceipt(toUserId) {
  220. this.send({
  221. type: 'read',
  222. fromUserId: this.userId,
  223. toUserId: toUserId,
  224. timestamp: Date.now()
  225. });
  226. }
  227. /**
  228. * 发送数据到WebSocket
  229. */
  230. send(data) {
  231. console.log('🔷 准备发送WebSocket消息');
  232. console.log(' 消息类型:', data.type);
  233. console.log(' isConnected:', this.isConnected);
  234. console.log(' 完整数据:', JSON.stringify(data, null, 2));
  235. if (!this.isConnected) {
  236. console.log('❌ WebSocket未连接,消息加入队列');
  237. // 加入消息队列
  238. if (this.messageQueue.length < this.maxQueueSize) {
  239. this.messageQueue.push(data);
  240. }
  241. return false;
  242. }
  243. try {
  244. const jsonStr = JSON.stringify(data);
  245. console.log('🔷 JSON字符串长度:', jsonStr.length);
  246. uni.sendSocketMessage({
  247. data: jsonStr,
  248. success: () => {
  249. console.log('✅ WebSocket消息已发送到服务器');
  250. console.log(' 消息类型:', data.type);
  251. console.log(' messageId:', data.messageId || 'N/A');
  252. },
  253. fail: (err) => {
  254. console.error('❌ WebSocket消息发送失败', err);
  255. console.error(' 错误详情:', JSON.stringify(err));
  256. // 发送失败,加入队列
  257. if (this.messageQueue.length < this.maxQueueSize) {
  258. this.messageQueue.push(data);
  259. }
  260. }
  261. });
  262. return true;
  263. } catch (e) {
  264. console.error('❌ 发送消息异常', e);
  265. console.error(' 异常详情:', e.message);
  266. return false;
  267. }
  268. }
  269. /**
  270. * 发送队列中的消息
  271. */
  272. flushMessageQueue() {
  273. if (this.messageQueue.length === 0) {
  274. return;
  275. }
  276. console.log(`发送队列中的 ${this.messageQueue.length} 条消息`);
  277. while (this.messageQueue.length > 0) {
  278. const message = this.messageQueue.shift();
  279. this.send(message);
  280. }
  281. }
  282. /**
  283. * 启动心跳
  284. */
  285. startHeartbeat() {
  286. this.stopHeartbeat();
  287. this.heartbeatTimer = setInterval(() => {
  288. if (this.isConnected) {
  289. this.send({
  290. type: 'ping',
  291. timestamp: Date.now()
  292. });
  293. }
  294. }, this.heartbeatInterval);
  295. }
  296. /**
  297. * 停止心跳
  298. */
  299. stopHeartbeat() {
  300. if (this.heartbeatTimer) {
  301. clearInterval(this.heartbeatTimer);
  302. this.heartbeatTimer = null;
  303. }
  304. }
  305. /**
  306. * 断线重连
  307. */
  308. reconnect() {
  309. if (this.reconnectCount >= this.maxReconnectCount) {
  310. console.log('达到最大重连次数,停止重连');
  311. uni.showToast({
  312. title: '连接失败,请检查网络',
  313. icon: 'none'
  314. });
  315. return;
  316. }
  317. // 指数退避算法
  318. const delay = Math.min(
  319. this.reconnectDelay * Math.pow(2, this.reconnectCount),
  320. 30000 // 最大30秒
  321. );
  322. console.log(`${delay}ms 后进行第 ${this.reconnectCount + 1} 次重连...`);
  323. this.reconnectTimer = setTimeout(() => {
  324. this.reconnectCount++;
  325. this.connect(this.userId, this.url.split('/ws/chat')[0]);
  326. }, delay);
  327. }
  328. /**
  329. * 取消重连
  330. */
  331. cancelReconnect() {
  332. if (this.reconnectTimer) {
  333. clearTimeout(this.reconnectTimer);
  334. this.reconnectTimer = null;
  335. }
  336. }
  337. /**
  338. * 关闭连接
  339. */
  340. close() {
  341. this.cancelReconnect();
  342. this.stopHeartbeat();
  343. if (this.socket) {
  344. uni.closeSocket();
  345. this.socket = null;
  346. }
  347. this.isConnected = false;
  348. }
  349. /**
  350. * 错误处理
  351. */
  352. onError(error) {
  353. if (this.onErrorCallback) {
  354. this.onErrorCallback(error);
  355. }
  356. }
  357. /**
  358. * 生成消息ID
  359. */
  360. generateMessageId() {
  361. return `${this.userId}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
  362. }
  363. /**
  364. * 设置消息回调
  365. */
  366. onMessage(callback) {
  367. this.onMessageCallback = callback;
  368. }
  369. /**
  370. * 设置连接回调
  371. */
  372. onConnect(callback) {
  373. this.onConnectCallback = callback;
  374. }
  375. /**
  376. * 设置断开回调
  377. */
  378. onDisconnect(callback) {
  379. this.onDisconnectCallback = callback;
  380. }
  381. /**
  382. * 设置错误回调
  383. */
  384. onErrorHandler(callback) {
  385. this.onErrorCallback = callback;
  386. }
  387. }
  388. // 导出单例
  389. export default new WebSocketManager();