บทความนี้เป็นคู่มือครบวงจรสำหรับการพัฒนาแอปพลิเคชัน Client-Server บน Android โดยใช้ Java Socket (TCP) แบบ Low-level เน้นความเข้าใจพื้นฐานของ Network Programming
🎯 เป้าหมายการเรียนรู้
- เข้าใจหลักการทำงานของ TCP Socket
- สร้าง Server ที่รองรับหลาย Client (Multi-threaded)
- จัดการ Thread และ UI Thread ให้ถูกต้อง
- รู้จักข้อจำกัดและ Best Practices ของ Network บน Android
🏛 สถาปัตยกรรมโดยรวม
┌─────────────────┐ WiFi LAN ┌─────────────────┐ │ Server App │ ◄──────────────────────► │ Client App │ │ (Port 8080) │ TCP Socket │ │ │ │ │ │ │ ServerSocket │ accept() ──► Socket │ Socket │ │ .bind() │ │ .connect() │ │ .listen() │ InputStream ◄────────► │ OutputStream │ │ .accept() │ OutputStream ────────► │ InputStream │ └─────────────────┘ └─────────────────┘
🖥 โปรเจกต์ Server (รายละเอียดเพิ่มเติม)
ปรับปรุง: รองรับหลาย Client (Multi-client Server)
// ใน ServerRunnable - แทนที่ accept() เดียว
while (!Thread.currentThread().isInterrupted()) {
try {
Socket clientSocket = serverSocket.accept();
// สร้าง Thread ใหม่สำหรับแต่ละ Client
new Thread(new ClientHandler(clientSocket)).start();
} catch (IOException e) {
if (!serverSocket.isClosed()) e.printStackTrace();
break;
}
}
// ClientHandler Class
private class ClientHandler implements Runnable {
private final Socket socket;
private PrintWriter out;
private BufferedReader in;
ClientHandler(Socket socket) { this.socket = socket; }
@Override
public void run() {
try {
out = new PrintWriter(socket.getOutputStream(), true);
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String msg;
while ((msg = in.readLine()) != null) {
broadcast(msg, this); // ส่งให้ Client ทุกคน
}
} catch (IOException e) {
// Client disconnect
} finally {
removeClient(this);
closeResources();
}
}
void send(String msg) { if (out != null) out.println(msg); }
}
Broadcast ข้อความให้ทุก Client
private final Listclients = Collections.synchronizedList(new ArrayList<>()); private void broadcast(String message, ClientHandler sender) { for (ClientHandler client : clients) { if (client != sender) { // ไม่ส่งกลับให้ตัวเอง (ถ้าเป็น Chat) client.send(message); } } // แสดงบน Server UI runOnUiThread(() -> tvMessages.append("Broadcast: " + message + "\n")); }
📱 โปรเจกต์ Client (ปรับปรุง)
เพิ่ม Auto-Reconnect
private void connectWithRetry(String ip, int port) {
new Thread(() -> {
int retryCount = 0;
final int MAX_RETRIES = 5;
while (retryCount < MAX_RETRIES && !isConnected && !Thread.currentThread().isInterrupted()) {
try {
socket = new Socket(ip, port);
// ... setup streams ...
runOnUiThread(() -> updateUIConnected(true));
startReceiveThread();
return; // Success
} catch (IOException e) {
retryCount++;
try { Thread.sleep(3000); } catch (InterruptedException ignored) {}
}
}
runOnUiThread(() -> tvMessages.append("Connection failed after " + MAX_RETRIES + " retries\n"));
}).start();
}
Heartbeat Mechanism (Ping/Pong)
// Client ส่ง Ping ทุก 30 วินาที
private void startHeartbeat() {
new Thread(() -> {
while (isConnected && !Thread.currentThread().isInterrupted()) {
try {
Thread.sleep(30000);
if (output != null) {
output.println("__PING__");
output.flush();
}
} catch (Exception e) {
break; // Disconnected
}
}
}).start();
}
// Server รับ Ping ตอบ Pong
if ("__PING__".equals(msg)) {
out.println("__PONG__");
out.flush();
} else {
broadcast(msg, this);
}
📋 AndroidManifest.xml Permissions (ทั้งคู่)
<!-- Server -->
<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" />
<!-- Client -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- Android 9+ Cleartext Traffic -->
<application
android:usesCleartextTraffic="true"
... >
⚡ ประสิทธิภาพและ Scalability
| ปัญหา | Solution |
|---|---|
| Thread ต่อ Client = Memory/Context Switch มาก | Thread Pool (ExecutorService) + Non-blocking I/O (NIO) |
| Blocking I/O (readLine) ค้าง Thread | NIO Selector / Netty / Okio |
| ไม่มี Protocol Framing | กำหนด Protocol: Length-prefixed, Delimiter, หรือ Protobuf/JSON |
| No Flow Control | Backpressure handling, Buffer limits |
🔒 Security Hardening
- TLS/SSL: ใช้
SSLSocketFactoryกับ Certificate - Authentication: Token/Key Exchange ใน Handshake แรก
- Input Validation: ตรวจสอบข้อความก่อน Process
- Rate Limiting: ป้องกัน DoS/DDoS
ตัวอย่าง SSL Socket (Server)
// ต้องมี Keystore (BKS format) ใน res/raw/keystore.bks
KeyStore keyStore = KeyStore.getInstance("BKS");
keyStore.load(getResources().openRawResource(R.raw.keystore), "password".toCharArray());
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(keyStore, "password".toCharArray());
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(kmf.getKeyManagers(), null, null);
SSLServerSocketFactory factory = sslContext.getServerSocketFactory();
ServerSocket serverSocket = factory.createServerSocket(PORT);
🧪 การทดสอบและ Debug
เครื่องมือช่วย
- netcat (nc): `nc
8080` - ทดสอบ Server จาก Terminal - Wireshark: ดู Packet การสื่อสาร
- Android Studio Network Profiler: ดู Traffic Real-time
adb logcat | grep -i socket- ดู Log
Test Cases
- Normal Send/Receive
- Client Disconnect/Reconnect
- Server Restart
- Multiple Clients (ถ้า Multi-threaded)
- Large Message (> 64KB)
- Network Switch (WiFi ↔ Mobile Data)
- App Background/Foreground
- Doze Mode / Battery Optimization
📚 สรุปและขั้นตอนต่อไป
ตัวอย่างนี้เป็นพื้นฐาน (Educational) สำหรับ Production จริงแนะนำ:
- ใช้ OkHttp WebSocket หรือ Ktor (Kotlin)
- หรือ Firebase/Backend-as-a-Service ไม่ต้องจัดการ Server เอง
- ศึกษา Netty หรือ Vert.x สำหรับ High-performance Server
- เรียนรู้ Protocol Buffers / FlatBuffers สำหรับ Serialization ที่เร็ว
💡 เคล็ดลับ: สำหรับแอป Chat จริง ให้ใช้ Firebase Realtime Database หรือ Firestore - ประหยัดเวลา ปลอดภัย Scale ได้ดี และรองรับ Offline Sync อัตโนมัติ