package com.izouma.meta.websocket; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.izouma.meta.config.Constants; import com.izouma.meta.domain.MetaVisitor; import com.izouma.meta.domain.MetaVisitorMMOLoginInfo; import com.izouma.meta.dto.*; import com.izouma.meta.repo.MetaVisitorRepo; import com.izouma.meta.utils.ApplicationContextUtil; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang.StringUtils; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import javax.websocket.*; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; @Service @ServerEndpoint(value = "/websocket/live/mmo/{userId}") @Slf4j public class LiveWebSocket { /** * 当前在线的客户端map */ private static final Map clients = new ConcurrentHashMap(); private static final Map user = new ConcurrentHashMap<>(); private final String REDIS_PREFIX = "meta-live:"; private RedisTemplate redisTemplate; private WebsocketCommon websocketCommon; private MetaVisitorRepo metaVisitorRepo; private void init() { if (Objects.isNull(metaVisitorRepo)) { metaVisitorRepo = (MetaVisitorRepo) ApplicationContextUtil.getBean("metaVisitorRepo"); } if (Objects.isNull(redisTemplate)) { redisTemplate = (RedisTemplate) ApplicationContextUtil.getBean("redisTemplate"); } if (Objects.isNull(websocketCommon)) { websocketCommon = (WebsocketCommon) ApplicationContextUtil.getBean("websocketCommon"); } } @OnOpen public void onOpen(@PathParam("userId") String userId, Session session) { init(); MetaVisitor metaVisitor = metaVisitorRepo.findById(Long.parseLong(userId)).orElse(null); if (Objects.isNull(metaVisitor)) { log.error("当前游客信息不存在!"); return; } if (!user.containsKey(userId)) { user.put(userId, metaVisitor); } // 判断当前玩家是否在其他地方登陆 if (clients.containsKey(REDIS_PREFIX.concat(userId))) { log.info(String.format("当前玩家[%S]已经在别处登陆,sessionId为[%S]", userId, session.getId())); // 关闭连接 MMOSingleMessage mmoSingleMessage = new MMOSingleMessage(); mmoSingleMessage.setMessageType(6); mmoSingleMessage.setMessage("您已在其他地方登录,已为您关闭上次登陆信息"); websocketCommon.sendMessageToSingle(clients, JSON.toJSONString(mmoSingleMessage), REDIS_PREFIX.concat(userId)); try { log.info("关闭上次登陆的session连接"); clients.get(REDIS_PREFIX.concat(userId)).close(); } catch (Exception e) { log.error("session close throw exception:", e); } } log.info("现在来连接的sessionId:" + session.getId() + "玩家id:" + userId + "玩家昵称" + metaVisitor.getNickname()); clients.put(REDIS_PREFIX.concat(userId), session); MetaVisitorMMOLoginInfo metaVisitorMMOLoginInfo = new MetaVisitorMMOLoginInfo(); metaVisitorMMOLoginInfo.setNickname(metaVisitor.getNickname()); metaVisitorMMOLoginInfo.setUserId(Long.parseLong(userId)); metaVisitorMMOLoginInfo.setSessionId(session.getId()); MetaVisitorMMOMessage mmoMessage = new MetaVisitorMMOMessage(); mmoMessage.setMessageType(5); mmoMessage.setMessage(metaVisitorMMOLoginInfo); log.info(String.format("通知玩家[%S],本次登陆信息[%S]", userId, JSON.toJSONString(mmoMessage))); websocketCommon.sendMessageToSingle(clients, JSON.toJSONString(mmoMessage), REDIS_PREFIX.concat(userId)); } @OnError public void onError(Session session, Throwable error) { error.printStackTrace(); // 异常处理 log.error(String.format("sessionId[%S]的服务端发生了错误:[%S]", session.getId(), error)); } @OnClose public void onClose(@PathParam("userId") String userId) { init(); if (StringUtils.isBlank(userId)) { return; } // 查询地图中玩家信息 MetaVisitorMMOLoginInfo metaVisitorMMOLoginInfo = (MetaVisitorMMOLoginInfo) redisTemplate.opsForValue().get(REDIS_PREFIX.concat(userId)); if (Objects.isNull(metaVisitorMMOLoginInfo)) { clients.remove(REDIS_PREFIX.concat(userId)); return; } String key = String.valueOf(metaVisitorMMOLoginInfo.getCityId()).concat(":").concat(String.valueOf(metaVisitorMMOLoginInfo.getRegionId())); List otherUserIds = remove(key, userId); if (CollectionUtils.isNotEmpty(otherUserIds)) { // 分发下线消息给区域内其他玩家 buildMessageForSendingToAllOther(otherUserIds, 3, metaVisitorMMOLoginInfo); } clients.remove(REDIS_PREFIX.concat(userId)); // 删除redis中自己的信息 redisTemplate.delete(REDIS_PREFIX.concat(userId)); // 移除地图中自己的信息 redisTemplate.opsForList().remove(REDIS_PREFIX.concat(key), 0, REDIS_PREFIX.concat(userId)); } @OnMessage public void onMessage(@PathParam("userId") String userId, String message, Session session) { init(); MetaVisitor metaVisitor = user.get(userId); if (Objects.isNull(metaVisitor)) { metaVisitor = metaVisitorRepo.findById(Long.parseLong(userId)).orElse(null); } if (Objects.isNull(metaVisitor)) { log.error("当前游客信息不存在!"); return; } if (StringUtils.isBlank(message)) { log.error("Illegal parameter : message can not be null"); return; } if (Constants.HEART_RECEIVE.equals(message)) { log.info(String.format("sessionId:[%S] userId:[%S] 连接正常", session.getId(), userId)); websocketCommon.sendMessageToSingle(clients, Constants.HEART_RETURN, REDIS_PREFIX.concat(userId)); return; } JSONObject jsonObject = JSON.parseObject(message); MetaServiceResult result = checkParams(jsonObject); if (!result.isSuccess()) { log.error(result.getMessage()); return; } log.info("来自客户端消息:" + message + "客户端的id是:" + session.getId()); int type = Integer.parseInt(jsonObject.getString("type")); String cityId = jsonObject.getString("cityId"); String regionId = jsonObject.getString("regionId"); String key = cityId.concat(":").concat(regionId); List otherUserIds = remove(key, userId); MetaVisitorMMOLoginInfo metaMMOLoginInfo; List metaMMOLoginInfos = redisTemplate.opsForValue().multiGet(otherUserIds); switch (type) { case 1: log.info("当前操作类型为1 -> 玩家进入地图"); metaMMOLoginInfo = buildMetaMMOLoginInfo(jsonObject, Long.parseLong(cityId), Long.parseLong(regionId), metaVisitor.getNickname(), userId); if (CollectionUtils.isNotEmpty(otherUserIds)) { if (CollectionUtils.isNotEmpty(metaMMOLoginInfos)) { // 分发区域内其他玩家信息给自己 buildMessageForSendingToUser(REDIS_PREFIX.concat(userId), 1, metaMMOLoginInfos); // 分发自己信息给区域内其他玩家 buildMessageForSendingToAllOther(otherUserIds, 4, metaMMOLoginInfo); } } // 将自己信息存到redis中 redisTemplate.opsForValue().set(REDIS_PREFIX.concat(userId), metaMMOLoginInfo); redisTemplate.opsForList().leftPush(REDIS_PREFIX.concat(key), REDIS_PREFIX.concat(userId)); break; case 2: log.info(String.format("当前操作类型为[%S] -> 玩家切换区域", type)); metaMMOLoginInfo = buildMetaMMOLoginInfo(jsonObject, Long.parseLong(cityId), Long.parseLong(regionId), metaVisitor.getNickname(), userId); if (CollectionUtils.isNotEmpty(otherUserIds)) { // 分发自己信息给区域内其他玩家 buildMessageForSendingToAllOther(otherUserIds, 4, metaMMOLoginInfo); // 分发区域内其他玩家信息给自己 buildMessageForSendingToUser(REDIS_PREFIX.concat(userId), 1, metaMMOLoginInfos); } MetaVisitorMMOLoginInfo oldMetaMMOLoginInfo = (MetaVisitorMMOLoginInfo) redisTemplate.opsForValue().get(REDIS_PREFIX.concat(userId)); if (Objects.isNull(oldMetaMMOLoginInfo)) { log.error("缺失玩家上次区域的地图缓存信息"); break; } String oldKey = String.valueOf(oldMetaMMOLoginInfo.getCityId()).concat(":").concat(String.valueOf(oldMetaMMOLoginInfo.getRegionId())); List oldUserIds = redisTemplate.opsForList().range(REDIS_PREFIX.concat(oldKey), 0, -1); if (CollectionUtils.isEmpty(oldUserIds)) { log.error("查询不到上次所进入的区域的玩家信息"); break; } // 分发消息给之前区域的玩家 buildMessageForSendingToAllOther(oldUserIds, 3, oldMetaMMOLoginInfo); // 清除玩家上次缓存的地图信息 redisTemplate.opsForList().remove(REDIS_PREFIX.concat(oldKey), 0, REDIS_PREFIX.concat(userId)); // 缓存玩家新的地图信息 redisTemplate.opsForValue().set(REDIS_PREFIX.concat(userId), metaMMOLoginInfo); redisTemplate.opsForList().leftPush(REDIS_PREFIX.concat(key), REDIS_PREFIX.concat(userId)); break; case 3: log.info(String.format("当前操作类型为[%S] -> 玩家在地图内移动", type)); metaMMOLoginInfo = (MetaVisitorMMOLoginInfo) redisTemplate.opsForValue().get(REDIS_PREFIX.concat(userId)); if (Objects.isNull(metaMMOLoginInfo)) { log.error("缓存中不存在本玩家信息"); break; } // 更新玩家位置信息 buildCommonProperty(metaMMOLoginInfo, jsonObject); // 分发玩家信息给区域内其他玩家 buildMessageForSendingToAllOther(otherUserIds, 2, metaMMOLoginInfo); redisTemplate.opsForValue().set(REDIS_PREFIX.concat(userId), metaMMOLoginInfo); break; case 4: log.info(String.format("当前操作类型为[%S] -> 玩家进入大厅", type)); metaMMOLoginInfo = (MetaVisitorMMOLoginInfo) redisTemplate.opsForValue().get(REDIS_PREFIX.concat(userId)); if (Objects.isNull(metaMMOLoginInfo)) { log.error("缓存中不存在本玩家信息"); break; } // 分发玩家信息给区域内其他玩家 buildMessageForSendingToAllOther(otherUserIds, 3, metaMMOLoginInfo); break; default: log.error(String.format("不存在的操作类型[%S]", type)); } } /** * 分发玩家信息给区域内其他玩家 * * @param userIds 玩家id集合 * @param messageType 消息类型 * @param metaMMOLoginInfo 消息体 */ private void buildMessageForSendingToAllOther(List userIds, int messageType, MetaVisitorMMOLoginInfo metaMMOLoginInfo) { MetaVisitorMMOMessage mmoMessage = new MetaVisitorMMOMessage(); mmoMessage.setMessageType(messageType); mmoMessage.setMessage(metaMMOLoginInfo); if (!clients.containsKey(REDIS_PREFIX.concat(String.valueOf(metaMMOLoginInfo.getUserId())))) { log.error("session信息不存在"); return; } userIds.forEach(id -> { try { log.info(String.format("服务器给所有当前区域内在线用户发送消息,当前在线人员为[%S]。消息:[%S]", id, JSON.toJSONString(mmoMessage))); clients.get(id).getBasicRemote().sendText(JSON.toJSONString(mmoMessage)); } catch (Exception e) { log.error(String.format("send message [%S] to [%S] throw exception [%S]:", JSON.toJSONString(mmoMessage), id, e)); } }); } /** * 分发区域内其他玩家信息给自己 * * @param userId 玩家id * @param messageType 消息类型 * @param metaMMOLoginInfos 消息体(其他玩家信息) */ private void buildMessageForSendingToUser(String userId, int messageType, List metaMMOLoginInfos) { MetaVisitorMMOMessage mmoMessage = new MetaVisitorMMOMessage(); mmoMessage.setMessageType(messageType); mmoMessage.setMap(metaMMOLoginInfos); websocketCommon.sendMessageToSingle(clients, JSON.toJSONString(mmoMessage), userId); } /** * 构建玩家登陆信息 * * @param jsonObject 玩家位置等信息json对象 * @param cityId 城市id * @param regionId 区域id * @return 玩家位置信息 */ private MetaVisitorMMOLoginInfo buildMetaMMOLoginInfo(JSONObject jsonObject, Long cityId, Long regionId, String nickName, String userId) { // 获取到进入地图时自己的信息 MetaVisitorMMOLoginInfo metaMMOLoginInfo = new MetaVisitorMMOLoginInfo(); buildCommonProperty(metaMMOLoginInfo, jsonObject); metaMMOLoginInfo.setCityId(cityId); metaMMOLoginInfo.setRegionId(regionId); metaMMOLoginInfo.setUserId(Long.parseLong(userId)); metaMMOLoginInfo.setNickname(nickName); if (Objects.nonNull(jsonObject.getString("role"))) { metaMMOLoginInfo.setRole(jsonObject.getString("role")); } return metaMMOLoginInfo; } /** * 根据jsonObject构建玩家位置信息 * * @param metaMMOLoginInfo 玩家登陆信息 * @param jsonObject 玩家位置信息json对象 */ private void buildCommonProperty(MetaVisitorMMOLoginInfo metaMMOLoginInfo, JSONObject jsonObject) { if (Objects.nonNull(jsonObject.getString("axisX"))) { metaMMOLoginInfo.setAxisX(Float.parseFloat(jsonObject.getString("axisX"))); } if (Objects.nonNull(jsonObject.getString("axisY"))) { metaMMOLoginInfo.setAxisY(Float.parseFloat(jsonObject.getString("axisY"))); } if (Objects.nonNull(jsonObject.getString("axisZ"))) { metaMMOLoginInfo.setAxisZ(Float.parseFloat(jsonObject.getString("axisZ"))); } if (Objects.nonNull(jsonObject.getString("eulerX"))) { metaMMOLoginInfo.setEulerX(Float.parseFloat(jsonObject.getString("eulerX"))); } if (Objects.nonNull(jsonObject.getString("eulerY"))) { metaMMOLoginInfo.setEulerY(Float.parseFloat(jsonObject.getString("eulerY"))); } if (Objects.nonNull(jsonObject.getString("eulerZ"))) { metaMMOLoginInfo.setEulerZ(Float.parseFloat(jsonObject.getString("eulerZ"))); } if (Objects.nonNull(jsonObject.getString("top"))) { metaMMOLoginInfo.setTop(Integer.parseInt(jsonObject.getString("top"))); } if (Objects.nonNull(jsonObject.getString("hat"))) { metaMMOLoginInfo.setHat(Integer.parseInt(jsonObject.getString("hat"))); } if (Objects.nonNull(jsonObject.getString("down"))) { metaMMOLoginInfo.setDown(Integer.parseInt(jsonObject.getString("down"))); } if (Objects.nonNull(jsonObject.getString("shoes"))) { metaMMOLoginInfo.setShoes(Integer.parseInt(jsonObject.getString("shoes"))); } if (Objects.nonNull(jsonObject.getString("anim"))) { metaMMOLoginInfo.setAnim(Integer.parseInt(jsonObject.getString("anim"))); } if (Objects.nonNull(jsonObject.getString("emoji"))) { metaMMOLoginInfo.setEmoji(Integer.parseInt(jsonObject.getString("emoji"))); } } /** * 校验参数 * * @param jsonObject 参数 * @return 校验结果 */ private MetaServiceResult checkParams(JSONObject jsonObject) { if (Objects.isNull(jsonObject)) { return MetaServiceResult.returnError("Illegal parameter : jsonObject can not be null"); } if (Objects.isNull(jsonObject.getString("type"))) { return MetaServiceResult.returnError("Illegal parameter : type can not be null"); } if (Objects.isNull(jsonObject.getString("cityId"))) { return MetaServiceResult.returnError("Illegal parameter : cityId can not be null"); } if (Objects.isNull(jsonObject.getString("regionId"))) { return MetaServiceResult.returnError("Illegal parameter : regionId can not be null"); } return MetaServiceResult.returnSuccess("check success"); } private List remove(String key, String userId) { List userIds = redisTemplate.opsForList().range(REDIS_PREFIX.concat(key), 0, -1); // 去除当前玩家id List otherUserIds = new ArrayList<>(); if (CollectionUtils.isNotEmpty(userIds)) { otherUserIds = userIds.stream().filter(id -> !Objects.equals(id, REDIS_PREFIX.concat(userId))).collect(Collectors.toList()); } return otherUserIds; } }