Message与LoginMessage改造
Message:
/**
* @author 14501
*/
@Data
public abstract class Message implements Serializable {
/**
* 根据消息类型字节,获得对应的消息 class
* @param messageType 消息类型字节
* @return 消息 class
*/
public static Class<? extends Message> getMessageClass(int messageType) {
return messageClasses.get(messageType);
}
private Long userId;
private int messageType;
private String token;
private Long id;
public abstract int getMessageType();
public static final int LoginRequestMessage = 0;
public static final int LoginResponseMessage = 1;
public static final int ChatRequestMessage = 2;
public static final int ChatResponseMessage = 3;
public static final int GroupCreateRequestMessage = 4;
public static final int GroupCreateResponseMessage = 5;
public static final int GroupJoinRequestMessage = 6;
public static final int GroupJoinResponseMessage = 7;
public static final int GroupQuitRequestMessage = 8;
public static final int GroupQuitResponseMessage = 9;
public static final int GroupChatRequestMessage = 10;
public static final int GroupChatResponseMessage = 11;
public static final int GroupMembersRequestMessage = 12;
public static final int GroupMembersResponseMessage = 13;
public static final int PingMessage = 14;
public static final int PongMessage = 15;
/**
* 请求类型 byte 值
*/
public static final int RPC_MESSAGE_TYPE_REQUEST = 101;
/**
* 响应类型 byte 值
*/
public static final int RPC_MESSAGE_TYPE_RESPONSE = 102;
private static final Map<Integer, Class<? extends Message>> messageClasses = new HashMap<>();
static {
messageClasses.put(LoginRequestMessage, LoginRequestMessage.class);
messageClasses.put(LoginResponseMessage, LoginResponseMessage.class);
messageClasses.put(ChatRequestMessage, com.dftdla.server.message.request.ChatRequestMessage.class);
messageClasses.put(ChatResponseMessage, com.dftdla.server.message.response.ChatResponseMessage.class);
messageClasses.put(GroupCreateRequestMessage, com.dftdla.server.message.request.GroupCreateRequestMessage.class);
messageClasses.put(GroupCreateResponseMessage, com.dftdla.server.message.response.GroupCreateResponseMessage.class);
messageClasses.put(GroupJoinRequestMessage, com.dftdla.server.message.request.GroupJoinRequestMessage.class);
messageClasses.put(GroupJoinResponseMessage, com.dftdla.server.message.response.GroupJoinResponseMessage.class);
messageClasses.put(GroupQuitRequestMessage, GroupQuitRequestMessage.class);
messageClasses.put(GroupQuitResponseMessage, GroupQuitResponseMessage.class);
messageClasses.put(GroupChatRequestMessage, com.dftdla.server.message.request.GroupChatRequestMessage.class);
messageClasses.put(GroupChatResponseMessage, com.dftdla.server.message.response.GroupChatResponseMessage.class);
messageClasses.put(GroupMembersRequestMessage, com.dftdla.server.message.request.GroupMembersRequestMessage.class);
messageClasses.put(GroupMembersResponseMessage, com.dftdla.server.message.response.GroupMembersResponseMessage.class);
}
@Override
public String toString() {
return JSON.toJSONString(this);
}
}
LoginRequestMessage:
/**
* @author 14501
*/
@EqualsAndHashCode(callSuper = true)
@Data
@ToString(callSuper = true)
public class LoginRequestMessage extends Message {
public LoginRequestMessage() {
}
@Override
public int getMessageType() {
return LoginRequestMessage;
}
}
WebSocketHandler和QuitHandler改造方案
WebSocketHandler我们用于对WS数据的解析和鉴权
QuitHandler我们用于对异常和正常关闭的channel的处理
WebSocketHandler改造
解析接收到的TextWebSocketFrame数据,并对解析结果进行鉴权检查
解析工具类,使用FastJson2:
/**
* @author 14501
*/
public class MessageUtil {
public static Message decode(String msg) {
Message message = null;
try {
JSONObject temp = JSONObject.parse(msg);
message = JSONObject.parseObject(msg, Message.getMessageClass((Integer) temp.get("messageType")));
}catch (Exception e) {
e.printStackTrace();
}
return message;
}
}
WebSocketHandler:
/**
* @author 14501
*/
@Slf4j
@Component
@ChannelHandler.Sharable
public class WebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
@Resource
private ChatUserService chatUserService;
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, TextWebSocketFrame textWebSocketFrame) throws Exception {
Message decode = MessageUtil.decode(textWebSocketFrame.text());
log.info("解析指令:{}",decode);
boolean login = decode.getMessageType() == Message.LoginRequestMessage;
if(login || doAuth(decode.getToken())){
//如果不是登录指令,则说明token鉴权通过,我们设置一个userId给Message以替代ThreadLocal保存的UserId,这样不用再手动回收
if(!login){
decode.setUserId(Long.valueOf(JwtUtil.parseJWT(decode.getToken()).getSubject()));
}
//交给下面的Handler进行类型匹配
channelHandlerContext.fireChannelRead(decode);
}else{
//鉴权未通过,关闭channel
channelHandlerContext.writeAndFlush(new TextWebSocketFrame(new ResponseResult<String>(Code.USER_AUTH_ERROR).toString()));
log.warn("非法的token!");
ChannelUtils.close(channelHandlerContext.channel());
}
}
/**
* 鉴定token是否合法
* @param token 待校验Auth
* @return true合法
*/
public boolean doAuth(String token){
return chatUserService.doAuth(token);
}
}
chatUserService鉴权方案:
@Override
public boolean doAuth(String token) {
boolean res;
Claims claims;
try {
claims = JwtUtil.parseJWT(token);
}catch (Exception e) {
e.printStackTrace();
claims = null;
}
if(claims == null){
//鉴权失败
res = false;
}else{
//token合法,检查是否是最新的token
Long userId = Long.valueOf(claims.getSubject());
Object object = redisCache.getCacheObject(RedisKey.loginKey(userId));
res = object != null && object.equals(token);
}
return res;
}
如此一来,后续handler接收到的请求都是合法请求,不需要验证权限,但指令数据是否合法还需判断,如果不嫌麻烦可以对Message添加一个自检方法,在WebSocketHandler这里运行一次自检也可以,我们后续默认指令数据合法
QuitHandler改造
对QuitHandler的改造实际上是Session的unbind方法的改造,在bind时会记录用户登录操作并在redis存储token,在unbind时会记录用户登出操作,并移除redis中的token
QuitHandler:
/**
* @author 14501
*/
@Slf4j
@Component
@ChannelHandler.Sharable
public class QuitHandler extends ChannelInboundHandlerAdapter {
@Resource
private Session session;
/**
* 当连接断开时触发 inactive 事件
* @param ctx channel
*/
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
session.unbind(ctx.channel());
log.debug("{} 已经断开", ctx.channel());
}
/**
* 当出现异常时触发
* @param ctx channel
* @param cause 原因
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
session.unbind(ctx.channel());
log.debug("{} 已经异常断开 异常是{}", ctx.channel(), cause.getMessage());
}
}
chatUserService的login和logout:
@Override
public String login(Long userId) {
String token = JwtUtil.createJWT(userId + "");
redisCache.setCacheObject(RedisKey.loginKey(userId),token,12, TimeUnit.HOURS);
//登录记录
UserHandler userHandler = new UserHandler();
userHandler.setUserId(userId);
userHandler.setType(UserHandler.LOGIN);
userHandlerService.save(userHandler);
return token;
}
@Override
public void logout(Long userId) {
redisCache.deleteObject(RedisKey.loginKey(userId));
UserHandler userHandler = new UserHandler();
userHandler.setUserId(userId);
userHandler.setType(UserHandler.LOGOUT);
userHandlerService.save(userHandler);
}
如此QuitHandler的全局Channel关闭也完成了
登录功能实现
LoginMessageHandler,后面当我们登录时需要拉取所有离线消息:
/**
* @author 14501
*/
@Slf4j
@Component
@ChannelHandler.Sharable
public class LoginRequestMessageHandler extends SimpleChannelInboundHandler<LoginRequestMessage> {
@Resource
private ChatUserService chatUserService;
@Resource
private Session session;
@Resource
private RedisCache redisCache;
@Override
protected void channelRead0(ChannelHandlerContext ctx, LoginRequestMessage msg) throws Exception {
Long userId = msg.getUserId();
boolean login = chatUserService.canLogin(userId);
LoginResponseMessage message;
if (login) {
String bind = session.bind(ctx.channel(), userId);
log.info("登陆成功");
message = new LoginResponseMessage(true, "登录成功");
message.setToken(bind);
} else {
log.warn("用户不合法或已登录");
message = new LoginResponseMessage(false, "用户不合法或已登录");
}
ctx.writeAndFlush(new TextWebSocketFrame(new ResponseResult<>(Code.SUCCESS.code,message).toString()));
}
}
这里有一个登录时拉取消息的问题:对于离线消息,我们存储在Redis中,如果Redis执行淘汰策略,把数据清空了,我们该如何判断离线消息?
这里我的思路是:在数据发送时存储数据,缓存离线消息,如果离线消息被删除,则用户通过漫游查询消息(类似微信太久没登陆会导致消息丢失)
另一种方式是存储消息的表添加一个消息状态,我们这里只是作为一个示例项目,就采用上面的方式了
注意事项
对于WS连接,我们在write数据时,要使用TextWebSocketFrame进行包装,否则客户端接收不到消息
并且Message的子类中,以往的代指username的Stirng类型数据都要改为Long类型(我们用于代指UserId)
调试
使用ApiFox的WebSocket接口测试,注意这里每开一个窗口,服务器那边都对应唯一的channe,即:一个窗口就是一个ws客户端
服务器日志:
下一篇:单聊功能实现