构建离线消息获取流程
在 “开发简单Android聊天软件(6)” 中,完成了完成消息接收和加载,构建一个完整的聊天流程。
但是我们只完成了一半,完成存量历史记录展示,和即时聊天的接受处理,和页面的实时刷新。目前我们来讲关于聊天记录的另一部分,离线消息数据获取。
一、服务端改造,存储离线聊天数据
1、首先我们要弄清楚一点:什么是在线?什么是离线?在线离线在程序中,实际应该由socket长链接作为代表。手机端和服务器的长链接挂上了,说明用户在线。长链接断开了,说明用户离线了。
手机端和服务端的socket就在这个时候是核心作用。回忆一下,在开发简单Android聊天软件(2)中,关于服务端长链接的ChatHandler类。
public class ChatHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
public static HashMap<ChannelHandlerContext,String> client=new HashMap<String, ChannelHandlerContext>();
@Override
public void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
System.out.println(msg.text());
if(msg.text().substring(0,1).equals("u")){ //判断传来是不是u开头,是则认为发送的为用户名
client.put(ctx,msg.text());//绑定上线用户user_id和ctx
System.out.println(msg.text()+" u_*** "+client.size());
}else {
System.out.println("收到消息"+msg.text());
if(client.get(ctx) != null){ //判断消息是否来自于已经绑定上线的用户,判断通过hashmap查询user_id不能为空
Gson gson = new Gson();
Im_msg_content im_msg_content = gson.fromJson(String.valueOf(msg.text()), Im_msg_content.class); //收到的是聊天消息
String recipient_id = im_msg_content.getRecipient_id; //获取消息接收方user_id:u_00002
ChannelHandlerContext recipient_ctx;
for(ChannelHandlerContext getCtx:client.keySet()){ //hashmap通过userid 获取ctx
if(client.get(getCtx).equals(recipient_id)){
recipient_ctx= getCtx; //拿到u_00002的ctx
sendMessage(recipient_ctx,msg.text()) //调用sendMesage方法,发送消息
}
}
}else{
System.out.println("未绑定用户id的长链接发出的消息,不予处理:"+msg.text());
}
}
}
//客户端连接
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
}
//客户端断开
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
client.remove(ctx)
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable e) throws Exception {
e.printStackTrace();
}
//给指定链接发消息
public void SendMessage(ChannelHandlerContext ctx String message) {
ctx.channel().writeAndFlush(new TextWebSocketFrame(message));
}
}
梳理一下核心逻辑。手机端长链接与服务器接上的那一刻,是handlerAdded()方法触发执行了。
然后手机端根据我们之前写好的逻辑,会主动发送一条,文本为自己用户id的消息,此时channelRead0()触发执行。
最后手机端关闭app,长链接断掉,此时handlerRemoved()触发执行。
理明白后,我们就可以过长链接的方法,来给长链接做一个简单的记录。也就是代码中定义的HashMap<ChannelHandlerContext,String> client
我们将在这个hashmap中,把“记录长链接ctx”和“删除长链接ctx”的两个动作,视为用户app的在线和离线。所以我们在channelRead0()中,识别到app端,用userid发消息,视为用户长链接挂载成功。给定义的hashmap添加ctx记录。为了方便知道是哪个用户上线,所以key为ctx,value为userid,行程对应关系。
if(msg.text().substring(0,1).equals("u")){ //判断传来是不是u开头,是则认为发送的为用户名
client.put(ctx,msg.text());//绑定上线用户user_id和ctx
System.out.println(msg.text()+" u_*** "+client.size());
}else{
System.out.println("收到消息"+msg.text());
....
}
同样的,我们也在handlerRemoved(),用户下线的地方,调用remove,清除长链接记录。在后续判断中,我们只需要判断用户userid是否存在与map中,则可以判断APP端是否在线。
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
client.remove(ctx)
}
2、接下来,开始做离线消息存储。逻辑大概分两步:1、判断消息属于离线消息,2、保存离线消息。
逻辑在 client.get(getCtx).equals(recipient_id)) 判断的 else 部分。
if(client.get(ctx) != null){ //判断消息是否来自于已经绑定上线的用户,判断通过hashmap查询user_id不能为空
Gson gson = new Gson();
Im_msg_content im_msg_content = gson.fromJson(String.valueOf(msg.text()), Im_msg_content.class); //收到的是聊天消息
String recipient_id = im_msg_content.getRecipient_id; //获取消息接收方user_id:u_00002
ChannelHandlerContext recipient_ctx;
for(ChannelHandlerContext getCtx:client.keySet()){ //hashmap通过userid 获取ctx
if(client.get(getCtx).equals(recipient_id)){
recipient_ctx= getCtx; //拿到u_00002的ctx
sendMessage(recipient_ctx,msg.text()) //调用sendMesage方法,发送消息
}else{ //在hashmap中找不到接收用户recipient_id:u_00002所需要的ctx,则判断用户u_00002未登录app端,为离线状态
addOfflineMsg(im_msg_content); //存储离线消息
//此处使用addOfflineMsg()的方法,对应方法实现通过jdbc调用insert ,将数据放入mysql数据库。
//其中涉及springboot项目配置使用mysql,数据库操作dao类等。
//全部展开的话与本节主题偏离甚远,所以本次就略过了。
//如果后续大家反馈需要的话,我就在后面章节补充此部分代码和逻辑
}
}
}else{
System.out.println("未绑定用户id的长链接发出的消息,不予处理:"+msg.text());
}
至此,发离线消息部分就全部完成了,后面是接受用户,从离线恢复到上线,其离线期间,消息的接收逻。辑
3、离线消息的接收,也是涉及服务端的改造。让我们回到 channelRead0() 中长链接判断用户上线的逻辑。
if(msg.text().substring(0,1).equals("u")){ //判断传来是不是u开头,是则认为发送的为用户名
client.put(ctx,msg.text());//绑定上线用户user_id和ctx
System.out.println(msg.text()+" u_*** "+client.size());
}
之前,我们通过用户发的消息是否是u开头的用户名,作为用户在app端上线的标志。
这样判断的原因,之前我们也明确过:因为我们在android侧,将长链接挂上之后,会立马执行发送用户id的文本。所以是可以通过这个判断的。
我们的离线消息获取的逻辑,也将会写在这里。
if(msg.text().substring(0,1).equals("u")){ //判断传来是不是u开头,是则认为发送的为用户名
client.put(ctx,msg.text());//绑定上线用户user_id和ctx
System.out.println(msg.text()+" u_*** "+client.size());
//在此继续
List<Im_msg_content> offlinelist = new ArrayList<>();
//queryOfflineContentByRecipientId: 通过userid搜索属于该用户的离线消息
//该逻辑也是涉及mysql,查询方法,实现逻辑略过
offlinelist = queryOfflineContentByRecipientId(msg.text().toString());
if (offlinelist.size() > 0) { //若存在离线消息记录
System.out.println(user_id + " 有" + offlinelist.size() + "条离线消息");
for (Im_msg_content i : offlinelist) {
int mid = i.getMid();
sendMessage(ctx, new Gson().toJson(i)); //引用Gson序列化,将对象转为json格式,发送
//deleteOfflineByMid: mysql操作,通过where mid = ? 条件,将发送成功的离线数据进行删除
deleteOfflineByMid(mid);
}
} else {
System.out.println(user_id + " 无离线消息");
}
}
这样改造完成之后,离线消息的接收就完成实现了。
(1)用户启动android app
(2)android socket自动触发连接成功
(3)socket 默认发出第一条消息:userid
(4)服务端收到 userid,判断用户登录成功。随后进行查询该用户的离线记录。然后通过长链接依次向android端发送mysql存下的离线消息。
(5)android通过长链接接收方法,收到离线消息,进行存储,同时刷新消息窗口页面
三、总结
本次我们也完成了两个部分的内容:
1、用户B不在线时,此期间收到的消息,都会进行存储。(理论上,无论是否在线,服务端都应该存储消息,供用户远程同步)
2、用户B上线后,查询离线期间,所有离线消息,然后发送到手机端,手机端通过“开发简单Android聊天软件(6)”中的逻辑进行刷新。
关于其中操作mysql数据库的那三个方法,我没有展开。
springboot如何操作数据库,网上教程应该不少。我把大致需要的表结构放在下面,大家可以各显神通,自行实现。
DROP DATABASE IF EXISTS `app`;
CREATE DATABASE IF NOT EXISTS `app`
USE `app`;
/*用户表 */
DROP TABLE IF EXISTS `user`;
CREATE TABLE IF NOT EXISTS `user` (
`id` int(11) NOT NULL,
`user_id` varchar(50) NOT NULL,
`user_nickname` varchar(50) NOT NULL,
`user_password` varchar(50) NOT NULL,
`user_name` varchar(50) NOT NULL,
`user_mail` varchar(50) NOT NULL,
`user_phone` varchar(50) DEFAULT NULL,
`user_token` varchar(50) DEFAULT NULL,
`status` varchar(50) NOT NULL COMMENT '0:管理员,1:注册用户'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*离线消息表 */
DROP TABLE IF EXISTS `im_msg_content_offline`;
CREATE TABLE IF NOT EXISTS `im_msg_content_offline` (
`mid` int(11) NOT NULL COMMENT '表主键',
`cid` varchar(50) NOT NULL COMMENT '会话id',
`id` int(11) NOT NULL COMMENT '个人会话id',
`content` varchar(1000) NOT NULL,
`sender_id` varchar(50) NOT NULL,
`recipient_id` varchar(50) NOT NULL,
`msg_type` int(11) NOT NULL,
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`mid`,`cid`),
INDEX `recipient_id` (`recipient_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*消息记录表 */
/*全量消息记录,用于用户远程同步聊天记录,1000w数据量以下无需分表 */
DROP TABLE IF EXISTS `im_msg_content`;
CREATE TABLE IF NOT EXISTS `im_msg_content` (
`mid` int(11) NOT NULL COMMENT '表主键',
`cid` varchar(50) NOT NULL COMMENT '会话id',
`id` int(11) NOT NULL COMMENT '个人会话id',
`content` varchar(1000) NOT NULL,
`sender_id` varchar(50) NOT NULL,
`recipient_id` varchar(50) NOT NULL,
`msg_type` int(11) NOT NULL,
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`mid`,`cid`)
INDEX `create_time` (`create_time`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;