|
@@ -2,6 +2,7 @@ package com.fs.app.websocket.service;
|
|
|
|
|
|
|
|
|
import com.alibaba.fastjson.JSONObject;
|
|
|
+import com.fs.app.websocket.auth.WebSocketConfigurator;
|
|
|
import com.fs.app.websocket.bean.SendMsgVo;
|
|
|
import com.fs.common.core.domain.R;
|
|
|
import com.fs.common.core.redis.RedisCache;
|
|
@@ -10,105 +11,188 @@ import com.fs.common.utils.StringUtils;
|
|
|
import com.fs.common.utils.spring.SpringUtils;
|
|
|
import com.fs.his.domain.FsUser;
|
|
|
import com.fs.his.service.IFsUserService;
|
|
|
+import com.fs.live.domain.LiveData;
|
|
|
import com.fs.live.domain.LiveMsg;
|
|
|
import com.fs.live.domain.LiveWatchUser;
|
|
|
+import com.fs.live.service.ILiveDataService;
|
|
|
import com.fs.live.service.ILiveMsgService;
|
|
|
import com.fs.live.service.ILiveService;
|
|
|
import com.fs.live.service.ILiveWatchUserService;
|
|
|
+import com.fs.live.vo.LiveWatchUserVO;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.springframework.scheduling.annotation.Scheduled;
|
|
|
import org.springframework.stereotype.Component;
|
|
|
|
|
|
import javax.websocket.*;
|
|
|
import javax.websocket.server.ServerEndpoint;
|
|
|
import java.io.IOException;
|
|
|
-import java.util.Date;
|
|
|
-import java.util.Map;
|
|
|
+import java.util.*;
|
|
|
import java.util.concurrent.ConcurrentHashMap;
|
|
|
-import java.util.stream.Collectors;
|
|
|
+import java.util.concurrent.CopyOnWriteArrayList;
|
|
|
+import java.util.concurrent.TimeUnit;
|
|
|
|
|
|
-@ServerEndpoint("/app/webSocket")
|
|
|
+@ServerEndpoint(value = "/app/webSocket",configurator = WebSocketConfigurator.class)
|
|
|
@Component
|
|
|
+@Slf4j
|
|
|
public class WebSocketServer {
|
|
|
|
|
|
-
|
|
|
- //concurrent包的线程安全Set,用来存放每个客户端对应的WebSocketServer对象。
|
|
|
- private static ConcurrentHashMap<Long, Session> sessionPools = new ConcurrentHashMap<>();
|
|
|
+ // 直播间用户session
|
|
|
+ private final static ConcurrentHashMap<Long, ConcurrentHashMap<Long, Session>> rooms = new ConcurrentHashMap<>();
|
|
|
+ // 管理端连接
|
|
|
+ private final static ConcurrentHashMap<Long, CopyOnWriteArrayList<Session>> adminRooms = new ConcurrentHashMap<>();
|
|
|
private final RedisCache redisCache = SpringUtils.getBean(RedisCache.class);
|
|
|
private final ILiveMsgService liveMsgService = SpringUtils.getBean(ILiveMsgService.class);
|
|
|
private final ILiveService liveService = SpringUtils.getBean(ILiveService.class);
|
|
|
private final ILiveWatchUserService liveWatchUserService = SpringUtils.getBean(ILiveWatchUserService.class);
|
|
|
private final IFsUserService fsUserService = SpringUtils.getBean(IFsUserService.class);
|
|
|
+ private final ILiveDataService liveDataService = SpringUtils.getBean(ILiveDataService.class);
|
|
|
+ // 直播间在线用户缓存
|
|
|
+ //private static final ConcurrentHashMap<Long, Integer> liveOnlineUsers = new ConcurrentHashMap<>();
|
|
|
|
|
|
- //发送消息
|
|
|
- public void sendMessage(Session session, String message) throws IOException {
|
|
|
- if (session != null) {
|
|
|
- synchronized (session) {
|
|
|
- System.out.println("发送数据:" + message);
|
|
|
- session.getBasicRemote().sendText(message);
|
|
|
- }
|
|
|
+ private static final String USER_VISIT_KEY = "live:user:visit:"; // 用户访问标识用于判断是否是首次访问
|
|
|
+ private static final String UNIQUE_VISITORS_KEY = "live:unique:visitors:"; //访客数
|
|
|
+ private static final String UNIQUE_VIEWERS_KEY = "live:unique:viewers:"; //累计观看人数
|
|
|
+ private static final String PAGE_VIEWS_KEY = "live:page:views:"; //浏览量
|
|
|
+ private static final String TOTAL_VIEWS_KEY = "live:total:views:"; //累计观看人次
|
|
|
+ private static final String MAX_ONLINE_USERS_KEY = "live:max:online:"; //最大在线人数
|
|
|
+ private static final String ONLINE_USERS_KEY = "live:online:users:"; //当前在线人数
|
|
|
+ //建立连接成功调用
|
|
|
+ @OnOpen
|
|
|
+ public void onOpen(Session session) {
|
|
|
+
|
|
|
+ Map<String, Object> userProperties = session.getUserProperties();
|
|
|
+ long liveId = (long) userProperties.get("liveId");
|
|
|
+ long userId = (long) userProperties.get("userId");
|
|
|
+ long userType = (long) userProperties.get("userType");
|
|
|
+
|
|
|
+ if (liveService.getById(liveId) == null) {
|
|
|
+ throw new BaseException("未找到直播间");
|
|
|
}
|
|
|
- }
|
|
|
|
|
|
- //给指定用户发送信息
|
|
|
- public void sendInfo(String id, String message) {
|
|
|
- Session session = sessionPools.get(id);
|
|
|
- try {
|
|
|
- if (session != null) {
|
|
|
+ ConcurrentHashMap<Long, Session> room = getRoom(liveId);
|
|
|
+ List<Session> adminRoom = getAdminRoom(liveId);
|
|
|
|
|
|
- sendMessage(session, message);
|
|
|
+ // 记录连接信息 管理员不记录
|
|
|
+ if (userType == 0) {
|
|
|
+ FsUser fsUser = fsUserService.selectFsUserByUserId(userId);
|
|
|
+ if (Objects.isNull(fsUser)) {
|
|
|
+ throw new BaseException("用户信息错误");
|
|
|
}
|
|
|
|
|
|
- } catch (Exception e) {
|
|
|
- e.printStackTrace();
|
|
|
- }
|
|
|
- }
|
|
|
+ liveWatchUserService.join(liveId, userId);
|
|
|
+ room.put(userId, session);
|
|
|
+ // 直播间浏览量 +1
|
|
|
+ redisCache.increment(PAGE_VIEWS_KEY + liveId, 1);
|
|
|
|
|
|
- //建立连接成功调用
|
|
|
- @OnOpen
|
|
|
- public void onOpen(Session session) {
|
|
|
- Map<String, String> params = getParams(session);
|
|
|
- if(!params.containsKey("liveId")) throw new BaseException("未找到直播间");
|
|
|
- if(!params.containsKey("userId")) throw new BaseException("用户信息错误");
|
|
|
- long liveId = Long.parseLong(params.get("liveId"));
|
|
|
- long userId = Long.parseLong(params.get("userId"));
|
|
|
- liveWatchUserService.join(liveId, userId);
|
|
|
- if (liveService.getById(liveId) == null) throw new BaseException("未找到直播间");
|
|
|
- sessionPools.put(liveId, session);
|
|
|
- System.out.println(liveId + "加入webSocket!当前人数为" + sessionPools.size());
|
|
|
+ // 累计观看人次 +1
|
|
|
+ redisCache.increment(TOTAL_VIEWS_KEY + liveId, 1);
|
|
|
|
|
|
+ // 记录在线人数
|
|
|
+ redisCache.increment(ONLINE_USERS_KEY + liveId, 1);
|
|
|
+ Integer currentOnline = redisCache.getCacheObject(ONLINE_USERS_KEY + liveId);
|
|
|
+ //最大同时在线人数
|
|
|
+ Integer maxOnline = redisCache.getCacheObject(MAX_ONLINE_USERS_KEY + liveId);
|
|
|
+ if (maxOnline == null || currentOnline > maxOnline) {
|
|
|
+ redisCache.setCacheObject(MAX_ONLINE_USERS_KEY + liveId, currentOnline);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 判断是否是该直播间的首次访客(独立访客统计)
|
|
|
+ boolean isFirstVisit = redisCache.setIfAbsent(USER_VISIT_KEY + userId, 1, 1, TimeUnit.DAYS);
|
|
|
+ if (isFirstVisit) {
|
|
|
+
|
|
|
+ redisCache.increment(UNIQUE_VISITORS_KEY + liveId, 1);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 判断是否是首次进入直播间的观众
|
|
|
+ boolean isFirstViewer = redisCache.setIfAbsent(UNIQUE_VIEWERS_KEY + liveId + ":" + userId, 1, 1, TimeUnit.DAYS);
|
|
|
+ if (isFirstViewer) {
|
|
|
+ redisCache.increment(UNIQUE_VIEWERS_KEY + liveId, 1);
|
|
|
+ }
|
|
|
+
|
|
|
+ LiveWatchUserVO liveWatchUserVO = liveWatchUserService.selectWatchUserByLiveIdAndUserId(liveId, userId);
|
|
|
+
|
|
|
+ SendMsgVo sendMsgVo = new SendMsgVo();
|
|
|
+ sendMsgVo.setLiveId(liveId);
|
|
|
+ sendMsgVo.setUserId(userId);
|
|
|
+ sendMsgVo.setUserType(userType);
|
|
|
+ sendMsgVo.setCmd("entry");
|
|
|
+ sendMsgVo.setMsg("用户进入");
|
|
|
+ sendMsgVo.setData(JSONObject.toJSONString(liveWatchUserVO));
|
|
|
+ sendMsgVo.setNickName(fsUser.getNickName());
|
|
|
+ sendMsgVo.setAvatar(fsUser.getAvatar());
|
|
|
+
|
|
|
+ // 广播连接消息
|
|
|
+ broadcastMessage(liveId, JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
|
|
|
+ } else {
|
|
|
+ adminRoom.add(session);
|
|
|
+ }
|
|
|
+
|
|
|
+ log.debug("加入webSocket liveId: {}, userId: {}, 直播间人数: {}", liveId, userId, room.size());
|
|
|
}
|
|
|
|
|
|
//关闭连接时调用
|
|
|
@OnClose
|
|
|
public void onClose(Session session) {
|
|
|
- Map<String, String> params = getParams(session);
|
|
|
- long liveId = Long.parseLong(params.get("liveId"));
|
|
|
- long userId = Long.parseLong(params.get("userId"));
|
|
|
- sessionPools.remove(liveId);
|
|
|
- liveWatchUserService.close(liveId, userId);
|
|
|
- System.out.println(liveId + "断开webSocket连接!当前人数为" + sessionPools.size());
|
|
|
+ Map<String, Object> userProperties = session.getUserProperties();
|
|
|
+
|
|
|
+ long liveId = (long) userProperties.get("liveId");
|
|
|
+ long userId = (long) userProperties.get("userId");
|
|
|
+ long userType = (long) userProperties.get("userType");
|
|
|
+
|
|
|
+ ConcurrentHashMap<Long, Session> room = getRoom(liveId);
|
|
|
+ List<Session> adminRoom = getAdminRoom(liveId);
|
|
|
+ if (userType == 0) {
|
|
|
+ FsUser fsUser = fsUserService.selectFsUserByUserId(userId);
|
|
|
+ if (Objects.isNull(fsUser)) {
|
|
|
+ throw new BaseException("用户信息错误");
|
|
|
+ }
|
|
|
+
|
|
|
+ liveWatchUserService.close(liveId, userId);
|
|
|
+ room.remove(userId);
|
|
|
+
|
|
|
+ if (room.isEmpty()) {
|
|
|
+ rooms.remove(liveId);
|
|
|
+ }
|
|
|
+
|
|
|
+ LiveWatchUserVO liveWatchUserVO = liveWatchUserService.selectWatchUserByLiveIdAndUserId(liveId, userId);
|
|
|
+
|
|
|
+ // 直播间在线人数 -1
|
|
|
+ redisCache.increment(ONLINE_USERS_KEY + liveId, -1);
|
|
|
+ SendMsgVo sendMsgVo = new SendMsgVo();
|
|
|
+ sendMsgVo.setLiveId(liveId);
|
|
|
+ sendMsgVo.setUserId(userId);
|
|
|
+ sendMsgVo.setUserType(userType);
|
|
|
+ sendMsgVo.setCmd("out");
|
|
|
+ sendMsgVo.setMsg("用户离开");
|
|
|
+ sendMsgVo.setData(JSONObject.toJSONString(liveWatchUserVO));
|
|
|
+ sendMsgVo.setNickName(fsUser.getNickName());
|
|
|
+ sendMsgVo.setAvatar(fsUser.getAvatar());
|
|
|
+
|
|
|
+ // 广播离开消息
|
|
|
+ broadcastMessage(liveId, JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
|
|
|
+ } else {
|
|
|
+ adminRoom.remove(session);
|
|
|
+ }
|
|
|
+
|
|
|
+ log.debug("断开webSocket liveId: {}, userId: {}, 直播间人数: {}", liveId, userId, room.size());
|
|
|
}
|
|
|
|
|
|
//收到客户端信息
|
|
|
@OnMessage
|
|
|
- public void onMessage(String message) throws IOException {
|
|
|
+ public void onMessage(Session session,String message) throws IOException {
|
|
|
+ Map<String, Object> userProperties = session.getUserProperties();
|
|
|
+
|
|
|
+ long liveId = (long) userProperties.get("liveId");
|
|
|
+ long userType = (long) userProperties.get("userType");
|
|
|
+
|
|
|
SendMsgVo msg = JSONObject.parseObject(message, SendMsgVo.class);
|
|
|
if(msg.isOn()) return;
|
|
|
- Session session;
|
|
|
- System.out.println("收到数据" + msg.getCmd());
|
|
|
try {
|
|
|
switch (msg.getCmd()) {
|
|
|
case "heartbeat":
|
|
|
- session = sessionPools.get(msg.getUserId());
|
|
|
- sendMessage(session, JSONObject.toJSONString(msg));
|
|
|
+ sendMessage(session, JSONObject.toJSONString(R.ok().put("data", msg)));
|
|
|
break;
|
|
|
case "sendMsg":
|
|
|
- session = sessionPools.get(msg.getLiveId());
|
|
|
- if (session == null) return;
|
|
|
- LiveWatchUser liveWatchUser = liveWatchUserService.getByLiveIdAndUserId(msg.getLiveId(), msg.getUserId());
|
|
|
- if(liveWatchUser.getMsgStatus() == 1){
|
|
|
- sendMessage(session, JSONObject.toJSONString(R.error("你以被禁言")));
|
|
|
- return;
|
|
|
- }
|
|
|
LiveMsg liveMsg = new LiveMsg();
|
|
|
liveMsg.setLiveId(msg.getLiveId());
|
|
|
liveMsg.setUserId(msg.getUserId());
|
|
@@ -116,26 +200,111 @@ public class WebSocketServer {
|
|
|
liveMsg.setAvatar(msg.getAvatar());
|
|
|
liveMsg.setMsg(msg.getMsg());
|
|
|
liveMsg.setCreateTime(new Date());
|
|
|
- liveMsgService.save(liveMsg);
|
|
|
+
|
|
|
+ if (userType == 0) {
|
|
|
+ LiveWatchUser liveWatchUser = liveWatchUserService.getByLiveIdAndUserId(msg.getLiveId(), msg.getUserId());
|
|
|
+ if(liveWatchUser.getMsgStatus() == 1){
|
|
|
+ sendMessage(session, JSONObject.toJSONString(R.error("你以被禁言")));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ liveMsgService.save(liveMsg);
|
|
|
+ }
|
|
|
+
|
|
|
msg.setOn(true);
|
|
|
- sendMessage(session, JSONObject.toJSONString(R.ok().put("data", msg)));
|
|
|
- break;
|
|
|
+ msg.setData(JSONObject.toJSONString(liveMsg));
|
|
|
|
|
|
+ // 广播消息
|
|
|
+ broadcastMessage(liveId, JSONObject.toJSONString(R.ok().put("data", msg)));
|
|
|
+ break;
|
|
|
}
|
|
|
} catch (Exception e) {
|
|
|
- System.out.println("收到数据" + e.getMessage());
|
|
|
+ log.error("webSocket 消息处理失败 msg: {}", e.getMessage(), e);
|
|
|
}
|
|
|
-
|
|
|
}
|
|
|
|
|
|
//错误时调用
|
|
|
@OnError
|
|
|
public void onError(Session session, Throwable throwable) {
|
|
|
- System.out.println("发生错误" + throwable.getMessage());
|
|
|
- throwable.printStackTrace();
|
|
|
+ log.error("webSocKet连接错误 msg: {}", throwable.getMessage(), throwable);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取房间
|
|
|
+ * @param liveId 直播间ID
|
|
|
+ * @return 容器
|
|
|
+ */
|
|
|
+ private ConcurrentHashMap<Long, Session> getRoom(Long liveId) {
|
|
|
+ return rooms.computeIfAbsent(liveId, k -> new ConcurrentHashMap<>());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取管理端房间
|
|
|
+ * @param liveId 直播间ID
|
|
|
+ * @return 容器
|
|
|
+ */
|
|
|
+ private List<Session> getAdminRoom(Long liveId) {
|
|
|
+ return adminRooms.computeIfAbsent(liveId, k -> new CopyOnWriteArrayList<>());
|
|
|
}
|
|
|
|
|
|
- private Map<String, String> getParams(Session session){
|
|
|
- return session.getRequestParameterMap().entrySet().stream().filter(e -> e.getValue() != null && !e.getValue().isEmpty() && StringUtils.isNotEmpty(e.getValue().get(0))).collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get(0)));
|
|
|
+ //发送消息
|
|
|
+ public void sendMessage(Session session, String message) throws IOException {
|
|
|
+ session.getAsyncRemote().sendText(message);
|
|
|
}
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 广播消息
|
|
|
+ * @param liveId 直播间ID
|
|
|
+ * @param message 消息内容
|
|
|
+ */
|
|
|
+ public void broadcastMessage(Long liveId, String message) {
|
|
|
+ ConcurrentHashMap<Long, Session> room = getRoom(liveId);
|
|
|
+ List<Session> adminRoom = getAdminRoom(liveId);
|
|
|
+
|
|
|
+ room.forEach((k, v) -> v.getAsyncRemote().sendText(message));
|
|
|
+ adminRoom.forEach(v -> v.getAsyncRemote().sendText(message));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ *定期将缓存的数据写入数据库
|
|
|
+ */
|
|
|
+ @Scheduled(fixedRate = 60000) // 每分钟执行一次
|
|
|
+ public void syncLiveDataToDB() {
|
|
|
+ List<Long> liveIds = liveDataService.getAllLiveIds(); // 获取所有正在直播的直播间ID
|
|
|
+ for (Long liveId : liveIds) {
|
|
|
+ LiveData liveData = liveDataService.selectLiveDataByLiveId(liveId);
|
|
|
+ if (liveData == null) {
|
|
|
+ continue; // 防止空指针异常
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ // 从 redis 获取数据,并提供默认值,避免 NPE
|
|
|
+ liveData.setPageViews(
|
|
|
+ Optional.ofNullable(redisCache.incrementCacheValue(PAGE_VIEWS_KEY + liveId,0)).orElse(0L)
|
|
|
+ );
|
|
|
+ liveData.setTotalViews(
|
|
|
+ Optional.ofNullable(redisCache.incrementCacheValue(TOTAL_VIEWS_KEY + liveId,0)).orElse(0L)
|
|
|
+ );
|
|
|
+ liveData.setUniqueVisitors(
|
|
|
+ Optional.ofNullable(redisCache.getCacheSet(UNIQUE_VISITORS_KEY + liveId))
|
|
|
+ .map(Set::size) // 获取集合大小
|
|
|
+ .map(Long::valueOf) // 转换为 Long 类型
|
|
|
+ .orElse(0L)
|
|
|
+ );
|
|
|
+ liveData.setUniqueViewers(
|
|
|
+ Optional.ofNullable(redisCache.getCacheSet(UNIQUE_VIEWERS_KEY + liveId))
|
|
|
+ .map(Set::size) // 获取集合大小
|
|
|
+ .map(Long::valueOf) // 转换为 Long 类型
|
|
|
+ .orElse(0L)
|
|
|
+ );
|
|
|
+ liveData.setPeakConcurrentViewers(
|
|
|
+ Optional.ofNullable(redisCache.incrementCacheValue(MAX_ONLINE_USERS_KEY + liveId,0)).orElse(0L)
|
|
|
+ );
|
|
|
+
|
|
|
+ // 更新数据库
|
|
|
+ liveDataService.updateLiveData(liveData);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
}
|