网页聊天系统项目
1.注册和登录功能模块
1.登录功能实现的总结
首先,根据username去数据库查询,如果查询的到,在根据输入的密码是否和数据库中的密码一致,如果不一致,则返回一个空的userInfo对象,则登录失败。
如果登录成功,则要在浏览器本地保存用户的信息,则要创建会话,如下图
2.注册功能实现总结
注册功能就是想数据库插入一条新的记录,此时我们也要考虑注册失败的情景,如果用户名重复,由于userId是unique属性,userid重复,注册就会失败,此时我们用try-catch语句来处理。
3.对登录和注册功能的测试
第一步,根据协议要求,返回对象的密码应该是空字符串,我们只需要在返回对象前将useInfo的password设置为空即可。
第二步:注意使用自增主键
4.注册与登录前端代码总结
新的获取输入内容的方式
2025年3月21号
css:增加滚动条
css:增加上边界线
css:实现隐藏和所有滚动条的隐藏
js:选中多个标签
2.好友数据库表的设计 ---面试考点
由于在聊天软件中,用户和好友之间是强相关的关系,比如,用户1是用户2的好友,同时用户2也是用户1的好友,所以,此处实体之间的关系是多对多的关系。
所以,此处设计一个关联表,通过关联表把用用户表中的两条数据联系到一起,在关联表中,我们可以用userId来实现将两条数据联系到一起,如下图
1.重要问题一
如果用户非常多,每个用户的好友也很多,就会导致这个表非常大,这时候怎么解决?
此时,我们可以通过分库分表的思路来解决。
比如针对userId,针对userId以某种算法来计算一个hashCode,然后再对hashCode进行切分。
假设我们分成100张表,表的编号为0~99,此时我们让hashCode%100,根据hashCode%100的结果,我们就将这个记录存放到对应编号的表中,此时,同一个userId的记录就会存在同一张表中了。
后续,如果我们要查询某个用户有几个好友,我们可以根据相同的算法,根据userId计算hashCode,让后根据hashCode去对应的表中去查询
2.重要问题二
在分库分表中的背景下,我们希望每个表都是相对均匀的,但是在用户中,可能存在大V,
这些大V一般都会拥有很多好友,此时,如果这些根据这些大V的userId计算出来的hashCode相同,那么这些大V就会存到一张表中,这就会导致一些表就会变得很大,直接导致分表的结果并不均匀,这个问题如何解决?
此时,我们可以特殊情况,特殊处理。
由于这些大V是极少数的用户,我们可以单独设计一个表去记录这些大V的userId,纪录下当前有哪些userId属于大V,然后,在使用专门的表来保存大V的好友关系。
在js中,可以根据id选中一个标签,然后可以通过innerHTML方法来修改选中原来标签中的内容。
通过document.createElement()方法在js中创建一个标签。
li.setAttribute方法是一个类似于Session.setAttribute的方法,存储特别信息,以备后用。
关于回调函数function中的body,网络上交互的数据,都是以字符串或者二进制的字节流的形式进行交互的,服务器返回响应的时候,也是先要把返回的对象转换为json格式的字符串,然后在进行传输。
此时,浏览器接收到的body参数也是json字符串,此时,我们要通过JSON.parse将body转换为JSON对象数组,但是在回调函数中已经自动帮我们转换为JSON对象数组了,不用我们手动去调用JSON.parse方法去转换。
3.查询用户的好友列表
在设计这个接口时,我们不需要将登录用户的id进行传参,因为在之前登录接口的设计中,我们用session保存了用户的登录信息,此时,我们只需要通过session来获取用户id即可。
3.会话数据库表的设计
这个的会话是指聊天记录,会话表的设计会涉及3个实体,分别是会话,用户和消息。
首先创建一个会话表
-- 创建会话表
drop table if exists message_session;
create table message_session (sessionId int primary key auto_increment,--上次访问时间lastTime datetime
);insert into message_session values(1,"2025-04-01 00:00:00");
insert into message_session values(2,"2025-05-01 00:00:00");
会话和用户之间的关系是多对多的关系,因为在一个会话中可以包含多个用户,一个用户也可以出现在多个回话中,此时,可以创建一个会话和用户之间的关联表,
-- 创建会话和用户的关联表
drop table if exists message_session_user;
create table message_session_user(sessionId int,userId int
);-- 1号会话中有张三和李四
insert into message_session_user values(1,1),(1,2);
-- 2号会话中有张三和王五
insert into message_session_user values(2,1),(2,3);
会话和消息之间的实体关系是一对多的关系,因为一个会话中可以包含多条消息,而一条消息只能存在一个对话中。
4.获取会话功能
1.如果在一张表中,我们想对这张表进行某一种操作,但是进行这个操作时,需要某一个数据,而这个数据恰好这张表没有,而另一张表中有这个数据,此时,我们就可以使用子查询或者连表查询。
如何去获取会话呢?步骤
1,首先根据session来获取到当前登录的用户,在根据当前用户的userId去获取当前用户存在于哪些会话中(哪个会话用sessionId来判断)
2,接着遍历sessionId,在根据sessionId去查询该会话中涉及到的好友。
3,接着查询每个会话的最后一条信息(还没实现)
5.会话管理功能
实现思路:首先设计一个会话类,如下图
接着创建一个MessageSession类的对象,对message_session表进行插入数据,由于sessionId是一个主键,当进行插入操作时,sessionId会自动进行增加,所以在进行插入前,不用message.setSessionId(),后面我们就可以直接通过MessageSession类的对象来去获取sessionId了。
然后,我们在对message_session_user表进行插入数据,不过对该表的插入操作要进行2次,我们要分别插入用户自己和用户的好友
复习了一个注解: @SessionAttribute
该注解可以直接获取session中存储的对象,后面加上对象的类型。
由于会话管理的操作涉及到了对两个表的操作,我们需要使用事务来确保进行插入操作时,两个表的插入操作都能成功,如果有一个表的插入操作失败了,使用事务就能回滚数据,保证了原子性,使用事务,直接在方法上面加@Transactional就行了。
完整代码
6.获取历史消息功能
在查询历史消息时,我们向获得messageId,fromId,username ,sessionId,content等信息,但是message表中没有username这个属性,而username的属性在user表中,所以这时候我们要用到联合查询,联合查询就是对两个表的数据进行笛卡尔积,在通过条件过滤掉无用信息。
写联合查询的时候,注意要对username起别名,因为在messag.xml中sql语句的返回值类型是Message类,如果没对username起别名,我们是可以查询到username,但是无法将username赋值给fromname,此时得到的fromName就为null。
收货2:定义css类的时候要加点(.),使用或者调用css时不能加点(.)
7.服务器---使用webSocket实现服务器转发消息给客户端
由于此时我们使用webSocket来实现消息的发送,我们使用JSON格式的数据作为webSocket中的payLoad部分(客户端发送的内容)
在实现服务器转发消息功能时,我们需要维护一个映射关系,即userId和WebSocketSession之间的映射关系。
因为客户端在发送消息时,服务器需要知道将这条消息转发给哪一个用户,由于我们在实现webSocket时,方法中都带有了webSocketSession这个参数,我们可以直接获取。
难点就是如何获取到userId?
在前面实现获取历史会话的时候,我们实现了获取会话涉及到的好友的功能,如下图。
但是,我们当当知道一个userId是不足够的,我们要知道用户和服务器之间WebSocketSession之间的关系。
在服务器中,每个连接到服务器的客服端都会保存一个对应的webSocketSession对象,当服务器需要将消息转发给某一个客户端时,通过对应的webSocketSession对象来发送。
我们可以用一个哈希表来存储userId和WebSocketSession之间的映射关系。
建立连接时,我们将映射关系插入哈希表中,断开连接时,我们清除哈希表中对应的映射关系。
我们如何去获取userId呢?
因为前面我们将用户的消息都存储在httpSession中,那如何获取httpSession中的信息,添加一份到webSocketSession中呢?
我们只需要在注册webSocket中添加一个拦截器即可,如下图
接着通过特定的方法来获取userId,如下图
session.getAttributes()方法就是将Session中存储的键值对添加一份到webSocketSession对象中,需要注意的是,session.getAttributes()的返回值类型是一个Map类型,我们需要get获取到需要的信息。
7.1 我遇到的错误
在实现这个功能时,运行时出现了webSocket建立连接出现异常的错误且通过抓包发现服务器这边报了500并且服务器这边没有出现webSocket建立连接成功时打印的代码,由于在这个功能中使用了webSocket,然后发现在webSocket中设置的路劲为“message”,而代码中有获取历史消息接口的url也是“message”,我首先要搞清这个请求是代码中的那一个部分发出的,通过异常信息中出现了sessionId为null的错误,此时,我就可以肯定这个请求是获取历史消息的接口发出去的,这时候我们只要将webSocket中的接口换一个名字即可。
7.2 实现用户管理
通过而外设计一个类来保存userId和webSocketSession之间的映射关系 ---OnlineUserManager,该类中提供了3个方法,分别是用户上线---online,用户下先---offline,根据userId获取对应的WebSocketSession对象---getSession
在OnlineUserManager这个类中,我们用一个哈希表来存储映射关系,但是需要注意的是,当用户上线时,我们会将映射关系存储到哈希表中,下线时,我们会将映射关系从哈希表中删除,这里的添加和删除是一种多线程的情况,可能某一时刻会有100甚至更多的用户上线或者下线,所以此时我们要保证线程安全,所以此时,我们要用ConcurrentHashMap这个类来记录映射关系,因为ConcurrentHashMap是线程安全的。
1.用户上线
用户上线会涉及到一个多开的情况,多开就是一个账号在不同的客户端同时登录了,针对多开,我们会遇到两种情况第一种情况就是后一个客户端账号登录会把前一个客户端的登录状态给踢掉,第二种情况是当后一个客户端登录时,会直接显示登录失败。
在我的项目中,采用的是第二种情况。
当遇到多开的情况,我们就不玩哈希表里面插入数据就行了,因为后面消息的发送都是通过映射关系来发送的,不添加多开的映射关系,自然就发送不了消息。
2.用户下线
用户下线时,我们要判断当前要退出的用户是否和服务器中记录的用户是否相同,相同我们才能成功下线,不相同则不能下线,否则可能就会出现登录用户为张三,你却可以将李四下线的情况。
3.根据userId获取对应的WebSocketSession对象
7.3 客户端发送消息,服务器转发消息---核心部分
建立的请求和响应对象。
首先,我们要获取用户的登录状态,也是通过WebSocketSession中提供的getAttributes()来获取,
接着,我们要对转发的消息进行封装,因为因为根据我们的协议,req是JSON类型,我们要将器转化为我们自定义的请求类型,也是通过objectMapper中提供的readValue方法来实现。
紧接着对消息类型进行判断,我们要传送的类型是message,如果匹配,就转发消息,转发消息我们另外设置一个函数来实现。
transferMessage的实现逻辑
transferMessage方法中的fromUser参数是表示消息是从哪一个用户发来的
首先,我们就要构造一个响应对象,这个响应对象就是后续我们要sendMessage的对象,这里根据我们的协议,我们要将response对象转换为JSON类型。
由于服务器要将消息转发出去,所以必须要知道将消息转发到哪一个会话中,所以根据请求messageRequest中的sessionId来确定是哪一个会话,接着获取到这个会话中有哪些用户,然后,我们就将消息转发给这些用户。
这个操作以前就实现过了,如下图
我们直接调用即可,注意,这里的friends是排除了发送消息的用户,此时,我们也要讲发送消息的用户加进去friends,方便后面前端实现时也可以将这个消息呈现出来
接着我们就遍历friends,依次将消息发送给friends中的friend对象。
为什么需要遍历呢?
因为有可能存在群聊的情况,一个群聊的会话会涉及到多个用户,所以friends中就不止一个friend,当friend不止一个时,我们要将消息都转发给friends中的用户。 但是,在我的项目中不涉及群聊的情况,所以friends中就一个friend。
一边遍历一遍发送消息,需要注意的是,我们不能直接将resp转发出去,需要包装一下。
最后,我们要将转发的消息插入到数据库表中,方便未上线用户后续上线时能及时获取历史消息。
8.添加好友设计思路
首先,用户可以在搜索框中输入一个用户名,点击搜索按钮,此时客户端就会向服务器发送一个添加好友的ajax请求,服务器这边就根据输入的用户名到数据库中的user表中去进行模糊查询,将名子符合的用户都显示在右侧界面上。每个搜索结果包含3个部分,搜索结果的用户名,输入框,添加好友的理由,和添加好友的按钮。
接着,发送添加好友的请求,当用户输入添加好友的理由,点击添加好友的按钮,客户端就会想服务器发送一个添加好友的ajax请求,此时,后端需要设计一个数据库表来保存这个添加好友的请求,然后,服务器就可以将添加好友的请求保存到数据库表中。
接着,对方接收到这个好友请求,决定是否接受好友请求,这时候会遇到两种情况。
第一种情况:该用户离线时,用户在下次上线时,可以从数据库中获取到之前的添加好友的请求有啥(可以通过ajax请求来得到)
第二种情况:用户在线时,用户就要立即看到添加好友的请求,此时我们就可以通过WebSocket来实时发送好友请求
此时,我么也要设计一个添加好友的WebSocket请求和响应,请求类型有我们自行决定。
4.当用户点击接受好友请求时,我们就在friend表中插入两条新数据,在FriengMapper中在实现一个插入语句,然后,我们就可以把 保存添加好友请求表 中对应的数据删掉。当用户点击拒绝时,当服务器接收到HTTP请求,啥都不做,也是将表中的对应的好友请求删掉就行了。