天天看點

Java網絡程式設計-Socket程式設計初涉二(基于BIO模型的簡易多人聊天室)

推薦:​​Java網絡程式設計彙總​​

Java網絡程式設計-Socket程式設計初涉二(基于BIO模型的簡易多人聊天室)

要求

我們要實作一個基于BIO模型的簡易多人聊天室,不能像上一個版本一樣(​​Java網絡程式設計-Socket程式設計初涉一(簡易用戶端-伺服器)​​),當伺服器與某個用戶端連接配接成功後,它們在進行資料互動過程中,其他用戶端的連接配接請求,伺服器是不會響應的,我們這裡要使用BIO模型來改善這一點。

所謂多人聊天室,就是某個使用者發送的消息,其他使用者也是可以看見的。

什麼是BIO模型?

​​BIO模型與僞異步I/O模型​​

多人聊天室的時序圖

時序圖如下圖所示,先不糾結時序圖是否标準,等下解釋代碼時,可以與時序圖結合起來進行了解。

Java網絡程式設計-Socket程式設計初涉二(基于BIO模型的簡易多人聊天室)

代碼

伺服器代碼

先給出全部代碼,然後分子產品進行解釋。

​​

​ChatServer類​

​是多人聊天室的伺服器部分。

package bio.chatroom.server;

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;

public class ChatServer {

    private final int DEFAULT_PORT = 8888;
    private final String QUIT = "quit";

    private ServerSocket serverSocket;

    // 把用戶端的port當作用戶端的id
    private Map<Integer , Writer> connectedClients;

    public ChatServer(){
        connectedClients = new HashMap<>();
    }

    public synchronized void addClient(Socket socket) throws IOException {
        if(socket != null){
            int port = socket.getPort();
            BufferedWriter writer = new BufferedWriter(
                    new OutputStreamWriter(socket.getOutputStream())
            );
            connectedClients.put(port , writer);
            System.out.println("用戶端["+port+"]已連接配接到伺服器");
        }
    }

    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+"]已斷開連接配接");
            }
        }
    }

    public synchronized void forwardMessage(Socket socket , String fwdMsg) throws IOException {
        // 發送消息的端口
        int sendMessagePort = socket.getPort();
        for(Integer port : connectedClients.keySet()){
            if(!port.equals(sendMessagePort)){
                Writer writer = connectedClients.get(port);
                writer.write(fwdMsg);
                writer.flush();
            }
        }
    }

    public boolean readyToQuit(String msg){
        return QUIT.equalsIgnoreCase(msg);
    }

    public synchronized void close(){
        if(serverSocket != null){
            try {
                serverSocket.close();
                System.out.println("關閉了ServerSocket");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public void start(){
        try {
            // 建立ServerSocket,綁定和監聽端口
            serverSocket = new ServerSocket(DEFAULT_PORT);
            System.out.println("啟動伺服器,監聽端口"+DEFAULT_PORT+"...");

            while(true){
                // 等待用戶端連接配接
                Socket socket = serverSocket.accept();
                // 建立ChatHandler線程
                new Thread(new ChatHandler(this , socket)).start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            close();
        }
    }

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

子產品一

private final int DEFAULT_PORT = 8888;
    private final String QUIT = "quit";

    private ServerSocket serverSocket;

    // 把用戶端的port當作用戶端的id
    private Map<Integer , Writer> connectedClients;
    
    public ChatServer(){
        connectedClients = new HashMap<>();
    }      

​DEFAULT_PORT​

​​是伺服器建立的​

​ServerSocket​

​​需要綁定、監聽的端口,​

​QUIT​

​​是用于判斷使用者是否準備退出,​

​connectedClients​

​​相當于存儲線上使用者的容器(實際上并沒有存儲使用者),而是以使用者的​

​port​

​​為​

​key​

​​,以伺服器向該​

​port​

​​的使用者發送消息的​

​Writer​

​​為​

​value​

​​,組成的​

​Map​

​​,這樣友善伺服器轉發某個使用者的消息給其他使用者。

構造器​​

​ChatServer()​

​​要初始化​

​connectedClients​

​,因為伺服器啟動後,就可能馬上需要存儲上線的使用者。

子產品二

public synchronized void addClient(Socket socket) throws IOException {
        if(socket != null){
            int port = socket.getPort();
            BufferedWriter writer = new BufferedWriter(
                    new OutputStreamWriter(socket.getOutputStream())
            );
            connectedClients.put(port , writer);
            System.out.println("用戶端["+port+"]已連接配接到伺服器");
        }
    }

    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+"]已斷開連接配接");
            }
        }
    }

    public synchronized void forwardMessage(Socket socket , String fwdMsg) throws IOException {
        // 發送消息的端口
        int sendMessagePort = socket.getPort();
        for(Integer port : connectedClients.keySet()){
            if(!port.equals(sendMessagePort)){
                Writer writer = connectedClients.get(port);
                writer.write(fwdMsg);
                writer.flush();
            }
        }
    }

    public boolean readyToQuit(String msg){
        return QUIT.equalsIgnoreCase(msg);
    }

    public synchronized void close(){
        if(serverSocket != null){
            try {
                serverSocket.close();
                System.out.println("關閉了ServerSocket");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }      

這些方法的作用也正如方法名一樣。為了一定的線程安全,有些需要涉及增删改查的方法直接用​

​synchronized​

​修飾(我們現在不用過分糾結高性能、線程安全等問題)。

  • ​addClient()​

    ​:存儲新上線的使用者。
  • ​removeClient()​

    ​:移除退出的使用者。
  • ​forwardMessage()​

    ​:把某個使用者的消息轉發給其他的使用者。
  • ​readyToQuit()​

    ​:判斷使用者是否準備退出。
  • ​close()​

    ​:關閉資源。

具體實作應該比較簡單,相信大家都能看懂。

子產品三

public void start(){
        try {
            // 建立ServerSocket,綁定和監聽端口
            serverSocket = new ServerSocket(DEFAULT_PORT);
            System.out.println("啟動伺服器,監聽端口"+DEFAULT_PORT+"...");

            while(true){
                // 等待用戶端連接配接
                Socket socket = serverSocket.accept();
                // 建立ChatHandler線程
                new Thread(new ChatHandler(this , socket)).start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            close();
        }
    }

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

​start()​

​​方法是多人聊天室伺服器的核心。

其實邏輯也很簡單,看過​​Java網絡程式設計-Socket程式設計初涉一(簡易用戶端-伺服器)​​這篇部落格的,應該很容易看懂,這裡比之前不過是多建立了一個線程,基于BIO模型,新建立一個線程用于與使用者的資料互動,主線程依舊是等待其他使用者的連接配接請求,這也很好的改善了上一個版本的弊端。

new Thread(new ChatHandler(this , socket)).start();      

接下來看看​

​ChatHandler類​

​。

當伺服器與用戶端建立連接配接後,伺服器會建立一個線程,用來與用戶端進行資料互動,進而不幹擾主線程的任務(等待用戶端的連接配接請求),​

​ChatHandler類​

​​就是實作這個功能的類,是以實作了​

​Runnable接口​

​,友善用于線程的建立,這裡的邏輯也很簡單,主要實作以下三個功能:

  • 将新上線的使用者添加到存儲線上使用者的容器中。
  • 讀取使用者發送過來的消息,并将消息轉發給其他的使用者。
  • 等使用者退出後,将該使用者在存儲線上使用者的容器裡移除。
package bio.chatroom.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;
    private Socket socket;

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

    @Override
    public void run() {
        try {
            // 存儲新上線使用者
            server.addClient(socket);

            // 讀取使用者發送的消息
            BufferedReader reader = new BufferedReader(
                    new InputStreamReader(socket.getInputStream())
            );

            String msg = null;
            while((msg = reader.readLine()) != null){
                String fwdMsg = "用戶端["+socket.getPort()+"]:"+msg+"\n";
                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();
            }
        }
    }
}      

伺服器的代碼就全部完成了,其實很簡單吧,主要的邏輯就是下面三個:

  • 建立ServerSocket,綁定、監聽一個端口。
  • 伺服器主線程阻塞,并且等待使用者連接配接請求。
  • 有使用者連接配接後,就建立一個新的線程用于與該使用者進行資料互動,主線程依然等待使用者連接配接請求,新開的線程,會将使用者添加到存儲線上使用者的容器中,并且一直讀取使用者發送過來的消息,再将該消息轉發給其他使用者,直到該使用者退出,當該使用者退出時,就将該使用者在存儲線上使用者容器中移除,再關閉資源。

用戶端代碼

先給出全部代碼,然後分子產品進行解釋。

​​

​ChatClient類​

​是多人聊天室的用戶端部分。

package bio.chatroom.client;

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

public class ChatClient {

    private final String DEFAULT_SERVER_HOST = "127.0.0.1";
    private final int DEFAULT_PORT = 8888;
    private final String QUIT = "quit";

    private Socket socket;
    private BufferedReader reader;
    private BufferedWriter writer;

    // 發送消息給伺服器
    public void send(String msg) throws IOException {
        if(!socket.isOutputShutdown()){
            writer.write(msg+"\n");
            writer.flush();
        }
    }

    // 接收伺服器的消息
    public String receive() throws IOException {
        String msg = null;
        if(!socket.isInputShutdown()){
            msg = reader.readLine();
        }
        return msg;
    }

    // 檢查使用者是否準備退出
    public boolean readyToQuit(String msg){
        return QUIT.equalsIgnoreCase(msg);
    }

    public void close(){
        if(writer != null){
            try {
                writer.close();
                System.out.println("關閉socket");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public void start(){
        try {
            // 建立socket
            socket = new Socket(DEFAULT_SERVER_HOST , DEFAULT_PORT);

            // 建立IO流
            reader = new BufferedReader(
                    new InputStreamReader(socket.getInputStream())
            );
            writer = 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 client = new ChatClient();
        client.start();
    }
}      

子產品一

private final String DEFAULT_SERVER_HOST = "127.0.0.1";
    private final int DEFAULT_PORT = 8888;
    private final String QUIT = "quit";

    private Socket socket;
    private BufferedReader reader;
    private BufferedWriter writer;      

​DEFAULT_SERVER_HOST​

​​是伺服器​

​ip​

​​(本地),​

​DEFAULT_PORT​

​​是伺服器監聽的端口,用戶端根據這兩個屬性可以向伺服器發送連接配接請求。​

​QUIT​

​​的作用和在伺服器代碼中的作用一樣。​

​socket​

​​是用戶端與伺服器建立連接配接後建立的​

​Socket​

​​,​

​reader​

​​是用于用戶端讀取伺服器回複的消息,​

​writer​

​是用于用戶端向伺服器發送消息。

子產品二

// 發送消息給伺服器
    public void send(String msg) throws IOException {
        if(!socket.isOutputShutdown()){
            writer.write(msg+"\n");
            writer.flush();
        }
    }

    // 接收伺服器的消息
    public String receive() throws IOException {
        String msg = null;
        if(!socket.isInputShutdown()){
            msg = reader.readLine();
        }
        return msg;
    }

    // 檢查使用者是否準備退出
    public boolean readyToQuit(String msg){
        return QUIT.equalsIgnoreCase(msg);
    }

    public void close(){
        if(writer != null){
            try {
                writer.close();
                System.out.println("關閉socket");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }      

這些方法的作用也正如方法名一樣。

  • ​send()​

    ​:用戶端向伺服器發送消息。
  • ​receive()​

    ​:用戶端接收伺服器的消息。
  • ​readyToQuit()​

    ​:判斷使用者是否準備退出。
  • ​close()​

    ​:關閉資源。

實作也都非常簡單,就不多說了。

子產品三

public void start(){
        try {
            // 建立socket
            socket = new Socket(DEFAULT_SERVER_HOST , DEFAULT_PORT);

            // 建立IO流
            reader = new BufferedReader(
                    new InputStreamReader(socket.getInputStream())
            );
            writer = 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 client = new ChatClient();
        client.start();
    }      

和上一個版本的用戶端差不多,也是多建立了一個線程,用于監聽使用者在控制台的輸入,而主線程就用于接收伺服器的消息,主要邏輯如下:

  • 先與伺服器建立連接配接。
  • 建立與伺服器互動的IO流。
  • 建立新線程,用于監聽使用者的控制台輸入。
  • 接收伺服器的消息。

​UserInputHandler類​

​​,是用戶端建立線程用于監聽使用者的控制台輸入,是以它也實作了​

​Runnable接口​

​,友善建立線程,當它監聽到使用者在控制台輸入資訊後,會将使用者輸入的資訊發送給伺服器,伺服器當然也會将該資訊轉發給其他的使用者。它還會判斷使用者是否準備退出,當使用者退出後,這個線程也就差不多結束了,伺服器也會将存儲線上使用者的容器中移除該使用者。

package bio.chatroom.client;

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

public class UserInputHandler implements Runnable{

    private ChatClient client;

    public UserInputHandler(ChatClient client){
        this.client = client;
    }

    @Override
    public void run() {
        try {
            // 等待使用者輸入消息
            BufferedReader consoleReader = new BufferedReader(
                    new InputStreamReader(System.in)
            );

            while(true){
                String input = consoleReader.readLine();

                // 向伺服器發送消息
                client.send(input);

                //檢查使用者是否準備退出
                if(client.readyToQuit(input)){
                    break;
                }
            }
        } catch (IOException e){
            e.printStackTrace();
        }
    }
}      

這裡我們便完成了一個基于BIO模型的簡易多人聊天室,大家可以自己去實作一下。

測試

Java網絡程式設計-Socket程式設計初涉二(基于BIO模型的簡易多人聊天室)
Java網絡程式設計-Socket程式設計初涉二(基于BIO模型的簡易多人聊天室)
Java網絡程式設計-Socket程式設計初涉二(基于BIO模型的簡易多人聊天室)