开发简单Android聊天软件(7)

构建离线消息获取流程

在 “开发简单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;
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值