目录
- 核心概念
- Socket 是什么?
- TCP vs. UDP:如何选择?
- 通信模型:客户端/服务器
- 服务器端实现 (Java)
- 使用
ServerSocket监听端口 - 使用多线程处理多个客户端连接
- 数据的发送与接收
- 使用
- Android 客户端实现 (Kotlin/Java)
- 使用
Socket连接服务器 - 在后台线程中处理网络操作(
AsyncTask,Thread+Handler,Kotlin Coroutines) - 数据的发送与接收
- 使用
- 数据格式化
- 为什么不能直接发送字符串?
- 解决方案:JSON + 长度前缀
- 完整项目示例
- 功能描述
- 服务器端代码
- Android 客户端代码
- 关键注意事项与最佳实践
- 网络权限
- 线程管理
- 异常处理
- 心跳机制
- 断线重连
核心概念
Socket 是什么?
可以把 Socket 想象成一个“电话插座”,服务器相当于总机,客户端相当于分机,客户端要打电话,必须知道总机的IP地址和分机号(端口号),一旦连接建立,双方就可以通过这个“电话线”(Socket)进行双向通话(数据传输)。

TCP vs. UDP:如何选择?
| 特性 | TCP (Transmission Control Protocol) | UDP (User Datagram Protocol) |
|---|---|---|
| 连接 | 面向连接(三次握手) | 无连接,直接发送 |
| 可靠性 | 可靠,通过确认、重传、排序等机制确保数据不丢失、不重复、按序到达。 | 不可靠,不保证数据一定能到达,不保证顺序,可能丢失或重复。 |
| 速度 | 较慢,因为需要建立连接和维护状态。 | 非常快,没有连接和可靠性开销。 |
| 资源消耗 | 较高,需要维护连接状态。 | 较低。 |
| 适用场景 | 要求高可靠性的应用,如文件传输、网页浏览、数据库连接、即时通讯 | 对实时性要求高、能容忍少量丢包的应用,如视频会议、在线游戏、DNS查询 |
对于绝大多数 Android Socket 应用(如聊天、数据同步),我们选择 TCP,因为它保证了数据的完整性。
通信模型:客户端/服务器
这是一个经典的请求-响应模型。
- 服务器:
- 在一个固定端口上启动,等待客户端连接。
- 监听到客户端连接请求后,接受连接,并创建一个新的
Socket与该客户端通信。 - 为了处理多个客户端,服务器必须为每个连接创建一个新的线程。
- 客户端:
- 知道服务器的 IP 地址 和 端口号。
- 主动向服务器发起连接请求。
- 连接成功后,就可以通过
Socket的输入/输出流与服务器收发数据。
服务器端实现 (Java)
服务器端通常是一个独立的 Java 应用程序,可以在电脑上运行。
import java.io.*;
import java.net.*;
public class SimpleSocketServer {
public static void main(String[] args) {
int port = 8080; // 服务器监听端口
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("服务器已启动,监听端口: " + port);
// 循环等待客户端连接
while (true) {
// accept() 是一个阻塞方法,直到有客户端连接进来
Socket clientSocket = serverSocket.accept();
System.out.println("客户端已连接: " + clientSocket.getInetAddress().getHostAddress());
// 为每个客户端连接创建一个新线程来处理
new Thread(new ClientHandler(clientSocket)).start();
}
} catch (IOException e) {
System.err.println("服务器启动或运行出错: " + e.getMessage());
}
}
}
// 客户端处理线程
class ClientHandler implements Runnable {
private final Socket clientSocket;
public ClientHandler(Socket socket) {
this.clientSocket = socket;
}
@Override
public void run() {
try (
// 从Socket中获取输入流和输出流
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)
) {
String inputLine;
// 循环读取客户端发送的数据
while ((inputLine = in.readLine()) != null) {
System.out.println("收到客户端消息: " + inputLine);
// 处理数据并返回响应
String response = "服务器已收到: " + inputLine;
out.println(response);
}
} catch (IOException e) {
System.err.println("处理客户端时出错: " + e.getMessage());
} finally {
try {
clientSocket.close();
} catch (IOException e) {
System.err.println("关闭客户端Socket出错: " + e.getMessage());
}
System.out.println("客户端连接已关闭");
}
}
}
Android 客户端实现
在 Android 中,所有网络操作都必须在后台线程中执行,否则会抛出 NetworkOnMainThreadException 异常,我们以现代的 Kotlin Coroutines 为例进行讲解。

1 添加网络权限
在 app/src/main/AndroidManifest.xml 文件中添加:
<uses-permission android:name="android.permission.INTERNET" />
<!-- 如果需要访问网络,还需要在 <application> 标签中添加 -->
<application
...
android:usesCleartextTraffic="true"> <!-- 允许HTTP明文流量,对于Socket开发通常需要 -->
</application>
2 使用 Kotlin Coroutines 实现客户端
import android.os.Bundle
import android.widget.Button
import android.widget.EditText
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import kotlinx.coroutines.*
import java.io.BufferedReader
import java.io.InputStreamReader
import java.io.PrintWriter
import java.net.Socket
import java.net.SocketException
class MainActivity : AppCompatActivity() {
private lateinit var socket: Socket
private lateinit var writer: PrintWriter
private lateinit var reader: BufferedReader
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
private val tvStatus: TextView by lazy { findViewById(R.id.tv_status) }
private val etMessage: EditText by lazy { findViewById(R.id.et_message) }
private val btnSend: Button by lazy { findViewById(R.id.btn_send) }
private val tvResponse: TextView by lazy { findViewById(R.id.tv_response) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
btnSend.setOnClickListener {
val message = etMessage.text.toString()
if (message.isNotEmpty()) {
// 在IO协程中发送消息
scope.launch(Dispatchers.IO) {
sendToServer(message)
}
}
}
// 启动时连接服务器
connectToServer()
}
private fun connectToServer() {
scope.launch(Dispatchers.IO) {
try {
// 服务器IP地址(请替换为你电脑的局域网IP)
val host = "192.168.1.100"
val port = 8080
tvStatus.postValue("正在连接服务器...")
socket = Socket(host, port)
writer = PrintWriter(socket.getOutputStream(), true)
reader = BufferedReader(InputStreamReader(socket.getInputStream()))
tvStatus.postValue("已连接到服务器: ${socket.inetAddress.hostAddress}")
// 启动一个单独的协程来持续监听服务器消息
launch(Dispatchers.IO) {
listenForMessages()
}
} catch (e: Exception) {
tvStatus.postValue("连接失败: ${e.message}")
e.printStackTrace()
}
}
}
private suspend fun sendToServer(message: String) {
try {
writer.println(message)
tvResponse.postValue("已发送: $message")
} catch (e: Exception) {
tvResponse.postValue("发送失败: ${e.message}")
}
}
private suspend fun listenForMessages() {
try {
var serverMessage: String?
while (reader.readLine().also { serverMessage = it } != null) {
// 将接收到的消息切换回主线程更新UI
tvResponse.postValue("服务器回复: $serverMessage")
}
} catch (e: SocketException) {
// 正常关闭连接会抛出此异常
tvResponse.postValue("连接已断开")
} catch (e: Exception) {
tvResponse.postValue("接收消息出错: ${e.message}")
}
}
override fun onDestroy() {
super.onDestroy()
// 在Activity销毁时,取消所有协程并关闭连接
scope.cancel()
try {
reader?.close()
writer?.close()
socket?.close()
} catch (e: IOException) {
e.printStackTrace()
}
}
}
注意: 168.1.100 是一个示例IP,你需要替换成你电脑在局域网内的实际IP地址,可以通过 ipconfig (Windows) 或 ifconfig (macOS/Linux) 命令查看。
数据格式化
直接使用 readLine() 和 println() 有一个问题:它依赖于换行符(\n)作为消息的结束标志,如果服务器发送的数据本身包含换行符,或者客户端没有按行发送,就会导致消息解析错误。
最佳实践:自定义协议

我们可以在每个消息前加上消息的长度,这样接收方就可以精确地读取指定长度的数据。
协议格式:[消息长度(4字节)][消息内容]
服务器端修改 (Java)
// 在 ClientHandler 的 run 方法中
try (DataInputStream in = new DataInputStream(clientSocket.getInputStream());
DataOutputStream out = new DataOutputStream(clientSocket.getOutputStream())) {
while (true) {
// 1. 读取消息长度 (4字节)
int length = in.readInt();
if (length <= 0) {
continue; // 忽略无效长度
}
// 2. 根据长度读取消息内容
byte[] messageBytes = new byte[length];
in.readFully(messageBytes); // 阻塞,直到读取满length个字节
String message = new String(messageBytes, StandardCharsets.UTF_8);
System.out.println("收到客户端消息: " + message);
// 3. 发送响应 (同样遵循协议)
String response = "Echo: " + message;
byte[] responseBytes = response.getBytes(StandardCharsets.UTF_8);
out.writeInt(responseBytes.length); // 先写长度
out.write(responseBytes); // 再写内容
}
} catch (IOException e) {
// ... 异常处理
}
Android 客户端修改 (Kotlin)
// 在 connectToServer 和 listenForMessages 中使用 DataInputStream/DataOutputStream
// 修改 connectToServer 中的流初始化
reader = BufferedReader(InputStreamReader(socket.inputStream)) // 保持用于调试或文本协议
// 用于正式协议的流
val dataInputStream = DataInputStream(socket.getInputStream())
val dataOutputStream = DataOutputStream(socket.getOutputStream())
// 修改 sendToServer 函数
private suspend fun sendToServer(message: String) {
try {
val messageBytes = message.toByteArray(Charsets.UTF_8)
dataOutputStream.writeInt(messageBytes.size)
dataOutputStream.write(messageBytes)
dataOutputStream.flush() // 确保数据被发送
tvResponse.postValue("已发送: $message")
} catch (e: Exception) {
tvResponse.postValue("发送失败: ${e.message}")
}
}
// 修改 listenForMessages 函数
private suspend fun listenForMessages() {
try {
while (true) {
val length = dataInputStream.readInt()
if (length <= 0) continue
val buffer = ByteArray(length)
dataInputStream.readFully(buffer)
val serverMessage = String(buffer, Charsets.UTF_8)
tvResponse.postValue("服务器回复: $serverMessage")
}
} catch (e: EOFException) {
// 对方关闭了连接
tvResponse.postValue("服务器已关闭连接")
} catch (e: SocketException) {
tvResponse.postValue("连接已断开")
} catch (e: Exception) {
tvResponse.postValue("接收消息出错: ${e.message}")
}
}
完整项目示例
这是一个简单的聊天应用,Android 客户端可以给 Java 服务器发送消息,并收到服务器的回显。
功能:
- Android 启动时自动连接服务器。
- 在输入框输入文字,点击发送,服务器会回显相同内容。
- 实时显示服务器返回的消息和连接状态。
服务器代码 (EchoServer.java):
import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
public class EchoServer {
public static void main(String[] args) {
int port = 8080;
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("Echo Server 启动,监听端口: " + port);
while (true) {
Socket clientSocket = serverSocket.accept();
System.out.println("客户端连接: " + clientSocket.getInetAddress());
new Thread(new ClientHandler(clientSocket)).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
class ClientHandler implements Runnable {
private final Socket clientSocket;
public ClientHandler(Socket socket) {
this.clientSocket = socket;
}
@Override
public void run() {
try (DataInputStream in = new DataInputStream(clientSocket.getInputStream());
DataOutputStream out = new DataOutputStream(clientSocket.getOutputStream())) {
System.out.println("客户端 " + clientSocket.getInetAddress() + " 已连接。");
while (true) {
int length = in.readInt();
if (length <= 0) continue;
byte[] messageBytes = new byte[length];
in.readFully(messageBytes);
String message = new String(messageBytes, StandardCharsets.UTF_8);
System.out.println("收到: " + message);
String response = "Echo: " + message;
byte[] responseBytes = response.getBytes(StandardCharsets.UTF_8);
out.writeInt(responseBytes.length);
out.write(responseBytes);
}
} catch (EOFException e) {
System.out.println("客户端 " + clientSocket.getInetAddress() + " 断开连接。");
} catch (IOException e) {
System.err.println("处理客户端时出错: " + e.getMessage());
} finally {
try {
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
Android 客户端 (MainActivity.kotlin):
参考上一节使用 DataInputStream 和 DataOutputStream 的完整代码,布局文件 activity_main.xml 可以包含 TextView, EditText, Button。
关键注意事项与最佳实践
- 网络权限:必须声明
INTERNET权限。 - 线程管理:网络是耗时操作,绝不能在主线程(UI线程)中进行,推荐使用
Kotlin Coroutines、RxJava或传统的AsyncTask/Handler。 - 异常处理:网络非常脆弱,必须妥善处理各种异常,如
SocketTimeoutException(连接超时)、UnknownHostException(主机无法找到)、SocketException(连接断开) 等。 - 心跳机制:对于需要长时间保持连接的应用(如聊天、物联网设备),客户端需要定期向服务器发送一个简单的“心跳”包(如 "ping"),服务器收到后回复 "pong",如果在一定时间内没有收到心跳,就认为连接已断开,可以触发重连逻辑。
- 断线重连:当检测到连接断开时,不要立即放弃,可以设计一个自动重连机制,比如每隔几秒尝试重新连接,并设置一个最大重试次数,避免无限重连消耗资源。
- 资源释放:当
Activity销毁或不再需要网络连接时,一定要关闭Socket和相关的输入/输出流,并取消后台任务,防止内存泄漏。 - 超时设置:为
Socket连接和读取设置超时时间,避免程序无限期等待。// 在客户端连接时设置 socket.connect(new InetSocketAddress(host, port), 5000); // 5秒连接超时 socket.setSoTimeout(10000); // 10秒读取超时
希望这份详细的指南能帮助你顺利实现 Android 与服务器的 Socket 通信!
