目录
1)从前端接收用户输入的用户名和密码(以json字符串形式)
3)Ajax发送请求,返回数据库中真实的群聊列表和当前的登录用户
8)登录用户获取到从上次注销之后到现在登陆之间没有查看的消息(返回个前端并展示)
9)某个用户断开连接时保存退出时间,并删除session和更新到数据库
一,简介
web聊天室为用户提供一个网页版的即时在线聊天平台,同时支持新用户注册登录,老用户登录查看历史消息
二,开发环境
JDK1.8,IDEA开发工具,Maven管理工具,MySQL数据库
三,涉及的技术
Ajax技术,WebSocket协议,Http协议,单例模式
四,主要功能
1.注册功能
2.登录功能
3.异地登陆(第一次登录网页会被强制退出)
4.发送消息
5.接收消息
6.注销登录
五,准备工作
1.引入开发需要的依赖包
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>chatroom</artifactId>
<version>1.0-SNAPSHOT</version>
<!-- 规定相应的打包格式 -->
<packaging>war</packaging>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.49</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.12.3</version>
</dependency>
<!-- 引入单元测试框架:方便我们做测试, -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.1</version>
</dependency>
</dependencies>
<!-- 为了打包后的名称简明扼要 -->
<build>
<finalName>chatroom</finalName>
</build>
</project>
2.设计数据库以及表关系
drop database if exists chatroom;
create database chatroom character set utf8mb4;
use chatroom;
create table user(
id int primary key auto_increment,
username varchar(20) not null unique comment '账号',
password varchar(50) not null comment '密码',
nickname varchar(20) not null comment '昵称',
head varchar(50) comment '头像url(相对路径)',
logout_time datetime comment '退出登录时间'
) comment '用户表';
insert into user(username, password, nickname, logout_time) values('a', '123', '张三', '2019-01-01 00:00:00');
insert into user(username, password, nickname, logout_time) values('b', '123', '李四', '2019-01-01 00:00:00');
insert into user(username, password, nickname, logout_time) values('c', '123', '王五', '2019-01-01 00:00:00');
create table channel(
id int primary key auto_increment,
name varchar(20) not null unique comment '频道名称'
) comment '频道';
insert into channel(name) values ('群聊一');
insert into channel(name) values ('群聊二');
insert into channel(name) values ('群聊三');
create table message(
id int primary key auto_increment,
user_id int comment '消息发送方:用户id',
user_nickname varchar(20) comment '消息发送方:用户昵称(历史消息展示需要)',
channel_id int comment '消息接收方:频道id',
content varchar(255) comment '消息内容',
send_time datetime comment '消息发送时间',
foreign key (user_id) references user(id),
foreign key (channel_id) references channel(id)
) comment '发送的消息记录';
3.设计实体类
作用:
/*
* 数据库实体类:
* http请求可能把请求数据转为一个实体类对象
* http响应可能把实体类对象/List<实体类>返回给客户端
* 数据库CRUD操作,都可能使用到实体类对象来进行操作
* 查询返回一个或多个实体类对象
* 插入修改就是使用实体类对象的属性来作为插入/修改的值
*/
4.设计工具类
作用:
编写一些公用的方法,或者是数据连接,后续在后端中只需通过类名调用即可
数据库连接类
package org.example.util;
import com.mysql.jdbc.jdbc2.optional.MysqlDataSource;
import org.junit.Test;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
public class DBUtil {
//定义一个单例的数据源/连接池对象
private static final MysqlDataSource DS = new MysqlDataSource();
static {
DS.setURL("jdbc:mysql://127.0.0.1:3306/chatroom");
DS.setUser("root");
DS.setPassword("111111");
DS.setUseSSL(false);
DS.setUseUnicode(true);
DS.setCharacterEncoding("UTF-8");
}
//获取数据库连接
public static Connection getConnection(){
try {
return DS.getConnection();
} catch (SQLException e) {
throw new RuntimeException("获取数据库连接失败", 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 RuntimeException("数据库jdbc释放数据库资源出错", e);
}
}
public static void close(Connection c, Statement s){
close(c,s,null);
}
@Test
public void getConnectionTest(){
System.out.println(getConnection());
}
}
公共方法类
序列化与反序列化,获取session中保存的用户信息等方法
package org.example.util;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.example.model.User;
import org.junit.Test;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.io.InputStream;
public class WebUtil {
private static final ObjectMapper M = new ObjectMapper();
//序列化:提供将Java对象转换为json字符串的方法
public static String write(Object o){
try {
return M.writeValueAsString(o);
} catch (JsonProcessingException e) {
throw new RuntimeException("转换Java对象为json字符串出错", e);
}
}
//反序列化:将json字符串转换为Java对象
//提供两个重载方法,InputStream和String来转换
public static <T> T read(InputStream is,Class<T> clazz){
try {
return M.readValue(is,clazz);
} catch (IOException e) {
throw new RuntimeException("转换输入流json字符串为Java对象出错", e);
}
}
public static <T> T read(String str,Class<T> clazz){
try {
return M.readValue(str,clazz);
} catch (IOException e) {
throw new RuntimeException("转换输入流json字符串为Java对象出错", e);
}
}
//获取session中保存的用户信息
public static User getLoginUser(HttpSession session){
if(session != null){
//session不为空,返回session中获取的用户
//获取时的键,需要和登录时保存的一致
return (User) session.getAttribute("user");
}
return null;
}
@Test
public void writeTest(){
User user = new User();
user.setId(1);
user.setUsername("abc");
user.setPassword("123");
user.setNickname("张三");
System.out.println(write(user));
}
@Test
public void readTest(){
String s = "{\"id\":1,\"username\":\"abc\",\"password\":\"123\",\"nickname\":\"张三\",\"logoutTime\":null}";
System.out.println(read(s,User.class));
}
}
六.注册功能
1)从前端获取用户输入的数据
有用户名,密码,昵称,图像四个字段
首先处理图像字段
//注册选择头像,显示预览图片
//e时传入的事件对象
showHead: function (e){
//获取选择的文件:文件控件可以配置为选择多个文件
let headFile = e.target.files[0];
//先保存
app.head.file = headFile;
//生成图片url(客户端本地)
app.head.src = URL.createObjectURL(headFile);
提交格式以form-data格式提交(因为有图像,是以图片文件传入的)
//注册功能
//使用FormData对象作为form-data格式上传的数据
let formData = new FormData();
//添加数据:append(key,value)
formData.append("username",app.username);
formData.append("password",app.password);
formData.append("nickname",app.nickname);
if(app.head.file != null)
formData.append("headFile",app.head.file);
2)后端获取前端传输的数据
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//设置请求正文的编码
req.setCharacterEncoding("UTF-8");
//获取form-data格式的数据,有上传文件,加servlet注解
String username = req.getParameter("username");
String password = req.getParameter("password");
String nickname = req.getParameter("nickname");
Part headFile = req.getPart("headFile");
//注册需要把数据插入到数据库,先把数据设置到User对象属性中
User user = new User();
user.setUsername(username);
user.setPassword(password);
user.setNickname(nickname);
//保存上传的图像在服务端本地的一个路径(不在项目的路径中)
if(headFile != null){
//使用一个随机字符串作为保存在服务端本地的文件名
//先获取上传文件的名称后缀
String fileName = headFile.getSubmittedFileName();
String suffix = fileName.substring(fileName.lastIndexOf("."));
//UUID是一个随机字符串(和机器,时间戳相关)
//这个文件名需要保存在数据库
fileName = UUID.randomUUID().toString() + suffix;
//保存文件
headFile.write(WebUtil.LOCAL_HEAD_PATH + "/" +fileName);
//数据库保存图像路径:/+fileName
user.setHead("/"+fileName);
}
3)判断数据库中是否有相同的账号昵称
//保存user到数据库:需要考虑账号是否已经存在
//先校验账号昵称是否已存在(username == ? or nickname = ?)
User exist = UserDao.checkIfExist(username,nickname);
4)如果没有将注册数据插入并返回后端提示给前端
//如果存在,响应返回错误信息,如果不存在,注册(插入数据)
//构造响应对象
JsonResult result = new JsonResult();
if(exist != null){
result.setOk(false);//可以省略,new出来时就是false(ok是基础字段)
result.setReason("账号或昵称已经存在");
}else{
int n = UserDao.insert(user);
result.setOk(true);
}
//返回给http响应给前端
resp.setContentType("application/json;charset=utf-8");
String body = WebUtil.write(result);
resp.getWriter().write(body);
5)前端接受后端的响应(注册成功或者是失败)
ajax({
method: "post",
url: "../register",
//上传文件,使用form-data格式
body: formData,
callback: function (status,responseText) {
if(status != 200){
alert("出错了,响应状态码:"+ status);
return;
}
let body = JSON.parse(responseText);
if(body.ok){
alert("注册成功!");
window.location.href = "../index.html";
}else{
//注册失败
app.errorMessage = body.reason;
}
}
});
七,登录功能
1)从前端接收用户输入的用户名和密码(以json字符串形式)
2)后端接收json字符串
req.setCharacterEncoding("UTF-8");
//解析json数据,使用输入流
User input = WebUtil.read(req.getInputStream(),User.class);
//校验账号密码
//可以先校验账号是否存在(username=?)如果不存在,提示账号不存在,如果存在,再校验密码
User exist = UserDao.checkIfExist(input.getUsername(),null);
3)进行用户登录验证并返回响应
//准备返回的数据
JsonResult result = new JsonResult();
if(exist == null){
result.setReason("账号不存在");
}else{
//校验用户输入的密码和该账号在数据库的密码是否一致
if(!exist.getPassword().equals(input.getPassword())){
result.setReason("账号或密码错误");
}else{
//校验成功:创建session并保存用户信息
HttpSession session = req.getSession(true);
session.setAttribute("user",exist);//保存数据库查询的用户
result.setOk(true);
}
}
resp.setContentType("application/json; charset=utf-8");
String body = WebUtil.write(result);
resp.getWriter().write(body);
4)前端返回登录成功或是失败的提示(成功还需要跳转)
methods: {
login: function (){
ajax({
method: "post",
url: "login",
contentType: "application/json",
body: JSON.stringify({
username: app.username,
password: app.password,
}),
callback: function (status,responseText) {
if(status != 200){
alert("出错了,响应状态码:"+ status);
return;
}
let body = JSON.parse(responseText);
if(body.ok){
alert("登录成功!");
window.location.href = "views/message.html";
}else{
//登陆失败
app.errorMessage = body.reason;
}
}
});
},
},
八,在线聊天页面(核心)
1)对前端需要填充刷新的地方进行修改
let app = new Vue({
el: "#app",
data: {
//当前登录用户
currentUser:{
nickname: "",
head: "",
},
//频道/群列表:先写静态数据验证前端代码,后续从servlet获取
channels:[
{
id: 1,
name:"相亲相爱",
//每个频道有自己的历史消息
historyMessages:[],
//每个频道有自己输入框的内容
inputMessageContent: "",
},
{
id: 2,
name:"一家人",
//每个频道有自己的历史消息
historyMessages:[],
//每个频道有自己输入框的内容
inputMessageContent: "",
},
],
//当前频道
currentChannel:{
id: 2,
name:"一家人",
//每个频道有自己的历史消息
historyMessages:[],
//每个频道有自己输入框的内容
inputMessageContent: "",
},
},
2)静态添加群组和用户,完成点击并跳转新的群聊
//点击切换频道:channel是点击的频道
changeChannel:function (channel) {
//如果点击的频道不是当前频道,才切换
if(channel.id != app.currentChannel.id){
app.currentChannel = channel;
}
},
3)Ajax发送请求,返回数据库中真实的群聊列表和当前的登录用户
ajax({
method: "get",
url: "../channelList",
callback: function (status,responseText) {
if(status != 200){
alert("出错了,响应状态码:"+ status);
return;
}
let body = JSON.parse(responseText);
//返回的channel是不带historyMessages和inputMessageContent
app.currentUser = body.user;
for(let i = 0;i < body.channels.length;i++){
body.channels[i].historyMessages = [];
body.channels[i].inputMessageContent ="";
//默认切换到第一个频道
if(i == 0){
app.currentChannel = body.channels[0];
}
}
}
});
4)后端验证是否是登陆用户并返回群聊列表
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws
ServletException, IOException {
//必须登录返回频道列表及登录用户信息
//校验是否登录
User loginUser = WebUtil.getLoginUser(req.getSession(false));
if(loginUser == null){//没有登陆,不允许访问
//设置http响应状态码403(禁止访问)
resp.setStatus(403);
return;
}
//登陆成功,获取频道列表数据
List<Channel> channels = ChannelDao.selectAll();
//返回的响应正文json字符串结构,前后端相同即可
Map<String,Object> map = new HashMap<>();
map.put("user",loginUser);
map.put("channels",channels);
//返回响应数据
String body = WebUtil.write(map);
resp.setContentType("application/json;charset=utf-8");
resp.getWriter().write(body);
}
5)处理响应,并填充到前端
- 填充用户信息
- 将数据库真实的群聊列表提供给前端并填充
- 默认是以数据库第一个群聊/频道
callback: function (status,responseText) {
if(status != 200){
alert("出错了,响应状态码:"+ status);
return;
}
let body = JSON.parse(responseText);
//返回的channel是不带historyMessages和inputMessageContent
app.currentUser = body.user;
//遍历显示群聊信息
for(let i = 0;i < body.channels.length;i++){
body.channels[i].historyMessages = [];
body.channels[i].inputMessageContent ="";
//默认切换到第一个频道
if(i == 0){
app.currentChannel = body.channels[0];
}
}
//将后端返回的真实的数据库填充到群聊数组
app.channels = body.channels;
}
6)使用websocket技术进行前后端的消息推送
- 前端(客户端)先创建websocket对象来进行建立连接的操作(并且在客户端发送Ajax请求之后,在回调函数中初始化)
initWebSocket:function () {
//先创建一个websocket对象,用来建立连接,客户端收发数据
//url格式————协议名://ip:port/contextPath/资源路径
//contextPath是部署的项目名/项目路径,websocket协议名ws
let protocol = location.protocol;//读取当前地址栏的url的协议名
let url = location.href;
//截取需要的字符串
url = url.substring((protocol+"//").length,url.indexOf("/views/message.html"));
let ws = new WebSocket("ws://"+url+"/message");
//为websocket对象绑定事件(事件发生的时候,由浏览器自动调用事件函数)
//建立连接的事件: e就是事件对象
ws.onopen = function(e){
console.log("客户端建立连接")
}
//关闭连接的事件:关闭可能是先由服务端关闭,或者先由客户端关闭
ws.onclose = function(e){
let reason = e.reason;
if(reason){
alert(reason)
}
}
//发生错误时的事件
ws.onerror = function(e){
console.log("websocket出错了")
}
//接收到消息时的事件
ws.onmessage = function(e){//服务端推送消息给客户端时,执行这个函数
// console.log(e.data)//通过事件对象.data就可以获取到服务端推送的消息(可以是二进制数据,或字符串)
//遍历频道列表,如果频道id和获取到的消息中channelId相同,放到历史消息数组中
let m = JSON.parse(e.data);
for(let channel of app.channels){
if(channel.id == m.channelId){
channel.historyMessages.push(m);
}
}
}
//刷新/关闭页面,也需要关闭websocket关闭
window.onbeforeunload = function (e) {//当前窗口关闭前执行的函数
//主动关闭websocket连接
ws.close();
}
app.websocket = ws;
},
后端也进行四个函数的注册,并且构建websocket的配置类
- 配置类的作用:简单点说,就是在websocket建立连接之前(执行onOpen方法之前)先完成一些工作
//websocket的一些配置类,建立连接之前,定义一个配置
public class WebSocketConfigurator extends ServerEndpointConfig.Configurator {
//进行握手阶段的一些配置:在客户端与服务端建立websocket连接时,服务端就可以使用这个配置来完成一些初始化工作
@Override
public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
//定义好配置,websocket建立连接时,先执行配置的代码,然后执行onOpen
HttpSession httpSession = (HttpSession) request.getHttpSession();
//保存HttpSession数据
if(httpSession != null){
sec.getUserProperties().put("HttpSession",httpSession);
}
//之后onOpen建立websocket连接,就可以通过websocket的session来获取保存的httpSession
}
}
- 在这里的作用是:将httpsession保存下来,在执行onOpen方法的时候判断这个用户是否登录
//先使用websocket的session获取httpSession
HttpSession httpSession = (HttpSession) session.getUserProperties().get("HttpSession");
//校验是否登录
User user = WebUtil.getLoginUser(httpSession);
if(user == null){//没有登录,关闭连接
CloseReason reason = new CloseReason(CloseReason.CloseCodes.NORMAL_CLOSURE,"没有登录,不允许发送消息");
session.close(reason);
return;
}
7)踢掉使用相同账号登录的网页用户
//踢掉相同账号登录的用户:找到相同用户的session,然后关闭websocket session
Session prevSession = onLineUsers.get(user.getId());
if(prevSession != null){
//所有key中,是否包含用户的id,做法:踢出上个用户
CloseReason reason = new CloseReason(CloseReason.CloseCodes.NORMAL_CLOSURE,"账号在别处登 录");
prevSession.close(reason);
//这里关闭的是之前登陆的会话,当前建立的会话是允许的
}
8)登录用户获取到从上次注销之后到现在登陆之间没有查看的消息(返回个前端并展示)
//建立websocket连接之后,都要接收所有的历史消息
List<Message> messages = MessageDao.query(user.getLogoutTime());
for(Message m : messages){
//遍历历史消息,全部发送到当前用户的websocket
String json = WebUtil.write(m);
session.getBasicRemote().sendText(json);
}
9)某个用户断开连接时保存退出时间,并删除session和更新到数据库
@OnClose
public void onClose(){
System.out.println("断开连接");
//关闭连接:删除map中的当前会话,记录当前用户的注销时间
onLineUsers.remove(loginUser.getId());
loginUser.setLogoutTime(new java.util.Date());
int n = UserDao.updateLogoutTime(loginUser);
}
10)接收消息(即发送消息)
- 当客户端的用户点击发送后,后端接收到新的消息
- 后端接收到后,将前端页面需要的字段获取到后先返回给前端,然后将历史消息存放到数据库
前端获取到用户的发送的消息
sendMessage:function () {
let content = app.currentChannel.inputMessageContent;
if(content){
//后台需要插入数据库,客户端发送的消息
app.websocket.send(JSON.stringify({
channelId:app.currentChannel.id,
content:content,
}));
}
},
后端接收到消息进行处理(并且立即返回给前端页面)
public void onMessage(String message) throws IOException {
Message m = WebUtil.read(message,Message.class);
m.setUserId(loginUser.getId());
m.setUserNickname(loginUser.getNickname());
//给所有在线用户,发送消息(服务端主动发)
for(Session session : onLineUsers.values()){
int n = MessageDao.insert(m);
String json = WebUtil.write(m);
session.getBasicRemote().sendText(json);
}
}
九,注销登录
依次删除session,记录退出时间,并返回至登陆界面
public class LogoutServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
HttpSession session = req.getSession(false);
User user = WebUtil.getLoginUser(session);
if(user == null){
resp.setStatus(403);
return;
}
session.removeAttribute("user");
//记录了上次用户注销时间
user.setLogoutTime(new java.util.Date());
UserDao.updateLogoutTime(user);
resp.sendRedirect("index.html");
}
}
十,部分包下的源码
1.api(接口包)
package org.example.api;
import org.example.dao.ChannelDao;
import org.example.model.Channel;
import org.example.model.User;
import org.example.util.WebUtil;
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.HashMap;
import java.util.List;
import java.util.Map;
@WebServlet("/channelList")
public class ChannelListServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//必须登录返回频道列表及登录用户信息
//校验是否登录
User loginUser = WebUtil.getLoginUser(req.getSession(false));
if(loginUser == null){
//没有登录不允许访问
//设置响应状态码
resp.setStatus(403);
return;
}
//登陆成功:获取频道列表数据
List<Channel> channels = ChannelDao.selectAll();
//返回的响应正文json字符串结构
Map<String,Object> map = new HashMap<>();
map.put("user",loginUser);
map.put("channels",channels);
//返回响应数据
String body = WebUtil.write(map);
resp.setContentType("application/json;charset=utf-8");
resp.getWriter().write(body);
}
}
package org.example.api;
import org.example.dao.UserDao;
import org.example.model.JsonResult;
import org.example.model.User;
import org.example.util.WebUtil;
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 doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("UTF-8");
//解析json对象,使用输入流
User input = WebUtil.read(req.getInputStream(),User.class);
//校验账号密码
//1.校验账号是否存在(username=?)
User exist = UserDao.checkIfExist(input.getUsername(),null);
//准备返回的数据
JsonResult result = new JsonResult();
if(exist == null){
result.setReason("账号不存在");
}else{
//校验用户输入的密码和该账号在数据库的密码是否一致
if(!exist.getPassword().equals(input.getPassword())){
result.setReason("账号或密码错误");
}else{
//校验成功,创建Session保存用户信息
HttpSession session = req.getSession(true);
session.setAttribute("user",exist);//保存数据库查询的用户
result.setOk(true);
}
}
//返回响应数据
resp.setContentType("application/json;charset=utf-8");
String body = WebUtil.write(result);
resp.getWriter().write(body);
}
}
package org.example.api;
import org.example.dao.UserDao;
import org.example.model.User;
import org.example.util.WebUtil;
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 doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
HttpSession session = req.getSession(false);
User user = WebUtil.getLoginUser(session);
if(user == null){
resp.setStatus(403);
return;
}
session.removeAttribute("user");
//记录了上次用户注销时间
user.setLogoutTime(new java.util.Date());
UserDao.updateLogoutTime(user);
resp.sendRedirect("index.html");
}
}
package org.example.api;
import org.example.dao.MessageDao;
import org.example.dao.UserDao;
import org.example.model.Message;
import org.example.model.User;
import org.example.util.WebSocketConfigurator;
import org.example.util.WebUtil;
import javax.servlet.http.HttpSession;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ServerEndpoint(value="/message", configurator = WebSocketConfigurator.class)
public class MessageEndpoint {
//当前客户端websocket的会话
private Session session;
//当前登录的用户对象
private User loginUser;
//使用一个共享的数据结构来保存所有客户端websocket会话
// private static List<Session> onlineUsers = new ArrayList<>();
private static Map<Integer,Session> onlineUsers = new HashMap<>();
//session对象,是建立连接的客户端websocket的会话(建立连接到关闭连接就是某个客户端的一次会话)
//这个session和登录时,使用HttpSession是不同,
// 但可以通过配置里边,先使用websocket session保存httpSession,
// 然后建立连接时获取到httpSession
@OnOpen
public void onOpen(Session session) throws IOException {
//先使用websocket session获取httpSession
HttpSession httpSession = (HttpSession) session.getUserProperties()
.get("HttpSession");
//校验:是否已登录
User user = WebUtil.getLoginUser(httpSession);
if(user == null){//没有登录:关闭连接
//CloseCodes是设置websocket的状态码
CloseReason reason = new CloseReason(CloseReason.CloseCodes.NORMAL_CLOSURE,
"没有登录,不允许发送消息");
session.close(reason);
return;
}
this.loginUser = user;
//踢掉使用相同账号登录的用户:找到相同用户的session,然后关闭websocket session
Session preSession = onlineUsers.get(user.getId());
if(preSession != null){
//相同账号重复登录
CloseReason reason = new CloseReason(CloseReason.CloseCodes.NORMAL_CLOSURE,
"账号在别处登录");
preSession.close(reason);
//这里关闭的是之前登录的会话,当前建立的会话是允许的
}
//保存session到成员变量,后边的事件方法就可以使用session来获取信息、发送消息
this.session = session;
//后续服务端接收到一个消息时,还需要把消息发送给所有在线用户:需要保存所有客户端会话
onlineUsers.put(user.getId(),session);
//建立websocket连接后,客户端需要接收所有的历史消息
List<Message> messages = MessageDao.query(user.getLogoutTime());
for(Message m : messages){//遍历历史消息,全发送到当前用户的websocket
//先把这个对象message转换为json字符串,在发送消息
String json = WebUtil.write(m);
session.getBasicRemote().sendText(json);
}
}
@OnClose
public void onClose(){
System.out.println("断开连接");
//关闭连接:删除map中的当前会话,记录当前用户的上次注销时间
onlineUsers.remove(loginUser.getId());
loginUser.setLogoutTime(new java.util.Date());
int n = UserDao.updateLogoutTime(loginUser);
}
@OnError
public void onError(Throwable t){
t.printStackTrace();
//出现异常删除map中的当前会话
onlineUsers.remove(loginUser.getId());
}
@OnMessage//服务端接收到消息
public void onMessage(String message) throws IOException {
Message m = WebUtil.read(message,Message.class);
m.setUserId(loginUser.getId());
m.setUserNickname(loginUser.getNickname());
System.out.println("服务端接收到消息:"+message);
//给所有在线用户,发送消息(消息推送:服务端主动发)
for(Session session : onlineUsers.values()){
int n = MessageDao.insert(m);
String json = WebUtil.write(m);
session.getBasicRemote().sendText(json);
}
}
}
package org.example.api;
import org.example.dao.UserDao;
import org.example.model.JsonResult;
import org.example.model.User;
import org.example.util.WebUtil;
import javax.servlet.ServletException;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.Part;
import java.io.IOException;
import java.util.UUID;
@WebServlet("/register")
@MultipartConfig//表示上传文件时(form-data)
public class RegisterServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//设置请求正文的编码
req.setCharacterEncoding("UTF-8");
//获取form-data格式的数据,有上传文件,加servlet注解
String username = req.getParameter("username");//页面必填,非空
String password = req.getParameter("password");//页面必填,非空
String nickname = req.getParameter("nickname");//页面必填,非空
Part headFile = req.getPart("headFile");//页面选填,可能为空
//注册需要把数据插入数据库,先把数据设置到User对象属性中
User user = new User();
user.setUsername(username);
user.setPassword(password);
user.setNickname(username);
//保存上传的头像在服务端本地的一个路径(不要在项目的路径中,项目要打包部署到tomcat运行)
if(headFile != null){
//使用一个随机字符串作为保存在服务端本地的文件名,文件的后缀需要和上传的文件的后缀一样
//先获取上传文件的名称后缀
String fileName = headFile.getSubmittedFileName();//上传文件名
String suffix = fileName.substring(fileName.lastIndexOf("."));
//UUID是一个随机字符串(和机器,时间戳有关)
fileName = UUID.randomUUID() + suffix;
//保存文件
headFile.write(WebUtil.LOCAL_HEAD_PATH+"/"+fileName);
//数据库保存头像路径:/+fileName
user.setHead("/"+fileName);
}
//保存数据到数据库:需要考虑是否账号昵称已经存在,不允许插入
//先校验账号昵称是否已经存在(username = ? or nickname = ?)
User exist = UserDao.checkIfExist(username,nickname);
//如果存在,响应返回错误信息,如果不存在,注册(插入数据)
//构造响应对象
JsonResult result = new JsonResult();
if (exist != null){
result.setOk(false);
result.setReason("账号或昵称已存在");
}else{
int n = UserDao.insert(user);
result.setOk(true);
}
//返回http响应给前端
resp.setContentType("application/json;charset=utf-8");
String body = WebUtil.write(result);
resp.getWriter().write(body);
}
}
2.dao包(数据库方法包)
package org.example.dao;
import org.example.model.Channel;
import org.example.util.DBUtil;
import org.junit.Test;
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 static List<Channel> selectAll() {
Connection c = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
c = DBUtil.getConnection();
String sql = "select * from channel order by id";
ps = c.prepareStatement(sql);
rs = ps.executeQuery();
List<Channel> channels = new ArrayList<>();
while (rs.next()){
//每一行数据转换为一个channel对象
Channel channel = new Channel();
int id = rs.getInt("id");
String name = rs.getString("name");
channel.setId(id);
channel.setName(name);
channels.add(channel);
}
return channels;
} catch (SQLException e) {
throw new RuntimeException("查询频道列表出错", e);
} finally {
DBUtil.close(c,ps,rs);
}
}
@Test
public void selectAllTest(){
System.out.println(selectAll());
}
}
package org.example.dao;
import org.example.model.Message;
import org.example.util.DBUtil;
import org.junit.Test;
import java.sql.*;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
public class MessageDao {
//查询用户上次注销后的历史消息
public static List<Message> query(Date logoutTime) {
Connection c = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
c = DBUtil.getConnection();
String sql = "select * from message";
if(logoutTime != null){
sql += " where send_time > ?";
}
ps = c.prepareStatement(sql);
if (logoutTime != null){
ps.setTimestamp(1, new Timestamp(logoutTime.getTime()));
}
rs = ps.executeQuery();
List<Message> messages = new ArrayList<>();
while (rs.next()){
Message m = new Message();
Integer id = rs.getInt("id");
String content = rs.getString("content");
Integer userId = rs.getInt("user_id");
String userNickname = rs.getString("user_nickname");
Integer channelId = rs.getInt("channel_id");
Timestamp sendTime = rs.getTimestamp("send_time");
//设置到message对象中
m.setId(id);
m.setContent(content);
m.setUserId(userId);
m.setUserNickname(userNickname);
m.setChannelId(channelId);
m.setSendTime(new Date(sendTime.getTime()));
messages.add(m);
}
return messages;
} catch (SQLException e) {
throw new RuntimeException("查询历史消息jdbc出错", e);
} finally {
DBUtil.close(c, ps, rs);
}
}
public static int insert(Message m) {
Connection c = null;
PreparedStatement ps = null;
try{
c = DBUtil.getConnection();
String sql = "insert into message (content,user_id,user_nickname,channel_id,send_time) values (?,?,?,?,now())";
ps = c.prepareStatement(sql);
ps.setString(1,m.getContent());
ps.setInt(2,m.getUserId());
ps.setString(3,m.getUserNickname());
ps.setInt(4,m.getChannelId());
m.setSendTime(new Date());
return ps.executeUpdate();
} catch (SQLException e) {
throw new RuntimeException("保存发送的消息jdbc出错", e);
} finally {
DBUtil.close(c, ps);
}
}
@Test
public void insertTest(){
Message m = new Message();
m.setUserId(1);
m.setContent("测试一下");
m.setUserNickname("哈哈");
m.setChannelId(2);
insert(m);
}
}
package org.example.dao;
import org.example.model.User;
import org.example.util.DBUtil;
import org.junit.Test;
import java.sql.*;
public class UserDao {
//注册检查账号,昵称是否已存在;登陆校验账号是否存在
public static User checkIfExist(String username, String nickname) {
Connection c = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
c = DBUtil.getConnection();
String sql = "select * from user where username=?";
if(nickname != null){
sql += " or nickname=?";
}
ps = c.prepareStatement(sql);
ps.setString(1,username);
if(nickname != null){
ps.setString(2,nickname);
}
rs = ps.executeQuery();
User query = null;
while(rs.next()){
query = new User();
Integer id = rs.getInt("id");
String loginNickname = rs.getString("nickname");
String password = rs.getString("password");
String head = rs.getString("head");
java.sql.Timestamp logoutTime = rs.getTimestamp("logout_time");
query.setId(id);
query.setUsername(username);
query.setPassword(password);
query.setNickname(loginNickname);
query.setHead(head);
if(logoutTime != null){
long l = logoutTime.getTime();
query.setLogoutTime(new java.util.Date(l));
}
}
return query;
} catch (SQLException e) {
throw new RuntimeException("注册检查账号昵称是否存在jdbc出错", e);
} finally {
DBUtil.close(c,ps,rs);
}
}
public static int updateLogoutTime(User loginUser) {
Connection c = null;
PreparedStatement ps = null;
try{
c = DBUtil.getConnection();
String sql = "update user set logout_time=? where id = ?";
ps = c.prepareStatement(sql);
ps.setTimestamp(1,new Timestamp(loginUser.getLogoutTime().getTime()));
ps.setInt(2,loginUser.getId());
return ps.executeUpdate();
} catch (SQLException e) {
throw new RuntimeException("更新用户上次注销时间jdbc时间出错", e);
} finally {
DBUtil.close(c, ps);
}
}
@Test
public void checkTest(){
//注册测试
// User user = checkIfExist("abc","");
User user = checkIfExist("abc",null);
System.out.println(user);
}
public static int insert(User user) {
Connection c = null;
PreparedStatement ps = null;
try{
c = DBUtil.getConnection();
String sql = "insert into user(username, password, nickname, head) values(?,?,?,?)";
ps = c.prepareStatement(sql);
ps.setString(1, user.getUsername());
ps.setString(2, user.getPassword());
ps.setString(3, user.getNickname());
ps.setString(4, user.getHead());
return ps.executeUpdate();
} catch (SQLException e) {
throw new RuntimeException("注册插入用户数据jdbc出错", e);
} finally {
DBUtil.close(c, ps);
}
}
@Test
public void insertTest(){
User u = new User();
u.setUsername("ttl");
u.setPassword("wjxbb");
u.setNickname("草莓甜甜圈");
insert(u);
}
}
3.model(实体类包)
package org.example.model;
public class Channel {
private Integer id;
private String name;
@Override
public String toString() {
return "Channel{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
package org.example.model;
public class JsonResult {
private boolean ok;//是否操作成功
private String reason;//ok=false,表示错误信息
private Object data;//ok=true,返回给前端的业务数据
@Override
public String toString() {
return "JsonResult{" +
"ok=" + ok +
", reason='" + reason + '\'' +
", data=" + data +
'}';
}
public boolean isOk() {
return ok;
}
public void setOk(boolean ok) {
this.ok = ok;
}
public String getReason() {
return reason;
}
public void setReason(String reason) {
this.reason = reason;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}
package org.example.model;
import java.util.Date;
public class Message {
private Integer id;
private Integer userId;
private String userNickname;
private String content;
private Integer channelId;
private java.util.Date sendTime;
@Override
public String toString() {
return "Message{" +
"id=" + id +
", userId=" + userId +
", userNickname='" + userNickname + '\'' +
", content='" + content + '\'' +
", channelId='" + channelId + '\'' +
", sendTime=" + sendTime +
'}';
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public Integer getUserId() {
return userId;
}
public void setUserId(Integer userId) {
this.userId = userId;
}
public String getUserNickname() {
return userNickname;
}
public void setUserNickname(String userNickname) {
this.userNickname = userNickname;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public Integer getChannelId() {
return channelId;
}
public void setChannelId(Integer channelId) {
this.channelId = channelId;
}
public Date getSendTime() {
return sendTime;
}
public void setSendTime(Date sendTime) {
this.sendTime = sendTime;
}
}
package org.example.model;
import java.util.Date;
public class User {
private Integer id;
private String username;
private String password;
private String nickname;
private String head;
private java.util.Date logoutTime;
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
", nickname='" + nickname + '\'' +
", head='" + head + '\'' +
", logoutTime=" + logoutTime +
'}';
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getNickname() {
return nickname;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
public String getHead() {
return head;
}
public void setHead(String head) {
this.head = head;
}
public Date getLogoutTime() {
return logoutTime;
}
public void setLogoutTime(Date logoutTime) {
this.logoutTime = logoutTime;
}
}
4.util(工具类包)
package org.example.util;
import com.mysql.jdbc.jdbc2.optional.MysqlDataSource;
import org.junit.Test;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
public class DBUtil {
private static volatile MysqlDataSource DS = null;
public static MysqlDataSource getDs(){
if(DS == null){
synchronized (DBUtil.class){
if(DS == null){
DS = new MysqlDataSource();
}
}
}
return DS;
}
static {
getDs();
DS.setURL("jdbc:mysql://127.0.0.1:3306/chatroom");
DS.setUser("root");
DS.setPassword("111111");
DS.setUseSSL(false);
DS.setUseUnicode(true);
DS.setCharacterEncoding("UTF-8");
}
public static Connection getConnection(){
try {
return DS.getConnection();
} catch (SQLException e) {
throw new RuntimeException("获取数据库连接失败",e);
}
}
//释放jdbc操作数据库资源
public static void close(Connection c, Statement s, ResultSet rs){
try {
if(rs != null) rs.close();
if(s != null) s.close();
if(c != null) c.close();
} catch (SQLException e) {
throw new RuntimeException("jdbc释放资源出错",e);
}
}
public static void close(Connection c, Statement s){
close(c, s, null);
}
@Test
public void Connection(){
System.out.println(getConnection());
}
}
package org.example.util;
import javax.servlet.http.HttpSession;
import javax.websocket.HandshakeResponse;
import javax.websocket.server.HandshakeRequest;
import javax.websocket.server.ServerEndpointConfig;
public class WebSocketConfigurator extends ServerEndpointConfig.Configurator {
//进行握手阶段的一些配置:在客户端与服务端建立websocket连接时,
// 服务端就可以使用这个配置来完成一些初始化工作
@Override
public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
//定义好配置,websocket建立连接时,先执行配置的代码,然后再执行OnOpen
HttpSession httpSession = (HttpSession) request.getHttpSession();
//保存HttpSession数据
if(httpSession != null){
sec.getUserProperties().put("HttpSession", httpSession);
}
//之后OnOpen建立websocket连接,就可以通过websocket的session来获取保存的HttpSession
}
}
package org.example.util;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.example.model.User;
import org.junit.Test;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.io.InputStream;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
public class WebUtil {
//服务端保存上传图像的一个路径
public static final String LOCAL_HEAD_PATH = "/root/upload";
private static final ObjectMapper M = new ObjectMapper();
//需要把消息中的日期,转换为年-月-日 时:分:秒
static {
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
M.setDateFormat(df);
}
//序列化,提供将Java对象转换为json字符串的方法
public static String write(Object o){
try {
return M.writeValueAsString(o);
} catch (JsonProcessingException e) {
throw new RuntimeException("转换Java对象为json字符串出错", e);
}
}
//反序列化,将json字符串转换为Java对象
//提供一个重载的方法:InputStream和String来转换
public static <T> T read(InputStream is,Class<T> clazz){
try {
return M.readValue(is,clazz);
} catch (IOException e) {
throw new RuntimeException("将json字符串转换为Java对象出错",e);
}
}
public static <T> T read(String str,Class<T> clazz){
try {
return M.readValue(str,clazz);
} catch (IOException e) {
throw new RuntimeException("将json字符串转换为Java对象出错",e);
}
}
//获取session中保存的用户信息
public static User getLoginUser(HttpSession session){
if(session != null){
return (User) session.getAttribute("user");
}
return null;
}
@Test
public void testWrite(){
User u = new User();
u.setId(1);
u.setUsername("abc");
u.setPassword("123456");
u.setNickname("张三");
System.out.println(write(u));
}
@Test
public void testRead(){
System.out.println(read("{\"id\":1,\"username\":\"abc\",\"password\":\"123456\",\"nickname\":\"张三\",\"logoutTime\":null}",User.class));
}
}