项目目的:实现多用户角色(在主项目中为:政府监控管理员、气体量化分析员、一线设备操作员)通过线上注册登录,在同一个聊天平台内针对已上线用户进行实时沟通,未上线用户在上线后能够查看未上线时聊天室内历史消息。
后端逻辑
聊天场景:(系统内的所有用户都在一个群里)
- 一个用户发送的消息,在线的所有用户都能收到(核心);
- 针对离线的用户(已注册,暂时未登录),等该用户再次登录时,补发未看到的历史消息;
- 首次注册的用户,看不到之前的历史消息。
效果图
未登录主页
弹出提示框,跳转登陆界面。
登录界面
登录成功后会提示点击跳转聊天室主页,登录失败会提示失败并点击跳转重新登陆或注册。
注册界面
根据注册框内容进行注册,注册成功后点击跳转聊天室主页。
登陆后主页
技术点知识预备:
1.什么是WebSocket?
Websocket协议基于TCP协议,可以在浏览器和服务器之间打开交互式会话。可以向服务器发送消息并接收到服务器的驱动,实现了真正意义上的全双工通信,也就是双方都可以发送和接收消息,类似于拨打电话双方,可以在同一时间进行发送和接收。
Websocket中的web就是HTTP协议,socket就是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
2.为什么选WebSocket?
因为项目需要实现即时通讯,那么实现即时通讯大致可以分为两种模型,一种是基于HTTP的请求响应模型,主要实现方式是有轮询和长连接,还有一种是项目中使用的基于websocket的订阅通知模型,类似于我在b站关注一个up主,当这个up主更新就会推送给我。
为什么不选择HTTP而选择websocket是因为HTTP是非持久化、单向的协议,建立连接后,只允许浏览器向服务器发送请求后,服务器才能返回相应的数据,这就会导致过多不必要的请求,浪费流量和服务器资源,每一次请求、应答可能都会浪费在相同的头部信息上。而websocket只需要浏览器和服务器通过HTTP协议进行一个握手,然后单独建立一条tcp的通信通道进行数据的传送就可以实现全双工通信。
3.Servlet和Endpoint
相同点:Endpoint这个类的对象,是由Tomcat给我们创建的,这个和Servlet类的对象创建是类似的。
区别:
- Servlet对象,在整体的Tomcat运行过程中,只会实例化一次(全局的、单例的)
- Endpoint对象,是每次有新的webSocket建立,就会实例化一个对象
servlet对象的属性,在每次请求 /XXX 时,都是同一个数据(因为只有一个对象,所以属性是同一个)共享的
Endpoint对象的属性,是每次建立 /XXX-ws 时,都是各自对象的数据,互相不是同一个数据
一、简单分析
需要一个在线用户管理机制(用来记录当前在线的用户信息),可以去动态、正确维护该信息,即有 用户登录 则 添加到在线列表,有 用户退出 则 从在线列表删除。
用户发送消息的数据流转流程:
初期架构图:
从上向下解释:
最上方的是 websocket 和前端的相互通信;
再往下,只要建立一条通信通道,就会生成一个 websocket 连接,就会生成一个 Endpoint 对象,所以会有多个 Endpoint 对象,每个 Endpoint 对象维护自己的在线用户信息,并去维护当时的session;
黑色路线:当有用户要发送消息时,前端把数据提供给对应的 Endpoint ,对应到Endpoint 对象上,然后消息会被发送到后端程序的后端(期间通过dao层保存消息到数据库),找到所有在线用户,针对每个在线用户再将消息发送回去;
蓝色路线:只要有用户上线,首先将上线用户记录并维护,其次从数据库中查询历史消息,针对该用户,将历史消息发送回去;
红色路线:用户下线,首先需要先从在线用户列表将其删除,其次在数据库更新其下线时间,这样是为了下次该用户上线时,根据下线时间发送这段时间内的历史消息;
通过初步分析,可能会对 WebSocket 产生一个疑问,那么接下来,简单来认识一下。
WebSocket 中事件驱动如何做到处理 1.用户上线;2.用户下线;3.发送消息 三种不同的业务事件?
WebSockets - Web API 接口参考 | MDN
用户上线 ---- onOpen ---- new WebScoket(...)
用户下线 ---- onClose ---- ws.close() 或者 前端关闭网页(浏览器调用ws.close(),浏览器自动清理资源)
发送消息 ---- onMessage ---- ws.send(...) 前端发送消息过来
二、数据库建表
用户表users
uid、用户名username(不允许重复)、昵称nickname、密码password、上次登出时间logout_at
消息表messages
mid、uid(谁发的)、发送内容content、发送时间published_at
关联:
查找未发送过的历史消息
注册用户时,insert 用户时将上次登录时间录成 null
if (user.上次登录的时间 != null) {
//不是新用户
SELECT * FROM 消息表 WHERE 发送时间 >= user.上次登录时间;
}
三、代码实现(详细注释)
代码大体划分层次
endpoint ——(负责 WebSocket接入) 每次连接一个
service ——(业务工作) 可以单例
dao ——(数据库相关) 可以单例
usercenter —— (管理在线用户) 必须单例
接口
用户管理接口
- online(用户上线)
- offline(用户下线)
发布消息接口
- publish(消息)
整体代码已上传到Gitee,代码内添加详细注释助理解。
四、项目中遇到的部分问题汇总
1.如何从WebSocket获取HttpSession
根据WebSocket原理我们可以知道,WebSocket在建立握手阶段是使用Http协议的,那么就从Http这次握手中拿去HttpSession.
根据我对文档进行查询,找到了下面的方法(这里都是由Tomcat实例化的)
//configurator = HttpSessionConfigurator.class
//webSocket 在建立连接时,会构造 HttpSessionConfigurator 对象,调用其 modifyHandshake 方法
//先调用 modifyHandshake 后执行 onOpen
@ServerEndpoint(value = "/message", configurator = HttpSessionConfigurator.class)
public class MessageEndpoint {
@onOpen
@onClose
@onMessage
@onError
}
//读取是通过 ServerEndpointConfig.Configurator
public class HttpSessionConfigurator extends ServerEndpointConfig.Configurator {
@Override
public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
//websocket建立握手阶段(建立连接阶段)
Object httpSession = request.getHttpSession();
//httpSession可能是null的
if (httpSession != null) {
sec.getUserProperties().put("httpSession", httpSession);
}
}
}
通过 Debug 可以看到执行过程(反射)
Tomcat 启动阶段,都是由 Tomcat 内部的代码触发的:
Configurator 的构造器方法 (执行一次)
- 每次 js 执行 new WebSocket(...) 执
- Configurator.modifyHandshake(...) 行
- Endpoint 的构造方法 多
- endpoint.onOpen 次
补充知识点:反射
对于任意一个类或对象,都能够获取到这个类的所有属性和方法(包括私有属性和方法),这种动态获取信息以及动态调用对象方法的功能就称为反射机制。简单来讲,通过反射,类对我们是完全透明的,想要获取任何东西都可以。
五、部署并发布项目
前提:
- 拥有一台云服务器并安装有Linux系统(这里我安装的是CentOS 7)
- 云服务器连接本地主机(我这里用的是Xshell和Xftp)
- 已安装git、jdk、maven、Tomcat、MySQL(我的本地MySQL Workbench已连接远程云服务器的数据库)
1.拉取 Gitee 上的项目
放在一个自己安排好的文件夹内
git pull [gitee地址]
2. maven 打包项目
要到有 pom.xml 文件的根目录下打包,生成 target 文件夹
mvn package [文件名]
将 target 目录下的 .war 文件复制到 tomcat 的 webapp 目录下,tomcat 会自动将 .war 文件生成项目文件夹
cp [.war文件名] [tomcat的webapp文件夹地址]
3.重启 tomcat
进入到 tomcat 的 bin 目录下,通过 sh 命令重启 tomcat(这里注意该文件夹内文件已获得 sh 权限)
sh shutdown.sh
sh startup.sh
4.通过外网访问聊天室
【云服务器外网ip地址】:【端口号,不写默认是8080端口】/【访问路径,不写默认是ROOT文件夹下的项目】