• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

WebSocket + SpringBoot的聊天系统

武飞扬头像
物与我皆无尽也
帮助3

基于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调优、数据结构优化等 。
  • 聊天记录定时删除。(这个倒是简单)
  • 增加发送离线消息功能。(现在只能发给在线的人)
  • 考虑并处理并发问题。
  • ······

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhfiagea
系列文章
更多 icon
同类精品
更多 icon
继续加载