Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> Android

Gửi nhận dữ liệu Socket Client-Server trên Android

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ở ServerSocket lắ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ử

  1. Kết nối 2 thiết bị (hoặc 1 thiết bị + 1 emulator) vào **cùng một WiFi LAN**.
  2. Cài đặt và chạy **Server App** trước. Ghi lại IP hiển thị (vd: 192.168.1.15).
  3. Cài đặt và chạy **Client App**. Nhập IP Server và Port 8080, nhấn KẾT NỐI.
  4. 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.