LiveWebSocket.java 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. package com.izouma.meta.websocket;
  2. import com.alibaba.fastjson.JSON;
  3. import com.alibaba.fastjson.JSONObject;
  4. import com.izouma.meta.config.Constants;
  5. import com.izouma.meta.domain.MetaVisitor;
  6. import com.izouma.meta.domain.MetaVisitorMMOLoginInfo;
  7. import com.izouma.meta.dto.*;
  8. import com.izouma.meta.repo.MetaVisitorRepo;
  9. import com.izouma.meta.utils.ApplicationContextUtil;
  10. import lombok.extern.slf4j.Slf4j;
  11. import org.apache.commons.collections.CollectionUtils;
  12. import org.apache.commons.lang.StringUtils;
  13. import org.springframework.data.redis.core.RedisTemplate;
  14. import org.springframework.stereotype.Service;
  15. import javax.websocket.*;
  16. import javax.websocket.server.PathParam;
  17. import javax.websocket.server.ServerEndpoint;
  18. import java.util.ArrayList;
  19. import java.util.List;
  20. import java.util.Map;
  21. import java.util.Objects;
  22. import java.util.concurrent.ConcurrentHashMap;
  23. import java.util.stream.Collectors;
  24. @Service
  25. @ServerEndpoint(value = "/websocket/live/mmo/{userId}")
  26. @Slf4j
  27. public class LiveWebSocket {
  28. /**
  29. * 当前在线的客户端map
  30. */
  31. private static final Map<String, Session> clients = new ConcurrentHashMap();
  32. private static final Map<String, MetaVisitor> user = new ConcurrentHashMap<>();
  33. private final String REDIS_PREFIX = "meta-live:";
  34. private RedisTemplate redisTemplate;
  35. private WebsocketCommon websocketCommon;
  36. private MetaVisitorRepo metaVisitorRepo;
  37. private void init() {
  38. if (Objects.isNull(metaVisitorRepo)) {
  39. metaVisitorRepo = (MetaVisitorRepo) ApplicationContextUtil.getBean("metaVisitorRepo");
  40. }
  41. if (Objects.isNull(redisTemplate)) {
  42. redisTemplate = (RedisTemplate) ApplicationContextUtil.getBean("redisTemplate");
  43. }
  44. if (Objects.isNull(websocketCommon)) {
  45. websocketCommon = (WebsocketCommon) ApplicationContextUtil.getBean("websocketCommon");
  46. }
  47. }
  48. @OnOpen
  49. public void onOpen(@PathParam("userId") String userId, Session session) {
  50. init();
  51. MetaVisitor metaVisitor = metaVisitorRepo.findById(Long.parseLong(userId)).orElse(null);
  52. if (Objects.isNull(metaVisitor)) {
  53. log.error("当前游客信息不存在!");
  54. return;
  55. }
  56. if (!user.containsKey(userId)) {
  57. user.put(userId, metaVisitor);
  58. }
  59. // 判断当前玩家是否在其他地方登陆
  60. if (clients.containsKey(REDIS_PREFIX.concat(userId))) {
  61. log.info(String.format("当前玩家[%S]已经在别处登陆,sessionId为[%S]", userId, session.getId()));
  62. // 关闭连接
  63. MMOSingleMessage mmoSingleMessage = new MMOSingleMessage();
  64. mmoSingleMessage.setMessageType(6);
  65. mmoSingleMessage.setMessage("您已在其他地方登录,已为您关闭上次登陆信息");
  66. websocketCommon.sendMessageToSingle(clients, JSON.toJSONString(mmoSingleMessage), REDIS_PREFIX.concat(userId));
  67. try {
  68. log.info("关闭上次登陆的session连接");
  69. clients.get(REDIS_PREFIX.concat(userId)).close();
  70. } catch (Exception e) {
  71. log.error("session close throw exception:", e);
  72. }
  73. }
  74. log.info("现在来连接的sessionId:" + session.getId() + "玩家id:" + userId + "玩家昵称" + metaVisitor.getNickname());
  75. clients.put(REDIS_PREFIX.concat(userId), session);
  76. MetaVisitorMMOLoginInfo metaVisitorMMOLoginInfo = new MetaVisitorMMOLoginInfo();
  77. metaVisitorMMOLoginInfo.setNickname(metaVisitor.getNickname());
  78. metaVisitorMMOLoginInfo.setUserId(Long.parseLong(userId));
  79. metaVisitorMMOLoginInfo.setSessionId(session.getId());
  80. MetaVisitorMMOMessage mmoMessage = new MetaVisitorMMOMessage();
  81. mmoMessage.setMessageType(5);
  82. mmoMessage.setMessage(metaVisitorMMOLoginInfo);
  83. log.info(String.format("通知玩家[%S],本次登陆信息[%S]", userId, JSON.toJSONString(mmoMessage)));
  84. websocketCommon.sendMessageToSingle(clients, JSON.toJSONString(mmoMessage), REDIS_PREFIX.concat(userId));
  85. }
  86. @OnError
  87. public void onError(Session session, Throwable error) {
  88. error.printStackTrace();
  89. // 异常处理
  90. log.error(String.format("sessionId[%S]的服务端发生了错误:[%S]", session.getId(), error));
  91. }
  92. @OnClose
  93. public void onClose(@PathParam("userId") String userId) {
  94. init();
  95. if (StringUtils.isBlank(userId)) {
  96. return;
  97. }
  98. // 查询地图中玩家信息
  99. MetaVisitorMMOLoginInfo metaVisitorMMOLoginInfo = (MetaVisitorMMOLoginInfo) redisTemplate.opsForValue().get(REDIS_PREFIX.concat(userId));
  100. if (Objects.isNull(metaVisitorMMOLoginInfo)) {
  101. clients.remove(REDIS_PREFIX.concat(userId));
  102. return;
  103. }
  104. String key = String.valueOf(metaVisitorMMOLoginInfo.getCityId()).concat(":").concat(String.valueOf(metaVisitorMMOLoginInfo.getRegionId()));
  105. List<String> otherUserIds = remove(key, userId);
  106. if (CollectionUtils.isNotEmpty(otherUserIds)) {
  107. // 分发下线消息给区域内其他玩家
  108. buildMessageForSendingToAllOther(otherUserIds, 3, metaVisitorMMOLoginInfo);
  109. }
  110. clients.remove(REDIS_PREFIX.concat(userId));
  111. // 删除redis中自己的信息
  112. redisTemplate.delete(REDIS_PREFIX.concat(userId));
  113. // 移除地图中自己的信息
  114. redisTemplate.opsForList().remove(REDIS_PREFIX.concat(key), 0, REDIS_PREFIX.concat(userId));
  115. }
  116. @OnMessage
  117. public void onMessage(@PathParam("userId") String userId, String message, Session session) {
  118. init();
  119. MetaVisitor metaVisitor = user.get(userId);
  120. if (Objects.isNull(metaVisitor)) {
  121. metaVisitor = metaVisitorRepo.findById(Long.parseLong(userId)).orElse(null);
  122. }
  123. if (Objects.isNull(metaVisitor)) {
  124. log.error("当前游客信息不存在!");
  125. return;
  126. }
  127. if (StringUtils.isBlank(message)) {
  128. log.error("Illegal parameter : message can not be null");
  129. return;
  130. }
  131. if (Constants.HEART_RECEIVE.equals(message)) {
  132. log.info(String.format("sessionId:[%S] userId:[%S] 连接正常", session.getId(), userId));
  133. websocketCommon.sendMessageToSingle(clients, Constants.HEART_RETURN, REDIS_PREFIX.concat(userId));
  134. return;
  135. }
  136. JSONObject jsonObject = JSON.parseObject(message);
  137. MetaServiceResult result = checkParams(jsonObject);
  138. if (!result.isSuccess()) {
  139. log.error(result.getMessage());
  140. return;
  141. }
  142. log.info("来自客户端消息:" + message + "客户端的id是:" + session.getId());
  143. int type = Integer.parseInt(jsonObject.getString("type"));
  144. String cityId = jsonObject.getString("cityId");
  145. String regionId = jsonObject.getString("regionId");
  146. String key = cityId.concat(":").concat(regionId);
  147. List<String> otherUserIds = remove(key, userId);
  148. MetaVisitorMMOLoginInfo metaMMOLoginInfo;
  149. List<MetaVisitorMMOLoginInfo> metaMMOLoginInfos = redisTemplate.opsForValue().multiGet(otherUserIds);
  150. switch (type) {
  151. case 1:
  152. log.info("当前操作类型为1 -> 玩家进入地图");
  153. metaMMOLoginInfo = buildMetaMMOLoginInfo(jsonObject, Long.parseLong(cityId), Long.parseLong(regionId), metaVisitor.getNickname(), userId);
  154. if (CollectionUtils.isNotEmpty(otherUserIds)) {
  155. if (CollectionUtils.isNotEmpty(metaMMOLoginInfos)) {
  156. // 分发区域内其他玩家信息给自己
  157. buildMessageForSendingToUser(REDIS_PREFIX.concat(userId), 1, metaMMOLoginInfos);
  158. // 分发自己信息给区域内其他玩家
  159. buildMessageForSendingToAllOther(otherUserIds, 4, metaMMOLoginInfo);
  160. }
  161. }
  162. // 将自己信息存到redis中
  163. redisTemplate.opsForValue().set(REDIS_PREFIX.concat(userId), metaMMOLoginInfo);
  164. redisTemplate.opsForList().leftPush(REDIS_PREFIX.concat(key), REDIS_PREFIX.concat(userId));
  165. break;
  166. case 2:
  167. log.info(String.format("当前操作类型为[%S] -> 玩家切换区域", type));
  168. metaMMOLoginInfo = buildMetaMMOLoginInfo(jsonObject, Long.parseLong(cityId), Long.parseLong(regionId), metaVisitor.getNickname(), userId);
  169. if (CollectionUtils.isNotEmpty(otherUserIds)) {
  170. // 分发自己信息给区域内其他玩家
  171. buildMessageForSendingToAllOther(otherUserIds, 4, metaMMOLoginInfo);
  172. // 分发区域内其他玩家信息给自己
  173. buildMessageForSendingToUser(REDIS_PREFIX.concat(userId), 1, metaMMOLoginInfos);
  174. }
  175. MetaVisitorMMOLoginInfo oldMetaMMOLoginInfo = (MetaVisitorMMOLoginInfo) redisTemplate.opsForValue().get(REDIS_PREFIX.concat(userId));
  176. if (Objects.isNull(oldMetaMMOLoginInfo)) {
  177. log.error("缺失玩家上次区域的地图缓存信息");
  178. break;
  179. }
  180. String oldKey = String.valueOf(oldMetaMMOLoginInfo.getCityId()).concat(":").concat(String.valueOf(oldMetaMMOLoginInfo.getRegionId()));
  181. List<String> oldUserIds = redisTemplate.opsForList().range(REDIS_PREFIX.concat(oldKey), 0, -1);
  182. if (CollectionUtils.isEmpty(oldUserIds)) {
  183. log.error("查询不到上次所进入的区域的玩家信息");
  184. break;
  185. }
  186. // 分发消息给之前区域的玩家
  187. buildMessageForSendingToAllOther(oldUserIds, 3, oldMetaMMOLoginInfo);
  188. // 清除玩家上次缓存的地图信息
  189. redisTemplate.opsForList().remove(REDIS_PREFIX.concat(oldKey), 0, REDIS_PREFIX.concat(userId));
  190. // 缓存玩家新的地图信息
  191. redisTemplate.opsForValue().set(REDIS_PREFIX.concat(userId), metaMMOLoginInfo);
  192. redisTemplate.opsForList().leftPush(REDIS_PREFIX.concat(key), REDIS_PREFIX.concat(userId));
  193. break;
  194. case 3:
  195. log.info(String.format("当前操作类型为[%S] -> 玩家在地图内移动", type));
  196. metaMMOLoginInfo = (MetaVisitorMMOLoginInfo) redisTemplate.opsForValue().get(REDIS_PREFIX.concat(userId));
  197. if (Objects.isNull(metaMMOLoginInfo)) {
  198. log.error("缓存中不存在本玩家信息");
  199. break;
  200. }
  201. // 更新玩家位置信息
  202. buildCommonProperty(metaMMOLoginInfo, jsonObject);
  203. // 分发玩家信息给区域内其他玩家
  204. buildMessageForSendingToAllOther(otherUserIds, 2, metaMMOLoginInfo);
  205. redisTemplate.opsForValue().set(REDIS_PREFIX.concat(userId), metaMMOLoginInfo);
  206. break;
  207. case 4:
  208. log.info(String.format("当前操作类型为[%S] -> 玩家进入大厅", type));
  209. metaMMOLoginInfo = (MetaVisitorMMOLoginInfo) redisTemplate.opsForValue().get(REDIS_PREFIX.concat(userId));
  210. if (Objects.isNull(metaMMOLoginInfo)) {
  211. log.error("缓存中不存在本玩家信息");
  212. break;
  213. }
  214. // 分发玩家信息给区域内其他玩家
  215. buildMessageForSendingToAllOther(otherUserIds, 3, metaMMOLoginInfo);
  216. break;
  217. default:
  218. log.error(String.format("不存在的操作类型[%S]", type));
  219. }
  220. }
  221. /**
  222. * 分发玩家信息给区域内其他玩家
  223. *
  224. * @param userIds 玩家id集合
  225. * @param messageType 消息类型
  226. * @param metaMMOLoginInfo 消息体
  227. */
  228. private void buildMessageForSendingToAllOther(List<String> userIds, int messageType, MetaVisitorMMOLoginInfo metaMMOLoginInfo) {
  229. MetaVisitorMMOMessage mmoMessage = new MetaVisitorMMOMessage();
  230. mmoMessage.setMessageType(messageType);
  231. mmoMessage.setMessage(metaMMOLoginInfo);
  232. if (!clients.containsKey(REDIS_PREFIX.concat(String.valueOf(metaMMOLoginInfo.getUserId())))) {
  233. log.error("session信息不存在");
  234. return;
  235. }
  236. userIds.forEach(id -> {
  237. try {
  238. log.info(String.format("服务器给所有当前区域内在线用户发送消息,当前在线人员为[%S]。消息:[%S]", id, JSON.toJSONString(mmoMessage)));
  239. clients.get(id).getBasicRemote().sendText(JSON.toJSONString(mmoMessage));
  240. } catch (Exception e) {
  241. log.error(String.format("send message [%S] to [%S] throw exception [%S]:", JSON.toJSONString(mmoMessage), id, e));
  242. }
  243. });
  244. }
  245. /**
  246. * 分发区域内其他玩家信息给自己
  247. *
  248. * @param userId 玩家id
  249. * @param messageType 消息类型
  250. * @param metaMMOLoginInfos 消息体(其他玩家信息)
  251. */
  252. private void buildMessageForSendingToUser(String userId, int messageType, List<MetaVisitorMMOLoginInfo> metaMMOLoginInfos) {
  253. MetaVisitorMMOMessage mmoMessage = new MetaVisitorMMOMessage();
  254. mmoMessage.setMessageType(messageType);
  255. mmoMessage.setMap(metaMMOLoginInfos);
  256. websocketCommon.sendMessageToSingle(clients, JSON.toJSONString(mmoMessage), userId);
  257. }
  258. /**
  259. * 构建玩家登陆信息
  260. *
  261. * @param jsonObject 玩家位置等信息json对象
  262. * @param cityId 城市id
  263. * @param regionId 区域id
  264. * @return 玩家位置信息
  265. */
  266. private MetaVisitorMMOLoginInfo buildMetaMMOLoginInfo(JSONObject jsonObject, Long cityId, Long regionId, String nickName, String userId) {
  267. // 获取到进入地图时自己的信息
  268. MetaVisitorMMOLoginInfo metaMMOLoginInfo = new MetaVisitorMMOLoginInfo();
  269. buildCommonProperty(metaMMOLoginInfo, jsonObject);
  270. metaMMOLoginInfo.setCityId(cityId);
  271. metaMMOLoginInfo.setRegionId(regionId);
  272. metaMMOLoginInfo.setUserId(Long.parseLong(userId));
  273. metaMMOLoginInfo.setNickname(nickName);
  274. if (Objects.nonNull(jsonObject.getString("role"))) {
  275. metaMMOLoginInfo.setRole(jsonObject.getString("role"));
  276. }
  277. return metaMMOLoginInfo;
  278. }
  279. /**
  280. * 根据jsonObject构建玩家位置信息
  281. *
  282. * @param metaMMOLoginInfo 玩家登陆信息
  283. * @param jsonObject 玩家位置信息json对象
  284. */
  285. private void buildCommonProperty(MetaVisitorMMOLoginInfo metaMMOLoginInfo, JSONObject jsonObject) {
  286. if (Objects.nonNull(jsonObject.getString("axisX"))) {
  287. metaMMOLoginInfo.setAxisX(Float.parseFloat(jsonObject.getString("axisX")));
  288. }
  289. if (Objects.nonNull(jsonObject.getString("axisY"))) {
  290. metaMMOLoginInfo.setAxisY(Float.parseFloat(jsonObject.getString("axisY")));
  291. }
  292. if (Objects.nonNull(jsonObject.getString("axisZ"))) {
  293. metaMMOLoginInfo.setAxisZ(Float.parseFloat(jsonObject.getString("axisZ")));
  294. }
  295. if (Objects.nonNull(jsonObject.getString("eulerX"))) {
  296. metaMMOLoginInfo.setEulerX(Float.parseFloat(jsonObject.getString("eulerX")));
  297. }
  298. if (Objects.nonNull(jsonObject.getString("eulerY"))) {
  299. metaMMOLoginInfo.setEulerY(Float.parseFloat(jsonObject.getString("eulerY")));
  300. }
  301. if (Objects.nonNull(jsonObject.getString("eulerZ"))) {
  302. metaMMOLoginInfo.setEulerZ(Float.parseFloat(jsonObject.getString("eulerZ")));
  303. }
  304. if (Objects.nonNull(jsonObject.getString("top"))) {
  305. metaMMOLoginInfo.setTop(Integer.parseInt(jsonObject.getString("top")));
  306. }
  307. if (Objects.nonNull(jsonObject.getString("hat"))) {
  308. metaMMOLoginInfo.setHat(Integer.parseInt(jsonObject.getString("hat")));
  309. }
  310. if (Objects.nonNull(jsonObject.getString("down"))) {
  311. metaMMOLoginInfo.setDown(Integer.parseInt(jsonObject.getString("down")));
  312. }
  313. if (Objects.nonNull(jsonObject.getString("shoes"))) {
  314. metaMMOLoginInfo.setShoes(Integer.parseInt(jsonObject.getString("shoes")));
  315. }
  316. if (Objects.nonNull(jsonObject.getString("anim"))) {
  317. metaMMOLoginInfo.setAnim(Integer.parseInt(jsonObject.getString("anim")));
  318. }
  319. if (Objects.nonNull(jsonObject.getString("emoji"))) {
  320. metaMMOLoginInfo.setEmoji(Integer.parseInt(jsonObject.getString("emoji")));
  321. }
  322. }
  323. /**
  324. * 校验参数
  325. *
  326. * @param jsonObject 参数
  327. * @return 校验结果
  328. */
  329. private MetaServiceResult checkParams(JSONObject jsonObject) {
  330. if (Objects.isNull(jsonObject)) {
  331. return MetaServiceResult.returnError("Illegal parameter : jsonObject can not be null");
  332. }
  333. if (Objects.isNull(jsonObject.getString("type"))) {
  334. return MetaServiceResult.returnError("Illegal parameter : type can not be null");
  335. }
  336. if (Objects.isNull(jsonObject.getString("cityId"))) {
  337. return MetaServiceResult.returnError("Illegal parameter : cityId can not be null");
  338. }
  339. if (Objects.isNull(jsonObject.getString("regionId"))) {
  340. return MetaServiceResult.returnError("Illegal parameter : regionId can not be null");
  341. }
  342. return MetaServiceResult.returnSuccess("check success");
  343. }
  344. private List<String> remove(String key, String userId) {
  345. List<String> userIds = redisTemplate.opsForList().range(REDIS_PREFIX.concat(key), 0, -1);
  346. // 去除当前玩家id
  347. List<String> otherUserIds = new ArrayList<>();
  348. if (CollectionUtils.isNotEmpty(userIds)) {
  349. otherUserIds = userIds.stream().filter(id -> !Objects.equals(id, REDIS_PREFIX.concat(userId))).collect(Collectors.toList());
  350. }
  351. return otherUserIds;
  352. }
  353. }