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

Java Socket 實現簡易控制台聊天室:服務端與客戶端完整代碼

     編輯:Java 相關知識

前言

Socket 是網絡編程基石。本文通過一個基於 Swing GUI 的單聊天室,演示 Java 阻塞 IO(BIO)模式下的服務端/客戶端雙向通信全流程。代碼結構清晰,適合網絡編程入門教學與快速原型驗證。

架構設計

  • 單線程服務端:主線程監聽端口,接受一個客戶端連接,主循環讀取客戶端消息並打印,發送消息由 GUI 事件線程觸發。
  • 客戶端:主線程連接服務端並循環讀取服務端消息,發送同樣由 GUI 事件觸發。
  • 協議:行文本協議,以換行符 \n 為邊界,PrintWriter.println() / BufferedReader.readLine() 自動處理。

服務端完整代碼

package qiu;

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.net.*;
import java.util.Calendar;

public class MyServer extends JFrame implements ActionListener {
    // GUI 組件
    private JTextArea area = new JTextArea();
    private JTextField input = new JTextField(20);
    private JButton sendBtn = new JButton("發送");
    private JScrollPane scroll = new JScrollPane(area);
    private JPanel bottom = new JPanel();

    // 網絡
    private PrintWriter out;  // 發送給客戶端
    private BufferedReader in; // 接收客戶端

    public static void main(String[] args) {
        SwingUtilities.invokeLater(MyServer::new);
    }

    public MyServer() {
        initGUI();
        startServer();
    }

    private void initGUI() {
        area.setEditable(false);
        sendBtn.addActionListener(this);
        sendBtn.setActionCommand("send");

        bottom.add(input);
        bottom.add(sendBtn);

        add(scroll, BorderLayout.CENTER);
        add(bottom, BorderLayout.SOUTH);

        setTitle("聊天服務端");
        setSize(400, 300);
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        setLocationRelativeTo(null);
        setVisible(true);
    }

    private void startServer() {
        new Thread(() -> {
            try (ServerSocket ss = new ServerSocket(9988)) {
                append("服務端啟動,等待客戶端連接...端口 9988");
                Socket socket = ss.accept();  // 阻塞等待
                append("客戶端已連接: " + socket.getInetAddress());

                in = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));
                out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), "UTF-8"), true);

                String msg;
                while ((msg = in.readLine()) != null) {
                    append("客戶端 " + now() + "\n" + msg);
                }
                append("客戶端斷開連接");
            } catch (IOException e) {
                append("服務端異常: " + e.getMessage());
                e.printStackTrace();
            }
        }).start();
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        if ("send".equals(e.getActionCommand())) {
            String msg = input.getText().trim();
            if (!msg.isEmpty() && out != null) {
                out.println(msg);
                append("服務端 " + now() + "\n" + msg);
                input.setText("");
            }
        }
    }

    private void append(String text) {
        SwingUtilities.invokeLater(() -> {
            area.append(text + "\n");
            area.setCaretPosition(area.getDocument().getLength());
        });
    }

    private String now() {
        Calendar c = Calendar.getInstance();
        return String.format("%02d:%02d:%02d", 
                c.get(Calendar.HOUR_OF_DAY), c.get(Calendar.MINUTE), c.get(Calendar.SECOND));
    }
}

客戶端完整代碼

package qiu;

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.net.*;
import java.util.Calendar;

public class MyClient extends JFrame implements ActionListener {
    private JTextArea area = new JTextArea();
    private JTextField input = new JTextField(20);
    private JButton sendBtn = new JButton("發送");
    private JScrollPane scroll = new JScrollPane(area);
    private JPanel bottom = new JPanel();

    private PrintWriter out;
    private BufferedReader in;

    public static void main(String[] args) {
        SwingUtilities.invokeLater(MyClient::new);
    }

    public MyClient() {
        initGUI();
        connectServer();
    }

    private void initGUI() {
        area.setEditable(false);
        sendBtn.addActionListener(this);
        sendBtn.setActionCommand("send");

        bottom.add(input);
        bottom.add(sendBtn);

        add(scroll, BorderLayout.CENTER);
        add(bottom, BorderLayout.SOUTH);

        setTitle("聊天客戶端");
        setSize(400, 300);
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        setLocationRelativeTo(null);
        setVisible(true);
    }

    private void connectServer() {
        new Thread(() -> {
            try {
                Socket socket = new Socket("127.0.0.1", 9988);
                append("已連接到服務端");

                in = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));
                out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), "UTF-8"), true);

                String msg;
                while ((msg = in.readLine()) != null) {
                    append("服務端 " + now() + "\n" + msg);
                }
                append("服務端斷開連接");
            } catch (IOException e) {
                append("連接異常: " + e.getMessage());
                e.printStackTrace();
            }
        }).start();
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        if ("send".equals(e.getActionCommand())) {
            String msg = input.getText().trim();
            if (!msg.isEmpty() && out != null) {
                out.println(msg);
                append("客戶端 " + now() + "\n" + msg);
                input.setText("");
            }
        }
    }

    private void append(String text) {
        SwingUtilities.invokeLater(() -> area.append(text + "\n"));
    }

    private String now() {
        Calendar c = Calendar.getInstance();
        return String.format("%02d:%02d:%02d", 
                c.get(Calendar.HOUR_OF_DAY), c.get(Calendar.MINUTE), c.get(Calendar.SECOND));
    }
}

運行步驟

  1. 編譯:javac qiu/MyServer.java qiu/MyClient.java
  2. 啟動服務端:java qiu.MyServer
  3. 啟動客戶端(可多開):java qiu.MyClient
  4. 在任意一邊輸入文字按「發送」,另一邊即時顯示。

關鍵技術點解析

點說明
字符編碼顯式指定 UTF-8,避免中文亂碼
自動刷新PrintWriter(..., true) 開啟 autoFlush,println 即發送
線程分離網絡 IO 放入獨立線程,避免阻塞 EDT 導致界面凍結
線程安全更新 UI所有 area.append 包裝在 SwingUtilities.invokeLater
資源關閉使用 try-with-resources 管理 ServerSocket/Socket/流
行協議readLine() 遇到 \n/\r\n 返回,適合文本聊天

局限性與擴展方向

⚠️ 當前局限

  • 單客戶端:ss.accept() 只接受一個,後續連接被忽略。
  • 無用戶標識:無登入、無用戶名、無私聊/群聊區分。
  • 阻塞 IO:每連接佔用一線程,C10K 問題明顯。
  • 無心跳/斷線重連:網絡抖動易導致連接靜默斷開。
  • 無消息協議擴展:純文本,難以傳輸文件、表情、系統指令。

🚀 擴展路線圖

  1. 多客戶端:主循環 while(true){ new Thread(new ClientHandler(ss.accept())).start(); },維護 Map<String, PrintWriter> 廣播/私聊。
  2. 協議升級:定義 JSON 協議 {"type":"chat","from":"A","to":"B","msg":"..."},使用 Jackson/Gson 序列化。
  3. NIO/Netty 重寫:單線程事件循環支撐萬級連接,引入 LengthFieldBasedFrameDecoder 解決粘包。
  4. 心跳機制:客戶端定時發送 {"type":"ping"},服務端超時移除失效連接。
  5. 持久化:接入 MySQL/Redis 存儲離線消息、歷史記錄。
  6. 安全:SSL/TLS 加密傳輸,集成 JWT 鑒權。

總結

本示例以最小可行代碼展示了 Java Socket 雙向通信核心:ServerSocket.accept()、Socket.getInputStream/OutputStream、BufferedReader/PrintWriter 行文本處理、Swing 線程分離與安全更新。理解這套基礎模型,是進階學習 Netty、WebSocket、gRPC 等高性能網絡框架的必經之路。

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