Java Socket+多線程 實現簡易聊天室(註釋版)

代碼基礎參考鏈接,十分感謝。


需求功能:

  1. 實現客戶端與服務器的連接
  2. 各個客戶端能夠共享消息界面,即一個客戶端發送消息後所有在線客戶端都能夠收到
  3. 客戶端登錄時可以自定義暱稱
  4. 客戶端登錄後顯示已在線成員
  5. 客戶端登錄後通知其他在線成員,下線後也通知
  6. 客戶端登錄後顯示之前的聊天記錄
  7. 服務器斷開後能通知各客戶端重啓
  8. 啓動客戶端時若服務器未打開顯示提示信息
  9. 客戶端退出後服務器能夠提示,登錄同

 

核心思想:

將服務器作爲轉接的中間站,用集合存儲鏈接的socket、記錄和在線成員。

 

原參考代碼會出現的主要問題:

任意關閉一個客戶端或關閉服務器時會拋出 java.net.SocketException: Connection reset 的異常,原因是:一端退出,但退出時並未關閉該連接,另一端如果在從連接中讀數據則拋出該異常。因此此處的解決方法爲在ServerThreadbuf讀取數據時加個try-catch塊,有異常後進行相應的處理。

 

運行截圖:

 

代碼部分:

Server.java  服務器

package ChatRoomDemo;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;

public class Server {

    public static List<Socket> list = new ArrayList<>();            // 客戶端連接
    public static List<String> record = new ArrayList<>();          // 聊天記錄
    public static List<String> online_member = new ArrayList<>();   // 在線成員
    private static ServerSocket server;

    public static void main(String[] args) {
        try {
            server = new ServerSocket(4233);
            System.out.println("Chatroom is opening!");
            while(true) {
                Socket socket = server.accept();
                list.add(socket);
                new Thread(new ServerThread(socket)).start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

ServerThread.java  服務器線程

package ChatRoomDemo;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;

public class ServerThread implements Runnable {

    private final Socket socket;

    public ServerThread(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        try {
            BufferedReader buf = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            PrintWriter pw = new PrintWriter(socket.getOutputStream());

            // 向客戶端提示輸入暱稱
            pw.println("Please enter your nickname:");
            pw.flush();

            // 讀取客戶端發送的暱稱,並在服務器提示上線
            String nickname = buf.readLine();
            System.out.println(nickname + " is online");

            // 給每個在線的客戶端發送該客戶端上線記錄
            for(Socket r : Server.list) {
                if(!r.equals(this.socket)) {
                    pw = new PrintWriter(r.getOutputStream());
                    pw.println(nickname + " is online");
                    pw.flush();
                }
                else {
                    pw = new PrintWriter(r.getOutputStream());
                    pw.println("Welcome " + nickname);
                    pw.flush();
                }
            }

            // 在該客戶端顯示其他已上線的成員,並將自己添加進去
            for(String s : Server.online_member) {
                pw.println(s + " is online");
                pw.flush();
            }
            Server.online_member.add(nickname);

            // 在該客戶端顯示聊天記錄
            pw = new PrintWriter(socket.getOutputStream());
            for(String s : Server.record) {
                pw.println(s);
                pw.flush();
            }

            // 自己聊天的部分
            while(true) {
                String str;
                try {
                    // 讀取客戶端發送的聊天信息,並記錄
                    str = buf.readLine();
                    Server.record.add(nickname + ":" + str);

                    // 若正確讀取聊天信息,給所有在線成員刷新該信息
                    for(Socket r : Server.list) {
                        pw = new PrintWriter(r.getOutputStream());
                        pw.println(nickname + ":" + str);
                        pw.flush();
                    }
                } catch (Exception e) {

                    // 客戶端關閉後
                    System.out.println(nickname + " is offline");
                    Server.list.remove(socket);
                    Server.online_member.remove(nickname);

                    // 通知其他客戶端該成員已下線
                    for(Socket r : Server.list) {
                        pw = new PrintWriter(r.getOutputStream());
                        pw.println(nickname + " is offline");
                        pw.flush();
                    }

                    break;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

Link.java  客戶端連接並運行

package ChatRoomDemo;

import java.io.IOException;
import java.net.Socket;

public class Link {
    public static void linkstart() {
        try {
            Socket socket = new Socket("localhost", 4233);
            System.out.println("Connect successfully!");
            new Thread(new ClientThread1(socket)).start(); // 將信息發送給服務器的線程
            new Thread(new ClientThread2(socket)).start(); // 從服務器讀取信息的線程
        } catch (IOException e) {

            // 若服務器未開啓
            System.out.println("Server is closed, please try again later");
        }
    }
}

ClientThread1.java  客戶端發送消息進程

package ChatRoomDemo;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.text.SimpleDateFormat;
import java.util.Date;

/*
Send message to server
 */

public class ClientThread1 implements Runnable{

    private Socket socket;

    public ClientThread1(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        try {
            BufferedReader buf = new BufferedReader(new InputStreamReader(System.in));
            PrintWriter pw = new PrintWriter(socket.getOutputStream());

            // 輸入暱稱,併發送給服務器
            String nickname = buf.readLine();
            pw.println(nickname);
            pw.flush();

            // 發送聊天信息
            while(true) {
                if(socket.isClosed()) break;
                String str = buf.readLine();
                String date = new SimpleDateFormat("HH:mm:ss").format(new Date());  // 時間
                pw.println(str + "        " + date);
                pw.flush();
            }

            buf.close();
            pw.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

ClientThread2.java  客戶端接收消息進程

package ChatRoomDemo;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.Socket;

/*
Get message from server
 */

public class ClientThread2 implements Runnable{
    private Socket socket;
    private BufferedReader buf = null;

    public ClientThread2(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        try {
            buf = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            while(true) {
                try {
                    String str = buf.readLine();
                    if(str!=null) System.out.println(str);
                } catch (Exception e) {

                    // 服務器關閉
                    System.out.println("Server is closed, please try to restart");
                    break;
                }
            }

            buf.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Client1.java 同所有Client

package ChatRoomDemo;

public class Client1 {

    public static void main(String[] args) {
        Link.linkstart();
    }
}

 

遺留待解決問題:

  1. 輸入暱稱時未阻塞,其他客戶端發信息時會直接顯示(應該是把設定暱稱的操作放到線程外,但有點不美觀就pass了..)
  2. 服務器斷開後客戶端收到提示但要手動關閉(發送信息的線程還在運作,暫時沒能成功判斷socket已失效)
  3. 聊天記錄顯示問題,本來想實現時間在上語句在下再接一行空行,會被各種喫換行符還是先pass了,只好弄了個殘疾版
  4. ServeThread中若在死循環退出後直接close bufpw,會導致一個客戶端的退出會讓所有服務器線程一起報錯,即一個退出全都退出。沒想到原因,希望有大佬解惑。

 

小注意點:不要在一個文件中看到socket用完就close,我就因爲在ClientThread中直接finally close導致debug了一下午(ry

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章