最近一个项目中需要用到一个用户实时聊天需求:需要很多用户(在不同的房间)进行实时聊天,也就是一个简单的聊天室,这里用的是websocket实现。
这里需要对每一个连接都指定两个参数:用户的userId和所加入的房间id(roomId);
@ServerEndpoint("/community/{ro_user}")使用{ro_user}来绑定请求参数,不同的用户连接的时候就把参数加入到连接的后面。这个参数在四个方法都可以绑定到具体参数,这个呢我就只是在onopen的时候绑定一次,然后把参数信息存放到私有变量中,因为用到的是两个整型参数,所以前端使用‘-’把两个参数拼在一起,在后台只需分割成两个参数。
package cn.wzy.sport.controller;
import cn.wzy.sport.entity.User_Message;
import cn.wzy.sport.service.User_MessageService;
import lombok.extern.log4j.Log4j;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* Create by Wzy
* on 2018/8/6 13:24
* 不短不长八字刚好
*/
@ServerEndpoint("/community/{ro_user}")
@Log4j
public class CommunityController {
private static final User_MessageService service;
static {
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
service = ((User_MessageService) ctx.getBean("user_MessageServiceImpl"));
}
private static final Map<Integer, CopyOnWriteArraySet<CommunityController>> rooms = new HashMap<>();
private Session session;
private Integer userId;
private Integer roomId;
@OnOpen
public void onOpen(@PathParam(value = "ro_user") String ro_user, Session session) {
this.session = session;
String[] param = ro_user.split("-");
this.roomId = Integer.parseInt(param[0]);
this.userId = Integer.parseInt(param[1]);
CopyOnWriteArraySet<CommunityController> friends = rooms.get(roomId);
if (friends == null) {
synchronized (rooms) {
if (!rooms.containsKey(roomId)) {
friends = new CopyOnWriteArraySet<>();
rooms.put(roomId, friends);
}
}
}
friends.add(this);
}
@OnClose
public void onClose() {
CopyOnWriteArraySet<CommunityController> friends = rooms.get(roomId);
if (friends != null) {
friends.remove(this);
}
}
@OnMessage
public void onMessage(final String message, Session session) {
//新建线程来保存用户聊天信息
new Thread(new Runnable() {
@Override
public void run() {
service.save(new User_Message(0, userId, message, roomId, new Date()));
}
}).start();
CopyOnWriteArraySet<CommunityController> friends = rooms.get(roomId);
if (friends != null) {
for (CommunityController item : friends) {
item.session.getAsyncRemote().sendText(message);
}
}
}
@OnError
public void onError(Session session, Throwable error) {
log.info("发生错误" + new Date());
error.printStackTrace();
}
}
CopyOnWriteArraySet<CommunityController> friends = rooms.get(roomId);
if (friends == null) {
synchronized (rooms) {
if (!rooms.containsKey(roomId)) {
friends = new CopyOnWriteArraySet<>();
rooms.put(roomId, friends);
}
}
}
friends.add(this);
这里是用到一个map<房间id, 用户set>来保存房间对应的用户连接列表,当有用户进入一个房间的时候,就会先检测房间是否存在,如果不存在那就新建一个空的用户set,再加入本身到这个set中;这里需要考虑线程安全问题,因为用到的是一个hashMap,如同时又AB两个用户加入一个空房间,同时访问friends为空,然后都会新建一个set再加入进去,那么可能会出现一个情况就是A检测不存在房间,然后创建加入进去,B也同时检测到不存在,也重新创建一个用户set,这样就会覆盖原来的set,也就是说A用户就加入失败。
现在设定的逻辑就保证了不存在的情况下就采用阻塞同步,保证只能一个线程新建房间,这里可能会有一个疑惑:同步块是检测了房间为空之后才进去的,为什么还要检测这个房间是否存在呢?这是因为上一句过去friends的语句:CopyOnWriteArraySet<CommunityController> friends = rooms.get(roomId)没采用任何同步措施,可能会产生AB两个线程同时访问这句,然后得出friends都是空,然后一个线程进入同步块,另一个则等待,如果不在后面再次检测是否存在该房间,那么阻塞的线程仍然会重新覆盖值。
用户连接set用的是一个CopyOnWriteArraySet,这个特点就是在写的时候会把所有数据都复制到另外一个数组中,进行修改之后再把原来set中数组指针指向新的数组,这个add方法是采用了lock的同步策略,所以不需要我们考虑线程安全问题。
效果:这里是用在线的websocket测试的
有三个连接:
连接1:
连接2:
连接3:
这样就实现了不同房间的所有人聊天,其他房间就收不到聊天信息。