架構概覽
- Java 端:ServerSocket 監聽埠口,接受連線,讀取一行文字,原樣回應
- PHP 端:socket_create 連線,發送資料,讀取回應,關閉連線
- 協定:純文字、換行符結尾 (
\n)、請求-回應模式
Java 伺服器端完整程式碼
import java.io.*;
import java.net.*;
public class EchoServer {
private static final int PORT = 5678;
public static void main(String[] args) {
System.out.println("Server starting on port " + PORT + "...");
try (ServerSocket server = new ServerSocket(PORT)) {
server.setReuseAddress(true);
System.out.println("Server started. Waiting for connections...");
while (true) {
try (Socket client = server.accept();
BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream(), "UTF-8"));
PrintWriter out = new PrintWriter(new OutputStreamWriter(client.getOutputStream(), "UTF-8"), true)) {
String clientIp = client.getInetAddress().getHostAddress();
System.out.println("Client connected: " + clientIp);
String line;
while ((line = in.readLine()) != null) {
System.out.println("Received from " + clientIp + ": " + line);
// 業務邏輯處理
String response = processRequest(line);
out.println(response); // 自動 flush
}
System.out.println("Client disconnected: " + clientIp);
} catch (IOException e) {
System.err.println("Client handling error: " + e.getMessage());
}
}
} catch (IOException e) {
System.err.println("Server fatal error: " + e.getMessage());
e.printStackTrace();
}
}
private static String processRequest(String input) {
// 這裡實作實際業務邏輯
// 範例:直接回聲
return "ECHO: " + input;
}
}
關鍵點說明
try-with-resources:Java 7+ 自動關閉資源,防洩漏
setReuseAddress(true):避免重啟時 Address already in use
PrintWriter(..., true):啟用自動 flush,println() 即時發送
readLine():阻塞讀取至換行符,適配 PHP PHP_NORMAL_READ
- 編碼統一指定
UTF-8,避免中文亂碼
PHP 客戶端完整程式碼
<?php
/**
* Socket 客戶端封裝類
*/
class SocketClient {
private $socket;
private $host;
private $port;
private float $timeout;
public function __construct(string $host = '127.0.0.1', int $port = 5678, float $timeout = 5.0) {
$this->host = $host;
$this->port = $port;
$this->timeout = $timeout;
}
public function connect(): bool {
$this->socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if ($this->socket === false) {
throw new RuntimeException('socket_create failed: ' . socket_strerror(socket_last_error()));
}
// 設定連線與收發逾時
socket_set_option($this->socket, SOL_SOCKET, SO_RCVTIMEO, ['sec' => (int)$this->timeout, 'usec' => 0]);
socket_set_option($this->socket, SOL_SOCKET, SO_SNDTIMEO, ['sec' => (int)$this->timeout, 'usec' => 0]);
$result = @socket_connect($this->socket, $this->host, $this->port);
if ($result === false) {
$err = socket_last_error($this->socket);
socket_close($this->socket);
throw new RuntimeException("socket_connect failed ({$err}): " . socket_strerror($err));
}
return true;
}
public function send(string $data): bool {
// 確保以 \n 結尾
if (!str_ends_with($data, "\n")) $data .= "\n";
$bytes = socket_write($this->socket, $data, strlen($data));
return $bytes !== false && $bytes === strlen($data);
}
public function receive(int $maxLength = 8192): ?string {
// PHP_NORMAL_READ: 讀取至 \n 或 \r\n
$data = socket_read($this->socket, $maxLength, PHP_NORMAL_READ);
if ($data === false) {
throw new RuntimeException('socket_read failed: ' . socket_strerror(socket_last_error($this->socket)));
}
return $data !== '' ? rtrim($data, "\r\n") : null;
}
public function close(): void {
if (is_resource($this->socket)) {
socket_close($this->socket);
$this->socket = null;
}
}
public function __destruct() {
$this->close();
}
}
// 使用範例
try {
$client = new SocketClient('127.0.0.1', 5678);
$client->connect();
$request = 'Hello from PHP ' . date('H:i:s');
echo "Sending: {$request}\n";
$client->send($request);
$response = $client->receive();
echo "Received: {$response}\n";
$client->close();
} catch (Throwable $e) {
echo "Error: {$e->getMessage()}\n";
exit(1);
}
進階:支援 JSON 協定的雙向通訊
<?php
class JsonSocketClient extends SocketClient {
public function sendRequest(string $action, array $payload = []): ?array {
$request = [
'action' => $action,
'payload' => $payload,
'timestamp' => microtime(true),
'request_id' => bin2hex(random_bytes(8)),
];
$json = json_encode($request, JSON_UNESCAPED_UNICODE);
if (!$this->send($json)) return null;
$response = $this->receive();
if ($response === null) return null;
$data = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
error_log('Invalid JSON response: ' . $response);
return null;
}
return $data;
}
}
// 使用
$client = new JsonSocketClient('127.0.0.1', 5678);
$client->connect();
$result = $client->sendRequest('calculate', ['a' => 10, 'b' => 5, 'op' => 'add']);
if ($result && ($result['status'] ?? '') === 'ok') {
echo "Result: {$result['data']['result']}\n"; // 15
}
$client->close();
Java 端對應 JSON 處理
// 使用 Jackson 或 Gson 解析
// 依賴: com.fasterxml.jackson.core:jackson-databind
private static String processRequest(String input) {
try {
ObjectMapper mapper = new ObjectMapper();
JsonNode req = mapper.readTree(input);
String action = req.get("action").asText();
JsonNode payload = req.get("payload");
ObjectNode resp = mapper.createObjectNode();
resp.put("request_id", req.get("request_id").asText());
resp.put("timestamp", System.currentTimeMillis() / 1000.0);
switch (action) {
case "calculate" -> {
double a = payload.get("a").asDouble();
double b = payload.get("b").asDouble();
String op = payload.get("op").asText();
double result = switch (op) {
case "add" -> a + b;
case "sub" -> a - b;
case "mul" -> a * b;
case "div" -> b != 0 ? a / b : Double.NaN;
default -> Double.NaN;
};
resp.put("status", "ok");
resp.putObject("data").put("result", result);
}
default -> {
resp.put("status", "error");
resp.put("message", "Unknown action: " + action);
}
}
return mapper.writeValueAsString(resp);
} catch (Exception e) {
ObjectNode err = mapper.createObjectNode();
err.put("status", "error");
err.put("message", e.getMessage());
return mapper.writeValueAsString(err);
}
}
部署與除錯清單
| 項目 | 檢查重點 |
| 防火牆 | 伺服器開放埠口 (5678),雲端安全群組同步設定 |
| 綁定位址 | Java new ServerSocket(port) 預設綁定 0.0.0.0,內網可用;若需限制本機改 new ServerSocket(port, 50, InetAddress.getByName("127.0.0.1")) |
| 編碼一致 | 雙端均指定 UTF-8,含 JSON 序列化 |
| 逾時設定 | PHP socket_set_option、Java socket.setSoTimeout() |
| 程序守護 | Java 用 systemd/supervisor 管理,自動重啟、日誌輪替 |
| 連線池 | 高頻呼叫建議 PHP 端用持久連線或連線池 (swoole/Workerman) |
替代方案比較
- HTTP/REST + JSON:通用性最好、除錯容易、支援負載平衡、但有 HTTP 標頭開銷
- gRPC:高效能、強型別契約、支援流式、但需 Protobuf、學習曲線較陡
- Message Queue (RabbitMQ/Kafka):非同步、解耦、削峰填谷、但引入基建複雜度
- Raw Socket (本文):極低延遲、雙向即時、適合長連線、但需自行處理協定、心跳、重連