Java BIO 實現多人聊天室

1. 前言

關於Socket和ServerSocket的知識可以看:Java Socket 通信

2. 功能需求

2.1 服務器端

  • 基於線程池服務器端可以同時與5個客戶端保持通信
  • 監聽客戶端連接,並創建獨立線程保持與客戶端的通信
  • 監聽客戶端消息,並轉發給聊天室裏的其他在線用戶
  • 通過HashMap存儲客戶端的信息

2.2 客戶端

  • 連接服務器,並創建獨立線程保持與服務器的通信(主要用於處理用戶輸入)
  • 發送消息給服務器,接收服務器發送的消息

3. 程序架構圖

在這裏插入圖片描述

4. 程序源代碼

4.1 服務器端

ChatServer類:

package bio.server;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ChatServer {
    private int DEFAULT_PORT = 8888;//默認端口
    private final String QUIT = "quit";//退出指令
    private ExecutorService executorService;
    private ServerSocket serverSocket;
    private Map<Integer, Writer> connectedClients;//存儲客戶端信息

    public ChatServer(){
        executorService = Executors.newFixedThreadPool(5);//創建線程池
        connectedClients = new HashMap<>();
    }

    /**
     * 客戶端連接,添加一個客戶端
     * synchronized 保證線程的安全性,防止多個線程同時添加客戶端
     * @param socket 客戶端的socket
     * @throws IOException
     */
    public synchronized void addClient(Socket socket) throws IOException {
        if(socket != null){
            int port = socket.getPort();
            BufferedWriter bufferedWriter = new BufferedWriter(
                    new OutputStreamWriter(socket.getOutputStream())
            );
            connectedClients.put(port, bufferedWriter);
            System.out.println("客戶端["+port+"]已連接到服務器");
        }
    }
    /**
     *  客戶端斷開連接,從列表中刪除客戶端
     *  synchronized 保證線程安全性, 防止多個線程同時刪除客戶端
      * @param socket 斷開連接的客戶端socket
     * @throws IOException
     */
    public synchronized void removeClient(Socket socket) throws IOException {
        if(socket != null){
            int port = socket.getPort();
            if(connectedClients.containsKey(port)){
                connectedClients.get(port).close();
            }
            connectedClients.remove(port);
            System.out.println("客戶端["+port+"]已斷開連接");
        }
    }
    /**
     * 向服務器中其他的客戶端轉發消息,除了消息本身的擁有者
     * synchronized 保證線程安全性,防止多個線程同時去轉發消息,訪問connectedClients
     * @param socket 消息所屬的客戶端的socket
     * @param fwdMsg 消息的具體內容
     * @throws IOException
     */
    public synchronized void forwardMessage(Socket socket, String fwdMsg) throws IOException {
        for(Integer id : connectedClients.keySet()){
            if(!id.equals(socket.getPort())){
                Writer writer = connectedClients.get(id);
                writer.write(fwdMsg);
                writer.flush();
            }
        }
    }
    /**
     * 如果用戶發送quit則準備退出
     * @param msg 用戶返送的消息
     * @return
     */
    public boolean readyToQuit(String msg){
        return QUIT.equals(msg);
    }
    /**
     * synchronized 保護serverSocket的狀態
     * 關閉serverSocket
     */
    public synchronized void close(){
        if(serverSocket != null){
            try {
                serverSocket.close();
                System.out.println("關閉serverSocket");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    /**
     * 開啓服務器,監聽端口,並且啓動ChatHandler線程
     */
    public void start(){

        try {
            // 綁定監聽端口
            serverSocket = new ServerSocket(DEFAULT_PORT);
            System.out.println("啓動服務器,監聽端口:" + DEFAULT_PORT + "...");

            while(true){
                //等待客戶端連接
                Socket socket = serverSocket.accept();
                //創建額外的ChatHandler線程
                executorService.execute(new ChatHandler(this, socket));
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            close();
        }

    }

    public static void main(String[] args) {
        ChatServer chatServer = new ChatServer();
        chatServer.start();
    }
}

ChatHandler類:

package bio.server;

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

public class ChatHandler implements Runnable{

    private ChatServer server; //用來操作服務器端的connectedClients
    private Socket socket;

    public ChatHandler(ChatServer server, Socket socket){
        this.server = server;
        this.socket = socket;
    }

    /**
     * 用於對客戶端進行服務
     */
    @Override
    public void run() {
        try {
            //存儲新上線用戶
            server.addClient(socket);
            //讀取用戶發送的消息
            BufferedReader bufferedReader = new BufferedReader(
                    new InputStreamReader(socket.getInputStream())
            );
            String msg = null;
            while((msg = bufferedReader.readLine()) != null){
                String fwdMsg = "客戶端["+socket.getPort() +"]:" + msg + "\n";//readLine()函數讀取需要加換行符
                System.out.print(fwdMsg);
                //將收到的消息轉發給聊天室裏在線的其他用戶
                server.forwardMessage(socket, fwdMsg);
                //檢查用戶是否準備退出
                if(server.readyToQuit(msg)){
                    break;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                server.removeClient(socket);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

4.2 客戶端

ChatClient類:

package bio.client;

import java.io.*;
import java.net.Socket;

public class ChatClient {

    private final String QUIT = "quit";
    private final String DEFAULT_SERVER_HOST = "127.0.0.1";
    private final int DEFAULT_SERVER_PORT = 8888;
    private Socket socket;
    private BufferedReader bufferedReader = null;
    private BufferedWriter bufferedWriter = null;

    /**
     * 發送消息給服務器
     * @param msg 用戶發送的消息
     * @throws IOException
     */
    public void send(String msg) throws IOException {
        if(!socket.isOutputShutdown()){//確定socket的輸出流未關閉
            bufferedWriter.write(msg + "\n");
            bufferedWriter.flush();
        }
    }

    /**
     * 從服務器接收消息
     * @return
     * @throws IOException
     */
    public String receive() throws IOException {
        String msg = null;
        if(!socket.isInputShutdown()){//確定socket的輸入流未關閉
            msg = bufferedReader.readLine();
        }
        return msg;
    }

    /**
     * 檢查用戶是否準備退出
     * @param msg 用戶發送的消息
     * @return
     */
    public boolean readyToQuit(String msg){
        return QUIT.equals(msg);
    }

    /**
     * 關閉socket
     */
    public void close(){
       if(bufferedWriter != null){
           try {//關閉bufferedWriter的同時也關閉socket
               System.out.println("關閉socket");
               bufferedWriter.close();
           } catch (IOException e) {
               e.printStackTrace();
           }
       }
    }
    /**
     * 創建實例、IO流、處理用戶輸入以及讀取服務器轉發的消息
     */
    public void start(){
        try {
            //創建socket實例
            socket = new Socket(DEFAULT_SERVER_HOST, DEFAULT_SERVER_PORT);
            //創建IO流
            bufferedReader = new BufferedReader(
                    new InputStreamReader(socket.getInputStream())
            );
            bufferedWriter = new BufferedWriter(
                    new OutputStreamWriter(socket.getOutputStream())
            );
            //創建額外的線程處理用戶的輸入
            new Thread(new UserInputHandler(this)).start();
            //讀取服務器轉發的消息
            String msg = null;
            while((msg = receive()) != null){
                System.out.println(msg);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            close();
        }
    }

    public static void main(String[] args) {
        ChatClient chatClient = new ChatClient();
        chatClient.start();
    }
}

UserInputHandler類:

package bio.client;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class UserInputHandler implements Runnable{

    private ChatClient chatclient;
    public UserInputHandler(ChatClient chatClient){
        this.chatclient = chatClient;
    }

    public UserInputHandler(nio.test.client.ChatClient chatClient) {
    }

    /**
     *
     */
    @Override
    public void run() {
        try {
            //等待用戶輸入的消息
            BufferedReader consoleReader = new BufferedReader(
                    new InputStreamReader(System.in)
            );
            while(true){
                String input = consoleReader.readLine();
                //向服務器發送消息
                chatclient.send(input);
                //檢查用戶是否準備退出
                if(chatclient.readyToQuit(input)){
                    break;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

5. 執行效果圖

在這裏插入圖片描述

在這裏插入圖片描述

在這裏插入圖片描述

在這裏插入圖片描述

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