
简介鸽哒IM即时通讯系统源码包面向需要私有化部署即时通讯服务的开发团队与个人开发者尤其适合企业内网通讯或业务深度定制的场景提供类似微信的全功能通讯方案。资源包含安卓、苹果、PC三端原生源码以及基于Java的酷信后台实现支持Linux、Windows、Docker三种部署方式可满足高并发场景并支持集群还配备完整部署教程降低上手门槛。整个压缩包共995个文件包含前后端及移动端完整工程以jar后端逻辑、png/gif界面资源及js/css前端代码为主另含sql数据库脚本、bat/sh部署脚本、jks/pfx证书文件及apk/ipa安装包整体大小约383MB目录结构清晰。已有855人学习下载。借助这套全开源代码开发者可深入理解即时通讯的加好友、私聊、群聊、朋友圈、红包、语音视频等功能的实现细节同时掌握3DES加密、端到端传输、阅后即焚等安全机制的落地方式适合用于二次开发或技术研究。1. 鸽哒IM即时通讯源码包一套能省掉三个月开发的多端方案如果甲方把“即时通讯系统”写进合同意味着交付物里必须有一个能同时跑在安卓、苹果、PC上的聊天应用服务端还得扛住长连接、离线消息、群组权限这些事。这种项目从零做起光是把多端协议对齐、消息状态同步理清楚就够折腾两三个月。我拿到这套最新的鸽哒IM即时通讯系统源码时第一反应不是一行行读代码而是直接拆包看它怎么组织服务端一个目录安卓、苹果、PC三端工程各自独立数据库脚本也放在显眼位置。跑通之后我对这类“全开源IM源码”的定义是省掉的不是写代码的时间而是“从0到1踩坑”的时间。下面按部署顺序写从架构、服务端、三端编译到避坑和二开尽量把关键参数和命令都交代清楚新手能照着复现熟手可以直接跳到参数表和避坑章节对比自己的方案。2. 鸽哒IM的架构拆解一条消息从发送到接收要过几道关拿到全开源 IM 源码时习惯先不看业务代码而是把“一条消息从发送到接收”这条主链路摸清楚。只要这条链路通了后面部署、二开、排错都有方向。国内这类源码的服务端大多跑在 Java 技术栈上通信层常用 Netty 那套长连接模型消息走自定义协议而不是裸 HTTP 轮询。这套思路的好处是实时性好、省流量坏处是服务端和客户端必须严格约定协议格式改一个字段两头都要动。下面按消息上行、存储、多端同步三个部分拆开讲。2.1 上行消息处理登录校验、落库、下发三步走客户端发一条消息服务端收到的不是一条普通的 HTTP 请求而是一个长连接上的数据包。这个包经过协议解码后进入消息处理入口。常见处理流程是先校验 token再落库然后找接收方的连接通道在线就直接推送不在线就写离线表。整体链路不长但顺序不能乱尤其是“先落库再推送”这一点很多二开的人图省事先推送后落库结果消息发出去了库里却没有记录导致多端同步时消息凭空消失。public void handleUpMessage(ChannelHandlerContext ctx, MessagePacket packet) { // 1. 先做登录态校验token 不合法直接断开连接 if (!authService.checkToken(packet.getToken())) { ctx.close(); return; } // 2. 消息落库拿到自增 id后续多端同步靠它定位 long msgId messageService.save(packet); // 3. 给在线接收方直接推送对方 ACK 后更新消息状态 Channel toChannel connectionManager.getChannel(packet.getToUid()); if (toChannel ! null toChannel.isActive()) { toChannel.writeAndFlush(packet); } else { // 4. 不在线则进离线表等登录后再按 seq 拉取 offlineMessageService.push(packet.getToUid(), msgId); } }这段伪代码把消息上行的主干逻辑写清楚了真实源码里可能还夹带消息过滤、敏感词检查、群成员权限判断但骨架不会变。注意第二步和第三步之间没有做事务这是刻意为之消息只要落库就在“不会丢”的安全区推送失败最多是“延迟到达”而不是“永久丢失”。如果既要落库又要推送保证原子性反而会把消息链路的吞吐量压下去因为数据库事务和网络 I/O 混在一起长连接线程很容易被慢 SQL 拖死。2.2 存储模型单聊、群聊、离线消息的表结构IM 系统的存储设计决定了很多上层功能的实现成本。最常见的做法是用户表、会话表、消息表三张主表再加一张离线消息表处理“接收方不在线”的场景。消息表的核心字段不是内容本身而是 from_uid、to_uid、msg_type、status 这些维度的组合。to_uid 在单聊里存对方 uid在群聊里存群 id靠 msg_type 或一个 chat_type 字段区分这是很多新手读表时会懵的地方。CREATE TABLE t_message ( id bigint NOT NULL AUTO_INCREMENT, from_uid bigint NOT NULL COMMENT 发送者uid, to_uid bigint NOT NULL COMMENT 单聊存对方uid群聊存群id, chat_type tinyint NOT NULL DEFAULT 1 COMMENT 1单聊 2群聊, msg_type tinyint NOT NULL DEFAULT 1 COMMENT 1文本 2图片 3文件 4语音, content text NOT NULL COMMENT 消息内容/文件路径, status tinyint NOT NULL DEFAULT 0 COMMENT 0未读 1已读 2撤回, create_time datetime NOT NULL, PRIMARY KEY (id), KEY idx_to_uid_create_time (to_uid, create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT消息表;这里有两个容易踩坑的点。第一个是字符集必须用 utf8mb4不能用 utf8因为 utf8 在 MySQL 里最多存 3 字节用户发一个 emoji 表情就直接报错第二个是联合索引的顺序idx_to_uid_create_time 把 to_uid 放在前面是因为“拉取某个会话的消息”是这个系统最高频的查询create_time 放在后面用于排序和分页。群聊场景下数据量大了以后建议再加一个 group_id 前缀索引否则群消息拉取会全表扫。离线消息表的设计相对简单核心是“谁没收到哪些消息”。我一般建议不要直接复制消息内容而是存 uid 和 msg_id 两个字段等客户端登录后服务端拿 msg_id 去消息表里回查完整数据。这样做离线表始终很小消息内容更新时也不用同步改两份。CREATE TABLE t_offline_message ( id bigint NOT NULL AUTO_INCREMENT, uid bigint NOT NULL COMMENT 接收方uid, msg_id bigint NOT NULL COMMENT 对应t_message.id, create_time datetime NOT NULL, PRIMARY KEY (id), KEY idx_uid_create_time (uid, create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT离线消息表;2.3 多端同步与未读数靠 seq 还是靠时间戳同一个账号在 PC 和手机同时登录手机收到的消息PC 端也必须收到而且已读状态要能同步。这是 IM 系统里最容易被低估的部分多数源码处理多端同步的通用方案是“客户端维护 last_seq”。服务端给每一条消息分配一个全局递增的 seq客户端本地记录自己已经拉到了哪个 seq每次重连或者从后台切回前台时带着 last_seq 去服务端增量拉取。GET /api/msg/pull?uid1001last_seq88920limit50返回结果是一个消息数组每条消息都带了自己的 seq 和 create_time。客户端拿到后把数组里最大的 seq 覆盖到本地 last_seq下次继续从这个位置往后拉。这套机制比用时间戳可靠得多因为时间戳在跨时区、手机时钟不准、服务端和客户端时钟漂移的情况下会产生重复或漏拉。seq 是单调递增的整数只要服务端保证分配时不回退多端同步就不会漏这是整个 IM 系统里“以序号为核心”的设计思路。未读数则是消息表 status 字段配合 Redis 计数算出来的。客户端推送过一条消息并不代表用户已读真正的“已读”动作发生在用户打开会话、看到消息气泡那一瞬间客户端发一个 ACK 包给服务端服务端再把未读计数减一。这里要特别提醒不要把“已读”和“消息送达”混为一谈。送达是长连接把包发出去已读是用户真正看到这两个事件之间隔着一个网络层和一个 UI 层很多消息已读不同步的问题根源都是客户端把 ACK 发早了。3. 服务端落地从源码包编译到第一行业务日志服务端是整个 IM 系统里最先要跑起来的部分因为三端客户端编译好之后都要连它。部署顺序我一般固定为三步先准备环境再改配置最后编译启动。不要跳步不要在没建数据库的情况下先编译否则启动时会因为连不上库而报一堆让人摸不着头脑的错。3.1 环境准备JDK、MySQL、Redis 的最低要求全开源 IM 服务端的技术栈常见是 Java MySQL Redis 的组合。JDK 负责跑业务逻辑MySQL 存用户、会话、消息这些结构化数据Redis 存登录 token、在线状态、未读计数这些需要高速读写的临时数据。版本选择上宁可保守一点也不要追新因为源码编译基本都用 Maven依赖仓库里很多老包在 JDK 17 上会报模块访问错误那是最让人头疼的坑。组件建议版本作用JDK1.8 或 11编译和运行服务端避免用 17MySQL5.7 或 8.0用户、消息、会话等持久化数据Redis5.x 及以上登录态、未读数、分布式锁Maven3.6 及以上拉依赖、打包Nginx1.18可选用于反向代理和 WSS 升级版本选择不是拍脑袋。JDK 8 是这类源码编译兼容性最好的版本如果你本机同时有多个项目建议单独装一个 JDK 8 目录部署时用JAVA_HOME指过去不要改系统全局的 JDKMySQL 5.7 和 8.0 在部署上几乎没有差别但 8.0 的默认认证插件是 caching_sha2_password部分老版本 JDBC 驱动不支持会报认证失败建议建用户时指定mysql_native_password认证方式。3.2 修改配置文件数据库、Redis、端口三处必改解压源码包后服务端工程里通常会有一个 application-prod.yml 或者 application.properties 作为生产环境配置。名字不固定但作用都一样告诉服务端数据库在哪、Redis 在哪、监听哪个端口。第一次搭建时这三处不改其他地方不建议乱动尤其是各种超时时间参数等系统跑起来以后再做调优。spring: datasource: url: jdbc:mysql://192.168.1.10:3306/gedaim?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai username: gedaim password: gd123456 driver-class-name: com.mysql.jdbc.Driver redis: host: 192.168.1.10 port: 6379 password: redis123 database: 0 server: port: 8848 im: ws-port: 8848 heartbeat-interval: 30这里的关键参数是连接串里的characterEncodingutf8mb4和serverTimezoneAsia/Shanghai。前者解决 emoji 乱码后者解决 MySQL 8.0 下 JDBC 连接时的时区报错这两个坑几乎每个第一次部署的人都会遇到。im 配置段里的 ws-port 是长连接的端口如果和 server.port 一致说明 HTTP 接口和 WebSocket 共用端口Nginx 代理时只需要对外开放一个端口就行如果分开就要记住长连接那个端口也要放行防火墙。数据库创建和账号授权建议用 MySQL 命令行完成不要用客户端工具的可视化建库因为字符集选项容易被忽略掉。CREATE DATABASE gedaim DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER gedaim% IDENTIFIED BY gd123456; GRANT ALL PRIVILEGES ON gedaim.* TO gedaim%; FLUSH PRIVILEGES;3.3 编译打包与启动验证从日志里确认服务真的起来了配置改完后进入服务端工程目录执行 Maven 打包。这里的要点是跳过测试因为很多源码包里的测试用例依赖外部环境跑不跑得过全看运气不影响主程序编译。# 进入服务端目录pom.xml 所在位置 cd /opt/gedaim/server # 打包跳过单元测试生成可执行 jar mvn clean package -DskipTests -Dmaven.test.skiptrue # 后台启动日志输出到 app.log nohup java -jar target/server-1.0.0.jar --spring.profiles.activeprod app.log 21 # 等 10 秒左右让 Spring 容器完成初始化 sleep 10 # 看启动日志最后几十行 tail -50 app.log启动日志里如果出现类似Started Application in xx seconds的字样说明服务端本体起来了。这时候还不算完还要确认端口真的在监听、数据库连接池没报错。netstat -tlnp | grep 8848能查到端口被 Java 进程占用再执行两三条 SQL 验证消息表能写入整个服务端才算真正可用。验证阶段最容易翻车的是“日志说启动成功但端口没监听”这通常是配置里 ws-port 和 server.port 写成了两个端口前面日志只代表 HTTP 部分起来了长连接部分可能因为协议初始化失败而静默退出。所以启动后一定把两个端口都查一遍还要去看日志里有没有Netty started或Channel bound之类的长连接初始化记录别只看 Spring 的启动字样就做后面三端联调。4. 安卓、苹果、PC 三端编译连接同一个服务端的配置入口全开源 IM 源码的价值很大程度体现在三端齐全这件事上。不用自己从零写协议对齐只需要把每端的服务器地址改成你自己的 IP就能跑起来。但三端工程各自有脾气安卓有 Gradle 和网络权限苹果有 CocoaPods 和证书PC 端则有跨平台壳和编译链。下面按顺序逐个说。4.1 安卓端构建Gradle 同步与服务器地址修改安卓工程一般是一个完整的 Android Studio 项目拿到后不要急着编译先全局搜一下 “localhost” 或 “127.0.0.1”把服务器地址统一替换成服务端所在机器的局域网 IP 或域名。这个地址通常会出现在一个常量类或 build.gradle 的 buildConfigField 里少数源码会放在res/values/strings.xml。# 进入安卓工程目录 cd /opt/gedaim/android # 首次同步依赖并打 debug 包Gradle 会自动下载依赖 ./gradlew assembleDebug # 打 release 包需要签名文件没有会失败 ./gradlew assembleReleaseassembleDebug是开发阶段最常用的目标产物在app/build/outputs/apk/debug/目录下可以直接装到手机上测试不需要证书签名跑通业务后再去配置 release 签名。这里有个容易忽略点Android 9 以上系统默认禁止明文 HTTP 流量如果服务端用的不是 HTTPSApp 会一直连接超时这时要在 AndroidManifest.xml 的 application 标签里临时打开明文流量开关后面避坑章节会细说。4.2 苹果端构建CocoaPods 与签名配置苹果端的坑明显比安卓多因为 iOS 真机运行必须有开发者证书。模拟器调试可以先不配证书但推送功能和真机体验就做不了。拿到苹果工程后先看有没有 Podfile有就说明依赖是用 CocoaPods 管理的必须执行pod install之后打开的不是 .xcodeproj 而是 .xcworkspace否则编译会报找不到头文件。cd /opt/gedaim/ios # 安装依赖生成 .xcworkspace 文件 pod install --repo-update # 打开工作区不要打开 xcodeproj open Gedaim.xcworkspace服务器地址在 iOS 工程里一般写在Config相关的 plist 文件或APIConfig.swift这类文件里全局搜索 localhost 能定位。推送证书的配置在AppDelegate的 didRegisterForRemoteNotificationsWithDeviceToken 回调里能看到注意证书分为开发环境和生产环境测试时用开发证书上架后用生产证书搞反了会收不到推送。如果你的源码包里已经带有推送模块第一次调试可以先把推送功能注释掉跑通登录和收发消息后再补减少变量。4.3 PC 端构建跨平台壳与 WebSocket 地址PC 端通常是 Electron 或者 Qt 工程。Electron 的好处是 Web 技术栈、界面好改、打包成 Windows 和 macOS 应用的成本低坏处是安装包体积大且长连接依赖 Node.js 的 WebSocket 库。修改服务器地址一般在src/config.js或main.js里集中配置把 wsUrl 和 apiUrl 指到同一个服务端地址即可。// src/config.js — PC 端集中配置服务器地址 module.exports { // 长连接地址ws 或 wss 取决于服务端是否走 nginx 加证书 wsUrl: ws://192.168.1.10:8848/ws, // HTTP 接口地址登录、拉取用户信息等 apiUrl: http://192.168.1.10:8848 };修改完成后在 PC 工程根目录执行npm install然后npm run dev可以先跑开发模式验证能登录、能收发消息最后的打包命令是npm run build或者electron-builder产出安装程序放在 dist 目录里。Electron 打包过程中最常见的两个问题一是 npm 依赖下载慢甚至失败建议把 registry 切到国内镜像二是 Windows 下打包需要下载 WinCodeSign 等工具属于网络问题多试几次或手动下载工具包放缓存目录就好。5. 鸽哒IM部署避坑5 个把时间耗光的现场这套源码跑通不难难的是“一次跑通”。下面五条坑是我在部署和联调中实际遇到过、并且修复成本都不低的典型问题每条按现象、原因、解决三步写清楚。如果你按前面章节操作到一半卡住了先来这个章节对照。5.1 安卓端连不上服务端IP 写死和明文流量双坑现象App 安装成功后登录页一直转圈最终提示网络异常或连接失败。服务端明明已经启动用 PC 端能连上安卓端就是不通。原因第一大概率是安卓工程里服务器地址没改还在用源码自带的示例域名或 localhost第二是 Android 9 之后的明文流量限制服务端没上 HTTPS 的话App 默认不允许走 HTTP 明文请求。解决先全局搜索替换服务器地址再在 AndroidManifest.xml 的 application 标签里加android:usesCleartextTraffictrue开发阶段最直接有效。application android:usesCleartextTraffictrue android:labelstring/app_name /application这个开关只建议开发阶段用正式上线必须给服务端配 HTTPS 和 WSS然后把开关去掉否则 App 上架审核和应用市场检测都会有问题。5.2 中文消息变问号建库时字符集没对齐现象自己在数据库里插入中文正常但 App 里发的消息一到服务端甚至消息记录里就显示成“???”用户昵称含生僻字也类似。原因MySQL 建库时默认字符集是 latin1或者建表时指定了 utf8导致 emoji 和部分生僻字存不进去。这是 IM 系统最典型的新手坑因为数据库连接串里写着 utf8 只是传输层编码存储层编码由库和表的 charset 决定。解决重建数据库明确指定 utf8mb4并确认所有表都是 utf8mb4。已经建好的库可以改默认字符集但已存在的表需要逐张转换不如直接重建干净。-- 查看当前库字符集 SHOW CREATE DATABASE gedaim; -- 修改库默认字符集 ALTER DATABASE gedaim DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 修改所有表的字符集table 名手动替换 ALTER TABLE t_message CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;5.3 iOS 离线推送收不到证书、环境、Bundle ID 三处必须一致现象App 在前台时能正常收消息把 App 切到后台甚至杀掉进程后服务端日志显示推送已发出但手机没有任何通知。原因APNs 推送链路里“证书环境”是最容易错的。开发环境必须用 Apple Push Notification Development 证书生产环境要用 Production 证书二者不能混用。此外工程的 Bundle ID 和证书里注册的 App ID 不一致也会导致推送被 APNs 静默丢弃。解决先去服务端推送配置里确认证书是哪个环境再看工程签名里 Bundle ID 是否与证书匹配。调试阶段建议直接看设备日志确认 deviceToken 有没有成功返回如果 token 都没拿到问题就出在证书或签名而不是服务端推送代码。5.4 离线消息丢一半双写顺序的坑现象用户 A 给不在线的 B 发消息B 过几小时上线后只收到最后几条消息中间一部分丢了。重启服务端后也可能复现。原因离线消息的写入逻辑和普通消息落库不是一个事务。如果源码里是先写 t_message 再写 t_offline_message中间发生异常或者服务端重启t_message 有数据但 t_offline_message 没写进去B 上线后按离线表拉取自然就缺消息。解决把两条 insert 放进同一个数据库事务或者先写离线表、成功后再更新消息状态。代码层不要依赖“先落库再离线”的顺序要在同一个事务里保证一致性。START TRANSACTION; INSERT INTO t_message (from_uid, to_uid, chat_type, msg_type, content, create_time) VALUES (1001, 1002, 1, 1, 你好, NOW()); INSERT INTO t_offline_message (uid, msg_id, create_time) VALUES (1002, LAST_INSERT_ID(), NOW()); COMMIT;5.5 群聊发消息全员掉线同步推送把线程池拖死现象单聊正常群聊里有人连发几条消息服务器 CPU 飙高随后大量连接断开日志出现 OutOfMemory 或 too many open files。原因部分源码为了省事群消息发送时用 for 循环逐个 channel.writeAndFlush这是同步调用群里有 500 人就要在同一个线程里写 500 个 channel任何一个 channel 的 socket buffer 满了都会阻塞最终把 Netty 的 worker 线程全部卡死。解决把群消息的推送改到独立线程池或者先通过内存队列异步处理channel.writeAndFlush 本身是异步方法但不要在一个循环里密集同步等待。如果源码不好改可以先控制群成员数量上限再逐步优化为“扇出”模型即先查询群成员 uid 列表再批量放入 MQ 或线程池分发这也是生产环境 IM 的标准做法。6. 二开三件事换 UI、加已读回执、做上线压测源码跑通只是开始二开才见真功夫。这里选三个最高频的二次开发需求每个都给你一条可以马上动手的路径不用把整个工程读完先把入口摸清局部改动就够了。6.1 换 UI 的最快路径从会话列表和气泡布局入手IM 源码的 UI 通常分两大类安卓是原生布局文件苹果是 Storyboard 或 xibPC 端是 HTML/CSS。换 UI 最忌讳的是从头重画正确做法是先找会话列表的 adapter 或渲染入口只改布局和样式资源。安卓的会话列表一般在ConversationActivity里列表项布局是item_conversation.xmlPC 端的 Electron 版本通常是 Vue 或 React 组件找ConversationItem.vue就能定位。先改气泡的背景、圆角、字体大小再改会话列表的时间显示格式这两个文件替换完视觉上就会脱胎换骨。6.2 给消息加已读回执要动三张表和两个接口已读回执是个看似简单、实则需要前后端配合的功能。先说字段t_message加read_time datetime NULL表示接收方何时已读t_offline_message不用改但拉取离线消息的接口需要返回 read_time。客户端在会话界面可见时发送一个 ACK 包服务端收到后更新 read_time并把这个回执同步给发送方。发送方的 UI 就可以从“已送达”改成“已读”。// 客户端打开会话页时批量标记已读 channel.writeAndFlush(new ReadAckPacket(conversationId, lastMsgId));服务端处理时一条 UPDATE 语句按会话和 lastMsgId 批量更新即可注意要按 uid 加索引否则群聊场景下这个更新会扫全表。6.3 上线前压测用脚本验证连接数和消息吞吐不要跳过压测直接上线。IM 系统最怕的不是并发高而是长连接一多就崩。常见的做法是用 JMeter 或者自写脚本模拟 N 个在线用户循环发送消息观察服务端的 CPU、内存、句柄数。# 用 netstat 统计当前连接数Linux netstat -an | grep 8848 | grep ESTABLISHED | wc -l # 压测前先记录基线压测中观察连接数是否稳定 # 连接数暴增后骤降 服务端崩过或被内核杀进程我个人的习惯是先用 500 个模拟连接压 10 分钟观察内存稳定后再做 100 条/秒的消息吞吐测试看日志有没有大量重传和超时。上线前把这三个指标记下来后面每次发版都对比一次连接数下降、消息延迟变长基本能提前嗅到代码退化。二次开发这条路上最大的教训是别相信“源码全开源就什么都改得动”IM 的真功夫在长连接稳定性和消息一致性UI 只是皮把消息链路保住二开就成功了一半。希望帮到你。本文还有配套的精品资源点击获取