实现一个在线网页的聊天室

Hello,今天给大家带来的是我的一个Web项目的开发过程的相关步骤,这个项目实现的功能是一个Web在线聊天室,简单的来说就是实现在网页版的聊天框,能够实现对于用户信息进行注册,登录,在网页上收发消息的功能。
这个项目也实现了我和别的小伙伴一起实现在线聊天的功能,这是我实现的Web聊天室网页链接地址:[http://47.100.138.17:8080/chatroom/index.html]
感兴趣的小伙伴可以注册登录呦在网上尝试一下聊天。
话不多说,我们直接开始对于开发过程进行实现吧:

第一步:首先是第一步对于需求分析创建需要的数据库表单

对于用户使用Web聊天室实现来说,需要用户用自己的账号密码登录,同时有自己设置的昵称信息头像信息;在登录之后有聊天室需要提供频道来使用户在其中进行交流;在交流的时候需要用户去发送消息,不同的用户会在不同的时间发不同的消息内容
因此呢,根据这些需求就设计了
User(用户表)、channel(频道表)、message(消息表)
三个表单信息:

create table user(id int primary key auto_increment,username varchar(15) not null unique comment '账号',password varchar(15) not null comment '密码',nickname varchar(20) not null comment '昵称',head varchar(50) comment '头像url(相对路径)',logout_time datetime comment '退出登录的时间'
) comment '用户表';
create  table  channel(id int primary key auto_increment,name varchar(20) not null unique comment '频道名称'
)comment '频道';
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 '发送的消息记录';

三个表单的关系在navicat的EP图中表现是如下的:

第二步:创建一个Mavaen项目,将三个表单的实体类放在Model中


根据MySQL中数据库设计的信息在实体类中实现其属性,利用@Getter、@Setter、@ToString注解快速实现对于类相关方法的生产(需要导入lombok的依赖包)。


第三步:设计关键性的工具类:数据库操作的JDBC工具类;json和java对象转换,session操作的Web工具类

(1)对于JDBC的工具类

在JDBC工具类设计中提供连接连接数据库和释放数据库资源的关键方法。同时为保证线程安全部分功能使用懒汉式的双重校验锁的形式来实现。实现代码如下:

//和数据库连接的工具类
public class DBUtil {//定义一个单例的数据源来连接对象private static MysqlDataSource DS=null;//懒汉式的双重校验锁的形式private static MysqlDataSource getDS(){if (DS==null) {synchronized (DBUtil.class) {if (DS == null) {//确保只有当前的操作能够访问数据库DS = new MysqlDataSource();//设置数据库连接的属性值DS.setURL("jdbc:mysql://127.0.0.1:3306/onlinechatroom");DS.setUser("root");DS.setPassword("123456");DS.setUseSSL(false);DS.setUseUnicode(true);DS.setCharacterEncoding("utf-8");}}}return DS;}//数据库的连接方法实现,数据库的关闭方法实现public static Connection getConnection(){try {return getDS().getConnection();} catch (SQLException e) {throw new RuntimeException("数据库连接异常",e);}}public static void close(Connection c , Statement s){close(c,s,null);}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("数据库释放资源出错",e);}}
}

(2)对于Web工具类

在Web工具类设计中提供Java对象转为json字符串,json字符串转为Java对象,获取当前登录用户的session信息的功能。为保证线程安全,使用懒汉式的双重校验锁的写法。具体实现代码的如下:

public class WebUtil {public static final String LOCAL_HEAD_PATH="E://TMP";//从json中读取到java对象,则jackson库中通过ObjectMapper实现了将数据集或对象转换的实现。private static ObjectMapper M=null;//使用懒汉式的双重校验锁的单例模式private static ObjectMapper getMapper(){if (M==null){synchronized (WebUtil.class){if (M==null){M=new ObjectMapper();//SimpleDateFormat是日期工具类能够实现将文本和日期的双重转化SimpleDateFormat df=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//设置自定义的时间结构M.setDateFormat(df);}}}return M;}//实现将 JAVA对象————>json字符串  的方法public static String Write(Object o){try {//通过return getMapper().writeValueAsString(o);} catch (JsonProcessingException e) {throw new RuntimeException("将JAVA对象转为json字符串时出错",e);}}//反序列化设计:将JSON字符串————>java对象//两个重载的方法:InputStream 和 String 来进行转换//inputStream 字节流输入 读取的数据public static <T> T read(InputStream inputStream,Class<T> tClass){try {return getMapper().readValue(inputStream,tClass);} catch (IOException e) {throw new RuntimeException("将json字符串转化为JAVA对象时出错",e);}}public static <T> T read(String string ,Class<T> tClass){try {return getMapper().readValue(string,tClass);} catch (IOException e) {throw new RuntimeException("将json字符串转化为JAVA对象时出错", e);}}//对于session的操作  获取session中的用户信息public static User getLoginUser(HttpSession session) {if (session != null) {//获取登录Session中的user信息   由于getAttribute 返回值是任意类型的 所以需要进行类型的强制转型//获取的键和登录时设置的键一样return (User) session.getAttribute("user");}return null;}
}

第四步:实现用户的注册功能

(1)对于用户注册的前端处理实现

在前端中需要创建相应的标签来让用户将自己的用户名,密码,昵称等相关信息输入当中,在标签中设置required则表示必须填写的内容。
用户将需要填写的内容在浏览器上输入完毕之后由前端页面将用户信息获取保存, 将前端中标记好的相关用户信息,放在一个formdata表单中进行存储。创建好格式,调用一个ajax请求,将当前页面的信息进行上传后端,用callback函数做接收信息的处理,如果成功就返回到登录页面,如果是失败就跳提示注册失败的原因。

举例:对于头像文件信息,设置为一个event事件,将event事件传入下方中showHead中。通过获取其中的文件将其保存在vue框架中的head中,在将文件信息写入到body中发送。
实现代码如下:

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>在线聊天室</title><link rel="stylesheet" href="../css/common.css"><link rel="stylesheet" href="../css/index.css">
</head>
<body><div id="app" v-cloak><form @submit.prevent="register()"><h3>聊天室注册</h3><div class="row"><span>用户名</span>
<!--      添加required表示为必填字段,不填写的话会发生报错          --><input type="text" v-model="username" required></div><div class="row"><span>密码</span><input type="password" v-model="password" required></div><div class="row"><span>昵称</span><input type="text" v-model="nickname" required></div><div class="row"><span>头像</span><!-- 绑定文件选择文件 改变原始数据添加新数据的改变事件  $event是vue的事件  就是下方函数e  传入的事件--><input type="file" accept="image/*" @change="showHead($event)"><img :src="head.src" v-if="head.src"></div>
<!--       后台内容显示报错信息 若果没有则显示为空     --><div class="error-message">{{ errorMessage }}</div><div class="row"><!-- 注册按钮点击提交信息                --><input type="submit" value="注册"></div><div class="row-right"><a href="../index.html">返回登录</a></div></form></div>
</div>
</body>
<script src="../js/util.js"></script>
<script src="../js/vue.js"></script>
<script>let app = new Vue({el: "#app",data: {errorMessage: "",username: "",password: "",nickname: "",head: {file: "",  //在这里保存选择的文件src: "",   //选择好图片还没上传,客户端本地有一个的图片},},methods: {//注册选择头像,显示预览图片//e是传入的事件对象showHead: function (e){//获取选择的文件: 通过e.target.file 可获取 在上面标签中的input中的 @changlet headFile = e.target.files[0];//保存信息  用上面的vue框架信息里面的file地址保存图片app.head.file= headFile;//将文件的信息转化为url  调用Url中的信息app.head.src=URL.createObjectURL(headFile);},register: function (){//注册功能的实现//使用FormData对象作为form-data格式上传的数据//创建FormData格式的对象来调用该形式let formData=new FormData();//添加数据,利用append将相关参数进行设置  使用 k  v 模型 k参数和APP中设置的参数一样formData.append("username",app.username);formData.append("password",app.password);formData.append("nickname",app.nickname);//如果上传了头像的信息if (app.head.file){//将头像的信息传入当中formData.append("headFile",app.head.file);}ajax({method: "post",//当前html位置是在/views/register.htmlurl: "../register",//当前html是在/views/register.html//上传文件,使用form-data格式,但是不能设置这个Content-Type//body中放置的信息 上传文件使用的form-data格式body: formData,//返回响应callback: function(status, responseText){//表示服务器返回的相应状态码出错// console.log(responseText);//查看一下响应正文的数据是否符合业务的,可以抓包(建议)if(status != 200){alert("出错了,响应状态码:"+status);return;}//表示正常返回200 就进行接下来的操作//响应正文的地方let body = JSON.parse(responseText);//响应正文if(body.ok){alert("注册成功");//跳转到登陆的页面window.location.href = "../index.html";}else{// //注册失败 显示错误,根据后端的reason反馈信息app.errorMessage = body.reason;}}});}},});
</script>
</html>

(2)对于用户注册的后端处理实现

tips:在写后端的响应之前做一个测试,利用抓包工具验证是否能够正常的发送请求和响应。)
接下来进行对于RegisterServlet的开发,首先设置Servlet注解获取前端传过来的formdata表单数据,在其次对于传过来的数据进行构造成一个实体类,存到数据库中。在针对于头像文件获取的时候,需要先获取存储照片的后缀名,将其保存下来创建一个时间戳相关的随机字符串构成重命名,最后形成新的文件保存在本地的文件中。
最后调用用户表的相关操作(封装在UserDao类中)将数据库的信息存储完毕之后就可以返回后续的响应,但是需要构造一个响应对象,需要设置JsonResult返回的对象类,设置好返回格式,返回信息。最后通过resp的相关API来实现对于响应数据的返回。
实现的代码如下:

//做前端注册页面的响应
@WebServlet("/register")
@MultipartConfig  //FormData上传数据需要
public class RegisterServlet extends HttpServlet {//对于前端页面发送的POST请求数据做后端解析@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {//设置请求正文的编码req.setCharacterEncoding("UTF-8");//获取前端传递过来的FormData表单格式数据//在这里需要添加一个 @MultipartConfig  的注解获取form data格式的数据//用String创建对象来接受传递过来的参数  通过请求的getParameter来获取String username=req.getParameter("username");String password=req.getParameter("password");String nickname=req.getParameter("nickname");//对于头像文件的获取,前端传递可能为空//如果存在就从数据中获取信息Part headFile=req.getPart("headFile");//将接受到的数据形成一个User对象 进一步的将User对象传递到数据库中 形成注册User user= new User();user.setUsername(username);user.setPassword(password);user.setNickname(nickname);//对于传递过来的头像文件需要进行判断是否为空才能进行存储if (headFile!=null){//传递头像文件的方法:将文件保存在服务端的一个路径中//先获取上传文件的后缀名称  getSubmittedFileName() 获取其原始名称String filename=headFile.getSubmittedFileName();//找到最后一个点的索引位置,并返回  能够获取其中的照片格式 如JPG 或者JPEGString suffix=filename.substring(filename.lastIndexOf('.'));//添加一个随即字串和时间戳有关 在拼接上后续的照片格式,实现了对于文件的重命名filename= UUID.randomUUID()+suffix;//保存文件的路径headFile.write(WebUtil.LOCAL_HEAD_PATH+"/"+filename);//数据库保存头像的路径user.setHead("/"+filename);}//将数据信息保存到数据库中:判断其是否存在用户名和账号重复User exist = UserDao.checkIfExist(username,nickname);//后续的逻辑信息,如果数据存在,就返回错误的信息, 如果不存在就 进行注册功能 并返回响应//此时需要一个返回格式  构建model——JsonResult//构造响应的对象JsonResult result=new JsonResult();if (exist!=null) {//表示查询不为空,用户信息存在//result.setOk(false); 初始的布尔值为false所以可以不用给设置result.setReason("账号或者昵称已存在");}else {//表示查询为空,执行数据库的插入信息功能int n= UserDao.insert(user);result.setOk(true);}//接下来应该返回HTTP响应给前端数据resp.setContentType("application/json; charset=utf-8");//需要将java对象转为json的形式String body=WebUtil.Write(result);resp.getWriter().write(body);}
}

第五步:对于数据库三个类的JDBC操作实现

(1)对于用户表的工具类实现

在实现数据存储到数据库中时需要去开发UserDao这个实体类用来存放用户信息到数据库,因此对于user用户表的查询,插入,修改操作是经常性的需要去进行完成。所以在这个user用户表的工具类中实现了对于插入、查询、修改的操作方法。
实现的代码如下:

//用户表数据库相关的操作
public class UserDao {//注册:检查账号、昵称是否存在  实现JDBC操作public static User checkIfExist(String username,String nickname){Connection c=null;PreparedStatement preparedStatement=null;ResultSet rs=null;try {c= DBUtil.getConnection();String sql="select * from user where username=?";if (nickname!=null){sql+="or nickname=?";}//将上面的预编译的的占位符进行替换数据preparedStatement=c.prepareStatement(sql);preparedStatement.setString(1,username);if (nickname!=null){preparedStatement.setString(2,nickname);}//执行查询操作,返回结果集进行接收rs=preparedStatement.executeQuery();//准备查询的User对象User queryUser=null;while (rs.next()){queryUser=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");queryUser.setId(id);queryUser.setUsername(username);queryUser.setPassword(password);queryUser.setNickname(loginNickname);queryUser.setHead(head);if (logoutTime!=null){//考虑数据是不是为空的情况long l=logoutTime.getTime();queryUser.setLogoutTime(new java.util.Date(l));}}return queryUser;} catch (SQLException e) {throw new RuntimeException("注册检查账号昵称是否存在JDBC出现错误",e);}finally {DBUtil.close(c,preparedStatement,rs);}}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(3, user.getHead());return ps.executeUpdate();} catch (SQLException e) {throw new RuntimeException("插入数据时出现错误",e);}finally {DBUtil.close(c,ps);}}//数据库修改用户的退出时间jdbc代码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);//替换时间站位符  获取用户中存储的退出时间long currentTime=loginUser.getLogoutTime().getTime();ps.setTimestamp(1,new Timestamp(currentTime));ps.setInt(2,loginUser.getId());return ps.executeUpdate();} catch (SQLException e) {throw new RuntimeException("更新用户上次注销时间出错", e);} finally {DBUtil.close(c,ps);}}}

(2)对于消息表的工具类实现

对于用户的发送的消息需要存储到数据库的消息表字段中,同时上线的用户也需要获取历史的消息,因此对于消息表需要实现插入和查询的方法。实现代码如下:

//对于数据库Message的JDBC操作
public class MessageDao {//对于Message操作有查询和插入操作//给用户放回从退出时间开始算起的保存的历史消息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中rs=ps.executeQuery();List<Message> messages=new ArrayList<>();while (rs.next()){//构建Message对象来接收rs中的消息Message getOldMessage=new Message();getOldMessage.setId(rs.getInt("id"));getOldMessage.setUserId(rs.getInt("user_id"));getOldMessage.setUserNickname(rs.getString("user_nickname"));getOldMessage.setChannelId(rs.getInt("channel_id"));getOldMessage.setContent(rs.getString("content"));getOldMessage.setSendTime(rs.getTimestamp("send_time"));//将获取的历史消息对象传到消息队列中messages.add(getOldMessage);}//将查询到的历史消息返回到队列中进行返回return messages;} catch (SQLException e) {throw new RuntimeException("查询历史消息出错",e);}finally {DBUtil.close(c,ps,rs);}}//插入数据操作public static int insert(Message m){Connection c=null;PreparedStatement ps=null;try {c=DBUtil.getConnection();//将接收到的用户发送的消息保存起来//接收到的用户信息的包含的字段 有 内容 用户的昵称 用户的id 频道号  发送的时间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.getId());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);}}
}

(3)对于频道表的工具类实现

用户在进入界面的时候能够获取到所有频道的信息,因此对于频道中所有频道需要实现查询的方法。实现代码如下:

public class ChannelDao {//实现查询返回 channels表单数据即可public static List<Channel> selectAll() {Connection c=null;Statement ps=null;ResultSet rs=null;try {//对于上述进行赋值c= DBUtil.getConnection();//展示的频道框为第一个String sql ="select * from channel order by id";ps=c.createStatement();rs=ps.executeQuery(sql);//用来接受所有的channelList<Channel> channels=new ArrayList<>();while (rs.next()){//获取的数据很多个,将每一个数据转为channel对象Channel channel=new Channel();//Channel的关键字段为 id  nameint id=rs.getInt("id");String name=rs.getString("name");channel.setId(id);channel.setName(name);//将数据添加的到List结构中的Channels里面channels.add(channel);}return  channels;} catch (SQLException e) {throw new RuntimeException("查询频道列表时出错",e);}finally {//释放资源DBUtil.close(c,ps,rs);}}}

第六步:对于登录功能的实现

(1)对于登录页面的前端实现

用户在登录页面登录自己的用户名和密码信息后,前端进行获取,前端获取后通过ajax发送到后端进行解析,通过回调函数来确定是否是注册账号,如果是就进行登录,如果不是就提示输入有误。
实现的代码如下:

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>在线聊天室</title><link rel="stylesheet" href="css/common.css"><link rel="stylesheet" href="css/index.css">
</head>
<body><div id="app" v-cloak><form @submit.prevent="login()"><h3>聊天室登录</h3><div class="row"><span>用户名</span><input type="text" v-model="username" required></div><div class="row"><span>密码</span><input type="password" v-model="password" required></div><div class="error-message">{{ errorMessage }}</div><div class="row"><input type="submit" value="登录"></div><div class="row-right"><a href="views/register.html">注册</a></div></form></div>
</body>
<script src="js/util.js"></script>
<script src="js/vue.js"></script>
<script>let app = new Vue({el: "#app",data: {errorMessage: "",username: "",password: "",},methods: {//实现前端的登录功能login: function (){ajax({method: "post",url: "login",contentType:"application/json",//转化为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;}}});},},});
</script>
</html>

(2)对于登录页面的后端实现

首先在后端是对于前端的登录页面发来的请求的进行获取信息,通过获取前端的json字符串的user信息查询数据库来实现对于用户信息的校验,如果存在就创建session保存用户信息,不存在就提示用户有错误。最后将查询的相关用户的信息返回到当前页面的响应中。实现的代码如下:

//对于后端登录页面的信息的功能实现
@WebServlet("/login")
public class loginServlet extends HttpServlet {//对于前端发起的Post方法做一个返回响应@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {//设置一个请求正文的编码req.setCharacterEncoding("utf-8");//解析传递过来的json数据  调用WebUtil中的读取方法即可   将输入流中的数据转为Class对象User input = WebUtil.read(req.getInputStream(),User.class);//对于账号密码的检验//先检验账号是否存在,如果不存在提示,如果存在,在校验密码   使用UserDao 中对于用户名和昵称的检验方法User exist= UserDao.checkIfExist(input.getUsername(),null);//准备返回的Web响应 调用JsonResult 实现即可JsonResult result=new JsonResult();//对于密码进行验证if (exist==null){result.setReason("您输入的账号不存在");}else{//对于密码进行判断//exits是查询的username对应在数据库中的信息   input.getPassword是用户在页面上登录的信息if (!exist.getPassword().equals(input.getPassword())){//表示校验失败 登录密码错误  设置返回原因result.setReason("您输入的密码有误,请重新输入");}else {//校验成功 需要给用户创建session保存信息HttpSession session=req.getSession();//保存数据库查询到的用户信息session.setAttribute("user",exist);result.setOk(true);}}//返回响应数据resp.setContentType("application/json; charset=utf-8");String body=WebUtil.Write(result);//把body数据写进当前页面的响应中resp.getWriter().write(body);}
}

第七步:对于聊天页面的功能实现

(1)对于聊天页面的前端实现

前端处理:先构建频道信息,对于每个对话框进行设计,能够实现基础的点击对话框就能跳转到非当前对话框。同时在页面加载的时候,需要对于对话框的列表进行获取和返回。根据与后端传递过来的channel参数信息来设置前端的响应,对于Channel中的属性继续的进行实现。对于消息推送功能的实现,使用WebSocket的方式实现,之所以不用Http是因为Http对于客户端和服务端之间需要一发一收后才能进行下一步操作,不能实现客户端和服务端全双工的特性,而WebSocket则实现了该特性,对于WebSocket,则是基于TCP协议,首先发送Http请求建立连接(目的是双方确定后续使用的协议和秘钥),后续使用Websocket协议(在应用层使用相同的数据格式来发送、接收数据)来实现收发数据。在前端使用socket的相关api来完成对于消息的处理。
前端处理代码实现:

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>在线聊天室</title><link rel="stylesheet" href="../css/common.css"><link rel="stylesheet" href="../css/message.css">
</head>
<body><div id="app" v-cloak><div id="nav"><!-- 在这里由 current.userNickname  换为 currentUser.nickname 和下端vue 进行对齐 --><span>欢迎进入在线聊天室!{{ currentUser.nickname }}</span><!-- 注销链接的实现   实现后端注销功能  --><a href="../logout">注销</a></div><div id="container"><div id="channel-list"><!--  这里的currentChannel.id和下面当前频道id需要对应                 --><!--  @click="changeChannel(c) 应用的是vue框架中的点击事件  c就是传递进去的对象每一个channel的对象 然后进行遍历  --><div :class="c.id== currentChannel.id ? 'channel-row-checked' : 'channel-row-unchecked'" v-for="c in channels" :key="c.id" @click="changeChannel(c)" ><!--    显示是当前频道的昵称  --><span class="channel-item"> {{ c.name }}</span><span v-if="c.unreadCount" class="unread">{{ c.unreadCount }}</span></div></div><div id="dialog"><div id="dialog-history"><div class="dialog-row" v-for="m in currentChannel.historyMessages" :key="m.id"><div class="dialog-date">{{ m.sendTime }}</div><div class="dialog-user">{{ m.userNickname }}</div><!-- 三目表达式来进行 判断当前的 发送的消息是否是用户自己发送的消息          --><div :class="m.userId==currentUser.id ? 'dialog-current-content' : 'dialog-other-content'">{{ m.content }}</div></div></div><!--  展示的消息内容 字段为currentChannel.inputMessageContent--><!-- @keyup="checkIfSend($event)" vue绑定按键弹起事件,$event就是事件对象 --><textarea id="dialog-content" v-model="currentChannel.inputMessageContent" @keyup="checkIfSend($event)"></textarea><div id="dialog-send"><button @click="sendMessage">发送(S)</button></div></div></div></div>
</body>
<script src="../js/util.js"></script>
<script src="../js/vue.js"></script>
<script>let app = new Vue({el: "#app",data: {websocket:null,currentUser: {//当前登录用户nickname: "",head: "",},//设置频道信息  写静态数据验证前端代码,后续从servlet的响应中获取数据channels: [{id: 1,name: "带刀侍卫群",//存放历史消息的地点historyMessage: [],//输入框的内容inputMessageContent: "",unreadCount: 0,},{id: 2,name: "门前麻将群",//存放历史消息的地点historyMessage: [],//输入框的内容 每个频道的输入框内容不一样inputMessageContent: "",unreadCount: 0,},],//当前频道currentChannel: {id: 1,name: "带刀侍卫群",//存放历史消息的地点historyMessage: [],//输入框的内容inputMessageContent: "",unreadCount: 0,},},methods: {//点击切换频道的功能实现changeChannel: function (channel) {//先判断点击的频道是否是当前频道if (channel.id != app.currentChannel.id) {app.currentChannel = channel;}//切换到一个频道后,滚到最后,并且未读消息=0app.scrollHistory();},//从后端获取频道列表 再设置到vue的变量中,页面就可以跟着去改变getChannels: function () {//发送AJAX请求获取数据ajax({method: "get",//获取频道列表url: "../channelList",callback: function(status, responseText){// console.log(responseText);//查看一下响应正文的数据是否符合业务的,可以抓包(建议)if(status != 200){alert("出错了,响应状态码:"+status);return;}//设置响应正文let body = JSON.parse(responseText);//响应正文//后端ChannelListServlet中返回的是{user:{},channels:[]}//返回的Channel是不带historyMessage(历史消息)   inputMessageContent(输入框消息)//需要给返回的数据添加上当前的消息//当前用户app.currentUser = body.user;for(let i=0; i<body.channels.length; i++){//添加历史消息  为数组的形式body.channels[i].historyMessages = [];//会话框的消息置为空body.channels[i].inputMessageContent = "";body.channels[i].unreadCount = 0;//默认切换到第一个频道if(i == 0){//设置现在的界面为初始的频道值app.currentChannel = body.channels[0];}}//将数据库中的响应值传递给前端中进行实现。app.channels = body.channels;//接收消息app.initWebsocket();}});},//接受消息功能initWebsocket: function () {//在这里面写入websocket的连接获取的消息//创建一个websocket对象,用来创建该连接,客户端收发数据//websocket的url格式      协议名://ip:port/contextPath/资源路径// websocket协议名 为ws     contextPath是部署的项目名/项目路径//获取前端当前页面的url的协议名  为 http:(有:)let protocal = location.protocol;//获取当前地址栏的url:http://xxx.xxx.xxx.xxx:8080/chatroom/views/message.htmllet url = location.href;//url:http://xxx.xxx.xxx.xxx:8080/chatroom/views/message.html//字符串.indexOf(str), 返回第一个匹配str的索引位置//截取后的url为   xxx.xxx.xxx:8080/chatroomurl = url.substring((protocal + "//").length, url.indexOf("/view/message.html"))//创建websocket的连接url//新的url为   ws://xxx.xxx.xxx.xxx:8080/chatroom/messagelet ws = new WebSocket("ws://" + url + "/message");//此时可以进行连接//绑定事件,事件发生的时候,由浏览器自动调用事件函数//建立连接事件:ews.onopen = function (e) {console.log("客户端连接")}//关闭连接  可能由服务器关闭或者先由客户端进行关闭ws.onclose = function (e) {let reason = e.reason;console.log("close:" + reason)if (reason) {alert(reason)}}//发生错误事件ws.onerror = function (e) {console.log("websocket出错")}//接收消息事件ws.onmessage = function (e) {//服务器推送消息给客户端执行该函数//获取 e  中推送的消息//消息对象let m = JSON.parse(e.data);//对于channels数组进行遍历for (let channel of app.channels) {if (channel.id == m.channelId) {//数组放置元素的方法  将获取的消息放在历史消息中channel.historyMessage.push(m);//如果是当前的频道,就滚动到最后if (m.channelId == app.currentChannel.id) {app.scrollHistory();} else {//不是当前频道,未读消息数++channel.unreadCount++;}}}}//刷新/关闭页面,需要关闭websocketwindow.onbeforeunload = function (e) {//主动关闭websocket连接ws.close();}app.websocket = ws;},//当前频道接收到消息后,滚动到最下面scrollHistory: function () {app.$nextTick(function () {//异步操作:vue渲染元素css,数据完成后,在执行//当前频道历史消息divlet history = document.querySelector("#dialog-history");//scrollTop是滚动条顶部   scrollHeight是整个滚动div的高//滚动条的设置history.scrollTop = history.scrollHeight;});//将未读消息设为0app.currentChannel.unreadCount = 0;},//绑定当前频道的键盘发送消息  为CTRL+ENTERcheckIfSend: function (e) {if (e.keyCode == 13 && e.ctrlKey) {//发送消息app.sendMessage();}},//发送消息sendMessage: function () {let content = app.currentChannel.inputMessageContent;if (content) {//表示消息不为空  利用websocket来进行发送消息//后台需要插入数据库一条客户端发送的消息//id,user_id, user_nick,send_time(后端可以获取到,不用发),channel_id, content...)//注意:后端是将json字符串反序列化为message对象(驼峰式)app.websocket.send(JSON.stringify({//发送当前的频道 idchannelId: app.currentChannel.id,//将当前的输入文本内容设置进去content: content,}));//清空当前频道的消息app.currentChannel.inputMessageContent = "";}},},});//在页面初始化的时候,就需要获取频道列表app.getChannels();
</script>
</html>

(2)对于聊天页面的后端实现

先对于ChannelList类进行设计,在后端返回响应之前需要对于用户信息的session进行确认,和查询的数据库信息进行比对,如果不同,则进行禁止访问的状态码设置。如果查询成功存在的话,查询数据库的所有信息返回到List中,再将数据写入Json字符串中,最后将数据写在响应中进行返回。
对于数据库查询所有的Channels信息用ChannelDao的一个方法来进行实现。然后对于后端messsage的功能实现,该功能是基于websocket实现所以需要导入Websocket的依赖包:

javax.websocket
javax.websocket-api
1.1
provided

接下来对于后端实现对于messageEndpoint的相关功能实现,构造能够保存在线用户信息的onlineUsers数据结构来,构造能够实现消息信息存储的数据结构,并创建线程不断的从消息队列中取出消息发给每一个用户,最后实现对于基于socket的注解的open 、close、error、message的方法实现。对于OnOpen方法需要判断用户是否登录,如果登录就需要踢掉上一个该账户的用户,来实现一个用户同时只能一个人来进行登录的情况,同时能够将之前的历史数据进行获取。对于OnClose方法在实现的时候需要去从在线用户中去除掉当前的用户信息同时,将数据库的用户退出时间做好记录,在此调用的是DBUtil中的修改用户退出时间的方法。对于OnError方法,表示出现错误,当前用户的登录信息被清除,重新进行登录。对于OnMessage方法,将用户发送的消息通过socket的连接发送过来进行接收,在这里将消息存储到消息队列中,同时将此条消息对于所有的在线用户进行推送过去。
具体的代码实现如下:

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.WebUtil;
import org.example.util.WebsocketConfigurator;import javax.servlet.http.HttpSession;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingQueue;//使用con
@ServerEndpoint(value = "/message",configurator = WebsocketConfigurator.class)
public class MessageEndpoint {//传入的session信息private Session session;//当前登录的用户对象private User loginUser;//用数据结构保存所有客户端的websocket会话//根据配置类的信息,则是用map结构保存所有websocket会话,判断是否相同账号重复登录,就比较方便(key: userId, value: Session)//基于ConcurrentHashMap多线程安全的方式保存在线用户的Sessionprivate static Map<Integer, Session> onlineUsers = new ConcurrentHashMap<>();//创建消息队列,将用户的消息存放在队列中//LinkedBlockingQueue是一个链表实现的,无边界阻塞队列private static BlockingQueue<Message> messageQueue = new LinkedBlockingQueue<>();//消费消息:创建一个或多个线程,从消息队列中一个一个拿,每个都转发到所有在线用户static {new Thread(new Runnable() {@Overridepublic void run() {//不断的取出消息while (true){try {//取出消息队列中的消息Message m=messageQueue.take();for (Session session : onlineUsers.values()){//将登录用户的Session信息保存到创建的session中//将session数据写入到字符串中String json= WebUtil.Write(m);//调整为包含插入的字段session.getBasicRemote().sendText(json);}}catch (InterruptedException e){e.printStackTrace();}catch (IOException o){o.printStackTrace();}}}}).start();}@OnOpenpublic void onOpen(Session session) throws IOException {System.out.println("建立连接");//验证一下是否登录,如果登录踢掉一个已登录的同一个用户//获取当前用户的httpsession信息HttpSession httpSession = (HttpSession) session.getUserProperties().get("HttpSession");//获取当前httpsession的User对象User user=WebUtil.getLoginUser(httpSession);//对于user进行判断if (user==null){//用户没有登录:关闭连接   返回一个socket的CloseReasonCloseReason reason=new CloseReason(CloseReason.CloseCodes.NORMAL_CLOSURE,"没有登录,禁止访问");return;}//对于session 进行判断//踢掉使用相同账号登录的用户 先对于已经在线的用户进行查询操作 当前登录的用户存放在Map集合中Session preSession =onlineUsers.get(user.getId());if (preSession!=null){//表示在所有的key中有用户的id//执行踢掉之前登录的用户CloseReason closeReason=new CloseReason(CloseReason.CloseCodes.NORMAL_CLOSURE,"账号在别处登录");//对于前一个用户进行剔除preSession.close(closeReason);}//将用户信息更新  踢出上一个用户操作完成this.loginUser=user;this.session=session;//用户信息保存在 在线用户数据中onlineUsers.put(user.getId(),session);//到此websocket连接,客户端需要接收所有的历史消息//调用MessageDao的查询方法,查询从上一次退出到现在的历史消息List<Message> messages=MessageDao.query(user.getLogoutTime());//获取历史消息for (Message m: messages){//变量获取到的历史消息 并发送到当前的用户账号中//此时是websocket在接收消息,所以需要用json格式,将数据进行转化String json =WebUtil.Write(m);session.getBasicRemote().sendText(json);}}@OnClosepublic void onClose(){System.out.println("断开连接");//删除在线用户信息onlineUsers.remove(loginUser.getId());//记录下退出的时间loginUser.setLogoutTime(new java.util.Date());int n = UserDao.updateLogoutTime(loginUser);}@OnErrorpublic void onError(Throwable t){t.printStackTrace();//出现异常删除当前用户登录的状态onlineUsers.remove(loginUser);}@OnMessagepublic void onMessage(String message) {//首先将接收到的json消息转为一个message对象//调用WebUtil中的读取方法Message m=WebUtil.read(message,Message.class);//将当前用户的信息传到构建的Message对象中m.setUserId(loginUser.getId());m.setUserNickname(loginUser.getNickname());System.out.println("收到消息:"+message);//将接收到消息保存到数据库int n = MessageDao.insert(m);//同时将消息放到在线的消息队列中,进行推送(这是服务端主动进行发送)//将消息放到阻塞队列中try {messageQueue.put(m);} catch (InterruptedException e) {e.printStackTrace();}}
}

第八步:对于注销功能的实现

在页面上有一个注销按钮,用户点击后就能实现注销功能。实现该功能是前端发送一个请求,后端执行一个servlet响应即可,在响应前需要核对用户的信息是否存在,不存在就禁止访问。如果存在就删除当前的session信息同时从当前的在线用户中退出,记录下退出的时间在数据库中进行更新,以便下次能够获取未读的历史消息,最后重定向初始的登录页面,注销功能完成。
实现的代码如下:

@WebServlet("/logout")
public class LogoutServlet extends HttpServlet {//前端发来的是get请求@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {//获取当前用户的session信息HttpSession session=req.getSession(false);//从session中获取用户信息User user= WebUtil.getLoginUser(session);if (user==null){//表示没有登录返回 403resp.setStatus(403);return;}//进行正常的用户退出功能//删除session中保存的用户session.removeAttribute("user");//给用户创建退出的时间user.setLogoutTime(new java.util.Date());//将用户退出时间传递给数据库UserDao.updateLogoutTime(user);//返回重定向到初始界面resp.sendRedirect("index.html");}
}

以上就是今天的全部内容了,后续的源码连接我会发到评论区,如果需要源码的话可以进行查看。如果觉得有用的话就点个赞吧!感谢!!!

八个步骤实现一个Web项目(在线聊天室)相关推荐

  1. php 开发一个聊天系统,ajax+php 实现一个简单的在线聊天室功能(附带源码)

    通过ajax和setInterval()函数,配合php+mysql实现一个简单的在线聊天室的功能.附带详细源码案例.这个聊天室是一个简单的聊天室,通过javascript setInterval() ...

  2. 如何简单的创建一个多人在线聊天室

    学习目标: 在本教程中,我们将要使用PHP和jQuery创建一个简单的在线聊天工具. 这种实用性的模块对于你想要有实时在线客户支持系统的网站可以说是完美. 废话不多说直接开始. 步骤1:HTML的代码 ...

  3. 详细介绍附代码:使用jquery,和php文件构建一个简单的在线聊天室,通过ip显示googlemap

    最近学习了关于使用最为流行的jquery发送请求,在实践中以最为简单的聊天室作为测验的辅助工具,对相关网页开发有一个初步的认识,希望大家能够一起学习进步.        首先介绍一下相关文件信息和功能 ...

  4. 第三章、C#简单界面在线聊天室C#一对多聊天(使用TCP转发实现的在线聊天室,文章末尾附免费项目资源)

    C#网络通信系列学习笔记 第一章.C#最简单的控制台网络通信&C#最简单的控制台socket通信 第二章.C#控制台实现一对一聊天&C#socket类的简单封装 第三章.C#简单在线聊 ...

  5. 基于Server-Sent Event的简单在线聊天室

    一.Web即时通信 所谓Web即时通信,就是说我们可以通过一种机制在网页上立即通知用户一件事情的发生,是不需要用户刷新网页的.Web即时通信的用途有很多,比如实时聊天,即时推送等.如当我们在登陆浏览 ...

  6. ASPNET使用Application实现在线聊天室

    什么是application Application对象是HttpApplicationState类的一个实例,用于定义ASPNET应用程序中的所有应用程序对象所有的方法.属性和事件. HttpApp ...

  7. 创建一个web项目的步骤

    花费了大量的时间在结构目录上. 记下来免得下次忘记了步骤 文章目录 创建一个web项目 创建c3p0的xml文件 使用细节 创建一个web项目 新建一个项目 创建好后目录中有图中方框内的内容 添加WE ...

  8. 一个web项目中web.xmlcontext-param的作用

    转 <context-param>的作用: web.xml的配置中<context-param>配置作用 1. 启动一个WEB项目的时候,容器(如:Tomcat)会去读它的配置 ...

  9. 一个web项目在myeclipse中add deployment时无法被识别出来的原因

    当我们一个web项目,在myeclipse中,add deployment时,可能发现,根本无法被识别成web项目,可能的原因有:    1. 项目的properties ->Myeclipse ...

  10. idea maven创建java项目_新版本IntelliJ IDEA 构建maven,并用Maven创建一个web项目(图文教程)...

    之前都没试过用maven来管理过项目,但是手动找包导包确实不方便,于是今天用2016版的IDEA进行了maven的初尝试. 打开IDEA,创建新项目: 然后选择Maven,以及选择自己电脑的jdk: ...

最新文章

  1. mysql主从复制缺陷_mysql主从复制及遇到的坑
  2. flutter 环境搭建
  3. Oracle SQL Parsing Flow Diagram(SQL 解析流程图)
  4. Apache Camel 2.18发布–包含内容
  5. 什么是索引?索引类型有几种,各有什么特点?
  6. 【转】OWIN是什么?
  7. JAVA classpath jar问题[zz]
  8. 浅谈算法(简单算法)
  9. Java 高阶 —— try/catch
  10. mysql主从错误1007_mysql主从错误:1032
  11. mysql监视器MONyog的使用
  12. 扫码枪测试软件,有线条码扫码枪的测试方法
  13. python制作一个简易计算器_最简易的python计算器实现源代码
  14. 泱脏武器库之 CVE 2021-4034 Polkit 提权小结
  15. Hexo博客与Next主题的高级应用
  16. python期权定价公式_如何理解 Black-Scholes 期权定价模型?
  17. 红米4 android 8,【红米4(标准版) 安卓6.0.1线刷包】MIUI V8.1.4.0.MCECNDI稳定版 可解账号锁...
  18. springMVC+mybatis+maven搭建过程
  19. 判断三条边是否构成三角形
  20. 51单片机stc15w204s串口通信发数据接收数据串口中断发中文字符串完美运行软件延时发送一字节函数全注释

热门文章

  1. 别浪费生活中灵光一闪的创意,发到实现网试试,万一实现了呢?
  2. spring 使用注解遇到的问题
  3. windows服务器设置开机启动的几种方式
  4. 今夜酒店特价:订得早,不如订得好
  5. 【AVD】视频解码时如何获取 coded_width coded_height 即参与编码的宽高
  6. 网络安全协议—SSL
  7. 信度spss怎么做_Spss详细图文教程——问卷信度和效度检验步骤图解
  8. 连八股文都不懂还指望在后端混下去么
  9. 数据分析师面试题攻略
  10. 面试常见简单编程题目