
简介一份经典的模仿腾讯QQ功能的即时通讯源代码工程采用C/C编写面向已有一定编程基础、希望系统掌握即时通讯软件开发的中级学习者涵盖界面布局、网络通信、多线程并发、事件驱动响应等完整技术链。压缩包共622个文件大小约1.1MB内容以bmp位图、h/cpp源码文件为主另有dsp工程文件、ico图标和sql数据库脚本目录结构清晰可直接打开研读。已有407人学习。透过源码可对照学习界面设计、多线程、网络通信、消息序列化、数据库操作、事件驱动、状态管理、加密传输、性能优化与测试调试等多项技术点的落地写法并能梳理网络层与界面层的解耦方式、状态机对界面刷新的驱动以及数据库读写与安全传输等实用细节帮助读者将零散知识串联成完整系统作为毕业设计或二次开发的蓝本。1. 仿腾讯QQ源代码一张登录态与消息链路图比界面更值钱仿腾讯QQ源代码这个方向很多人第一反应是做聊天界面、气泡、头像真正做进去会发现QQ这类IM最值钱的不是UI而是“一条消息怎么可靠地从A到达B”这条链路。登录态、心跳、离线补拉、消息顺序这些才是仿QQ的核心工程量也是面试和实战最常被追问的模块。这篇笔记按我实际搭过的最小IM系统来讲先从架构和协议边界说起再逐步落到登录、消息、心跳、离线、群聊的代码与参数最后把踩过的坑列出来。适合正在做课程设计、想进IM方向或者公司要做内部聊天系统的开发者照着复现一套能跑、能扛、能说清原理的仿QQ源代码。2. 仿QQ先定架构边界技术栈、协议与最小骨架2.1 客户端与服务端技术栈怎么选别一上来就追求“全栈还原”仿QQ的常见误区是客户端和服务端同时开工结果两边都没做透。我的建议是先定边界重点放在服务端消息链路客户端先用Web或Electron验证不要一上来就写C高仿界面。模块常见选型理由服务端Java Netty / Go长连接管理成熟Netty对粘包半包有现成解码器客户端WebSocket Vue/React 或 QtWeb验证逻辑最快Qt贴近QQ桌面端形态数据库MySQL RedisRedis存session和在线状态MySQL存离线消息文件本地磁盘 Nginx图片头像走HTTP避免大包阻塞消息通道曾经见过有人用C还原QQ2008界面做了三个月UI消息收发还没通。仿QQ的落地顺序应该是先服务端把链路跑通再客户端贴界面。技术栈不需要和腾讯一致QQ当年是C自研但今天做仿品用Netty或Go都能达到同样的效果关键是先把链路做扎实。2.2 传输协议TCP私有协议与WebSocket怎么取舍QQ的早期版本用的是UDPTCP混合私有协议心跳、消息指纹都在二进制包里。仿QQ时最常见的做法是用WebSocket或TCP JSON两者各有利弊WebSocket对浏览器客户端友好调试成本低天然有帧边界不用自己处理粘包适合先用它验证消息链路。TCP 自定包头则更贴近QQ真实形态能控制包体压缩但要自己处理粘包、半包、心跳、重传工程量大适合想深入IM底层的人选。协议包体的通用设计是“头部固定字段 消息体”头部至少包含魔数、包长度、命令字、序列号。序列号是消息去重和回执的地基没有它后面的心跳重传、消息补拉都做不了。// 协议头示例C结构体风格按网络字节序传输 typedef struct _msg_header { uint16_t magic; // 魔数固定0x5A5A防止错位解析 uint16_t cmd; // 命令字登录/心跳/消息/回执 uint32_t seq; // 序列号客户端自增用于去重 uint32_t body_len; // 消息体长度单位字节 } msg_header_t;魔数的意义是让解码器在粘包和错位时能快速重新找头命令字决定后续body按哪个结构体解析序列号是心跳重发时判断“这条消息客户端到底收到没”的关键。这个头部设计在TCP和WebSocket下都通用区别只在WebSocket把边界问题交给了协议本身处理。2.3 最小可跑服务端骨架Netty监听连接与半包处理服务端骨架不需要一开始就做全部业务。先用Netty搭一个只做三件事的demo接受连接、读心跳、原样回pong。这是仿QQ系统里唯一能“跑通后再往上加功能”的地基。// 服务端启动类监听8848端口注册解码器与业务Handler EventLoopGroup boss new NioEventLoopGroup(1); EventLoopGroup worker new NioEventLoopGroup(8); ServerBootstrap bootstrap new ServerBootstrap(); bootstrap.group(boss, worker) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializerSocketChannel() { protected void initChannel(SocketChannel ch) { ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(65536, 6, 4, -10, 0)); ch.pipeline().addLast(new MsgDecoder()); ch.pipeline().addLast(new HeartbeatHandler()); } }); bootstrap.bind(8848).sync();LengthFieldBasedFrameDecoder的参数是本段的核心65536是单包最大字节数6是长度字段的偏移量也就是magic占2字节、cmd占2字节、seq占2字节之后从第6个字节开始是body_len这个长度字段4表示长度字段自身占4字节-10这参数的含义是分帧后要跳过多少字节找到真正的消息体。具体场景下lengthFieldOffset6lengthFieldLength4lengthAdjustment-10表示“长度字段结束位置到整个包结束位置的距离调整值”因为协议头总共10字节定义里报头要占满实际长度所以取负数把报头长度扣除initialBytesToStrip0表示解码后的数据保留全部协议头。这样TCP的粘包半包问题在解码器层被解决业务代码里不会看到半截包。心跳Handler先简单写业务后面再加// 读到一个包后如果是心跳命令字就回pong if (msg.getCmd() CMD_HEARTBEAT) { ctx.writeAndFlush(new MsgPacket(CMD_HEARTBEAT_PONG, msg.getSeq())); }这样最小骨架就有了解析和回包能力。后面的登录态、消息转发都是在这个pipeline上加Handler而不是推倒重来。骨架跑通后下一步就是最核心的登录态与消息链路。3. 把一条消息从登录送到好友session、上行、下发与心跳3.1 登录接口与token下发仿QQ里第一个要较真的模块登录在仿QQ里是所有东西的前提用户不上线消息无从谈起。常见的做法是客户端调HTTP登录接口服务端校验账号密码成功后签发一个token同时把连接与token绑定。// 登录成功后为连接创建Session并绑定用户 public Session login(Channel ch, long userId, String token) { Session s new Session(); s.setChannel(ch); s.setUserId(userId); s.setToken(token); // 踢掉同userId的旧连接这里保留最后一次登录 Session old sessionManager.removeByUserId(userId); if (old ! null) { old.getChannel().close(); } sessionManager.addSession(userId, s); return s; }这里有两个容易被忽略的细节。一是踢旧连接QQ同一账号在桌面端和手机端可以共存但仿QQ最小版本一般先做成“后登录踢先登录”否则消息会同时下发到两条连接造成混乱。二是Session要放在内存管理器里而不是散落在各Handler中方便后面做离线判断、消息转发时快速查到目标连接。还要处理“连接建立但还没登录”的状态Netty里连接一进来就有一个Channel实例但此时不该能收发业务消息。我习惯在SessionManager里维护两个集合一个放已登录的在线用户一个放未认证的连接只有token校验通过后才把连接迁过去。否则有人开一堆连接不发登录包就白占内存和线程。3.2 消息上行与下发协议体字段少一个后面就得多补一个接口仿QQ的消息转发是最核心的业务代码。A要发消息给B完整流程是A把消息发给服务端服务端查B在不在线在线就下发不在线就写离线消息。消息体的设计决定了后面扩展是否顺手。{ cmd: msg.send, seq: 1001, from: 10001, to: 10002, msg_type: 1, body: { content: hello, client_time: 1710000000 } }msg_type用数字而不用字符串是为了压缩包体字符型可读性好但同样内容每个字多占若干字节。body里的client_time是客户端的发送时间而服务端时间要单独记录这两个时间是后面消息排序和“消息延迟”排查的重要依据。仿QQ里最常见的错误是不存client_time出问题时连“是客户端延迟还是服务端延迟”都分不清。服务端的转发Handler核心逻辑如下public void onMsgSend(Channel fromCh, MsgPacket req) { long fromUid sessionManager.getUserId(fromCh); long toUid req.getTo(); Message msg new Message(fromUid, toUid, req.getBody()); // 消息先落库并生成全局ID再决定在线下发还是离线存储 long msgId messageStore.insert(msg); Session target sessionManager.getSession(toUid); if (target ! null target.getChannel().isActive()) { target.getChannel().writeAndFlush(buildMsgDownPacket(msgId, msg)); } else { offlineStore.save(toUid, msg); } }先落库再下发这条顺序很关键。如果先下发再落库客户端收到了消息但数据库里没有后面客户端一刷新记录就丢了如果先落库但落库失败应该直接拒绝这次发送并告知客户端。消息ID用全局自增或雪花ID都行但必须是服务端生成而不是客户端带过来否则两个客户端时钟不同步消息排序必然乱。3.3 心跳与重连的默认参数QQ式保活是怎么调出来的长连接最怕的不是断线而是“我以为还连着”。仿QQ里心跳是维持session有效性的闸门。常见参数客户端每30秒发一个心跳包服务端60秒没收到就判定连接死亡清理session。// 服务端空闲检测读空闲60秒就关闭并清理在线状态 pipeline.addLast(new IdleStateHandler(60, 0, 0, TimeUnit.SECONDS)); // 客户端那边可以用Timer或Netty写空闲检测30秒发一次心跳这个60/30的组合是实测下来相对稳妥的默认值。太短会打爆服务器一台机器上几万个连接每个10秒发一次包量就上来了太长则踢不掉死连接服务器内存里堆积大量“僵尸session”。生产上如果发现大量客户端“在打字但发不出消息”多半是NAT超时比心跳间隔短比如运营商NAT映射60秒没流量就回收那心跳就得改成20秒。心跳还有一个隐藏作用补偿客户端的网络切换。手机从WiFi切到4G旧TCP连接已经死了客户端只有靠发心跳发现收不到pong才触发重连。重连后要做的是先关旧连接、重新登录、再补拉离线消息。这串动作的先后顺序如果反了就会出现“重连成功但数据一直不刷新”的现象。4. 离线消息与群聊存储层决定你能撑多少人4.1 离线消息的存储与补拉先想清楚“消息发给了谁”单聊的离线消息在仿QQ里最好处理A发给BB不在线就把这条消息存到一张离线表里。字段做成收件人ID、发件人ID、消息内容、服务端时间、全局消息ID。B上线时一次性拉取拉完标记已读。CREATE TABLE offline_msg ( id BIGINT PRIMARY KEY AUTO_INCREMENT, to_uid BIGINT NOT NULL, from_uid BIGINT NOT NULL, content TEXT, server_time BIGINT NOT NULL, is_read TINYINT DEFAULT 0, KEY idx_to_uid_time (to_uid, server_time) );补拉接口要带两个参数上次拉取的全局消息ID和数量上限。原因是用户上线可能积压上百条消息一次拉完网络包太大也容易把客户端卡死。每次按100条拉取客户端通过消息ID判断自己是第几次拉。这里有个判断标准离线消息拉取是“至少一次”语义还是“恰好一次”语义仿QQ阶段选至少一次即可客户端对重复消息用自己的去重表兜底。4.2 群聊的扩散读与扩散写群里50个成员同时说话时你该怎么做群聊是仿QQ里最容易拖垮库的设计。两种主流模型扩散写和扩散读。扩散写是群里50人发一条消息就往群里每个成员的收件箱各写一份消息副本扩散读是只存一条群消息成员上线时现查群成员表去读。QQ这类大体量IM用的是混合策略小群扩散写、大群扩散读。建议仿QQ在一开始就采用“群消息单份存储 成员拉取”的扩散读理由很实际代码量少一半也不会出现群里500人、一条消息写500份离线表的可怕场景。缺点是群成员超过2000时读扩散的查询会越来越大那时再做扩散写和大群缓存才有必要。CREATE TABLE group_msg ( id BIGINT PRIMARY KEY AUTO_INCREMENT, group_id BIGINT NOT NULL, from_uid BIGINT NOT NULL, content TEXT, server_time BIGINT NOT NULL, KEY idx_group_time (group_id, server_time) );群消息已读位置的记录用一张独立的“成员游标表”每人的已读位置存的是最后一条群消息ID。新消息下发时直接广播给在线成员离线成员下次登录时按游标补拉。游标表的写入频率是关键如果每次用户看一条消息就写库压力很大常见做法是客户端定时上报已读位置而不是每条都上报。4.3 聊天记录与文件想导出QQ聊天记录背后就是两张表加一个HTTP服务仿QQ发展到一定阶段用户一定会要聊天记录导出、图片头像、文件传输这些能力。聊天记录导出本质上是把上面的消息表按用户维度查出来生成文件下载没有太多技术难点。头像和图片则一定要走独立HTTP通道不能让大文件阻塞长连接的消息链路。文件服务拆成两个角色上传和下载。上传时客户端先HTTP POST到文件服务服务端返回一个file_id消息体里只带file_id和文件MD5接收方拿到file_id再去下载。这样消息通道里永远只传小包大文件不拖慢心跳和消息转发。文件传输的坑集中在断点续传和MD5校验上。仿QQ阶段可以用“整文件上传 key存OSS或本地磁盘”起步续传、秒传这些等文件业务量大了再加。聊天记录导出要注意权限只能导出本人相关的会话这个在做查询SQL时就要带上uid条件不能写一条裸查询把全库记录导出去。5. 仿QQ实现里的典型翻车现场现象、原因与修复5.1 现象客户端收到的消息body被截断或多包拼在一起原因TCP是流式协议没有包边界。没加解码器时第一条消息发了“hello”第二条发了“world”客户端一次读到了“helloworld”解析JSON直接失败。解决在服务端pipeline最前面加LengthFieldBasedFrameDecoder客户端加同样的长度处理。注意解码器的长度字段偏移和长度调整必须和协议头完全一致错一个字节就全部翻车。检查时先看十六进制抓包确认magic和body_len对得上再怀疑别的。5.2 现象心跳重传之后对方手机出现两条一模一样的内容原因客户端发消息时没等服务端回ack就超时重发导致服务端收到两条seq不同的相同消息。这是典型的“至少一次”语义带来的副作用。解决重发时复用同一个业务消息ID服务端用消息ID做去重重复的丢弃或只作为回执处理。去重表放在Redis里key用msgId过期时间设为24小时覆盖网络抖动的最坏情况。另外客户端发消息时就要记下msgId服务端回执里带同一个ID客户端才能对应上。5.3 现象用户断网重连后明明发出去的消息好友没收到原因重连时只重建了TCP连接没有重新登录。服务端认为旧session还在新连接的channel没有绑定userId消息来了找不到发送者。解决重连成功后强制走登录流程拿新token并把之前channel上绑定的session清理掉。服务端在收到消息时校验channel上的session是否有效无效一律拒绝。这里别图省事跳过登录直接复用旧token否则旧连接被踢后新连接拿着旧token又连上状态会乱。5.4 现象同时在线的人数一多服务端频繁GC消息延迟跟着上来原因每个连接都绑定了大数组编码解码后对象长期存活GC压力大。常见的是心跳包进来每次都new一个MsgPacket对象量大了就内存飞涨。解决对心跳包做专门处理不走普通消息编解码链路减少对象创建聊天消息对象池复用。一台4G堆的机器在线5000连接就很卡接入对象池后撑到2万才需要扩容。排查GC问题时先看日志里是Young GC还是Full GCFull GC多半是session泄漏或解码器缓冲没释放。5.5 现象偶尔出现消息错发到别人那里原因多半是sessionManager的HashMap在多线程下并发操作连接事件和消息事件线程不一致导致get和put之间竞争取到了错的channel。解决sessionManager加锁或者用ConcurrentHashMap并保证同一userId的读写都落到同一个线程。Netty里给每个channel串行化执行不让跨channel的线程污染状态。这种错发最难复现抓包看到目标channel不对基本就是并发竞争先加锁再观察。仿QQ里这个坑最隐蔽因为单机测一百条消息可能都正常在线一多就露馅。6. 验证一个仿QQ系统压测、抓包和机器人三件套仿QQ系统写完验证方式不是人工开两个窗口互发而是三件套走一遍压测、抓包、挂机器人。压测我常用Netty自带的方式或JMeter重点压两个接口登录爆发和消息转发。登录爆发会瞬间打满数据库连接池消息转发则考验内存和GC。压测时盯三个指标TPS、P99延迟、GC pause任何一个异常都要回到上一章排查。给自己定个底线单机2万连接P99延迟小于50msGC pause小于200ms达不到就继续调。抓包拿Wireshark看TCP流里的粘包半包处理是否正确抓包时注意TLS或加密协议不会让你看到明文。为了验证协议逻辑先在本地关掉加密跑通后再开启TLS否则抓包抓到的全是密文排查效率极低。挂QQ机器人是个取巧但有效的验证手段写一个自动回复的客户端接入自己的仿QQ服务端创建两个机器人账号互聊自动跑一晚上第二天看消息丢失率。比人工点一整天有效得多。机器人这边会用到心跳包重传、断线重连、离线补拉这些逻辑正好把第3章和第5章的代码都过一遍相当于一个自动化的回归测试脚本。我自己的习惯是每次改动协议或存储层先跑三件套里性价比最高的压测发现问题就回到第5章的清单逐条排查。这套流程走下来仿QQ系统才算真正能交付而不是一个只能演示的demo。希望帮到你。本文还有配套的精品资源点击获取