SpringBoot+Mybatis-Plus使用webSocket实现一对一聊天

SpringBoot+Mybatis-Plus使用webSocket实现一对一聊天

进一步修改完善连接:https://blog.csdn.net/w75545521/article/details/108642973

一、WebSocket

WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。

WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

在 WebSocket API 中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。

二、代码实现

1、 websocket依赖

<!--websocket依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>

2、WebSocketConfig配置类

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

/**
 * 
 * @description:WebSocketConfig配置类,
 *              注入对象ServerEndpointExporter,这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint
 *
 * @author 火烛
 * @since 2020-9-11
 */

@Configuration
public class WebSocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter(){
        return new ServerEndpointExporter();
    }
}

接下来就是重点了

3、一对一聊天


import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.medical.health.MyUtils.MapUnite;
import com.medical.health.MyUtils.MyUtils;
import com.medical.health.entity.Message;
import com.medical.health.entity.Users;
import com.medical.health.service.impl.MessageServiceImpl;
import com.medical.health.service.impl.UsersServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RestController;
import redis.clients.jedis.Jedis;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 *  描述:
 *  一对一聊天
 *
 * @ServerEndpoint 注解是一个类层次的注解,它的功能主要是将目前的类定义成一个websocket服务器端,
 *                 注解的值将被用于监听用户连接的终端访问URL地址,客户端可以通过这个URL来连接到WebSocket服务器端
 */
@RestController
@ServerEndpoint(value = "/webSocketOneToOne/{param}")
public class WebSocketOneToOne {

	//这里是之前想用Redis存放聊天记录,以实现离线消息的推送
//    private static Jedis jedis = new Jedis("localhost");

    // 这里使用静态,让 service 属于类
    private static UsersServiceImpl userService;
    
    // 注入的时候,给类的 service 注入
    @Autowired
    public void setUserService(UsersServiceImpl userService) {
        WebSocketOneToOne.userService = userService;
    }
    private static MessageServiceImpl messageService;
    @Autowired
    public void setChatMsgService(MessageServiceImpl messageService) {
        WebSocketOneToOne.messageService = messageService;
    }

    // 静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
    private static int onlineCount;
    //实现服务端与单一客户端通信的话,可以使用Map来存放,其中Key为用户标识
    private static final Map<String,WebSocketOneToOne> connections = new ConcurrentHashMap<>();
    // 与某个客户端的连接会话,需要通过它来给客户端发送数据
    private Session session;
    private String sendId;
    private String roomId;

    /**
     * 连接建立成功调用的方法
     *
     * @param session
     * 可选的参数。session为与某个客户端的连接会话,需要通过它来给客户端发送数据
     */
    @OnOpen
    public void onOpen(@PathParam("param") String param, Session session) {
        this.session = session;
        String[] arr = param.split(",");
        this.sendId = arr[0];             //用户标识
        this.roomId = arr[1];         //会话标识
        connections.put(sendId,this);     //添加到map中
        addOnlineCount();               // 在线数加
        System.out.println(param);
        System.out.println(this.session);
        System.out.println("有新连接加入!新用户:"+sendId+",当前在线人数为" + getOnlineCount());
        Users userByid = userService.getById(sendId);
        //用户上次下线时间
        String downTime = userByid.getDownTime();
        System.out.println("downTime------------" + downTime);
        //最后一条消息时间
//        String createTime = jedis.hget(roomId, "createTime");
        String createTime = messageService.getLastCreateTime();
        System.out.println("createTime------------" + createTime);
        //当前时间戳
        long timeMillis = LocalDateTime.now().toEpochSecond(ZoneOffset.ofHours(8));
        System.out.println("timeMillis-------------" + timeMillis);
        if (downTime != null && createTime != null){
            if (Integer.valueOf(downTime) < Integer.valueOf(createTime)){
                //用户下线以后再有新消息
//                send(jedis.hget(roomId, "content"), jedis.hget(roomId, "sendId"), sendId, roomId, "1");
                List<Message> messages = messageService.queryDownTime(downTime);
                for (Message message : messages){
                    downSend(message.getContent(), message.getSendId(), message.getReceiveId(), message.getRoomId(), message.getType());
                }
            }
        }
    }


    /**
     * 连接关闭调用的方法
     * 
     * 这里本来是想离线清空数据库中存放在线时间之前的聊天记录
     * 目前还未实现
     */
    @OnClose
    public void onClose() {
        Users users = new Users();
        users.setId(sendId);
        users.setDownTime(String.valueOf(LocalDateTime.now().toEpochSecond(ZoneOffset.ofHours(8))));
        userService.updateById(users);
        connections.remove(sendId);  // 从map中移除
        subOnlineCount();          // 在线数减
        System.out.println("有一连接关闭!当前在线人数为" + getOnlineCount());

    }

    /**
     * 收到客户端消息后调用的方法
     *
     * @param message
     *            客户端发送过来的消息
     * @param session
     *            可选的参数
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        System.out.println("来自客户端的消息:" + message);
        JSONObject json= JSON.parseObject(message);
        String msg = (String) json.get("message");  //需要发送的信息
        String receiveId = (String) json.get("receiveId");      //发送对象的用户标识(接收者)
        String type = (String) json.get("type");      //发送对象的用户标识(接收者)
        send(msg,sendId,receiveId,roomId,type);
    }

    /**
     * 发生错误时调用
     *
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error) {
        System.out.println("发生错误");
        error.printStackTrace();
    }


    //发送给指定角色
    public void send(String msg,String sendId,String receiveId,String roomId,String type){
        Message message = new Message();
        message.setId(MyUtils.getRandomString(10));
        message.setContent(msg);
        message.setCreateTime(LocalDateTime.now());
        //时间格式化
        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        message.setCreateTimeM(String.valueOf(dateTimeFormatter.format(LocalDateTime.now())));
        message.setReceiveId(receiveId);
        message.setRoomId(roomId);
        message.setSendId(sendId);
        message.setType(type);
        try {
            Users u = userService.getById(sendId);
            System.out.println(u);
            //to指定用户
            WebSocketOneToOne con = connections.get(receiveId);
            if(con!=null){
                if(roomId.equals(con.roomId)){
                    Map map = MapUnite.getMap(message);
                    //用户头像
                    map.put("avatar",u.getIcon());
					//webSocket向前端推送消息的重点                    
                    con.session.getBasicRemote().sendText(JSON.toJSONString(map));
                }
            }
            //from具体用户
            WebSocketOneToOne confrom = connections.get(sendId);
            if(confrom!=null){
                if(roomId.equals(confrom.roomId)){
                    Map map = MapUnite.getMap(message);
					//Redis hash 是一个 string 类型的 field(字段) 和 value(值)
					//的映射表,hash 特别适合用于存储对象。
					//发现只有最后一条记录,之前的因为key和filed相同全部被覆盖
//                    jedis.hset(roomId, "sendId", message.getSendId());
//                    jedis.hset(roomId, "receiveId", message.getReceiveId());
//                    jedis.hset(roomId, "content", message.getContent());
//                    jedis.hset(roomId, "roomId", message.getRoomId());
//                    jedis.hset(roomId, "createTime", String.valueOf(message.getCreateTime().toEpochSecond(ZoneOffset.ofHours(8))));
					//最终妥协直接存入数据库,目前还没有想到更好我方法
					messageService.save(message);
                    map.put("avatar",u.getIcon());
                    confrom.session.getBasicRemote().sendText(JSON.toJSONString(map));
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    //发送离线消息给指定角色
    public void downSend(String msg,String sendId,String receiveId,String roomId,String type){
        Message message = new Message();
        message.setId(MyUtils.getRandomString(10));
        message.setContent(msg);
        message.setCreateTime(LocalDateTime.now());
        //时间格式化
        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        message.setCreateTimeM(String.valueOf(dateTimeFormatter.format(LocalDateTime.now())));
        message.setReceiveId(receiveId);
        message.setRoomId(roomId);
        message.setSendId(sendId);
        message.setType(type);
        try {
            Users u = userService.getById(sendId);
            System.out.println(u);
            //to指定用户
            WebSocketOneToOne con = connections.get(receiveId);
            if(con!=null){
                if(roomId.equals(con.roomId)){
                    Map map = MapUnite.getMap(message);
                    map.put("avatar",u.getIcon());
                    con.session.getBasicRemote().sendText(JSON.toJSONString(map));
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static synchronized int getOnlineCount() {
        return onlineCount;
    }

    public static synchronized void addOnlineCount() {
        WebSocketOneToOne.onlineCount++;
    }

    public static synchronized void subOnlineCount() {
        WebSocketOneToOne.onlineCount--;
    }

}

记一次修改:

原因:

   如果某用户A已经离线接收到用户B、用户C发来的消息
   
   1、用户A上线只查看了B发来的消息,没有看用户C发送的消息就下线
   2、用户A上线没有查看任何消息就下线
   
   这个时候用户A再次上线,这时用户A的最后一次离线到上线期间没有接收到
   任何消息,之前的离线消息推送就会失效。

解决:

给聊天记录一个已读/未读的标识,用户查看消息标记为已读

代码修改:

WebSocketOneToOne

//        Users userByid = userService.getById(sendId);
//        //用户上次下线时间
//        String downTime = userByid.getDownTime();
//        System.out.println("downTime------------" + downTime);
//        //最后一条消息时间
        String createTime = jedis.hget(roomId, "createTime");
//        String createTime = messageService.getLastCreateTime();
//        System.out.println("createTime------------" + createTime);
//        //当前时间戳
//        long timeMillis = LocalDateTime.now().toEpochSecond(ZoneOffset.ofHours(8));
//        System.out.println("timeMillis-------------" + timeMillis);
//        if (downTime != null && createTime != null){
//            if (Integer.valueOf(downTime) < Integer.valueOf(createTime)){
//                //用户下线以后再有新消息
                send(jedis.hget(roomId, "content"), jedis.hget(roomId, "sendId"), sendId, roomId, "1");
//                List<Message> messages = messageService.queryDownTime(downTime);
//                for (Message message : messages){
//                    downSend(message.getContent(), message.getSendId(), message.getReceiveId(), message.getRoomId(), message.getType());
//                }
                System.out.println("用户下线以后再有新消息");
//            }
//        }
        List<Message> messageList = messageService.queryByType(sendId);
        for (Message message : messageList){
            downSend(message.getContent(), message.getSendId(), message.getReceiveId(), message.getRoomId(), message.getType());
        }

MessageController


import com.medical.health.MyUtils.responseVo;
import com.medical.health.service.IMessageService;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

/**
 * <p>
 * 消息列表 前端控制器
 * </p>
 *
 * @author 火烛
 * @since 2020-09-11
 */
@RestController
@RequestMapping("/health/message")
public class MessageController {

    @Resource
    private IMessageService messageService;

    /**
     * 聊天记录标记为已读
     *
     * @param userId
     * @param roomId
     * @return
     */
    @RequestMapping(value = "updateType", method = RequestMethod.PUT)
    @ApiOperation(value = "聊天记录标记为已读")
    public responseVo updateType(String userId, String roomId){
        responseVo result = messageService.updateType(userId, roomId);
        return result;
    }
}

MessageServiceImpl

package com.medical.health.service.impl;

import com.medical.health.MyUtils.responseVo;
import com.medical.health.entity.Message;
import com.medical.health.mapper.MessageMapper;
import com.medical.health.service.IMessageService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.TransactionAspectSupport;

import javax.annotation.Resource;
import java.util.List;

/**
 * <p>
 * 消息列表 服务实现类
 * </p>
 *
 * @author 火烛
 * @since 2020-09-11
 */
@Transactional
@Service
public class MessageServiceImpl extends ServiceImpl<MessageMapper, Message> implements IMessageService {

    @Resource
    private MessageMapper messageMapper;

    @Override
    public responseVo updateType(String userId, String roomId) {
        try {
            messageMapper.updateType(userId, roomId);
        }catch (Exception e){
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
            e.printStackTrace();
            return new responseVo(500, "修改失败", null);
        }
        return new responseVo(200, "修改成功", null);
    }

    @Override
    public List<Message> queryByType(String sendId) {
        return messageMapper.queryByType(sendId);
    }
}

最后放上一点SQL(type = 0为未读,1为已读):
SELECT * FROM message WHERE type = '0' AND receive_id = #{userId}


如有违规请联系wx:qy773787875

特别感谢:Mr_Song_799
参考链接:https://blog.csdn.net/songxinyong1995/article/details/104635981

  • 3
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值