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. 执行效果图

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/RivenDong/article/details/102785696