Realization of the "Web chat room" project from scratch, but not completely from scratch and not so prenatal (with code)

最终整个项目的完整代码我会放在文末
Summary of the overall process:
1. New project preparation
(1) Create a maven project; select maven-archetype-webapp
(2) modify web.xml under WEB-INF; configure dependency packages under pom.xml
(3) Plugins in Setting Install Lombok in this project (the role of this project is to no longer generate private Get and Set methods)
(4) You also need to prepare some toolkits and put them under the webapp, "css", "fonts" and "js" Some packages, these mainly constitute the previous page (I have it in my code)

2. Demand analysis
(1) Open the homepage and see the login page
(2) Successfully log in, enter the main page
(3) You can see the current channel (room) list in the main page
(4) Click on a channel, you can see Message in the channel (room)
(5) Click on a channel to send a message, and other users can also see the message at this time

3. Front-end and back-end API design
In this project, the front-end page has been given, and we need to return the content required by the front-end according to the development document. Before writing these interfaces, you can run the front-end page and have a look. Tell me here The provided index.html is placed in the webapp file; then configure the tomcat to start it, you can see the interface, you can try to click the following to log in or register, it must be no response, in the developer tools (Google browser press F12), we can See that the login port is 404. Next, let's write the back-end code part

Insert picture description here


ps: here is the code of the database, before the following projects, first use these SQL statements to create the database

drop database if exists java_chatroom;
create database java_chatroom character set utf8mb4;

use java_chatroom;

create table user (userId int primary key auto_increment,
                   name varchar(50) unique,
                   password varchar(50),
                   nickName varchar(50),   -- 昵称
                   iconPath varchar(2048), -- 头像路径
                   signature varchar(100),
                   lastLogout DateTime -- 上次登录时间
); -- 个性签名

insert into user values(null, 'test', '123', '周', '', '一起来打游戏呀', now());
insert into user values(null, 'test2', '123', '周2', '', '有没有怪猎来联机', now());
insert into user values(null, 'test3', '123', '周3', '', '我Rise200小时萌新', now());
insert into user values(null, 'test4', '123', '周4', '', '或者一起玩原神呀', now());



create table channel (channelId int primary key auto_increment,
                      channelName varchar(50)
);
insert into channel values(null, '体坛赛事');
insert into channel values(null, '娱乐八卦');
insert into channel values(null, '时事新闻');
insert into channel values(null, '午夜情感');



create table message (messageId int primary key auto_increment,
                      userId int, -- 谁发的
                      channelId int, -- 发到哪个频道中
                      content text, -- 消息内容是啥
                      sendTime DateTime default now()    -- 发送时间
);

insert into message values (null, 1, 1, 'hehe1', now());
insert into message values (null, 1, 1, 'hehe2', now());
insert into message values (null, 1, 1, 'hehe3', now());

(1) First write a tool class

  • ①The method of establishing a connection with the database
  • ②Serialize and deserialize the json string; later we will see that in the front-end and back-end API interfaces, the objects returned by the back-end have a fixed format, so we need to serialize
    this part of the code. I also paste it here (The detailed code description is written in the code comment)
package example.util;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mysql.jdbc.jdbc2.optional.MysqlDataSource;
import example.exception.AppException;


import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

public class Util {

    private static final ObjectMapper M = new ObjectMapper(); // 一种数据模型转换框架
                                                              // 方便将模型对象转换为JSON
    private static final MysqlDataSource DS = new MysqlDataSource(); // 用来数据库连接的对象

    // 设置静态变量
    static {
        DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); // 设置时间的标准格式
        M.setDateFormat(df);
        DS.setURL("jdbc:mysql://localhost:3306/java_chatroom"); // 数据库名字设置成自己的数据库(我提供的数据库名字叫做java_chatroom)
        DS.setUser("root"); // 设置mysql的用户名
        DS.setPassword("123456"); // 设置mysql的密码(用户名和密码设置自己本机的)
        DS.setUseSSL(false); // 当JDBC 比 mysql 版本不兼容(JDBC版本高于mysql兼容版本)设置为true
        DS.setCharacterEncoding("UTF-8"); // 防止中文乱码
    }

    /***
     * json序列化:java对象转化为json字符串
     *
     * json字符串就理解为前后端沟通常用的一种字符串格式
     */
    public static String serialize(Object o){
        try{
            return M.writeValueAsString(o);
        }catch (JsonProcessingException e){ // 注意异常的种类不要写错了
            throw new AppException("json序列化失败" + o, e);
        }
    }


    /***
     * json反序列化:json字符串转换为java对象
     */
    public static <T> T deserialize(String s, Class<T> c){
        // 这里我们用到了泛型,因为我们要转换成为的java对象并不固定
        // 比如我们要把json中的信息转换成用户对象;把另一个json中的信息转换成发送的消息对象
        // 所以这里用泛型来定义反序列化
        try{
            return M.readValue(s, c); // 注意这里不是readValues,我当时就没注意被这个s折磨了老久
        }catch (JsonProcessingException e){
            throw new AppException("json反序列化失败", e);
        }
    }

    // 为了满足输入是InputStream对象,我们重载(同一个类下,方法名一样,参数和返回值不一样)反序列方法
    public static <T> T deserialize(InputStream is, Class<T> c){
        try {
            return M.readValue(is, c);
        }catch (IOException e){
            throw new AppException("json反序列化失败", e);
        }
    }

    /**
     * 获取数据库链接
     * */
    public static Connection getConnection(){
        try{
            return DS.getConnection();
        }catch (SQLException e){
            throw new AppException("获取数据库连接失败", e);
        }
    }

    /**
     * 释放jdbc资源
     */
    public static void close(Connection c, Statement s, ResultSet r){
        try{
            if(r != null) r.close();
            if(s != null) s.close();
            if(c != null) c.close();
        }catch (SQLException e){
            throw new AppException("释放数据资源出错", e);
        }
    }
    public static void close(Connection c, Statement s){
        close(c, s, null);
    }

    // 以上我们就把一些常用工具写完了,这里可以写一个主函数测试一下
//    public static void main(String[] args){
//        // 测试一下json序列化
//        Map<String, Object> map = new HashMap<>();
//        map.put("ok", true);
//        map.put("d", new Date());
//
//        System.out.println(serialize(map));
//        // 运行后就可以看到,这里将使用map存放的键和值转化成了一个JSON字符串(用map的原因应该是有键值对儿的原因吧)
//
//        // 测试数据库链接,执行这步前,先把我提供的初始化数据库代码在cmd的mysql里面运行一下,保证自己本机有这个数据库
//        System.out.println(getConnection());
//    }
}



(2) Realize the login function

According to the description of the request and response of the login function in the development document as follows:

请求:
POST /login
{
   name: xxx,
   password: xxx
}
响应:
HTTP/1.1 200 OK
{
   ok: true,
   reason: xxx,
   userId: xxx,
   name: xxx,
   nickName: xxx,
   signature: xxx
}

Create a servlet class under the java folder to store and deliver the front-end and back-end information.
Create a LoginServlet class:

  • ① It is customary to inherit HttpServlet and write the routing address of WebServlet(). According to the development document, the routing address is known as @WebServlet("/login")
  • ② Then generate doGet and doPost methods;
    here is an explanation of the function of this login page: when we enter the main page, we should check whether we have logged in at this time, so when we implement the login function, we must also implement one The function of detecting the login status; because this information is saved in the browser session after the user logs in, in order to facilitate the user not to re-login the information without refreshing the browser once, we need to confirm whether the browser session has been saved and logged in If there is this information, it will directly enter the screen channel page. If there is no such information, it will display the initial "Please login page"
  • ③ We implement the login interface in doPost; implement the interface to detect the login status in doGet: The convention is mainly to note that I write in the code and paste it below
 注意: 这部分代码实现需要以下面4,5,6部分为前提,其中一些功能的定义和实现在下面,可以先往后看
 
package example.servlet;
import example.dao.UserDAO;
import example.exception.AppException;
import example.model.User;
import example.util.Util;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;

@WebServlet("/login")
public class LoginServlet extends HttpServlet {

    // 第二步编写:检测登陆状态接口,主要是在页面初始化时执行
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        req.setCharacterEncoding("UTF-8");  // 请求编码格式
        resp.setCharacterEncoding("UTF-8"); // 响应编码格式
        resp.setContentType("application/json"); // 前后端是以json字符串的格式传递的

        // 返回给前端的还是user对象的用户信息
        User user = new User();

        // 获取当前请求的Session,并从中获取用户信息,如果获取不到,返回 ok 为false
        HttpSession session = req.getSession(false);// false的意思就是如果没有获取到session信息就不创建新的session
        if (session != null) {
            User get = (User) session.getAttribute("user"); //这里的"user"和登陆下面的setAttribute是对应的
            if (get != null) {
                // 说明已经登陆了
                // 设置返回个前端的参数
                user = get;
                user.setOk(true);
                resp.getWriter().println(Util.serialize(user)); // 返回响应数据
                return;
            }
        }
        // 没有获取到session或者用户信息
        user.setOk(false); // 其实默认就是false
        user.setReason("用户未登录");
        // 返回响应数据:从响应对象获取数据流,打印输出响应体body
        resp.getWriter().println(Util.serialize(user));
    }



    // 第一步编写:登陆接口
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        req.setCharacterEncoding("UTF-8"); // 设置请求的编码格式
        resp.setCharacterEncoding("UTF-8"); // 设置响应的编码格式
        resp.setContentType("application/json"); // 前后端是以json字符串的格式传递的

        // 根据前端的请求(输入的账号密码)要去数据库中查询是否有与之匹配的信息
        User user = new User(); // 用于返回响应给前端
        try{
            // 1、解析请求数据,根据接口文档,需要使用反序列化操作
            //          将前端请求信息反序列化为用户对象
            User input = Util.deserialize(req.getInputStream(), User.class); // 反序列化为用户类对象

            // 2、业务处理:去数据库验证账号密码,如果验证通过,保存用户信息于Session中
            //      首先根据账号查询是否有此用户
            User query = UserDAO.queryByName(input.getName());
            if(query == null){
                // 没有在用户表中查询到
                throw  new AppException("用户不存在");
            }
            if(!query.getPassword().equals(input.getPassword())){
                // 从数据库中拿到该用户名的密码和前端输入的密码不一致
                throw new AppException("账号或密码错误");
            }
            // 能执行到这里,说明验证通过了
            // 在session中保存用户信息
            HttpSession session = req.getSession(); // 根据请求拿到session,如果没有,就创建一个session,默认是true
            session.setAttribute("user", query); // 以"user"作为名字,保存用户信息

            user = query; //将查询到的用户信息传给要返回给前端的user
            // 设置返回参数,成功返回了,就设置ok为true
            user.setOk(true);
        }catch (Exception e){
            e.printStackTrace();
            // 构造返回给前端响应失败了,就设置ok为false
            user.setOk(false);

            // 这里就体现出了自定义异常的好处,我们给前端返回我们想要返回的内容
            if(e instanceof AppException){
                user.setReason(e.getMessage());
            }else{
                // 如果出现了非自定义异常的情况,可以不报英文,报我们给定的内容
                user.setReason("未知错误,请联系管理员");
            }
        }
        // 3、返回相应数据:从响应对象中获取输出流(序列化为JSON字符串),打印输出到响应体body
         resp.getWriter().println(Util.serialize(user));

    }
}

  • ④ Before implementing the doPost and doGet functions, another point is that since we want to get user information from the Session, there should be a user object, and it can be seen from the development documentation that the request and response are both an object. Didn't we write a json serialization and deserialization before? When logging in, we need to deserialize the input into a user object (we need to create this object); when checking whether to log in, we also convert the information in the Session As a user object. So, in this step, you need to create a new template class folder, and create a new user class object template below. The code is here
package example.model;

// 因为我们这里在File -> Settings -> Plugins -> 中安装了Lombok(没有安装的,在里面搜索Lombok然后点击install,安装完重启idea就行)
// 所以可以直接简写私有变量的get,set和toString方法

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

import java.io.Serializable;

@Getter
@Setter
@ToString

public class User extends Response implements Serializable {
   // 因为数据库使用、前后端ajax、Session保存是基于对象和二进制的数据转换,所以要实现串行化接口

   private  static  final  Long serialVersionUID = 1L;
   // serialVersionUID 的作用主要是验证传来的字节流中seriaVersionUID与本地响应实体类的serialVersionUID进行比较
   // 如果相同说明是一致,就可以进行反序列化(这个概念我也不是很理解,大概就是个安全验证的意思吧)
   // 1L就是默认生成  serialVersionUID 的方式


   // 这里要根据数据库中来定义
   // 数据库中 用户表 有多少个 属性
   // 在 用户的模板类中 就有多少个 成员变量
   // 要一一对应起来
   private Integer userId;   // 用户Id
   private String  name;     // 用户名(账号)
   private String  password; // 用户密码
   private String  nickName; // 昵称
   private String  iconPath; // 头像路径(这个属性本项目不用)
   private String signature; // 个性签名
   private java.util.Date lastLogout; // 用户最后一次登陆的时间(记录的是用户下线的时间点)
}

  • ⑤ When I define the template class here, I use the response as a separate template class. This template class contains general response information, which is also for the unification of the front-end and back-end interface fields.
package example.model;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

/**
 * 前后端接口需要的统一字段
 */

// 因为我们这里在File -> Settings -> Plugins -> 中安装了Lombok(没有安装的,在里面搜索Lombok然后点击install,安装完重启idea就行)
// 所以可以直接简写私有变量的get,set和toString方法

@Getter
@Setter
@ToString

public class Response {

    // 当前借口响应是否操作成功
    private boolean ok; // 默认为false

    // 操作失败是,前端要展示的错误信息
    private String reason;

    // 保存要返回给前端的业务数据
    private Object data;
}

  • ⑥ When adding, deleting, checking and modifying the database, you also need to create a specific operation class folder, and create a specific operation method under this folder. When logging in here, we need to check whether there is such a user in the database. Therefore, first create a dao class, and create a UserDAO class under this class folder, specifically to use specific operation classes for operations on the user table. Specific comments are also reflected in the code
package example.dao;

import example.exception.AppException;
import example.model.User;
import example.util.Util;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Timestamp;
import java.util.Date;

public class UserDAO {


    /**
     * 根据用户名查询数据表中的用户
     * */
    public static User queryByName(String name) {
        // 先定义我们肯定要用的对象
        Connection connection = null; // 用于连接数据库
        PreparedStatement preparedStatement = null; // 用户sql注入,就是在写sql语句是可以使用占位符
        ResultSet resultSet = null; // 查询的结果集

        // 先定义返回数据,根据技术文档要返回给前端的是一个用户类型的对象
        User user = null;

        try{
            // 1、获取数据库连接
            connection = Util.getConnection(); // 调用先前在工具类中写的连接数据库地方法

            // 2、通过Connection + sql 创建操作命令对象Statement
            String sql = "select * from user where name=?";
            preparedStatement = connection.prepareStatement(sql);

            // 3、执行sql:执行前替换占位符
            preparedStatement.setString(1, name);
            resultSet = preparedStatement.executeQuery(); // 存放查询后的结果集

            // 4、如果是查询操作,处理结果集
            while(resultSet.next()){
                user = new User();
                // 设置结果集字段到用户对象的属性中
                user.setUserId(resultSet.getInt("userId")); // 注意这个“userId”要和表中的属性名相同
                user.setName(name);
                user.setPassword(resultSet.getString("password"));
                user.setNickName(resultSet.getString("nickName"));
                user.setIconPath(resultSet.getString("iconPath"));
                user.setSignature(resultSet.getString("signature"));
                java.sql.Timestamp lastLogout = resultSet.getTimestamp("lastLogout"); // 得到从1970年到今的毫秒数
                user.setLastLogout(new Date(lastLogout.getTime())); // 根据毫秒数拿到年月日时分秒
            }
            return user;
        }catch (Exception e){
            throw new AppException("查询用户账号出错", e);
        }finally {
            // 5、无论如何都要释放资源
            Util.close(connection, preparedStatement, resultSet);
        }
    }

    public static int updateLastLogout(Integer userId) {
        Connection c = null;
        PreparedStatement ps = null;
        try{
            c = Util.getConnection();
            String sql = "update user set lastLogout=? where userId=?";
            ps = c.prepareStatement(sql);
            ps.setTimestamp(1, new Timestamp(System.currentTimeMillis()));
            ps.setInt(2, userId);
            return ps.executeUpdate();
        }catch (Exception e){
            throw new AppException("修改用户上次登录时间出错", e);
        }finally {
            Util.close(c, ps);
        }
    }
}

⑦ After the login function is implemented, the interface for detecting the login status is implemented

⑧ After implementing this part of the code, you can try to start the project, you can find that you can log in now, and refresh the page after logging in, the login status will still be maintained. At this time, run and press F12 and you can see that there are two errors. One is that the channel list cannot be obtained, and the other is that the WebSocket function of sending and receiving messages cannot be realized. In the next step, we first realize the acquisition of the channel list.

(3) Realize the logout function (exit function)

According to the description of the request and response of the logout function in the development document, the description is as follows:

请求:
GET /logout
响应:
HTTP/1.1 200 OK
{
   ok: true,
   reason: xxx
}

Create a LogoutServlet class

  • ① Similarly, first inherit HttpServle, and then write the routing address of @WebServlet: @WebServlet("/logout")
  • ② Generate doGet() and doPost() methods, here only doGet() method is needed, doPost() can also be used to call doGet() The
    main implementation logic is: get user information from the session saved by the browser, if you get When it arrives, delete the user information, if it is not obtained, return the "User not logged in" message. The
    specific process is written in the code, ------------Paste the LogoutServlet.java code here---- -----------
package example.servlet;

import example.model.Response;
import example.model.User;
import example.util.Util;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;

@WebServlet("/logout")
public class LogoutServlet extends HttpServlet {

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 惯例开头先设置格式
        req.setCharacterEncoding("UTF-8");
        resp.setCharacterEncoding("UTF-8");
        resp.setContentType("application/json");

        // 从浏览器中获取Session信息
        HttpSession session = req.getSession(false); // false 表示,如果没有获取到Session 则不创建新的
                                                    // 默认为true 表示,没有获取到就创建新的Session
        if(session != null) {
            // 如果session不为空
            // 根据session中的信息,user是我们在登陆的时候设置到session中的字段,根据它中的信息,以User为模板创建user对象
            User user = (User) session.getAttribute("user");
            if (user != null) {
                // 用户已登陆,要实现注销,就要删除session中保存的用户信息
                session.removeAttribute("user");
                // 注销成功,根据开发手册,返回OK为true
                Response r = new Response();
                r.setOk(true);
                // 返回相应数据:从响应对象中获取输出流(序列化为JSON字符串),打印输出到响应体body
                resp.getWriter().println(Util.serialize(r));
                return;
            }
        }

        // 用户未登录
        Response r = new Response();
        r.setReason("用户未登录,不允许访问");
        resp.getWriter().println(Util.serialize(r));
    }
}


(3) Realize channel query

This function can only be used after the user logs in, and the queried channel information is displayed on the page after the user logs in.
According to the description of the request and response of the channel query in the development document, the description is as follows:

请求:
GET /channel
响应:
HTTP/1.1 200 OK
[
{
       channelId: 1,
       channelName: xxx
  },
  {
       channelId: 2,
       channelName: xxx
  }
]

We have not yet created a template for channel information, so first create a Channel class under the model to record basic channel-related information
—here, paste the Channel.java code—
create a ChannelServlet class:

  • ① Inherit HttpServlet, write WebServlet address: @WebServlet("/channel")
  • ② Generate doGet() and doPost() methods, here only doGet() method is needed, doPost() can also be used to call doGet(). The
    main implementation logic is: the old three
    (1) set the request response format (2) implementation Business logic (3) return corresponding data.
    In the second part, we need to create a specific method for querying channel data table information, that is, create the ChannelDAO class under the dao file, and implement the query method query() to be used here in this class
    --- -Paste ChannelDAO.java and ChannelServlet.java here—
package example.dao;

import example.exception.AppException;
import example.model.Channel;
import example.model.Response;
import example.util.Util;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;

public class ChannelDAO {
    public static List<Channel> query() {
        // 定义查询数据库要用的对象
        Connection c = null;
        PreparedStatement ps = null;
        ResultSet rs = null;

        // 定义存放最终放回的数据的列表
        List<Channel> list = new ArrayList<>(); // 返回的对象都是频道对象
        try {
            // 1、获取数据库连接Connection
            c = Util.getConnection();

            // 2、通过Connection + sql 操作命令对象Statement
            String sql = "select * from channel";
            ps = c.prepareStatement(sql);

            // 3、执行sql:执行前 替换占位符
            rs = ps.executeQuery();

            // 4、如果是查询操作,需要处理结果集
            while(rs.next()){ // 移动到下一行,有数据返回true
                Channel channel = new Channel(); // 在返回的结果List中是一个个的Channel对象
                // 设置属性
                channel.setChannelId(rs.getInt("channelId")); // 要与数据表中的属性对应
                channel.setChannelName(rs.getString("channelName"));
                list.add(channel); // 将频道信息添加进list中
            }
            return list;

        }catch (Exception e){
            throw new AppException("查询频道出错", e); //我们自定义的异常输出方法
        }finally {
            // 5、释放资源
            Util.close(c,ps,rs);
        }

    }
}
package example.servlet;

import example.dao.ChannelDAO;
import example.model.Channel;
import example.model.Response;
import example.util.Util;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;

@WebServlet("/channel")
public class ChannelServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 1、设置请求响应格式
        req.setCharacterEncoding("UTF-8");
        resp.setCharacterEncoding("UTF-8");
        resp.setContentType("application/json");

        // 2、实现业务逻辑
        // 目标是从数据库中查询到频道信息,将频道信息返回给前端
        Response response = new Response(); // 将内容以我们定义的响应格式传输
        try {
            // 查询所有频道,以列表的形式返回
            List<Channel> List = ChannelDAO.query();
            response.setOk(true); // 查询成功,设置输出给前端的ok为true
            response.setData(List); // 将查询到的频道信息列表返回给前端
        }catch (Exception e){
            e.printStackTrace();
            response.setReason(e.getMessage()); // 查询失败,返回错误信息
        }
        // 3、返回相应数据:从响应对象中获取输出流(序列化为JSON字符串),打印输出到响应体body
        resp.getWriter().println(Util.serialize(response));


    }
}

(4) Use WebSocket to send and receive messages

It should be noted that the session in the websocket is different from the session in the Http protocol

    建立连接    

    请求:
    ws://[ip]:[port]/message/{userId}
    只要登陆成功就会出发建立连接操作,发送/接收消息格式如下:

    {
   "userId": 1,
   "nickName": "蔡徐坤",
   "channelId": 1,
   "content": "这是消息正文"
    }

Here we need to use a new thing, WebSocket, simply take a look at the concept:
WebSocket is a protocol for full-duplex (both the client and the server can accept and send) communication on a single TCP connection. In the API, the client and server only need to complete a handshake, and a persistent link can be created between the two, and two-way data transmission can be carried out.

Next, we carry out the code part, write and analyze in the code.
First, create the MessageWebsocket class

  • ① The difference from the original is that instead of @WebSocket getting the route, @ServerEndPoint() is used to establish a connection, @ServerEndpoint("/message/{userId}") is to get the userId according to the URL address
  • ② In implementing the WebSocket function, we need to rewrite some of its own methods. The methods that
    need to be rewritten are as follows:
    @OnOpen // Successfully establish a connection
    @OnClose // Close the connection
    @OnMessage // Receive a message
    @OnError // Connection error
  • ③ Before rewriting, you need to create a message template class MessageCenter class to save all client sessions.
    In this class, we implement some basic methods about sending messages in chat rooms:
  • This class is called MessageCenter, as the name suggests, it is the place where messages are stored. Messages sent by the client must first arrive at the server, and then forwarded to another client through the server.
  • The addMessage() method is to store the message received from the client in a blocking queue on the server, and send it via another thread
  • The addOnLinUser() method is to save the user's id and client session information after the WebSocket is established, and save it in ConcurrentHashMap (a map structure that supports thread safety, and meets high concurrency (read and write, read and read concurrency, Write and write mutual exclusion))
  • The delOnlinUser() method is to delete the saved client session information when the websocket link is closed or the program error occurs
  • The sendMessage() method, when a message sent by each client is received, the message is forwarded to all clients
package example.model;

import javax.websocket.Session;
import java.io.IOException;
import java.util.Enumeration;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingDeque;

public class MessageCenter {

    /**
     *  ConcurrentHashMap:支持线程安全的map结构,并且满足高并发(读写,读读并发,写写互斥)
     * **/
    private static final ConcurrentHashMap<Integer, Session> clients = new ConcurrentHashMap<>();

    /**
     *  阻塞队列,用来存放消息,接受客户端的消息放进队列;
     *
     *  再启动一个线程,不停的拉去队列中的消息,发送
     * **/
    private static BlockingDeque<String> queue = new LinkedBlockingDeque<>();

    // 定义类
    private static MessageCenter center;

    // 构造方法
    private MessageCenter(){}
    
    /**
     * 不直接发送消息,先将消息存放在队列中,由另一个线程去发送消息
     * **/
    public void addMessage(String message){
        queue.add(message);
    }

    /**
     *  WebSocket建立连接时,添加用户id和客户端session,并保存起来
     * **/
    public static void  addOnLinUser(Integer userId, Session session){
        clients.put(userId, session);
    }

    /**
     *  关闭websocket连接、或出错时,删除客户端的session
     * **/
    public static void delOnlinUser(Integer userId){
        clients.remove(userId);
    }

    /**
     * 接收到某用户的消息时,转发到所有客户端:
     * **/
    public static void sendMessage(String message){
        try{
            Enumeration<Session> e = clients.elements();
            while(e.hasMoreElements()){ //  遍历每个用户的session
                Session session = e.nextElement();
                session.getBasicRemote().sendText(message);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }



}

  • ④ If we want to save the message sent from the client to the server, we need a message template to save various information about this message.
    Create a new Message class under the model, which contains: the id of this message , The id of the user who sent the message, the id of the channel where the message was sent, the content of the message, the time the message was sent, the nickname of the user who sent the message
package example.model;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Getter
@Setter
@ToString
public class Message {
    private Integer messageId;
    private Integer userId;
    private Integer channelId;
    private String content;
    private java.util.Date sendTime;

    //接收客户端发送的消息,转发到所有客户端的消息,需要昵称
    private String nickName;
}

  • ⑤ When the server receives a message from the client Farley, it stores the message in the message database, so we have to write a specific method class for operating the message database.
    First create a class named MessageDAO, the specific operation method, Write when you need it.

为了不放排版看起来特别乱,我这里将本来是写在下面的部分代码,拿到这这里

package example.dao;
import example.model.Message;
import example.exception.AppException;
import example.util.Util;


import java.sql.*;
import java.util.ArrayList;
import java.util.List;

public class MessageDAO {

    // 根据用户id查询该用户最后一次下线之后,服务器数据库中接收到的消息(当前用户应该接受到的消息)
    public static List<Message> queryByLastLogout(Integer userId) {
        Connection c = null;
        PreparedStatement ps = null;
        ResultSet rs = null;

        // 定义返回数据
        List<Message> list = new ArrayList<>(); // 返回的是一个消息列表,包含了用户下线这个时间段内所有的消息
        try{
            // 1、获取数据库链接
            c = Util.getConnection();

            // 2、通过Connection + sql 创建操作命令对象 Statement
            String sql = "select m.*,u.nickName from message m join user u on u.userId=m.userId where m.sendTime>(select lastLogout from user where userId=?)";
            // 该sql语句的意思为:以message表中发送时间大于use表中该用户下线时间为条件,联合用户表和消息表查询message表的所有信息与user表中的用户昵称
            ps = c.prepareStatement(sql);

            // 3、执行sql,执行前替换占位符
            ps.setInt(1,userId);
            rs = ps.executeQuery();

            // 4、对于查询操作,需要处理结果及
            while(rs.next()){ // 看下一行是否有数据,有数据则为true,进入循环
                // 获取结果集字段,设置所需要的对象属性
               Message m = new Message();
               m.setUserId(userId);
               m.setNickName(rs.getString("nickName"));
               m.setContent(rs.getString("content"));
               m.setChannelId(rs.getInt("channelId"));
               list.add(m);
            }
            return list;


        } catch (Exception e) {
            throw new AppException("查询用户[" + userId + "]的消息出错");
        } finally {
            Util.close(c, ps, rs);
        }
    }

    public static int insert(Message msg) {
        Connection c = null;
        PreparedStatement ps = null;
        try{
            c = Util.getConnection();
            String sql = "insert into message values(null, ?, ?, ?, ?)";
            ps = c.prepareStatement(sql);
            ps.setInt(1,msg.getUserId());
            ps.setInt(2,msg.getChannelId());
            ps.setString(3, msg.getContent());
            ps.setTimestamp(4,new Timestamp(System.currentTimeMillis()));
            return ps.executeUpdate();
        }catch (Exception e){
            throw new AppException("保存消息出错", e);
        }finally {
            Util.close(c, ps);
        }
    }
}


⑥ Next, we start to rewrite the method in websocket
(1) Rewrite OnOpen —— establish a connection (establishing a connection means logging in, just imagine, when we use WeChat QQ, after logging in again, we are not logged in for this period of time , All the messages sent to us by others are displayed, so when we establish a connection, we must also judge at the same time what messages are sent to the user during this time period when the user is offline)

  • 1. Save the session of each client. With this session information, the server can forward subsequent messages to these clients
  • 2. Query the messages sent by other users in a certain channel after this user (this client) last logged in (these messages are stored in the database, and these messages are obtained according to the timestamp).
    Here, they must be in the MessageDAO class Next write a method: queryByLastLogout(userid), query the message received by the database after the last login (query according to user id)
    ------Paste the queryByLastLogout method code in MessageDAO here-------
  • 3. Send these messages to the current user

(2) Rewrite OnMessage -forward the message sent by user A to other users and the server saves the message sent by user A

  • 1. Traverse all sessions and send messages to each session
  • 2, this news server deserialize objects Message type, the message into the server database,
    where a method to write at MessageDAO categories: insert (msg), the message will be inserted into the database
    - -----Paste the insert method code in MessageDAO here-------

(3) Override OnClose -close the connection and disconnect a certain client

  • 1. The client is disconnected, and the session information of the client saved in MessageCenter needs to be deleted
    . The delOnlineUser method written in MessageCenter is used here
  • 2. Next time the user establishes a connection, he needs to receive a message sent to the client by other clients during the time that the user is offline, so the time of the user's last offline time must be updated
    here in MessageDAO Write a method under the class: updateLastLogout(userId) to update the last online time of the disconnected user

(4) Rewrite OnError —— When an error occurs, the connection is also closed, and the writing method is the same as closing the connection

package example.servlet;

import example.dao.MessageDAO;
import example.dao.UserDAO;
import example.model.Message;
import example.model.MessageCenter;
import example.util.Util;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.List;

@ServerEndpoint("/message/{userId}")
public class MessageWebsocket {

    // 建立连接
    @OnOpen
    public void onOpen(@PathParam("userId") Integer userId,
                       Session session) throws IOException {
        // 1、把每个客户端的session都保存起来,之后将消息转发到所有的客户端时要使用
        MessageCenter.addOnLinUser(userId, session);
        // 2、查询本客户端(用户)在上次登录之后,别人发送到服务器的消息(在数据库查)
        List<Message> list = MessageDAO.queryByLastLogout(userId);
        // 3、将这些查询到的消息发送给当前用户
        for (Message m: list) {
            // 将消息序列化为JSON字符串后发送
            session.getBasicRemote().sendText(Util.serialize(m));
        }
        System.out.println("建立连接" + userId);

    }

    // 服务器转发消息,并将消息存储在数据库中
    @OnMessage
    public void onMessage(Session session, String message){

        // 1、遍历所保存的所有session信息,对每个都发送消息
        MessageCenter.sendMessage(message);
        // 2、将消息保存在数据库中
        // (1) 反序列化json字符串为message对象
        Message msg = Util.deserialize(message, Message.class);
        // (2)插入数据库
        int n = MessageDAO.insert(msg);

        // 服务器显示一下接收到的消息
        System.out.printf("接收到消息:%s\n", message);


    }

    // 断开连接
    @OnClose
    public void onClose(@PathParam("userId") Integer userId){
        // 1、该客户端断开连接,要将在MessageCenter中保存的该客户端的session信息删除
        MessageCenter.delOnlinUser(userId);

        // 2、下次该用户如果建立连接时,需要收到在该用户下线的这段时间有其他客户端发送给该客户端的消息,
        // 所以要更新该用户最后下线时刻的时间
        int n = UserDAO.updateLastLogout(userId);
        System.out.println("关闭连接");
    }

    @OnError
    public void onError(@PathParam("userId") Integer userId, Throwable t){
        System.out.println("出错了");
        MessageCenter.delOnlinUser(userId);
        t.printStackTrace();
        //和关闭连接的操作一样
    }
}

In this way, the function of the chat room is realized. The entire project project is here:
https://download.csdn.net/download/MercuryG/19356248?spm=1001.2014.3001.5501
Link: https://pan.baidu.com/s/1P1hza2CXZiMlFxwp5pagYQ
Extraction code: 51i7