【Java网络编程】基于BIO/NIO/AIO的多人聊天室(二):BIO聊天室

课程《一站式学习Java网络编程 全面理解BIO/NIO/AIO》的学习笔记(二):
BIO编程模型 & 基于BIO的多人聊天室实现

源码地址:https://github.com/NoxWang/web-program

【Java网络编程】基于BIO/NIO/AIO的多人聊天室(一):java IO与内核IO
【Java网络编程】基于BIO/NIO/AIO的多人聊天室(三):NIO概述与实践
【Java网络编程】基于BIO/NIO/AIO的多人聊天室(四):NIO聊天室
【Java网络编程】基于BIO/NIO/AIO的多人聊天室(五):AIO聊天室
【Java网络编程】基于BIO/NIO/AIO的多人聊天室(六):思维导图

一、BIO编程模型

BIO:阻塞式IO

  • 线程 Acceptor 负责接收客户端发起的连接请求
  • 每当有新客户端连接,服务器都为其创建一个新线程Handler,负责处理与该客户端相关的输入输出操作
    • 创建新线程的原因:如果直接在线程 Acceptor 中处理客户端的输入输出操作,则服务器无法再接收其他客户端的连接请求
      BIO编程模型

二、基于BIO的多人聊天室设计

2.1 需求分析

  • 基于BIO模型
  • 支持多人同时在线
  • 每个用户的发言都被转发给其他在线用户

2.2 聊天室设计

2.2.1 服务器端

  1. 需要一个 Acceptor 线程,使用 ServerSocket 的 accept() 函数阻塞式地接受客户端连接
  2. 为每一个连接进来的客户端新建一个线程,该线程需要实现两个功能:1)接收该客户端发来的信息;2)将该信息转发给其他客户端
  3. 转发功能要求客户端能够存储当前在线的所有客户端列表

2.2.2 客户端

  1. 可以和服务端建立连接
  2. 客户端需要完成两个功能:1)接收用户在控制台的输入;2)读取其他客户端发送来的消息
  3. 由于等待用户输入的过程是阻塞的,所以客户端也需要两条线程,分别完成上述两个功能

2.2.3 BIO的体现

  1. ServerSocket.accept():该方法为阻塞式调用,在有新客户端连接进来之前一直阻塞
  2. InputStream.read() & OutputStream.read():这两个方法均为阻塞式调用,在用户输入消息前,read()方法会一直阻塞,因此需要为每个客户端都创建一个线程

三、基于BIO的多人聊天室实现

代码结构如下:

  • server
    • ChatServer.java:服务端的主线程,负责启动服务端、接收客户端请求、存储当前在线客户端、为客户端新建处理线程等
    • ChatHandler.java:处理和客户端之间的输入输出工作
  • client
    • ChatClient.java:客户端的主线程,负责连接服务端,读取服务端转发来的其他客户端信息
    • UserInputHandler.java:等待和发送用户输入

代码结构

3.1 服务端

3.1.1 ChatServer.java

服务端的主线程,负责启动服务端、接收客户端请求、存储当前在线客户端、为客户端新建处理线程等

package 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 int SERVER_PORT = 8888;

    /** 客户端退出标志(客户端发送\quit表示退出聊天室 */
    private final String QUIT = "\\quit";

    /** 服务端 Socket */
    private ServerSocket serverSocket;

    /**
     * 存储已连接的客户端
     * key:客户端的端口号
     * value:向该端口发信息所使用的 Writer
     */
    private Map<Integer, Writer> connectedClients;

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

    /**
     * 添加新在线客户端
     * @param socket 新增客户端的socket
     * @throws IOException
     */
    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 + "]已连接到服务器");
        }
    }

    /**
     * 移除已下线客户端
     * @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 + "]已断开连接");
        }
    }

    /**
     * 转发信息给其他所有在线客户端
     * @param 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();
            }
        }
    }

    /**
     * 服务端主要逻辑
     */
    public void start() {
        try {
            // 为服务端绑定端口
            serverSocket = new ServerSocket(SERVER_PORT);
            System.out.println("服务器启动,监听端口:" + SERVER_PORT + "...");

            while (true) {
                // accept()方法是阻塞式的
                Socket socket = serverSocket.accept();

                // 有客户端连接后,为它创建一个ChatHandler线程
                new Thread(new ChatHandler(this, socket)).start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            close();
        }
    }

    /**
     * 判断客户端是否准备退出
     * @param msg 客户端发送的消息
     * @return true:准备退出
     */
    public boolean readyToQuit(String msg) {
        return QUIT.equals(msg);
    }

    /**
     * 关闭服务器
     */
    public synchronized void close() {
        if (serverSocket != null) {
            try {
                serverSocket.close();
                System.out.println("服务器关闭");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

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

可使用线程池对服务器端进行进一步的改进,限制可以进入聊天室的客户端个数,避免为用户开启的线程过多。改进后的部分代码如下:

...

public class ChatServer {

    ...

    /** 线程池 */
    private ExecutorService executorService;

    public ChatServer(int threadNum) {
        // 创建线程池
        executorService = Executors.newFixedThreadPool(threadNum);
        connectedClient = new HashMap<>();
    }

    ...

    /**
     * 服务端主要逻辑
     */
    public void start() {
        try {
            // 绑定监听端口
            serverSocket = new ServerSocket(SERVER_PORT);
            System.out.println("服务器启动,监听端口" + SERVER_PORT + "...");

            while (true) {
                // 等待客户端连接
                Socket socket = serverSocket.accept();

                // 向线程池提交任务
                executorService.execute(new ChatHandler(this, socket));
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            close();
        }
    }

    public static void main(String[] args) {
    	// 创建服务器类时传入允许的最大在线用户数
        ChatServer chatServer = new ChatServer(3);
        chatServer.start();
    }
}

3.1.2 ChatHandler.java

每当有新客户端连接进服务器时,服务器均会为其创建一个ChatHandler线程,处理服务器和客户端之间的输入输出工作

package 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;

    /** 当前客户端 Socket */
    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) {
                // 检查用户是否退出
                if (server.readyToQuit(msg)) {
                    break;
                }

                String fwdMsg = "客户端[" + socket.getPort() + "]:" + msg + "\n";
                System.out.print(fwdMsg);

                // 转发消息至其他在线用户
                server.forwardMessage(socket, fwdMsg);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                server.removeClient(socket);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

3.2 客户端

3.2.1 ChatClient.java

客户端的主线程,负责连接服务端,读取服务端转发来的其他客户端信息

package client;

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

public class ChatClient {

    /** 服务器 IP */
    private final String SERVER_HOST = "127.0.0.1";

    /** 服务器监听端口号 */
    private final int SERVER_PORT = 8888;

    /** 客户端退出命令 */
    private final String QUIT = "\\quit";

    /** 客户端 Socket */
    private Socket socket;

    /** 从服务端读取信息的 Reader */
    private BufferedReader reader;

    /** 向服务端发送消息的 Writer */
    private BufferedWriter writer;

    /**
     * 发送消息给服务器
     * @param msg
     * @throws IOException
     */
    public void send(String msg) throws IOException {
        // 确定输出流没有被关闭
        if (!socket.isOutputShutdown()) {
            writer.write(msg + "\n");
            writer.flush();
        }
    }

    /**
     * 检查用户是否准备退出
     */
    public boolean readyToQuit(String msg) {
        return QUIT.equals(msg);
    }

    /**
     * 关闭服务器
     */
    public void close() {
        if (writer != null) {
            try {
                writer.close();
                System.out.println("关闭客户端");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public void start() {
        try {
            // 创建socket
            socket = new Socket(SERVER_HOST, SERVER_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 = reader.readLine()) != null) {
                System.out.println(msg);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            close();
        }
    }

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

3.2.2 UserInputHandler.java

需要为每一个客户端进程都创建一个UserInputHandler线程,负责等待和发送用户输入

package 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;
    }

    @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();
        }
    }
}

3.3 运行结果

  1. 启动服务器和三个服务端进程(左上角为服务器,注意各个客户端是不同的端口):
    启动服务器和客户端

  2. 三个客户端可各自发送消息,并接受由服务器转发的其他客户端的消息
    多人群聊

  3. 某客户端发送“\quit”后退出聊天室(右下角)
    客户端退出

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值