實現一個 **最小可用** 的 Socket 聊天雛形:
// 常量
public final class SocketConstants {
public static final String SERVER_IP = "192.168.1.100"; // 實際部署 IP
public static final int PORT = 8888;
}
// 消息實體(Gson 序列化)
public class Message {
private long msgId;
private long userId;
private String userName;
private int type; // 1=文本 2=圖片 3=語音...
private String content;
private long timestamp;
// getter/setter/構造器/ toString 省略
}
// 用戶信息
public class User {
private long userId;
private String userName;
private String avatarUrl;
// getter/setter 省略
}
public class ChatServer {
private final ServerSocket serverSocket;
private final Gson gson = new Gson();
public ChatServer(int port) throws IOException {
this.serverSocket = new ServerSocket(port);
}
public void start() {
System.out.println("Chat Server started on port " + serverSocket.getLocalPort());
while (true) {
try {
Socket client = serverSocket.accept();
System.out.println("New client: " + client.getRemoteSocketAddress());
// 每個客戶端獨立線程處理
new Thread(new ClientHandler(client)).start();
} catch (IOException e) {
System.err.println("Accept failed: " + e.getMessage());
}
}
}
private class ClientHandler implements Runnable {
private final Socket socket;
private final BufferedReader in;
private final PrintWriter out;
ClientHandler(Socket socket) throws IOException {
this.socket = socket;
this.in = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
this.out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8), true);
}
@Override
public void run() {
// 1. 開啟「控制台輸入 → 推送給該客戶端」線程
new Thread(() -> {
try (BufferedReader console = new BufferedReader(new InputStreamReader(System.in))) {
String line;
while ((line = console.readLine()) != null) {
Message msg = new Message();
msg.setUserName("Server");
msg.setContent(line);
msg.setType(1);
msg.setTimestamp(System.currentTimeMillis());
out.println(gson.toJson(msg)); // 每條消息以 \n 結尾
}
} catch (IOException ignored) {}
}).start();
// 2. 主線程循環讀取客戶端消息
String line;
try {
while ((line = in.readLine()) != null) {
Message msg = gson.fromJson(line, Message.class);
System.out.printf("[%s] %s: %s%n",
new Date(msg.getTimestamp()), msg.getUserName(), msg.getContent());
}
} catch (IOException e) {
System.err.println("Client read error: " + e.getMessage());
} finally {
closeQuietly();
}
}
private void closeQuietly() {
try { socket.close(); } catch (IOException ignored) {}
}
}
public static void main(String[] args) throws IOException {
new ChatServer(SocketConstants.PORT).start();
}
}
public class ChatApplication extends Application {
public static ChatClient chatClient;
public static User currentUser;
}
public class ChatClient {
private Socket socket;
private PrintWriter out;
private BufferedReader in;
private Handler uiHandler; // 主線程 Handler,用於更新 UI
private final Gson gson = new Gson();
public void connect(String ip, int port, Handler handler) {
this.uiHandler = handler;
new Thread(() -> {
try {
socket = new Socket();
socket.connect(new InetSocketAddress(ip, port), 3000);
out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8), true);
in = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
// 連接成功通知 UI
uiHandler.obtainMessage(0, "CONNECTED").sendToTarget();
// 循環讀取服務端消息
String line;
while ((line = in.readLine()) != null) {
Message msg = gson.fromJson(line, Message.class);
uiHandler.obtainMessage(1, msg).sendToTarget(); // 1 = 收到消息
}
} catch (IOException e) {
uiHandler.obtainMessage(-1, e.getMessage()).sendToTarget();
}
}).start();
}
public void send(String text) {
if (out == null) return;
Message msg = new Message();
msg.setUserId(ChatApplication.currentUser.getUserId());
msg.setUserName(ChatApplication.currentUser.getUserName());
msg.setContent(text);
msg.setType(1);
msg.setTimestamp(System.currentTimeMillis());
out.println(gson.toJson(msg));
}
public void disconnect() {
try { if (socket != null) socket.close(); } catch (IOException ignored) {}
}
}
// LoginActivity: 簡單驗證後啟動 ChatClient
public class LoginActivity extends AppCompatActivity {
// ... findViewById 省略
loginBtn.setOnClickListener(v -> {
String name = nameEt.getText().toString().trim();
String pwd = pwdEt.getText().toString().trim();
if ("admin".equals(name) && "123".equals(pwd)) {
ChatApplication.currentUser = new User(1, name, "");
ChatApplication.chatClient = new ChatClient();
ChatApplication.chatClient.connect(SocketConstants.SERVER_IP, SocketConstants.PORT, uiHandler);
startActivity(new Intent(this, ChatActivity.class));
finish();
} else { Toast.makeText(this, "帳號密碼錯誤", Toast.LENGTH_SHORT).show(); }
});
private final Handler uiHandler = new Handler(msg -> {
if (msg.what == 0) Toast.makeText(this, "連接成功", Toast.LENGTH_SHORT).show();
else if (msg.what == -1) Toast.makeText(this, "連接失敗: " + msg.obj, Toast.LENGTH_LONG).show();
return true;
});
}
// ChatActivity: 發送/接收消息更新 RecyclerView/ListView
public class ChatActivity extends AppCompatActivity {
private List msgList = new ArrayList<>();
private MessageAdapter adapter; // 省略實現
private final Handler uiHandler = new Handler(msg -> {
if (msg.what == 1) {
msgList.add((Message) msg.obj);
adapter.notifyItemInserted(msgList.size() - 1);
recyclerView.scrollToPosition(msgList.size() - 1);
}
return true;
});
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_chat);
// ... init RecyclerView, adapter
sendBtn.setOnClickListener(v -> {
String text = inputEt.getText().toString().trim();
if (!text.isEmpty()) {
ChatApplication.chatClient.send(text);
inputEt.setText("");
}
});
// 註冊 Handler 到 ChatClient
ChatApplication.chatClient.setUiHandler(uiHandler); // 需在 ChatClient 添加 setter
}
@Override
protected void onDestroy() {
super.onDestroy();
ChatApplication.chatClient.disconnect();
}
}
| 決策點 | 本文方案 | 局限與演進方向 |
|---|---|---|
| 消息分隔 | 換行符 \n 作為幀邊界 | 含換行的文本會截斷 → 第 2 篇引入 Length Field Based Frame Decoder |
| 編碼 | UTF-8 + Gson JSON | 通用性好,體積稍大 → 高性能場景可改 Protobuf |
| 線程模型 | Server: 每連接一線程;Client: 連接線程 + UI 線程 | C10K 問題 → 第 3 篇遷移 Netty EventLoop |
| 心跳/重連 | 無 | 生產必備 → 第 2 篇加入 IdleStateHandler |
ChatServer.main(),控制台顯示 Chat Server started on port 8888。Hello Android → 手機端立即收到並顯示。Hi Server → Server 控制台打印 [timestamp] Client: Hi Server。至此,一個雙向通信的 Socket 聊天雛形已跑通。下一篇將引入「多客戶端管理、廣播/私聊、心跳檢測」,敬請期待。