IM框架(单体项目版)

码云地址:https://gitee.com/yuanmaxinxi/villa_im_sdk.git

支持通信协议: tcp/udp/ws

支持数据交互协议: json/protobuf 

支持qos策略,防重防丢失

支持事件机制,可以在事件中阻断框架默认行为,或记录消息和离线消息等

还需完善的功能:

1. 阅后即焚的一些交互逻辑,当然阅后即焚消息对于IM框架来说可能不需要做额外处理

2. 端对端加密,端对端加密可能需要添加type来区分交换秘钥等这些操作,当然IM框架可能不需要做额外处理,可以使用自定义协议类型去业务层处理,后面再说吧

3. 负载架构,需要通过队列分发,这个肯定需要加的,后面再说吧

因为只能通过业余时间来做,所以慢慢会逐步去完善这些功能。有想法的朋友也可以跟我一起交流哦

我感兴趣的方案包括:IM,音视频,GIS,低代码平台,有想法的朋友咱们可以一起讨论下,我也可以分享一些我写的或者抽取的一些公共方案

 

代码片段:

一、事件接口:


/**
 * 逻辑业务类
 * @作者 微笑い一刀
 * @bbs_url https://blog.csdn.net/u012169821
 */
public interface LogicProcess {
    /**
     * type-1 单聊 type-8 群聊
     * 根据toId获取要转发的目标群体
     * 得到的结果可以是好友id/获取在同一个群的所有用户id
     */
    List<String> getTargets(Protocol protocol);

    /**
     * 将消息存到数据库
     */
    void addMessage(Protocol protocol);

    /**
     * 长连接的登录请求前置方法,如果返回false 则不会执行长连接登录功能
     * 可以用来做账号密码验证,签名验证等
     * 当然也可以将登录请求独立到http接口去,这也是本框架推荐的做法,而这个方法需要做的仅仅是验签即可
     */
    boolean loginBefore(Channel channel,Protocol protocol);

    /**
     * 长连接的退出登录请求前置方法,如果返回false 则不会执行长连接登出功能
     * 可以用来做销毁登录session或者token等操作
     * 当然也可以将退出登录请求独立到http接口,闲置此方法即可 但需要返回true 让长连接登出功能得以继续
     * 如果需要抛错到长连接  调用#ProtocolManager.sendAck(Channel,dataContent,type-ChannelConst中的消息协议类型);
     */
    boolean logoutBefore(Channel channel,Protocol protocol);

    /**
     * 服务器接收到通用消息的前置方法 返回false 则直接阻断长连接后续功能 比如转发消息/qos/回调  都不再执行 直接砍断
     * 目前未想到此方法的作用,留作一些个性化业务吧 默认返回true即可
     */
    boolean sendMsgBefore(Channel channel,Protocol protocol);

    /**
     * qos失败的回调
     * 这个方法是真正意义上的失败,也就是对方离线或qos过程中离线
     * 此方法适合用来做离线消息
     * 每条消息,每个接收者支触发一次
     * @param channelId 客户端唯一标志,这个标志可用于记录当前离线消息属于谁的
     * @param protocol 消息体
     */
    void sendFailCallBack(String channelId,Protocol protocol);

    /**
     * 消息真正意义上的发送成功回调 目标客户端收到并回执给服务器算真正意义的成功
     * 每条消息,每个接收者支触发一次
     */
    void sendSuccessCallBack(Protocol protocol);
    /**
     * 发送数据的回调
     * 发送了一次数据的回调(成功则成功,失败则会走qos),并不意味着成功或者失败。
     * 每条消息,每个接收者只触发一次
     * TODO...udp协议无法监听成功与失败(虽然这里无需成功和失败的状态,但是还需要再次测试udp是否能监听到发送成功)
     */
    void sendCallBack(Object protocol);
    /**
     * 自定义协议类型的处理方法 可扩展框架协议外的其他自定义类型协议
     * @param channel 客户端消息通道
     * @param protocol 消息体
     */
    void customProtocolHandler(Channel channel,Protocol protocol);
}

协议类型:

//-----------------消息协议类型(100内保留给当前SDK做系统指令,自定义指令请使用101以上)--------------------------
    //客户端登录
    public static final int CHANNEL_LOGIN = 0;
    //消息交互
    public static final int CHANNEL_ONE2ONE_MSG = 1;
    //消息回执
    public static final int CHANNEL_ACK = 2;
    //接收到的客户端心跳
    public static final int CHANNEL_HEART = 3;
    //群聊消息交互
    public static final int CHANNEL_GROUP_MSG = 8;
    //客户端正常退出
    public static final int CHANNEL_LOGOUT = 9;

    //------------------------------提示类协议类型------------------------------------------------------
    /** 客户端未登录 */
    public static final int CHANNEL_NO_LOGIN = 11;
    /** 登录未携带连接标志符 */
    public static final int CHANNEL_NOT_LOGIN_ID = 12;
    /** 处理成功 */
    public static final int CHANNEL_LOGIN_SUCCESS = 13;
    /** 此消息必须携带消息ID,但未携带 */
    public static final int CHANNEL_MESSAGE_NO_ID = 14;

这个算是核心逻辑处理类吧:

package com.villa.im.manager;

import com.alibaba.fastjson.JSON;
import com.villa.im.handler.ChannelHandler;
import com.villa.im.model.ChannelConst;
import com.villa.im.model.MsgDTO;
import com.villa.im.model.Protocol;
import com.villa.im.util.Util;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;

import java.util.Iterator;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 这个类包含了服务器往客户端发送的所有功能
 * @作者 微笑い一刀
 * @bbs_url https://blog.csdn.net/u012169821
 */
public class ProtocolManager {
    static{
        /**
         * 原生定时器实现qos每间隔指定时间进行第N次发送,一直到qos中删除这个包或用户端掉线
         * 这里与主线程并行,所以可能会出现:主线程发送了消息后,qos立马又发送了一条,
         * 且间隔未达到ChannelConst.QOS_DELAY指定间隔,所以通过preSendTimeStamp字段来排除这种情况
         * 以保证qos的消息,至少间隔ChannelConst.QOS_DELAY指定时间发送
         *
         * 当前设计中并没有限制qos发送次数,也就是如果用户端未返回ack,那么这个消息包会一直间隔发送
         * 这里为什么不对发送次数进行限制的原因是:用户端弱网,但并没有掉线/断线 如果达到次数限制,记录为离线消息。
         * 但是用户端并掉线/断线,从而没有触发重连机制,就导致这个这次消息不会实时更新到用户端,虽然消息被记录为离线消息。
         * 但是用户端不会触发去拉去离线消息机制,如果等到用户下一次上线或下一次拉取离线时,这条消息已经被后面的新消息顶到看不见了....
         */
        new Timer().schedule(new TimerTask() {
            public void run() {
                //获取待发消息集合的迭代器
                Iterator<MsgDTO> iterator = msgs.values().iterator();
                while (iterator.hasNext()){
                    MsgDTO msgDTO = iterator.next();
                    //如果在线就发送待发消息
                    if(ChannelHandler.getInstance().isOnline(msgDTO.getChannelId())){
                        //当前发送时间必须比上次发送时间至少间隔ChannelConst.QOS_DELAY
                        long cur_timestamp = System.currentTimeMillis();
                        if(System.currentTimeMillis()-msgDTO.getPreSendTimeStamp()<ChannelConst.QOS_DELAY){
                            continue;
                        }
                        Channel realChannel = ChannelHandler.getInstance().getChannelUDPFirst(msgDTO.getChannelId());
                        //记录当前发送时间
                        msgDTO.setPreSendTimeStamp(cur_timestamp);
                        baseSend(realChannel, msgDTO.getProtocol());
                    }else{
                        //如果不在线  就将当前待发消息直接删除
                        msgs.remove(msgDTO.getProtocol().getId());
                        //qos是在线才会触发 这里不在线代表 之前在线 后来不在线了 算是失败了 提供回调到业务层
                        ChannelConst.LOGIC_PROCESS.sendFailCallBack(msgDTO.getChannelId(),msgDTO.getProtocol());
                    }
                }
            }
        },500,ChannelConst.QOS_DELAY);
    }
    //待转发的消息集合
    private static ConcurrentHashMap<String, MsgDTO> msgs = new ConcurrentHashMap<>();

    /**
     * 接收到客户端回执
     */
    public static void ack(Protocol protocol) {
        //只需要删除对应的待发消息就行了--消息补偿流程就结束了
        if(Util.isNotEmpty(protocol.getId())){
            MsgDTO msgDTO = msgs.remove(protocol.getId());
            //成功回调
            ChannelConst.LOGIC_PROCESS.sendSuccessCallBack(msgDTO.getProtocol());
        }
    }
    /**
     * 统一的消息转发
     */
    public static void sendMsg(Channel channel, Protocol protocol){
        //开启了qos 客户端需要一个回执
        if(protocol.getAck()==100){
            //聊天消息必须携带一个消息ID 如果没有就回执报错
            if(!Util.isNotEmpty(protocol.getId())){
                ProtocolManager.sendAck(channel,ChannelConst.CHANNEL_MESSAGE_NO_ID);
                return;
            }
            //先给发送方一个消息回执,代表服务器收到了消息
            ProtocolManager.sendAck(channel,protocol,ChannelConst.CHANNEL_ACK);
            //判断是否已经存在待发送消息
            //判断这条消息是否已存在,如果已存在不做任何处理,否则会出现消息重复
            if(msgs.contains(protocol.getId())){
                return;
            }
            /**
             * 当消息不存在的时候 将当前消息作为当前客户端的待发消息进行存储,等待回执删除
             * 这里需要启动消息补偿机制,只需要将消息存入待转发消息集合就行
             * 但是这里的设计是:
             * 1。 对于发送着来说,会将发送者携带的消息编号存入集合 作为带转发消息(其实已经发了,只是需要等到客户端回执才会清除这个消息)而服务器会
             *     给客户端进行回执,告诉客户端服务器已经收到了你的消息,但是客户端还需要给服务器一个回执,让服务器清除这个待发送消息 形成完整闭环
             *     而客户端回执的这个消息又会与 需转发客户端的回执一样,服务器也就采用同样的处理,进行消息删除
             * 2。 对于需转发客户端来说,消息编号会新生成一个,与接收到的不同,而且不同的客户端生成不同的消息编号,与客户端对应
             */
            msgs.put(protocol.getId(),new MsgDTO(Util.getChannelId(channel),protocol));
        }
        /**
         * 不管是否在线 而且不管是否转发成功  都需要  --存到消息记录中
         * 这里不在回调函数中执行 是因为在回调中可能被执行多次 这样会导致添加消息的方法会被调用多次
         * 而回调函数一般用来做离线消息存储 或push推送等
         */
        ChannelConst.LOGIC_PROCESS.addMessage(protocol);
        //------------------------处理转发逻辑-----------------
        //利用线程池加速
        ThreadManager.getInstance().execute(()-> {
            //通过业务处理器获取多个目标并转发和qos
            ChannelConst.LOGIC_PROCESS.getTargets(protocol).forEach(target -> {
                sendMsg(target, protocol);
            });
        });
    }

    /**
     * 提供内外皆可调用的通用方法
     * 此方法会开启qos和结果回调
     */
    public static void sendMsg(String channelId, Protocol protocol){
        //判断目标是否在线 而且不是自己 自己不能给自己发消息
        if(ChannelHandler.getInstance().isOnline(channelId)&&!channelId.equals(protocol.getFrom())){
            //客户端要求启用qos机制才启用 ack==100
            if(protocol.getAck()==100){
                //将待转发消息存起来 给每个客户端对应当前消息生成一个唯一消息编号
                String new_msg_no = Util.getRandomStr();
                //将新的消息编号设置到消息中,替换原来的消息编号  原来的消息编号只与发送它的客户端对应
                protocol.setId(new_msg_no);
                msgs.put(new_msg_no,new MsgDTO(channelId,protocol));
            }
            //获取优先级最高的协议
            Channel realChannel = ChannelHandler.getInstance().getChannelUDPFirst(channelId);
            /** 直接发送 这里如果发送失败,会有补偿机制去做重发
             *  成功不需要做什么操作
             *  如果客户端收到ack为100的此条消息 客户端需要给服务器回执,不然服务器的qos队列消息不会删除
             */
            send(realChannel, protocol);
            return;
        }
        //对方客户端不在线 则直接调用失败回调函数通知
        ChannelConst.LOGIC_PROCESS.sendFailCallBack(channelId,protocol);
    }
    /**
     * 通用的发送数据方法
     * @param channel   客户端连接
     * @param protocol  发送的数据
     */
    public static void send(Channel channel,Object protocol){
        baseSend(channel,protocol).addListener((ChannelFutureListener) result -> {
            if(ChannelConst.LOGIC_PROCESS==null)return;
            //回调函数处理
            ChannelConst.LOGIC_PROCESS.sendCallBack(protocol);
        });
    }

    private static ChannelFuture baseSend(Channel channel,Object protocol) {
        switch (Util.getChannelProtoType(channel)){
            case WS:
                return channel.writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(protocol)));
            case TCP:
            case UDP:
                return channel.writeAndFlush(protocol);
        }
        return null;
    }

    /**
     * 统一回复客户端的应答方法
     */
    public static void sendAck(Channel channel,int type){
        baseSend(channel,new Protocol(type));
    }
    public static void sendAck(Channel channel, Protocol protocol, int type){
        //这里为了不对原对象进行修改,所以新new一个对象赋值 其中type为ack类型 ack为1代表应答包
        baseSend(channel,new Protocol(type,protocol.getFrom(),protocol.getTo(),protocol.getData(),1,protocol.getId()));
    }
}

项目结构截图:

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
您好!对于即时通讯项目的实战,可以考虑使用以下技术和步骤: 1. 选择合适的开发框架和技术:您可以选择使用现有的即时通讯框架,如Socket.IO、Firebase Realtime Database等,也可以使用自己搭建的WebSocket服务器。 2. 用户认证和授权:设计用户注册、登录和权限管理系统,确保只有授权用户可以使用您的即时通讯应用。 3. 建立消息传递机制:使用WebSocket或其他长连接技术来实现消息的实时传递,确保消息能够快速、可靠地到达目标用户。 4. 好友系统和联系人管理:设计好友系统,允许用户添加、删除和管理好友关系,同时提供联系人列表展示。 5. 实时聊天功能:实现即时聊天功能,包括一对一聊天和群组聊天。可以使用消息队列来处理聊天记录,确保消息的有序性和可靠性。 6. 消息推送和通知:为了及时通知用户收到新消息,可以使用推送技术,如APNs(iOS)和FCM(Android)。 7. 数据存储和同步:选择合适的数据库来存储用户数据和聊天记录,并确保数据的同步性和一致性。 8. 界面设计和用户体验:设计友好的界面,使用户能够方便地使用您的即时通讯应用。 9. 安全性和隐私保护:确保用户数据的安全性和隐私保护,采取必要的安全措施,如数据加密、防止消息劫持等。 10. 测试和优化:进行全面的测试,解决可能出现的bug和性能问题,优化应用的稳定性和响应速度。 希望以上步骤能对您的即时通讯项目实战有所帮助!如有更多问题,请随时提问。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

微笑い一刀

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值