凌峰创科服务平台

Android与服务器socket

目录

  1. 核心概念
    • Socket 是什么?
    • TCP vs. UDP:如何选择?
    • 通信模型:客户端/服务器
  2. 服务器端实现 (Java)
    • 使用 ServerSocket 监听端口
    • 使用多线程处理多个客户端连接
    • 数据的发送与接收
  3. Android 客户端实现 (Kotlin/Java)
    • 使用 Socket 连接服务器
    • 在后台线程中处理网络操作(AsyncTask, Thread + Handler, Kotlin Coroutines
    • 数据的发送与接收
  4. 数据格式化
    • 为什么不能直接发送字符串?
    • 解决方案:JSON + 长度前缀
  5. 完整项目示例
    • 功能描述
    • 服务器端代码
    • Android 客户端代码
  6. 关键注意事项与最佳实践
    • 网络权限
    • 线程管理
    • 异常处理
    • 心跳机制
    • 断线重连

核心概念

Socket 是什么?

可以把 Socket 想象成一个“电话插座”,服务器相当于总机,客户端相当于分机,客户端要打电话,必须知道总机的IP地址分机号(端口号),一旦连接建立,双方就可以通过这个“电话线”(Socket)进行双向通话(数据传输)。

Android与服务器socket-图1
(图片来源网络,侵删)

TCP vs. UDP:如何选择?

特性 TCP (Transmission Control Protocol) UDP (User Datagram Protocol)
连接 面向连接(三次握手) 无连接,直接发送
可靠性 可靠,通过确认、重传、排序等机制确保数据不丢失、不重复、按序到达。 不可靠,不保证数据一定能到达,不保证顺序,可能丢失或重复。
速度 较慢,因为需要建立连接和维护状态。 非常快,没有连接和可靠性开销。
资源消耗 较高,需要维护连接状态。 较低。
适用场景 要求高可靠性的应用,如文件传输、网页浏览、数据库连接、即时通讯 对实时性要求高、能容忍少量丢包的应用,如视频会议、在线游戏、DNS查询

对于绝大多数 Android Socket 应用(如聊天、数据同步),我们选择 TCP,因为它保证了数据的完整性。

通信模型:客户端/服务器

这是一个经典的请求-响应模型。

  • 服务器
    1. 在一个固定端口上启动,等待客户端连接。
    2. 监听到客户端连接请求后,接受连接,并创建一个新的 Socket 与该客户端通信。
    3. 为了处理多个客户端,服务器必须为每个连接创建一个新的线程。
  • 客户端
    1. 知道服务器的 IP 地址端口号
    2. 主动向服务器发起连接请求。
    3. 连接成功后,就可以通过 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 为例进行讲解。

Android与服务器socket-图2
(图片来源网络,侵删)

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)作为消息的结束标志,如果服务器发送的数据本身包含换行符,或者客户端没有按行发送,就会导致消息解析错误。

最佳实践:自定义协议

Android与服务器socket-图3
(图片来源网络,侵删)

我们可以在每个消息前加上消息的长度,这样接收方就可以精确地读取指定长度的数据。

协议格式:[消息长度(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 服务器发送消息,并收到服务器的回显。

功能:

  1. Android 启动时自动连接服务器。
  2. 在输入框输入文字,点击发送,服务器会回显相同内容。
  3. 实时显示服务器返回的消息和连接状态。

服务器代码 (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): 参考上一节使用 DataInputStreamDataOutputStream 的完整代码,布局文件 activity_main.xml 可以包含 TextView, EditText, Button


关键注意事项与最佳实践

  1. 网络权限:必须声明 INTERNET 权限。
  2. 线程管理:网络是耗时操作,绝不能在主线程(UI线程)中进行,推荐使用 Kotlin CoroutinesRxJava 或传统的 AsyncTask/Handler
  3. 异常处理:网络非常脆弱,必须妥善处理各种异常,如 SocketTimeoutException (连接超时)、UnknownHostException (主机无法找到)、SocketException (连接断开) 等。
  4. 心跳机制:对于需要长时间保持连接的应用(如聊天、物联网设备),客户端需要定期向服务器发送一个简单的“心跳”包(如 "ping"),服务器收到后回复 "pong",如果在一定时间内没有收到心跳,就认为连接已断开,可以触发重连逻辑。
  5. 断线重连:当检测到连接断开时,不要立即放弃,可以设计一个自动重连机制,比如每隔几秒尝试重新连接,并设置一个最大重试次数,避免无限重连消耗资源。
  6. 资源释放:当 Activity 销毁或不再需要网络连接时,一定要关闭 Socket 和相关的输入/输出流,并取消后台任务,防止内存泄漏。
  7. 超时设置:为 Socket 连接和读取设置超时时间,避免程序无限期等待。
    // 在客户端连接时设置
    socket.connect(new InetSocketAddress(host, port), 5000); // 5秒连接超时
    socket.setSoTimeout(10000); // 10秒读取超时

希望这份详细的指南能帮助你顺利实现 Android 与服务器的 Socket 通信!

分享:
扫描分享到社交APP
上一篇
下一篇