解决方案

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

seo靠我 2023-09-25 03:52:50

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

Hello,今天给大家带来的是我的一个Web项目的开发过程的相关步骤,这个项目实现的功能是一个Web在线聊天室,简单的来说就是实现在网页版的聊天框,能够实现对于用户信息进行注册SEO靠我,登录,在网页上收发消息的功能。

这个项目也实现了我和别的小伙伴一起实现在线聊天的功能,这是我实现的Web聊天室网页链接地址:[http://47.100.138.17:8080/chatroom/inSEO靠我dex.html]

感兴趣的小伙伴可以注册登录呦在网上尝试一下聊天。

话不多说,我们直接开始对于开发过程进行实现吧:

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

对于用户使用Web聊天室实现来说,需SEO靠我用户用自己的账号密码登录,同时有自己设置的昵称信息头像信息;在登录之后有聊天室需要提供频道来使用户在其中进行交流;在交流的时候需要用户去发送消息,不同的用户会在不同的时间发不同的消息内容

因此呢SEO靠我,根据这些需求就设计了

User(用户表)、channel(频道表)、message(消息表)

三个表单信息:create table user(id int primary key auto_increSEO靠我ment,username varchar(15) not null unique comment 账号,password varchar(15) not null comment 密码,nicknaSEO靠我me varchar(20) not null comment 昵称,head varchar(50) comment 头像url(相对路径),logout_time datetime commentSEO靠我 退出登录的时间 ) comment 用户表; create table channel(id int primary key auto_increment,name SEO靠我varchar(20) not null unique comment 频道名称 )comment 频道; create table message(id int prSEO靠我imary key auto_increment,user_id int comment 消息发送方:用户id,user_nickname varchar(20) comment 消息发送方:用户昵称SEO靠我(历史消息展示需要),channel_id int comment 消息接收方:频道id,content varchar(255) comment 消息内容,send_time datetime coSEO靠我mment 消息发送时间,foreign key (user_id) references user(id),foreign key (channel_id) references channel(iSEO靠我d) ) comment 发送的消息记录;

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

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

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

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

(1)对于JDBC的工具类

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

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

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

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

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

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

在前端中需要创建相应的标签来让用户将自己的用户名,密码,昵称等相关信息输SEO靠我入当中,在标签中设置required则表示必须填写的内容。

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

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

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

tips

:在写后端的响应之前做一个测试,利用抓包工具验证是否能够正常的发送请求和响应。)

接下来进行对于RegisterServlet的开发,首先设置Servlet注解获取前端传过来的forSEO靠我mdata表单数据,在其次对于传过来的数据进行构造成一个实体类,存到数据库中。在针对于头像文件获取的时候,需要先获取存储照片的后缀名,将其保存下来创建一个时间戳相关的随机字符串构成重命名,最后形成新的SEO靠我文件保存在本地的文件中。

最后调用用户表的相关操作

(封装在UserDao类中)将数据库的信息存储完毕之后就可以返回后续的响应,但是需要构造一个响应对象,需要设置JsonResult返回的对象类,设置好返SEO靠我回格式,返回信息。最后通过resp的相关API来实现对于响应数据的返回。

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

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

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

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

实现的代码如下://用户表数据库相关的操作 public class UserDao {//注册:检查账号、昵称是否存在 实现JDBC操作public static User chSEO靠我eckIfExist(String username,String nickname){Connection c=null;PreparedStatement preparedStatement=nuSEO靠我ll;ResultSet rs=null;try {c= DBUtil.getConnection();String sql="select * from user where username=?"SEO靠我;if (nickname!=null){sql+="or nickname=?";}//将上面的预编译的的占位符进行替换数据preparedStatement=c.prepareStatement(SEO靠我sql);preparedStatement.setString(1,username);if (nickname!=null){preparedStatement.setString(2,nicknSEO靠我ame);}//执行查询操作,返回结果集进行接收rs=preparedStatement.executeQuery();//准备查询的User对象User queryUser=null;while (SEO靠我rs.next()){queryUser=new User();//将结果集的字段设置到属性中Integer id=rs.getInt("id");String loginNickname=rs.geSEO靠我tString("nickname");String password=rs.getString("password");String head=rs.getString("head");java.sSEO靠我ql.Timestamp logoutTime=rs.getTimestamp("logout_time");queryUser.setId(id);queryUser.setUsername(useSEO靠我rname);queryUser.setPassword(password);queryUser.setNickname(loginNickname);queryUser.setHead(head);SEO靠我if (logoutTime!=null){//考虑数据是不是为空的情况long l=logoutTime.getTime();queryUser.setLogoutTime(new java.utiSEO靠我l.Date(l));}}return queryUser;} catch (SQLException e) {throw new RuntimeException("注册检查账号昵称是否存在JDBCSEO靠我出现错误",e);}finally {DBUtil.close(c,preparedStatement,rs);}}public static int insert(User user) {ConneSEO靠我ction c=null;PreparedStatement ps=null;try {c=DBUtil.getConnection();String sql="insert into user (uSEO靠我sername,password,nickname,head)"+" values(?,?,?,?)";//进行预编译ps=c.prepareStatement(sql);//替换占位符ps.setSSEO靠我tring(1, user.getUsername());ps.setString(2, user.getPassword());ps.setString(3, user.getNickname())SEO靠我;ps.setString(3, user.getHead());return ps.executeUpdate();} catch (SQLException e) {throw new RuntiSEO靠我meException("插入数据时出现错误",e);}finally {DBUtil.close(c,ps);}}//数据库修改用户的退出时间jdbc代码public static int updaSEO靠我teLogoutTime(User loginUser) {Connection c=null;PreparedStatement ps=null;try {c=DBUtil.getConnectioSEO靠我n();String sql="update user set logout_time=? where id=?";ps=c.prepareStatement(sql);//替换时间站位符 获取用户中SEO靠我存储的退出时间long currentTime=loginUser.getLogoutTime().getTime();ps.setTimestamp(1,new Timestamp(currentTSEO靠我ime));ps.setInt(2,loginUser.getId());return ps.executeUpdate();} catch (SQLException e) {throw new RSEO靠我untimeException("更新用户上次注销时间出错", e);} finally {DBUtil.close(c,ps);}}} (2)对于消息表的工具类实现

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

//对于数据库Message的JDBC操作 public SEO靠我class MessageDao {//对于Message操作有查询和插入操作//给用户放回从退出时间开始算起的保存的历史消息public static List<Message> query(DatSEO靠我e logoutTime){Connection c=null;PreparedStatement ps=null;ResultSet rs = null;try {c= DBUtil.getConnSEO靠我ection();String sql="select * from message";//对于刚注册是用户,没有上次注销的时间,要进行判断一下if (logoutTime!=null) {//表示有SEO靠我用户存在sql += " where send_time > ?";}ps=c.prepareStatement(sql);if (logoutTime!=null){//表示用户存在,将预编译的信息SEO靠我进行参数设置//从退出的时间开始算起 保存的数据ps.setTimestamp(1,new Timestamp(logoutTime.getTime()));}//得到执行的结果 存放在rs中rs=pSEO靠我s.executeQuery();List<Message> messages=new ArrayList<>();while (rs.next()){//构建Message对象来接收rs中的消息MeSEO靠我ssage getOldMessage=new Message();getOldMessage.setId(rs.getInt("id"));getOldMessage.setUserId(rs.geSEO靠我tInt("user_id"));getOldMessage.setUserNickname(rs.getString("user_nickname"));getOldMessage.setChannSEO靠我elId(rs.getInt("channel_id"));getOldMessage.setContent(rs.getString("content"));getOldMessage.setSenSEO靠我dTime(rs.getTimestamp("send_time"));//将获取的历史消息对象传到消息队列中messages.add(getOldMessage);}//将查询到的历史消息返回到队列SEO靠我中进行返回return messages;} catch (SQLException e) {throw new RuntimeException("查询历史消息出错",e);}finally {DBSEO靠我Util.close(c,ps,rs);}}//插入数据操作public static int insert(Message m){Connection c=null;PreparedStatemenSEO靠我t ps=null;try {c=DBUtil.getConnection();//将接收到的用户发送的消息保存起来//接收到的用户信息的包含的字段 有 内容 用户的昵称 用户的id 频道号 发送的时SEO靠我间String sql="insert into message(content, user_id, user_nickname, channel_id, send_time) " +" valuesSEO靠我(?,?,?,?,now())";ps=c.prepareStatement(sql);ps.setString(1,m.getContent());ps.setInt(2,m.getId());psSEO靠我.setString(3,m.getUserNickname());ps.setInt(4,m.getChannelId());//设置接收到的信息发给前端m.setSendTime(new DateSEO靠我());return ps.executeUpdate();} catch (SQLException e) {throw new RuntimeException("保存发送的消息jdbc出错",eSEO靠我);}finally {DBUtil.close(c,ps);}} } (3)对于频道表的工具类实现

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

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

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

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

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

实现的代码如下:<!DOCTYPE html> <html lang="en"> SEO靠我 <head><meta charset="UTF-8"><title>在线聊天室</title><link rel="stylesheet" href="css/common.css"SEO靠我><link rel="stylesheet" href="css/index.css"> </head> <body><div id="app" v-cloak><fSEO靠我orm @submit.prevent="login()"><h3>聊天室登录</h3><div class="row"><span>用户名</span><input type="text" v-moSEO靠我del="username" required></div><div class="row"><span>密码</span><input type="password" v-model="passwoSEO靠我rd" required></div><div class="error-message">{{ errorMessage }}</div><div class="row"><input type="SEO靠我submit" value="登录"></div><div class="row-right"><a href="views/register.html">注册</a></div></form></dSEO靠我iv> </body> <script src="js/util.js"></script> <script src="js/vue.js"></scrSEO靠我ipt> <script>let app = new Vue({el: "#app",data: {errorMessage: "",username: "",password: ""SEO靠我,},methods: {//实现前端的登录功能login: function (){ajax({method: "post",url: "login",contentType:"applicatioSEO靠我n/json",//转化为json对象 将数据转为字符串body:JSON.stringify({username: app.username,password: app.password,}),//SEO靠我回调函数callback:function (status,responseText){if (status!=200){alert("登录出错了,服务器可能开小差了。" +"返回响应状态码为:"+sSEO靠我tatus);return;}let body=JSON.parse(responseText);if (body.ok){alert("账号密码验证成功,欢迎进入在线聊天室")//跳转到聊天框页面进SEO靠我行访问window.location.href="views/message.html"}else {//登录失败,显示错误信息app.errorMessage=body.reason;}}});},SEO靠我},}); </script> </html> (2)对于登录页面的后端实现

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

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

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

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

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

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

先对于ChannelList类进行设计,在后端返回响应之前需要对于用户信SEO靠我息的session进行确认,和查询的数据库信息进行比对,如果不同,则进行禁止访问的状态码设置。如果查询成功存在的话,查询数据库的所有信息返回到List中,再将数据写入Json字符串中,最后将数据写在响SEO靠我应中进行返回。

对于数据库查询所有的Channels信息用ChannelDao的一个方法来进行实现。然后对于后端messsage的功能实现,该功能是基于websocket实现所以需要导入WebsockeSEO靠我t的依赖包:

javax.websocket

javax.websocket-api

1.1

provided

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

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

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

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

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

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

“SEO靠我”的新闻页面文章、图片、音频、视频等稿件均为自媒体人、第三方机构发布或转载。如稿件涉及版权等问题,请与 我们联系删除或处理,客服邮箱:html5sh@163.com,稿件内容仅为传递更多信息之目的,不代表本网观点,亦不代表本网站赞同 其观点或证实其内容的真实性。

网站备案号:浙ICP备17034767号-2