web聊天功能如何设计?
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了web聊天功能如何设计?相关的知识,希望对你有一定的参考价值。
我现在要做一个聊天的功能,有群聊和单聊。用nodejs写的,我现在是把所有已登录的用户存入一个数组里,单聊天的话就是从这个数组中直接取出对应的对象,服务器就只要将消息转发给取出的对象。而群聊就要先去数据库查出该群组中有哪些成员,然后再从数组中取出这些用户,在进行消息转发,每条消息都要查一次数据库。感觉这样很不好,请问有什么好的设计方法解决这个问题?
参考技术A java中的jsp技术,php技术、asp.net技术都可以,只要用到session就可以了,每次聊天,被聊天对象的session也存放聊天的内容,显示层方面用ext.js就可以了~~追问我现在就是把所有客户端与服务器连接的对象放在一个数组里,群聊的时候是去查数据库该群有哪些用户,在从数组里取出来,再把消息发给他们。这样做比较麻烦,有没有什么好的方式能解决不用频繁的查数据库?
Web聊天室
目录
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响以上是关于web聊天功能如何设计?的主要内容,如果未能解决你的问题,请参考以下文章