jjzjj

私信功能的设计与实现

独一无二的哈密瓜 2024-01-05 原文

文章目录

1. 效果展示

项目源码: https://gitee.com/wangzhi430/ChatSystem





2. 基础准备

2.1 项目创建




2.2 配置文件

spring.datasource.url=jdbc:mysql://localhost:3306/ChatSystem?characterEncoding=utf8&useSSL=true
spring.datasource.username=root
spring.datasource.password=0000
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

mybatis.mapper-locations=classpath:mapper/**Mapper.xml

debug=true
logging.level.root=INFO
logging.level.com.example.onlinemusic.mapper=debug
logging.level.druid.sql.Statement=DEBUG
logging.level.com.example=DEBUG

3. 数据库的设计与实现

数据表分为三个表,用户表、聊天关系表、聊天列表。
用户表用来存储用户的信息。

这里设计三个字段,用户Id,用户账户,用户密码。

聊天关系表用来存储聊天的两个用户的关系。

这里设计三个字段,关系Id,发送者Id,接收者Id。

聊天列表用来存储对应的聊天的信息,根据关系Id,来识别是哪两者的用户。

这里设计5个字段,列表Id,关系Id,发送用户Id,发送内容,发送时间。

实现代码:

create database if not exists ChatSystem;

use ChatSystem;

drop table if exists user;

-- 创建一个用户信息表
create table user (
    userId int primary key auto_increment,
    username varchar(128) unique,
    password varchar(128) not null
);

drop table if exists user_link;

-- 聊天关系表
create table user_link (
    `linkId` int primary key auto_increment,
    `from` int not null,
    `to` int not null
);

drop table if exists chat_list;

-- 聊天列表
create table chat_list(
    listId int primary key auto_increment,
    linkId int,
    userId int,
    content varchar(128) not null,
    createtime datetime
);

4. 登录注册模块的设计与实现

4.1 登录注册统一响应类

这里登录注册后端返回的数据,统一是这个格式。

import lombok.Data;

@Data
public class ResponseBodyMessage<T> {
    private int status;
    private String message;
    private T data;

    public ResponseBodyMessage(int status,String message,T data) {
        this.status = status;
        this.message = message;
        this.data = data;
    }
}

4.2 BCrypt加密

4.2.1 添加依赖类

在pom.xml中添加BCrypt的依赖类

	<!-- security依赖包 (加密)-->
	<dependency>
		<groupId>org.springframework.security</groupId>
		<artifactId>spring-security-web</artifactId>
	</dependency>
	<dependency>
		<groupId>org.springframework.security</groupId>
		<artifactId>spring-security-config</artifactId>
	</dependency>

4.2.2 在启动类中添加代码


@SpringBootApplication(exclude = {org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration.class})
public class DemoApplication {
	public static void main(String[] args) {
		SpringApplication.run(DemoApplication.class, args);
	}

}

4.2.3 在AppConfig类中注入Bean对象

@Configuration
public class AppConfig {
	 @Bean
	    public BCryptPasswordEncoder getBCryptPasswordEncoder() {
	        return new BCryptPasswordEncoder();
	    }
}

4.3 添加拦截器

防止未登录用户进入非法界面.

4.3.1 LoginInterceptor 类

public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        HttpSession session = request.getSession(false);
        if (session != null && session.getAttribute("user") != null){
            return true;
        }
        response.setStatus(403);
        response.sendRedirect("/login.html");
        return false;
    }
}

4.3.2 AppConfig 类

@Configuration
public class AppConfig implements WebMvcConfigurer{
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .addPathPatterns("/**")
                .excludePathPatterns("/**/*.js")
                .excludePathPatterns("/**/*.jpg")
                .excludePathPatterns("/**/*.css")
                .excludePathPatterns("/**/*.png")
                .excludePathPatterns("/**/login.html")
                .excludePathPatterns("/**/register.html")
                .excludePathPatterns("/**/login")
                .excludePathPatterns("/**/register");
    }

    @Bean
    public BCryptPasswordEncoder getBCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

4.4 具体代码实现

之前代码中, 登录注册流程很清楚, 可以查看之前博客.

@RestController
public class UserController {
    @Resource
    private UserService userService;

    @Autowired
    private BCryptPasswordEncoder bCryptPasswordEncoder;

    @RequestMapping("/login")
    public ResponseBodyMessage<Boolean> userLogin(@RequestBody User user, HttpServletRequest request) {
        User truUser = userService.selectByUserName(user.getUsername());
        if (truUser == null) {
            System.out.println("登录失败!");
            return new ResponseBodyMessage<>(-1, "用户名密码错误!", false);
        } else {
            boolean flg = bCryptPasswordEncoder.matches(user.getPassword(), truUser.getPassword());
            if (!flg) {
                return new ResponseBodyMessage<>(-1, "用户名密码错误!", false);
            }
            System.out.println("登录成功!");
            HttpSession session = request.getSession(true);
            System.out.println(session);
            session.setAttribute("user", truUser);
            return new ResponseBodyMessage<>(1, "登录成功!", true);
        }
    }


    @RequestMapping("/register")
    public ResponseBodyMessage<User> register(@RequestBody User user) {
        if(user.getUsername() == null || "".equals(user.getUsername().trim())
                || user.getPassword() == null || "".equals(user.getPassword().trim())){
            return new ResponseBodyMessage<>(-1,"输入内容为空!",null);
        }
        User truUser = userService.selectByUserName(user.getUsername());
        if (truUser != null) {
            return new ResponseBodyMessage<>(-1,"当前用户名已经存在!",null);
        } else{
            String password = bCryptPasswordEncoder.encode(user.getPassword());
            user.setPassword(password);

            userService.addUser(user);
            return new ResponseBodyMessage<>(1,"注册成功!",user);
        }
    }

    @RequestMapping("/logout")
    public void userLogout(HttpServletRequest request, HttpServletResponse response) throws IOException {
        HttpSession session = request.getSession(false);
        // 拦截器的拦截, 所以不可能出现session为空的情况
        session.removeAttribute("user");
        response.sendRedirect("login.html");
    }

}

5. 私信模块的设计与实现

这里的私信功能, 主要运用到了WebSocket

5.1 基础配置

5.1.1 依赖类

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

5.1.2 在AppConfig中配置

@Configuration
@EnableWebSocket
public class AppConfig implements WebMvcConfigurer, WebSocketConfigurer {

    @Autowired
    private ChatController chatController;

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(chatController,"/intoChat")
                .addInterceptors(new HttpSessionHandshakeInterceptor());
    }
}

5.2 设计思路

在成功建立websocket连接的时候, 后端发送请求给前端, 让用户根据请求来绘制好友列表.

{
	status:  1, // 这里的1为成功请求, -1 为失败的请求
	message: "getUser",	
	users: "", // 列表用户
	fromusername: "", // 当前用户
}

在绘制好好友列表之后, 可以选择好友来进行对话.在点击好友之后, 发送请求给服务器, 服务器根据两者的用户账户进行建立会话, 读取之前的聊天记录.

请求

{
	from: , 发送用户账户
	to: , 接收用户账户
	message: "loadMessage",
}

响应

{
	status: 1,
	messages: "", // 以前的聊天记录, 每一条包含, 用户Id, 内容, 是否是发送者.
	message: "loadMessage",
	tousername: "", 接收用户账户
}

在进入聊天界面之后. 可以输入对应的消息, 然后点击发送, 进行发送消息.服务器就将消息发送给当前两者用户, 谁在线, 就更新列表. 不在线就不更新.

请求

{
	from: "", // 发送用户账户
	to: "", // 接收用户账户
	content: "", // 发送的信息内容
	message: "sendMessage"
}

响应

{
	status: 1,
	message: "sendMessage",
	messages: "", // 发送的消息
}

用户异常退出或者用户退出的时候, 在在线状态中设置离线, 在进入的时候设置上线.

5.3 用户在线状态管理器

这里使用 ConcurrentHashMap 来进行存储, key为用户Id, valueWebSocketSession

主要是三个功能
① 进入私聊界面添加用户状态到哈希表中
② 退出私聊界面删除哈希表中的用户状态
③ 获取当前用户的状态.

@Component
public class OnlineUserManager {
    // 哈希表存储的是用户的当前的状态,在线就存储到哈希表中
    private ConcurrentHashMap<Integer, WebSocketSession> userState = new ConcurrentHashMap<>();

    public void enterHall(int userId, WebSocketSession webSocketSession) {
        userState.put(userId,webSocketSession);
    }

    public void exitHall(int userId) {
        userState.remove(userId);
    }

    public WebSocketSession getState(int userId) {
        return userState.get(userId);
    }
}

5.4 设计数据库操作

主要是两个功能:

  1. 在点击用户头像的时候, 加载聊天界面, 并且读取聊天记录.
  2. 在点击发送按钮的时候, 发送消息, 并且加载消息到在线用户的窗口中.

实现功能1 需要去数据中, 查找两个用户的关系, 是否聊过天, 如果没有聊过天, 查询到的记录就是null. 如果聊过天, 根据关系去聊天列表中查询对应的记录.

功能1 涉及的数据库操作:

  1. 根据两个用户的Id, 在聊天关系表中查找linkId
  2. 根据linkId, 在聊天列表中查找对应的聊天记录

实习功能2, 需要去数据中, 查询是否两个用户聊过天, 是否存在linkId, 如果不存在, 就需要建立关系, 创建一个linkId, 并且根据linkId, 去插入数据, 如果存在, 直接根据linkId插入数据.

功能2 涉及的数据库操作:

  1. 在聊天关系表中, 插入一条数据, from, to分别为两者用户的Id
  2. 在聊天列表中, 根据linkId 插入一条数据, 添加发送该消息的用户Id,消息内容, 创建时间.

5.4.1 创建实体类

ChatList 实体类

@Data
public class ChatList {
    private int listId;
    private int linkId;
    private int userId;
    private String content;
    private Timestamp createtime;
}

UserLink 实体类

@Data
public class UserLink {
    private int linkId;
    private int from;
    private int to;
}

5.4.2 在mapper文件夹下创建对应xml

ChatListMapper.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.example.demo.mapper.ChatListMapper">
    <select id="selectChat" resultType="com.example.demo.model.ChatList">
        select * from chat_list where linkId = #{linkId} order by createtime asc;   
    </select>

    <insert id="insertChat" keyProperty="listId" keyColumn="listId">
        insert into chat_list(linkId,userId,content,createtime) values (#{linkId},#{fromId},#{content},#{timestamp});
    </insert>
</mapper>

5.4.3 对应Mapper接口

ChatListMapper 接口

@Mapper
public interface ChatListMapper {
    List<ChatList> selectChat(int linkId);

    void insertChat(Integer linkId, Integer fromId, String content, Timestamp timestamp);
}

UserLinkMapper 接口

@Mapper
public interface UserLinkMapper {
    Integer selectLinkId(int fromId,int toId);

    void insertLink(int min, int max);
}

5.4.4 对应的Service类

ChatListService 类

@Service
public class ChatListService {
    @Autowired
    private ChatListMapper chatListMapper;

    public List<ChatList> selectChat(int linkId){
        return chatListMapper.selectChat(linkId);
    }

    public void insertChat(Integer linkId, Integer fromId, String content, Timestamp timestamp) {
        chatListMapper.insertChat(linkId,fromId,content,timestamp);
    }
}

UserLinkService

@Service
public class UserLinkService {
    @Autowired
    private UserLinkMapper userLinkMapper;

    public Integer selectLinkId(int fromId,int toId){
        return userLinkMapper.selectLinkId(fromId,toId);
    }

    public void insertLink(int min, int max) {
        userLinkMapper.insertLink(min,max);
    }
}

5.5 Controller类的实现

这里采用了webSocket方法, 主要是四个类来实现,

  1. afterConnectionEstablished 连接成功的时候调用
  2. handleTextMessage 接收请求的时候调用
  3. handleTransportError 连接异常断开的时候调用
  4. afterConnectionClosed 连接断开的时候调用

5.5.1 连接成功的时候调用类

  1. 首先判断当前用户是否已经登录, 防止用户多开
  2. 将用户的在线状态设置为在线
  3. 从数据库中查找所有的用户
  4. 设置响应类, 并添加对应的信息
  5. 返回响应.
    // 连接成功调用
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        ResponseMessage responseMessage = new ResponseMessage();
        // 1. 首先判断当前用户是否已经登录, 防止用户多开
        User user = (User) session.getAttributes().get("user");
        if(onlineUserManager.getState(user.getUserId()) != null) {
            responseMessage.setStatus(-1);
            responseMessage.setMessage("当前用户已经登录了, 不要重复登录");
            session.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(responseMessage)));
            return;
        }
        // 2. 将用户的在线状态设置为在线
        onlineUserManager.enterHall(user.getUserId(),session);
        // 3. 从数据库中查找所有的用户
        // 4. 设置响应类, 并添加对应的信息
        responseMessage.setStatus(1);
        responseMessage.setMessage("getUser");
        responseMessage.setFromusername(user.getUsername());
        responseMessage.setUsers(userService.selectAllUser(user.getUsername()));
        // 5. 返回响应
        session.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(responseMessage)));
    }

5.5.2 接收请求的时候调用类

  1. 解析请求的内容
  2. 判断是加载消息记录, 还是发送消息
    加载消息
         2.a.1 根据两者的用户Id 查看 linkId, 这里让from始终最小,to始终最大,方便查找
         2.a.2 判断当前的linkId是否存在, 不存在就不需要加载聊天记录了
         2.a.3 存在聊天记录,需要加载
         2.a.4 设置对应的响应, 并返回
    发送消息
         2.b.1 查找对应的linkId
         2.b.2 判断当前linkId是否为空, 为空需要创建linkId
         2.b.3 根据linkId, 在聊天列表中添加数据
         2.b.4 设置对应的响应信息
         2.b.5 获取两者用户的session, 并判断是否在线, 给在线的用户返回响应,刷新聊天框
  // 连接成功收到的响应
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        ResponseMessage responseMessage = new ResponseMessage();
        User user = (User) session.getAttributes().get("user");
        // 1. 解析请求的内容
        String payload = message.getPayload();
        System.out.println(payload);
        RequestMessage requestMessage = objectMapper.readValue(payload,RequestMessage.class);
        // 2. 判断是加载消息记录, 还是发送消息
        if(requestMessage.getMessage().equals("loadMessage")){
            // 2.a.1 根据两者的用户Id 查看 linkId, 这里让from始终最小,to始终最大,方便查找
            responseMessage.setMessage("loadMessage");
            responseMessage.setTousername(requestMessage.getTo());
            Integer fromId = userService.selectUserId(requestMessage.getFrom());
            Integer toId = userService.selectUserId(requestMessage.getTo());
            int min = Math.min(fromId,toId);
            int max = Math.max(fromId,toId);
            Integer linkId = userLinkService.selectLinkId(min,max);
            // 2.a.2 判断当前的linkId是否存在, 不存在就不需要加载聊天记录了
            if(linkId == null || linkId == 0) {
                responseMessage.setStatus(1);
                responseMessage.setMessages(null);
            }else{
                // 2.a.3 存在聊天记录,需要加载,
                responseMessage.setStatus(1);
                List<ChatList> chatLists = chatListService.selectChat(linkId);
                List<Message> messages = new ArrayList<>();
                for(ChatList chatList : chatLists) {
                    Message message1 = new Message();
                    message1.setMessage(chatList.getContent());
                    message1.setUserId(chatList.getUserId());
                    message1.setSender(chatList.getUserId() == user.getUserId());
                    messages.add(message1);
                }
                responseMessage.setMessages(messages);
            }
            // 2.a.4 设置对应的响应, 并返回
            session.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(responseMessage)));
        }
        if(requestMessage.getMessage().equals("sendMessage")) {
            // 2.b.1 查找对应的linkId
            Integer fromId = userService.selectUserId(requestMessage.getFrom());
            Integer toId = userService.selectUserId(requestMessage.getTo());
            int min = Math.min(fromId,toId);
            int max = Math.max(fromId,toId);
            Integer linkId = userLinkService.selectLinkId(min,max);
            // 2.b.2 判断当前linkId是否为空, 为空需要创建linkId
            if(linkId == null || linkId == 0) {
                responseMessage.setStatus(1);
                userLinkService.insertLink(min,max);
                linkId = userLinkService.selectLinkId(min,max);
            }
            // 2.b.3 根据linkId, 在聊天列表中添加数据
            String content = requestMessage.getContent();

            chatListService.insertChat(linkId,fromId,content,new Timestamp(System.currentTimeMillis()));
            // 2.b.4 设置对应的响应信息
            responseMessage.setStatus(1);
            responseMessage.setMessage("sendMessage");
            Message message1 = new Message();
            // 2.b.5 获取两者用户的session, 并判断是否在线, 给在线的用户返回响应, 刷新聊天框
            WebSocketSession session1 = onlineUserManager.getState(fromId);
            WebSocketSession session2 = onlineUserManager.getState(toId);
            if(session1 != null) {
                message1.setSender(true);
                message1.setUserId(fromId);
                message1.setMessage(content);
                List<Message> list = new ArrayList<>();
                list.add(message1);
                responseMessage.setMessages(list);
                session1.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(responseMessage)));
            }
            if(session2 != null) {
                message1.setSender(false);
                message1.setUserId(toId);
                message1.setMessage(content);
                List<Message> list = new ArrayList<>();
                list.add(message1);
                responseMessage.setMessages(list);
                session2.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(responseMessage)));
            }
        }
    }

5.5.3 连接异常断开的时候调用

// 连接异常调用
    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        User user = (User) session.getAttributes().get("user");
        WebSocketSession webSocketSession = onlineUserManager.getState(user.getUserId());
        if(webSocketSession == session) {
            // 2. 设置在线状态
            onlineUserManager.exitHall(user.getUserId());
        }
        System.out.println("用户"+user.getUsername()+"退出");
    }

5.5.4 连接断开的时候调用

    // 连接关闭调用
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        User user = (User) session.getAttributes().get("user");
        WebSocketSession webSocketSession = onlineUserManager.getState(user.getUserId());
        if(webSocketSession == session) {
            // 2. 设置在线状态
            onlineUserManager.exitHall(user.getUserId());
        }
        System.out.println("用户"+user.getUsername()+"退出");
    }

5.5 前端代码

5.5.1 html文件

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>好友界面</title>
    <link rel="stylesheet" href="css/index.css">
</head>
<body>
    <div class="parent">
        <div class="left">
            <div class="titleList">
                <img src="image/头像.jpg" class="pg">
                <span class="username">1234124124</span>
            </div>
            <ul class="chatList">
                <!-- <li class="touser">
                    <img src="image/头像.jpg" class="pg">
                    <div class="information">
                        <span class="tousername">1233</span>
                        <span class="preview">dmwlfmwfmw</span>
                    </div>
                </li> -->
            </ul>
        </div>
        <div class="right">
            <!-- <div class="touserName"><span>1233123213</span></div>
            <div class="MessageList">
                <div class="tous">
                    <img src="image/头像.jpg" class="pg">
                    <div class="sendMes">123</div>
                </div>
                <div class="fromus">
                    <img src="image/头像.jpg" class="pg">
                    <div class="sendMes">123</div>
                </div>
            </div>
            <div class="inputList">
                <textarea οninput="updateNum()" maxlength="100" id="textareaContent"></textarea>
                <div class="commentText">
                    <div class="textLine">还能输入<em>100</em>个字符</div>
                    <button class="sendComment">发送</button>
                </div>     
            </div> -->
        </div>
    </div>
</body>
</html>
<script src="js/jquery.min.js"></script>
<script>
    function updateNum() {
        let text = $("#textareaContent").val();
        $("em").html(100-text.length);
    }




    let websocketUrl = 'ws://'+ location.host +'/intoChat';
    let websocket = new WebSocket(websocketUrl);


    websocket.onopen = function() {
        console.log("房间链接成功!");
    }
    websocket.onclose = function() {
        console.log("房间断开链接");
    }
    websocket.onerror = function() {
        console.log("房间出现异常");
    }
    window.onbeforeunload = function() {
        websocket.close();
    }
    websocket.onmessage = function(e) {
        console.log(e.data);
        let resp = JSON.parse(e.data);
        if(resp.status == -1) {
            alert(resp.message);
            location.assign("login.html");
            return;
        }else{
            if(resp.message == "getUser"){
                let username = document.querySelector('.username');
                username.innerHTML = resp.fromusername;
                createList(resp.users);
            }
            if(resp.message == "loadMessage") {
                createChatList(resp);
            }
            if(resp.message == "sendMessage") {
                let MessageList = document.querySelector('.MessageList');
                for(let message of resp.messages) {
                    if(message.isSender){
                        let fromus = document.createElement('div');
                        fromus.className = 'fromus';
                        let img = document.createElement('img');
                        img.src='image/头像.jpg';
                        img.className = 'pg';
                        let sendMes = document.createElement('div');
                        sendMes.className = 'sendMes';
                        sendMes.innerHTML = message.message;
                        fromus.appendChild(img);
                        fromus.appendChild(sendMes);
                        MessageList.appendChild(fromus);
                    }else{
                        let tous = document.createElement('div');
                        tous.className = 'tous';
                        let img = document.createElement('img');
                        img.src='image/头像.jpg';
                        img.className = 'pg';
                        let sendMes = document.createElement('div');
                        sendMes.className = 'sendMes';
                        sendMes.innerHTML = message.message;
                        tous.appendChild(img);
                        tous.appendChild(sendMes);
                        MessageList.appendChild(tous);
                    }
                }
            }
        }
    }

    function createChatList(resp){
        let messages = resp.messages;
        let tousername = resp.tousername;
        let s = "";
        s += "<div class='touserName'><span>"+tousername+"</span></div>";
        s += "<div class='MessageList'>";
        if(messages != null) {
            for(let message of messages) {
                if(message.isSender){
                    s+="<div class='fromus'>";
                    s+="<img src='image/头像.jpg' class='pg'>"
                    s+="<div class='sendMes'>"+message.message+"</div>"
                    s+="</div>";
                }else{
                    s+="<div class='tous'>";
                    s+="<img src='image/头像.jpg' class='pg'>"
                    s+="<div class='sendMes'>"+message.message+"</div>";
                    s+="</div>";
                }
            }
        }
        s += "</div>";
        s += "<div class='inputList'>"
        s += "<textarea οninput='updateNum()' maxlength='100' id='textareaContent'></textarea>";
        s += "<div class='commentText'>"
        s += "<div class='textLine'>还能输入<em>100</em>个字符</div>"
        s += "<button class='sendComment' οnclick=btnOn('"+tousername+"')>发送</button>";
        s += "</div></div>";
        $('.right').html(s);
    }

    function createList(users) {
        let s = "";
        for(let user of users) {
            s += "<li class='touser' οnclick=Chat('"+user.username+"')>";
            s += "<img src='image/头像.jpg' class='pg'>";
            s += "<div class='information'>";
            s += "<span class='tousername'>"+user.username+"</span>";
            s += "<span class='preview'></span></div></li>"
        }
        $('.chatList').html(s);
    }

    function Chat(s) {
        let username = document.querySelector('.username');
        let req = {
            from: username.textContent,
            to: s,
            message: "loadMessage",
        }

        websocket.send(JSON.stringify(req));
    }

    function btnOn(tousername) {
        let username = document.querySelector('.username');
        let text = $("#textareaContent").val().trim();
        if(text == ""){
            alert("请输入内容,不要输入空格");
            return;
        }
        let req = {
            from : username.textContent,
            to: tousername,
            content: text,
            message: "sendMessage",
        }
        websocket.send(JSON.stringify(req));
        let text2 = document.querySelector("#textareaContent");
        text2.value = "";
    }

</script>

5.5.2 css文件

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
    /* background-image: url(../image/1.jpg); */
}
html,body{
    height: 100%;
    background-image: url(../image/1.jpg);
    background-position: center center;
    background-size: cover;
    background-repeat: no-repeat;
}
.parent{
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100%;
    width: 100%;
}

.left{
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    height: 80%;
    width: 300px;
}

.titleList{
    background-color: #2e2e2e;
    width: 100%;
    height: 75px;
    padding-left: 10px;

    display: flex;
    justify-content: flex-start;
    align-items: center;

    
    border-bottom: 1px solid white;
}

.chatList{
    background-color: #bdb0b0;
    width: 100%;
    height: calc(100% - 75px);
}

.right{
    background-color: rgba(235, 229, 229, 0.9);
    height: 80%;
    width: 700px;

    display: flex;
    flex-direction: column;
    justify-content: flex-start;
}

.pg{
    border-radius: 50%;
    height: 50px;
    width: 50px;
}
.username{
    color: white;
    margin-left: 10px;
}
.touser{
    padding: 10px;

    display: flex;
    justify-content: flex-start;
    align-items: center;

    border-bottom: 1px solid white;
}
.tousername{
    color: white;
}
.preview{
    color: white;
}
.information{
    margin-left: 10px;

    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: flex-start;
}
#textareaContent{
    display: block;
    width: 97%;
    background: rgba(248,249,251,0.8);
    border: none;
    padding: 0 16px;
    border-radius: 4px;
    resize: none;
    height: 88px;
    font-size: 14px;
    line-height: 22px;
    margin: 8px;
}


.commentText{
    padding: 0 16px;
    display: flex;
    justify-content: space-between;
    align-items: center;
}

.sendComment{
    display: block;
    width: 77px;
    height: 24px;
    background: #fc5531;
    color: #fff;
    border-radius: 16px;
    font-size: 14px;
    text-align: center;
    line-height: 24px;
    border: none;
}
.sendComment:active{
    color: #fc5531;
    background: #fff;
}
.textLine{
    font-size: 8px;
}
em{    
    color: #222226;
    margin: 0 4px;
    font-style: normal;
}

.touserName{
    height: 64px;
    width: 100%;
    border-bottom: 2px solid black;
}

.touserName span{
    margin-left: 14px;
    line-height: 64px;
}

.MessageList{
    margin: 10px;
    height: 66%;

    overflow:auto;
}

.inputList{
    border-top: 2px solid black;
    height: calc(34% - 64px);
}

.tous{
    display: flex;
    align-items: center;
    justify-content: flex-start;
}

.fromus{
    display: flex;
    align-items: center;
    flex-direction: row-reverse;
}

.sendMes{
    margin: 10px;
    line-height: 28px;
    padding: 4px 12px;
    color: #222226;
    background: #cad9ff;
    border-radius: 5px;
}

6. 群聊功能

这里页面没设计, 主要是通过在用户在线状态管理器中添加几个类, 返回在线的所有用户, 在界面的时候一个用户发送消息, 直接返回给所有的用户.

6.1 用户状态管理器

    public WebSocketSession getState(int userId) {
        return userState.get(userId);
    }

    public int getOnlinePeople() {
        return userState.size();
    }

6.2 代码实现

6.2.1 后端代码

package com.example.demo.controller;

import com.example.demo.model.User;
import com.example.demo.room.OnlineUserManager;
import com.example.demo.room.RequestMessage;
import com.example.demo.room.ResponseMessage;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

import java.util.Collection;

@Component
public class RoomController extends TextWebSocketHandler {
    private ObjectMapper objectMapper = new ObjectMapper();

    @Autowired
    private OnlineUserManager onlineUserManager;

    // 连接成功调用
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        User user = (User) session.getAttributes().get("user");
        onlineUserManager.enterHall(user.getUserId(),session);
        System.out.println("当前人数: " + onlineUserManager.getOnlinePeople());
        ResponseMessage responseMessage = new ResponseMessage();
        responseMessage.setMessage("people");
        responseMessage.setNumber(onlineUserManager.getOnlinePeople());
        Collection<WebSocketSession> collection = onlineUserManager.getAllSession();
        for(WebSocketSession s : collection) {
            s.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(responseMessage)));
        }
    }

    // 连接成功收到的响应
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        User user = (User) session.getAttributes().get("user");
        String payload = message.getPayload();
        System.out.println(payload);
        RequestMessage requestMessage = objectMapper.readValue(payload,RequestMessage.class);
        ResponseMessage responseMessage = new ResponseMessage();
        responseMessage.setMessage("chatMessage");
        responseMessage.setContent(requestMessage.getContent());
        Collection<WebSocketSession> collection = onlineUserManager.getAllSession();
        for(WebSocketSession s : collection) {
            s.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(responseMessage)));
        }
    }

    // 连接异常调用
    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        User user = (User) session.getAttributes().get("user");
        WebSocketSession webSocketSession = onlineUserManager.getState(user.getUserId());
        if(webSocketSession == session) {
            // 2. 设置在线状态
            onlineUserManager.exitHall(user.getUserId());
        }
        System.out.println("用户"+user.getUsername()+"退出");
    }

    // 连接关闭调用
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        User user = (User) session.getAttributes().get("user");
        WebSocketSession webSocketSession = onlineUserManager.getState(user.getUserId());
        if(webSocketSession == session) {
            // 2. 设置在线状态
            onlineUserManager.exitHall(user.getUserId());
        }
        System.out.println("用户"+user.getUsername()+"退出");
    }
}

6.2.2 前端代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <input type="text" id="content">
    <input type="submit" id="up">
    <hr>
    <div id="chat">
        <div id="notic">当前在线用户: <span id="totalPeople">0</span></div>
    </div>
</body>
</html>
<script>
    let websocketUrl = 'ws://'+ location.host +'/intoRoom';
    let websocket = new WebSocket(websocketUrl);


    websocket.onopen = function() {
        console.log("房间链接成功!");
    }
    websocket.onclose = function() {
        console.log("房间断开链接");
    }
    websocket.onerror = function() {
        console.log("房间出现异常");
    }
    window.onbeforeunload = function() {
        websocket.close();
    }
    websocket.onmessage = function(e) {
        console.log(e.data);
        let resp = JSON.parse(e.data);
        let chat = document.querySelector('#chat');
        let div = document.createElement('div');
        if(resp.message == 'people'){
            let total = document.querySelector("#totalPeople");
            total.innerHTML = resp.number;
        }
        if(resp.message == 'chatMessage'){
            div.innerHTML=resp.content;
        }
        chat.appendChild(div);
    }

    let submit = document.querySelector("#up");
    submit.onclick = function() {
        let content = document.querySelector("#content");
        let req = {
            content: content.value,
        }
        websocket.send(JSON.stringify(req));
    }
</script>

有关私信功能的设计与实现的更多相关文章

  1. ruby-on-rails - Rails - 子类化模型的设计模式是什么? - 2

    我有一个模型:classItem项目有一个属性“商店”基于存储的值,我希望Item对象对特定方法具有不同的行为。Rails中是否有针对此的通用设计模式?如果方法中没有大的if-else语句,这是如何干净利落地完成的? 最佳答案 通常通过Single-TableInheritance. 关于ruby-on-rails-Rails-子类化模型的设计模式是什么?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.co

  2. ruby-on-rails - 使用 rails 4 设计而不更新用户 - 2

    我将应用程序升级到Rails4,一切正常。我可以登录并转到我的编辑页面。也更新了观点。使用标准View时,用户会更新。但是当我添加例如字段:name时,它​​不会在表单中更新。使用devise3.1.1和gem'protected_attributes'我需要在设备或数据库上运行某种更新命令吗?我也搜索过这个地方,找到了许多不同的解决方案,但没有一个会更新我的用户字段。我没有添加任何自定义字段。 最佳答案 如果您想允许额外的参数,您可以在ApplicationController中使用beforefilter,因为Rails4将参数

  3. ruby - 如何根据特征实现 FactoryGirl 的条件行为 - 2

    我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden

  4. ruby-on-rails - Cucumber 是否只是 rspec 的包装器以帮助将测试组织成功能? - 2

    只是想确保我理解了事情。据我目前收集到的信息,Cucumber只是一个“包装器”,或者是一种通过将事物分类为功能和步骤来组织测试的好方法,其中实际的单元测试处于步骤阶段。它允许您根据事物的工作方式组织您的测试。对吗? 最佳答案 有点。它是一种组织测试的方式,但不仅如此。它的行为就像最初的Rails集成测试一样,但更易于使用。这里最大的好处是您的session在整个Scenario中保持透明。关于Cucumber的另一件事是您(应该)从使用您的代码的浏览器或客户端的角度进行测试。如果您愿意,您可以使用步骤来构建对象和设置状态,但通常您

  5. 华为OD机试用Python实现 -【明明的随机数】 2023Q1A - 2

    华为OD机试题本篇题目:明明的随机数题目输入描述输出描述:示例1输入输出说明代码编写思路最近更新的博客华为od2023|什么是华为od,od薪资待遇,od机试题清单华为OD机试真题大全,用Python解华为机试题|机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为o

  6. 基于C#实现简易绘图工具【100010177】 - 2

    C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.

  7. LC滤波器设计学习笔记(一)滤波电路入门 - 2

    目录前言滤波电路科普主要分类实际情况单位的概念常用评价参数函数型滤波器简单分析滤波电路构成低通滤波器RC低通滤波器RL低通滤波器高通滤波器RC高通滤波器RL高通滤波器部分摘自《LC滤波器设计与制作》,侵权删。前言最近需要学习放大电路和滤波电路,但是由于只在之前做音乐频谱分析仪的时候简单了解过一点点运放,所以也是相当从零开始学习了。滤波电路科普主要分类滤波器:主要是从不同频率的成分中提取出特定频率的信号。有源滤波器:由RC元件与运算放大器组成的滤波器。可滤除某一次或多次谐波,最普通易于采用的无源滤波器结构是将电感与电容串联,可对主要次谐波(3、5、7)构成低阻抗旁路。无源滤波器:无源滤波器,又称

  8. MIMO-OFDM无线通信技术及MATLAB实现(1)无线信道:传播和衰落 - 2

     MIMO技术的优缺点优点通过下面三个增益来总体概括:阵列增益。阵列增益是指由于接收机通过对接收信号的相干合并而活得的平均SNR的提高。在发射机不知道信道信息的情况下,MIMO系统可以获得的阵列增益与接收天线数成正比复用增益。在采用空间复用方案的MIMO系统中,可以获得复用增益,即信道容量成倍增加。信道容量的增加与min(Nt,Nr)成正比分集增益。在采用空间分集方案的MIMO系统中,可以获得分集增益,即可靠性性能的改善。分集增益用独立衰落支路数来描述,即分集指数。在使用了空时编码的MIMO系统中,由于接收天线或发射天线之间的间距较远,可认为它们各自的大尺度衰落是相互独立的,因此分布式MIMO

  9. 计算机毕业设计ssm+vue基本微信小程序的小学生兴趣延时班预约小程序 - 2

    项目介绍随着我国经济迅速发展,人们对手机的需求越来越大,各种手机软件也都在被广泛应用,但是对于手机进行数据信息管理,对于手机的各种软件也是备受用户的喜爱小学生兴趣延时班预约小程序的设计与开发被用户普遍使用,为方便用户能够可以随时进行小学生兴趣延时班预约小程序的设计与开发的数据信息管理,特开发了小程序的设计与开发的管理系统。小学生兴趣延时班预约小程序的设计与开发的开发利用现有的成熟技术参考,以源代码为模板,分析功能调整与小学生兴趣延时班预约小程序的设计与开发的实际需求相结合,讨论了小学生兴趣延时班预约小程序的设计与开发的使用。开发环境开发说明:前端使用微信微信小程序开发工具:后端使用ssm:VU

  10. 【Java入门】使用Java实现文件夹的遍历 - 2

    遍历文件夹我们通常是使用递归进行操作,这种方式比较简单,也比较容易理解。本文为大家介绍另一种不使用递归的方式,由于没有使用递归,只用到了循环和集合,所以效率更高一些!一、使用递归遍历文件夹整体思路1、使用File封装初始目录,2、打印这个目录3、获取这个目录下所有的子文件和子目录的数组。4、遍历这个数组,取出每个File对象4-1、如果File是否是一个文件,打印4-2、否则就是一个目录,递归调用代码实现publicclassSearchFile{publicstaticvoidmain(String[]args){//初始目录Filedir=newFile("d:/Dev");Datebeg

随机推荐