基于Java的WebSocket聊天室项目完整开发流程

开发环境与技术栈

  • Windows/Mac/Linux
  • WebSocket
  • Maven
  • Servlet
  • MySQL
  • Gson

项目功能

主要业务:实现多人群聊功能,记录并管理所有聊天消息。

  • 用户注册(打开主页,可以看到注册按钮);
  • 用户登录(打开主页,可以看到登录界面);
  • 登录成功后可以进入主界面,显示当前所有的聊天频道列表;
  • 点击某个频道后,可以看到该频道的消息;
  • 每个用户都可以在频道发信息,并且该频道的所有人都能接收到他的消息;
  • 历史消息功能。用户登陆成功之后,可以看到从上次自己下线到这次上线这段时间中错过的历史消息。

核心技术要点:利用websocket,实现消息推送机制。

项目演示

基于Java的WebSocket聊天室项目功能演示

开发准备

1.IDEA编码配置:
在这里插入图片描述

2.自动编码设置:
在这里插入图片描述

3.配置好pom.xml文件,指定打包方式,引入相关依赖等等;

//比如引入websocket的依赖
<dependency>
    <groupId>javax.websocket</groupId>
    <artifactId>javax.websocket-api</artifactId>
    <version>1.1</version>
    <scope>provided</scope>
 </dependency>

4.生成项目的web.xml文件:
在这里插入图片描述

5.在IDEA中设置好tomcat部署项目自动发布 功能:
在这里插入图片描述
6.准备好前端资源

数据库设计

1.数据库关系说明

根据需求可以确定,项目大致涉及三个实体,分别是用户User、频道Channel和消息Message。

  • 首先是用户和频道,按理说应该是多对多的关系。但是为了代码实现方便,默认让所有用户关注了所有频道。所以,用户和频道可以简化地看作为没关系;
  • 其实是用户和消息,为了实现历史消息功能,一个用户可能有多条未获取的消息,所以用户和消息是一对多的关系;
  • 最后是频道和消息,按理说应该是一对多的关系,但是这个过程我准备直接拿websocket代码处理。不需要从数据中拿,所以此处也可以看作是没关系。

2.创建数据库及数据库表

create database chatroom;

use chatroom;

-- 创建用户表
drop table if exists user;
create table user (
    userId int primary key auto_increment,   -- 用户id
    name varchar(50) unique,   -- 用户名
    password varchar(50),      -- 用户密码
    nickName varchar(50),      -- 昵称(聊天时显示的名字)
    lastLogout datetime  -- lastLogout 表示上次退出的时间. 用来实现历史记录功能.
);

-- 创建频道表
drop table if exists channel;
create table channel (
    channelId int primary key auto_increment,
    channelName varchar(50) -- 频道名
);

-- 创建消息表
drop table if exists message;
create table message (
    messageId int primary key auto_increment,
    userId int, -- 谁发送的消息.
    channelId int,  -- 频道id
    content text,  -- 消息正文
    sendTime datetime -- 消息的发送时间.
);

前后端接口约定

要实现功能,需要先明确前后端需要约定好的接口。接口的定义一般是前后端约定好的,所以也和前端代码息息相关,前端需要什么数据,后端需要什么数据,数据的格式等等,也都会在接口中体现。

1.用户注册

(1)请求

POST   /register    //请求方法和路径

{
	name:xxx,
	password:xxx,
	nickName:xxx
}

(2)响应

{
	ok:1,  //约定1表示成功,0表示失败
	reason:xxx   //成功返回"",失败返回一个具体的原因
}

2.用户登录

(1)请求

POST /login

{
	name:xxx,
	password:xxx
}

(2)响应

{
	ok:1,
	reason:xxx,
	userId:xxx,
	name:xxx,
	nickName:xxx    //后三条主要是为了前端页面实现方便
}

3.检测登录状态

(1)请求

GET  /login

(2)响应

{
	ok:1, //ok为1表示已登录,ok为2表示未登录
	reason:xxx,
	userId:xxx;
	name:xxx,
	nickName:xxx
}

4.新增频道

(1)请求

POST  /channel

{
	channelName:xxx
}

(2)响应

{
	ok:1,
	reason:xxx
}

5.获取频道列表

(1)请求

GET  /channel

(2)响应

[
	{
	channelId:xxx,
	channelName:xxx
},
{
	channelId:xxx,
	channelName:xxx
}
]

6.删除频道

(1)请求

DELETE /channel?channelId=?

(2)响应

{
	ok:1,
	reason:xxx
}

7.建立websocket连接

此时就不是http协议了,请求响应格式也跟上面不太一样。

//每个登录中的用户,都需要有自己的连接,连接之间用各自唯一标识userId来区分
ws://[ip]:[port]/message/{userId}

8.发送/接收消息

为了方便,规定发送和接收的格式都为JSON格式。

当前这个message是通过WebSocket来传输,而不是走HTTP协议了。

{
	userId:xxx,   //消息是谁发送的
	nickName:xxx, //消息发送者的昵称
	channelId:xxx, //消息是往哪个频道发送的
	content:xxx    //消息内容
}

有了上述的约定,后面不管是后端代码,还是前端代码都需要严格遵守上述约定。否则,就无法完成交互。

代码设计

1.设计数据库实体类

(1)User实体类

package model;

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

import java.sql.Timestamp;

//一个 User 对象就用来表示一条数据库的记录.
//对象的属性基本和数据库的表结构一致. (大部分是一致的, 可能有细节上的差异)
@Getter
@Setter
@ToString
public class User {
    private int userId;
    private String name;
    private String password;
    private String nickName;
    private Timestamp lastLogout;
}

(2)Channel实体类

package model;

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

@Getter
@Setter
@ToString
public class Channel {
    private int channelId;
    private String channelName;
}

(3)Message实体类

package model;

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

import java.sql.Timestamp;

@Getter
@Setter
@ToString
public class Message {
    private int messageId;
    private int userId;
    private int channelId;
    private String content;
    private Timestamp sendTime;

    // nickName 在 message 表中并不存在. 但可以根据 userId 在 user 表中查到
    // 直接把用户的昵称放到这里, 方便后面的界面显示.
    private String nickName;
}

2.设计工具类

(1)DBUtil工具类。(对JDBC操作进行简单的封装)

package util;

import com.mysql.jdbc.jdbc2.optional.MysqlDataSource;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

// 功能: 帮助我们管理连接. DBUtil 本质上是实现了 DataSource 类的单例版本.
// 对于一个应用程序来说, DataSource 只需要有一个实例就可以了.
public class DBUtil {
    private static final String URL = "jdbc:mysql://127.0.0.1:3306/chatroom?character=utf-8&useSSL=true";
    private static final String USERNAME = "用户";
    private static final String PASSWORD = "数据库密码";

    private static volatile DataSource dataSource = null;

    public static DataSource getDataSource() {
        // 获取到这个单例的 DataSource 实例.
        if (dataSource == null) {
            synchronized (DBUtil.class) {
                if (dataSource == null) {
                    dataSource = new MysqlDataSource();
                    // 必须要告诉代码, 数据库是谁, 以啥样的方式登陆上去.
                    ((MysqlDataSource)dataSource).setURL(URL);
                    ((MysqlDataSource)dataSource).setUser(USERNAME);
                    ((MysqlDataSource)dataSource).setPassword(PASSWORD);
                }
            }
        }
        return dataSource;
    }

    //获取连接
    public static Connection getConnection () {
        try {
            return getDataSource().getConnection();
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return null;
    }

    //释放连接
    public static void close(Connection connection, PreparedStatement statement, ResultSet resultSet) {
        try {
            if (resultSet != null) {
                resultSet.close();
            }
            if (statement != null) {
                statement.close();
            }
            if (connection != null) {
                connection.close();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

(2)自定义异常类。(抛出一些自定义异常)

package util;

public class ChatroomException extends RuntimeException{
    public ChatroomException(String message) {
        super(message);
    }
}

(3)JSONUtil类。(读取body中JSON格式数据,并转为字符串)

package util;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;

public class JSONUtil {
    public static String readBody(HttpServletRequest req) throws UnsupportedEncodingException {
        //ContentLength()是请求body的长度。单位是字节。
        int contentLength = req.getContentLength();
        //buffer来保存读到的body内容.
        byte[] buffer = new byte[contentLength];
        try (InputStream inputStream = req.getInputStream()) {
            inputStream.read(buffer, 0, contentLength);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return new String(buffer,0,contentLength,"UTF-8");
    }
}

3.设计dao层

dao层是数据访问层,这里设计几个dao类,来封装对数据库表进行增删查改等基础操作的功能。

(1)UserDao。(封装针对user表的基本操作)

package dao;

import model.User;
import util.ChatroomException;
import util.DBUtil;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

// 通过UserDao这个类来完成针对User表的基本操作。
public class UserDao {
    // 方法一:新增一个用户(具体就是用户注册功能)
    public void add(User user) {
        // 1. 获取数据库连接.
        Connection connection = DBUtil.getConnection();
        // 2. 拼装 SQL 语句
        String sql = "insert into user values(null, ?, ?, ?, now())";
        PreparedStatement statement = null;
        try {
            statement = connection.prepareStatement(sql);
            statement.setString(1, user.getName());
            statement.setString(2, user.getPassword());
            statement.setString(3, user.getNickName());
            // 3. 执行 SQL 语句
            int ret = statement.executeUpdate();
            if (ret != 1) {
                //抛出一个自定义的异常.
                throw new ChatroomException("插入用户失败");
            }
            System.out.println("插入用户成功");
        } catch (SQLException e) {
            e.printStackTrace();
            throw new ChatroomException("插入用户失败");
        } finally {
            // 4. 释放连接.
            DBUtil.close(connection, statement, null);
        }
    }

    //方法二:按用户名查找用户信息(具体就是登陆功能)
    public User selectByName(String name) {
        // 1. 获取到连接
        Connection connection = DBUtil.getConnection();
        // 2. 拼装 SQL
        String sql = "select * from user where name = ?";
        PreparedStatement statement = null;
        ResultSet resultSet = null;
        try {
            statement = connection.prepareStatement(sql);
            statement.setString(1, name);
            // 3. 执行 SQL
            resultSet = statement.executeQuery();
            // 4. 遍历结果集合。
            //    由于查找结果预期最多只有一条记录, 所以这里直接使用 if 判定
            if (resultSet.next()) {
                User user = new User();
                user.setUserId(resultSet.getInt("userId"));
                user.setName(resultSet.getString("name"));
                user.setPassword(resultSet.getString("password"));
                user.setNickName(resultSet.getString("nickName"));
                user.setLastLogout(resultSet.getTimestamp("lastLogout"));
                return user;
            }
        } catch (SQLException e) {
            e.printStackTrace();
            throw new ChatroomException("按用户名查找用户失败");
        } finally {
            // 5. 释放连接.
            DBUtil.close(connection, statement, resultSet);
        }
        return null;
    }

    //方法三:按用户id查找用户信息(具体就是把userId转换成昵称的时候)
    public User selectById(int userId) {
        // 1. 获取连接
        Connection connection = DBUtil.getConnection();
        // 2. 拼装 SQL
        String sql = "select * from user where userId = ?";
        PreparedStatement statement = null;
        ResultSet resultSet = null;
        try {
            statement = connection.prepareStatement(sql);
            statement.setInt(1, userId);
            // 3. 执行 SQL
            resultSet = statement.executeQuery();
            // 4. 遍历结果集
            if (resultSet.next()) {
                User user = new User();
                user.setUserId(resultSet.getInt("userId"));
                user.setName(resultSet.getString("name"));
                user.setPassword(resultSet.getString("password"));
                user.setNickName(resultSet.getString("nickName"));
                user.setLastLogout(resultSet.getTimestamp("lastLogout"));
                return user;
            }
        } catch (SQLException e) {
            e.printStackTrace();
            throw new ChatroomException("按 id 查找用户失败");
        } finally {
            // 5. 释放连接.
            DBUtil.close(connection, statement, resultSet);
        }
        return null;
    }

    //方法四:更新用户的 lastLogout 时间. (用户下线时更新,具体就是 为了实现历史记录功能)
    public void updateLogout(int userId) {
        // 哪个用户下线了, 就更新哪个.
        // 1. 获取连接.
        Connection connection = DBUtil.getConnection();
        // 2. 拼装 SQL
        String sql = "update user set lastLogout = now() where userId = ?";
        PreparedStatement statement = null;
        try {
            statement = connection.prepareStatement(sql);
            statement.setInt(1, userId);
            // 3. 执行 SQL
            int ret = statement.executeUpdate();
            if (ret != 1) {
                throw new ChatroomException("更新退出时间失败");
            }
            System.out.println("更新退出时间成功");
        } catch (SQLException e) {
            e.printStackTrace();
            throw new ChatroomException("更新退出时间失败");
        } finally {
            // 4. 释放连接.
            DBUtil.close(connection, statement, null);
        }
    }

    //写完后,可以用这里的代码对上面的功能简单的测试一下
    public static void main(String[] args) {
        UserDao userDao = new UserDao();
        // 针对上面的代码进行简单验证.
        // 1.验证add方法
        /*User user = new User();
        user.setName("akl");
        user.setPassword("123");
        user.setNickName("阿卡丽");
        userDao.add(user);*/
        // 2. 按名字查找用户信息
        /*User user = userDao.selectByName("akl");
        System.out.println(user);*/
        //3.按用户id查找用户信息
        /*User user = userDao.selectById(1);
        System.out.println(user);*/
        // 4. 更新用户的退出时间。
        //userDao.updateLogout(1);
    }    
}

(2)ChannelDao。(封装针对channel表的基本操作)

package dao;

import model.Channel;
import util.ChatroomException;
import util.DBUtil;

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


//对频道表进行基本的操作
public class ChannelDao {
    //方法一:新增频道
    public void add(Channel channel) {
        // 1. 获取数据库连接
        Connection connection = DBUtil.getConnection();
        // 2. 拼装 SQL
        String sql = "insert into channel values(null, ?)";
        PreparedStatement statement = null;
        try {
            statement = connection.prepareStatement(sql);
            statement.setString(1, channel.getChannelName());
            // 3. 执行 SQL
            int ret = statement.executeUpdate();
            if (ret != 1) {
                throw new ChatroomException("插入频道失败");
            }
            System.out.println("插入频道成功");
        } catch (SQLException e) {
            e.printStackTrace();
            throw new ChatroomException("插入频道失败");
        } finally {
            // 4. 断开连接
            DBUtil.close(connection, statement, null);
        }
    }

    //方法二:删除频道
    public void delete(int channelId) {
        // 1. 获取数据库连接
        Connection connection = DBUtil.getConnection();
        // 2. 拼装 SQL
        String sql = "delete from channel where channelId = ?";
        PreparedStatement statement = null;
        try {
            statement = connection.prepareStatement(sql);
            statement.setInt(1, channelId);
            // 3. 执行 SQL
            int ret = statement.executeUpdate();
            if (ret != 1) {
                throw new ChatroomException("删除频道失败");
            }
            System.out.println("删除频道成功");
        } catch (SQLException e) {
            e.printStackTrace();
            throw new ChatroomException("删除频道失败");
        } finally {
            // 4. 断开连接
            DBUtil.close(connection, statement, null);
        }
    }

    //方法三:查看频道列表
    public List<Channel> selectAll() {
        List<Channel> channels = new ArrayList<>();
        // 1. 建立连接
        Connection connection = DBUtil.getConnection();
        // 2. 拼装 SQL
        String sql = "select * from channel";
        PreparedStatement statement = null;
        ResultSet resultSet = null;
        try {
            statement = connection.prepareStatement(sql);
            // 3. 执行 SQL
            resultSet = statement.executeQuery();
            // 4. 遍历结果集合
            while (resultSet.next()) {
                Channel channel = new Channel();
                channel.setChannelId(resultSet.getInt("channelId"));
                channel.setChannelName(resultSet.getString("channelName"));
                channels.add(channel);
            }
            return channels;
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            DBUtil.close(connection, statement, resultSet);
        }
        return null;
    }

    // 针对这个类也可以进行简单的单元测试.
    public static void main(String[] args) {
        // 创建一个 ChannelDao 实例
        ChannelDao channelDao = new ChannelDao();
        // 1. 验证 add 操作
        /*Channel channel = new Channel();
        channel.setChannelName("午夜情感");
        channelDao.add(channel);*/
        // 2. 验证 selectAll 操作
        /*List<Channel> channels = channelDao.selectAll();
        System.out.println(channels);*/
        // 3. 验证 delete 操作
        channelDao.delete(4);
    }
}

(3)MessageDao。(封装针对message表的基本操作)

package dao;

import model.Message;
import util.ChatroomException;
import util.DBUtil;

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

public class MessageDao {
    // 方法一:新增消息
    public void add(Message message) {
        // 1. 获取数据库连接
        Connection connection = DBUtil.getConnection();
        // 2. 拼装 SQL
        String sql = "insert into message values (null, ?, ?, ?, ?)";
        PreparedStatement statement = null;
        try {
            statement = connection.prepareStatement(sql);
            statement.setInt(1, message.getUserId());
            statement.setInt(2, message.getChannelId());
            statement.setString(3, message.getContent());
            statement.setTimestamp(4, message.getSendTime());
            // 3. 开始执行 SQL
            int ret = statement.executeUpdate();
            if (ret != 1) {
                throw new ChatroomException("插入消息失败");
            }
            System.out.println("插入消息成功");
        } catch (SQLException e) {
            e.printStackTrace();
            throw new ChatroomException("插入消息失败");
        } finally {
            // 4. 释放连接
            DBUtil.close(connection, statement, null);
        }
    }
    
    // 方法二:按时间段查询消息
    public List<Message> selectByTimeStamp(Timestamp from, Timestamp to) {
        List<Message> messages = new ArrayList<>();
        // 1. 获取到连接
        Connection connection = DBUtil.getConnection();
        // 2. 拼装 SQL
        //    MySQL 中的 datetime 类型是可以直接比较大小的.
        String sql = "select * from message where sendTime >= ? and sendTime <= ?";
        PreparedStatement statement = null;
        ResultSet resultSet = null;
        try {
            statement = connection.prepareStatement(sql);
            statement.setTimestamp(1, from);
            statement.setTimestamp(2, to);
            System.out.println("selectByTimeStamp: " + statement);
            // 3. 执行 SQL
            resultSet = statement.executeQuery();
            // 4. 遍历结果集合
            while (resultSet.next()) {
                Message message = new Message();
                message.setMessageId(resultSet.getInt("messageId"));
                message.setUserId(resultSet.getInt("userId"));
                message.setChannelId(resultSet.getInt("channelId"));
                message.setContent(resultSet.getString("content"));
                message.setSendTime(resultSet.getTimestamp("sendTime"));
                messages.add(message);
            }
            return messages;
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            DBUtil.close(connection, statement, resultSet);
        }
        return null;
    }

	//简单的测试一下代码
    public static void main(String[] args) {
        MessageDao messageDao = new MessageDao();
        // 1. 测试新增消息
//        Message message = new Message();
//        message.setUserId(1);
//        message.setChannelId(1);
//        message.setContext("你好哇");
//        message.setSendTime(new Timestamp(System.currentTimeMillis()));
//        messageDao.add(message);

        // 2. 获取指定时间段的消息
        List<Message> messages = messageDao.selectByTimeStamp(
                new Timestamp(1595503500000L), new Timestamp(1595503800000L)
        );
        System.out.println(messages);
    }
}

4.设计API接口

(1)创建一个RegisterServlet类来实现注册功能。

实现思路大致可以分为以下几步:

  • a).读取 body 中的信息(一个JSON格式的字符串);
  • b).把 json 数据转成 Java 中的对象;
  • c).在数据库中查一下, 看看用户名是否已经存在了,如果存在了就认为注册失败;
  • d).把新的用户名和密码构造User对象并插入数据库;
  • e).返回一个注册成功的响应结果。
package api;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonSyntaxException;
import dao.UserDao;
import model.User;
import util.ChatroomException;
import util.JSONUtil;

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

public class RegisterServlet extends HttpServlet {
    private Gson gson = new GsonBuilder().create();

    //这个类以内部类的方式来组织。这个 Request 类只是针对 RegisterServlet来使用的.
    //其他的Servlet对应的Request类可能结构不同.
    //从body的JSON中转换过来的。
    static class Request {
        public String name;
        public String password;
        public String nickName;

        @Override
        public String toString() {
            return "Request{" +
                    "name='" + name + '\'' +
                    ", password='" + password + '\'' +
                    ", nickName='" + nickName + '\'' +
                    '}';
        }
    }

    // 响应的数据内容
    // 要把这个对象再转回JSON字符串, 并写回给客户端.
    static class Response {
        public int ok;
        public String reason;
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        req.setCharacterEncoding("utf-8");
        //响应对象,存放响应结果
        Response response = new Response();
        try {
            // 1. 读取 body 中的信息(一个JSON格式的字符串)
            String body = JSONUtil.readBody(req);
            System.out.println("body: " + body);
            
             //2.把json数据转成一个Java中的对象。
            //    创建一个 Request 类来表示这次请求的结构
            //    需要把 body 转成 Request 对象
            //    此处使用 GSON库来实现。   下面参数中body是json格式的字符串  第二个参数是类对象
            Request request = gson.fromJson(body, Request.class);  //把json字符串转成Java对象。
            
            // 3. 在数据库中查一下, 看看用户名是否已经存在了. 如果存在了就认为注册失败.
            UserDao userDao = new UserDao();
            User existsUser = userDao.selectByName(request.name);
            if (existsUser != null) {
                throw new ChatroomException("用户名已经存在");
            }
            
            // 4. 把新的用户名和密码构造 User 对象并插入数据库
            User user = new User();
            user.setName(request.name);
            user.setPassword(request.password);
            user.setNickName(request.nickName);
            userDao.add(user);
            
            // 5. 返回一个注册成功的响应结果.
            response.ok = 1;
            response.reason = "";
        } catch (ChatroomException | JsonSyntaxException e) {
            e.printStackTrace();
            response.ok = 0;
            response.reason = e.getMessage();
        } finally {
            resp.setContentType("application/json; charset=utf-8");
            String jsonString = gson.toJson(response); //将响应对象转换为JSON字符串
            resp.getWriter().write(jsonString);
        }
    }
}

(2)创建一个LoginServlet类来实现登陆和检测登陆状态的。

登录功能实现思路大致可以分为以下几步:

  • a).读取 body 中的数据;
  • b).把读到的数据转成 Request 对象;
  • c).按用户名在数据库中查找, 匹配密码是否正确;
  • d). 如果登陆失败, 则给出提示;
  • e).如果登陆成功, 创建一个 session 对象;
  • f).把结果写回到浏览器。

而检测登录状态的逻辑就会简单很多,就两步:

  • a).根据请求, 查看该 sessionId 对应的 session 是否存在。如果 session 不存在, 就是登陆是失败的状态;
  • b).如果 session 存在, 直接返回一个登陆成功即可。
package api;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonSyntaxException;
import dao.UserDao;
import model.User;
import util.ChatroomException;
import util.JSONUtil;

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

public class LoginServlet extends HttpServlet {
    private Gson gson = new GsonBuilder().create();

    static class Request {
        public String name;
        public String password;
    }

    static class Response {
        public int ok;
        public String reason;
        public int userId;
        public String name;
        public String nickName;
    }

    //登陆功能
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        req.setCharacterEncoding("utf-8");
        Response response = new Response();
        try {
            // 1. 读取 body 中的数据
            String body = JSONUtil.readBody(req);
            
            // 2. 把读到的数据转成 Request 对象.
            Request request = gson.fromJson(body, Request.class);
            
            // 3. 按用户名在数据库中查找, 匹配密码是否正确.
            UserDao userDao = new UserDao();
            User user = userDao.selectByName(request.name);
            
            // 4. 如果登陆失败, 则给出提示。失败分下面两种情况:
            //   a) 用户名没查到
            //   b) 密码不匹配
            if (user == null || !request.password.equals(user.getPassword())) {
                throw new ChatroomException("用户名或密码错误");
            }
            
            // 5. 如果登陆成功, 创建一个 session 对象.
            HttpSession httpSession = req.getSession(true);
            httpSession.setAttribute("user", user);
            
            // 6. 把结果写回到浏览器
            response.ok = 1;
            response.name = user.getName();
            response.userId = user.getUserId();
            response.nickName = user.getNickName();
            response.reason = "";
        } catch (JsonSyntaxException | ChatroomException e) {
            e.printStackTrace();
            response.ok = 0;
            response.reason = e.getMessage();
        } finally {
            resp.setContentType("application/json; charset=utf-8");
            String jsonString = gson.toJson(response);
            resp.getWriter().write(jsonString);
        }
    }

    // 检测登陆状态
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        Response response = new Response();
        try {
            // 1. 根据请求, 查看该 sessionId 对应的 session 是否存在。
            //如果 session 不存在, 就是登陆是失败的状态.
            HttpSession httpSession = req.getSession(false);
            if (httpSession == null) {
                // 不存在的情况
                throw new ChatroomException("当前未登陆");
            }
            User user = (User) httpSession.getAttribute("user");
            
            // 2. 如果 session 存在, 直接返回一个登陆成功即可.
            response.ok = 1;
            response.userId = user.getUserId();
            response.name = user.getName();
            response.nickName = user.getNickName();
            response.reason = "";
        } catch (ChatroomException e) {
            e.printStackTrace();
            response.ok = 0;
            response.reason = e.getMessage();
        } finally {
            resp.setContentType("application/json; charset=utf-8");
            String jsonString = gson.toJson(response);
            resp.getWriter().write(jsonString);
        }
    }
}

(3)小测试
写完代码不测等于赌博,上面三个功能写完之后,就可以拿Postman先测一下了。先在IDEA中运行tomcat,然后再去Postman中发送http请求:

a)注册功能

在这里插入图片描述
可以看到在Postman中发送http请求成功之后,user表中也出现了注册用户。所以,注册功能是没问题的。
在这里插入图片描述

b)登陆功能
在这里插入图片描述
可以看到在发送http请求成功后,在响应中成功返回了用户的昵称,ID等相关信息,所以登陆功能也是没问题的。

c)登陆状态检测功能
在这里插入图片描述

可以看到,在给服务器发送一个GET请求之后,成功检测出登陆状态,而且cookie中的sessionId,和上面登陆成功后分配的sessionId是一样的。

(4)创建一个ChannelServlet类来实现新增,删除和获取频道列表的功能

新增频道的操作很简单,实现时可以分为以下几步:

  • a)验证登录状态;
  • b)增加频道;
  • c)返回一个注册成功的响应结果。

获取频道列表,也分为三步:
a)验证登陆状态. 如果未登陆则不能查看;
b)查数据库;
c)把查询结果包装成响应内容。

package api;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import dao.ChannelDao;
import model.Channel;
import model.User;
import util.ChatroomException;
import util.JSONUtil;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

// 这个代码是处理频道操作的 API
public class ChannelServlet extends HttpServlet {
    private Gson gson = new GsonBuilder().create();

    static class Response {
        public int ok;
        public String reason;
    }

    // 新增频道
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        Response response = new Response();
        try {
            //1.验证登录状态
            HttpSession httpSession = req.getSession(false);
            if(httpSession == null) {
                throw  new ChatroomException("您尚未登录");
            }
            //2.增加频道
            ChannelDao channelDao = new ChannelDao();
            String body = JSONUtil.readBody(req);
            Channel channel = gson.fromJson(body,Channel.class);
            if("".equals(channel.getChannelName())) {
                throw new ChatroomException("频道名不能为空");
            }
            channelDao.add(channel);
            //3.返回一个注册成功的响应结果
            response.ok = 1;
            response.reason = "";
        } catch (ChatroomException e) {
            e.printStackTrace();
        }finally {
            resp.setContentType("application/json; charset=utf-8");
            String jsonString = gson.toJson(response);
            resp.getWriter().write(jsonString);
        }
    }

    // 获取频道列表
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        List<Channel> channels = new ArrayList<>();
        try {
            // 1. 验证登陆状态. 如果未登陆则不能查看.
            HttpSession httpSession = req.getSession(false);
            if (httpSession == null) {
                throw new ChatroomException("您尚未登陆");
            }
            User user = (User) httpSession.getAttribute("user");
            // 2. 查数据库
            ChannelDao channelDao = new ChannelDao();
            channels = channelDao.selectAll();
        } catch (ChatroomException e) {
            e.printStackTrace();
            // 如果前面触发了异常, 此时 channels 将是一个空的 List
            // 下面的 finally 中将会构造出一个空数组的 json
        } finally {
            // 3. 把查询结果包装成响应内容
            //    如果参数是个 List, 转出的 json 字符串就是一个 数组
            resp.setContentType("application/json; charset=utf-8");
            String jsonString = gson.toJson(channels);
            resp.getWriter().write(jsonString);
        }
    }
}

(5)创建一个MessageCenter类来实现管理所有用户消息和在线用户列表的功能

这个类是实现这个聊天室项目的关键代码,它主要实现了两个功能。一个是管理在线用户,包括用户上线,用户下线;另一个功能就是管理所有消息并实现消息转发。

这个类中包含了两个重要的数据结构:
a)保存消息的阻塞式队列。阻塞式队列,用来保存服务器收到的消息,它是后续转发的基础。
只要服务器接收到的消息就会放到这个消息队列中(生产者),另外启动一个线程,让这个线程扫描队列,并把消息转发给其他客户端(消费者)
b)保存在线用户列表。因为服务器转发消息的时候要转发给所有在线的用户。

package api;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import model.Message;

import javax.websocket.Session;
import java.io.IOException;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingQueue;

//这个类主要用来管理消息和用户列表. 实现消息转发.
//其次这个类作为一个单例即可.
public class MessageCenter {
    private volatile static MessageCenter instance = null;

	//双重校验锁的方式来写一个单例模式
    public static MessageCenter getInstance() {
        if (instance == null) {
            synchronized (MessageCenter.class) {
                if (instance == null) {
                    instance = new MessageCenter();
                }
            }
        }
        return instance;
    }

    // 1. 保存消息的队列.
    private BlockingQueue<Message> messages = new LinkedBlockingQueue<>();
    // 2. 保存在线用户列表
    private ConcurrentHashMap<Integer, Session> onlineUsers = new ConcurrentHashMap<>();

    // 1. 用户上线功能
    public void addOnlineUser(int userId, Session session) {
        onlineUsers.put(userId, session);
    }
    
    // 2. 用户下线
    public void delOnlineUser(int userId) {
        onlineUsers.remove(userId);
    }
    
    // 3. 新增消息
    public void addMessage(Message message) throws InterruptedException {
        messages.put(message);
    }

    // 接下来这个代码非常重要!!!!
    // 创建一个线程, 来一直扫描消息的队列, 把里面存的消息转发给所有的在线用户.
    //在构造MessageCenter 实例的时候, 就启动这个线程.
    private MessageCenter() {
        Thread t = new Thread() {
            @Override
            public void run() {
                Gson gson = new GsonBuilder().create();
                while (true) {
                    try {
                        // 1. 从队列中尝试取消息
                        //    如果队列为空, 此时 take 就会阻塞
                        Message message = messages.take();
                        // 2. 把消息转成 json 字符串
                        String jsonString = gson.toJson(message);
                        // 3. 遍历在线用户列表, 把消息转发给每个用户
                        for (ConcurrentHashMap.Entry<Integer, Session> entry : onlineUsers.entrySet()) {
                            Session session = entry.getValue();
                            session.getBasicRemote().sendText(jsonString);
                        }
                    } catch (InterruptedException | IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        t.start();
    }
}

6)建立WebSocket连接

package api;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import dao.MessageDao;
import dao.UserDao;
import model.Message;
import model.User;

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

// 这个类用来实现 websocket 相关接口
// 此处 userId 是一个变量. 是客户端连接的时候, 具体提交的 userId 。
@ServerEndpoint(value="/message/{userId}")
public class MessageAPI {
    private Gson gson = new GsonBuilder().create();
    // 通过用户连接的 url 中获取到的.
    private int userId;

    @OnOpen
    public void onOpen(@PathParam("userId") String userIdStr, Session session) throws IOException {
        // 1. 获取 userId
        this.userId = Integer.parseInt(userIdStr);
        System.out.println("连接建立: " + userId);
        
        // 2. 把该用户加入到在线用户列表中.
        MessageCenter.getInstance().addOnlineUser(userId, session);
        
        // 3. 拉取历史消息, 并直接转发给该用户.
        UserDao userDao = new UserDao();
        User user = userDao.selectById(userId);
        Timestamp lastLogout = user.getLastLogout();
        MessageDao messageDao = new MessageDao();
        List<Message> messages = messageDao.selectByTimeStamp(lastLogout, new Timestamp(System.currentTimeMillis()));
        for (Message message : messages) {
            String jsonString = gson.toJson(message);
            session.getBasicRemote().sendText(jsonString);
        }
    }

    @OnClose
    public void onClose() {
        System.out.println("连接断开! " + userId);
        // 把用户从在线列表中删掉
        MessageCenter.getInstance().delOnlineUser(userId);
        // 更新下线时间
        UserDao userDao = new UserDao();
        userDao.updateLogout(userId);
    }

    @OnError
    public void onError(Session session, Throwable error) {
        System.out.println("连接出现错误! " + userId);
        error.printStackTrace();
        // 把用户从在线列表中删掉
        MessageCenter.getInstance().delOnlineUser(userId);
        // 更新下线时间
        UserDao userDao = new UserDao();
        userDao.updateLogout(userId);
    }

    @OnMessage
    public void onMessage(String request, Session session) throws InterruptedException {
        System.out.println("收到消息! " + userId + ": " + request);
        // 1. 解析 message 格式. 收到的 request 对象应该是一个 json 格式的字符串
        Message message = gson.fromJson(request, Message.class);
        // 2. 把消息的发送时间填写一下
        message.setSendTime(new Timestamp(System.currentTimeMillis()));
        // 3. 把消息写入消息中心.
        MessageCenter.getInstance().addMessage(message);
        // 4. 把这个消息写入数据库
        MessageDao messageDao = new MessageDao();
        messageDao.add(message);
    }
}

项目代码结构

在这里插入图片描述

  • 8
    点赞
  • 42
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值