บทความนี้สาธิตการสร้างแอปพลิเคชัน 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>
🚀 วิธีทดสอบ
- Build และ Install Server App บนอุปกรณ์เครื่องแรก
- Build และ Install Client App บนอุปกรณ์เครื่องที่สอง
- เปิด Server App → จดบันทึก IP Address ที่แสดง (เช่น 192.168.1.45)
- เปิด Client App → ใส่ IP และ Port (8080) → กด "เชื่อมต่อ Server"
- เมื่อเชื่อมต่อสำเร็จ พิมพ์ข้อความส่งไปมาระหว่าง 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)