首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java Socket 聊天室實戰(一):從單播推送到多線程架構完整演進

     編輯:Java 相關知識

系列規劃

  • 第 1 篇(本文):單客戶端雙向通信、服務端主動推送、Android 客戶端對接。
  • 第 2 篇:多客戶端廣播、私聊、心跳檢測、斷線重連。
  • 第 3 篇:Netty 重構、協議編解碼、集群擴展。

本文目標

實現一個 **最小可用** 的 Socket 聊天雛形:

  1. Server 啟動監聽固定端口(8888)。
  2. Client(Android)連接 Server,雙方均可發送文本消息。
  3. Server 控制台輸入內容 → 推送給 Client;Client 輸入 → 發送給 Server 並顯示在 UI。
  4. 採用 **阻塞 I/O + 多線程**(每連接一線程)架構,代碼清晰易懂。

共用常量與實體類

// 常量
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();
    }
}

Android 客戶端實現

1. 全局 Application 持有 Socket 連接

public class ChatApplication extends Application {
    public static ChatClient chatClient;
    public static User currentUser;
}

2. 核心 ChatClient(單例、後台線程讀寫、Handler 回調 UI)

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) {}
    }
}

3. 登錄 / 聊天 Activity 關鍵代碼

// 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

運行驗證

  1. 啟動 ChatServer.main(),控制台顯示 Chat Server started on port 8888。
  2. 在真機/模擬器運行 Android App,點擊登錄,顯示「連接成功」。
  3. Server 控制台輸入 Hello Android → 手機端立即收到並顯示。
  4. 手機端輸入 Hi Server → Server 控制台打印 [timestamp] Client: Hi Server。

至此,一個雙向通信的 Socket 聊天雛形已跑通。下一篇將引入「多客戶端管理、廣播/私聊、心跳檢測」,敬請期待。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved