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="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</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>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
+1 -1
View File
@@ -8,5 +8,5 @@
</list>
</option>
</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>
@@ -1,12 +1,10 @@
package com.link.im.handler;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.link.im.manager.GroupManager;
import com.link.im.manager.UserChannelManager;
import com.link.im.manager.WriteManager;
import com.link.im.model.ChatMessage;
import io.netty.channel.Channel;
import com.link.im.model.ImMessage;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
@@ -15,9 +13,6 @@ import io.netty.handler.timeout.IdleStateEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@ChannelHandler.Sharable
public class ImWebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
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 {
String text = msg.text();
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) {
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",
"private": true,
"version": "0.0.0",
"type": "module",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "vite",
"build": "vite build",
+1 -1
View File
@@ -5,7 +5,7 @@ import { useWebSocket } from './stores/websocketStore'
const websocket = useWebSocket()
onMounted(() => {
websocket.connect('ws://192.168.1.136:8090/ws')
websocket.connect('ws://127.0.0.1:8090/ws')
})
onBeforeUnmount(() => {
@@ -1,6 +1,6 @@
<script setup>
import { Icon } from '@iconify/vue'
import { computed, defineEmits, defineProps } from 'vue'
import { computed } from 'vue'
// 定义 props
const props = defineProps({
@@ -6,58 +6,153 @@ export function useWebSocket() {
const isConnected = ref(false)
const listeners = ref(new Set())
// 定时器
let pingInterval, pongTimeout, connectTimeout
let reconnectAttempts = 0
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 reconnectDelay = 3000
let currentUrl = null
let isReconnecting = false
// 常量
const MAX_RECONNECT_DELAY = 30000
const PING_INTERVAL = 5000
const PONG_TIMEOUT = 10000
const CONNECT_TIMEOUT = 3000
// --------------------
// 私有方法
// --------------------
function cleanup() {
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) {
currentUrl = url
if (isConnected.value)
return
if (socket.value)
disconnect()
const ws = new WebSocket(url)
socket.value = ws
const newSocket = new WebSocket(url)
// 连接超时
connectTimeout = setTimeout(() => !isConnected.value && ws.close(), CONNECT_TIMEOUT)
ws.onopen = () => {
clearTimeout(connectTimeout)
newSocket.onopen = () => {
isConnected.value = true
reconnecting = false
reconnectDelay = 3000
startHeartbeat()
// 关闭重连通知
reconnectNotification?.close()
reconnectNotification = null
isReconnecting = false
clearReconnectNotification()
ElNotification({
title: '成功',
message: 'WebSocket 已连接',
title: '提示',
message: 'WebSocket 连接成功',
type: 'success',
showClose: false,
duration: 2000,
})
console.log('WebSocket 已连接')
reconnectAttempts = 0
startHeartbeat()
}
ws.onmessage = (event) => {
newSocket.onmessage = (event) => {
let message
try {
const message = JSON.parse(event.data)
if (message.type === 'PONG')
return resetPongTimeout()
message = JSON.parse(event.data)
if (message.type === 'PONG') {
handlePong()
return
}
notifyListeners(message)
}
catch (err) {
@@ -66,17 +161,17 @@ export function useWebSocket() {
}
}
ws.onclose = (event) => {
newSocket.onclose = () => {
handleDisconnect()
if (event.code !== 1000)
scheduleReconnect(url)
if (!isReconnecting)
handleReconnect()
}
ws.onerror = (err) => {
newSocket.onerror = (err) => {
console.error('WebSocket错误:', err)
handleDisconnect()
scheduleReconnect(url)
}
socket.value = newSocket
}
function disconnect() {
@@ -86,7 +181,8 @@ export function useWebSocket() {
function sendMessage(type, data) {
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)
}
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())
return {
@@ -397,7 +397,7 @@ import Icon from '@/components/icon.vue'
.im__chat-item {
display: flex;
align-items: center;
height: 56px;
height: 64px;
width: 100%;
padding: 0 10px;
overflow-x: hidden;
@@ -425,12 +425,14 @@ import Icon from '@/components/icon.vue'
width: 100%;
}
.im__chat-item-message {
margin-top: 4px;
margin-top: 8px;
line-height: 1;
}
.im__chat-item-name {
font-size: 14px;
font-weight: 500;
line-height: 1;
}
.im__chat-item-last-time,
.im__chat-item-last-message {
@@ -450,6 +452,7 @@ import Icon from '@/components/icon.vue'
.im__chat-item-last-time {
margin-left: 8px;
line-height: 1;
}
.im__chat-item-unread-count {
@@ -650,7 +653,7 @@ import Icon from '@/components/icon.vue'
min-height: 0;
.im__group-item {
height: 56px;
height: 64px;
padding: 0 10px;
display: flex;
align-items: center;
@@ -667,7 +670,7 @@ import Icon from '@/components/icon.vue'
font-size: 14px;
}
.im__group-item-desc {
margin-top: 6px;
margin-top: 8px;
font-size: 12px;
color: #666;
}