简介:本资源是一套基于Socket通信实现的Android端仿微信即时通讯软件完整源码,面向Android开发初学者与中级工程师,适用于网络编程、客户端-服务器架构设计及IM应用开发学习场景。压缩包共797个文件,包含76个Java核心逻辑代码、78个XML布局与配置文件、368张UI资源PNG图、133个编译后Class字节码,以及服务端相关jar包、SQL建表脚本、APK安装包和关键技术文档(如消息类型定义、组件缩写说明、Bug历史记录等),整体体积10.64MB,结构完整且模块划分清晰。已有288人下载学习,可直接运行调试客户端与服务端,深入理解长连接管理、消息收发流程、UI交互逻辑与跨平台通信协议设计,是掌握Android网络应用开发落地实践的典型参考案例。
1. 这不是“仿微信”App,而是一套可调试、可拆解、可进阶的 Android 端 Socket 实时通信最小闭环系统
很多人下载这个名为“Android仿微信聊天软件,Socket实现源码,包含服务端.zip”的压缩包后,第一反应是:能直接跑起来发消息吗?结果发现启动 App 后连接失败、服务端报bind: address already in use、Android 端抛SecurityException: Permission denied,甚至 logcat 里只有一行E/SocketClient: java.net.ConnectException: failed to connect to /192.168.1.100 (port 8080) from /:: (port 43752): connect failed: ECONNREFUSED。问题不在“像不像微信”,而在于它本质是一套面向学习者与初阶开发者设计的、聚焦于 TCP 层通信控制权的端到端验证型工程——UI 是简化版的(无朋友圈、无支付、无语音转文字),但 Socket 连接管理、心跳保活、消息编解码、服务端线程模型、Android 网络权限与后台限制适配,全部显式暴露、无封装黑盒。适合 Android 开发者补全网络通信底层认知,也适合 Java 后端工程师快速搭建一个可控的移动端测试对端。它不解决高并发或消息持久化,但能让你亲手看到字节流如何从EditText经OutputStream发出,又如何被ServerSocket.accept()接收并原样回显。
2. 用 Java Socket 在 Ubuntu 24.04 上跑通服务端:从编译、端口绑定到多客户端隔离
服务端代码通常位于server/目录下,主类为ChatServer.java,采用最基础的ServerSocket+Thread模型。这不是 Spring Boot 或 Netty,而是刻意回归 JDK 原生 API 的教学选择——便于理解阻塞 I/O 的生命周期和资源边界。
2.1 编译与运行服务端(Ubuntu 24.04 环境实测)
确保已安装 OpenJDK 17(apt install openjdk-17-jdk),进入服务端目录后执行:
javac -encoding UTF-8 ChatServer.java java ChatServer 8080提示:端口号
8080可任意修改,但需与 Android 客户端配置一致;若提示Error: listen tcp 127.0.0.1:11434: bind: only one usage of each socket address,说明该端口已被占用,可用sudo lsof -i :8080查看进程并kill -9 <PID>释放。
参数说明:
8080:监听端口,必须为非特权端口(1024–65535);-encoding UTF-8:强制源码编码,避免中文消息乱码;- 无
-jar因为这是裸.class文件,非打包 Jar。
服务端启动后会打印:
[INFO] ChatServer started on port 8080 [INFO] Waiting for client connection...此时服务端处于accept()阻塞状态,等待 TCP 握手请求。
2.2 客户端连接逻辑与服务端响应机制
服务端核心逻辑在ChatServer.java的while (true)循环中:
Socket clientSocket = serverSocket.accept(); // 阻塞等待连接 new ClientHandler(clientSocket).start(); // 每个连接启一个新线程ClientHandler类继承Thread,重写run()方法,关键流程如下:
public void run() { try ( BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream(), "UTF-8")); PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true, StandardCharsets.UTF_8) ) { String inputLine; while ((inputLine = in.readLine()) != null) { // 读取客户端发来的文本 System.out.println("[RECV] " + inputLine); out.println("Echo: " + inputLine); // 原样回显(即“服务端响应”) } } catch (IOException e) { System.err.println("[ERROR] Client disconnected: " + e.getMessage()); } finally { try { clientSocket.close(); } catch (IOException ignored) {} } }关键点解析:
BufferedReader.readLine()按\n分割消息,要求 Android 客户端发送时必须以\n结尾(常见坑:忘记加writer.write(message + "\n"); writer.flush(););PrintWriter构造函数第三个参数true表示自动 flush,否则需手动调用flush();StandardCharsets.UTF_8显式指定字符集,避免 Linux 系统默认ISO-8859-1导致中文显示为??;finally中关闭clientSocket是必须操作,否则连接句柄泄漏,达到系统上限后accept()报Too many open files。
2.3 多客户端并发验证:用 telnet 或 nc 快速测试
无需启动 Android App,先用系统工具验证服务端是否真正就绪:
# 终端1:启动服务端(保持运行) java ChatServer 8080 # 终端2:连接并发送消息 telnet 127.0.0.1 8080 # 输入:Hello Android\n(按回车) # 服务端控制台应输出 [RECV] Hello Android,且 telnet 窗口收到 Echo: Hello Android # 终端3:再开一个 telnet 连接,验证并发性 telnet 127.0.0.1 8080 # 输入:Test from terminal 2\n → 服务端分别打印两行 [RECV],两个 telnet 窗口各自收到回显注意:Ubuntu 24.04 默认未安装
telnet,执行sudo apt install telnet即可;若偏好nc(netcat),命令为nc 127.0.0.1 8080,输入后需按Ctrl+D发送 EOF 触发readLine()返回(因nc不自动加\n)。
| 测试项 | 预期现象 | 失败原因定位 |
|---|---|---|
telnet 127.0.0.1 8080连接超时 | 服务端未运行或端口被占 | sudo lsof -i :8080+ps aux | grep java |
| 连接成功但无回显 | 客户端未发送\n或服务端未flush() | 检查PrintWriter构造参数、客户端write()后是否flush() |
| 多个 telnet 连接后新连接卡住 | ClientHandler线程未正确close()导致文件描述符耗尽 | ulimit -n查当前上限,lsof -p <pid> | wc -l统计打开数 |
3. Android 客户端 Socket 连接实战:适配 Android 10+ 网络限制与主线程策略
Android 客户端代码集中在app/src/main/java/com/example/chat/下,核心类为ChatClient.java或SocketManager.java。它不是用 Retrofit 或 OkHttp,而是直接new Socket(host, port)—— 这正是理解底层通信的关键切口。
3.1 权限声明与 AndroidManifest.xml 必配项
在AndroidManifest.xml中,以下三处缺一不可:
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <!-- Android 9+ 强制明文流量支持 --> <application android:usesCleartextTraffic="true" ... >提示:
android:usesCleartextTraffic="true"是关键。Android 9(API 28)起默认禁止 HTTP(非 HTTPS)明文流量,而本项目使用纯 TCP Socket(非 HTTP),但系统仍将其归类为 cleartext。若省略此属性,Socket构造会静默失败,logcat 仅提示java.net.SocketException: socket failed: EACCES (Permission denied),实际是网络策略拦截而非权限缺失。
3.2 主线程 vs 子线程:为什么不能在 onCreate() 里 new Socket()
Android 严格禁止在主线程(UI 线程)执行网络 I/O 操作,否则抛NetworkOnMainThreadException。正确做法是将 Socket 创建与读写封装进Thread或AsyncTask(已废弃,推荐Thread或ExecutorService):
// ✅ 正确:在子线程中建立连接 new Thread(() -> { try { socket = new Socket("192.168.1.100", 8080); // 注意:填你 Ubuntu 的局域网 IP,非 127.0.0.1 writer = new PrintWriter(socket.getOutputStream(), true, StandardCharsets.UTF_8); reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8")); // 启动接收循环 receiveMessages(); } catch (IOException e) { runOnUiThread(() -> Toast.makeText(this, "连接失败:" + e.getMessage(), Toast.LENGTH_SHORT).show()); } }).start();关键参数说明:
"192.168.1.100":必须是 Ubuntu 主机在局域网中的真实 IP(ip a \| grep "inet "查看),绝不能填127.0.0.1(那是 Android 设备自身回环);StandardCharsets.UTF_8:与服务端严格一致,否则中文乱码;runOnUiThread(...):异常信息必须切回主线程弹 Toast,否则崩溃。
3.3 消息收发与 UI 更新的线程安全处理
接收消息需持续轮询,典型实现:
private void receiveMessages() { try { String msg; while ((msg = reader.readLine()) != null) { // 阻塞直到收到 \n final String finalMsg = msg; runOnUiThread(() -> { // 更新 RecyclerView 或 TextView messageList.add(finalMsg); adapter.notifyItemInserted(messageList.size() - 1); }); } } catch (IOException e) { // 连接断开,清理资源 closeSocket(); } }为什么reader.readLine()必须配\n?
readLine()内部以\n、\r\n或\r为分隔符,若客户端发送"Hi"无换行,则该方法永远阻塞;- Android 端发送时务必:
writer.write("Hi from Android\n"); // ✅ 加 \n writer.flush(); // ✅ 强制发送缓冲区
| Android 版本 | 对 Socket 的特殊限制 | 应对措施 |
|---|---|---|
| Android 10(API 29)+ | 后台应用无法开启网络连接 | 确保 App 在前台运行测试;若需后台收消息,必须申请FOREGROUND_SERVICE并启动前台 Service |
| Android 12(API 31)+ | Socket构造可能触发SecurityException | 检查android:exported="true"是否误设在无关组件上,且usesCleartextTraffic已启用 |
| 所有版本 | StrictMode默认开启 | 开发时可在Application.onCreate()中临时禁用(仅调试):StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder().permitAll().build()); |
4. 消息协议与心跳保活:让 Socket 连接在 Wi-Fi 切换、锁屏后依然存活
原始源码往往只实现“发-收-回显”,但真实聊天场景需解决两个核心问题:消息粘包/半包和移动网络下的连接空闲断连。本项目虽未内置完整解决方案,但提供了可扩展的钩子。
4.1 自定义消息头:解决粘包问题(附可抄作业的 ProtocolHeader 类)
TCP 是字节流协议,readLine()依赖\n可靠,但若消息含换行符(如用户发送“你好\n今天好吗?”),就会被错误切分。工业级做法是添加固定长度消息头,例如 4 字节表示后续内容长度:
// Android 客户端发送(Java) public void sendMessage(String content) throws IOException { byte[] data = content.getBytes(StandardCharsets.UTF_8); int len = data.length; // 写入 4 字节长度头(大端序) outputStream.write((len >> 24) & 0xFF); outputStream.write((len >> 16) & 0xFF); outputStream.write((len >> 8) & 0xFF); outputStream.write(len & 0xFF); // 写入实际数据 outputStream.write(data); outputStream.flush(); }服务端对应读取:
// ChatServer.java 中 ClientHandler.run() byte[] header = new byte[4]; int read = 0; while (read < 4) { int r = inputStream.read(header, read, 4 - read); if (r == -1) throw new IOException("Connection closed"); read += r; } int length = ((header[0] & 0xFF) << 24) | ((header[1] & 0xFF) << 16) | ((header[2] & 0xFF) << 8) | (header[3] & 0xFF); byte[] payload = new byte[length]; read = 0; while (read < length) { int r = inputStream.read(payload, read, length - read); if (r == -1) throw new IOException("Connection closed"); read += r; } String message = new String(payload, StandardCharsets.UTF_8);提示:此方案完全规避
\n依赖,支持任意二进制内容(图片 Base64、JSON 等)。长度头用大端序(Network Byte Order)是跨平台通用约定。
4.2 心跳机制:防止 NAT 超时与系统休眠断连
Android 设备锁屏后,Wi-Fi 可能进入节能模式,NAT 路由器也会在 300 秒无数据时回收连接。解决方案是定时发送轻量心跳包:
// Android 端:每 30 秒发一次心跳 private ScheduledExecutorService heartbeatScheduler; private void startHeartbeat() { heartbeatScheduler = Executors.newSingleThreadScheduledExecutor(); heartbeatScheduler.scheduleAtFixedRate(() -> { if (socket != null && socket.isConnected() && !socket.isClosed()) { try { writer.write("HEARTBEAT\n"); writer.flush(); } catch (Exception e) { // 心跳失败,主动断开重连 closeSocket(); connectToServer(); } } }, 0, 30, TimeUnit.SECONDS); }服务端无需额外处理,只需确保readLine()能接收并忽略"HEARTBEAT"字符串即可。重点在于客户端主动维持连接活跃度,而非等待服务端探测。
| 场景 | 现象 | 心跳作用 |
|---|---|---|
| 用户锁屏 5 分钟 | Android 断开 Socket 连接 | 心跳包阻止系统休眠断连 |
| 手机切换 Wi-Fi/4G | IP 变更导致服务端 socket 失效 | 心跳失败后触发重连逻辑 |
| 路由器 NAT 超时(默认 5 分钟) | 服务端readLine()阻塞不返回 | 心跳包刷新 NAT 映射表 |
5. 调试与排错:从 logcat 抓关键线索到服务端日志分级
当 Android 端显示“连接失败”却不知原因时,不要盲目改代码,按以下顺序逐层验证。
5.1 Android 端 logcat 必查关键词与过滤命令
在 Android Studio Terminal 或 ADB shell 中执行:
# 过滤本 App 的所有网络相关日志(含系统拦截) adb logcat | grep -E "(Socket|connect|exception|permission|cleartext)" # 精准定位你的包名(如 com.example.chat) adb logcat -s "SocketClient" "ChatActivity" "ChatServer" # 实时查看异常堆栈(最有效) adb logcat *:S SocketClient:I ChatClient:I System.err:I常见有效日志片段:
W/System.err: java.net.ConnectException: failed to connect to /192.168.1.100 (port 8080) ...→ 检查 IP 和端口;E/AndroidRuntime: FATAL EXCEPTION: main Process: com.example.chat, PID: 12345 android.os.NetworkOnMainThreadException→ 网络操作未放子线程;W/Settings: Setting android_id has moved from android.provider.Settings.System to android.provider.Settings.Global.→ 无关警告,忽略;I/Choreographer: Skipped 35 frames!→ UI 卡顿,但非网络问题。
5.2 服务端日志增强:添加时间戳与客户端标识
原始System.out.println()不够,建议升级为带客户端 IP 的日志:
// ClientHandler 构造函数中记录 private final String clientAddress; public ClientHandler(Socket socket) { this.socket = socket; this.clientAddress = socket.getRemoteSocketAddress().toString(); // e.g. /192.168.1.101:54321 } // 日志替换为 System.out.printf("[%s][%s] RECV: %s%n", new SimpleDateFormat("HH:mm:ss").format(new Date()), clientAddress, inputLine );输出效果:
[14:22:35][/192.168.1.101:54321] RECV: Hello from Pixel 6 [14:22:37][/192.168.1.102:49210] RECV: Hi from Mi 13这样可快速确认:
- 是否真有设备连接(排除 Android 端根本没发请求);
- 多个设备是否共用同一 IP(判断是否在同一个局域网);
- 某个设备连接后立即断开(看日志是否有
Client disconnected)。
5.3 三步定位“连接被拒绝”(Connection refused)
这是最高频错误,按顺序排查:
服务端是否运行?
ps aux | grep "java.*ChatServer"→ 无输出则服务端未启动;端口是否监听?
sudo ss -tuln | grep ":8080"→ 应显示LISTEN状态;若无,检查java ChatServer 8080是否执行成功;防火墙是否放行?
Ubuntu 24.04 默认启用ufw:sudo ufw status verbose→ 若为Status: active,则执行sudo ufw allow 8080;
若用iptables,则sudo iptables -L -n | grep 8080,无规则则sudo iptables -A INPUT -p tcp --dport 8080 -j ACCEPT。
注意:
Connection refused意味着 TCP SYN 包到达目标主机,但该主机上无进程监听对应端口——这与No route to host(网络不通)或Timeout(防火墙丢包)有本质区别。抓住这点,就能跳过 80% 的无效排查。
最后,把ChatServer.java中的System.out.println全部重定向到文件,比控制台更可靠:
// 在 main 方法开头添加 System.setOut(new PrintStream(new FileOutputStream("server.log", true), true, StandardCharsets.UTF_8)); System.setErr(System.out);这样即使关闭终端,日志仍持续写入server.log,方便复盘。
本文还有配套的精品资源,点击获取