
简介面向计算机专业课程设计场景这份仿QQ即时通讯系统项目基于Android Studio开发环境构建模拟主流社交应用核心交互模式完整提供客户端与服务端程序源码、结构化数据库以及实验报告文档适合计算机科学与技术、软件工程等专业学生作为期末大作业或综合实践课题技术难度控制在中等水平。压缩包共含826个文件整体大小约5.68MB以Java程序源码、界面布局配置和图片资源为主体另有数据库脚本与数据库文件、构建脚本及外部依赖库目录组织清晰便于导入编译与分模块学习。项目在专业教师指导下完成评审得分98分并经教学助理审核认定实现了用户注册登录、好友管理、消息收发与实时监听、数据库连接池等核心功能各模块经多轮测试可稳定运行对掌握Android网络通信、数据持久化以及客户端与服务器协作方式具有完整参考价值。目前已有48人学习下载。1. Android Studio仿QQ即时通讯系统这个项目的完整度比标题看起来要高得多如果有人拿“Android Studio仿QQ即时通讯系统完整源码、数据库与实验报告”来问我参考怎么做我会先泼一盆冷水这个标题看着像个客户端项目真正卡人的地方几乎都在Android之外。仿QQ意味着三层必须同时成立——能跑通登录和聊天的Socket长连接、能落进SQLite且不丢不重的数据层、能把设计和踩坑讲明白的实验报告。它适合当作课程设计、毕业设计或者只想快速拿一套可用底座做二次开发的初级工程师。下面按“先定架构、再落数据、再写通信、最后过验收”的顺序拆参数和排错顺序都给到照着复现一次就知道坑在哪。2. 技术选型先行客户端-服务器架构与四个模块的边界先说选型。很多人在Android Studio里新建项目后第一反应是找个聊天UI模板把列表、气泡、输入框做出来。这个顺序在仿QQ项目里是反的消息到底从哪来、怎么保证发出去能到、掉线怎么重连这些通信问题不先定下来界面再像也只是静态图。我一般会把通信方案和模块边界放在第一优先级。2.1 为什么仿QQ必须用真Socket而非HTTP轮询HTTP轮询是最容易上手的方案客户端每隔一两秒请求一次服务器把新消息拉回来。它在实时性、流量消耗和服务端推送能力上都有明显短板。仿QQ要被追问的核心点通常是“在线状态怎么维护、消息为什么实时、重连怎么处理”只有长连接能把这三个问题讲完整。方案实时性服务端主动推送实现成本适合场景HTTP轮询秒级取决于轮询间隔不支持低消息频率很低的演示Socket长连接毫秒级支持中仿QQ、单聊、在线客服WebSocket毫秒级支持中高浏览器端IM客户端配合库较复杂要做长连接常见做法是java.net.Socket加多线程。服务器用一个端口监听每个连接进来后分一个线程处理客户端持有一条连接通过Line-based协议收发JSON。端口我习惯选8888这种1024以上的段避开HTTP的8080联调时不容易和本机其他服务撞。2.2 模块划分登录、会话、消息、联系人UI与通信层解耦仿QQ的最小闭环可以先圈定在“单聊好友列表”群聊、文件传输、语音视频都不要进第一版。范围定了之后代码分包就清晰了我一般会按下表拆模块职责典型类UI层聊天列表、消息气泡、输入框、登录注册界面ChatActivity、MessageAdapter通信层Socket连接、心跳、重连、收发JSONSocketClient数据层SQLite增删改查、未读数更新、会话列表DbHelper、MessageDao协议层消息体、登录体、字段定义与JSON序列化Message、LoginRequest这里有个重点Socket收到的JSON解析必须放在协议层UI层只拿已经组装好的实体对象。常见做法是通信层的回调里先把JSON转成Message再通过Handler抛给Activity。如果让Activity直接碰流界面一多、协议一改整个项目都会跟着返工。消息气泡可以用Android自定义组件来做但不必真去继承View画圆角。偷懒又可靠的方式是shape drawable自己发的消息用绿色圆角背景左侧加一截小尾巴对方的消息用浅白色圆角背景。资源文件命名一律小写字母加下划线图标写成ic_chat.png别写成“Chat.png”或中文名否则后面编译时很容易遇到Android Studio的资源重复错误R类生成失败会连累整个项目。!-- 右侧气泡背景自己发送的消息 -- shape xmlns:androidhttp://schemas.android.com/apk/res/android solid android:color#95EC69 / corners android:radius4dp / /shape这段资源的逻辑很简单solid定义填充色corners定义圆角半径。参数上注意radius不要超过气泡高度的一半否则看起来会像胶囊仿QQ的直角小气泡风格就偏了。左侧气泡同理把solid改成#FFFFFF再加一个1dp的灰色描边即可。模块边界定清楚后联调顺序我一般这样走先让SocketClient和服务端用纯文本登录跑通再看服务端日志确认消息转发最后才让MessageAdapter绑定数据。先通链路再画界面后面改协议时不会动到界面代码。3. 数据库设计与落地SQLite建表与增删改查的注意点标题里写“数据库”落到Android本地就是SQLite。仿QQ的离线消息、聊天记录、未读数都要靠它存。常见做法是直接用SQLiteOpenHelper建库不用Room因为课程设计阶段要展示的就是你懂不懂原生增删改查Room封装太多答辩时反而不容易讲出细节。下面这套表结构是跑通过的最小方案。3.1 建表SQL用户表、会话表、消息表的关系设计-- 用户表既是登录注册表也兼任好友表is_friend1表示互为好友 CREATE TABLE user ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, password TEXT NOT NULL, nickname TEXT, is_friend INTEGER DEFAULT 0, online INTEGER DEFAULT 0, last_login_time INTEGER ); -- 会话表一个会话对应一个好友保存最后一条消息用于列表展示 CREATE TABLE conversation ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, peer_id INTEGER NOT NULL, last_msg TEXT, last_time INTEGER, unread_count INTEGER DEFAULT 0 ); -- 消息表msg_id唯一用来去重 CREATE TABLE message ( id INTEGER PRIMARY KEY AUTOINCREMENT, msg_id TEXT NOT NULL UNIQUE, conversation_id INTEGER NOT NULL, from_id INTEGER NOT NULL, to_id INTEGER NOT NULL, content TEXT, msg_type INTEGER DEFAULT 0, status INTEGER DEFAULT 0, timestamp INTEGER NOT NULL ); CREATE INDEX idx_msg_conv_time ON message(conversation_id, timestamp);外键在这里不建是故意的。SQLite默认不开启外键约束写了外键也只是多个声明删数据时还要自己操心顺序。课程设计的规模里应用层保证关系比数据库约束更直接。timestamp统一用INTEGER存毫秒时间戳排序、格式化都比TEXT清爽。索引建在conversation_id和timestamp上是为了让会话里的聊天记录分页查询走索引消息多时不会全表扫描。3.2 消息落库用insertWithOnConflict做去重用事务保证不脏登录注册和消息写入都走SQLiteOpenHelper的getWritableDatabase()。插入消息是最容易写错的地方因为Socket收包线程、重连后的离线拉取线程可能同时往库里写。我一般这样写ContentValues values new ContentValues(); values.put(msg_id, message.getMsgId()); values.put(conversation_id, getConversationId(message.getFromId(), message.getToId())); values.put(from_id, message.getFromId()); values.put(to_id, message.getToId()); values.put(content, message.getContent()); values.put(msg_type, message.getMsgType()); values.put(status, 1); values.put(timestamp, System.currentTimeMillis()); SQLiteDatabase db dbHelper.getWritableDatabase(); try { db.beginTransaction(); long rowId db.insertWithOnConflict(message, null, values, SQLiteDatabase.CONFLICT_IGNORE); db.setTransactionSuccessful(); return rowId ! -1L; } finally { db.endTransaction(); }这里两个参数是关键。insertWithOnConflict配合msg_id的唯一索引重复的消息会被静默忽略rowId返回-1这就是幂等去重的落点。事务包裹的意义在于如果后面还要同时更新会话表的last_msg字段两条写操作要么都成功要么都回滚不会出现“消息表多了一行、会话列表没更新”的脏状态。会话列表的读取也固定成一条SQLSELECT c.peer_id, u.nickname, c.last_msg, c.last_time, c.unread_count FROM conversation c LEFT JOIN user u ON c.peer_id u.id WHERE c.user_id ? ORDER BY c.last_time DESC;参数用?占位通过selectionArgs传用户ID不要拼字符串。数据库增删改查的完整闭环在这套结构里都能对应上注册是insert user登录是select user发消息是insert message清空聊天记录是delete message已读是update unread_count。3.3 实验报告里的数据库部分E-R图、数据字典与核心SQL怎么写报告里如果只贴建表语句评阅人看不出你理解了什么。数据库部分我建议放四样东西E-R图、数据字典、核心SQL、一段“为什么这样设计”的分析。E-R图用draw.io画三个实体框User、Conversation、Message关系标成一对多即可不要画复杂。数据字典片段可以按这个格式整理字段类型说明msg_idTEXT UNIQUE消息唯一标识由发送方生成conversation_idINTEGER指向会话表用于拉取聊天记录statusINTEGER0发送失败1入库成功2已读timestampINTEGER发送时间毫秒时间戳“为什么这样设计”写两句就有区分度一是msg_id用唯一索引是为了在网络重试时幂等二是会话表冗余了last_msg和last_time是为了聊天列表不需要join全表消息。这两点能直接回应“你考虑过数据一致性和查询性能吗”。4. 手写通信层从Socket握手到心跳包的完整流程通信层是仿QQ的灵魂。正文不会给你一个调好的现成jar包但下面这套最小实现可以原样落到你的工程里跑通协议、端口、心跳参数都是可调的。4.1 服务端最小实现多线程Socket与登录认证服务端我习惯单独建一个Java工程跑在电脑上不放进Android项目。代码用纯Java日志直接打到控制台排错时比Android的Logcat更直观。public class ChatServer { private static final int PORT 8888; private final ConcurrentHashMapString, PrintWriter onlineClients new ConcurrentHashMap(); public void start() throws IOException { ServerSocket serverSocket new ServerSocket(PORT); ExecutorService pool Executors.newFixedThreadPool(200); while (true) { Socket socket serverSocket.accept(); pool.execute(new ClientHandler(socket)); } } private class ClientHandler implements Runnable { private final Socket socket; ClientHandler(Socket socket) { this.socket socket; } Override public void run() { String username null; try (BufferedReader in new BufferedReader(new InputStreamReader( socket.getInputStream(), UTF-8)); PrintWriter out new PrintWriter(new OutputStreamWriter( socket.getOutputStream(), UTF-8), true)) { String line; while ((line in.readLine()) ! null) { JSONObject json new JSONObject(line); String type json.optString(type); if (login.equals(type)) { username json.getString(username); onlineClients.put(username, out); out.println(new JSONObject() .put(type, login_result) .put(result, 1) .put(msg, ok)); } else if (chat.equals(type)) { String to json.getString(to); PrintWriter target onlineClients.get(to); if (target ! null) { target.println(json.toString()); } else { // 目标用户不在线写入离线消息 saveOfflineMessage(json); } } else if (ping.equals(type)) { out.println(new JSONObject().put(type, pong)); } } } catch (Exception e) { e.printStackTrace(); } finally { if (username ! null) { onlineClients.remove(username); } } } } public static void main(String[] args) throws IOException { new ChatServer().start(); } }这段的逻辑是按行读JSON登录时把用户名和输出流存进ConcurrentHashMap聊天时按to字段找到目标连接在线就原样转发不在线就落离线表心跳回一个pong。参数上注意两点线程池用newFixedThreadPool(200)而不是CachedThreadPool长连接场景下无界线程池会被慢连接拖垮PrintWriter第二个参数true表示每行自动flush这样客户端readLine才能立刻拿到数据。前端同学如果熟悉HTTP会问“这是不是类似HTTP的请求响应”其实这里是一条TCP连接持续复用和HTTP无关。4.2 Android客户端Socket封装连接、收包与心跳Android端的关键是不能在UI线程碰Socket。我把连接和收发都放进一个线程回调通过接口抛给调用方Activity再交给Handler刷新界面。public class SocketClient { public interface Callback { void onConnected(); void onMessage(String json); void onError(String message); } private Socket socket; private PrintWriter out; private volatile boolean running; private Callback callback; public void connect(final String host, final int port, Callback cb) { callback cb; running true; new Thread(new Runnable() { Override public void run() { try { socket new Socket(); socket.connect(new InetSocketAddress(host, port), 5000); out new PrintWriter(new OutputStreamWriter( socket.getOutputStream(), UTF-8), true); BufferedReader in new BufferedReader(new InputStreamReader( socket.getInputStream(), UTF-8)); callback.onConnected(); String line; while (running (line in.readLine()) ! null) { callback.onMessage(line); } } catch (IOException e) { callback.onError(连接失败或断开: e.getMessage()); } } }).start(); } public synchronized void send(String json) { if (out ! null) { out.println(json); } } public void close() { running false; try { if (socket ! null) socket.close(); } catch (IOException ignored) { } } }connectTimeout设5000毫秒超过即抛异常。send加synchronized是为了防止心跳线程和发消息线程同时println把两行JSON拼成一行导致服务端解析失败。心跳我单独起一个线程每30秒发送{type:ping,ts:当前时间戳}服务端回pong。socket.setSoTimeout不要设保持阻塞读断线靠异常触发心跳的职责是确认“双方还活着”而不是靠读超时猜断线。4.3 消息收发协议与离线消息msgId是去重的关键协议格式统一成一个JSON一行字段用小驼峰避免服务端和客户端因为命名对不上而翻车。字段类型说明typeStringlogin、chat、ping、pongmsgIdString发送方ID时间戳随机数fromString发送方用户名toString接收方用户名contentString消息内容tslong发送时间毫秒离线消息的常见做法是服务端多一张offline_message表保存完整JSON。用户登录成功后服务端先把离线记录逐条推给客户端再删除对应行。客户端收到消息时不能直接刷新界面要先按msgId查一次本地库查得到说明重复直接丢弃查不到再入库并通知UI。这段逻辑配合第3章的CONFLICT_IGNORE从服务端到本地做了两层防重。SQLiteDatabase db dbHelper.getReadableDatabase(); Cursor cursor db.rawQuery( SELECT id FROM message WHERE msg_id ?, new String[] { msgId }); boolean exists cursor.moveToFirst(); cursor.close(); if (!exists) { // 新消息走3.2的插入事务并通知UI }这个查询用msg_id的UNIQUE索引单行命中很快不需要担心性能。特别注意不要在这里做“先删旧再插新”那会把网络抖动变成消息丢失。5. 常见问题与避坑登录不上、消息丢失、数据库锁死的排查联调阶段出问题先看服务端日志再看协议字段最后才看数据库。这个顺序能省掉一大半的折腾时间。阶段先看什么常见结果连接阶段服务端有没有accept模拟器IP错、防火墙拦截登录阶段服务端是否打印login_result协议字段大小写不一致转发阶段目标用户是否在onlineClients登录未完成、映射键不对入库阶段SQLite里有没有新行database is locked、msgId冲突5.1 连接类问题现象客户端点登录后很快抛ConnectException提示连接127.0.0.1失败。原因一般不是代码而是地址选错模拟器里的localhost是模拟器自己不是电脑真机调试时又没有把IP改成电脑的局域网IP。解决模拟器访问宿主机用10.0.2.2真机用电脑在同一WiFi下的局域网IP联调前先在电脑上查一次本机IP再写死到配置类里。现象点击登录后界面卡住过几秒弹ANR。原因是new Socket()写在了Activity主线程。解决网络操作全部放进Thread回调里用Handler切回UI线程如果需要同步等登录结果用CountDownLatch等子线程返回不要在主线程sleep等待。5.2 消息类问题现象A发消息提示发送成功B没有任何反应服务端控制台也没有日志。这最常见是两个原因一是客户端登录时发的字段是userName服务端读的是username登录根本没注册进onlineClients二是chat消息里to字段格式不一致服务端get不到目标。解决协议字段列成表格统一小驼峰catch到JSONException时打印原始报文别只e.printStackTrace()就继续。现象网络不好时点一次发送对方收到两遍。原因是发送按钮点击后先落库失败后又重发同一个msgId被插了两次或者服务端重试转发。解决msgId唯一索引配合CONFLICT_IGNORE发送前先生成msgId发送结果只更新status不重复insert。5.3 数据与编译类问题现象运行一段时间后Logcat里出现SQLiteException: database is locked消息卡住不动。原因Socket收包线程、UI线程、离线拉取线程同时getWritableDatabase()写库SQLite的库级锁让写请求撞车。解决SQLiteOpenHelper做成单例所有写操作串行消息从收包到入库到刷新UI整个流程固定在同一个Handler线程里不要到处都能调db.insert。开发期还会遇到Android Studio资源重复错误build报duplicate resourceR类无法解析。原因大多是drawable目录下同时存在同名的ic_chat.png和IC_CHAT.png或者文件名带了中文。解决资源名统一小写字母加下划线改完Build、Clean Project实在不行再File菜单里Invalidate Caches并重启。6. 进阶实验报告与演示验收的六个检查点代码能跑不算交付实验报告和现场演示才是决定评价的部分。下面这套验收顺序我每次都会走一遍。检查点演示动作报告对应部分注册登录闭环A注册、B登录服务端能看到两条login日志系统设计、运行结果在线状态A登录后B的会话列表里A显示在线杀进程后30秒内变灰心跳与断线处理消息实时到达A发消息B界面不刷新直接出现消息推送流程离线消息B退出登录A发三条B重新登录后三条都出现且不重复离线消息存储本地持久化杀掉A进程重启聊天记录还在SQLite设计、数据字典心跳重连关掉WiFi再打开客户端自动重连成功连接管理聊天列表刷新时用局部更新代替整表刷新。消息多了以后notifyDataSetChanged会让整个RecyclerView闪烁输入框还可能被顶走。增量插入才是聊天界面的正确姿势chatAdapter.notifyItemInserted(chatList.size() - 1); recyclerView.scrollToPosition(chatList.size() - 1);这段代码的逻辑是告诉RecyclerView只在尾部插了一行然后滚到底部。要注意chatList和Adapter内部持有的必须是同一个List对象否则角标对不上。验证持久化时可以进模拟器直接查库adb shell run-as com.example.qqclient sqlite3 /data/data/com.example.qqclient/databases/im.db select msg_id, from_id, content, status from message order by timestamp desc limit 5;run-as只在debug包可用生产签名下会报错模拟器没有sqlite3命令时用Android Studio自带的Database Inspector打开同一个数据库文件效果一样。我最早做这类项目时喜欢先把聊天界面画得和QQ一模一样再回头写Socket结果协议一改、界面全崩联调时间全花在来回改字段上。后来改成先通链路、再过六项验收、最后补报告进度反而快得多。希望帮到你。本文还有配套的精品资源点击获取