拿来主义——WebSocket多人聊天室和聊天列表实现

最近领导让做一个多人聊天功能,小编抠脑壳硬是半天没写出来个所以然,于是网上找了个多人聊天室Demo改出来一款基于SpringBoot实现的实用性较强的聊天室功能(也适用于单人聊天)。

思想:总共用两个连接,一个用于聊天室消息监听,一个用于监听聊天室列表未读消息。
调用顺序:先连接聊天室列表,再连接聊天室。
当有用户在聊天室发送消息时,消息正常推送,同步调用聊天室列表推送消息,通过所有房间在线人数和用户所属聊天室关系过滤,找出对应聊天室需要推送未读消息的在线用户。然后推送未读消息。逻辑大概是这样。

下面看实现:

pom引入的依赖

<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-websocket</artifactId>
		</dependency>
		<dependency>
			<groupId>jakarta.websocket</groupId>
			<artifactId>jakarta.websocket-api</artifactId>
			<version>1.1.2</version>
			<scope>compile</scope>
		</dependency>

WebSocket配置


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

@Configuration
public class WebSocketConfig {

    /**
     * 这个bean的注册,用于扫描带有@ServerEndpoint的注解成为websocket,如果你使用外置的tomcat就不需要该配置文件
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }

	/**
	 * 注入service
	 * @param complainService
	 */
	@Autowired
	public void setChatRoomMessageService(ChatRoomMessageService chatRoomMessageService){
		WebSocketUtil.chatRoomMessageService = chatRoomMessageService;
	}
}

聊天传参

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

/**
 * @author ywb
 * @date 2024/7/29 16:14
 */
@Data
public class SocketMsg {

	@Schema(description = "聊天类型 0 全局广播 1 单聊 2 群聊")
	private int type;

	@Schema(description = "发送者id")
	private Long userId;

	@Schema(description = "发送者")
	private String userName;

	@Schema(description = "接收者")
	private String receiveUser;

	@Schema(description = "房间id")
	private Long roomId;

	@Schema(description = "消息")
	private String content;

	@Schema(description = "消息类型(IMAGE-照片,VIDEO-视频,VOICEMAIL-语音,TITLE-文字)")
	private ChatRoomMessageTypeEnum messageType;
}

聊天列表和详情公用的参数

import lombok.Data;

import javax.websocket.Session;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author ywb
 * @date 2024/8/2 9:07
 */
@Data
public abstract class WebSocketShareParam {

	/**
	 * 存放Session集合,方便推送消息 (jakarta.websocket)
	 */
	public static ConcurrentHashMap<String, Session> sessionMap = new ConcurrentHashMap<>();

	/**
	 * 存放房间用户集合,方便推送消息 (jakarta.websocket)
	 */
	public static HashMap<Long, List<Long>> groupSessionMap = new HashMap<>();

	public static ChatRoomMessageService chatRoomMessageService;

}

聊天室连接

import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSON;
import cn.hutool.json.JSONUtil;

import javax.annotation.Resource;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;

import com.google.gson.Gson;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

/**
 * @author ywb
 * @date 2024/7/29
 * @description WebSocket工具类
 */
@Slf4j
@Component
@ServerEndpoint("/websocket/{userId}/{roomId}")
public class WebSocketUtil extends WebSocketShareParam{

	private Long userId;
	private Session session;
	private Long roomId;

	/**
	 * 群聊:向指定房间ID推送消息
	 */
	public synchronized static void groupMessage(SocketMsg socketMsg) {
		// 存储房间号和用户信息
		Long roomId = socketMsg.getRoomId();
		// 判断是否有这个房间
		List<Long> strings = groupSessionMap.get(roomId);
		if (ObjectUtils.isEmpty(strings)) {
			List<Long> users = new ArrayList<>();
			users.add(socketMsg.getUserId());
			groupSessionMap.put(roomId, users);
		} else {
			// 这里应该写接口,先添加房间ID,简易写法直接传过来
			List<Long> users = groupSessionMap.get(roomId);
			Long sendOutUser = socketMsg.getUserId();
			boolean contains = users.contains(sendOutUser);
			if (!contains) {
				users.add(sendOutUser);
			}
		}

		// 发送给接收者
		if (roomId != null) {
			//保存历史消息
			Long messageId = chatRoomMessageService.sendMessage(socketMsg);
			ChatRoomMessageDTO dto = chatRoomMessageService.getNewMessages(messageId);
			JSON json = JSONUtil.parse(dto);
			// 发送给接收者
			log.info("发送消息对象:"+json.toString());
			// 此时要判断房间有哪些人,把这些消息定向发给处于此房间的用户
			List<Long> roomUser = groupSessionMap.get(roomId);

			for (Long userId : roomUser) {
				// 接收消息的用户
				Session receiveUser = sessionMap.get(roomId + "_" + userId);
				receiveUser.getAsyncRemote().sendObject(json.toString());
			}
			WebSocketListUtil.countUnreadMessage(roomId);
		} else {
			// 发送消息的用户
			log.info(socketMsg.getUserName() + " 私聊的用户 " + socketMsg.getReceiveUser() + " 不在线或者输入的用户名不对");
			Session sendOutUser = sessionMap.get(roomId + "_"  + socketMsg.getUserName());
			// 将系统提示推送给发送者
			sendOutUser.getAsyncRemote().sendObject("系统消息:对方不在线或者您输入的用户名不对");
		}
	}

	/**
	 * 私聊:向指定客户端推送消息
	 */
	public synchronized static void privateMessage(SocketMsg socketMsg) {
		Long roomId = socketMsg.getRoomId();
		// 接收消息的用户
		Session receiveUser = sessionMap.get(roomId + "_" + socketMsg.getReceiveUser());
		// 发送给接收者
		if (receiveUser != null) {
			// 发送给接收者
			log.info(socketMsg.getUserName() + " 向 " + socketMsg.getReceiveUser() + " 发送了一条消息:" + socketMsg.getContent());
			receiveUser.getAsyncRemote().sendObject(socketMsg.getUserName() + ":" + socketMsg.getContent());
		} else {
			// 发送消息的用户
			log.info(socketMsg.getUserName() + " 私聊的用户 " + socketMsg.getReceiveUser() + " 不在线或者输入的用户名不对");
			Session sendOutUser = sessionMap.get(roomId + "_" + socketMsg.getUserName());
			// 将系统提示推送给发送者
			sendOutUser.getAsyncRemote().sendObject("系统消息:对方不在线或者您输入的用户名不对");
		}
	}

	/**
	 * 聊天室里面推送消息
	 */
	public synchronized static void roomMessage(String room,List<Long> orDefault,String message) {
		for (Long userId : orDefault){
			Session session = sessionMap.get(room + userId);
			ChatRoomMessageDTO dto = new ChatRoomMessageDTO();
			dto.setType(ChatRoomMessageTypeEnum.SYSTEM);
			dto.setContent(message);
			session.getAsyncRemote().sendObject( JSONUtil.parse(dto).toString());
		}
		log.info("系统接收了一条消息:" + message);
	}

	/**
	 * 聊天室监听:连接成功
	 *
	 * @param session
	 * @param userId 连接的用户id
	 */
	@OnOpen
	public void onOpen(Session session, @PathParam("userId") Long userId, @PathParam("roomId") Long roomId) {
		this.userId = userId;
		this.session = session;
		this.roomId = roomId;
		String room = roomId +"_";
		String name = room + userId;
		List<Long> orDefault = groupSessionMap.getOrDefault(roomId, new ArrayList<>());
		if (!orDefault.contains(userId)){
			orDefault.add(userId);
			groupSessionMap.put(roomId, orDefault);
		}
		if (!sessionMap.containsKey(name)) {
			sessionMap.put(name, session);
			// 在线数加1
			String tips = userId + " 加入聊天室。当前聊天室人数为" + orDefault.size();
			log.info(tips);
			roomMessage(room,orDefault,tips);
		}
	}

	/**
	 * 聊天室监听: 连接关闭
	 */
	@OnClose
	public void onClose() {
		String room = roomId +"_";
		String name = room + userId;
		List<Long> list = groupSessionMap.get(roomId);
		list.remove(userId);
		// 连接关闭后,将此websocket从set中删除
		sessionMap.remove(name);
		String tips = userId + " 退出聊天室。当前聊天室人数为" + list.size();
		log.info(tips);
		roomMessage(room,list,tips);
	}

	/**
	 * 监听:收到客户端发送的消息
	 * @param message 发送的信息(json格式,里面是 SocketMsg 的信息)
	 */
	@OnMessage
	public void onMessage(String message) {
		if (JSONUtil.isTypeJSONObject(message)) {
			SocketMsg socketMsg = JSONUtil.toBean(message, SocketMsg.class);
			if (socketMsg.getType() == 2) {
				// 群聊,需要找到发送者和房间ID
				groupMessage(socketMsg);
			} else if (socketMsg.getType() == 1) {
				// 单聊,需要找到发送者和接受者
				privateMessage(socketMsg);
			} else {
				// 全局广播群发消息
//				publicMessage(socketMsg.getContent());
				// 聊天室广播群发消息
				List<Long> list = groupSessionMap.get(roomId);
				roomMessage(roomId +"_",list,socketMsg.getContent());
			}
		}
	}

	/**
	 * 监听:发生异常
	 * @param error
	 */
	@OnError
	public void onError(Throwable error) {
		log.info("userName为:" + userId + ",发生错误:" + error.getMessage());
		error.printStackTrace();
	}
}

聊天室列表未读消息连接

import cn.hutool.json.JSONUtil;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

/**
 * @author ywb
 * @date 2024/8/2 9:04
 */
@Slf4j
@Component
@ServerEndpoint("/websocket/list/{userId}")
public class WebSocketListUtil extends WebSocketShareParam{

	private Long userId;
	private Session session;

	/**
	 * 存放聊天室列表Session集合,推送消息
	 */
	public static ConcurrentHashMap<String, Session> roomListsessionMap = new ConcurrentHashMap<>();

	//用户所有的房间信息
	private static Map<Long, List<Long>> roomIdMap = new HashMap<>();

	/**
	 * 固定前缀
	 */
	private static final String ROOM_PREFIX = "ROOM_";

	/**
	 * 聊天室监听:连接成功
	 *
	 * @param session
	 * @param userId 连接的用户id
	 */
	@OnOpen
	public void onOpen(Session session, @PathParam("userId") Long userId) {
		this.userId = userId;
		this.session = session;

		List<Long> roomIds = chatRoomMessageService.selectRoomIds(userId);

		roomIdMap.put(userId, roomIds);

		String name = ROOM_PREFIX + userId;
		if (!roomListsessionMap.containsKey(name)) {
			roomListsessionMap.put(name, session);
			// 在线数加1
			String tips = userId + " 进入聊天室列表。当前聊天室列表人数为" + roomIdMap.size();
			log.info(tips);
			roomListMessage(tips);
		}
	}

	/**
	 * 聊天室监听: 连接关闭
	 */
	@OnClose
	public void onClose() {
		String name = ROOM_PREFIX + userId;
		roomIdMap.remove(userId);
		// 连接关闭后,将此websocket从set中删除
		roomListsessionMap.remove(name);
		String tips = userId + " 退出聊天室列表。当前聊天室列表人数为" + roomListsessionMap.size();
		log.info(tips);
		roomListMessage(tips);
	}

	/**
	 * 监听:收到客户端发送的消息
	 *
	 * @param param 房间id
	 */
	@OnMessage
	public void onMessage(String param) {
		if (JSONUtil.isTypeJSONObject(param)) {
			Long roomId = JSONUtil.parse(param).getByPath("roomId", Long.class);
//			groupMessage(roomId);
			countUnreadMessage(roomId);
		}
	}

	/**
	 * 统计在线用户roomId的未读消息总数
	 */
	public synchronized static void countUnreadMessage(Long roomId) {
	//聊天室列表在线用户,至少有1个(消息发送者)
		Enumeration<String> enumeration = roomListsessionMap.keys();
		List<Long> userIds = new ArrayList<>();
		while(enumeration.hasMoreElements()) {
			Long userId = Long.parseLong(enumeration.nextElement().replace(ROOM_PREFIX, ""));
			userIds.add(userId);
		}
		//统计房间对应在线用户的未读消息
		List<ChatRoomListDTO> unreadMessage = chatRoomMessageService.countUnreadMessageCount(userIds,roomId);

		for (ChatRoomListDTO dto : unreadMessage){
			Long userId = dto.getUserId();
			Session receiveUser = roomListsessionMap.get(ROOM_PREFIX + userId);
			if (receiveUser != null){
				receiveUser.getAsyncRemote().sendObject(JSONUtil.parse(dto).toString());
			}
		}		
	}

	/**
	 * 统计在线用户roomId的未读消息+1
	 * 群聊:向指定用户列表添加未读消息(过滤了当前在聊天室的用户,如果不过来这部分用户,可以省略判断,直接发送消息)
	 */
	public synchronized static void groupMessage(Long roomId) {
		//聊天室列表在线用户,至少有1个(消息发送者)
		Enumeration<String> enumeration = roomListsessionMap.keys();

		//聊天室内在线用户
		List<Long> orDefault = groupSessionMap.get(roomId);

		//过滤出所有不在该房间的在线用户未读消息+1
		while(enumeration.hasMoreElements()) {
			Long userId = Long.getLong(enumeration.nextElement().replace(ROOM_PREFIX, ""));
			if (orDefault.contains(userId)){
				continue;
			}
			//用户所有的聊天室
			List<Long> roomIds = roomIdMap.get(userId);
			if (roomIds.contains(roomId)){
				// 接收消息的用户
				Session receiveUser = null;
				for (Long room : roomIds){
					if (sessionMap.get(room + "_" + userId) != null){
						receiveUser = roomListsessionMap.get(ROOM_PREFIX + userId);
						break;
					}
				}
				if (receiveUser == null) continue;
				receiveUser.getAsyncRemote().sendText(userId + " 的房间 【 " + roomId + " 】 新增了一条未读消息。" );
			}
		}
	}

	public synchronized static void roomListMessage( String message) {
		for (Long userId : roomIdMap.keySet()){
			Session session = roomListsessionMap.get(ROOM_PREFIX + userId);
			session.getAsyncRemote().sendText( message);
			log.info("聊天室列表收了一条消息:" + message);
		}
	}

	/**
	 * 监听:发生异常
	 *
	 * @param error
	 */
	@OnError
	public void onError(Throwable error) {
		log.info("userName为:" + userId + ",发生错误:" + error.getMessage());
		error.printStackTrace();
	}
}

这里 —> 参考链接
前端对接可以参考链接实现,小编前端是个菜鸟,就不班门弄斧了。

最后我们来说一下连接测试

小编使用的是Apifox进行的后端测试(没有的自行百度下载)

注意:Apifox 版本号需 ≥ 2.2.32 才能管理 WebSocket 接口

在这里插入图片描述
如图点击新建WebSocket接口进入后输入:ws://localhost:8080/websocket/{userId}/{roomId}
进行连接测试。

注意参数修改:
port -> 修改自己的端口号为项目端口号
userId -> 用户id 可以使用userName测试更直观,但是可能存在重名情况
roomId -> 聊天室id
在这里插入图片描述

至此,功能实现。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值