websocket.js 10 KB

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