资讯动态

Android仿QQ微信聊天系统:WebSocket消息链路与完整实现

发布时间:2026/9/10 17:17:02 来源:尧图企业网站定制
简介基于Android平台的仿QQ微信聊天系统项目第二部分专为具备Java基础、希望学习移动端即时通讯应用开发的读者打造围绕用户注册登录、好友管理、实时消息传递、群聊等核心功能展开同时覆盖Android四大组件生命周期、XML布局设计、网络通信、SQLite本地存储等关键知识点便于系统了解聊天类App的功能闭环。压缩包共251个文件包含Java源码、XML布局、class文件、PNG图片素材、APK安装包及工程配置文件其中Java源码负责业务逻辑、XML定义界面、APK可直接安装体验包体仅2.07MB结构紧凑可直接导入Android Studio分析学习或二次开发。内容预览中可见MainActivity、LoginActivity、BuddyActivity、ChatActivity等关键类表明项目已实现登录、好友、聊天等模块资源附带可运行APK便于对照界面与功能非常适合作为课程设计、毕业设计或求职作品集参考。目前已有649人学习下载读者可从中理解聊天类App的整体架构设计掌握用户认证、好友管理、消息收发、数据持久化等实现思路同时借鉴其分包命名与资源组织方式提升Android工程化开发能力。1. 仿QQ微信聊天系统先定位“消息链路”再谈界面看到“基于Android的仿QQ微信聊天系统”这个项目名大多数人第一反应是照着聊天界面截图去画气泡、背景和头像。真正把课程设计拖进死胡同的通常不是 UI而是消息链路A 发出去的消息怎么到达 B、连接断了怎么办、离线消息什么时候补拉。这个标题本质上是一个轻量 IM 系统包含 Android 客户端与最小可用服务端。“仿”字决定了只需要把 1 对 1 聊天、会话列表、未读数这三件事闭环不必碰群聊、表情商城这类重型功能。下面按传输层选型、界面还原、消息链路、存储建模、编译调试这条线展开所有命令和代码都可以直接落到 Android Studio 工程里跑中间涉及的参数会说明取值范围也适合拿来做二次改造。2. 仿QQ微信聊天系统的传输层选型WebSocket 与客户端分层聊天系统的技术选型第一件事是定传输层。UI 做得再像消息送不到用户手里就等于零。传输层决定了后续心跳、重连、离线消息、消息幂等怎么实现一动就是全局改动所以这一章先把方案对比讲清楚再落到工程依赖和服务端最小实现。2.1 传输层选型对比WebSocket、XMPP 与 MQTT方案协议形态Android 端实现成本适合场景关键取舍WebSocket基于 TCP 的全双工协议低OkHttp 原生支持轻量 IM、课程设计级聊天系统文本/二进制帧调试直观能直接复用现有 HTTP 端口XMPPXML 标准消息协议中需引入 Smack标准化协议、群聊扩展XML 冗长服务端模块重对单聊演示偏杀鸡用牛刀MQTT发布/订阅消息协议低Eclipse PahoIoT、弱网推送发布订阅语义不等于 1 对 1 会话需要自己封装会话层自研 TCP 长连接私有二进制协议高需处理粘包拆包高并发产品级 IM可控性最强但半包、粘包、序列化都要自己造轮子我一般选 WebSocket理由很直接Android 端 OkHttp 4.x 直接带 WebSocket 客户端服务端用 Netty 加一个 handler 就能对接调试时甚至可以用浏览器控制台模拟另一个客户端连上来发消息不需要额外装抓包工具。XMPP 看着标准但协议里大量 XML 字段对移动端不友好做头像、已读回执还要自己扩 extension工作量反而大。自研 TCP 链路要处理的东西太多对“仿 QQ 微信聊天系统”这个标题来说性价比太低。2.2 客户端模块划分与 Gradle 依赖配置客户端结构上照搬单 Activity 多 Fragment 的写法避免在多个 Activity 之间传消息对象。按登录、会话列表、聊天页、联系人、个人中心切成 feature 包网络层、数据库层、推送服务层放独立的core模块。这样后面替换消息实现或加群聊时改的是模块内部而不是调用方。android { compileSdk 34 defaultConfig { applicationId com.example.demoim minSdk 24 targetSdk 34 versionCode 1 versionName 1.0 } } dependencies { implementation androidx.appcompat:appcompat:1.6.1 implementation com.google.android.material:material:1.11.0 implementation androidx.constraintlayout:constraintlayout:2.1.4 implementation androidx.recyclerview:recyclerview:1.3.2 implementation androidx.swiperefreshlayout:swiperefreshlayout:1.1.0 // 网络与长连接 implementation com.squareup.okhttp3:okhttp:4.12.0 // 本地消息存储 implementation androidx.room:room-runtime:2.6.1 kapt androidx.room:room-compiler:2.6.1 // 图片加载 implementation com.github.bumptech.glide:glide:4.16.0 }逻辑说明compileSdk 和 targetSdk 保持 34是因为 Android 14 之后前台服务类型、通知权限行为都变了targetSdk 拉高能提前暴露问题minSdk 24 覆盖绝大多数存量设备同时能用java.time处理时间戳不必再和Date的时区问题纠缠。Room 负责消息表持久化配合kapt做编译期注解处理这里要记得在plugins里配好kotlin-kapt否则会直接报找不到处理器。Glide 负责聊天气泡里的图片加载头像也用同一套缓存。上面的版本号以你 build 时解析到的最新稳定 patch 为准别在 gradle 里写动态版本否则换台机器就构建出不一样的依赖树。2.3 服务端最小可用设计转发与存储分离仿 IM 项目的服务端不需要做成腾讯那样强壮。一个 HTTP 接口做登录换 token一个 WebSocket 端口做消息转发就够了。转发逻辑先不落库消息根据to字段找到目标连接直接发出去用户离线时再写离线表等客户端重连后拉取。public void messageReceived(ChannelHandlerContext ctx, TextWebSocketFrame frame) { JsonObject msg parse(frame.text()); String to msg.get(to).getAsString(); Channel target sessionManager.get(to); if (target ! null target.isActive()) { // 在线直接转发并回 ACK 给发送方 target.writeAndFlush(new TextWebSocketFrame(frame.text())); ack(ctx, msg.get(msgId).getAsString(), SUCCESS); } else { // 离线写入离线表等客户端调用 sync 拉取 offlineMessageStore.save(msg); ack(ctx, msg.get(msgId).getAsString(), DELIVERED); } }逻辑说明先判断在线再落离线表顺序不能反过来因为在线状态下走了离线表会导致消息延迟用户看到自己发的消息转了一圈才出去。离线写入不要直接在 Netty 的 IO 线程里同步写数据库否则高并发下 IO 线程会阻塞课程设计级项目可以先用一个ConcurrentHashMap做离线缓冲再起一个定时任务批量刷库。sessionManager.get(to)这段确保消息只投递给与to字段匹配的单个连接不做群发符合单聊定位。3. 用协调布局和 RecyclerView 还原仿QQ微信聊天系统的消息界面UI 层是开发者的舒适区但也是埋坑最容易的地方。仿 IM 界面要解决三个问题不同消息类型的复用、输入栏与软键盘的冲突、历史消息分页加载。这三个问题处理不当界面就会在真机上出现“键盘顶飞列表”“加载旧消息时列表跳动”这种很掉价的表现。3.1 聊天气泡的 ViewType 复用与发送状态消息列表的 item 分成自己发的和对方发的两种但左右两侧的 ViewType 只需要两种不要把图片消息、文本消息、时间消息各做一套 layout。用消息类型字段区分内容用方向字段区分左右否则 item 数量膨胀后维护成本直线上升。class ChatAdapter( private val messages: MutableListMessage, ) : RecyclerView.AdapterChatAdapter.MessageHolder() { companion object { private const val TYPE_LEFT 0 private const val TYPE_RIGHT 1 } override fun getItemViewType(position: Int): Int if (messages[position].fromMe()) TYPE_RIGHT else TYPE_LEFT override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MessageHolder if (viewType TYPE_RIGHT) MessageHolder(layoutInflater.inflate(R.layout.item_msg_right, parent, false)) else MessageHolder(layoutInflater.inflate(R.layout.item_msg_left, parent, false)) override fun onBindViewHolder(holder: MessageHolder, position: Int) { val msg messages[position] holder.content.text msg.content when (msg.status) { SENDING - holder.progress.visibility View.VISIBLE FAILED - holder.progress.visibility View.GONE SUCCESS - holder.progress.visibility View.GONE } } }逻辑说明getItemViewType只按消息方向返回文本、图片、语音等类型在onBindViewHolder里用when分支处理这样新增引用消息类型时不用改 Adapter 的结构。SENDING状态时显示一个 12dp 的ProgressBar而不是把整个 item 置灰这样用户在弱网下也能看到消息正在往外发。FAILED状态要把进度条关掉同时显示一个重发按钮按钮点击事件通过接口回调到 ViewModel在 ViewModel 里重新走发送流程不要在 Adapter 里直接操作数据库。3.2 输入栏与软键盘冲突的三种处理方式聊天气泡画好了一弹键盘就露馅。常见处理方式有三种方案配置适用场景风险adjustResizeActivity 的 windowSoftInputMode 设为adjustResize根布局是非滚动容器部分国产 ROM 上不生效CoordinatorLayout根布局用协调布局RecyclerView 配 behavior页面结构较常规需要正确设置 fitsSystemWindowsadjustNothing监听 ViewTreeObserver 手动计算剩余高度需要同时处理拍照/表情面板代码量最大但可控性最好androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:androidhttp://schemas.android.com/apk/res/android xmlns:apphttp://schemas.android.com/apk/res-auto android:layout_widthmatch_parent android:layout_heightmatch_parent androidx.recyclerview.widget.RecyclerView android:idid/chat_list android:layout_widthmatch_parent android:layout_heightmatch_parent app:layout_behaviorstring/appbar_scrolling_view_behavior / LinearLayout android:idid/input_bar android:layout_widthmatch_parent android:layout_heightwrap_content android:layout_gravitybottom android:orientationhorizontal EditText android:idid/input_text android:layout_width0dp android:layout_heightwrap_content android:layout_weight1 / Button android:idid/btn_send android:layout_widthwrap_content android:layout_heightwrap_content / /LinearLayout /androidx.coordinatorlayout.widget.CoordinatorLayout逻辑说明app:layout_behaviorstring/appbar_scrolling_view_behavior是配合 AppBarLayout 用的如果聊天页没有 AppBarRecyclerView 不会自动避让输入栏。更稳的做法是给根布局设置fitsSystemWindowstrue并手动监听WindowInsetsCompat里的 IME 高度再设置 RecyclerView 的下 padding。注意adjustResize在华为和小米的部分定制系统上不一定生效测试时一定要用真机不要只看模拟器。输入栏如果还要支持“按住说话”应该把语音按钮和文字输入放在同一个布局里通过visibility切换避免每次切换都重新绘制整个底部区域。3.3 历史消息加载时的分页与进度条状态会话列表往下翻永远是新的往上翻要加载旧消息。这个需求不需要 Paging 库RecyclerView 的OnScrollListener足够。触发条件是列表完全回到顶部也就是computeVerticalScrollOffset() 0的瞬间。chatList.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { if (recyclerView.computeVerticalScrollOffset() 0 !isLoading) { isLoading true adapter.appendHeaderProgressBar() viewModel.loadPreviousPage() } } })逻辑说明computeVerticalScrollOffset()为 0 表示当前列表滚到了最顶部此时触发上一页加载。加载上一页时在位置 0 插入一个 header 形式的进度条也就是一个占满 item 宽度的ProgressBar不是加在列表末尾的 footer。isLoading这个布尔值必须置位否则用户快速上滑会连续触发多次网络请求造成消息分页重复。pageSize 默认取 20拉到 30 以上在低端机上滚动会有掉帧风险加载完成后移除 progress item并用chatList.post { chatList.scrollToPosition(previousCount - 1) }把列表锚在原来可见位置附近否则用户会看到列表突然跳到了新插入的旧消息这是仿 IM 项目里最常见的分页体验问题。4. 仿QQ微信聊天系统的消息链路Socket 心跳、重连与 SQLite 建模如果说 UI 是面子消息链路就是里子。把消息从 A 送到 B拆开来看是四步建连、保活、发送、落库。每一步都有默认参数也都有对应的坑。网络抖动时消息丢失多半不是服务器问题而是心跳参数和重连策略没配对。4.1 WebSocket 长连接封装与心跳参数设置用 OkHttp 封装长连接时最容易被忽略的是超时时间。普通 HTTP 请求的超时逻辑不能直接套在 WebSocket 上否则读超时会周期性断掉长连接。class IMWebSocket( private val url: String, private val messageListener: (String) - Unit, ) { private val client OkHttpClient.Builder() .readTimeout(0, TimeUnit.MILLISECONDS) .writeTimeout(0, TimeUnit.MILLISECONDS) .pingInterval(30, TimeUnit.SECONDS) .build() private var ws: WebSocket? null fun connect(token: String) { val request Request.Builder() .url(url) .header(Authorization, Bearer $token) .build() ws client.newWebSocket(request, webSocketListener()) } fun send(content: String) { ws?.send(content) } private fun webSocketListener() object : WebSocketListener() { override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { scheduleReconnect() } override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { scheduleReconnect() } override fun onMessage(webSocket: WebSocket, text: String) { messageListener(text) } } }逻辑说明readTimeout和writeTimeout必须是 0否则 OkHttp 默认的 10 秒超时会在空闲时把连接掐掉。pingInterval(30, TimeUnit.SECONDS)控制底层 Ping 帧服务端 Netty 通常会在 60 秒左右判定死链客户端 30 秒发一帧能保证链路活跃。应用层还要加一个业务心跳登录成功后再启动定时器每 25 秒发{type:heartbeat,ts:...}比底层 Ping 帧短 5 秒目的是让服务端能验证“用户态在线”而不只是“TCP 在线”。重连退避建议按指数走1s、2s、4s封顶 30s每次重连都重置退避计数否则弱网恢复瞬间会扎堆重连。4.2 消息协议、去重与重试消息协议不要设计太长够用就好。核心字段是msgId、from、to、content、timestamp{ type: message, msgId: a3f2c1e0-8b64-4a10-89dc-1234567890ab, from: u_1001, to: u_2002, content: hello, timestamp: 1699999999999 }msgId必须是客户端生成用 UUID 就行。服务端拿它做幂等收到重复msgId直接回 ACK不再转发。timestamp用客户端本地时间不要用服务端时间回传因为收发双方都会拿这个字段做排序两端时间不同步会导致消息顺序看起来是乱的。发送流程上客户端先把消息插本地库状态置为SENDING同时把 WebSocket 消息发出去收到服务端 ACK 后把状态改为SUCCESS15 秒没等到 ACK 就标记FAILED让用户手动触发重发。15 秒是经验值要避开和心跳间隔重叠否则心跳回执和消息 ACK 在同一个时间窗口到达会干扰异常判断。4.3 消息表结构与未读数计算Room 的建表语句直接决定后面查询是否顺畅。消息表用msg_id当主键天然去重CREATE TABLE message ( msg_id TEXT PRIMARY KEY, session_id TEXT NOT NULL, from_user TEXT NOT NULL, to_user TEXT NOT NULL, content TEXT NOT NULL, type INTEGER NOT NULL DEFAULT 0, status INTEGER NOT NULL DEFAULT 0, create_at INTEGER NOT NULL ); CREATE INDEX idx_message_session ON message(session_id, create_at DESC); CREATE INDEX idx_message_status ON message(status);逻辑说明msg_id直接做主键避免重复插入。session_id是会话 key单聊场景下用排序后的用户 ID 拼接比如u_1001_u_2002这样 A 找 B 和 B 找 A 落到同一个会话。create_at存毫秒时间戳排序用ORDER BY create_at ASC。索引建在(session_id, create_at DESC)上正好覆盖“查某个会话的历史消息”这个高频查询。未读数不建议每次COUNT(*)再算消息表到几万条之后 COUNT 会明显拖慢会话列表常见的做法是建一个独立的会话表用unread_count字段维护收到新消息时自增一次进入会话时清零。已读位置单独记录不要每条消息都标记已读。4.4 离线消息的补拉策略用户关掉 App 十分钟再打开这段窗口期的消息要靠重连后拉取。最简单实现是客户端本地记录lastSyncTime重连成功后发一个同步请求{ type: sync, lastSyncTime: 1699999999999, limit: 100 }服务端按create_at过滤出该时间点之后、且to或from与当前用户相关的消息按会话分组返回。客户端收到后先按msg_id去重再落库最后统一刷一遍会话列表的未读数。这个同步请求放在 WebSocket 层发不要在重连瞬间发 HTTP因为 WebSocket 连接建立本身就代表链路恢复了HTTP 还要再走一次握手和鉴权慢一步。服务端离线表只留最近 7 天的消息超过 7 天直接清理课程设计级系统不需要做“永久漫游”否则本地库膨胀后启动速度会越来越差。5. 打包与调试Android 仿QQ微信聊天系统的到达率检查清单很多仿 IM 项目卡在编译阶段而不是业务代码。版本策略定好后能少走弯路minSdk 24、targetSdk 34、compileSdk 34 是我常用的组合。SDK Manager 里勾选对应 API 级别时如果出现“无法勾选”的情况先确认安装盘剩余空间和维护工具链的磁盘权限换个目录重新安装 SDK 通常能解决。打开旧工程报Unsupported class file major version多半是 Gradle JDK 版本太低Android Studio 里把 Gradle JDK 切到 17 再同步。编译系统镜像或集成外部 SDK 时如果碰到build tag number over 30 is not supported先排查是不是把 framework 编译产物混进了 App 依赖这种 tag 不出现在普通 App 工程里通常要把对应的 SDK 改成provided引入避免参与 dex 打包。targetSdk 升到 34 后/storage/emulated/0/Android/data目录不能像以前那样直接访问聊天系统里的图片预览、文件下载模块如果还在用老路径要尽快切到 MediaStore 或getExternalFilesDir否则在 Android 14 真机上会直接抛权限异常。这一项很容易被当作“偶发闪退”实际上是从 targetSdk 30 开始就有的分区存储限制越早改越好。5.1 WebSocket 服务放进前台服务的注意事项消息接收要保证 App 退到后台也不断常见做法是把 WebSocket 放在前台服务里。Android 14 对前台服务类型有限制类型不对会在启动时抛MissingForegroundServiceTypeExceptionManifest 里需要做对应声明uses-permission android:nameandroid.permission.INTERNET/ uses-permission android:nameandroid.permission.POST_NOTIFICATIONS/ uses-permission android:nameandroid.permission.FOREGROUND_SERVICE/ uses-permission android:nameandroid.permission.FOREGROUND_SERVICE_DATA_SYNC/ service android:name.IMService android:foregroundServiceTypedataSync android:exportedfalse/逻辑说明POST_NOTIFICATIONS是 Android 13 之后必须运行时申请的通知权限不申请的话前台服务能启动但通知栏不显示用户会以为消息完全没通知。foregroundServiceTypedataSync对应数据同步场景比较贴合 IM 长连接如果服务还处理蓝牙设备连接那要改用connectedDevice类型写错在 Android 14 真机上启动会直接崩溃。前台服务的通知栏建议放一个小文本“连接已建立”不要放自定义 View厂商 ROM 对自定义通知样式的兼容性差异很大。5.2 用送达回执验证端到端链路功能写完后拿两台真机把链路完整验一遍。我习惯用这张检查表快速定位问题场景操作预期结果建连登录后看 Logcat 过滤 IM 标签输出 connected无异常堆栈心跳放置 2 分钟不动服务端窗口没有断开记录在线消息A 发 “hello”B 的列表立刻出现回复能回到 A离线消息B 杀掉进程A 发 3 条B 重新打开B 自动触发 sync未读显示 3弱网飞行模式 10 秒后恢复自动重连成功期间消息未丢失在线消息验证时注意看 B 端的消息时间戳用的是 B 侧本地时间不是 A 的如果发现顺序错乱检查是否把timestamp错误地替换成了服务端时间。离线消息验证要留意lastSyncTime的取值它以服务端消息落库时间为准而不是客户端本地时间否则两次拉取之间会因为时钟偏差重复拉取或漏拉。弱网恢复后断线期间发的消息要能通过 sync 或重连后的 ACK 机制重新拿到如果发现消息消失先看服务端离线表里到底有没有写入再看客户端重连后的 sync 请求是否真的发出去了。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价