目录
- 核心概念
- Android 平台的特殊性
- 实现步骤
- 第 1 步:添加网络权限
- 第 2 步:创建后台线程(避免 ANR)
- 第 3 步:实现客户端 Socket 连接
- 第 4 步:实现数据收发(输入/输出流)
- 第 5 步:处理连接生命周期(连接、断开、异常)
- 完整代码示例
- 进阶主题
- 使用
AsyncTask(旧方法,但有助于理解) - 使用现代协程
- 使用第三方库(如 Okio)
- 使用
- 常见问题与最佳实践
核心概念
- Socket (套接字):网络通信的端点,你可以把它想象成一个电话,客户端(你的 App)通过这个电话号码(IP 地址和端口号)拨打服务器(接听方)的电话。
- IP 地址:服务器的网络地址,
168.1.100(局域网)或8.8.8(公网)。 - 端口号:服务器上应用程序的标识,范围是 0-65535,Web 服务器常用 80 端口,HTTPS 用 443 端口,你需要使用服务器应用程序监听的端口号。
- 流:数据在 Socket 中是以字节流的形式传输的,你需要将你的数据(如字符串、图片)转换为字节数组,通过输出流发送;然后通过输入流接收字节数组,再转换回原始数据。
Android 平台的特殊性
这是 Android 开发中至关重要的一点,也是新手最容易出错的地方。

-
网络访问在主线程(UI 线程)中被禁止:
- 原因:网络请求是不可预测的,可能会因为网络延迟、服务器无响应而长时间阻塞,如果在主线程中执行,会导致用户界面卡死,系统会弹出 "Application Not Responding (ANR)" 对话框,甚至可能杀死你的应用。
- 解决方案:所有的网络操作(Socket 连接、读写数据)都必须在后台线程中执行。
-
网络权限:
你的应用必须声明它需要访问网络的权限,否则无法进行任何网络通信。
实现步骤
我们将创建一个简单的客户端,它连接到服务器,发送一条消息,并接收服务器的回复。

第 1 步:添加网络权限
在 AndroidManifest.xml 文件中,在 <application> 标签之前添加以下权限:
<uses-permission android:name="android.permission.INTERNET" />
第 2 步:创建后台线程
我们不能在 onCreate 或按钮点击事件等主线程方法中直接写网络代码,我们将使用一个 Thread 配合 Handler 的方式,这是最经典和易于理解的方式。
- Thread:负责执行耗时任务(网络连接)。
- Handler:负责在主线程中更新 UI(显示连接状态、消息内容)。
第 3 步:实现客户端 Socket 连接
在后台线程中,创建 Socket 对象,并提供服务器的 IP 地址和端口号。new Socket() 的过程会阻塞线程,直到连接成功或抛出异常。
Socket socket = new Socket("服务器IP地址", 端口号);
第 4 步:实现数据收发
一旦连接成功,就可以通过 socket.getInputStream() 和 socket.getOutputStream() 获取输入流和输出流。

- 发送数据:将字符串转换为字节数组,然后用
OutputStream.write()发送。PrintWriter out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream())); out.println("你好,服务器!"); // 发送字符串 out.flush(); // 重要!确保数据被立即发送 - 接收数据:使用
InputStream.read()读取服务器返回的字节数组,然后转换成字符串。BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream())); String response = in.readLine(); // 阻塞,直到读取到一行
第 5 步:处理连接生命周期
- 连接成功:通过
Handler发送消息到主线程,更新 UI 显示“连接成功”。 - 连接失败/异常:使用
try-catch捕获IOException,并通过Handler通知主线程显示错误信息。 - 断开连接:在
finally块中,务必关闭Socket、InputStream和OutputStream,以释放系统资源。
完整代码示例
这是一个完整的 Activity 示例,它使用一个按钮来触发连接,并用 TextView 显示结果。
activity_main.xml (布局文件)
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:padding="16dp"
tools:context=".MainActivity">
<Button
android:id="@+id/btn_connect"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="连接服务器" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_marginTop="20dp">
<TextView
android:id="@+id/tv_log"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="monospace"
android:text="等待连接..." />
</ScrollView>
</LinearLayout>
MainActivity.java
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import android.widget.Button;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;
public class MainActivity extends AppCompatActivity {
private static final String TAG = "SocketClient";
private static final String SERVER_IP = "你的服务器IP"; // 替换为你的服务器IP
private static final int SERVER_PORT = 8888; // 替换为你的服务器端口
private TextView tvLog;
private Button btnConnect;
// Handler用于在主线程更新UI
private Handler mainHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
String log = (String) msg.obj;
tvLog.append(log + "\n");
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tvLog = findViewById(R.id.tv_log);
btnConnect = findViewById(R.id.btn_connect);
btnConnect.setOnClickListener(v -> {
// 禁用按钮,防止重复点击
btnConnect.setEnabled(false);
// 启动一个新线程来执行网络任务
new Thread(new SocketTask()).start();
});
}
// 定义一个HandlerMessage常量,方便识别
private static final int MSG_UPDATE_LOG = 1;
private class SocketTask implements Runnable {
@Override
public void run() {
Socket socket = null;
try {
// 1. 创建Socket并连接服务器
sendLogToUI("正在连接服务器 " + SERVER_IP + ":" + SERVER_PORT + "...");
socket = new Socket(SERVER_IP, SERVER_PORT);
sendLogToUI("连接成功!");
// 2. 获取输入输出流
PrintWriter out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream()), true);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
// 3. 发送数据
String messageToSend = "Hello, Android Client!";
sendLogToUI("发送消息: " + messageToSend);
out.println(messageToSend);
// 4. 接收数据
String response = in.readLine();
if (response != null) {
sendLogToUI("服务器回复: " + response);
}
} catch (UnknownHostException e) {
sendLogToUI("错误: 未知的主机 " + SERVER_IP);
e.printStackTrace();
} catch (IOException e) {
sendLogToUI("错误: 连接或读写失败");
e.printStackTrace();
} finally {
// 5. 关闭Socket和流
if (socket 