从零开始,但又不是完全丛零开始的也不那么胎教的”Web聊天室“项目实现(附代码)

最终整个项目的完整代码我会放在文末
整体流程总结:
1、新建项目准备
(1)创建maven项目;选择maven-archetype-webapp
(2)修改WEB-INF下的web.xml;配置pom.xml下的依赖包
(3)在Setting的Plugins中安装Lombok(这个在本项目中的作用是不用再生成private的Get和Set的方法了)
(4)还需要准备一些工具包放在webapp下,“css”、“fonts”和“js”的一些包,这些主要是构成前段页面的(我代码中有了)

2、需求分析
(1)打开主页,见到登陆页面
(2)登陆成功,进入主页面
(3)主页面中可以看到当前的频道(房间)列表
(4)点击某个频道,可以看到频道(房间)中的消息
(5)点击某个频道,可以发送消息,此时其他用户也都能看到该消息

3、前后端API设计
在本次项目中,前端页面已给出来,我们需要根据开发文档返回给前端所要求的内容,再写这些接口之前,可以把前端页面先运行起来看一下,讲我这里提供的index.html放在webapp文件下;然后配置一下tomcat启动一下,可以看到界面,可以尝试点击以下登陆或者注册,肯定是没反应,在开发者工具(谷歌浏览器按F12)里面我们可以看到登陆端口是404的状态。接下来我们来写后端代码部分
在这里插入图片描述
ps:这里附上数据库的代码,在进行以下项目之前,先用这些sql语句将数据库创建了

<details>
  <summary>展开查看</summary>
  <pre><code> 
     System.out.println("Hello");

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', '', '我擅长rap', 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());


  </code></pre>
</details>

(1)首先写一个工具类

  • ①与数据库建立连接的方法
  • ②对json字符串进行序列化与反序列化;稍后我们会看到前后端的API接口中,后端返回的对象是有固定格式的,所以我们要进行序列化
    这部分代码我也贴在这里(详细的代码说明我写在代码的注释里)
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)实现登陆功能

根据开发文档中对登陆功能的请求与响应的描述如下:

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

在java文件夹下创建一个servlet类专门存放与负责实现传递前后端信息的类
创建一个LoginServlet类:

  • ① 惯例,继承HttpServlet,写WebServlet()的路由地址,根据开发文档知道路由地址为@WebServlet("/login")

  • ② 然后生成doGet和doPost方法;
      此处要对这个登录页面的功能进行一个说明:在我们一进入到主页面时,此时应该检测我们有没有登陆,所以在实现登陆功能时,也要实现一个检测登陆状态的功能;因为用户在登陆以后这个信息是保存在浏览器的Session中的,为了方便用户不用没刷新一次浏览器就要重新登陆信息,我们要确认浏览器的Session有没有保存已经登陆的用户信息,如果有这个信息,就直接进入到屏道页面,如果没有这个信息,那就显示的是最开始的”请先登录页面“

  • ③ 我们在doPost中实现登陆的接口;在doGet中实现检测登陆状态的接口:惯例主要注释我写在代码中贴在下面

  • ④ 在实现doPost和doGet功能之前,还有一点就是,我们既然要从Session中得到用户的信息,那应该有一个用户的对象,并且可以从开发文档中看到,请求和响应都是一个对象,我们之前不是写了一个json序列化和反序列化么,在登陆时,就需要将输入反序列化为一个用户对象(这个对象我们需要新建);检测是否登陆时,也是将Session中的信息转换成用户对象。所以,这步要新建一个模板类文件夹,在下面新建一个用户类对象的模板,代码在这里

  • ⑤ 在我这里定义模板类时,我把响应也单独作为了一个模板类,这个模板类里面放着通用的响应信息,也是为了前后端接口字段的统一

  • ⑥ 在具体与数据库进行增删查改时,还需要创建一个具体操作的类文件夹,在该文件夹下创建具体的操作方法,此处登陆时,我们需要在数据库中查询是否有此用户。所以,先创建一个dao类,并在该类文件夹下创建一个UserDAO类,专门用改针对用户表进行操作的具体操作类。具体注释也在代码中体现

  • ⑦ 实现完登录功能后,实现检测登陆状态的接口

  • ⑧ 实现这部分代码后,可以尝试启动一下项目,可以发现,现在可以登录了,并且登陆之后刷新页面也依旧会保持登陆状态。此时运行按F12可以看到出现两个两个报错,一个是无法获取频道列表,另一个是收发消息的WebSocket功能无法实现。下一步我们先实现频道列表的获取。

(3)实现注销功能(退出功能)

根据开发文档中对注销功能的请求与响应的描述如下:

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

创建一个LogoutServlet类

  • ① 同样,先继承HttpServle,再写@WebServlet的路由地址:@WebServlet("/logout")
  • ② 生成doGet()与doPost()方法,这里只需doGet()方法就好,可以用doPost()也调用doGet()
      主要的实现逻辑为:从浏览器保存的Session中获取用户信息,如果获取到了就将用户信息删除,如果没获取到,就返回“用户未登录”信息
    具体流程写在代码中, ------------这里贴上LogoutServlet.java代码---------------

(3)实现频道查询

该功能必须在用户登陆后才可以使用,将查询到的频道信息显示在用户登陆后的页面上
根据开发文档中对频道查询的请求与响应的描述如下:

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

我们还没有创建有关频道信息的模板,所以先在model下创建一个Channel类,来记录基本的有关频道的信息
—这里贴上Channel.java代码—
创建一个ChannelServlet类:

  • ① 继承HttpServlet,写WebServlet地址:@WebServlet("/channel")
  • ② 生成doGet()与doPost()方法,这里只需doGet()方法就好,可以用doPost()也调用doGet()
      主要实现逻辑为:老三样
    (1)设置请求响应格式 (2)实现业务逻辑 (3)返回相应数据
    需要在第二部分创建具体查询频道数据表信息的方法,即在dao文件下创建ChannelDAO类,在该类中实现此处要用的查询方法query()
    ----这里贴上ChannelDAO.java与ChannelServlet.java—

(4)使用WebSocket实现发送和接收消息

需要注意的是,在websocket中的session和Http协议中的session是不一样的

    建立连接    

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

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

这里需要用到一个新的东西,WebSocket,简单看看概念:
  WebSocket是一种在单个TCP连接上进行全双工(客户端和服务器端均既可接受也可发送)通讯的协议,在WebSocket的API中,客户端和服务器端只需要完成一次握手,两者之间就可以创建持久性的链接,并进行双向数据传输

接下来,我们进行代码部分,在代码中边写,边分析
首先,创建MessageWebsocket类

  • ① 与原先不同的是,此时不是@WebSocket获取路由了,而是使用@ServerEndPoint()建立连接,@ServerEndpoint("/message/{userId}"),是根据URL地址获取到userId

  • ② 在实现WebSocket功能中,我们要重写一些它自身的方法
    需要重写的方法有以下几种:
      @OnOpen // 成功建立连接
      @OnClose // 关闭连接
      @OnMessage // 收到消息
      @OnError // 连接出错

  • ③ 在重写之前需要创建一个关于消息的模板类MessageCenter类来保存所有客户端的session
    在这个类中,我们实现一些有关在聊天室中发送消息的基本方法:

    • 这个类是叫做 MessageCenter 顾名思义,是存放消息的地方,客户端发送的消息都要先到达服务器这里,然后再通过服务器转发给另一个客户端
    • addMessage() 方法,是将从客户端接收到的消息在服务器端先存放在一个阻塞队列中,经由另外的线程去发送
    • addOnLinUser() 方法,是将在WebSocket建立后,将用户的id和客户端的session信息保存起来,保存在 ConcurrentHashMap中(一种支持线程安全的map结构,并且满足高并发(读写,读读并发,写写互斥))
    • delOnlinUser() 方法,是当关闭websocket链接,或是程序出错时,删除保存的客户端session信息
    • sendMessage() 方法,当接收到每个客户端发送来的消息时,将该消息转发到所有的客户端
  • ④ 从客户端发送到服务器端的消息,我们要把它保存起来,就需要一个消息模板,来保存有关这条消息的各种信息
      在model下新建一个Message类,里面包含了:这条消息的id,发送这条消息的用户的id,发送这条消息所在频道的id,消息的内容,消息所发送的时间,发送这条消息的用户的昵称

  • ⑤ 当服务器接收到客户端法莱的消息时,是把这条消息储存在 消息数据库 中,所以我们要写一个具体的操作消息数据库的方法类
    先创建一个名为MessageDAO的类,具体操作方法,在用到时再写。

  • ⑥ 接下来我们开始重写websocket中的方法
    (1)重写OnOpen —— 建立连接(建立连接就表示登陆,试想,当我们使用微信QQ,重新登录上去后,在我们没有登录的这一段时间内,别人发给我们的消息全都显示了出来,所以我们在建立连接时,要同时判断一下,在该用户下线的这个时间段里,都有哪些发送给该用户的消息)

    • 1、将每个客户端的session都保存起来,有了这些session信息,服务器端就可以将后续的消息转发到这些客户端
    • 2、查询本用户(本客户端)在上次登录之后,其他用户在某一频道里发送的消息(这些消息储存在数据库中,根据时间戳,拿到这些消息)
      此处就要在MessageDAO类下写一个方法:queryByLastLogout(userid),查询最后一次登录后数据库收到的消息(根据用户id查询)
      ------这里贴上MessageDAO中的queryByLastLogout方法代码-------
    • 3、将这些消息发送给当前用户

    (2)重写OnMessage —— 将A用户发送来的消息转发给其他用户并且服务器保存接收到用户A发送来的消息

    • 1、遍历所有的session,对每个session都发送消息
    • 2、服务器将这条消息反序列化为Message类型的对象后,插入到服务器的 消息数据库 中
      此处要在MessageDAO类下写一个方法:insert(msg),将就受到的消息插入到数据库中
      ------这里贴上MessageDAO中的insert方法代码-------

    (3)重写OnClose —— 关闭连接,将某一客户端断开连接

    • 1、该客户端断开连接,要将在MessageCenter中保存的该客户端的session信息删除
      此处用到了前面在MessageCenter中写的delOnlineUser方法
    • 2、下次该用户如果建立连接时,需要收到在该用户下线的这段时间有其他客户端发送给该客户端的消息,所以要更新该用户最后下线时刻的时间
      此处要在MessageDAO类下写一个方法:updateLastLogout(userId),更新当前这个断开链接的用户最后的在线时间

    (4)重写OnError —— 出现错误的时候,同样关闭连接,写法和关闭连接一样

这样聊天室的功能就实现了

热门文章

暂无图片
编程学习 ·

Java输出数组的内容

Java输出数组的内容_一万个小时-CSDN博客_java打印数组内容1. 输出内容最常见的方式// List<String>类型的列表List<String> list new ArrayList<String>();list.add("First");list.add("Second");list.add("Third");list.ad…
暂无图片
编程学习 ·

母螳螂的“魅惑之术”

在它们对大蝗虫发起进攻的时候&#xff0c;我认认真真地观察了一次&#xff0c;因为它们突然像触电一样浑身痉挛起来&#xff0c;警觉地面对限前这个大家伙&#xff0c;然后放下自己优雅的身段和祈祷的双手&#xff0c;摆出了一个可怕的姿势。我被眼前的一幕吓到了&#xff0c;…
暂无图片
编程学习 ·

疯狂填词 mad_libs 第9章9.9.2

#win7 python3.7.0 import os,reos.chdir(d:\documents\program_language) file1open(.\疯狂填词_d9z9d2_r.txt) file2open(.\疯狂填词_d9z9d2_w.txt,w) words[ADJECTIVE,NOUN,VERB,NOUN] str1file1.read()#方法1 for word in words :word_replaceinput(fEnter a {word} :)str1…
暂无图片
编程学习 ·

HBASE 高可用

为了保证HBASE是高可用的,所依赖的HDFS和zookeeper也要是高可用的. 通过参数hbase.rootdir指定了连接到Hadoop的地址,mycluster表示为Hadoop的集群. HBASE本身的高可用很简单,只要在一个健康的集群其他节点通过命令 hbase-daemon.sh start master启动一个Hmaster进程,这个Hmast…
暂无图片
编程学习 ·

js事件操作语法

一、事件的绑定语法 语法形式1 事件监听 标签对象.addEventListener(click,function(){}); 语法形式2 on语法绑定 标签对象.onclick function(){} on语法是通过 等于赋值绑定的事件处理函数 , 等于赋值本质上执行的是覆盖赋值,后赋值的数据会覆盖之前存储的数据,也就是on…
暂无图片
编程学习 ·

Photoshop插件--晕影动态--选区--脚本开发--PS插件

文章目录1.插件界面2.关键代码2.1 选区2.2 动态晕影3.作者寄语PS是一款栅格图像编辑软件&#xff0c;具有许多强大的功能&#xff0c;本文演示如何通过脚本实现晕影动态和选区相关功能&#xff0c;展示从互联网收集而来的一个小插件&#xff0c;供大家学习交流&#xff0c;请勿…
暂无图片
编程学习 ·

vs LNK1104 无法打开文件“xxx.obj”

写在前面&#xff1a; 向大家推荐两本新书&#xff0c;《深度学习计算机视觉实战》和《学习OpenCV4&#xff1a;基于Python的算法实战》。 《深度学习计算机视觉实战》讲了计算机视觉理论基础&#xff0c;讲了案例项目&#xff0c;讲了模型部署&#xff0c;这些项目学会之后可以…
暂无图片
编程学习 ·

工业元宇宙的定义与实施路线图

工业元宇宙的定义与实施路线图 李正海 1 工业元宇宙 给大家做一个关于工业元宇宙的定义。对于工业&#xff0c;从设计的角度来讲&#xff0c;现在的设计人员已经做到了普遍的三维设计&#xff0c;但是进入元宇宙时代&#xff0c;就不仅仅只是三维设计了&#xff0c;我们的目…
暂无图片
编程学习 ·

【leectode 2022.1.15】完成一半题目

有 N 位扣友参加了微软与力扣举办了「以扣会友」线下活动。主办方提供了 2*N 道题目&#xff0c;整型数组 questions 中每个数字对应了每道题目所涉及的知识点类型。 若每位扣友选择不同的一题&#xff0c;请返回被选的 N 道题目至少包含多少种知识点类型。 示例 1&#xff1a…
暂无图片
编程学习 ·

js 面试题总结

一、js原型与原型链 1. prototype 每个函数都有一个prototype属性&#xff0c;被称为显示原型 2._ _proto_ _ 每个实例对象都会有_ _proto_ _属性,其被称为隐式原型 每一个实例对象的隐式原型_ _proto_ _属性指向自身构造函数的显式原型prototype 3. constructor 每个prot…
暂无图片
编程学习 ·

java练习代码

打印自定义行数的空心菱形练习代码如下 import java.util.Scanner; public class daYinLengXing{public static void main(String[] args) {System.out.println("请输入行数");Scanner myScanner new Scanner(System.in);int g myScanner.nextInt();int num g%2;//…
暂无图片
编程学习 ·

RocketMQ-什么是死信队列?怎么解决

目录 什么是死信队列 死信队列的特征 死信消息的处理 什么是死信队列 当一条消息初次消费失败&#xff0c;消息队列会自动进行消费重试&#xff1b;达到最大重试次数后&#xff0c;若消费依然失败&#xff0c;则表明消费者在正常情况下无法正确地消费该消息&#xff0c;此时…
暂无图片
编程学习 ·

项目 cg day04

第4章 lua、Canal实现广告缓存 学习目标 Lua介绍 Lua语法 输出、变量定义、数据类型、流程控制(if..)、循环操作、函数、表(数组)、模块OpenResty介绍(理解配置) 封装了Nginx&#xff0c;并且提供了Lua扩展&#xff0c;大大提升了Nginx对并发处理的能&#xff0c;10K-1000K Lu…
暂无图片
编程学习 ·

输出三角形

#include <stdio.h> int main() { int i,j; for(i0;i<5;i) { for(j0;j<i;j) { printf("*"); } printf("\n"); } }
暂无图片
编程学习 ·

stm32的BOOTLOADER学习1

序言 最近计划学习stm32的BOOTLOADER学习,把学习过程记录下来 因为现在网上STM32C8T6还是比较贵的,根据我的需求flash空间小一些也可以,所以我决定使用stm32c6t6.这个芯片的空间是32kb的。 #熟悉芯片内部的空间地址 1、flash ROM&#xff1a; 大小32KB&#xff0c;范围&#xf…
暂无图片
编程学习 ·

通过awk和shell来限制IP多次访问之学不会你打死我

学不会你打死我 今天我们用shell脚本&#xff0c;awk工具来分析日志来判断是否存在扫描器来进行破解网站密码——限制访问次数过多的IP地址&#xff0c;通过Iptables来进行限制。代码在末尾 首先我们要先查看日志的格式&#xff0c;分析出我们需要筛选的内容&#xff0c;日志…
暂无图片
编程学习 ·

Python - 如何像程序员一样思考

在为计算机编写程序之前&#xff0c;您必须学会如何像程序员一样思考。学习像程序员一样思考对任何学生都很有价值。以下步骤可帮助任何人学习编码并了解计算机科学的价值——即使他们不打算成为计算机科学家。 顾名思义&#xff0c;Python经常被想要学习编程的人用作第一语言…
暂无图片
编程学习 ·

蓝桥杯python-数字三角形

问题描述 虽然我前后用了三种做法&#xff0c;但是我发现只有“优化思路_1”可以通过蓝桥杯官网中的测评&#xff0c;但是如果用c/c的话&#xff0c;每个都通得过&#xff0c;足以可见python的效率之低&#xff08;但耐不住人家好用啊&#xff08;哭笑&#xff09;&#xff09…