简介:面向具备一定开发经验、希望自建即时通讯系统的技术人员,这份视酷酷信IM源码提供安卓、苹果、PC与Web四端互通方案,采用国产tio通信技术,可用于搭建私域聊天平台或作为二次开发底稿。包内共9个文件,以6个txt解析文本为主,辅以1个doc、1个html和1个jpg,压缩包约99KB;文本侧重系统架构、四端互通与双空间切换代码实现等说明,html和doc便于整理阅读,图片可作界面或结构示意参考。已有500人学习,适合对照教程理解部署流程、服务端与客户端通信逻辑,以及Java、OC、C#、Vue多语言端的组织方式。源码支持修改与扩展,可参考MongoDB数据存储、CentOS 7.6部署环境及单机万人并发、集群扩展等描述,用于评估自身业务落地和二次开发加商城等需求,同时强调无后门与私有服务器部署的数据安全考量。
1. 从白屏到跑通:视酷酷信 IM 即时通讯源码四端部署的第一道坎
去年帮一个小团队部署视酷酷信即时通讯这套 IM 即时通讯源码,四端互通的那种:服务端加 Android、iOS、PC、H5。文档给得不算敷衍,结果第一晚就翻车——服务端起来了,H5 用 Safari 打开一片白屏,iOS 装完包登录一直转圈,PC 端能登进去但发不出消息。三个现象看着是三件事,查下去发现是同一个根源:环境版本和配置项没对齐。这类全端源码最容易出问题的地方,往往就在于你以为装完 PHP 就结束了。它适合手上有点动手能力、打算自己部署一套即时通讯做二次开发的人,照着这条线往下走能少熬几个晚上。
2. 环境清单钉死:Nginx、PHP、MySQL、Redis 与四端编译环境各装哪版
装这套东西最忌讳边装边改版本。源码包的部署文档一般只写一个区间,比如“PHP 7.x”“MySQL 5.7 以上”,真到机器上必须自己收窄到具体分支。我的做法是在动手前把整张清单写在纸上,装一台记一台,后面哪一步报错能立刻定位到是不是版本背锅。
2.1 服务端四件套的版本取舍和扩展清单
服务端是典型的 Nginx + PHP + MySQL + Redis 组合,长连接另起一个常驻进程。PHP 我一般锁 7.4 或 8.0 这两个分支,8.1 往上要慎重,很多带年份的老源码里in_array、strpos、strlen(null)这类隐式转换会直接抛 deprecated 甚至中断流程。MySQL 走 5.7 或 8.0 都行,字符集必须是utf8mb4——聊天消息里有表情,utf8三字节存不下,写进去就是问号或者直接报错。Redis 5 以上,用来放 token、在线状态和消息队列缓存。Nginx 1.18 往上随便挑。
装完 PHP 第一件事不是跑站点,是数扩展。这套系统至少要求fileinfo、redis、sockets、pcntl、posix、mbstring、pdo_mysql、openssl、curl、gd、bcmath十一个。少一个都可能是致命的那种。
# 先看 PHP 版本,再逐个确认扩展有没有真的加载 php -v php -m | grep -E 'fileinfo|redis|sockets|pcntl|posix|mbstring|pdo_mysql|openssl|curl|gd|bcmath'php -m列出来的是实际加载的模块,不是 php.ini 里写了就算数。这里面sockets、pcntl、posix三个最容易被漏掉,因为它们跟普通的 Web 请求没关系,只有长连接进程启动时才用到。缺sockets的表现是进程启动后直接退出,日志里就一行干巴巴的启动失败,没有堆栈,属于典型的问题现场。补装扩展的写法按系统源不同,CentOS 系常见是:
# 扩展名以源里的包名为准,装完记得重启 php-fpm yum install -y php74-php-redis php74-php-sockets php74-php-pcntl \ php74-php-posix php74-php-bcmath php74-php-gd systemctl restart php74-php-fpmMySQL 8.0 有个绕不开的坑:默认认证插件是caching_sha2_password,一些老版本 PHP 的pdo_mysql根本握手不上,报的是“Authentication plugin cannot be loaded”这类看起来跟密码无关的错误。解决办法是建完业务账号后单独把它切回mysql_native_password,这个放到下一章建库时一起做。
Redis 别裸奔。至少设requirepass,bind只留127.0.0.1,protected-mode yes。装完用一条命令验:
# 返回 PONG 才算通,返回 NOAUTH 说明密码没带上 redis-cli -h 127.0.0.1 -a '你的密码' ping2.2 四端各自的编译环境,别想着用一台机器全搞定
四端互通听着完整,落地时是四套截然不同的环境。
H5 端最轻,不需要编译,改完配置扔到站点目录就能跑。但对环境最挑:必须是 https,WebSocket 必须走 wss,否则在 Safari 和各类内嵌浏览器里直接被拦。证书可以是正式的,也可以内网自签,自签的话 Safari 会先弹一次警告,接受之后才继续。
Android 端要 Android Studio,具体版本跟工程里的 Gradle 插件版本绑定。判断方法很简单:打开工程先看build.gradle里的classpath 'com.android.tools.build:gradle:x.x.x',插件版本决定 JDK,比较老的一套工程一般配 JDK 8 或 11,新一点的配 17。JDK 装错版本,症状是同步阶段就红一片,根本走不到编译。
iOS 端要 Xcode,而且必须在 macOS 上做。这里有个硬性约束要提前想清楚:如果应用要上架,接口就必须是 https,iOS 的 ATS(App Transport Security)默认会拦掉所有明文请求。有人图省事在Info.plist里开NSAllowsArbitraryLoads,本地调试确实通了,审核时大概率被退回。我的习惯是调试期单独配NSExceptionDomains白名单,只放测试域名,上线前删干净。
PC 端看源码包里给的是什么形态。给的是工程源码就要对应的打包环境,给的是现成安装包,那 PC 端的工作就只剩改服务地址和清缓存两件事。
2.3 端口和目录一次规划定死,后面不再改
这套东西涉及端口不少,装到一半改端口是自找麻烦,因为改一处要连带改配置文件、Nginx 反代、四端的接口地址。开头就定好:
| 端口 | 用途 | 是否对外 | 备注 |
|---|---|---|---|
| 80 | HTTP,仅用于跳转 https | 是 | 全站强制跳转 |
| 443 | HTTPS 站点 | 是 | H5 与全部接口入口 |
| 9000 | php-fpm | 否 | 只监听 127.0.0.1 |
| 3306 | MySQL | 否 | 只监听 127.0.0.1 |
| 6379 | Redis | 否 | 必须设密码 |
| 8282 | 长连接服务 | 否 | 由 Nginx 的 wss 反代出去 |
目录也顺手定死。我一般放/www/wwwroot/im,root指向里面的public目录而不是项目根目录,理由是.env、runtime、日志这些不该被公网直接下载到。这个习惯看起来多余,但少一次目录穿越就少一次事故。
3. 服务端落地:从建库到长连接进程守护的六个配置项
服务端这步是整个部署的重心,四端能不能连上全看这里。我把这一步拆成四个动作:建库导数据、配站点、改配置、起进程,每步都有验证点,走完一步验一步,别攒到最后一起查。
3.1 建库、导 SQL 与 MySQL 8.0 的认证插件回退
先建库。字符集和排序规则在这里就必须定对,后面改要导数据重来。
-- 建库时锁定 utf8mb4,表情和生僻字都能存 CREATE DATABASE im_core DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; -- 业务账号只允许本机连,别开 % 通配 CREATE USER 'im_user'@'127.0.0.1' IDENTIFIED BY '替换成十六位以上的强密码'; -- MySQL 8.0 特有:把认证插件切回 mysql_native_password,否则老 PDO 连不上 ALTER USER 'im_user'@'127.0.0.1' IDENTIFIED WITH mysql_native_password BY '替换成十六位以上的强密码'; GRANT ALL PRIVILEGES ON im_core.* TO 'im_user'@'127.0.0.1'; FLUSH PRIVILEGES;'127.0.0.1'这个主机位不要图省事写成'%'。IM 的库里有全部聊天记录,暴露面越小越好。最后一句FLUSH PRIVILEGES是刷新权限缓存,改完账号权限不执行这句,新权限可能不生效,排错时容易误判。
导数据时带上字符集参数,否则中文表名或注释可能乱码:
# --default-character-set 必须显式指定,默认值在部分客户端上是 latin1 mysql -h 127.0.0.1 -u im_user -p --default-character-set=utf8mb4 im_core < install.sql导入完成后核对一遍,三个值都应该是utf8mb4:
SHOW VARIABLES LIKE 'character_set%'; -- 重点看 character_set_client / character_set_connection / character_set_database如果导完发现中文是问号,别急着重新导,先看这三个变量。多数情况是客户端字符集问题,数据本身没坏,重导一遍就行;但如果表结构建的时候就用了utf8,那必须重建表,改不了。
3.2 站点配置:root 指向、伪静态和那行必须给足的超时
Nginx 的配置决定了这个站点能不能正常访问、长连接会不会掉。下面这份是我压过的版本:
server { listen 443 ssl http2; server_name im.example.com; # 换成自己的域名 root /www/wwwroot/im/public; # 指到 public,不是项目根 index index.php index.html; ssl_certificate /etc/nginx/ssl/im.pem; ssl_certificate_key /etc/nginx/ssl/im.key; # 上传大小三处联动中的 Nginx 这一处,语音和图片都要过 client_max_body_size 24m; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } # 长连接的 wss 反代,路径以源码里的配置为准 location /ws { proxy_pass http://127.0.0.1:8282; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_read_timeout 3600s; # 这个值给不够,连接会被周期性掐断 } }proxy_read_timeout是这份配置里最容易被忽略的一行。默认 60 秒,意味着长连接每分钟被断一次,客户端要不停重连。表现出来不是报错,而是“消息延迟一会儿才收到”“在线状态一会儿绿一会儿灰”,查起来非常费劲。给到 3600 秒,配合客户端心跳,链路才稳。
try_files那行是伪静态,少了它,除了首页之外的路由全部 404。
目录权限单独说一下,很多人的 500 错误就是这里来的:
# 目录 755、文件 644,只给 runtime 和上传目录写权限 chown -R www:www /www/wwwroot/im find /www/wwwroot/im -type d -exec chmod 755 {} \; find /www/wwwroot/im -type f -exec chmod 644 {} \; chmod -R 775 /www/wwwroot/im/runtime chmod -R 775 /www/wwwroot/im/public/uploadschmod -R 777是省事,但别用。整套源码之后还要做二次开发,权限放太开,后面接手的人排查问题会更痛苦。
3.3 配置文件里必须逐项确认的六处
源码的配置一般在项目根目录的.env或者config目录下,字段名各家不同,但需要改的东西就这六类:
| 配置项 | 典型值 | 不填会怎样 |
|---|---|---|
| 数据库地址/库名/账号 | 127.0.0.1 / im_core | 首页直接 500 |
| 数据库密码 | 上一步设的强密码 | 连接失败,报驱动异常 |
| Redis 地址/密码 | 127.0.0.1:6379 + 密码 | 登录能过,但发消息无响应 |
| 站点域名 | https://im.example.com | 图片、语音地址拼成内网 IP,四端都显示不出来 |
| 长连接地址 | wss://im.example.com/ws | H5 端连不上,原生端走 TCP 可能正常 |
| 时区 | Asia/Shanghai | 消息时间差八小时,群里会吵 |
时区这项特别值得单说。PHP 的date.timezone和 MySQL 的time_zone是两套东西,只改一个还是错。我的做法是两边都写死,并且在代码入口统一调用date_default_timezone_set,三处一致才不出问题。这套系统涉及聊天记录时间戳、消息排序,差八小时不是显示问题,是消息顺序可能整体错乱。
顺便把上传限制三处一起改掉,只改一处等于没改:
; php.ini upload_max_filesize = 20M post_max_size = 24M date.timezone = Asia/ShanghaiNginx 那处client_max_body_size前面已经写了。三处的关系是层层设限,最小的那个决定实际能传多大。
3.4 长连接进程与守护:别用 nohup 了
长连接服务是常驻进程,不是 fpm 那种来一个请求起一个的模式。启动命令以源码包内自带的启动脚本为准,常见形态是根目录下的start.php,带start / stop / restart / status参数。开发期直接前台跑看日志,跑通后必须交给系统守护。
nohup ... &能用,但机器重启就没了,进程挂了还得人工发现。我现在一律写 systemd:
[Unit] Description=im long-connection worker After=network.target mysql.service redis.service [Service] Type=simple User=www Group=www WorkingDirectory=/www/wwwroot/im ExecStart=/usr/bin/php /www/wwwroot/im/start.php start ExecStop=/usr/bin/php /www/wwwroot/im/start.php stop Restart=always RestartSec=5 StandardOutput=append:/var/log/im-worker.log StandardError=append:/var/log/im-worker.err [Install] WantedBy=multi-user.target几个参数值得说:Restart=always加RestartSec=5是自动恢复的关键,进程崩了 5 秒后拉起来,避免疯狂重启打满 CPU。After里带上 mysql 和 redis,是为了避免开机时进程比数据库先起,连不上就退出,然后一直循环。日志单独指到文件,跟站点日志分开,出问题的时候能直接tail -f。
启用并验证:
systemctl daemon-reload systemctl enable --now im-worker # 状态是 active (running) 才算起,restart 计数一直在涨说明在反复崩 systemctl status im-worker4. 四端联调:H5 在 Safari 上的兼容项与 Android、iOS、PC 的接口改造
服务端跑通只是开始,四端联调才是真正花时间的地方。四端里 H5 最麻烦,因为它跑在别人家的浏览器里,而 Safari 的脾气跟 Chrome 差得不是一点半点。
4.1 H5 端:域名、wss 和 Safari 上必查的五处
H5 部署本身没有编译步骤,把构建产物放到站点目录就能访问,难点全在配置和兼容。第一件事是把接口地址全部改成 https 域名,包括 WebSocket 地址,必须是wss://。在 Safari 里,https 页面去连ws://会被静默拦截,控制台可能只留一行不痛不痒的警告,表现就是“登录成功但消息永远发不出去”。
第二件事是时间解析。这是 Safari 上最经典的一个问题:后端返回2024-05-01 10:20:30这种带短横线的格式,Safari 的new Date()解析结果是Invalid Date,Chrome 却正常。聊天列表的时间显示全变 NaN,看起来像后端问题,其实是浏览器差异。
// Safari 不认 "2024-05-01 10:20:30" 这种带短横线的时间串 // 统一换成斜杠再交给 Date 解析,四端表现才一致 function parseServerTime(str) { if (!str) return null; // iOS 上还可能是带 T 的 ISO 格式,一并归一化 const normalized = String(str).replace(/-/g, '/').replace('T', ' '); const d = new Date(normalized); // 解析失败时兜底为当前时间,避免列表整片显示 NaN return isNaN(d.getTime()) ? new Date() : d; }第三件事是语音消息播放。Safari 禁止没有用户手势的自动播放,audio.play()在没有用户交互的情况下会返回一个 rejected 的 Promise。解决办法是把播放绑到点击事件里,并且先load()再play(),同时把 Promise 的 catch 接住,失败时给个“再点一次”的提示,别让用户以为没反应。
// 语音播放必须由用户手势触发,先在 click 里 load 再 play playBtn.addEventListener('click', () => { audio.load(); const p = audio.play(); if (p !== undefined) { // Safari 会拒绝无手势的播放,这里必须接住,否则控制台报未捕获异常 p.catch(() => { showToast('请再点一次播放'); }); } });第四件事是登录态。Safari 的隐私策略对第三方 Cookie 卡得很严,如果 H5 和接口不同域,登录后用不了多久就掉线,刷新一次要重登一次。我的做法是尽量让 H5 和接口同主域,跨域时把 Cookie 的SameSite和Secure显式写清楚,别指望默认值。
第五件事是输入框。iOS 上键盘弹起会把整个页面顶上去,聊天气泡错位,键盘收起后不回落。处理方式是监听resize和focus/blur,在键盘弹出时把消息列表容器高度改成window.innerHeight算出来的实际值,而不是写死100vh——Safari 的100vh包含地址栏高度,这是老问题了。
4.2 Android 端:包名、签名和接口地址三处联动
Android 端要改的地方不多,但每处都不能漏。包名(applicationId)一旦改掉,推送、第三方登录的回调配置都要跟着改,所以建议在正式打包之前就定死,别等到上架前再动。
接口地址一般集中在某个常量类或者build.gradle的buildConfigField里。用buildConfigField的好处是能按 debug / release 分环境,不用手工改:
// 按构建类型区分接口地址,避免测试包连到生产 android { buildTypes { debug { buildConfigField "String", "API_BASE", "\"https://im-test.example.com\"" } release { buildConfigField "String", "API_BASE", "\"https://im.example.com\"" } } }带 WebSocket 的还要单独配一个WS_BASE,值是wss://开头。有人只改了API_BASE,登录通了但消息不走,就是漏了这一项。
签名这块,release 包必须用正式 keystore,debug 签名打出来的包在某些机型上装都装不上。keystore 文件和密码单独保管,不要跟着源码一起传,这是血泪经验——换一次签名等于所有已装用户必须卸载重装。
4.3 iOS 与 PC 端:ATS 配置和缓存清理
iOS 端除了前面提到的 ATS,还要注意最低支持版本。工程里的IPHONEOS_DEPLOYMENT_TARGET如果设得太高,老设备装不上;设得太低,某些新 API 又要加一堆@available判断。按目标用户群的设备情况定,别照抄别人的配置。
ATS 的写法前面说过,用NSExceptionDomains白名单而不是全放开:
<!-- 只对测试域名开例外,上线前整段删掉 --> <key>NSAppTransportSecurity</key> <dict> <key>NSExceptionDomains</key> <dict> <key>im-test.example.com</key> <dict> <key>NSIncludesSubdomains</key> <true/> <key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key> <true/> </dict> </dict> </dict>PC 端最容易出问题的不是代码,是缓存。改完服务地址重新打开还是连旧服务器,通常是本地缓存或者配置文件没跟着更新。我一般让测试的同学在改地址后清一次应用数据目录,确认配置文件里写的是新地址,再启动。
5. 排错现场:装完打不开的五个典型现象与处置顺序
这类四端系统出错时,报错信息往往指向错误的地方。按下面这五条对号入座,能省掉大量瞎猜的时间。
现象一:首页 502,或者浏览器提示“No input file specified”。原因基本锁定在三处:root指到了项目根目录而不是public,fastcgi_param SCRIPT_FILENAME那一行漏了或者写错,再就是 php-fpm 根本没起来。解决顺序是先systemctl status php-fpm看进程,再nginx -t看配置语法,最后看 Nginx 的error.log,那里会明确写“connect() failed”还是“Primary script unknown”。前者是 fpm 没起,后者是路径不对。
现象二:登录一直转圈,Network 面板里 socket 请求失败或者返回 400。这是长连接没通。检查顺序:先用curl -i -H "Upgrade: websocket" -H "Connection: Upgrade" https://域名/ws看返回是不是 101,不是 101 就是 Nginx 反代没配对;再看长连接进程是不是活着;最后确认客户端连的是wss://不是ws://。Safari 这类浏览器对混合内容卡得很死,https 页面连明文 ws 会被直接拦掉,控制台那行警告很容易被忽略。
现象三:消息发出去,对方刷新后才看得到,实时收不到。典型的常驻进程问题。systemctl status im-worker看Active和重启计数,如果计数一直在涨,说明进程起来就崩。多半是缺sockets或pcntl扩展,或者 Redis 连不上。看/var/log/im-worker.err,前面几行通常就有答案。还有一种情况是进程活着但业务逻辑崩在某个回调里,这时候看它自己的业务日志,别只看系统日志。
现象四:图片、语音上传失败,接口返回 413 或 500。三处大小限制没对齐:php.ini的upload_max_filesize、post_max_size,还有 Nginx 的client_max_body_size。改完必须重启 php-fpm 和 reload nginx,只改文件不重启等于没改。另外确认上传目录有写权限,且用户是 www,root跑的应用写出来的文件后面会各种权限报错。
现象五:Safari 上登录态反复丢失,Chrome 却一切正常。前面提过,Cookie 的SameSite和Secure没配好,跨域场景下 Safari 直接不存。排查方式是打开开发者工具的资源面板看 Cookie 有没有落下来,没落下来就是策略问题,跟后端逻辑无关。顺带确认时间戳解析有没有问题,登录返回的过期时间如果被解析成Invalid Date,前端会立刻判定过期,表现也是“登录完马上掉线”。
还有一种情况不在这五条里但很常见:改完配置不生效。九成是没重启对应服务,或者改错了配置文件——同一台机器上装过多个 PHP 版本,php.ini有好几份,改的是不用那份。php --ini能看到实际加载的是哪个文件。
6. 验收跑通:用一条消息走完全链路来判定安装成功
装完不等于跑通。四端系统最怕的情况是“看起来能用”,实际上某个环节已经断了但没人发现,上线后用户量一上来就暴露。我验收只做一件事:让 A 端发一条消息给 B 端,然后沿途每一站都验一遍。
具体是这么走的。先在服务端盯四个窗口:tail -f /var/log/im-worker.log看长连接日志、tail -f /var/log/nginx/access.log看请求有没有打到反代、redis-cli -a 密码 monitor看有没有 key 写入、mysql -u im_user -p im_core -e "select id,from_id,to_id,content,create_time from im_message order by id desc limit 5;"看消息落库。然后 A 端发一条带文字、再发一条语音。
判定的标准按顺序卡:
| 检查点 | 通过标准 | 不通过的定位方向 |
|---|---|---|
| Nginx 日志 | 出现/ws且状态 101 | 反代配置、Upgrade 头 |
| worker 日志 | 有连接建立和消息投递记录 | 进程没起或扩展缺失 |
| Redis | 能看到 token 和在线 key | Redis 密码或库号配错 |
| 数据库 | 新消息 id 递增,content有值 | 字符集或写入逻辑 |
| B 端 | 无刷新即收到,时间显示正确 | 长连接地址、Safari 时间解析 |
| 语音回流 | B 端点开能播,Safari 上同样能播 | 上传目录权限、播放手势绑定 |
这一步最值钱的地方是它把“谁的问题”切开了。消息进了 Redis 但没进库,是消费端的事;进了库但 B 端没收到,是推送链路的事;B 端收到但时间是 NaN,是前端解析的事。一路卡一遍,问题基本跑不掉。
再补两个收尾动作。一是重启机器,等所有服务自动起来后再发一条消息,验证 systemd 的After依赖配对了没有;二是把四端各退出登录再登一次,确认退出逻辑清了 Redis 里的在线状态,否则会留下“永远在线”的假数据,时间长了内存一直涨。
从那以后我每次部署这类多端系统,不管文档写得多完整,上线前都强制走一遍这条发消息的链路,哪怕只是改了一个配置项。四端互通的好处是所有端体验一致,坏处是任何一处配置错了,症状都可能跑到另一个端上表现出来。这套源码本身支持二次开发,后面加商城、加功能都要在这个基础上动,基线没验干净,后面的问题会越堆越难查。希望帮到你。
本文还有配套的精品资源,点击获取