目录
一,简介
二,开发环境
三,涉及的技术
四,主要功能
1.注册功能
2.登录功能
3.异地登陆(第一次登录网页会被强制退出)
4.发送消息
5.接收消息
6.注销登录
五,准备工作
1.引入开发需要的依赖包
2.设计数据库以及表关系
3.设计实体类
4.设计工具类
数据库连接类
公共方法类
六.注册功能
1)从前端获取用户输入的数据
2)后端获取前端传输的数据
3)判断数据库中是否有相同的账号昵称
4)如果没有将注册数据插入并返回后端提示给前端
5)前端接受后端的响应(注册成功或者是失败)
七,登录功能
1)从前端接收用户输入的用户名和密码(以json字符串形式)
2)后端接收json字符串
3)进行用户登录验证并返回响应
4)前端返回登录成功或是失败的提示(成功还需要跳转)
八,在线聊天页面(核心)
1)对前端需要填充刷新的地方进行修改
2)静态添加群组和用户,完成点击并跳转新的群聊
3)Ajax发送请求,返回数据库中真实的群聊列表和当前的登录用户
4)后端验证是否是登陆用户并返回群聊列表
5)处理响应,并填充到前端
6)使用websocket技术进行前后端的消息推送
7)踢掉使用相同账号登录的网页用户
8)登录用户获取到从上次注销之后到现在登陆之间没有查看的消息(返回个前端并展示)
9)某个用户断开连接时保存退出时间,并删除session和更新到数据库
10)接收消息(即发送消息)
九,注销登录
十,部分包下的源码
1.api(接口包)
2.dao包(数据库方法包)
3.model(实体类包)
4.util(工具类包)
一,简介
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);}@Testpublic 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;}@Testpublic void writeTest(){User user = new User();user.setId(1);user.setUsername("abc");user.setPassword("123");user.setNickname("张三");System.out.println(write(user));}@Testpublic 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);//数据库保存图像路径:/+fileNameuser.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和inputMessageContentapp.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和inputMessageContentapp.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协议名wslet 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连接时,服务端就可以使用这个配置来完成一些初始化工作@Overridepublic void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {//定义好配置,websocket建立连接时,先执行配置的代码,然后执行onOpenHttpSession 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){//遍历历史消息,全部发送到当前用户的websocketString json = WebUtil.write(m);session.getBasicRemote().sendText(json);
}
9)某个用户断开连接时保存退出时间,并删除session和更新到数据库
@OnClosepublic 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 {@Overrideprotected 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 {@Overrideprotected 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 {@Overrideprotected 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 {@Overrideprotected 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@OnOpenpublic void onOpen(Session session) throws IOException {//先使用websocket session获取httpSessionHttpSession 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 sessionSession 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);}}@OnClosepublic void onClose(){System.out.println("断开连接");//关闭连接:删除map中的当前会话,记录当前用户的上次注销时间onlineUsers.remove(loginUser.getId());loginUser.setLogoutTime(new java.util.Date());int n = UserDao.updateLogoutTime(loginUser);}@OnErrorpublic 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 {@Overrideprotected 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);//数据库保存头像路径:/+fileNameuser.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);}}@Testpublic 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);}}@Testpublic 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);}}@Testpublic 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);}}@Testpublic 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;@Overridepublic 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,返回给前端的业务数据@Overridepublic 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;@Overridepublic 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;@Overridepublic 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);}@Testpublic 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连接时,// 服务端就可以使用这个配置来完成一些初始化工作@Overridepublic void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {//定义好配置,websocket建立连接时,先执行配置的代码,然后再执行OnOpenHttpSession 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;}@Testpublic void testWrite(){User u = new User();u.setId(1);u.setUsername("abc");u.setPassword("123456");u.setNickname("张三");System.out.println(write(u));}@Testpublic void testRead(){System.out.println(read("{\"id\":1,\"username\":\"abc\",\"password\":\"123456\",\"nickname\":\"张三\",\"logoutTime\":null}",User.class));}
}