Khi phát triển ứng dụng Android yêu cầu giao tiếp mạng liên tục — ví dụ như truyền metadata bài hát từ trình phát nhạc đến máy chủ PC — việc đặt logic Socket trực tiếp trong Activity thường gây ra nhiều vấn đề: kết nối bị gián đoạn khi Activity chuyển sang trạng thái không hoạt động, khởi tạo lại socket trùng lặp, hoặc mất dữ liệu do vòng đời Activity không đồng bộ với luồng mạng.
Giải pháp bền vững là tách toàn bộ xử lý mạng ra khỏi giao diện người dùng bằng cách sử dụng Service, đặc biệt là ForegroundService (đối với Android 8.0+) nhằm đảm bảo hệ thống không tự động dừng tiến trình nền. Dưới đây là cách triển khai một dịch vụ socket server đơn giản nhưng đầy đủ tính năng: chấp nhận kết nối, nhận dữ liệu nhị phân, phát hiện ngắt kết nối và thông báo về UI qua broadcast.
1. Lớp SocketNetworkService
Dịch vụ này mở cổng 12589 để lắng nghe kết nối từ client (PC), sau đó khởi chạy luồng riêng biệt để đọc dữ liệu từ socket. Nó cũng đăng ký một BroadcastReceiver để phản hồi các lệnh điều khiển từ Activity — ví dụ như yêu cầu kết nối lại.
package com.example.audiocontrol;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.IBinder;
import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
public class SocketNetworkService extends Service {
private static final String TAG = "SocketNetworkService";
private static final int LISTEN_PORT = 12589;
private ServerSocket serverSocket;
private Socket activeClient;
private AcceptThread acceptThread;
private ReceiveThread receiveThread;
private final String CONTROL_ACTION = "com.example.audiocontrol.ACTION_CONTROL";
private final String DATA_BROADCAST = "com.example.audiocontrol.BROADCAST_DATA";
private boolean isRunning = false;
@Override
public void onCreate() {
super.onCreate();
Log.i(TAG, "Service created");
// Đăng ký receiver để nhận lệnh từ UI
IntentFilter filter = new IntentFilter(CONTROL_ACTION);
registerReceiver(controlReceiver, filter);
startListening();
}
private void startListening() {
if (acceptThread != null && acceptThread.isAlive()) {
acceptThread.interrupt();
}
acceptThread = new AcceptThread();
acceptThread.start();
}
private final BroadcastReceiver controlReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (CONTROL_ACTION.equals(intent.getAction())) {
String cmd = intent.getStringExtra("command");
if ("reconnect".equals(cmd)) {
Log.i(TAG, "Reconnection requested");
startListening();
}
}
}
};
private class AcceptThread extends Thread {
@Override
public void run() {
try {
serverSocket = new ServerSocket(LISTEN_PORT);
Log.i(TAG, "Server listening on port " + LISTEN_PORT);
while (!isInterrupted()) {
try {
activeClient = serverSocket.accept();
Log.i(TAG, "Client connected: " + activeClient.getInetAddress());
// Gửi địa chỉ IP client tới UI
sendBroadcast(DATA_BROADCAST, "client_ip", activeClient.getInetAddress().getHostAddress());
receiveThread = new ReceiveThread(activeClient);
receiveThread.start();
break; // Chỉ chấp nhận 1 kết nối tại thời điểm
} catch (IOException e) {
if (!isInterrupted()) {
Log.w(TAG, "Accept failed", e);
}
}
}
} catch (IOException e) {
Log.e(TAG, "Server socket init failed", e);
}
}
}
private class ReceiveThread extends Thread {
private final Socket socket;
private final InputStream inputStream;
ReceiveThread(Socket s) throws IOException {
this.socket = s;
this.inputStream = s.getInputStream();
}
@Override
public void run() {
byte[] buffer = new byte[2048];
try {
while (!socket.isClosed() && !Thread.currentThread().isInterrupted()) {
int bytesRead = inputStream.read(buffer);
if (bytesRead == -1) {
// Client đóng kết nối
handleDisconnect("Client closed connection");
return;
}
String message = new String(buffer, 0, bytesRead, StandardCharsets.UTF_8).trim();
if (!message.isEmpty()) {
sendBroadcast(DATA_BROADCAST, "received_data", message);
}
}
} catch (IOException e) {
if (!socket.isClosed()) {
handleDisconnect("IO Exception: " + e.getMessage());
}
}
}
}
private void handleDisconnect(String reason) {
Log.w(TAG, "Connection lost: " + reason);
sendBroadcast(DATA_BROADCAST, "disconnected", reason);
try {
if (activeClient != null && !activeClient.isClosed()) {
activeClient.close();
}
if (serverSocket != null && !serverSocket.isClosed()) {
serverSocket.close();
}
} catch (IOException ignored) {}
activeClient = null;
isRunning = false;
}
private void sendBroadcast(String action, String key, String value) {
Intent intent = new Intent(action);
intent.putExtra("type", key);
intent.putExtra("payload", value);
sendBroadcast(intent);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return START_STICKY;
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onDestroy() {
super.onDestroy();
Log.i(TAG, "Service destroyed");
if (acceptThread != null) acceptThread.interrupt();
if (receiveThread != null) receiveThread.interrupt();
unregisterReceiver(controlReceiver);
try {
if (serverSocket != null && !serverSocket.isClosed()) {
serverSocket.close();
}
} catch (IOException ignored) {}
}
}2. Tích hợp vào Activity
Để nhận thông báo từ service, mỗi Activity cần đăng ký BroadcastReceiver tương ứng:
// Trong Activity
private BroadcastReceiver dataReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String type = intent.getStringExtra("type");
String payload = intent.getStringExtra("payload");
switch (type) {
case "client_ip":
Log.d("UI", "Connected to: " + payload);
break;
case "received_data":
Log.d("UI", "Received: " + payload);
break;
case "disconnected":
Log.w("UI", "Connection dropped: " + payload);
break;
}
}
};
// Trong onCreate()
IntentFilter filter = new IntentFilter("com.example.audiocontrol.BROADCAST_DATA");
registerReceiver(dataReceiver, filter);
// Khởi động service
startService(new Intent(this, SocketNetworkService.class));3. Phát hiện ngắt kết nối đáng tin cậy
Android không tự động phát sinh exception khi client đóng TCP đột ngột. Giải pháp thực tế là kết hợp hai cơ chế:
- Đọc trả về -1: Khi
InputStream.read()trả về-1, nghĩa là client đã gọiclose()một cách rõ ràng. - Kiểm tra lỗi gửi dữ liệu khẩn cấp: Nếu đọc thành công nhưng sau đó
socket.sendUrgentData(0xFF)ném ngoại lệ, chứng tỏ kết nối đã bị đứt ở tầng mạng (ví dụ: client tắt mạng).
Cơ chế này được tích hợp sẵn trong lớp ReceiveThread ở trên — đảm bảo không bỏ sót bất kỳ trường hợp mất kết nối nào.
4. Lưu ý quan trọng
- Với Android 8.0+, bắt buộc phải gọi
startForeground()nếu muốn service chạy lâu dài — tránh bị hệ thống kill. - Nên sử dụng
HandlerThreadhoặcExecutorServicethay vìThreadthuần túy để quản lý luồng hiệu quả hơn. - Không lưu tham chiếu
Contexttừ Activity vào Service — dễ gây rò rỉ bộ nhớ. - Mã hóa dữ liệu đầu vào (UTF-8) và xử lý buffer đúng cách để tránh lỗi cắt xén chuỗi.