netty实现聊天功能
parent
7e82814906
commit
9e465b1ebc
@ -0,0 +1,67 @@
|
||||
package com.example.venue_reservation_service.domain;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
/**
|
||||
* 系统异常日志表
|
||||
* @TableName venue_exception
|
||||
*/
|
||||
@TableName(value ="venue_exception")
|
||||
@Data
|
||||
public class VeException implements Serializable {
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 异常类型
|
||||
*/
|
||||
private String exceptionType;
|
||||
|
||||
/**
|
||||
* 异常信息
|
||||
*/
|
||||
private String exceptionMessage;
|
||||
|
||||
/**
|
||||
* 异常堆栈
|
||||
*/
|
||||
private String exceptionStack;
|
||||
|
||||
/**
|
||||
* 方法名称
|
||||
*/
|
||||
private String methodName;
|
||||
|
||||
/**
|
||||
* 参数数据
|
||||
*/
|
||||
private String parameterData;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private Integer userId;
|
||||
|
||||
/**
|
||||
* 操作时间
|
||||
*/
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime operationTime;
|
||||
|
||||
@TableField(exist = false)
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
@ -0,0 +1,90 @@
|
||||
package com.example.venue_reservation_service.exception;
|
||||
|
||||
import com.example.venue_reservation_service.domain.VeException;
|
||||
import com.example.venue_reservation_service.service.VeExceptionService;
|
||||
import com.example.venue_reservation_service.vo.Result;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.servlet.HandlerMapping;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
|
||||
@RestControllerAdvice
|
||||
@Slf4j
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
@Autowired
|
||||
private VeExceptionService exceptionService;
|
||||
|
||||
// 文件移除异常处理
|
||||
@ExceptionHandler(MinioRemoveException.class)
|
||||
public Result handleMinioRemoveException(MinioRemoveException e) {
|
||||
log.error("文件移除失败: {}", e.getMessage());
|
||||
recordException(e, "MinioRemoveException");
|
||||
return Result.fail().message("旧头像删除失败,请重试");
|
||||
}
|
||||
|
||||
// 文件校验异常处理
|
||||
@ExceptionHandler(ImageValidateException.class)
|
||||
public Result handleImageValidateException(ImageValidateException e) {
|
||||
String errorMsg = "FILE_SIZE".equals(e.getErrorType())
|
||||
? "文件大小超过250MB限制" : "不支持的文件类型";
|
||||
log.error("文件校验失败: {} - {}", e.getErrorType(), e.getMessage());
|
||||
recordException(e, "ImageValidateException");
|
||||
return Result.fail().message(errorMsg);
|
||||
}
|
||||
|
||||
// 文件上传异常处理
|
||||
@ExceptionHandler(MinioUploadException.class)
|
||||
public Result handleMinioUploadException(MinioUploadException e) {
|
||||
log.error("文件上传失败: {}", e.getMessage());
|
||||
recordException(e, "MinioUploadException");
|
||||
return Result.fail().message("头像上传失败,请稍后重试");
|
||||
}
|
||||
|
||||
// 记录异常到数据库
|
||||
private void recordException(Exception e, String exceptionType) {
|
||||
try {
|
||||
HttpServletRequest request = ((ServletRequestAttributes)
|
||||
RequestContextHolder.currentRequestAttributes()).getRequest();
|
||||
|
||||
HandlerMethod handlerMethod = (HandlerMethod) request.getAttribute(
|
||||
HandlerMapping.BEST_MATCHING_HANDLER_ATTRIBUTE);
|
||||
|
||||
VeException logEntity = new VeException();
|
||||
logEntity.setExceptionType(exceptionType);
|
||||
logEntity.setExceptionMessage(e.getMessage());
|
||||
|
||||
// 获取堆栈信息
|
||||
StringWriter sw = new StringWriter();
|
||||
e.printStackTrace(new PrintWriter(sw));
|
||||
logEntity.setExceptionStack(sw.toString());
|
||||
|
||||
logEntity.setMethodName(handlerMethod.getMethod().getName());
|
||||
|
||||
// 获取参数数据
|
||||
Map<String, String[]> params = request.getParameterMap();
|
||||
logEntity.setParameterData(params.toString());
|
||||
|
||||
// 从请求中获取用户ID
|
||||
String userId = request.getParameter("userId");
|
||||
if (userId != null) {
|
||||
logEntity.setUserId(Integer.parseInt(userId));
|
||||
}
|
||||
|
||||
logEntity.setOperationTime(LocalDateTime.now());
|
||||
|
||||
exceptionService.save(logEntity);
|
||||
} catch (Exception ex) {
|
||||
log.error("记录异常日志失败: {}", ex.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package com.example.venue_reservation_service.exception;
|
||||
|
||||
// 文件校验异常(包含文件过大和非图片文件)
|
||||
public class ImageValidateException extends RuntimeException {
|
||||
private final String errorType;
|
||||
|
||||
public ImageValidateException(String message, String errorType) {
|
||||
super(message);
|
||||
this.errorType = errorType;
|
||||
}
|
||||
|
||||
public String getErrorType() {
|
||||
return errorType;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,8 @@
|
||||
package com.example.venue_reservation_service.exception;
|
||||
|
||||
// 文件移除异常
|
||||
public class MinioRemoveException extends RuntimeException {
|
||||
public MinioRemoveException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
package com.example.venue_reservation_service.exception;
|
||||
|
||||
// 文件上传异常
|
||||
public class MinioUploadException extends RuntimeException {
|
||||
public MinioUploadException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package com.example.venue_reservation_service.mapper;
|
||||
|
||||
import com.example.venue_reservation_service.domain.VeException;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
|
||||
/**
|
||||
* @author 31586
|
||||
* @description 针对表【venue_exception(系统异常日志表)】的数据库操作Mapper
|
||||
* @createDate 2025-06-18 09:39:17
|
||||
* @Entity generator.domain.VeException
|
||||
*/
|
||||
public interface VeExceptionMapper extends BaseMapper<VeException> {
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
@ -0,0 +1,177 @@
|
||||
package com.example.venue_reservation_service.netty;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.SimpleChannelInboundHandler;
|
||||
import io.netty.channel.group.ChannelGroup;
|
||||
import io.netty.channel.group.DefaultChannelGroup;
|
||||
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
|
||||
import io.netty.util.concurrent.GlobalEventExecutor;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class ChatServerHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
|
||||
|
||||
private static final ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
|
||||
private static final Map<String, Channel> userChannelMap = new HashMap<>();
|
||||
private static final Map<Integer, String> roleMap = new HashMap<>();
|
||||
|
||||
static {
|
||||
roleMap.put(0, "普通用户");
|
||||
roleMap.put(1, "普通用户");
|
||||
roleMap.put(2, "普通用户");
|
||||
roleMap.put(3, "系统管理员");
|
||||
roleMap.put(4, "超级管理员");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
|
||||
Channel incoming = ctx.channel();
|
||||
channels.add(incoming);
|
||||
System.out.println("New client connected: " + incoming.remoteAddress());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
|
||||
Channel incoming = ctx.channel();
|
||||
String username = null;
|
||||
|
||||
// 查找并移除用户
|
||||
for (Map.Entry<String, Channel> entry : userChannelMap.entrySet()) {
|
||||
if (entry.getValue().equals(incoming)) {
|
||||
username = entry.getKey();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (username != null) {
|
||||
userChannelMap.remove(username);
|
||||
broadcastMessage("系统通知", username + " 已退出聊天", 3);
|
||||
}
|
||||
|
||||
channels.remove(incoming);
|
||||
System.out.println("Client disconnected: " + incoming.remoteAddress());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame frame) throws Exception {
|
||||
String request = frame.text();
|
||||
Channel incoming = ctx.channel();
|
||||
|
||||
try {
|
||||
JSONObject jsonObject = JSON.parseObject(request);
|
||||
String type = jsonObject.getString("type");
|
||||
|
||||
if ("login".equals(type)) {
|
||||
handleLogin(incoming, jsonObject);
|
||||
} else if ("message".equals(type)) {
|
||||
handleMessage(incoming, jsonObject);
|
||||
} else {
|
||||
sendErrorMessage(incoming, "未知消息类型: " + type);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.err.println("Error processing message: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
sendErrorMessage(incoming, "消息格式错误");
|
||||
}
|
||||
}
|
||||
|
||||
private void handleLogin(Channel incoming, JSONObject jsonObject) {
|
||||
String username = jsonObject.getString("username");
|
||||
int userRole = jsonObject.getIntValue("userRole");
|
||||
|
||||
if (username == null || username.isEmpty()) {
|
||||
sendErrorMessage(incoming, "用户名不能为空");
|
||||
return;
|
||||
}
|
||||
|
||||
if (userChannelMap.containsKey(username)) {
|
||||
sendErrorMessage(incoming, "用户名已被使用");
|
||||
return;
|
||||
}
|
||||
|
||||
userChannelMap.put(username, incoming);
|
||||
String roleName = roleMap.getOrDefault(userRole, "未知角色");
|
||||
broadcastMessage("系统通知", username + " (" + roleName + ") 加入了聊天", 3);
|
||||
System.out.println(username + " logged in with role: " + roleName);
|
||||
}
|
||||
|
||||
private void handleMessage(Channel incoming, JSONObject jsonObject) {
|
||||
String username = jsonObject.getString("username");
|
||||
int userRole = jsonObject.getIntValue("userRole");
|
||||
String content = jsonObject.getString("content");
|
||||
|
||||
if (content == null || content.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!userChannelMap.containsKey(username) || !userChannelMap.get(username).equals(incoming)) {
|
||||
sendErrorMessage(incoming, "用户未登录");
|
||||
return;
|
||||
}
|
||||
|
||||
String roleName = roleMap.getOrDefault(userRole, "未知角色");
|
||||
String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
|
||||
|
||||
JSONObject messageJson = new JSONObject();
|
||||
messageJson.put("type", "message");
|
||||
messageJson.put("sender", username);
|
||||
messageJson.put("senderRole", userRole);
|
||||
messageJson.put("roleName", roleName);
|
||||
messageJson.put("content", content);
|
||||
messageJson.put("timestamp", timestamp);
|
||||
|
||||
broadcastMessage(messageJson.toJSONString());
|
||||
}
|
||||
|
||||
private void broadcastMessage(String message) {
|
||||
for (Channel channel : channels) {
|
||||
channel.writeAndFlush(new TextWebSocketFrame(message));
|
||||
}
|
||||
}
|
||||
|
||||
private void broadcastMessage(String from, String content, int role) {
|
||||
JSONObject messageJson = new JSONObject();
|
||||
messageJson.put("type", "system");
|
||||
messageJson.put("sender", from);
|
||||
messageJson.put("senderRole", role);
|
||||
messageJson.put("message", content);
|
||||
messageJson.put("timestamp", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
|
||||
|
||||
broadcastMessage(messageJson.toJSONString());
|
||||
}
|
||||
|
||||
private void sendErrorMessage(Channel channel, String message) {
|
||||
JSONObject errorJson = new JSONObject();
|
||||
errorJson.put("type", "error");
|
||||
errorJson.put("message", message);
|
||||
channel.writeAndFlush(new TextWebSocketFrame(errorJson.toJSONString()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
|
||||
Channel channel = ctx.channel();
|
||||
String username = null;
|
||||
|
||||
// 查找并移除用户
|
||||
for (Map.Entry<String, Channel> entry : userChannelMap.entrySet()) {
|
||||
if (entry.getValue().equals(channel)) {
|
||||
username = entry.getKey();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (username != null) {
|
||||
userChannelMap.remove(username);
|
||||
broadcastMessage("系统通知", username + " 连接异常断开", 3);
|
||||
}
|
||||
|
||||
cause.printStackTrace();
|
||||
ctx.close();
|
||||
}
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
package com.example.venue_reservation_service.netty;
|
||||
|
||||
import io.netty.bootstrap.ServerBootstrap;
|
||||
import io.netty.channel.ChannelFuture;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
import io.netty.channel.ChannelOption;
|
||||
import io.netty.channel.EventLoopGroup;
|
||||
import io.netty.channel.nio.NioEventLoopGroup;
|
||||
import io.netty.channel.socket.SocketChannel;
|
||||
import io.netty.channel.socket.nio.NioServerSocketChannel;
|
||||
import io.netty.handler.codec.http.HttpObjectAggregator;
|
||||
import io.netty.handler.codec.http.HttpServerCodec;
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
|
||||
import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketServerCompressionHandler;
|
||||
import io.netty.handler.stream.ChunkedWriteHandler;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class NettyChatServer {
|
||||
|
||||
@Value("${server.netty.port}")
|
||||
private int port;
|
||||
|
||||
private EventLoopGroup bossGroup;
|
||||
private EventLoopGroup workerGroup;
|
||||
|
||||
@PostConstruct
|
||||
public void start() throws Exception {
|
||||
bossGroup = new NioEventLoopGroup();
|
||||
workerGroup = new NioEventLoopGroup();
|
||||
|
||||
ServerBootstrap b = new ServerBootstrap();
|
||||
b.group(bossGroup, workerGroup)
|
||||
.channel(NioServerSocketChannel.class)
|
||||
.option(ChannelOption.SO_BACKLOG, 100)
|
||||
.childOption(ChannelOption.SO_KEEPALIVE, true)
|
||||
.childHandler(new ChannelInitializer<SocketChannel>() {
|
||||
@Override
|
||||
public void initChannel(SocketChannel ch) throws Exception {
|
||||
ch.pipeline().addLast(
|
||||
new HttpServerCodec(),
|
||||
new HttpObjectAggregator(65536),
|
||||
new ChunkedWriteHandler(),
|
||||
new WebSocketServerCompressionHandler(),
|
||||
new WebSocketServerProtocolHandler("/ws", null, true),
|
||||
new ChatServerHandler()
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
ChannelFuture f = b.bind(port).sync();
|
||||
System.out.println("Netty Chat Server started and listen on port " + port);
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void stop() {
|
||||
if (bossGroup != null) {
|
||||
bossGroup.shutdownGracefully();
|
||||
}
|
||||
if (workerGroup != null) {
|
||||
workerGroup.shutdownGracefully();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package com.example.venue_reservation_service.service;
|
||||
|
||||
import com.example.venue_reservation_service.domain.VeException;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
|
||||
/**
|
||||
* @author 31586
|
||||
* @description 针对表【venue_exception(系统异常日志表)】的数据库操作Service
|
||||
* @createDate 2025-06-18 09:39:17
|
||||
*/
|
||||
public interface VeExceptionService extends IService<VeException> {
|
||||
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
package com.example.venue_reservation_service.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.example.venue_reservation_service.mapper.VeExceptionMapper;
|
||||
import com.example.venue_reservation_service.service.VeExceptionService;
|
||||
import com.example.venue_reservation_service.domain.VeException;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* @author 31586
|
||||
* @description 针对表【venue_exception(系统异常日志表)】的数据库操作Service实现
|
||||
* @createDate 2025-06-18 09:39:17
|
||||
*/
|
||||
@Service
|
||||
public class VeExceptionServiceImpl extends ServiceImpl<VeExceptionMapper, VeException>
|
||||
implements VeExceptionService {
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.example.venue_reservation_service.mapper.VeExceptionMapper">
|
||||
|
||||
<resultMap id="BaseResultMap" type="com.example.venue_reservation_service.domain.VeException">
|
||||
<id property="id" column="id" jdbcType="BIGINT"/>
|
||||
<result property="exceptionType" column="exception_type" jdbcType="VARCHAR"/>
|
||||
<result property="exceptionMessage" column="exception_message" jdbcType="VARCHAR"/>
|
||||
<result property="exceptionStack" column="exception_stack" jdbcType="VARCHAR"/>
|
||||
<result property="methodName" column="method_name" jdbcType="VARCHAR"/>
|
||||
<result property="parameterData" column="parameter_data" jdbcType="VARCHAR"/>
|
||||
<result property="userId" column="user_id" jdbcType="INTEGER"/>
|
||||
<result property="operationTime" column="operation_time" jdbcType="TIMESTAMP"/>
|
||||
</resultMap>
|
||||
|
||||
<sql id="Base_Column_List">
|
||||
id,exception_type,exception_message,
|
||||
exception_stack,method_name,parameter_data,
|
||||
user_id,operation_time
|
||||
</sql>
|
||||
</mapper>
|
Loading…
Reference in New Issue