Socket cho phép giao tiếp hai chiều (full-duplex) qua TCP/IP giữa hai ứng dụng Android (hoặc Android với Server backend). Bài viết triển khai mẫu **Server** và **Client** riêng biệt chạy trên 2 thiết bị/emulator cùng WiFi LAN.
Kiến trúc
- Server App: Mở
ServerSocketlắng nghe port cố định (vd 8080), chấp nhận kết nối, đọc/ghi stream. - Client App: Kết nối đến IP:Port của Server qua
Socket, gửi/nhận tin nhắn. - Cả hai chạy network operation trên **background thread** (Thread/RxJava/Coroutine), cập nhật UI qua
runOnUiThread.
Quyền Manifest (Cả 2 app)
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <!-- Server cần thêm để lấy IP local --> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
PHẦN 1: SERVER APP
Layout Server (res/layout/activity_main.xml)
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="https://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp">
<TextView android:id="@+id/tvServerIp" android:layout_width="wrap_content" android:layout_height="wrap_content"
android:text="Server IP: ..." android:textAppearance="@style/TextAppearance.AppCompat.Medium" />
<TextView android:id="@+id/tvServerPort" android:layout_width="wrap_content" android:layout_height="wrap_content"
android:layout_below="@id/tvServerIp" android:text="Port: 8080" android:textAppearance="@style/TextAppearance.AppCompat.Medium" />
<TextView android:id="@+id/tvConnStatus" android:layout_width="match_parent" android:layout_height="wrap_content"
android:layout_below="@id/tvServerPort" android:text="Chờ kết nối..." android:textAppearance="@style/TextAppearance.AppCompat.Medium"
android:layout_marginTop="8dp" />
<TextView android:id="@+id/tvMessages" android:layout_width="match_parent" android:layout_height="0dp"
android:layout_above="@id/etMsg" android:layout_below="@id/tvConnStatus"
android:inputType="textMultiLine" android:background="@android:drawable/editbox_background"
android:padding="8dp" android:layout_marginTop="8dp" android:scrollbars="vertical" />
<EditText android:id="@+id/etMsg" android:layout_width="match_parent" android:layout_height="wrap_content"
android:layout_above="@id/btnSend" android:layout_alignParentBottom="false"
android:hint="Nhập tin nhắn" android:inputType="text" android:layout_marginTop="8dp" />
<Button android:id="@+id/btnSend" android:layout_width="match_parent" android:layout_height="wrap_content"
android:layout_alignParentBottom="true" android:text="GỬI" />
</RelativeLayout>
MainActivity Server (Java)
package com.example.socketserver;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
public class MainActivity extends AppCompatActivity {
private static final int PORT = 8080;
private ServerSocket serverSocket;
private Socket clientSocket;
private PrintWriter out;
private BufferedReader in;
private Thread serverThread;
private TextView tvIp, tvPort, tvStatus, tvMessages;
private EditText etMsg;
private Button btnSend;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tvIp = findViewById(R.id.tvServerIp);
tvPort = findViewById(R.id.tvServerPort);
tvStatus = findViewById(R.id.tvConnStatus);
tvMessages = findViewById(R.id.tvMessages);
etMsg = findViewById(R.id.etMsg);
btnSend = findViewById(R.id.btnSend);
String localIp = getLocalIpAddress();
tvIp.setText("Server IP: " + (localIp != null ? localIp : "Không xác định"));
tvPort.setText("Port: " + PORT);
startServer();
btnSend.setOnClickListener(v -> {
String msg = etMsg.getText().toString().trim();
if (!msg.isEmpty() && out != null) {
sendMessage(msg);
}
});
}
private String getLocalIpAddress() {
try {
WifiManager wm = (WifiManager) getApplicationContext().getSystemService(WIFI_SERVICE);
WifiInfo info = wm.getConnectionInfo();
int ipInt = info.getIpAddress();
if (ipInt == 0) return null;
byte[] bytes = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(ipInt).array();
return InetAddress.getByAddress(bytes).getHostAddress();
} catch (Exception e) { return null; }
}
private void startServer() {
serverThread = new Thread(() -> {
try {
serverSocket = new ServerSocket(PORT);
runOnUiThread(() -> tvStatus.setText("Đang lắng nghe port " + PORT + "..."));
clientSocket = serverSocket.accept(); // Blocking
out = new PrintWriter(clientSocket.getOutputStream(), true);
in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
runOnUiThread(() -> tvStatus.setText("Client đã kết nối: " + clientSocket.getInetAddress()));
// Thread đọc tin nhắn từ client
new Thread(() -> {
String msg;
try {
while ((msg = in.readLine()) != null) {
final String finalMsg = msg;
runOnUiThread(() -> tvMessages.append("Client: " + finalMsg + "\n"));
}
} catch (IOException e) { e.printStackTrace(); }
finally { onClientDisconnected(); }
}).start();
} catch (IOException e) { e.printStackTrace(); }
});
serverThread.start();
}
private void sendMessage(String msg) {
out.println(msg);
runOnUiThread(() -> {
tvMessages.append("Server: " + msg + "\n");
etMsg.setText("");
});
}
private void onClientDisconnected() {
runOnUiThread(() -> {
tvStatus.setText("Client ngắt kết nối. Đang chờ kết nối mới...");
tvMessages.append("-- Mất kết nối --\n");
});
// Restart server loop
startServer();
}
@Override
protected void onDestroy() {
super.onDestroy();
try {
if (in != null) in.close();
if (out != null) out.close();
if (clientSocket != null) clientSocket.close();
if (serverSocket != null) serverSocket.close();
} catch (IOException ignored) {}
}
}
PHẦN 2: CLIENT APP
Layout Client (res/layout/activity_main.xml)
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="https://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp">
<EditText android:id="@+id/etServerIp" android:layout_width="match_parent" android:layout_height="wrap_content"
android:hint="IP Server (vd: 192.168.1.5)" android:inputType="text" />
<EditText android:id="@+id/etServerPort" android:layout_width="match_parent" android:layout_height="wrap_content"
android:layout_below="@id/etServerIp" android:hint="Port (vd: 8080)" android:inputType="number" />
<Button android:id="@+id/btnConnect" android:layout_width="match_parent" android:layout_height="wrap_content"
android:layout_below="@id/etServerPort" android:layout_marginTop="16dp" android:text="KẾT NỐI" />
<TextView android:id="@+id/tvMessages" android:layout_width="match_parent" android:layout_height="0dp"
android:layout_above="@id/etMsg" android:layout_below="@id/btnConnect"
android:inputType="textMultiLine" android:background="@android:drawable/editbox_background"
android:padding="8dp" android:layout_marginTop="8dp" android:scrollbars="vertical" />
<EditText android:id="@+id/etMsg" android:layout_width="match_parent" android:layout_height="wrap_content"
android:layout_above="@id/btnSend" android:layout_alignParentBottom="false"
android:hint="Nhập tin nhắn" android:inputType="text" android:layout_marginTop="8dp" />
<Button android:id="@+id/btnSend" android:layout_width="match_parent" android:layout_height="wrap_content"
android:layout_alignParentBottom="true" android:text="GỬI" android:enabled="false" />
</RelativeLayout>
MainActivity Client (Java)
package com.example.socketclient;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class MainActivity extends AppCompatActivity {
private Socket socket;
private PrintWriter out;
private BufferedReader in;
private Thread receiveThread;
private EditText etIp, etPort, etMsg;
private Button btnConnect, btnSend;
private TextView tvMessages;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
etIp = findViewById(R.id.etServerIp);
etPort = findViewById(R.id.etServerPort);
btnConnect = findViewById(R.id.btnConnect);
btnSend = findViewById(R.id.btnSend);
etMsg = findViewById(R.id.etMsg);
tvMessages = findViewById(R.id.tvMessages);
btnConnect.setOnClickListener(v -> connectToServer());
btnSend.setOnClickListener(v -> sendMessage());
}
private void connectToServer() {
String ip = etIp.getText().toString().trim();
String portStr = etPort.getText().toString().trim();
if (ip.isEmpty() || portStr.isEmpty()) return;
int port = Integer.parseInt(portStr);
new Thread(() -> {
try {
socket = new Socket(ip, port);
out = new PrintWriter(socket.getOutputStream(), true);
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
runOnUiThread(() -> {
tvMessages.setText("Đã kết nối đến " + ip + ":" + port + "\n");
btnConnect.setEnabled(false);
btnSend.setEnabled(true);
etIp.setEnabled(false);
etPort.setEnabled(false);
});
// Thread nhận tin nhắn từ server
receiveThread = new Thread(() -> {
String msg;
try {
while ((msg = in.readLine()) != null) {
final String f = msg;
runOnUiThread(() -> tvMessages.append("Server: " + f + "\n"));
}
} catch (IOException e) { e.printStackTrace(); }
finally { onDisconnected(); }
});
receiveThread.start();
} catch (IOException e) {
e.printStackTrace();
runOnUiThread(() -> tvMessages.setText("Kết nối thất bại: " + e.getMessage()));
}
}).start();
}
private void sendMessage() {
String msg = etMsg.getText().toString().trim();
if (!msg.isEmpty() && out != null) {
out.println(msg);
runOnUiThread(() -> {
tvMessages.append("Client: " + msg + "\n");
etMsg.setText("");
});
}
}
private void onDisconnected() {
runOnUiThread(() -> {
tvMessages.append("-- Mất kết nối --\n");
btnConnect.setEnabled(true);
btnSend.setEnabled(false);
etIp.setEnabled(true);
etPort.setEnabled(true);
});
}
@Override
protected void onDestroy() {
super.onDestroy();
try {
if (in != null) in.close();
if (out != null) out.close();
if (socket != null) socket.close();
} catch (IOException ignored) {}
}
}
Hướng dẫn chạy thử
- Kết nối 2 thiết bị (hoặc 1 thiết bị + 1 emulator) vào **cùng một WiFi LAN**.
- Cài đặt và chạy **Server App** trước. Ghi lại IP hiển thị (vd: 192.168.1.15).
- Cài đặt và chạy **Client App**. Nhập IP Server và Port 8080, nhấn KẾT NỐI.
- Sau khi kết nối thành công, hai bên có thể chat realtime.
Lưu ý kỹ thuật quan trọng
| Vấn đề | Giải pháp |
|---|---|
| NetworkOnMainThreadException | Luôn dùng Thread/RxJava/Coroutine cho Socket I/O |
| IP thay đổi (DHCP) | Server hiển thị IP động, Client nhập tay hoặc dùng mDNS/NSD discovery |
| Firewall chặn port | Mở port 8080 trên router/tường lửa, hoặc dùng port > 1024 |
| Emulator localhost | Emulator dùng 10.0.2.2 để truy cập localhost máy host |
| Đóng app không đóng socket | Override onDestroy() close tất cả stream/socket |
| Reconnection | Server loop accept() mới sau khi client disconnect (xem code) |
Mở rộng thực tế
- Dùng **Kotlin Coroutines + Flow** cho code ngắn gọn, structured concurrency.
- Protocol: JSON/Protobuf thay vì plain text line-based.
- Heartbeat (ping/pong) để detect dead connection.
- TLS/SSL (SSLSocket) cho bảo mật dữ liệu nhạy cảm.
- Network Service Discovery (NSD) để tự động tìm Server IP.
Kết luận
Socket TCP trên Android đơn giản nhưng mạnh mẽ cho giao tiếp realtime LAN. Yêu cầu quản lý thread cẩn thận, vòng đời socket, và xử lý lỗi mạng. Code mẫu Java trên chạy được ngay, dễ chuyển sang Kotlin Coroutines cho production.