查看原文
其他

Android IM即时通信,设计一个聊天中间件吧

狼窝山下的青年 郭霖 2023-03-24


/   今日科技快讯   /

近日,多家海外媒体报道,脸书(Facebook)母公司 Meta 计划裁员1万名员工,并关闭约5000个空缺职位的招聘。

去年11月该公司曾宣布解雇1.1万人,约占该公司员工总数的13%,路透社称 Meta 是第一家宣布二次大规模裁员的大型科技公司。据科技媒体 TechCrunch 报道,此次裁员将分批次进行,技术部门将于4月开始裁员,业务部门的裁员或发生在5月。

/   作者简介   /

本篇文章来自狼窝山下的青年的投稿,文章主要分享了他设计和实现一个即时通信中间件的过程,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章。

狼窝山下的青年的博客地址:
https://juejin.cn/user/2365804752418232

/   前言   /

本项目是一个基于多进程的 Android IM 即时通信中间件,旨在帮助 Android 开发者了解如何在实现IM即时通信的同时,提高应用程序的稳定性和安全性。

通过本项目,您将了解到多进程在 Android IM 即时通信中的重要性,并学习到如何设计和实现一个基于 WebSocket 的中间件。我们将深入讲解多进程架构和流程设计,并展示详细的时序图和类图,让您更好地理解该中间件的实现过程。

此外,我们还将介绍如何通过多进程通信来实现消息传递和状态同步,提高应用程序的性能和稳定性。最后,我们将展示如何将该中间件集成到您的 Android 应用程序中,让您的用户可以更加快速、可靠地进行即时通信。

如果您是一个 Android 开发者,并且正在寻找一种稳定、安全、可扩展的即时通信解决方案,那么本项目将是您的不二选择。

客户端项目地址:
https://github.com/kongxiaoan/IM-Middleware.git



/   移动端即时通信特点   /

  1. 即时传输:即时通信是指消息的传输速度非常快,消息可以在几乎同时发送和接收。
  2. 实时反馈:移动端即时通信通常能够提供实时反馈,让用户可以迅速得到对方的回应,从而使得交流更加流畅。
  3. 多媒体支持:移动端即时通信可以支持多种类型的媒体文件,如文字、图片、语音、视频等,使得用户能够更加方便地进行交流和分享。
  4. 多设备同步:移动端即时通信通常能够支持多设备同步,即同一个用户可以在不同的设备上同时登录并进行交流,而且消息会自动同步到所有的设备上。
  5. 安全性:移动端即时通信在数据传输过程中通常会采用加密技术,确保用户的通信内容不会被窃取或篡改。
  6. 个性化设置:移动端即时通信通常能够提供个性化设置,例如自定义聊天背景、表情包、字体颜色等,让用户可以更加自由地表达自己。
  7. 群组聊天:移动端即时通信通常能够支持群组聊天,用户可以创建一个群组并邀请其他用户加入,从而进行多人交流。
  8. 在线状态:移动端即时通信通常能够显示用户的在线状态,让用户可以了解对方是否在线,并且可以方便地与对方建立联系。

这些所有的特性都是在“可以实现快速、方便、实时的沟通和交流”的基础上实现的,然而在 Android 客户端,能否实时取决于进程是否存活,如果进程被回收了,那这些特性都将失效。

做 Android 的都知道,进程的回收 APP 是自己管控不了的。

/   Android进程   /

在 Android 系统中,进程是指正在运行的应用程序的实例。每个进程都有自己独立的虚拟机和系统资源,它们之间相互隔离,互不干扰。

进程通常由一个或多个组件构成,如 Activity、Service、BroadcastReceiver 和 ContentProvider 等。每个组件都运行在它所在的进程中,但不同的组件可以运行在不同的进程中。

生命周期受到系统内存管理机制的影响。当系统内存不足时,系统会根据一定的策略来回收一些进程,以释放内存资源。Android 进程管理机制是基于 Linux 内核的,系统会根据进程的重要性和内存占用情况来进行进程回收和优先级调度。

Android 进程并不是一个静态的概念,它的状态是动态变化的。应用程序启动时会创建进程,当应用程序不再需要该进程时,系统会将其回收。

进程基本分为四种类型


  1. 前台进程(Foreground Process):这类进程是指正在与用户交互的应用程序进程,如正在显示在屏幕上的 Activity,或正在播放音乐的应用程序进程等。前台进程优先级最高,系统会尽可能保持这类进程的运行状态,以确保用户能够流畅地使用应用程序。
  2. 可见进程(Visible Process):这类进程是指虽然没有直接与用户进行交互,但是对用户当前操作有影响的应用程序进程,如正在后台播放音乐的应用程序进程等。可见进程的优先级次于前台进程,但仍然比较高,系统会尽可能保持这类进程的运行状态。
  3. 后台进程(Background Process):这类进程是指已经被用户关闭,但是仍然在后台运行的应用程序进程。后台进程的优先级较低,当系统内存占用过高时,系统会优先回收这类进程来释放内存。
  4. 空进程(Empty Process):这类进程是指系统为了加速应用程序的启动而创建的进程,但是没有实际运行任何应用程序代码的进程。这类进程的主要作用是为应用程序的启动提供一个空壳,以缩短启动时间。

以上四类进程的优先级和占用资源等因素都会影响 Android 系统的性能和稳定性,因此,在开发应用程序时,需要尽可能地减少内存的占用和资源的浪费,以提高系统的响应速度和用户体验。

进程回收


为了保证系统的稳定性和资源管理的效率,系统会对进程进行回收。进程回收的流程如下:

  1. 当系统出现内存不足的情况时,系统会根据进程的优先级和占用的资源量等因素,选择一些进程进行回收。
  2. 系统首先会杀掉进程中的所有后台进程和服务进程,释放它们占用的资源,但会保留前台进程和用户正在交互的应用程序进程。
  3. 如果系统还是内存不足,那么就会继续回收前台进程和用户正在交互的应用程序进程,直到系统获得足够的内存为止。


单进程和多进程的区别


  1. 应用单进程回收:如果应用程序只有一个进程,那么当系统回收进程时,整个应用程序都会被回收,包括应用程序中的所有组件和数据等信息。这会导致应用程序需要重新启动,并重新加载数据等信息,用户体验不佳。
  2. 应用多进程回收:如果应用程序采用多进程的方式来运行,那么当系统回收进程时,只有被回收的进程会被停止,其他进程仍然可以继续运行,从而保证应用程序的部分功能可以正常使用。这种情况下,用户体验相对较好,但同时也会增加系统的负担和内存占用量。

在开发中多进程拉活虽然不能保活了,但是也是伪保活的一种重要手段。

/   即时通信对于Android进程该如何做   /

其实简单的过一下 Android 进程相关的东西,是为了说明 APP 对自身的生命周期基本没有控制力,要想保证即时通信成立,所在进程活着,显然是最主要的。

什么是服务进程


服务进程并不是进程的分类,而是进程的一种特殊用途。在 Android 中,服务进程是指运行服务组件的进程。服务进程通常会在应用程序启动时被创建,并在整个应用程序的生命周期内一直存在,直到应用程序被关闭或者系统内存不足时被回收。

服务进程通常用于执行一些需要在后台长期运行的任务,如播放音乐、下载文件、推送消息等。将这些任务放在服务进程中可以保证它们不会因为应用程序被关闭而中断,从而保证应用程序的持久连接和稳定性。

即时通信应该切进程


  1. 为了确保即时通信的实时性,通常将即时通信放在服务进程中。服务进程拥有较高的优先级和稳定的生命周期,系统会优先保留服务进程,而且服务进程的优先级比普通应用程序进程的优先级高,能够更长时间地保留在内存中。因此,将即时通信放在服务进程中,可以保证即时通信的稳定性和实时性。
  2. 服务进程与应用程序进程是分离的,即使应用程序进程被回收,服务进程仍然可以继续运行,从而保证即时通信的持续性。另外,服务进程还可以通过绑定方式,为应用程序进程提供即时通信的功能,从而提高了通信的效率和稳定性。


切进程的优势


看完上面的一长串介绍后应该理解,切进程的优势是非常巨大的,总结一下:

  1. 提高响应速度:将即时通信相关的任务放在单独的进程中处理,可以避免应用程序主进程的阻塞,从而提高应用程序的响应速度和用户体验。
  2. 提高稳定性:将即时通信相关的任务放在单独的进程中处理,可以减少应用程序崩溃的风险,从而提高应用程序的稳定性。
  3. 方便管理:将即时通信相关的任务放在单独的进程中处理,可以方便地对其进行管理和调试,从而提高开发效率。

/   客户端组件结构物理设计   /

将 APP 作为客户端,依赖 IM 中间件提供的接口实现即时通信。中间件作为服务进程独立运行,负责长连接、Push、Https等数据源处理,将处理后的数据通过 AIDL 回调到 APP 中进行显示和处理。

可以有效避免进程回收对通信质量的影响,提高即时通信的稳定性和可靠性。同时,中间件作为服务进程独立于应用进程运行,可以保证即使应用进程被回收,也不会影响即时通信服务的运行。这种设计方案可以让数据处理逻辑与应用程序逻辑分离,提高代码的可维护性和扩展性。

如下图(箭头方向为依赖方向):


简单类图表示物理结构为:



  1. Application:表示 Android 应用程序的入口,负责启动和管理 IM 中间件进程;
  2. IMiddleware:表示 IM 中间件的接口,定义了登录和发送消息的方法;
  3. Middleware:表示 IM 中间件的实现,实现了 IMiddleware 接口,负责长连接、Push、Https 等数据源处理;
  4. ICallback:表示回调接口,定义了消息接收的回调方法;
  5. Callback:表示回调的实现,实现了 ICallback 接口,负责将接收到的消息回调到应用程序中;
  6. Application依赖 Middleware,即应用程序依赖IM中间件;
  7. Middleware实现了 IMiddleware 接口,提供了登录和发送消息的功能;
  8. Middleware依赖 Callback,即中间件依赖回调实现,用于将接收到的消息回调到应用程序中;
  9. Callback实现了 ICallback 接口,提供了消息接收的回调功能。


调用时序图


/   详细IM中间件设计   /

进程间通信接口设计


按照接口隔离原则,我们可以将接口分为两个部分:

客户端向服务端

  • 注册/反注册客户端接口
  • 发送消息

interface IMessageProvider{
    void sendMessage(String message);

    void registerMessageReceiveListener(IMMessageReceiver messageReceiver);

    void unRegisterMessageReceiveListener(IMMessageReceiver messageReceiver);

    void registerLoginReceiveListener(IMLoginStatusReceiver loginStatusReceiver);

    void unRegisterLoginReceiveListener(IMLoginStatusReceiver loginStatusReceiver);
}

服务端向客户端

  • 发送消息


    // 转发服务器消息
    interface IMMessageReceiver {
        void onMessageReceived(in MessageModel receiveMessage);
    }

  • 同步登录状态

     interface IMLoginStatusReceiver {
       void loginStatus(int status);
   }

传输数据

目前先使用 model 代替,后续采取 pb 传输。

中间件的流程



  1. 客户端调用 IMClient 的 init 方法,并传入参数 Application、IMLoginStatusReceiver.Stub 实现类、IMMessageReceiver.Stub 实现类。
  2. IMClient 在 init 方法中启动 MessageService,并返回 Binder 对象。
  3. 客户端通过 ServiceConnection 连接 MessageService,连接成功后 IMClient 通过 MessageService 注册 IMMessageReceiver 和IMLoginStatusReceiver。
  4. MessageService 在注册 IMLoginStatusReceiver 成功后开始调用 WebSocket 连接,连接成功后通过 IMLoginStatusReceiver 的回调接口通知客户端登录状态的变化。
  5. 客户端通过 IMClient 的 send 方法发送消息,IMClient 在回调接口中调用 WebSocket 的发送方法发送消息。
  6. WebSocket 接收到消息后,通过已经注册的 IMMessageReceiver 回调给客户端。
  7. 客户端调用 IMClient 的 loginOut 方法,IMClient 反注册 IMMessageReceiver 和 IMLoginStatusReceiver,并取消 WebSocket 连接。
  8. 最后,IMClient 停止 MessageService,MessageService 销毁。

/   中间件   /



    开发一个websocket服务器

    写 Demo 的时候没有调试用的服务器,简单实用 node.js 写一个。

    环境

    Node

    Mac 上安装 Node.js 的步骤:

    1. 前往 Node.js 官网(nodejs.org/)下载 Node.js 的安装程序。
    2. 打开下载好的安装程序,按照提示进行安装。
    3. 安装完成后,打开终端应用程序(Terminal),输入以下命令检查 Node.js 是否已正确安装:

    Copy code
    node -v

    1. 如果成功安装了 Node.js,会显示 Node.js 的版本号。


    脚本


    const WebSocket = require('ws');

    // 创建 WebSocket 服务器
    const server = new WebSocket.Server({ port: 8080 });

    // 监听连接事件
    server.on('connection', (socket) => {
      console.log('Client connected');

      // 监听消息事件
      socket.on('message', (data) => {
        console.log(`Received message: ${data}`);

        // 发送消息到客户端
        socket.send(`You sent: ${data}`);
      });

      // 监听断开连接事件
      socket.on('close', () => {
        console.log('Client disconnected');
      });
    });

    console.log('WebSocket server started');

    将脚本保存为 .js 文件,然后在终端应用程序中使用以下命令运行脚本:

    node 文件名.js 

    其中,“文件名.js”是你保存脚本的文件名。运行该命令后,将启动 WebSocket 服务器,并开始监听本地的 8080 端口。启动成功后。


    中间件开发

    不熟悉 AIDL 开发的可以找资料看看,整个中间件部分都有 IMClient 控制,客户端在进行操作时,只需要调用 IMClient 即可。


    MessageService 鉴权


    为了安全起见,我们可以使用自定义权限和包名鉴别启动程序是否为合法程序。

    override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean {
        var packageName: String? = null
        val packages: Array<String> =
            packageManager.getPackagesForUid(getCallingUid()) ?: arrayOf()
        if (packages.isNotEmpty()) {
            packageName = packages[0]
        }
        // 指定包名
        if (packageName == null || !packageName.startsWith("com.example")) {
            Logger.log("权限校验失败  $packageName")
            throw RuntimeException("非法调用 $packageName")
        }
        return super.onTransact(code, data, reply, flags)
    }

    自定义权限

    1. 在 AndroidManifest 中添加:

    <permission
        android:name="com.example.mylibrary.permission.REMOTE_SERVICE_PERMISSION"
        android:protectionLevel="normal" />

    1. 在 MessageService 中进行检查。

    override fun onBind(intent: Intent?): IBinder? {
        if (checkCallingOrSelfPermission("com.example.mylibrary.permission.REMOTE_SERVICE_PERMISSION") == PackageManager.PERMISSION_DENIED) {
            throw RuntimeException("非法调用, 未添加正确权限")
        }
        return messageSender
    }

    初始化


    由于是多进程,所以需要在 Application 中判断进程,在应用进程中初始化即可。

    private fun init() {
        val myPid = Process.myPid()
        val mActivityManager =
            this.getSystemService(Context.ACTIVITY_SERVICE) as android.app.ActivityManager
        val var3 = mActivityManager.runningAppProcesses?.iterator()
        while (var3?.hasNext() == true) {
            val appProcessInfo = var3.next() as android.app.ActivityManager.RunningAppProcessInfo
            if (appProcessInfo.pid == myPid && appProcessInfo.processName.equals(
                    this.packageName,
                    ignoreCase = true
                )
            ) {
                this.initApp()
                break
            }
        }
    }

    // 初始化东西
    abstract fun initApp()

    在应用中提供工具类,做与中间件的交互。

    /**
     * 初始化IM
     */
    fun init(application: Application) {
        Logger.log("IM 初始化")
        IMClient.init(application, IMParams.Builder().build(), object : IMLoginStatusReceiver.Stub() {
            override fun loginStatus(status: Int) {
                Logger.log("登录状态 $status")
            }
        }, IMReceiver())
    }

    /**
     * 发送消息
     */
    fun send(message: String) {
        IMClient.send(message)
    }

    /**
     * 登出
     */
    fun logOut() {
        IMClient.loginOut()
    }

    消息接收

    class IMReceiver : IMMessageReceiver.Stub() {
        override fun onMessageReceived(receiveMessage: MessageModel?) {
            Logger.log("客户端接收到的消息 $receiveMessage")
        }
    }

    连接websocket


    object WebSocketManager {
        private const val WS_URL = "ws://192.168.31.222:8080"

        private val httpClient by lazy {
            OkHttpClient().newBuilder()
                .readTimeout(10, TimeUnit.SECONDS)
                .writeTimeout(10, TimeUnit.SECONDS)
                .connectTimeout(10, TimeUnit.SECONDS)
                .pingInterval(40, TimeUnit.SECONDS)
                .retryOnConnectionFailure(true)
                .build()
        }

        private var mWebSocket: WebSocket? = null

        public fun connect() {
            val request = Request.Builder()
                .url(WS_URL)
                .build()
            mWebSocket = httpClient.newWebSocket(request, wsListener)
        }

        fun release() {
            mWebSocket?.cancel()
        }

        fun send(message: String) {
            mWebSocket?.send(message)
        }

        /**
         * 获取当前进程名
         *
         * @param context 上下文
         * @return
         */
        fun getCurProcessName(context: Context): String? {
            // 获取此进程的标识符
            val pid = Process.myPid()
            // 获取活动管理器
            val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
            // 从应用程序进程列表找到当前进程,是:返回当前进程名
            for (appProcess in activityManager.runningAppProcesses) {
                if (appProcess.pid == pid) {
                    return appProcess.processName
                }
            }
            return null
        }


        private val wsListener = object : WebSocketListener() {
            override fun onOpen(webSocket: WebSocket, response: Response) {
                super.onOpen(webSocket, response)
                Logger.log("onOpen ${webSocket == null}")
                mWebSocket = webSocket
                IMClient.sendLoginStatus(IMLoginStatus.CONNECT_SUCCESS.ordinal)
                Logger.log("onOpen ${mWebSocket == null}")
                webSocket.send(
                    "我是客户端代码发送的 ${
                        IMClient?.mApplication?.applicationContext?.let {
                            getCurProcessName(
                                it
                            )
                        }
                    }"
                )
                IMClient.loginCallback?.loginStatus(IMLoginStatus.CONNECT_SUCCESS.ordinal)
            }

            override fun onMessage(webSocket: WebSocket, text: String) {
                super.onMessage(webSocket, text)
                Logger.log("onMessage text $text")
                val messageModel = MessageModel().apply {
                    from = "service"
                    to = "client"
                    content = "${System.currentTimeMillis()}"
                }
                IMClient.mReceiver?.onMessageReceived(messageModel)
            }

            override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
                super.onClosed(webSocket, code, reason)
                IMClient.sendLoginStatus(IMLoginStatus.CONNECT_FAIL.ordinal)
            }

            override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
                super.onClosing(webSocket, code, reason)
                IMClient.sendLoginStatus(IMLoginStatus.CONNECT_FAIL.ordinal)
            }

            override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
                super.onFailure(webSocket, t, response)
                IMClient.sendLoginStatus(IMLoginStatus.CONNECT_FAIL.ordinal)
                Logger.log("onFailure " + t.localizedMessage)
            }
        }


    }

    Demo

    目前完成

    1. IM单进程,双向可以通信
    2. webscoket 连接
    3. 添加 webscoket 登录验证、心跳等常规操作

    后续规划

    1. 添加重试机制
    2. 添加中间件数据传输封装(使用pb)
    3. 完善离线消息处理
    4. 想到啥写啥

    项目地址:
    https://github.com/kongxiaoan/IM-Middleware

    推荐阅读:
    我的新书,《第一行代码 第3版》已出版!
    Android 14 Developer Preview一览
    利用反编译,仿写一个小红书图片指示器吧

    欢迎关注我的公众号
    学习技术或投稿


    长按上图,识别图中二维码即可关注

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存