Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Android

ส่งและรับข้อมูลด้วย Socket ใน Android: ตัวอย่าง Server-Client ครบวงจร

บทความนี้สาธิตการสร้างแอปพลิเคชัน Client-Server บน Android โดยใช้ Java Socket (TCP) สื่อสารผ่าน WiFi LAN ต้องการรัน 2 โปรเจกต์แยกกัน (Server App และ Client App)

🏗 สถาปัตยกรรม

  • Server App: เปิด ServerSocket รอการเชื่อมต่อ ที่ Port 8080
  • Client App: เชื่อมต่อไปยัง Server ผ่าน IP และ Port
  • การสื่อสาร: แบบ Real-time ส่ง/รับข้อความผ่าน PrintWriter / BufferedReader

📋 สิ่งที่ต้องเตรียม

  • อุปกรณ์ Android 2 เครื่อง (หรือ 1 เครื่อง + Emulator)
  • เชื่อมต่อ WiFi เครือข่ายเดียวกัน (LAN เดียวกัน)
  • ทราบ IP Address ของเครื่อง Server (ดูได้จากแอป Server เอง)

🖥 ส่วนที่ 1: Server Application

Layout (res/layout/activity_main.xml)

<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/tvIP"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="IP: detecting..."
        android:textSize="16sp"/>

    <TextView
        android:id="@+id/tvPort"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/tvIP"
        android:text="Port: 8080"
        android:textSize="16sp"/>

    <TextView
        android:id="@+id/tvStatus"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/tvPort"
        android:text="Status: Waiting..."
        android:textSize="16sp"
        android:layout_marginTop="8dp"/>

    <TextView
        android:id="@+id/tvMessages"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:layout_above="@id/etMessage"
        android:layout_below="@id/tvStatus"
        android:layout_marginTop="8dp"
        android:background="#F5F5F5"
        android:padding="8dp"
        android:inputType="textMultiLine"
        android:textSize="14sp"/>

    <EditText
        android:id="@+id/etMessage"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_above="@id/btnSend"
        android:layout_marginTop="8dp"
        android:hint="พิมพ์ข้อความ..."
        android:inputType="text"/>

    <Button
        android:id="@+id/btnSend"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:text="ส่งข้อความ"/>
</RelativeLayout>

MainActivity.java (Server)

package com.example.socketserver;

import android.annotation.SuppressLint;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
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.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;

@SuppressLint("SetTextI18n")
public class MainActivity extends AppCompatActivity {

    private ServerSocket serverSocket;
    private Thread serverThread;
    private TextView tvIP, tvPort, tvStatus, tvMessages;
    private EditText etMessage;
    private Button btnSend;

    public static final int SERVER_PORT = 8080;
    public static String SERVER_IP = "";
    private PrintWriter output;
    private BufferedReader input;
    private Socket clientSocket;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        tvIP = findViewById(R.id.tvIP);
        tvPort = findViewById(R.id.tvPort);
        tvStatus = findViewById(R.id.tvStatus);
        tvMessages = findViewById(R.id.tvMessages);
        etMessage = findViewById(R.id.etMessage);
        btnSend = findViewById(R.id.btnSend);

        // หา IP Address ของเครื่องนี้
        try {
            SERVER_IP = getLocalIpAddress();
        } catch (UnknownHostException e) {
            e.printStackTrace();
            SERVER_IP = "Error";
        }

        tvIP.setText("Server IP: " + SERVER_IP);
        tvPort.setText("Port: " + SERVER_PORT);
        tvStatus.setText("Status: Starting Server...");

        // เริ่ม Server Thread
        serverThread = new Thread(new ServerRunnable());
        serverThread.start();

        btnSend.setOnClickListener(v -> {
            String msg = etMessage.getText().toString().trim();
            if (!msg.isEmpty() && output != null) {
                new Thread(() -> sendMessage(msg)).start();
            }
        });
    }

    private String getLocalIpAddress() throws UnknownHostException {
        WifiManager wifiManager = (WifiManager) getApplicationContext().getSystemService(WIFI_SERVICE);
        if (wifiManager == null) throw new UnknownHostException("WifiManager null");
        WifiInfo wifiInfo = wifiManager.getConnectionInfo();
        int ipInt = wifiInfo.getIpAddress();
        if (ipInt == 0) throw new UnknownHostException("No IP");
        byte[] ipBytes = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(ipInt).array();
        return InetAddress.getByAddress(ipBytes).getHostAddress();
    }

    private class ServerRunnable implements Runnable {
        @Override
        public void run() {
            try {
                serverSocket = new ServerSocket(SERVER_PORT);
                runOnUiThread(() -> tvStatus.setText("Status: Listening on " + SERVER_IP + ":" + SERVER_PORT));

                // รอ Client เชื่อมต่อ (Blocking)
                clientSocket = serverSocket.accept();
                
                output = new PrintWriter(clientSocket.getOutputStream(), true);
                input = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));

                runOnUiThread(() -> tvStatus.setText("Status: Client Connected!"));

                // เริ่ม Thread อ่านข้อมูลจาก Client
                new Thread(new ReceiveRunnable()).start();

            } catch (IOException e) {
                e.printStackTrace();
                runOnUiThread(() -> tvStatus.setText("Error: " + e.getMessage()));
            }
        }
    }

    private class ReceiveRunnable implements Runnable {
        @Override
        public void run() {
            try {
                String msg;
                while ((msg = input.readLine()) != null) {
                    final String finalMsg = msg;
                    runOnUiThread(() -> tvMessages.append("Client: " + finalMsg + "\n"));
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                // Client ตัดการเชื่อมต่อ - รอ Client ใหม่
                runOnUiThread(() -> tvStatus.setText("Status: Client Disconnected. Restarting..."));
                restartServer();
            }
        }
    }

    private void sendMessage(String msg) {
        output.println(msg);
        output.flush();
        runOnUiThread(() -> {
            tvMessages.append("Server: " + msg + "\n");
            etMessage.setText("");
        });
    }

    private void restartServer() {
        try {
            if (clientSocket != null) clientSocket.close();
            if (serverSocket != null) serverSocket.close();
        } catch (IOException ignored) {}
        serverThread = new Thread(new ServerRunnable());
        serverThread.start();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        try {
            if (output != null) output.close();
            if (input != null) input.close();
            if (clientSocket != null) clientSocket.close();
            if (serverSocket != null) serverSocket.close();
        } catch (IOException ignored) {}
    }
}

AndroidManifest.xml (Server)

<manifest xmlns:android="https://schemas.android.com/apk/res/android"
    package="com.example.socketserver">

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

    <application
        android:label="Socket Server"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

📱 ส่วนที่ 2: Client Application

Layout (res/layout/activity_main.xml)

<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/etIP"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Server IP (เช่น 192.168.1.100)"
        android:inputType="text"/>

    <EditText
        android:id="@+id/etPort"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/etIP"
        android:layout_marginTop="8dp"
        android:hint="Port (เช่น 8080)"
        android:inputType="number"/>

    <Button
        android:id="@+id/btnConnect"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/etPort"
        android:layout_marginTop="16dp"
        android:text="เชื่อมต่อ Server"/>

    <TextView
        android:id="@+id/tvMessages"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:layout_above="@id/etMessage"
        android:layout_below="@id/btnConnect"
        android:layout_marginTop="8dp"
        android:background="#F5F5F5"
        android:padding="8dp"
        android:inputType="textMultiLine"
        android:textSize="14sp"/>

    <EditText
        android:id="@+id/etMessage"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_above="@id/btnSend"
        android:layout_marginTop="8dp"
        android:hint="พิมพ์ข้อความ..."
        android:inputType="text"/>

    <Button
        android:id="@+id/btnSend"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:text="ส่งข้อความ"/>
</RelativeLayout>

MainActivity.java (Client)

package com.example.socketclient;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
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 EditText etIP, etPort, etMessage;
    private TextView tvMessages;
    private Button btnConnect, btnSend;
    private Socket socket;
    private PrintWriter output;
    private BufferedReader input;
    private Thread receiveThread;
    private boolean isConnected = false;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        etIP = findViewById(R.id.etIP);
        etPort = findViewById(R.id.etPort);
        tvMessages = findViewById(R.id.tvMessages);
        etMessage = findViewById(R.id.etMessage);
        btnConnect = findViewById(R.id.btnConnect);
        btnSend = findViewById(R.id.btnSend);

        btnSend.setEnabled(false);

        btnConnect.setOnClickListener(v -> {
            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(new ConnectRunnable(ip, port)).start();
        });

        btnSend.setOnClickListener(v -> {
            String msg = etMessage.getText().toString().trim();
            if (!msg.isEmpty() && output != null) {
                new Thread(() -> sendMessage(msg)).start();
            }
        });
    }

    private class ConnectRunnable implements Runnable {
        private final String ip;
        private final int port;

        ConnectRunnable(String ip, int port) {
            this.ip = ip;
            this.port = port;
        }

        @Override
        public void run() {
            try {
                socket = new Socket(ip, port);
                output = new PrintWriter(socket.getOutputStream(), true);
                input = new BufferedReader(new InputStreamReader(socket.getInputStream()));

                runOnUiThread(() -> {
                    tvMessages.setText("Connected to " + ip + ":" + port + "\n");
                    btnConnect.setText("Disconnect");
                    btnSend.setEnabled(true);
                    isConnected = true;
                });

                receiveThread = new Thread(new ReceiveRunnable());
                receiveThread.start();

            } catch (IOException e) {
                e.printStackTrace();
                runOnUiThread(() -> tvMessages.setText("Connection Failed: " + e.getMessage()));
            }
        }
    }

    private class ReceiveRunnable implements Runnable {
        @Override
        public void run() {
            try {
                String msg;
                while ((msg = input.readLine()) != null) {
                    final String finalMsg = msg;
                    runOnUiThread(() -> tvMessages.append("Server: " + finalMsg + "\n"));
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                runOnUiThread(() -> {
                    tvMessages.append("\nDisconnected from server.\n");
                    btnConnect.setText("Connect");
                    btnSend.setEnabled(false);
                    isConnected = false;
                });
            }
        }
    }

    private void sendMessage(String msg) {
        output.println(msg);
        output.flush();
        runOnUiThread(() -> {
            tvMessages.append("Me: " + msg + "\n");
            etMessage.setText("");
        });
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        try {
            if (output != null) output.close();
            if (input != null) input.close();
            if (socket != null) socket.close();
        } catch (IOException ignored) {}
    }
}

AndroidManifest.xml (Client)

<manifest xmlns:android="https://schemas.android.com/apk/res/android"
    package="com.example.socketclient">

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

    <application
        android:label="Socket Client"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

🚀 วิธีทดสอบ

  1. Build และ Install Server App บนอุปกรณ์เครื่องแรก
  2. Build และ Install Client App บนอุปกรณ์เครื่องที่สอง
  3. เปิด Server App → จดบันทึก IP Address ที่แสดง (เช่น 192.168.1.45)
  4. เปิด Client App → ใส่ IP และ Port (8080) → กด "เชื่อมต่อ Server"
  5. เมื่อเชื่อมต่อสำเร็จ พิมพ์ข้อความส่งไปมาระหว่าง 2 เครื่อง

⚠️ ข้อควรระวังและ Best Practices

🔒 Security

  • Socket ธรรมดาไม่มีการเข้ารหัส - ใช้ได้เฉพาะ Local Network ที่เชื่อถือได้
  • Production ควรใช้ SSL/TLS (SSLSocket) หรือ WebSocket over WSS

🧵 Threading

  • Network Operation ห้ามทำบน Main Thread (ใช้ Thread/RxJava/Coroutines)
  • UI Update ต้องใช้ runOnUiThread() หรือ Handler

🔄 Connection Management

  • จัดการ Reconnection เมื่อขาดสัญญาณ
  • Heartbeat/Ping-Pong เพื่อตรวจสอบการเชื่อมต่อ
  • Buffer ข้อความขณะ Disconnected

📱 Android Version

  • Android 9+ (API 28): Cleartext Traffic ถูกบล็อคโดย Default ต้องเพิ่ม android:usesCleartextTraffic="true" ใน Manifest application tag

🔧 แนวทางทันสมัย (Modern Alternatives)

  • OkHttp WebSocket: ใช้ Library OkHttp รองรับ WebSocket ทันสมัย
  • Firebase Realtime Database / Firestore: Sync ข้อมูล Real-time ไม่ต้องจัดการ Server เอง
  • MQTT (Paho/Eclipse): Protocol เบา สำหรับ IoT
  • gRPC: High-performance RPC Framework
  • Nearby Connections API: Google API สำหรับ Peer-to-Peer ใกล้เคียง (Bluetooth/WiFi Direct)