初始提交,实现WebSocket聊天室基本功能

This commit is contained in:
2024-04-14 12:11:56 +08:00
parent 7a4faa4e2e
commit 532e23938a
34 changed files with 2427 additions and 24 deletions
@@ -0,0 +1,15 @@
package cn.airnan.dg;
import cn.airnan.dg.service.TopicService;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DrawAndGuessApplication {
public static void main(String[] args) {
SpringApplication.run(DrawAndGuessApplication.class, args);
TopicService.loadTopics();
}
}
@@ -0,0 +1,19 @@
package cn.airnan.dg.configs;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 配置跨域
*/
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedOriginPatterns("*")
.allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
.allowCredentials(true).maxAge(3600);
}
}
@@ -0,0 +1,58 @@
package cn.airnan.dg.configs;
import cn.airnan.dg.entity.Player;
import cn.airnan.dg.entity.Room;
import cn.airnan.dg.entity.Topic;
import jakarta.websocket.Session;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* 全局缓存变量
*/
public class Global {
/**
* 消息类型-房间聊天(回答)
*/
public static final String MSG_TYPE_ROOM_CHAT = "room-chat";
/**
* 消息类型-房间绘画
*/
public static final String MSG_TYPE_ROOM_DRAW = "room-draw";
/**
* 消息类型-房间通知消息
*/
public static final String MSG_TYPE_ROOM_MSG = "room-msg";
/**
* 题库
*/
public static List<Topic> TOPICS = new ArrayList<>();
/**
* 房间列表
*/
public static final Map<String, Room> ROOMS = new HashMap<>();
/**
* 玩家列表
*/
public static final Map<String, Player> PLAYERS = new HashMap<>();
/**
* 会话列表
*/
public static final CopyOnWriteArraySet<Session> SESSIONS = new CopyOnWriteArraySet<>();
/**
* 在线连接池
*/
public static final Map<String, Session> SESSION_POOL = new HashMap<>();
}
@@ -0,0 +1,14 @@
package cn.airnan.dg.configs;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WebSocketConfiguration {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
@@ -0,0 +1,78 @@
package cn.airnan.dg.controller;
import cn.airnan.dg.configs.Global;
import cn.airnan.dg.entity.Player;
import cn.airnan.dg.entity.Room;
import cn.airnan.dg.service.PlayerService;
import cn.airnan.dg.service.RoomService;
import cn.airnan.dg.utils.CommonResponse;
import jakarta.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;
/**
* 玩家相关接口
*/
@RestController
@RequestMapping("/api/player")
public class PlayerController {
private static final Logger log = LoggerFactory.getLogger(PlayerController.class);
@Resource
private PlayerService service;
/**
* 获取玩家列表
*
* @return 玩家列表
*/
@GetMapping("/list")
public CommonResponse list() {
try {
return CommonResponse.success(service.getPlayers());
} catch (Exception e) {
return CommonResponse.error(e.getMessage());
}
}
/**
* 判断玩家是否还在
*
* @return true/false
*/
@PostMapping("/isExist")
public CommonResponse isExist(@RequestBody Player player) {
try {
return CommonResponse.success(Global.PLAYERS.containsKey(player.getId()));
} catch (Exception e) {
return CommonResponse.error(e.getMessage());
}
}
/**
* 创建玩家
*
* @return 玩家信息
*/
@PostMapping("/create")
public CommonResponse create(@RequestBody Player player) {
try {
if (service.canCreatePlayer(player.getId())) {
var result = service.createPlayer(player);
if (result != null) {
log.info("【创建玩家】名称:{}", player.getId());
return CommonResponse.success(result);
} else {
return CommonResponse.error("创建玩家失败,请重试!");
}
} else {
return CommonResponse.error("玩家名称已存在,换一个吧!");
}
} catch (Exception e) {
return CommonResponse.error(e.getMessage());
}
}
}
@@ -0,0 +1,91 @@
package cn.airnan.dg.controller;
import cn.airnan.dg.configs.Global;
import cn.airnan.dg.entity.Player;
import cn.airnan.dg.entity.Room;
import cn.airnan.dg.service.RoomService;
import cn.airnan.dg.utils.CommonResponse;
import jakarta.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Objects;
/**
* 房间相关接口
*/
@RestController
@RequestMapping("/api/room")
public class RoomController {
private static final Logger log = LoggerFactory.getLogger(RoomController.class);
@Resource
private RoomService service;
/**
* 获取房间列表
*
* @return 房间列表
*/
@GetMapping("/list")
public CommonResponse list() {
try {
return CommonResponse.success(service.getRooms());
} catch (Exception e) {
return CommonResponse.error(e.getMessage());
}
}
/**
* 创建房间
*
* @return 房间信息
*/
@PostMapping("/create")
public CommonResponse create(@RequestBody Room room) {
try {
if (service.canCreateRoom(room.getId())) {
var result = service.createRoom(room);
if (result != null) {
log.info("【创建房间】{}创建房间:{}", room.getMaster(), room.getId());
return CommonResponse.success(result);
} else {
return CommonResponse.error("创建房间失败,请重试!");
}
} else {
return CommonResponse.error("房间名已存在,换一个吧!");
}
} catch (Exception e) {
return CommonResponse.error(e.getMessage());
}
}
/**
* 判断是否可以加入房间
*
* @return true/false
*/
@PostMapping("/canJoin")
public CommonResponse canJoin(@RequestBody Room room) {
try {
if (service.canJoinRoom(room.getId())) {
List<Player> players = Global.ROOMS.get(room.getId()).getPlayers();
for (Player player : players) {
if (Objects.equals(room.getPlayers().getFirst().getId(), player.getId())) {
return CommonResponse.error("已经在房间里了!");
}
}
return CommonResponse.success(true);
} else {
return CommonResponse.error("房间已满或已经开始游戏,再换一个吧!");
}
} catch (Exception e) {
return CommonResponse.error(e.getMessage());
}
}
}
@@ -0,0 +1,37 @@
package cn.airnan.dg.controller;
import cn.airnan.dg.service.TopicService;
import cn.airnan.dg.utils.CommonResponse;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 题目相关接口
*/
@RestController
@RequestMapping("/api/topic")
public class TopicController {
@Resource
private TopicService service;
/**
* 随机获取题目列表
*
* @return 题目列表
*/
@GetMapping("/random")
public CommonResponse random() {
try {
return CommonResponse.success(service.randomTopics(4));
} catch (Exception e) {
return CommonResponse.error(e.getMessage());
}
}
}
@@ -0,0 +1,18 @@
package cn.airnan.dg.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* Web首页
*/
@Controller
public class indexController {
@RequestMapping("/")
public String index() {
return "index.html";
}
}
@@ -0,0 +1,41 @@
package cn.airnan.dg.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.ArrayList;
import java.util.List;
/**
* 画板实体类
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Board {
/**
* 画板ID
*/
private int id;
/**
* 画笔点位列表
*/
private List<Brush> brushes = new ArrayList<>();
/**
* 添加画笔点位
*
* @param brush 画笔点位
*/
private void addDraw(Brush brush) {
this.brushes.add(brush);
}
/**
* 清空画板
*/
public void clear() {
brushes.clear();
}
}
@@ -0,0 +1,28 @@
package cn.airnan.dg.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 画笔实体类
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Brush {
/**
* 画笔颜色
*/
private String color;
/**
* 画笔粗细
*/
private String width;
/**
* 画笔坐标点
*/
private Point point;
}
@@ -0,0 +1,73 @@
package cn.airnan.dg.entity;
import cn.airnan.dg.utils.SocketMsg;
import com.alibaba.fastjson2.JSON;
import jakarta.websocket.Session;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 玩家实体类
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Player {
/**
* 玩家要绑定的会话
*/
private Session session;
/**
* 玩家ID
*/
private String id;
/**
* 玩家位置
*/
private Integer index;
/**
* 玩家分数
*/
private Integer score = 0;
/**
* 加分
*
* @param add 要增加的分数
*/
private void addScore(Integer add) {
this.score += add;
}
/**
* 重置分数
*/
private void clearScore() {
this.score = 0;
}
/**
* 发送消息给玩家
*
* @param message 要发送的消息
*/
public void sendMessage(String type, String message) {
Session session = getSession();
if (session != null && session.isOpen()) {
try {
SocketMsg msg = new SocketMsg();
msg.setType(type);
msg.setData(message);
session.getAsyncRemote().sendText(JSON.toJSONString(msg));
} catch (Exception e) {
// 处理发送消息失败的情况
e.printStackTrace();
}
}
}
}
@@ -0,0 +1,22 @@
package cn.airnan.dg.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 坐标点实体类
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Point {
/**
* X轴坐标点
*/
private double x;
/**
* Y轴坐标点
*/
private double y;
}
@@ -0,0 +1,99 @@
package cn.airnan.dg.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
/**
* 房间实体类
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Room {
private static final Logger log = LoggerFactory.getLogger(Room.class);
/**
* 房间ID
*/
private String id;
/**
* 房主ID
*/
private String master;
/**
* 玩家列表(最多五人)
*/
private List<Player> players = new ArrayList<>(5);
/**
* 画板
*/
private Board board;
/**
* 题目
*/
private Topic topic;
/**
* 游戏开始状态
*/
private boolean isBegin = false;
/**
* 倒计时
*/
private Integer countDown = 0;
/**
* 添加玩家
*
* @param player 要添加的玩家
*/
public void addPlayer(Player player) {
if (players.size() < 5) {
players.add(player);
} else {
System.out.println("房间已满,无法添加更多玩家。");
}
}
/**
* 根据玩家id移除房间内的玩家
*
* @param playerId 要移除的玩家id
*/
public void removePlayerById(String playerId) {
Player player = new Player();
player.setId(playerId);
players.remove(player);
}
/**
* 广播消息给房间内所有玩家
*
* @param type 消息类型
* @param message 要广播的消息
* @param playerId 发送者的ID(用于排除发送者)
*/
public void sendMessage(String type, String playerId, String message) {
log.info("【房间消息】{}: {}", id, message);
for (Player player : players) {
// 向除自己之外的玩家发送消息
// if (!player.getId().equals(playerId)) {
// player.sendMessage(type, message);
// }
// 向房间内所有玩家发送
player.sendMessage(type, message);
}
}
}
@@ -0,0 +1,25 @@
package cn.airnan.dg.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 题目实体类
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Topic {
/**
* 题目名称
*/
private String name;
/**
* 题目提示
*/
private String tip;
}
@@ -0,0 +1,13 @@
package cn.airnan.dg.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class RoomListVO {
private String id;
private String master;
}
@@ -0,0 +1,53 @@
package cn.airnan.dg.service;
import cn.airnan.dg.configs.Global;
import cn.airnan.dg.entity.Player;
import org.springframework.stereotype.Service;
import java.util.Map;
/**
* 玩家相关业务逻辑
*/
@Service
public class PlayerService {
/**
* 获取玩家列表
*
* @return
*/
public Map<String, Player> getPlayers() {
return Global.PLAYERS;
}
/**
* 创建玩家
*
* @return
*/
public Player createPlayer(Player player) {
try {
Global.PLAYERS.put(player.getId(), player);
return player;
}catch (Exception e){
return null;
}
}
/**
* 判断是否可以创建玩家
*
* @return true/false
*/
public boolean canCreatePlayer(String playerId) {
boolean exist = Global.PLAYERS.containsKey(playerId);
if (exist) {
return false;
}
return true;
}
}
@@ -0,0 +1,73 @@
package cn.airnan.dg.service;
import cn.airnan.dg.configs.Global;
import cn.airnan.dg.entity.Room;
import cn.airnan.dg.pojo.RoomListVO;
import org.springframework.stereotype.Service;
import java.util.*;
/**
* 房间相关业务逻辑
*/
@Service
public class RoomService {
/**
* 获取房间列表
*
* @return
*/
public List<RoomListVO> getRooms() {
List<RoomListVO> list = new ArrayList<>();
Global.ROOMS.values().forEach(room -> {
RoomListVO roomListVO = new RoomListVO();
roomListVO.setId(room.getId());
roomListVO.setMaster(room.getMaster());
list.add(roomListVO);
});
return list;
}
/**
* 创建房间
*
* @return
*/
public Room createRoom(Room room) {
try {
Global.ROOMS.put(room.getId(), room);
return room;
} catch (Exception e) {
return null;
}
}
/**
* 判断是否可以创建房间
*
* @return true/false
*/
public boolean canCreateRoom(String roomId) {
boolean exist = Global.ROOMS.containsKey(roomId);
if (exist) {
return false;
}
return true;
}
/**
* 判断是否可以加入房间
*
* @return true/false
*/
public boolean canJoinRoom(String roomId) {
int result = 5 - Global.ROOMS.get(roomId).getPlayers().size();
// 有空位并且游戏没有开始
if (result > 0 && !Global.ROOMS.get(roomId).isBegin()) {
return true;
}
return false;
}
}
@@ -0,0 +1,70 @@
package cn.airnan.dg.service;
import cn.airnan.dg.DrawAndGuessApplication;
import cn.airnan.dg.configs.Global;
import cn.airnan.dg.entity.Topic;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 题目相关业务逻辑
*/
@Slf4j
@Service
public class TopicService {
/**
* 从文件加载题库
*/
public static void loadTopics() {
try (InputStream inputStream = DrawAndGuessApplication.class.getResourceAsStream("/topic.json")) {
if (inputStream != null) {
String content = new String(inputStream.readAllBytes());
JSONArray jsonArray = JSON.parseArray(content);
for (Object obj : jsonArray) {
JSONObject jsonObject = (JSONObject) obj;
for (String key : jsonObject.keySet()) {
String value = jsonObject.getString(key);
Global.TOPICS.add(new Topic(key, value));
}
}
log.info("共加载{}条题目", Global.TOPICS.size());
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 随机取出N道题目
*
* @param n 取出的题目数量
* @return 题目列表
*/
public List<Topic> randomTopics(int n) {
Map<Integer, String> map = new HashMap();
List<Topic> news = new ArrayList();
if (Global.TOPICS.size() <= n) {
return Global.TOPICS;
} else {
while (map.size() < n) {
int random = (int) (Math.random() * Global.TOPICS.size());
if (!map.containsKey(random)) {
map.put(random, "");
news.add(Global.TOPICS.get(random));
}
}
return news;
}
}
}
@@ -0,0 +1,24 @@
package cn.airnan.dg.utils;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class CommonResponse<T> {
private int code;
private String msg;
private T data;
public static <T> CommonResponse<T> success(T data) {
return new CommonResponse<T>(0, "success", data);
}
public static <T> CommonResponse<T> success() {
return new CommonResponse<T>(0, "success", null);
}
public static <T> CommonResponse<T> error(String msg) {
return new CommonResponse<T>(1, msg, null);
}
}
@@ -0,0 +1,35 @@
package cn.airnan.dg.utils;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* Socket响应体
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SocketMsg<T> {
/**
* 消息类型
*/
private String type;
/**
* 消息发送人
*/
private String playerId;
/**
* 消息发送房间
*/
private String roomId;
/**
* 消息内容
*/
private T data;
}
@@ -0,0 +1,174 @@
package cn.airnan.dg.utils;
import cn.airnan.dg.configs.Global;
import cn.airnan.dg.entity.Player;
import cn.airnan.dg.entity.Room;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.TypeReference;
import jakarta.websocket.OnClose;
import jakarta.websocket.OnMessage;
import jakarta.websocket.OnOpen;
import jakarta.websocket.Session;
import jakarta.websocket.server.PathParam;
import jakarta.websocket.server.ServerEndpoint;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.Map;
@Component
@Slf4j
@ServerEndpoint("/websocket/{playerId}/{roomId}")
public class WebSocket {
/**
* 加入房间时触发WebSocket事件
*
* @param session 当前会话
* @param playerId 玩家id
* @param roomId 房间id
*/
@OnOpen
public void onOpen(Session session, @PathParam(value = "playerId") String playerId, @PathParam(value = "roomId") String roomId) {
try {
// 将会话添加到会话列表
Global.SESSIONS.add(session);
// 将会话添加到连接池
Global.SESSION_POOL.put(playerId, session);
// 将会话与玩家绑定
Player player = Global.PLAYERS.get(playerId);
if (player != null) {
player.setSession(session);
}
// 加入房间
Room room = Global.ROOMS.get(roomId);
if (room != null) {
room.addPlayer(player);
}
// 通知其他玩家有新玩家加入
if (room != null) {
String message = "玩家【" + playerId + "】加入了房间";
room.sendMessage(Global.MSG_TYPE_ROOM_MSG, playerId, message);
room.sendMessage(Global.MSG_TYPE_ROOM_CHAT, playerId, "系统:"+message);
}
log.info("【加入房间】玩家 {} 进入房间 {},房间人数为:{}", playerId, roomId, Global.SESSIONS.size());
} catch (Exception e) {
log.error(e.getMessage());
}
}
@OnClose
public void onClose(Session session) {
try {
// 从会话列表中移除关闭的会话
Global.SESSIONS.remove(session);
// 从连接池中移除关闭的会话
String playerIdToRemove = null;
for (Map.Entry<String, Session> entry : Global.SESSION_POOL.entrySet()) {
if (entry.getValue().equals(session)) {
playerIdToRemove = entry.getKey();
break;
}
}
if (playerIdToRemove != null) {
Global.SESSION_POOL.remove(playerIdToRemove);
}
String playerId = "";
// 清除玩家与会话的绑定
for (Player player : Global.PLAYERS.values()) {
if (player.getSession() != null && player.getSession().equals(session)) {
player.setSession(null);
playerId = player.getId();
break;
}
}
// 从房间中移除玩家
for (Room room : Global.ROOMS.values()) {
room.removePlayerById(playerId);
String message = "玩家【" + playerId + "】退出了游戏,房间人数为:" + room.getPlayers().size();
room.sendMessage(Global.MSG_TYPE_ROOM_MSG, playerId, message);
room.sendMessage(Global.MSG_TYPE_ROOM_CHAT, playerId, "系统:"+message);
}
} catch (Exception e) {
log.error(e.getMessage());
}
}
@OnMessage
public void onMessage(String message) {
// log.info("【WebSocket消息】收到客户端消息:{}", message);
try {
// 解析消息为 SocketMsg 结构体
SocketMsg<String> socketMsg = JSON.parseObject(message, new TypeReference<>() {
});
// 匹配房间并转发消息
Global.ROOMS.get(socketMsg.getRoomId()).sendMessage(socketMsg.getType(), socketMsg.getPlayerId(), socketMsg.getData());
} catch (Exception e) {
log.error("解析消息失败:{}", message);
}
}
/**
* 此为广播消息
*
* @param message 消息
*/
public static void sendAllMessage(String message) {
for (Session session : Global.SESSIONS) {
try {
if (session.isOpen()) {
session.getAsyncRemote().sendText(message);
}
} catch (Exception e) {
log.error(e.getMessage());
}
}
}
/**
* 此为单点消息
*
* @param playerId 用户编号
* @param message 消息
*/
public static void sendOneMessage(String playerId, String message) {
Session session = Global.SESSION_POOL.get(playerId);
if (session != null && session.isOpen()) {
try {
synchronized (session) {
session.getAsyncRemote().sendText(message);
}
} catch (Exception e) {
log.error(e.getMessage());
}
}
}
/**
* 此为单点消息(多人)
*
* @param playerIds 用户编号列表
* @param message 消息
*/
public static void sendMoreMessage(String[] playerIds, String message) {
for (String playerId : playerIds) {
Session session = Global.SESSION_POOL.get(playerId);
if (session != null && session.isOpen()) {
try {
session.getAsyncRemote().sendText(message);
} catch (Exception e) {
log.error(e.getMessage());
}
}
}
}
public static Integer getOnLine() {
return Global.SESSION_POOL.size();
}
}