u
This commit is contained in:
1 parent
db2208cc92
commit
35951777b0
13 files changed
+225
-151
No files matched your search
@@ -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" />
|
||||
|
||||
Generated
+1
-1
@@ -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;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user