springboot 整合 websocket实现在线聊天和消息通知

一、内容一览

本文由下文改进而来:https://blog.csdn.net/qq_41463655/article/details/92410518

适合做简易聊天室,多聊天室,发送接收消息可添加分组id
适合做及时消息通知

聊天页面,比较简陋

springboot 整合 websocket实现在线聊天和消息通知_第1张图片

另外提供 WebSocketController web接口

API一览

springboot 整合 websocket实现在线聊天和消息通知_第2张图片

发送消息/通知接口API

springboot 整合 websocket实现在线聊天和消息通知_第3张图片

代码结构

springboot 整合 websocket实现在线聊天和消息通知_第4张图片

二、代码部分

1、添加maven 依赖

        
        
            org.springframework.boot
            spring-boot-starter-websocket
        

2、dto 创建 SendMsgDTO

package com.ws.ldy.common.websocket.model.dto;

import com.ws.ldy.base.convert.Convert;
import lombok.Data;


/***
 * TODO  websocket客服端-发送消息-入参参数
 * @author 王松
 * @mail [email protected]
 * @date 2020/6/30 0030 18:24
 */
@Data
public class SendMsgDTO extends Convert {
    /**
     * 接收人用户Id (目标ID,逗号分隔) (所有人使用-ALL)
     */
    private String to;
    /**
     * 发送内容
     */
    private String content;

    /**
     * @param to      接收人用户Id (目标ID,逗号分隔) (所有人使用-ALL)
     * @param content 发送内容
     */
    public SendMsgDTO(String to, String content) {
        this.to = to;
        this.content = content;
    }

    public SendMsgDTO() {
    }
}

3、entity/ 创建 OnlineUser

package com.ws.ldy.common.websocket.model.entity;

import com.ws.ldy.base.convert.Convert;
import com.ws.ldy.common.utils.LocalDateTimeUtils;
import lombok.AllArgsConstructor;
import lombok.Data;

import javax.websocket.Session;
import java.time.LocalDateTime;

/***
 * TODO  当前在线列表信息
 * @author 王松
 * @mail [email protected]
 * @date 2020/7/1 0001 11:58 
 */
@Data
@AllArgsConstructor
public class OnlineUser extends Convert {
    /**
     * 用户ID
     */
    private String userId;
    /**
     * 用户名称
     */
    private String username;
    /**
     * 用户会话 (使用该对象进行消息发送)
     */
    private Session session;
    /**
     * 连接时间
     */
    private String createTime;

    /**
     * @param userId   用户id
     * @param username 用户名称
     * @param session  用户session 回话信息
     */
    public OnlineUser(String userId, String username, Session session) {
        this.userId = userId;
        this.username = username;
        this.session = session;
        this.createTime = LocalDateTimeUtils.parse(LocalDateTime.now());
    }
}

4、vo/ 创建 OnlineUserVO

package com.ws.ldy.common.websocket.model.vo;

import com.ws.ldy.base.convert.Convert;
import lombok.Data;

/**
 * TODO  在线列表信息返回
 */
@Data
public class OnlineUserVO extends Convert {

    /**
     * 用户ID
     */
    private String userId;
    /**
     * 用户名称
     */
    private String username;
    /**
     * 连接时间
     */
    private String createTime;
}

5、vo/ 创建 SendMsgVO (发送消息的所有内容)

package com.ws.ldy.common.websocket.model.vo;

import com.ws.ldy.base.convert.Convert;
import com.ws.ldy.common.websocket.service.impl.WebsocketServiceImpl;
import com.ws.ldy.common.utils.LocalDateTimeUtils;
import lombok.Data;

import java.time.LocalDateTime;


/**
 * TODO  websocket客户端-接收端-返回参数
 *
 * @author 王松
 * @mail [email protected]
 * @date 2020/6/30 0030 18:24
 */
@Data
public class SendMsgVO extends Convert {
    /**
     * 消息类型(1-上线通知 2-下线通知 3-在线名单通知  4-代表普通消息通知 )
     */
    private Integer mesType;
    /**
     * 发送人用户Id(来源Id,上线为上线线人的用户Id)
     */
    private String from;
    /**
     * 发送人用户名( 上下线为上线线人的用户名)
     */
    private String username;
    /**
     * 接收人用户Id (目标ID,逗号分隔) (所有人使用-ALL,包括自己在内也能接收)
     */
    private String to;
    /**
     * 发送内容
     */
    private String content;
    /**
     * 扩展消息字段(json)
     */
    private String extras;
    /**
     * 当前在线人数
     */
    private Integer onlineNum;
    /**
     * 消息创建时间(YYYY-MM-DD )
     */
    private String createTime;

//    /**
//     * 消息类型,int类型(0:text、1:image、2:voice、3:vedio、4:music、5:news)
//     */
//    private Integer msgType;


    /**
     * @param mesType  消息类型(1-上线通知 2-下线通知 3-在线名单通知  4-代表普通消息通知 )
     * @param from     发送人Id(来源Id),上下线为上线人的用户id
     * @param username 发送人姓名username,上下线为上线人的用户名
     * @param to       接收人Id(目标ID,逗号分隔,所有人使用-ALL)
     * @param content  发送消息内容
     * @param extras   发送消息扩展字段
     */
    public SendMsgVO(Integer mesType, String from, String username, String to, String content, String extras) {
        this.mesType = mesType;
        this.from = from;
        this.username = username;
        this.to = to;
        this.content = content;
        this.extras = extras;
        this.onlineNum = WebsocketServiceImpl.onlineNumber.intValue();
        this.createTime = LocalDateTimeUtils.parse(LocalDateTime.now());
    }
}

6、WebsocketService 接口

package com.ws.ldy.common.websocket.service;

import com.ws.ldy.common.websocket.model.vo.OnlineUserVO;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import java.util.List;

/**
 * TODO websocket 监听类(连接,断点,消息发送等)
 * 

* /websocket/{userId}/{username} = /websocket/用户Id/用户名 来连接websocket,该参数会带到每一个监听方法中 *

*/ public interface WebsocketService { //================================================================================ //================================================================================ //=============================== 监听方法 ======================================= //================================================================================ //================================================================================ /** * TODO 监听连接(有用户连接,立马到来执行这个方法),session 发生变化 * * @param userId 用户id * @param username 用户名 * @param session 当前用户会话 * @return void * @date 2020/6/30 0030 9:28 */ public void onOpen(@PathParam("userId") String userId, @PathParam("username") String username, Session session); /** * TODO 监听断开连接(有用户退出,会立马到来执行这个方法) * * @param userId 用户id * @param username 用户名 * @param session 当前用户会话 */ public void onClose(@PathParam("userId") String userId, @PathParam("username") String username, Session session); /** * TODO 异常停止 * * @param userId 用户id * @param username 用户名 * @param session 当前用户会话 * @param error 异常信息 */ public void onError(@PathParam("userId") String userId, @PathParam("username") String username, Session session, Throwable error); /** * TODO 监听消息发送(收到客户端的消息立即执行) * * @param userId 用户id * @param username 用户名 * @param message 传递的消息内容, json数据( to=接收人用户Id (目标ID,逗号分隔) || content=内容) * @param session 当前用户会话 * *

* // 前端发送内容格式 * .... * // 拼接参数 * let message = { "content": content, "to": to }; * // 发送数据 * webSocket.send(JSON.stringify(message)); * .... *

*/ public void onMessage(@PathParam("userId") String userId, @PathParam("username") String username, String message, Session session); //================================================================================ //================================================================================ //======================= service方法(http接口调用操作) ============================ //================================================================================ //================================================================================ /** * 获取当前在线人数 * * @return */ public Integer getOnlineCount(); /** * 获取当前在线用户列表 * * @return */ public List getOnlineUsersList(); /** * 发送消息 * * @param form 发送人id * @param username 发送人用户名 * @param to 接收人id(多个逗号分隔) * @param content 发送内容 * @param extras 扩暂发送内容 * @return */ public List send(String form, String username, String to, String content, String extras); }

7、WebsocketServiceImpl 接口实现( 重点)

package com.ws.ldy.common.websocket.service.impl;

import com.alibaba.fastjson.JSON;
import com.ws.ldy.common.utils.JsonUtils;
import com.ws.ldy.common.websocket.model.dto.SendMsgDTO;
import com.ws.ldy.common.websocket.model.entity.OnlineUser;
import com.ws.ldy.common.websocket.model.vo.OnlineUserVO;
import com.ws.ldy.common.websocket.model.vo.SendMsgVO;
import com.ws.ldy.common.websocket.service.WebsocketService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * TODO websocket 监听类(连接,断点,消息发送等)
 * 

* /websocket/{userId}/{username} = /websocket/用户Id/用户名 来连接websocket,该参数会带到每一个监听方法中 *

* * @ServerEndpoint: socket链接地址 */ @ServerEndpoint("/websocket/{userId}/{username}") @Service @Slf4j public class WebsocketServiceImpl implements WebsocketService { /** * 在线人数 //使用原子类AtomicInteger, ---> static并发会产生线程安全问题, //public static Integer onlineNumber = 0; */ public static AtomicInteger onlineNumber = new AtomicInteger(0); /** * 所有用户信息(session + userId + username + createTime --> 以用户的id为key, 通过用户key来获取用户session进行消息发送) */ public static Map clients = new ConcurrentHashMap<>(); //================================================================================ //================================================================================ //=============================== 监听方法 ======================================= //================================================================================ //================================================================================ /** * TODO 监听连接(有用户连接,立马到来执行这个方法),session 发生变化 */ @OnOpen public void onOpen(@PathParam("userId") String userId, @PathParam("username") String username, Session session) { // 自增1 onlineNumber.getAndIncrement(); // 保存新用户id,用户名,session会话,登录时间 clients.put(userId, new OnlineUser(userId, username, session)); // 告诉所有人,我上线了 String content = "系统消息:" + username + " 上线了"; this.send(new SendMsgVO(1, userId, username, "ALL", content, null)); // 给自己发一条消息:告诉自己现在都有谁在线 this.send(new SendMsgVO(3, userId, username, userId, JSON.toJSONString(getOnlineUsers()), null)); log.info("有新连接加入!sessionId:{} userId:{} userName:{} 当前在线人数:{}", session.getId(), userId, username, onlineNumber); } /** * TODO 监听断开连接(有用户退出,会立马到来执行这个方法) */ @OnClose public void onClose(@PathParam("userId") String userId, @PathParam("username") String username, Session session) { // 自减1 onlineNumber.getAndDecrement(); // 所有在线用户中去除下线用户 clients.remove(userId); // 告诉所有人,我下线了 String content = "系统消息:" + username + " 下线了"; this.send(new SendMsgVO(2, userId, username, "ALL", content, null)); // 日志 log.info(username + ":已离线! 当前在线人数" + onlineNumber); } /** * TODO 异常停止 */ @OnError public void onError(@PathParam("userId") String userId, @PathParam("username") String username, Session session, Throwable error) { error.printStackTrace(); log.info("服务端发生了错误" + error.getMessage()); } /** * TODO 监听消息发送(收到客户端的消息立即执行) */ @OnMessage public void onMessage(@PathParam("userId") String userId, @PathParam("username") String username, String message, Session session) { log.info("服务器接收到发送消息请求,发送人id={},用户名={}, 接收发送消息={}", userId, username, message); // 请求参数(接收人+发送内容) SendMsgDTO sendMsgDTO = JsonUtils.parseEntity(message, SendMsgDTO.class); // 发送消息 this.send(new SendMsgVO(4, userId, username, sendMsgDTO.getTo(), sendMsgDTO.getContent(), null)); } /** * 消息发送( 遍历用户Id , 在通过sendMsg方法发送消息) * * @param sendMsg:消息内容 */ private void send(SendMsgVO sendMsg) { if ("ALL".equals(sendMsg.getTo()) || "all".equals(sendMsg.getTo())) { // 发送消息给所有人 Set userIds = clients.keySet(); for (String userId : userIds) { this.sendMsg(userId, sendMsg); } } else { //发送消息给指定人 String[] userIds = sendMsg.getTo().split(","); for (String userId : userIds) { this.sendMsg(userId, sendMsg); } } } /** * 消息发送(最后发送, 在send方法中循环用户Id 列表依次发送消息给指定人) *

* // 消息发送(同步:getBasicRemote 异步:getAsyncRemote) *

* * @param userId 消息接收人ID , onlineUsers 的 key * @param sendMsg 消息内容 */ private void sendMsg(String userId, SendMsgVO sendMsg) { // 判断用户是否在线, 在线发送消息推送 if (clients.containsKey(userId)) { try { clients.get(userId).getSession().getBasicRemote().sendText(JSON.toJSONString(sendMsg)); } catch (IOException e) { e.printStackTrace(); log.info(userId, sendMsg.getUsername() + "上线的时候通知所有人发生了错误"); } } } /** * 获取当前在线列表 *

* 获取当前在线列表, 把onlineUsers 转到 OnlineUsersVO返回 *

* * @return */ public synchronized List getOnlineUsers() { List onlineUsersVOList = new ArrayList<>(); for (OnlineUser onlineUsers : clients.values()) { onlineUsersVOList.add(onlineUsers.convert(OnlineUserVO.class)); } return onlineUsersVOList; } //================================================================================ //================================================================================ //======================= service方法(http接口调用操作) ============================ //================================================================================ //================================================================================ /** * 获取当前在线人数 * * @return */ public Integer getOnlineCount() { return onlineNumber.get(); } /** * 获取当前在线用户信息 * * @return */ public List getOnlineUsersList() { return getOnlineUsers(); } /** * 发送消息 (单向通知发送,不可回复) * * @param form 发送人id * @param username 发送人用户名 * @param to 接收人id(多个逗号分隔) * @param content 发送内容 * @param content 发送内容 * @param content 扩暂发送内容 * @return */ public List send(String form, String username, String to, String content, String extras) { // 发送消息 this.send(new SendMsgVO(4, form, username, to, content, extras)); return getOnlineUsers(); } }

8、WebsocketController web-Api 接口方法

package com.ws.ldy.base.controller;

import com.ws.ldy.common.result.Result;
import com.ws.ldy.common.result.ResultEnum;
import com.ws.ldy.common.websocket.model.vo.OnlineUserVO;
import com.ws.ldy.common.websocket.service.WebsocketService;
import com.ws.ldy.config.error.ErrorException;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * websocket类
 *
 * @ServerEndpoint: socket链接地址
 */
@Api(value = "WebSocketController", tags = "websocket 相关通知/聊天")
@RequestMapping("/websocket")
@RestController
@Slf4j
public class WebSocketController {

    /**
     * websocket ip 或域名
     */
    @Value("${websocket.ip}")
    private String ip;

    /**
     * websocket 端口号
     */
    @Value("${websocket.port}")
    private String port;

    /**
     * websocket接口
     */
    @Value("${websocket.interfaceName}")
    private String interfaceName;


    /**
     * TODO 获取webSocket  连接地址,  // 实际情况根据用户 token获取用户信息返回
     * 获取socket地址
     * 获取用户名
     * 获取用户Id
     */
    @RequestMapping(value = "/getPath", method = RequestMethod.GET)
    @ApiOperation("游客登录获取websocket连接地址")
    public Result> getPath() {
        // 配置检查
        if (StringUtils.isBlank(ip) || StringUtils.isBlank(port) || StringUtils.isBlank(interfaceName)) {
            throw new ErrorException(ResultEnum.SYS_SOCKET_CONFIG_ERROR);
        }
        // 随机用户名
        String username = "游客:" + new SimpleDateFormat("ssSSS").format(new Date());
        // 随机用户id
        String userId = new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date());

        // 连接地址, // "ws://192.168.0.154:9049/websocket/1/张三"
        String path = "ws://" + ip + ":" + port + interfaceName + "/" + userId + "/" + username;
        log.info("websocket请求地址:" + path);

        //返回参数
        Map map = new HashMap<>();
        map.put("path", path);
        map.put("userId", userId);
        map.put("username", username);
        return Result.success(map);
    }

    // websocket 逻辑代码
    @Autowired
    private WebsocketService websocketService;

    /**
     * TODO 发送消息
     */
    @RequestMapping(value = "/send", method = RequestMethod.POST)
    @ApiOperation("发送消息")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "form", value = "发送人Id", required = true),
            @ApiImplicitParam(name = "username", value = "发送人用户名", required = true),
            @ApiImplicitParam(name = "to", value = "接收人Id, 全部为ALL", required = true),
            @ApiImplicitParam(name = "content", value = "发送内容", required = true),
            @ApiImplicitParam(name = "extras", value = "附加发送内容", required = true)
    })
    public Result send(String form, String username, String to, String content, String extras) {
        websocketService.send(form, username, to, content, extras);
        return Result.success();
    }

    /**
     * TODO 获取当前在线人数
     */
    @RequestMapping(value = "/getOnlineCount", method = RequestMethod.GET)
    @ApiOperation("发送消息")
    public Result getOnlineCount() {
        Integer onlineCount = websocketService.getOnlineCount();
        return Result.success(onlineCount);
    }


    @RequestMapping(value = "/getOnlineUsersList", method = RequestMethod.GET)
    @ApiOperation("获取当前在线用户列表")
    public Result> getOnlineUsersList() {
        return Result.success(websocketService.getOnlineUsersList());
    }
}


9、yml 创建websocket配置

## websocket 配置
websocket:
  # websocket 服务器部署地址的ip或者域名 --> 本地可以使用127.0.0.1 || localhost
  ip: 127.0.0.1
  # websocket 服务器端口,获取当前服务器端口
  port: ${server.port}
  # websocket 连接接口, WebsocketServiceImpl的 @ServerEndpoint 内参数
  interfaceName: /websocket

10、最后别忘了 WebSocketConfig 配置


/**
  * TODO  WebSocket 配置类
  * @author 王松
  * @mail  [email protected]
  * @date  2020/6/30 0030 9:26
  */
@Configuration
public class WebSocketConfig {

    /**
     * 服务器节点
     * 

* 如果使用独立的servlet容器,而不是直接使用springboot的内置容器,就不要注入ServerEndpointExporter,因为它将由容器自己提供和管理 * * @return */ @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } }

11、前端页面html




    
    
    
    

    websocket
    
    
    
    







  • 群聊信息
  • 在线列表
  • 消息发送至:
  • 私聊信息


  • 重点还是在与html 的js中获取到参数如何处理参数,以及发送消息处理,到此可以先线上完成websocket 聊天功能了,
    以及直接使用web-api 接口获取到在线人数, 直接所有发送消息Api 接口向聊天室发送任意消息

    本文到此结束,如果觉得有用,动动小手点赞或关注一下呗,将不定时持续更新更多的内容…,感谢大家的观看!

    你可能感兴趣的:(springboot)