简介:这是一套面向移动端与桌面端全栈开发者的即时通讯应用开源源码,适用于希望快速构建仿微信社交社区、掌握双端(iOS/Android+Windows/macOS)实时通信架构的中高级开发者。资源包含2000个文件,主体为323个Java(Android客户端逻辑)、489个JS(前端交互与PC端Web技术栈)、380个H头文件与242个M实现文件(iOS原生模块),辅以JSON配置、XML布局、CSS样式及Markdown文档,总大小120.24MB,结构完整覆盖客户端、服务通信、UI组件与多媒体处理等核心模块。已有299人学习下载,适合用于深入理解IM协议集成、跨平台消息同步机制、好友关系链设计及群组动态分享等典型社交功能实现。源码可直接编译运行,含清晰目录分层与注释,是学习高并发通信、本地存储优化与多端状态一致性方案的优质实践材料。
1. 这不是“仿微信”的UI套壳,而是用原生能力重建即时通讯核心链路的双端工程
很多人看到“原生仿微信”第一反应是:又一个带圆角头像和气泡消息的壳子。但真正打开这个.zip包会发现,它不依赖任何跨平台框架(如 Flutter、React Native 或 UniApp),Android 端用 Java/Kotlin 直接调用WorkManager+ForegroundService管理长连接保活,iOS 端用 Swift 封装NWConnection实现 TCP/SSL 双栈连接,并在AppDelegate中精细控制后台唤醒策略——这不是 UI 层的像素级还原,而是对微信类 IM 应用底层通信模型、消息状态机、离线同步逻辑的原生重实现。它解决的是中小团队在合规前提下快速构建高可用、可审计、可深度定制的私有化社交聊天能力的问题:消息端到端加密可插拔、已读回执与消息撤回状态严格幂等、PC 客户端与移动端共享同一套 WebSocket+MQTT 混合协议栈。适合需要将聊天模块嵌入自有业务系统(如在线教育答疑、医疗问诊、工单协同)且对网络抖动容忍度低、对 Android 12+ 后台限制和 iOS 17 后台静默策略有明确适配要求的开发者。
2. 基于原生 Socket 与协议分层设计的双端通信架构落地
2.1 为什么放弃 SDK 封装,坚持从Socket和NWConnection开始写起
市面上多数“仿微信”项目直接集成第三方 IM SDK(如融云、环信、声网),虽省时但带来三重硬伤:一是 SDK 内部心跳机制与系统省电策略冲突,在华为 EMUI、小米 MIUI 上频繁断连;二是消息加密密钥由服务端托管,无法满足金融、政务类客户对密钥自主可控的审计要求;三是 PC 客户端若用 Electron 封装,内存占用常超 800MB,而本项目 PC 端基于 Qt 6.5 + OpenSSL 3.0 自研网络层,实测 idle 状态仅 120MB。因此本项目采用分层协议设计:底层为可切换的传输通道(TCP 长连接 / MQTT / HTTP/2 Server-Sent Events),中层为自定义二进制协议帧(含 magic number、version、cmd_type、seq_id、body_len、crc32),上层为业务指令集(MSG_SEND,MSG_ACK,CONVERSATION_SYNC,CONTACT_UPDATE)。这种结构让 Android 的OkHttp异步回调、iOS 的URLSession数据任务、PC 端的QTcpSocket信号槽能统一接入同一套解析器,避免跨平台逻辑分裂。
提示:不要试图复用微信官方协议(如 MMProtocol)。其未公开字段、加密盐值、设备指纹绑定机制均属黑盒,逆向风险极高且违反《微信软件许可及服务协议》第 5.2 条。本项目所有协议字段均为自主设计,
cmd_type使用 uint16 无符号整型,预留 0x0000–0x0FFF 供业务扩展,0x1000 起为系统指令,杜绝与任何商用协议冲突。
2.2 Android 端保活链路:ForegroundService + JobIntentService + AlarmManager 三级兜底
Android 8.0+ 对后台服务限制极严,单纯startService()在 10 秒内未转为前台服务即被系统强杀。本项目采用三段式保活:
- 第一级(实时):用户在前台时,
ChatService继承Service并调用startForeground(1, notification),notification 设置setOngoing(true)且不可清除; - 第二级(延迟):当应用进入后台,触发
JobIntentService执行心跳包发送(每 90 秒一次),该组件由系统调度,不受targetSdkVersion影响; - 第三级(兜底):在
onDestroy()中注册AlarmManager.setExactAndAllowWhileIdle(),设定 15 分钟后唤醒,执行WakefulBroadcastReceiver拉起ChatService。
关键代码如下(Kotlin):
// ChatService.kt override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { startForeground(1, buildNotification()) // 必须在 onStartCommand 内调用 } return START_STICKY } private fun buildNotification(): Notification { val channel = NotificationChannel( "chat_service", "聊天服务", NotificationManager.IMPORTANCE_LOW ).apply { setShowBadge(false) } notificationManager.createNotificationChannel(channel) return NotificationCompat.Builder(this, "chat_service") .setContentTitle("聊天服务运行中") .setSmallIcon(R.drawable.ic_chat) .setOngoing(true) .build() }注意:
START_STICKY仅表示系统内存紧张时杀死服务后尝试重启,不保证立即恢复。必须配合JobIntentService的enqueueWork()主动触发心跳,否则在华为/OPPO 等定制 ROM 上 3 分钟内必然断连。测试时需用adb shell dumpsys activity services | grep com.yourpackage.ChatService验证服务存活状态。
2.3 iOS 端后台唤醒:Background Modes + VoIP Push + NWConnection 重连策略
iOS 对后台网络限制更苛刻:普通 TCP 连接在 App 进入后台 30 秒后被系统挂起。本项目启用Background Modes中的Audio, AirPlay, and Picture in Picture(伪装音视频通话场景)与Voice over IP(VoIP 推送),并通过PushKit接收 VoIP 通知唤醒 App。关键在于:VoIP 推送 payload 必须包含"aps": {"alert": "msg"}且content-available: 1,否则无法触发PKPushRegistry的didReceiveIncomingPushWith回调。
// AppDelegate.swift func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) { guard let aps = payload.dictionaryPayload["aps"] as? [String: Any], aps["content-available"] as? Int == 1 else { completion(); return } // 此处必须启动 NWConnection 并设置 timeout = 0(永不超时) let connection = NWConnection(host: "im.yourdomain.com", port: 443, using: .tls) connection.stateUpdateHandler = { newState in switch newState { case .ready: self.sendHeartbeat(connection) // 发送心跳维持连接 completion() case .failed(let error): print("VoIP 唤醒失败: \(error)") completion() default: break } } connection.start(queue: .main) }提示:VoIP Push 需单独申请 Apple Developer Account 的 VoIP 证书,且推送服务器必须使用
gateway.push.apple.com:2195(非普通 APNs 地址)。测试阶段可用openssl s_client -connect gateway.push.apple.com:2195 -cert voip_cert.pem -key voip_key.pem验证证书有效性。
3. 消息状态机与离线同步的原生实现细节
3.1 消息发送的五态流转:从本地草稿到全链路确认
微信类 IM 的核心难点不在“发出去”,而在“确认对方收到并展示”。本项目定义消息生命周期为五个原子状态:
| 状态码 | 名称 | 触发条件 | 数据库字段status |
|---|---|---|---|
| 0 | DRAFT | 用户输入未点击发送,存于本地 SQLitedrafts表 | 0 |
| 1 | SENDING | 调用NWConnection.send()成功,但未收到服务端ACK | 1 |
| 2 | SENT | 收到服务端返回{"cmd": "MSG_ACK", "seq_id": 123, "status": "success"} | 2 |
| 3 | DELIVERED | 服务端通过另一条通道(如 MQTT topic/user/1001/deliver)推送送达回执 | 3 |
| 4 | READ | 对方 App 主动上报{"cmd": "MSG_READ", "msg_id": "abc123"} | 4 |
关键约束:状态只能单向递进(0→1→2→3→4),禁止降级。SQLite 表messages设计如下:
CREATE TABLE messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, msg_id TEXT NOT NULL UNIQUE, -- 全局唯一 UUID v4 from_user_id INTEGER NOT NULL, to_user_id INTEGER NOT NULL, content TEXT NOT NULL, status INTEGER DEFAULT 0 CHECK(status BETWEEN 0 AND 4), created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), is_deleted INTEGER DEFAULT 0 CHECK(is_deleted IN (0,1)) );注意:
msg_id必须由客户端生成(UUID v4),而非服务端分配。否则在弱网环境下用户连续点击发送,服务端可能因重复请求返回相同msg_id,导致客户端状态覆盖错误。iOS 端用NSUUID().uuidString,Android 端用UUID.randomUUID().toString()。
3.2 离线消息同步:基于时间戳 + 游标分页的增量拉取
当用户重连时,不能简单SELECT * FROM messages WHERE to_user_id = ? ORDER BY created_at DESC LIMIT 100—— 这会导致新消息漏同步(因created_at可能重复)。本项目采用双游标机制:
- 主游标:
last_sync_time(毫秒时间戳),记录上次完整同步完成时刻; - 辅游标:
last_msg_id(字符串),用于处理同一毫秒内多条消息的排序。
同步请求体为:
{ "cmd": "CONVERSATION_SYNC", "params": { "last_sync_time": 1717023456789, "last_msg_id": "a1b2c3d4-e5f6-7890-g1h2-i3j4k5l6m7n8", "limit": 50 } }服务端 SQL 查询(MySQL 8.0+):
SELECT id, msg_id, from_user_id, to_user_id, content, status, created_at FROM messages WHERE to_user_id = ? AND (created_at > ? OR (created_at = ? AND msg_id > ?)) ORDER BY created_at ASC, msg_id ASC LIMIT 50;提示:
created_at精确到毫秒,但分布式环境下仍可能碰撞,故必须用msg_id作为第二排序键。msg_id为 UUID v4,天然满足字典序唯一性,无需额外索引。
3.3 已读回执的幂等设计:服务端去重 + 客户端防抖
已读回执(Read Receipt)极易因网络重传产生脏数据。本项目在服务端增加幂等表read_receipts:
CREATE TABLE read_receipts ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id INTEGER NOT NULL, target_user_id INTEGER NOT NULL, msg_id VARCHAR(36) NOT NULL, created_at BIGINT NOT NULL, UNIQUE KEY uk_user_target_msg (user_id, target_user_id, msg_id) );客户端发送回执前做 300ms 防抖:
// PC 端 JavaScript(Qt WebEngine 内嵌) let readDebounceTimer = null; function sendReadReceipt(msgId) { clearTimeout(readDebounceTimer); readDebounceTimer = setTimeout(() => { const payload = { cmd: "MSG_READ", params: { msg_id: msgId } }; websocket.send(JSON.stringify(payload)); }, 300); }注意:防抖仅解决用户快速滚动多次触发,不能替代服务端唯一索引。若客户端崩溃重连,需重新拉取未读消息列表并批量上报,此时服务端
INSERT IGNORE INTO read_receipts确保不重复计数。
4. PC 客户端与移动端共享协议栈的关键配置项
4.1 WebSocket 连接参数调优:心跳间隔、重连退避、SSL 验证绕过控制
PC 客户端(Qt 6.5)使用QWebSocket,但默认配置在企业内网易断连。必须显式设置:
| 参数 | 推荐值 | 说明 |
|---|---|---|
pingInterval | 30000 ms | 每 30 秒发一次 ping,避免 NAT 超时 |
pingTimeout | 5000 ms | ping 发出后 5 秒未收到 pong 则断开 |
maxReconnectDelay | 60000 ms | 指数退避上限,避免雪崩式重连 |
sslConfiguration | peerVerifyMode = QSslSocket::VerifyNone | 仅限测试环境;生产环境必须部署合法证书并设为VerifyPeer |
// MainWindow.cpp QWebSocket *ws = new QWebSocket(); ws->setPingInterval(30000); ws->setPingTimeout(5000); // 生产环境必须验证证书 QSslConfiguration config = ws->sslConfiguration(); config.setPeerVerifyMode(QSslSocket::VerifyPeer); config.setCaCertificates(QSslSocket::systemCaCertificates()); ws->setSslConfiguration(config);提示:
VerifyNone在开发阶段可绕过自签名证书报错,但上线前必须替换为 Let's Encrypt 或商业 CA 签发的证书,并将ca.crt嵌入 Qt 资源文件(:ssl/ca.crt),否则 Windows/macOS 用户会遭遇SSL handshake failed。
4.2 消息体压缩与二进制协议解析:Protobuf 替代 JSON 的实测收益
原始 JSON 消息体(含 base64 图片)平均 12KB,经 gzip 压缩后仍 4.2KB。改用 Protobuf 后:
| 消息类型 | JSON 大小 | Protobuf 大小 | 压缩后大小 | 传输耗时(2G 网络) |
|---|---|---|---|---|
| 文本消息 | 1.2 KB | 0.3 KB | 0.15 KB | 82 ms → 31 ms |
| 图片消息 | 12.0 KB | 3.8 KB | 1.9 KB | 420 ms → 156 ms |
.proto定义精简版:
syntax = "proto3"; package im; message Message { string msg_id = 1; // UUID v4 int32 from_user_id = 2; int32 to_user_id = 3; int32 msg_type = 4; // 1=text, 2=image, 3=voice bytes content = 5; // text=utf8, image=jpeg raw, voice=amr-wb int64 timestamp = 6; // milliseconds int32 status = 7; // 0=draft, 1=sending... }Android 端用protobuf-java,iOS 用SwiftProtobuf,PC 端 Qt 用protobuf-c(C binding)。序列化后直接写入QByteArray发送,不经过任何 JSON 中间层。
注意:Protobuf 字段编号 1–15 占 1 字节,16–2047 占 2 字节,故高频字段(
msg_id,from_user_id)必须编号 ≤15。content字段编号 5 是刻意为之——它体积最大,但编号小不影响总长度,因 Protobuf 采用 tag-length-value 编码,小编号 tag 更省空间。
4.3 双端消息去重:基于 msg_id 的本地缓存与服务端布隆过滤器
即使协议层可靠,网络层重传仍可能导致客户端收到重复消息。本项目在两端均实现两级去重:
- 客户端内存缓存:LruCache<String, Boolean> 存储最近 1000 个
msg_id,有效期 5 分钟; - 服务端布隆过滤器:Redis 中维护
bloom:im:receipt:20240529,使用bf.add插入msg_id,bf.exists判断是否已处理。
服务端伪代码(Go):
func handleMessage(ctx context.Context, msg *im.Message) error { key := fmt.Sprintf("bloom:im:receipt:%s", time.Now().Format("20060102")) exists, _ := redisClient.BFExists(ctx, key, msg.MsgId).Result() if exists { return nil // 丢弃重复消息 } redisClient.BFAdd(ctx, key, msg.MsgId) // 加入布隆过滤器 // ... 正常处理逻辑 }提示:布隆过滤器存在误判率(本项目设为 0.01%),但不会漏判。误判仅导致少量消息被丢弃,用户感知为“偶尔收不到”,远好于重复消息引发的状态混乱。每日新建 key 避免 Bloom Filter 膨胀,TTL 设为 24 小时。
5. Android 14 与 iOS 17 兼容性加固:针对新系统限制的专项修复
5.1 Android 14 的 Foreground Service 启动限制绕过方案
Android 14(API 34)强制要求 Foreground Service 必须由用户显式触发(如点击按钮),禁止BOOT_COMPLETED广播或AlarmManager启动。本项目采用PendingIntent.getActivity()创建前台 Activity 作为跳板:
// 在 Application.onCreate() 中 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { val intent = Intent(this, ForegroundStubActivity::class.java) intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK val pendingIntent = PendingIntent.getActivity( this, 0, intent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_ONE_SHOT ) startForegroundService(pendingIntent) // 系统允许此方式启动 }ForegroundStubActivity仅做一件事:立即调用startService()启动ChatService并finish()。该 Activity 无 UI,主题设为Theme.Translucent.NoTitleBar,用户无感知。
注意:
PendingIntent.getActivity()在 Android 14 上是唯一被允许的前台服务启动入口。PendingIntent.getService()和PendingIntent.getBroadcast()均被禁止,否则抛出SecurityException。
5.2 iOS 17 的 Background App Refresh 关闭应对策略
iOS 17 默认关闭Background App Refresh,导致 VoIP Push 无法唤醒 App。本项目在首次安装时引导用户手动开启:
// FirstLaunchViewController.swift func checkBackgroundRefresh() { if #available(iOS 17.0, *) { let status = BGProcessingTaskRequest.isAvailable ? "可用" : "不可用" let alert = UIAlertController(title: "后台刷新建议", message: "为保障消息及时接收,请开启【设置→通用→后台App刷新】", preferredStyle: .alert) alert.addAction(UIAlertAction(title: "去设置", style: .default) { _ in UIApplication.shared.open(URL(string: "App-Prefs:root=BACKGROUND_APP_REFRESH")!) }) present(alert, animated: true) } }同时服务端增加APNs普通通知作为 fallback:当检测到用户设备 5 分钟未上报心跳,向该设备发送一条sound: "default"的静音通知(content-available: 0),利用 iOS 对静音通知的宽松策略唤起 App。
5.3 消息列表卡顿优化:RecyclerView 与 UITableView 的原生渲染技巧
Android 端RecyclerView卡顿主因是Glide加载头像时未指定override()尺寸,导致每次onBindViewHolder()都触发 Bitmap 重采样。修复后代码:
Glide.with(holder.itemView.context) .load(userAvatarUrl) .override(120, 120) // 强制缩放到 120x120,避免 layout 计算 .centerCrop() .into(holder.avatarView)iOS 端UITableView卡顿源于cellForRowAt中同步解密消息内容。改为异步解密 + 占位符:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "MessageCell")! let msg = messages[indexPath.row] cell.textLabel?.text = "[解密中...]" // 占位符 DispatchQueue.global(qos: .userInitiated).async { let decrypted = self.decrypt(msg.content) // AES-GCM 解密 DispatchQueue.main.async { if cell.tag == indexPath.row { // 防止 Cell 复用错乱 cell.textLabel?.text = decrypted } } } return cell }提示:
cell.tag用于标记当前 Cell 绑定的行号,if cell.tag == indexPath.row是防止异步解密结果返回时 Cell 已被复用到其他行的标准做法。不加此判断会导致消息内容错位显示。
本文还有配套的精品资源,点击获取