This commit is contained in:
oneao committed 2025-10-13 22:23:08 +08:00
1 parent db2208cc92
commit 35951777b0
13 files changed
+225 -151

No files matched your search

+5
View File
@@ -21,6 +21,11 @@
<option name="name" value="JBoss Community repository" /> <option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" /> <option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository> </remote-repository>
<remote-repository>
<option name="id" value="aliyun" />
<option name="name" value="aliyun" />
<option name="url" value="http://maven.aliyun.com/nexus/content/groups/public" />
</remote-repository>
<remote-repository> <remote-repository>
<option name="id" value="central" /> <option name="id" value="central" />
<option name="name" value="Central Repository" /> <option name="name" value="Central Repository" />
+1 -1
View File
@@ -8,5 +8,5 @@
</list> </list>
</option> </option>
</component> </component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK" /> <component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="temurin-1.8" project-jdk-type="JavaSDK" />
</project> </project>
@@ -1,12 +1,10 @@
package com.link.im.handler; package com.link.im.handler;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.link.im.manager.GroupManager; import com.link.im.manager.GroupManager;
import com.link.im.manager.UserChannelManager; import com.link.im.manager.UserChannelManager;
import com.link.im.manager.WriteManager; import com.link.im.manager.WriteManager;
import com.link.im.model.ChatMessage; import com.link.im.model.ImMessage;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.SimpleChannelInboundHandler;
@@ -15,9 +13,6 @@ import io.netty.handler.timeout.IdleStateEvent;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@ChannelHandler.Sharable @ChannelHandler.Sharable
public class ImWebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> { public class ImWebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
private static final Logger logger = LoggerFactory.getLogger(ImWebSocketHandler.class); private static final Logger logger = LoggerFactory.getLogger(ImWebSocketHandler.class);
@@ -43,9 +38,9 @@ public class ImWebSocketHandler extends SimpleChannelInboundHandler<TextWebSocke
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception { protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
String text = msg.text(); String text = msg.text();
try { try {
ChatMessage chatMsg = MAPPER.readValue(text, ChatMessage.class); ImMessage imMessage = MAPPER.readValue(text, ImMessage.class);
System.out.println(chatMsg); System.out.println(imMessage);
} catch (Exception e) { } catch (Exception e) {
WriteManager.writeToChannel(ctx.channel(),"JSON格式错误"); WriteManager.writeToChannel(ctx.channel(),"JSON格式错误");
} }
@@ -1,40 +0,0 @@
package com.link.im.model;
import java.util.List;
public class ChatMessage {
private String type;
private String msg;
public ChatMessage() {
}
public ChatMessage(String type, String msg) {
this.type = type;
this.msg = msg;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
@Override
public String toString() {
return "ChatMessage{" +
"type='" + type + '\'' +
", msg='" + msg + '\'' +
'}';
}
}
@@ -0,0 +1,70 @@
package com.link.im.model;
public class ImMessage {
private String type;
private String msg;
private String from;
private String to;
private String content;
private long timestamp;
public ImMessage() {
}
public ImMessage(String type, String content, String msg, String from, String to, long timestamp) {
this.type = type;
this.content = content;
this.msg = msg;
this.from = from;
this.to = to;
this.timestamp = timestamp;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public String getFrom() {
return from;
}
public void setFrom(String from) {
this.from = from;
}
public String getTo() {
return to;
}
public void setTo(String to) {
this.to = to;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public long getTimestamp() {
return timestamp;
}
public void setTimestamp(long timestamp) {
this.timestamp = timestamp;
}
}
+2 -2
View File
@@ -1,8 +1,8 @@
{ {
"name": "frontend", "name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module", "type": "module",
"version": "0.0.0",
"private": true,
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "vite build",
+1 -1
View File
@@ -5,7 +5,7 @@ import { useWebSocket } from './stores/websocketStore'
const websocket = useWebSocket() const websocket = useWebSocket()
onMounted(() => { onMounted(() => {
websocket.connect('ws://192.168.1.136:8090/ws') websocket.connect('ws://127.0.0.1:8090/ws')
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
@@ -1,6 +1,6 @@
<script setup> <script setup>
import { Icon } from '@iconify/vue' import { Icon } from '@iconify/vue'
import { computed, defineEmits, defineProps } from 'vue' import { computed } from 'vue'
// 定义 props // 定义 props
const props = defineProps({ const props = defineProps({
@@ -6,58 +6,153 @@ export function useWebSocket() {
const isConnected = ref(false) const isConnected = ref(false)
const listeners = ref(new Set()) const listeners = ref(new Set())
// 定时器 let reconnectAttempts = 0
let pingInterval, pongTimeout, connectTimeout let pingTimer = null
let pongTimeout = null
const PING_INTERVAL = 10000 // 收到PONG后间隔再发送下一次PING
const PONG_TIMEOUT = 15000 // PING发送后15秒未收到PONG就重连
// 重连状态
let reconnecting = false
let reconnectNotification = null let reconnectNotification = null
let reconnectDelay = 3000 let currentUrl = null
let isReconnecting = false
// 常量 // --------------------
const MAX_RECONNECT_DELAY = 30000 // 私有方法
const PING_INTERVAL = 5000 // --------------------
const PONG_TIMEOUT = 10000 function cleanup() {
const CONNECT_TIMEOUT = 3000 if (pingTimer) {
clearTimeout(pingTimer)
pingTimer = null
}
if (pongTimeout) {
clearTimeout(pongTimeout)
pongTimeout = null
}
}
function handleDisconnect() {
isConnected.value = false
cleanup()
}
function notifyListeners(message) {
listeners.value.forEach(cb => cb(message))
}
// --------------------
// 心跳逻辑(链式)
// --------------------
function sendPing() {
if (!socket.value || socket.value.readyState !== WebSocket.OPEN)
return
try {
sendMessage('PING')
}
catch (err) {
console.error('发送 PING 失败', err)
}
// 设置 PONG 超时
if (pongTimeout)
clearTimeout(pongTimeout)
pongTimeout = setTimeout(() => {
console.warn('未收到PONG,触发重连')
handleReconnect()
}, PONG_TIMEOUT)
}
function handlePong() {
// 收到 PONG,清理超时
if (pongTimeout) {
clearTimeout(pongTimeout)
pongTimeout = null
}
// 等 PING_INTERVAL 后再发送下一次 PING
if (pingTimer)
clearTimeout(pingTimer)
pingTimer = setTimeout(sendPing, PING_INTERVAL)
}
function startHeartbeat() {
sendPing()
}
// --------------------
// 重连逻辑
// --------------------
function handleReconnect() {
if (isReconnecting)
return
isReconnecting = true
cleanup()
isConnected.value = false
socket.value?.close()
reconnectAttempts++
if (reconnectNotification)
reconnectNotification.close()
reconnectNotification = ElNotification({
title: '提示',
message: `WebSocket 连接失败,正在第 ${reconnectAttempts} 次重连...`,
type: 'warning',
duration: 0,
key: 'websocket-reconnect',
})
const delay = Math.min(3000 * 2 ** (reconnectAttempts - 1), 30000)
setTimeout(() => {
isReconnecting = false
connect(currentUrl)
}, delay)
}
function clearReconnectNotification() {
if (reconnectNotification) {
reconnectNotification.close()
reconnectNotification = null
}
}
// --------------------
// 公共方法
// --------------------
function connect(url) { function connect(url) {
currentUrl = url
if (isConnected.value) if (isConnected.value)
return return
if (socket.value) if (socket.value)
disconnect() disconnect()
const ws = new WebSocket(url) const newSocket = new WebSocket(url)
socket.value = ws
// 连接超时 newSocket.onopen = () => {
connectTimeout = setTimeout(() => !isConnected.value && ws.close(), CONNECT_TIMEOUT)
ws.onopen = () => {
clearTimeout(connectTimeout)
isConnected.value = true isConnected.value = true
reconnecting = false isReconnecting = false
reconnectDelay = 3000 clearReconnectNotification()
startHeartbeat()
// 关闭重连通知
reconnectNotification?.close()
reconnectNotification = null
ElNotification({ ElNotification({
title: '成功', title: '提示',
message: 'WebSocket 已连接', message: 'WebSocket 连接成功',
type: 'success', type: 'success',
showClose: false,
duration: 2000, duration: 2000,
}) })
console.log('WebSocket 已连接')
reconnectAttempts = 0
startHeartbeat()
} }
ws.onmessage = (event) => { newSocket.onmessage = (event) => {
let message
try { try {
const message = JSON.parse(event.data) message = JSON.parse(event.data)
if (message.type === 'PONG')
return resetPongTimeout() if (message.type === 'PONG') {
handlePong()
return
}
notifyListeners(message) notifyListeners(message)
} }
catch (err) { catch (err) {
@@ -66,17 +161,17 @@ export function useWebSocket() {
} }
} }
ws.onclose = (event) => { newSocket.onclose = () => {
handleDisconnect() handleDisconnect()
if (event.code !== 1000) if (!isReconnecting)
scheduleReconnect(url) handleReconnect()
} }
ws.onerror = (err) => { newSocket.onerror = (err) => {
console.error('WebSocket错误:', err) console.error('WebSocket错误:', err)
handleDisconnect()
scheduleReconnect(url)
} }
socket.value = newSocket
} }
function disconnect() { function disconnect() {
@@ -86,7 +181,8 @@ export function useWebSocket() {
function sendMessage(type, data) { function sendMessage(type, data) {
if (isConnected.value && socket.value) { if (isConnected.value && socket.value) {
socket.value.send(JSON.stringify({ type, data })) const message = { type, data }
socket.value.send(JSON.stringify(message))
} }
} }
@@ -95,61 +191,6 @@ export function useWebSocket() {
return () => listeners.value.delete(callback) return () => listeners.value.delete(callback)
} }
function notifyListeners(message) {
listeners.value.forEach(cb => cb(message))
}
function startHeartbeat() {
pingInterval = setInterval(() => {
if (socket.value?.readyState === WebSocket.OPEN) {
sendMessage('PING')
pongTimeout = setTimeout(() => {
console.warn('未收到PONG,关闭WebSocket')
socket.value?.close()
}, PONG_TIMEOUT)
}
}, PING_INTERVAL)
}
function resetPongTimeout() {
clearTimeout(pongTimeout)
}
function handleDisconnect() {
isConnected.value = false
cleanup()
}
function scheduleReconnect(url) {
if (reconnecting)
return
reconnecting = true
// 显示一次重连提示
if (!reconnectNotification) {
reconnectNotification = ElNotification({
title: '提示',
message: 'WebSocket 连接断开,正在重连...',
type: 'warning',
showClose: false,
duration: 0,
})
}
console.log(`WebSocket将在${reconnectDelay / 1000}s后尝试重连`)
setTimeout(() => {
connect(url)
reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_DELAY)
reconnecting = false
}, reconnectDelay)
}
function cleanup() {
clearInterval(pingInterval)
clearTimeout(pongTimeout)
clearTimeout(connectTimeout)
}
onScopeDispose(() => disconnect()) onScopeDispose(() => disconnect())
return { return {
@@ -397,7 +397,7 @@ import Icon from '@/components/icon.vue'
.im__chat-item { .im__chat-item {
display: flex; display: flex;
align-items: center; align-items: center;
height: 56px; height: 64px;
width: 100%; width: 100%;
padding: 0 10px; padding: 0 10px;
overflow-x: hidden; overflow-x: hidden;
@@ -425,12 +425,14 @@ import Icon from '@/components/icon.vue'
width: 100%; width: 100%;
} }
.im__chat-item-message { .im__chat-item-message {
margin-top: 4px; margin-top: 8px;
line-height: 1;
} }
.im__chat-item-name { .im__chat-item-name {
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
line-height: 1;
} }
.im__chat-item-last-time, .im__chat-item-last-time,
.im__chat-item-last-message { .im__chat-item-last-message {
@@ -450,6 +452,7 @@ import Icon from '@/components/icon.vue'
.im__chat-item-last-time { .im__chat-item-last-time {
margin-left: 8px; margin-left: 8px;
line-height: 1;
} }
.im__chat-item-unread-count { .im__chat-item-unread-count {
@@ -650,7 +653,7 @@ import Icon from '@/components/icon.vue'
min-height: 0; min-height: 0;
.im__group-item { .im__group-item {
height: 56px; height: 64px;
padding: 0 10px; padding: 0 10px;
display: flex; display: flex;
align-items: center; align-items: center;
@@ -667,7 +670,7 @@ import Icon from '@/components/icon.vue'
font-size: 14px; font-size: 14px;
} }
.im__group-item-desc { .im__group-item-desc {
margin-top: 6px; margin-top: 8px;
font-size: 12px; font-size: 12px;
color: #666; color: #666;
} }