基于WebSocket + SpringBoot的聊天系统

基于WebSocket + SpringBoot + MongoDB的聊天系统


近期实现了一个一直想做的聊天系统项目,使用Vue + WebSocket + SpringBoot + MongoDB + Mysql + Github图床 + GitHub Api调用+ BootStrap完成,支持头像更改、私聊、聊天室、聊天记录存储、敏感词过滤、发送表情、消息未读等功能,欢迎交流,跪谢Star
https://github.com/Mazai-Liu/WebSocketChat.git

界面如下图。(Lec是聊天室)
在这里插入图片描述


大致实现

前端部分

本人对前端以及Vue不熟悉,很多地方只是会用,细节也不太会处理,界面也不好看(BootStrap Stdio拖出来的) 。

// ChatComponent.vue
export default {
    computed: {
        // 创建ws连接(附带token)
        ws: function() {
            return new WebSocket("ws://localhost:8070/websocket/chat" + "?token=" + this.token);
        },
    },
    
    // ...
    
     mounted(){
         // 挂载函数,在websocket连接不同的事件触发时执行对应的操作	
         
         // 连接成功时
         this.ws.onopen = () => {
             console.log("Connection Created");
             // ...
         };
         
         // 连接关闭时
         this.ws.onclose = () => {
             console.log("Connection Closed");
             // ...
         };

         // 本websocket客户端收到消息时
         this.ws.onmessage = (event) => {
             // ...
         };
     }
}

后端部分

WebSocket部分

WebSocketConfig.java,后端websocket配置类:

@Configuration
@EnableWebSocket
@EnableWebMvc
public class WebSocketConfig implements WebSocketConfigurer{
    @Autowired
    private WebSocketInterceptor webSocketInterceptor;

    @Autowired
    private ChatWebSocketHandler chatWebSocketHandler;

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        // /ws/websocket/log 路径就是前端要访问的路径 类似@ServerEndpoint("/websocket/chat")
        //添加处理器、添加拦截地址、添加拦截器
        registry.addHandler(chatWebSocketHandler, "/websocket/chat")
                .setAllowedOriginPatterns("*")
                .addInterceptors(webSocketInterceptor);
    }
}

WebSocketInterceptor.javawebsocket拦截器:

@Component
@Slf4j
public class WebSocketInterceptor implements HandshakeInterceptor {
    @Override
    public boolean beforeHandshake(ServerHttpRequest request,
                                   ServerHttpResponse response,
                                   WebSocketHandler wsHandler,
                                   Map<String, Object> attributes) throws Exception {
        log.info("Before Handshake");

        // check token
        // ...

        // 设置本次websocket连接的属性
        attributes.put("id",id);
        attributes.put("username",username);
        
        return true;
    }

    @Override
    public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {

    }
}

ChatWebSocketHandler.java, websocket逻辑处理部分:

@Component
@Slf4j
public class ChatWebSocketHandler extends TextWebSocketHandler {
	// 用户名与Session互相的映射,用户名可用id
    private static Map<String,WebSocketSession> USER2SESSION = new ConcurrentHashMap<>();
    private static Map<WebSocketSession, String> session2username = new ConcurrentHashMap<>();

    private static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    // 在线人数
    public static Integer headcount = 0;

    @Autowired
    private RecordService recordService;
    
    // 敏感词过滤
    @Autowired
    private SensitiveWordUtil sensitiveWordUtil;
    
    @Autowired
    private UserService userService;

    /**
     * 连接建立后保存用户的登录状态
     * @param session
     * @throws Exception
     */
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        log.info("连接" + session + "建立");
        String username = (String) session.getAttributes().get("username");

        // 建立会话和用户名的映射
        // ...

        headcount++;
        listUsers();
    }

    /**
     * 向所有websocket客户端更新在线用户
     */
    private void listUsers(){
        // ...
        broadcast(message);
    }

    /**
     * 获取在线用户的名字和头像用以展示
     * @return
     */
    public List<NameAndAvatar> getNamesAndAvatars(){
        // ...
    }

    /**
     * 向所有websocket客户端广播消息
     * @param message
     */
    private void broadcast(String message){
        Set<String> names = USER2SESSION.keySet();
        for(String name : names){
            WebSocketSession session = USER2SESSION.get(name);
            try {
                session.sendMessage(new TextMessage(message));
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 收到发送的聊天消息
     * @param session 发送者的 WebSocketSession
     * @param message 消息
     * @throws Exception
     */
    @Override
    public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
        // 封装聊天记录对象
        // ...
        
        // 新增聊天记录到MongoDB
        // ...
    }

    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {

    }

    /**
     * 连接关闭后,更新用户在线状态
     * @param session
     * @param closeStatus
     * @throws Exception
     */
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
        // 改变用户名和Session映射 
        // ...
        
        headcount--;
        listUsers();
    }

    @Override
    public boolean supportsPartialMessages() {
        return false;
    }
}
MongoDB部分

MongoConfig.java 以及mongoTemplate的使用

@Configuration
public class MongoConfig {
    @Bean
    public MappingMongoConverter mappingMongoConverter(MongoDatabaseFactory factory, MongoMappingContext context, BeanFactory beanFactory) {
        DbRefResolver dbRefResolver = new DefaultDbRefResolver(factory);
        MappingMongoConverter mappingConverter = new MappingMongoConverter(dbRefResolver, context);
        try {
            mappingConverter.setCustomConversions(beanFactory.getBean(CustomConversions.class));
        } catch (NoSuchBeanDefinitionException ignore) {
        }

        // Don't save _class to mongo
        mappingConverter.setTypeMapper(new DefaultMongoTypeMapper(null));

        return mappingConverter;
    }
}
@Service
public class RecordServiceImpl implements RecordService {
    @Autowired
    private MongoTemplate mongoTemplate;

    /**
     * 获取两人间的聊天记录
     * @param getRecordsForm 消息发送者,接受者
     * @return 二者的聊天记录
     */
    @Override
    public Result<List<Record>> getRecords(GetRecordsForm getRecordsForm) {
        String fromString = getRecordsForm.getFromName(), toString = getRecordsForm.getToName();

        // 这是聊天室的聊天内容处理
        if(toString.equals("Lec")){
            // ...
        }

        // select (fromName = a and toName = b) || (fromName = b and toName = a)

        // one part ...
        // two part ...
        // or Operator ...

        List<Record> records = mongoTemplate.find(query, Record.class, "record");

        return Result.success(MessageAndCode.OK,records);
    }


    /**
     * 在用户修改完头像后,更新历史聊天记录中的用户头像
     * @param username 修改头像的用户
     * @param newAvatarPath 新头像的图床路径
     */
    public void setAvatarAfterChanged(String username, String newAvatarPath){
       // ...
    }

    /**
     *  插入聊天记录
     * @param record 一条聊天记录
     * @return
     */
    @Override
    public Result<?> insertRecord(Record record) {
       // ...
    }

    public Result<?> insertRecords(List<Record> records) {
       // ...
    }
}
GitHub Api调用

使用GitHubAPi完成将用户上传的头像上传到Github图床(Github图床当图床很卡就是了)

@Component
@Slf4j
public class GithubUploader {

    public static final Set<String> ALLOW_FILE_SUFFIX = new HashSet<>(Arrays.asList("jpg", "png", "jpeg", "gif"));

    public static final String AVATAR_PREFIX = "img/chat/avatar/";

    @Value("${github.bucket.url}")
    private String url;

    @Value("${github.bucket.api}")
    private String api;

    @Value("${github.bucket.access-token}")
    private String accessToken;

    @Autowired
    RestTemplate restTemplate;

    /**
     * 上传头像到GitHub图床
     * @param multipartFile 用户上传的文件
     * @return GitHubApi的响应对象
     */
    public JSONObject uploadAvatar (MultipartFile multipartFile){
        String fileName = getNewFileName(multipartFile);
        // 最终的文件路径
        String filePath = AVATAR_PREFIX + fileName;

        // 封装GitHubApi要求的请求体
        
        log.info("成功上传头像到Github:{}", filePath);
        return process(filePath,HttpMethod.PUT,map);
    }

    public JSONObject deleteAvatar(String filePath){
        // 同上传
    }

    public String getNewFileName(MultipartFile multipartFile){
        // 使用UUID进行文件重命名
    }

    public JSONObject process(String filePath, HttpMethod method,HashMap<String,String> requestbody){
        String body = JSON.toJSONString(requestbody);

        // 封装GitHubApi要求的请求头 ...

        // 发送请求
        ResponseEntity<JSONObject> responseEntity =
                this.restTemplate.exchange(this.api + filePath, method,
                        new HttpEntity(body, httpHeaders), JSONObject.class);


        log.info("执行完毕: {}", responseEntity.getBody());
        return responseEntity.getBody();
    }

    /**
     * 获取文件的后缀
     * @param fileName
     * @return
     */
    protected String getSuffix(String fileName) {
        int index = fileName.lastIndexOf(".");
        if (index != -1) {
            String suffix = fileName.substring(index + 1);
            if (!suffix.isEmpty()) {
                return suffix;
            }
        }
        return null;
    }
}

TODO

有时间的话还是想继续完善,大概会从以下方面吧。

  • 发送表情包,消息撤回、ui优化等聊天的优化。
  • 心跳检测。防止巨量没有请求连接占用资源。
  • 项目上线。有钱买服务器或者找到一些能免费上线的平台。
  • 项目压测、JVM调优、数据结构优化等 。
  • 聊天记录定时删除。(这个倒是简单)
  • 增加发送离线消息功能。(现在只能发给在线的人)
  • 考虑并处理并发问题。
  • ······
  • 3
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值