Java uses WebSocket to realize one-to-one real-time dialogue

There is a need for real-time dialogue in a recent project. Here is also a lot of information. Using webSocket to realize one-to-one dialogue is the most.

Link: https://pan.baidu.com/s/1Vn1e1qw7nRnU1_4R-4fcGg

Extraction code: qwer 

Logic explanation:

Now we want to send a hello to Zhang Sanfeng, we have to solve the following two problems

·  How to display this hello in my window
·   How to display this hello in Zhang Sanfeng's window


The first problem is easy to solve. Our messages are all stored in an array, and our front end is implemented using Vue. We only need to push new messages into this array when sending messages.

We want to push messages, we only need to know the session to push, we can put each session into the Map when connecting, and use userId as the key.

After the front end gets the data, just push it to the array in the websocketonmessage.

This enables two users to chat in real time.

If the current session exists, a message will be displayed, if the current session does not exist, a message will be displayed +1

We know that a person may chat with multiple users. If the current message is sent by the session user, then our message should be displayed (the above said directly push to the list), if it is not the current session, we will display the message+ 1.

Introduce dependency: This dependency encapsulates the creation to unlink of the real-time dialog

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

Tables needed for the project:

 message table

It is used to save each conversation message, and through the current user id and clicking on the conversation list, we can query the news of the current conversation.

CREATE TABLE `msg_info` (
  `id` int(6) NOT NULL AUTO_INCREMENT COMMENT '消息id',
  `from_user_id` int(6) NOT NULL COMMENT '消息发送者id',
  `from_user_name` varchar(50) NOT NULL COMMENT '消息发送者名称',
  `to_user_id` int(6) NOT NULL COMMENT '消息接收者id',
  `to_user_name` varchar(50) NOT NULL COMMENT '消息接收者名称',
  `content` varchar(200) NOT NULL COMMENT '消息内容',
  `create_time` datetime NOT NULL COMMENT '消息发送时间',
  `un_read_flag` int(1) NOT NULL COMMENT '是否已读(1 已读)',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=273 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='消息表';

 conversation list

A and B, A and C, A and D, B and A can all establish sessions. We need to store all existing sessions, so that users can see their previous session list as soon as they log in.

CREATE TABLE `session_list` (
  `id` int(6) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `user_id` int(6) NOT NULL COMMENT '所属用户',
  `to_user_id` int(6) NOT NULL COMMENT '到用户',
  `list_name` varchar(50) NOT NULL COMMENT '会话名称',
  `un_read_count` int(6) NOT NULL COMMENT '未读消息数',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=51 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='会话列表';

 User table (generally only need id and user name to establish a call, others can be added if necessary)

CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '用户名',
  `password` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '密码',
  `nick_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '昵称',
  `age` int(11) DEFAULT NULL COMMENT '年龄',
  `sex` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '性别',
  `address` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '地址',
  `avatar` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '头像',
  `account` decimal(10,2) DEFAULT 0.00 COMMENT '账户余额',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=20 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='用户信息表';

Configure WebSocketConfig

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

@Configuration
public class WebSocketConfig {

    /**
     * 注入一个ServerEndpointExporter,该Bean会自动注册使用@ServerEndpoint注解申明的websocket endpoint
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }

}

Configure cross-domain


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

@Configuration
public class CorsConfig {

    // 当前跨域请求最大有效时长。这里默认1天
    private static final long MAX_AGE = 24 * 60 * 60;

    private CorsConfiguration buildConfig() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.addAllowedOrigin("*"); // 1 设置访问源地址
        corsConfiguration.addAllowedHeader("*"); // 2 设置访问源请求头
        corsConfiguration.addAllowedMethod("*"); // 3 设置访问源请求方法
        corsConfiguration.setMaxAge(MAX_AGE);
        return corsConfiguration;
    }

    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", buildConfig()); // 4 对接口配置跨域设置
        return new CorsFilter(source);
    }
}

 mybatis-plus configuration paging plugin

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 *  mybatis-plus 分页插件
 */
@Configuration
@MapperScan("com.jt.mapper")
public class MybatisPlusConfig {

    /**
     * 分页插件
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }

}

Util tool class

CurPool manages session, websocket and curUser in a unified way (stores the currently logged in user)

package com.jt.util;

import com.jt.pojo.User;
import com.jt.websocket.WebSocket;

import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 统一管理session、websocket、curUser
 */
public class CurPool {

    //    public static CopyOnWriteArraySet<WebSocket> webSockets =new CopyOnWriteArraySet<>();
    public static Map<Integer, WebSocket> webSockets = new ConcurrentHashMap<>();
    // list 里面第一个存sessionId,第二个存session
    public static Map<Integer, List<Object>> sessionPool = new ConcurrentHashMap<>();
    // 当前登录用户x
    public static Map<String, User> curUserPool = new ConcurrentHashMap<>();
}

JsonUtils JSON conversion tool class, in fact, you can use other

For example: alibba's JSON gson's JSON

package com.jt.util;

import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.IOException;
import java.util.List;
import java.util.Map;

/**
 * json 相关格式化
 */
public class JsonUtils {

    // 定义jackson对象
    private static ObjectMapper MAPPER = new ObjectMapper();

    {
        MAPPER.setSerializationInclusion(Include.ALWAYS);
    }

    /**
     * 将对象转换成json字符串。
     * <p>Title: pojoToJson</p >
     * <p>Description: </p >
     * @param data
     * @return
     */
    public static String objectToJson(Object data) {
        try {
            String string = MAPPER.writeValueAsString(data);
            return string;
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        return null;
    }

    public static JsonNode stringToJsonNode(String data) {
        try {
            JsonNode jsonNode = MAPPER.readTree(data);
            return jsonNode;
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 将json结果集转化为对象
     *
     * @param jsonData json数据
     * @param beanType 对象中的object类型
     * @return
     * @throws Exception
     */
    public static <T> T objectToPojo(Object jsonData, Class<T> beanType) throws Exception {
        T t = MAPPER.readValue(MAPPER.writeValueAsString(jsonData), beanType);
        return t;
    }

    /**
     * 将json结果集转化为对象
     *
     * @param jsonData json数据
     * @param beanType 对象中的object类型
     * @return
     * @throws Exception
     */
    public static <T> T jsonToPojo(String jsonData, Class<T> beanType) throws Exception {
        T t = MAPPER.readValue(jsonData, beanType);
        return t;
    }

    /**
     * 将json数据转换成pojo对象list
     * <p>Title: jsonToList</p >
     * <p>Description: </p >
     * @param jsonData
     * @param beanType
     * @return
     */
    public static <T>List<T> jsonToList(String jsonData, Class<T> beanType) {
        JavaType javaType = MAPPER.getTypeFactory().constructParametricType(List.class, beanType);
        try {
            List<T> list = MAPPER.readValue(jsonData, javaType);
            return list;
        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }

    /**
     * 将object数据转换成pojo对象list
     * <p>Title: jsonToList</p >
     * <p>Description: </p >
     * @param jsonData
     * @param beanType
     * @return
     */
    public static <T>List<T> objectToList(Object jsonData, Class<T> beanType) {
        JavaType javaType = MAPPER.getTypeFactory().constructParametricType(List.class, beanType);
        try {
            List<T> list = MAPPER.readValue(MAPPER.writeValueAsString(jsonData), javaType);
            return list;
        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }

    /**
     * 将JSON数据转换成Map
     * @param jsonData
     * @return
     */
    public static Map<String,Object> jsonToMap(String jsonData) {
        try {
            Map map = MAPPER.readValue(jsonData, Map.class);
            return map;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

}

SpringContextUtil Important: Get the bean object tool class in spring, because mapper cannot be used directly in websocket

package com.jt.util;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

/**
 * 获取spring中的bean对象工具类
 */
@Component
public class SpringContextUtil implements ApplicationContextAware {

    /**
     *  Spring应用上下文环境
     */
    private static ApplicationContext applicationContext;


    /**
     * 实现ApplicationContextAware接口的回调方法,设置上下文环境
     */
    @Override
    public void setApplicationContext(ApplicationContext applicationContext)
            throws BeansException {
        SpringContextUtil.applicationContext = applicationContext;
    }

    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    /**
     * 获取对象 这里重写了bean方法,起主要作用
     */
    public static Object getBean(String beanId) throws BeansException {
        return applicationContext.getBean(beanId);
    }
}

Controller

MsgInfoController gets the message list

package com.jt.controller;


import com.jt.mapper.MsgInfoMapper;
import com.jt.mapper.SessionListMapper;
import com.jt.pojo.MsgInfo;
import com.jt.pojo.SessionList;
import com.jt.vo.SysResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

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

@RestController
public class MsgInfoController {

    @Resource
    private MsgInfoMapper msgInfoMapper;

    @Autowired
    private SessionListMapper sessionListMapper;

    // 消息列表
    @GetMapping("/msgList")
    public SysResult msgList(@RequestParam Integer sessionId){
        SessionList sessionList = sessionListMapper.selectByPrimaryKey(sessionId);
        if(sessionList == null){
            return SysResult.success();
        }
        Integer fromUserId = sessionList.getUserId();
        Integer toUserId = sessionList.getToUserId();
        List<MsgInfo> msgInfoList = msgInfoMapper.selectMsgList(fromUserId,toUserId);
        // 更新消息已读
        msgInfoMapper.msgRead(fromUserId, toUserId);
        // 更新会话里面的未读消息
        sessionListMapper.delUnReadCount(fromUserId, toUserId);
        return SysResult.success(msgInfoList);
    }


}

SessionController creates sessions, queries for established sessions, and queries for established sessions

package com.jt.controller;


import com.jt.mapper.SessionListMapper;
import com.jt.mapper.UserMapper;
import com.jt.pojo.SessionList;
import com.jt.pojo.User;
import com.jt.vo.SysResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class SessionController {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private SessionListMapper seesionListMapper;

    /**
     * 已建立会话
     */
    @GetMapping("/sessionList/already")
    public SysResult sessionListAlready(@RequestParam Integer id){
        List<SessionList> sessionLists = seesionListMapper.selectByUserId(id);
        return  SysResult.success(sessionLists);
    }

    // 可建立会话的用户
    @GetMapping("/sessionList/not")
    public SysResult sessionListNot(@RequestParam Integer id){
        List<Integer> list = seesionListMapper.selectUserIdByUserId(id);
        list.add(id);
        List<User> cloudList = userMapper.getCloudList(list);
        return SysResult.success(cloudList);
    }

    // 创建会话
    @GetMapping("/createSession")
    public SysResult createSession(@RequestParam Integer id,@RequestParam Integer toUserId,@RequestParam String toUserName){
        SessionList sessionList = new SessionList();
        sessionList.setUserId(id);
        sessionList.setUnReadCount(0);
        sessionList.setListName(toUserName);
        sessionList.setToUserId(toUserId);
        seesionListMapper.insert(sessionList);
        // 判断对方和我建立会话没有? 没有也要建立
        Integer integer = seesionListMapper.selectIdByUser(toUserId, id);
        if (integer == null || integer <= 0){
            User user = userMapper.selectByPrimaryKey(id);
            sessionList.setUserId(toUserId);
            sessionList.setToUserId(id);
            sessionList.setListName(user.getName());
            seesionListMapper.insert(sessionList);
        }
        return SysResult.success();
    }

    // 删除会话
    @GetMapping("/delSession")
    public SysResult delSession(@RequestParam Integer sessionId){
        seesionListMapper.deleteByPrimaryKey(sessionId);
        return SysResult.success();
    }

}

UserController user registration, login (store the user in the pool when logging in), logout (clear the user in the pool when the user logs out)

package com.jt.controller;


import com.jt.mapper.UserMapper;
import com.jt.pojo.User;
import com.jt.util.CurPool;
import com.jt.vo.SysResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {

    @Autowired
    private UserMapper userMapper;

    // 注册用户
    @GetMapping("/register")
    public SysResult register(@RequestParam String name) {
        String s = userMapper.selectByName(name);
        if (s != null || "".equals(s)){
            return SysResult.fail().setMsg("改用户已存在");
        }
        User user = new User();
        user.setName(name);
        userMapper.insert(user);
        return SysResult.success();
    }

    // 登录
    @GetMapping("/login")
    public SysResult login(@RequestParam String name){

        if (CurPool.curUserPool.get(name) != null){
            return SysResult.fail().setMsg("该用户已登录");
        }
        User user = userMapper.selectUserByName(name);
        if (user == null || user.getId() == null){
            return SysResult.fail().setMsg("该用户不存在");
        }
        CurPool.curUserPool.put(user.getName(), user);
        return SysResult.success(user);
    }

    // 注销
    @GetMapping("/loginOut")
    public SysResult loginOut(@RequestParam String name){
        if (name != null && !"".equals(name)){
            CurPool.curUserPool.remove(name);
            User user = userMapper.selectUserByName(name);
            CurPool.sessionPool.remove(user.getId());
            CurPool.webSockets.remove(user.getId());
            System.out.println("【websocket消息】连接断开,总数为:"+CurPool.webSockets.size());
        }
        return SysResult.success();
    }


}

Mapper 

MsgInfoMapper

package com.jt.mapper;

import com.jt.pojo.MsgInfo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;

@Mapper
public interface MsgInfoMapper  {

    int insert(MsgInfo msgInfo);

    void msgRead(@Param("fromUserId") Integer fromUserId, @Param("toUserId") Integer toUserId);

    List<MsgInfo> selectMsgList(@Param("fromUserId") Integer fromUserId, @Param("toUserId") Integer toUserId);
}

 MsgInfoMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.jt.mapper.MsgInfoMapper">
    <resultMap id="BaseResultMap" type="com.jt.pojo.MsgInfo">
        <id column="id" jdbcType="INTEGER" property="id" />
        <result column="from_user_id" jdbcType="INTEGER" property="fromUserId" />
        <result column="from_user_name" jdbcType="VARCHAR" property="fromUserName" />
        <result column="to_user_id" jdbcType="INTEGER" property="toUserId" />
        <result column="to_user_name" jdbcType="VARCHAR" property="toUserName" />
        <result column="content" jdbcType="VARCHAR" property="content" />
        <result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
        <result column="un_read_flag" jdbcType="INTEGER" property="unReadFlag" />
    </resultMap>
    <sql id="Base_Column_List">
        id, from_user_id, from_user_name, to_user_id,to_user_name, content, create_time, un_read_flag
    </sql>

    <insert id="insert" parameterType="com.jt.pojo.MsgInfo"  useGeneratedKeys="true" keyProperty="id">
        insert into msg_info (id, from_user_id, from_user_name,
                              to_user_id, to_user_name, content, create_time,
                              un_read_flag)
        values (#{id,jdbcType=INTEGER}, #{fromUserId,jdbcType=INTEGER}, #{fromUserName,jdbcType=VARCHAR},
                #{toUserId,jdbcType=INTEGER},#{toUserName,jdbcType=VARCHAR},  #{content,jdbcType=VARCHAR}, #{createTime,jdbcType=TIMESTAMP},
                #{unReadFlag,jdbcType=INTEGER})
    </insert>



    <update id="msgRead">
        UPDATE msg_info SET un_read_flag = 1 WHERE from_user_id = #{fromUserId} AND to_user_id = #{toUserId}
    </update>

    <select id="selectMsgList" resultMap="BaseResultMap">
        SELECT * FROM msg_info
        WHERE (
                      (to_user_id = #{toUserId} AND from_user_id = #{fromUserId})
                      OR (to_user_id = #{fromUserId} AND from_user_id = #{toUserId})
                  )
    </select>
</mapper>

SessionListMapper

package com.jt.mapper;


import com.jt.pojo.SessionList;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;

@Mapper
public interface SessionListMapper {

    int deleteByPrimaryKey(Integer id);

    int insert(SessionList sessionList);

    SessionList selectByPrimaryKey(Integer id);

    List<Integer> selectUserIdByUserId(Integer id);

    List<SessionList> selectByUserId(Integer id);

    Integer selectIdByUser(@Param("fromId") Integer fromId,@Param("toId") Integer toId);


    void addUnReadCount(@Param("userId") Integer userId,@Param("toUserId") Integer toUserId);

    void delUnReadCount(@Param("fromUserId") Integer fromUserId,@Param("toUserId") Integer toUserId);
}

 SessionListMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.jt.mapper.SessionListMapper">
    <resultMap id="BaseResultMap" type="com.jt.pojo.SessionList">
        <id column="id" jdbcType="INTEGER" property="id" />
        <result column="user_id" jdbcType="INTEGER" property="userId" />
        <result column="to_user_id" jdbcType="INTEGER" property="toUserId" />
        <result column="list_name" jdbcType="VARCHAR" property="listName" />
        <result column="un_read_count" jdbcType="INTEGER" property="unReadCount" />
    </resultMap>
    <sql id="Base_Column_List">
        id, user_id,to_user_id, list_name, un_read_count
    </sql>
    <select id="selectByPrimaryKey" parameterType="java.lang.Integer" resultMap="BaseResultMap">
        select
        <include refid="Base_Column_List" />
        from session_list
        where id = #{id,jdbcType=INTEGER}
    </select>
    <delete id="deleteByPrimaryKey" parameterType="java.lang.Integer">
        delete from session_list
        where id = #{id,jdbcType=INTEGER}
    </delete>
    <insert id="insert" parameterType="com.jt.pojo.SessionList">
        insert into session_list (id, user_id, to_user_id,list_name,
                                  un_read_count)
        values (#{id,jdbcType=INTEGER}, #{userId,jdbcType=INTEGER}, #{toUserId,jdbcType=INTEGER}, #{listName,jdbcType=VARCHAR},
                #{unReadCount,jdbcType=INTEGER})
    </insert>



    <select id="selectUserIdByUserId" resultType="java.lang.Integer">
        SELECT to_user_id FROM session_list WHERE user_id = #{id}
    </select>

    <select id="selectByUserId" resultMap="BaseResultMap">
        SELECT * FROM session_list WHERE user_id = #{id}
    </select>

    <select id="selectIdByUser" resultType="java.lang.Integer">
        SELECT id FROM session_list WHERE user_id = #{fromId} AND to_user_id = #{toId}
    </select>



    <update id="addUnReadCount">
        UPDATE session_list SET un_read_count = un_read_count + 1 WHERE user_id = #{userId} AND to_user_id = #{toUserId}
    </update>

    <update id="delUnReadCount">
        UPDATE session_list SET un_read_count = 0 WHERE user_id = #{fromUserId} AND to_user_id = #{toUserId}
    </update>
</mapper>

UserMapper

package com.jt.mapper;

import cn.hutool.db.Page;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jt.pojo.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;

@Mapper
public interface UserMapper {

    int insert(User user);

    User selectByPrimaryKey(Integer id);


    String selectByName(String name);

    List<User> getCloudList(@Param("list") List<Integer> list);

    User selectUserByName(@Param("name") String name);

}

 UserMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.jt.mapper.UserMapper">
    <resultMap id="BaseResultMap" type="com.jt.pojo.User">
        <id column="id" jdbcType="INTEGER" property="id" />
        <result column="name" jdbcType="VARCHAR" property="name" />
    </resultMap>
    <sql id="Base_Column_List">
        id, `name`
    </sql>
    <select id="selectByPrimaryKey" parameterType="java.lang.Integer" resultMap="BaseResultMap">
        select
        <include refid="Base_Column_List" />
        from user
        where id = #{id,jdbcType=INTEGER}
    </select>


    <insert id="insert" parameterType="com.jt.pojo.User">
        insert into user (id, `name`)
        values (#{id,jdbcType=INTEGER}, #{name,jdbcType=VARCHAR})
    </insert>



    <select id="selectByName" resultType="java.lang.String">
        SELECT name FROM user WHERE name = #{name}
    </select>

    <select id="getCloudList" resultMap="BaseResultMap">
        SELECT id,name FROM user
        <if test="list != null">
            WHERE id not in
            <foreach collection="list" open="(" close=")" separator="," item="i">
                #{i}
            </foreach>
        </if>
    </select>

    <select id="selectUserByName" resultMap="BaseResultMap" parameterType="java.lang.String">
        SELECT id,name FROM user WHERE name = #{name}
    </select>
</mapper>

The method onOpen is  called when the connection is successfully established                

                  The method onClose called when the connection is closed      

The method onMessage called after receiving the client message          

                 onError is called when an error occurs               

package com.jt.websocket;


import com.jt.mapper.MsgInfoMapper;
import com.jt.mapper.SessionListMapper;
import com.jt.mapper.UserMapper;
import com.jt.pojo.MsgInfo;
import com.jt.pojo.SessionList;
import com.jt.pojo.User;
import com.jt.util.CurPool;
import com.jt.util.JsonUtils;
import com.jt.util.SpringContextUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

@Slf4j
@Component
@ServerEndpoint("/websocket/{userId}/{sessionId}")
//此注解相当于设置访问URL
public class WebSocket {

    @Autowired
    private SessionListMapper sessionListMapper;

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private MsgInfoMapper msgInfoMapper;

    private Session session;

    /**
     * 连接建立成功调用的方法
     */
    @OnOpen
    public void onOpen(Session session,@PathParam(value="userId")Integer userId, @PathParam(value="sessionId")String sessionId) {
        this.session = session;
        CurPool.webSockets.put(userId,this);
        List<Object> list = new ArrayList<>();
        list.add(sessionId);
        list.add(session);
        CurPool.sessionPool.put(userId , list);
        System.out.println("【websocket消息】有新的连接,总数为:"+CurPool.webSockets.size());
    }
    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose() {
        // 断开连接删除用户删除session
        Integer userId = Integer.parseInt(this.session.getRequestParameterMap().get("userId").get(0));
        CurPool.sessionPool.remove(userId);
        CurPool.webSockets.remove(userId);
        if (userMapper == null){
            this.userMapper = (UserMapper) SpringContextUtil.getBean("userMapper");
        }
        User user = userMapper.selectByPrimaryKey(userId);
        CurPool.curUserPool.remove(user.getName());
        System.out.println("【websocket消息】连接断开,总数为:"+CurPool.webSockets.size());
    }
    /**
     * 收到客户端消息后调用的方法
     * 后台收到客户端发送过来的消息
     * onMessage 是一个消息的中转站
     * @param message 客户端发送过来的消息
     */
    @OnMessage
    public void onMessage(String message) {

        String sessionId = this.session.getRequestParameterMap().get("sessionId").get(0);
        if (sessionId == null){
            System.out.println("sessionId 错误");
        }
        // 在这里无法注入Mapper所以使用这种方式注入Mapper
        if (sessionListMapper == null){
            this.sessionListMapper = (SessionListMapper)SpringContextUtil.getBean("sessionListMapper");
        }
        if (userMapper == null){
            this.userMapper = (UserMapper)SpringContextUtil.getBean("userMapper");
        }
        if (msgInfoMapper == null){
            this.msgInfoMapper = (MsgInfoMapper)SpringContextUtil.getBean("msgInfoMapper");
        }
        SessionList sessionList = sessionListMapper.selectByPrimaryKey(Integer.parseInt(sessionId));
        User user = userMapper.selectByPrimaryKey(sessionList.getUserId());
        MsgInfo msgInfo = new MsgInfo();
        msgInfo.setContent(message);
        msgInfo.setCreateTime(new Date());
        msgInfo.setFromUserId(sessionList.getUserId());
        msgInfo.setFromUserName(user.getName());
        msgInfo.setToUserId(sessionList.getToUserId());
        msgInfo.setToUserName(sessionList.getListName());
        msgInfo.setUnReadFlag(0);
        // 消息持久化
        msgInfoMapper.insert(msgInfo);

        // 判断用户是否存在,不存在就结束
        List<Object> list = CurPool.sessionPool.get(sessionList.getToUserId());
        if (list == null || list.isEmpty()){
            // 用户不存在,更新未读数
            sessionListMapper.addUnReadCount(sessionList.getToUserId(),sessionList.getUserId());
        }else{
            // 用户存在,判断会话是否存在
            String id = sessionListMapper.selectIdByUser(sessionList.getToUserId(), sessionList.getUserId())+"";
            String o = list.get(0) + "";
            if (id.equals(o)){
                // 会话存在直接发送消息
                sendTextMessage(sessionList.getToUserId(), JsonUtils.objectToJson(msgInfo));
            }else {
                // 判断会话列表是否存在
                if (id == null || "".equals(id) || "null".equals(id)){
                    // 新增会话列表
                    SessionList tmpSessionList = new SessionList();
                    tmpSessionList.setUserId(sessionList.getToUserId());
                    tmpSessionList.setToUserId(sessionList.getUserId());
                    tmpSessionList.setListName(user.getName());
                    tmpSessionList.setUnReadCount(1);
                    sessionListMapper.insert(tmpSessionList);
                }else {
                    // 更新未读消息数量
                    sessionListMapper.addUnReadCount(sessionList.getToUserId(),sessionList.getUserId());
                }
                // 会话不存在发送列表消息
                List<SessionList> sessionLists = sessionListMapper.selectByUserId(sessionList.getToUserId());
                sendTextMessage(sessionList.getToUserId() ,JsonUtils.objectToJson(sessionLists));
            }
        }
        System.out.println("【websocket消息】收到客户端消息:"+message);
    }

    @OnError
    public void onError(Session session, Throwable error) {
        log.error("发生错误");
        error.printStackTrace();
    }

    // 此为广播消息
//    public void sendAllMessage(String message) {
//        for(WebSocket webSocket : webSockets) {
//            System.out.println("【websocket消息】广播消息:"+message);
//            try {
//                webSocket.session.getAsyncRemote().sendText(message);
//            } catch (Exception e) {
//                e.printStackTrace();
//            }
//        }
//    }

    // 此为单点消息 (发送文本)
    public void sendTextMessage(Integer userId, String message) {
        Session session = (Session)CurPool.sessionPool.get(userId).get(1);
        if (session != null) {
            try {
                session.getBasicRemote().sendText(message);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    // 此为单点消息 (发送对象)
//    public void sendObjMessage(String sessionId, Object message) {
//        Session session = CurPool.sessionPool.get(sessionId);
//        if (session != null) {
//            try {
                session.getAsyncRemote().sendObject(message);
//                session.getBasicRemote().sendText(JsonUtils.objectToJson(message));
//            } catch (Exception e) {
//                e.printStackTrace();
//            }
//        }
//    }

}

Front-end vue call:

<template>
 <div class="main"> 
  <div class="main_up">
   <div v-if="curUserName == ''">
    <el-button @click="openDialog('登录')">登录</el-button>
    <el-button @click="openDialog('注册')">注册</el-button>
   </div>
   <div v-else>
    {
   
   {this.curUserName}}
    <el-button type="info" @click="loginOut">退出登录</el-button>
   </div>
   
  </div>
  <div class="main_down">
   <div class="left">
    <div class="left_up">
     <div class="label_title">
      已建立会话
     </div>
     <div :class="curSessionId == item.id ? 'box_select' : 'box'" v-for="item in sessionList_already" :key="item.id">
      <div class="box_left"  @click="startSession(item.id)">
       {
   
   {item.listName}}
      </div>
      <div class="right_left">
       <div class="right_left_count">
        {
   
   {item.unReadCount}}
       </div>
       <div class="right_left_del">
        <i class="el-icon-close" @click="delSession(item.id)"></i>
       </div>
      </div>
     </div>
    </div>
    <div class="left_down">
     <div class="label_title">
      可建立会话
     </div>
     <div v-for="item in sessionList_not" :key="item.id" class="box" @click="createSession(item.id, item.name)">
      <div class="box_left">
       {
   
   {item.name}}
      </div>
     </div> 
    </div>

   </div>
   <div class="right">
    <div class="up" ref="element" id="msg_end">
     <div v-for="(item,i) in list" :key="i" :class="item.fromUserId === curUserId ? 'msg_right' : 'msg_left'">
      <div class="msg_right_up">
       {
   
   {item.fromUserName}}
      </div>
      <div :class="item.fromUserId === curUserId ? 'msg_right_down' : 'msg_left_down'">
       {
   
   {item.content}}
      </div>
     </div>
     
    </div>
    <div class="down">
     <el-input
     type="textarea"
     :rows="9"
     placeholder="请输入内容,回车发送!"
     @keyup.enter.native = "sendMsg"
     v-model="textarea">
     </el-input>

     <!-- <el-button  @click="sendMsg">发送</el-button> -->
    </div>
   </div>
  </div>

  <el-dialog
   :title="dialogTitle"
   :visible.sync="dialogVisible"
   width="30%"
   >
   <el-input v-model="loginName" placeholder="请输入用户名..."></el-input>
   <span slot="footer" class="dialog-footer">
    <el-button @click="dialogVisible = false">取 消</el-button>
    <el-button type="primary" @click="loginOrRegister">确 定</el-button>
   </span>
  </el-dialog>
  </div>
</template>

<script>
import axios from 'axios';
export default {
 
 name: 'HelloWorld',
 data(){
  return{
   dialogVisible: false,
   dialogTitle: '',
   loginName: '',
   textarea: "",
   list: [],
   curUserId: "",
   curUserName: "",
   curSessionId: '',
   sessionList_already:[],
   sessionList_not:[],
  }
 },
 created() { // 页面创建生命周期函数              
  
 },     
 updated(){
  // 解决每次发消息到最后面
  var elmnt = document.getElementById("msg_end");
  elmnt.scrollTop = elmnt.scrollHeight;
 },   
 destroyed: function () { // 离开页面生命周期函数              
  this.websocketclose();        
 },
 methods: {            
  initWebSocket: function (userId,sessionId) {                
   // WebSocket与普通的请求所用协议有所不同,ws等同于http,wss等同于https                
   this.websock = new WebSocket("ws://127.0.0.1:9090/websocket/"+userId+"/"+sessionId);                
   this.websock.onopen = this.websocketonopen;                
   this.websock.onerror = this.websocketonerror;                
   this.websock.onmessage = this.websocketonmessage;                
   this.websock.onclose = this.websocketclose;
  },              
  websocketonopen: function () {                
   console.log("WebSocket连接成功");    
  },              
  websocketonerror: function (e) { 
   console.log("WebSocket连接发生错误",e);              
  },              
  websocketonmessage: function (e) {  
   let data = JSON.parse(e.data);
   if(data instanceof Array){
    // 列表数据
    this.sessionList_already = data
   }else{
    // 消息数据
    this.list.push(data)
   }
  },              
  websocketclose: function (e) {
   if(this.curUserId != null){
    if(this.curSessionId != null){
     this.initWebSocket(this.curUserId, this.curSessionId)
    }else{
     this.initWebSocket(this.curUserId, 99999999)
    }
   }
   console.log("connection closed",e);     
   console.log(e);              
  },
  // 消息发送
  sendMsg(){
   if(this.curSessionId == ''){
    return this.$message.error("请选择左边的对话框开始聊天!")
   }
   let data = {
    "fromUserId": this.curUserId,
    "fromUserName": this.curUserName,
    "content": this.textarea
   }
   this.list.push(data)
   this.websock.send(this.textarea)
   this.textarea = ''
  },
  openDialog(openName){
   this.dialogTitle = openName
   this.dialogVisible = true
  },
  // 登录or注册
  loginOrRegister(){
   let thus = this
   if(this.dialogTitle == "注册"){
    axios.get('http://127.0.0.1:9090/register?name=' + this.loginName)
    .then(function (response) {
     if(response.data.code == -1){
      return thus.$message.error(response.data.errDesc);
     }
     thus.$message.success("注册成功");
     thus.dialogVisible = false
    })
    .catch(function (error) {
     console.log(error);
    });
   }else if(this.dialogTitle == '登录'){
    axios.get('http://127.0.0.1:9090/login?name=' + this.loginName)
    .then(function (response) {
     console.log(response.data);
     if(response.data.code == -1){
      return thus.$message.error(response.data.errDesc);
     }
     thus.curUserId = response.data.data.id
     thus.curUserName = response.data.data.name
     thus.dialogVisible = false
     thus.getSessionListNot()
     thus.sessionListAlready()
     thus.startSession(99999999)
    })
    .catch(function (error) {
     console.log(error);
    });
   }
  },
  // 获取可建立会话列表
  getSessionListNot(){
   let thus = this
   axios.get('http://127.0.0.1:9090/sessionList/not?id=' + this.curUserId)
   .then(function (response) {
    if(response.data.code == -1){
     return thus.$message.error(response.data.errDesc);
    }
    thus.sessionList_not = response.data.data
   })
   .catch(function (error) {
    console.log(error);
   });
  },
  // 获取已存在的会话列表
  sessionListAlready(){
   let thus = this
   axios.get('http://127.0.0.1:9090/sessionList/already?id=' + this.curUserId)
   .then(function (response) {
    if(response.data.code == -1){
     return thus.$message.error(response.data.errDesc);
    }
    thus.sessionList_already = response.data.data
   })
   .catch(function (error) {
    console.log(error);
   });
  },
  // 创建会话
  createSession(toUserId, toUserName){
   let thus = this
   axios.get('http://127.0.0.1:9090/createSession?id=' + this.curUserId + "&toUserId=" + toUserId + "&toUserName=" + toUserName)
   .then(function (response) {
    if(response.data.code == -1){
     return thus.$message.error(response.data.errDesc);
    }
    thus.getSessionListNot()
    thus.sessionListAlready()
   })
   .catch(function (error) {
    console.log(error);
   });
  },
  // 开始会话
  startSession(sessionId){
   console.log(this.websock);
   if(this.websock != undefined){
    this.websock.close()
   }
   this.curSessionId = sessionId
   this.initWebSocket(this.curUserId, sessionId)
   this.msgList(sessionId)
  },
  // 删除会话
  delSession(sessionId){
   let thus = this
   axios.get('http://127.0.0.1:9090/delSession?sessionId=' + sessionId)
   .then(function (response) {
    if(response.data.code == -1){
     return thus.$message.error(response.data.errDesc);
    }
    thus.getSessionListNot()
    thus.sessionListAlready()
   })
   .catch(function (error) {
    console.log(error);
   });
  },
  // 退出登录
  loginOut(){
   let thus = this
   axios.get('http://127.0.0.1:9090/loginOut?name=' + this.curUserName)
   .then(function (response) {
    if(response.data.code == -1){
     return thus.$message.error(response.data.errDesc);
    }
    thus.curUserId = ''
    thus.curUserName = ''
    return thus.$message.success("退出登录成功");
   })
   .catch(function (error) {
    console.log(error);
   });
  },
  // 获取消息数据
  msgList(sessionId){
   let thus = this
   axios.get('http://127.0.0.1:9090/msgList?sessionId=' + sessionId)
   .then(function (response) {
    if(response.data.code == -1){
     return thus.$message.error(response.data.errDesc);
    }
    thus.list = response.data.data
    // 从新获取列表
    thus.sessionListAlready()
   })
   .catch(function (error) {
    console.log(error);
   });
 
  }
 }    
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
 .main{
  width: 980px;
  /* border: 1px #1890ff solid; */
  margin-top: 50px;
  height: 790px;
 }
 .main_up{
  width: 980px;
  height: 40px;
  /* border:1px red solid; */
 }
 .main_down{
  width: 981px;
  height: 750px;
  border: 1px #1890ff solid;
  display: flex;
  justify-self: space-between;
 }
 .left{
  width: 300px;
  height: 750px;
  border-right: 1px #1890ff solid;
 }
 .left_up{
  width: 300px;
  height: 450px;
  overflow-y: auto;
  /* border: 1px red solid; */
 }
 .label_title{
  width: 282px;
  height: 25px;
  background-color: #f8f8f8;
  font-weight: 600;
  padding: 8px;
 }
 .left_down{
  width: 300px;
  height: 300px;
  overflow-y: auto;
  /* border: 1px green solid; */
 }
 .right{
  width: 680px;
  height: 750px;
  /* border-right: 1px #1890ff solid; */
 }
 .box{
  width: 250px;
  height: 22px;
  padding: 10px 25px 10px 25px;
  display: flex;
  justify-self: flex-end;
  /* border: 1px red solid; */
 }
 .box:hover{
  background-color: gainsboro;
  cursor: pointer;
 }
 .box_select{
  width: 250px;
  height: 22px;
  padding: 10px 25px 10px 25px;
  display: flex;
  justify-self: flex-end;
  background-color: gainsboro;
 }
 .box_left{
  width: 230px;
  height: 22px;
 }
 .right_left{
  width: 50px;
  height: 22px;
  display: flex;
  justify-content: space-between;
  /* border: 1px red solid; */
 }
 .right_left_count{
  /* border: 1px blue solid; */
  padding-left: 10px;
  width: 20px;
 }
 .right_left_del{
  width: 20px;
  padding-left: 10px;
 }
 .up{
  width: 680px;
  height: 550px;
  overflow-y: scroll;
  overflow-x: hidden;
  /* padding-bottom: 40px; */
  border-bottom: 1px #1890ff solid;
 }
 .msg_left{
  width: 675px;
  /* padding-left: 5px; */
  margin-top: 5px;
  /* border: 1px #1890ff solid; */
 }
 .msg_left_up{
  height: 25px;
  margin-top: 5px;
 }
 .msg_left_down{
  height: 25px;
  /* border: 1px #1890ff solid; */
  padding-left: 10px;
 }
 .msg_right{
  width: 660px;
  /* padding-right: 20px; */
  margin-top: 5px;
  /* border: 1px #1890ff solid; */
  text-align: right;
 }
 .msg_right_up{
  height: 25px;
  
 }
 .msg_right_down{
  height: 25px;
  /* border: 1px #1890ff solid; */
  padding-right: 10px;
 }
 .down{
  width: 680px;
  height: 200px;
  /* border: 1px red solid; */
 }
</style>

b2fa5c23d4fd40619320ed04b70ad32b.png

 942308e290054955ad3ea82a88a32eab.png

Realize sending voice

Generate an audio file, save it and upload it to the server. Then save the file name in the database. During chatting, the front end judges whether the returned data is voice. If it is voice, it directly calls the audio file according to the name to obtain the voice content.

1. The user clicks to send the voice, save the voice to the server, and save the name to the database

2. Get the audio file when receiving the voice

3. The front end can display according to the obtained audio file.

 

package com.jt.controller;

import com.jt.util.FtpUtil;

import javax.sound.sampled.*;
import java.io.*;

public class EngineeCore {

    String filePath = timeNumber()+".mp3";//要存储文件的位置

    AudioFormat audioFormat;
    TargetDataLine targetDataLine;
    boolean flag = true;

    public static void main(String[] args) {
        EngineeCore engineeCore = new EngineeCore();  //调用麦克风获取音频

        engineeCore.startRecognize();
    }

    private void stopRecognize() {
        flag = false;
        targetDataLine.stop();
        targetDataLine.close();
    }private AudioFormat getAudioFormat() {
        float sampleRate = 16000;
        // 8000,11025,16000,22050,44100
        int sampleSizeInBits = 16;
        // 8,16
        int channels = 1;
        // 1,2
        boolean signed = true;
        // true,false
        boolean bigEndian = false;
        // true,false
        return new AudioFormat(sampleRate, sampleSizeInBits, channels, signed, bigEndian);
    }// end getAudioFormat


    private void startRecognize() {
        try {
            // 获得指定的音频格式
            audioFormat = getAudioFormat();
            DataLine.Info dataLineInfo = new DataLine.Info(TargetDataLine.class, audioFormat);
            targetDataLine = (TargetDataLine) AudioSystem.getLine(dataLineInfo);

            // Create a thread to capture the microphone
            // data into an audio file and start the
            // thread running. It will run until the
            // Stop button is clicked. This method
            // will return after starting the thread.
            flag = true;
            new CaptureThread().start();
        } catch (Exception e) {
            e.printStackTrace();
        } // end catch
    }// end captureAudio method

    class CaptureThread extends Thread {
        public void run() {
            AudioFileFormat.Type fileType = null;

            File audioFile = new File(filePath);

            fileType = AudioFileFormat.Type.WAVE;
            //声音录入的权值
            int weight = 2;
            //判断是否停止的计数
            int downSum = 0;

            ByteArrayInputStream bais = null;
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            AudioInputStream ais = null;
            try {
                targetDataLine.open(audioFormat);
                targetDataLine.start();
                byte[] fragment = new byte[1024];

                ais = new AudioInputStream(targetDataLine);
                while (flag) {

                    targetDataLine.read(fragment, 0, fragment.length);
                    //当数组末位大于weight时开始存储字节(有声音传入),一旦开始不再需要判断末位
                    if (Math.abs(fragment[fragment.length-1]) > weight || baos.size() > 0) {
                        baos.write(fragment);
                        System.out.println("守卫:"+fragment[0]+",末尾:"+fragment[fragment.length-1]+",lenght"+fragment.length);
                        //判断语音是否停止
                        if(Math.abs(fragment[fragment.length-1])<=weight){
                            downSum++;
                        }else{
                            System.out.println("重置奇数");
                            downSum=0;
                        }//计数超过20说明此段时间没有声音传入(值也可更改)
                        if(downSum>20){
                            System.out.println("停止录入");
                            break;
                        }

                    }
                }

                //取得录音输入流
                audioFormat = getAudioFormat();
                byte audioData[] = baos.toByteArray();
                bais = new ByteArrayInputStream(audioData);
                ais = new AudioInputStream(bais, audioFormat, audioData.length / audioFormat.getFrameSize());
                //定义最终保存的文件名
                System.out.println("开始生成语音文件");
                AudioSystem.write(ais, AudioFileFormat.Type.WAVE, audioFile);
                downSum = 0;
                stopRecognize();

                //上传到服务器中
                BufferedInputStream buffer = new BufferedInputStream(new FileInputStream(filePath));
                boolean b = FtpUtil.uploadFileFTP("111.111.111.111", 21, "ftpuser", "ftpuser", "/www/wwwroot/ftpuser", timeNumber()+".mp3", buffer);
                System.out.println("b = " + b);

                boolean delete = audioFile.delete();//删除本地文件
                System.out.println("delete = " + delete);
                
                buffer.close();
                
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                //关闭流

                try {
                    ais.close();
                    bais.close();
                    baos.reset();
                    
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

        }
    }
    //获得当前时间戳
    public Long timeNumber(){
        long l = System.currentTimeMillis();
        return l;
    }
}

Guess you like

Origin blog.csdn.net/Java_Mr_Jin/article/details/126470723