javaBIO之实现一个聊天室(2)

上篇简单实现了回音壁,但是这也太不够意思了,如果可以多人发言那就稍微有点意思了。
这里先来说说java BIO 编程模型。
BIO也就是我们经常说的java IO,在这种传统的模式下,如果要实现服务端服务,
1.首先他要等待客户端连接吧?
执行serverSocket.accept();会一直阻塞在这句话这里,直到有客户端连接才会做其他的事情。
2.那么如何做到服务多个客户端呢?
我们采用新开一个线程来处理与这个客户端的交互,在这里,我采用ChatHandler来负责处理与客户端的交互,之后主线程继续在一个循环里面等待新的客户端连接。
咳咳,书上是这么说的,我也是这么做的。
首先定义IP地址 和要监听的端口:

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

那么如果真的有多个用户,如何保存呢?这里采用最简单粗暴的map接口存储,以客户端的端口为Key,客户端为value值。

private Map<Integer,Writer> connectedClients;
在构造函数里面,主要负责初始化。
public ChatServer(){
 connectedClients = new HashMap<>();
 }

启动函数非常的简单,就是等待客户端连接,如果有就分配一个线程启动ChatHandler来处理,否则就继续的等待。

 public void start(){
        try {
            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();
        }
    }

在ChatHandler主要做的事情如下 :
将这个客户端加入我们的聊天室内,也就是map中,接收这个客户端发来的消息 转发给其他的客户端,并且判断客户端发来的消息是不是表示要退出,如果退出,则ChatHandler结束服务。

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;
            while ((msg = reader.readLine())!=null){
                System.out.println("客户端【" +socket.getPort()+"】:"+msg);
                //转发消息给其他的在线用户
                server.forwardMsg(socket,"客户端【" +socket.getPort()+"】:"+msg+"\n");
                //检查用户发送的消息是不是下线消息
                if (server.readToQuit(msg)){
                    break;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            //不管怎么了,也是要移除这个掉线用户的
            try {
                server.removeClient(socket);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

暂时在这里,addClient,forwardMsg,removeClient都是暂时还没有实现的方法。
这里回过头来,来实现以上没有实现的方法。
添加客户端的方法:

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 forwardMsg(Socket socket,String fwdMsg) throws IOException {
        for (Integer port : connectedClients.keySet()) {
            if (!port.equals(socket.getPort())){
                Writer writer = connectedClients.get(port);
                writer.write(fwdMsg);
                writer.flush();
            }
        }
    }

服务端的代码大概就是 如此,那么客户端呢?

客户端的实现比较简单,既然服务端是检测客户端的输入流输出流,那么客户端只要关注操作这两个就好了。

 private final String DEFAULT_SERVER_HOST = "127.0.0.1";
    private final int DEFAULT_SERVER_PORT = 8888;
    private final String QUIT = "quit";
    private Socket socket;
    private BufferedReader reader;
    private BufferedWriter writer;

同理,在客户端等待用户的输入,也是一个阻塞调用,我们总不能一个客户端就等着用户输入,啥也不做吧?这里同样的是采用一个新开线程UserInputHandler来监听用户的输入。
所以start函数,要做的事情就这么多。

 public void start(){
        try {
            socket = new Socket(DEFAULT_SERVER_HOST,DEFAULT_SERVER_PORT);
            //创建IO流
            reader = new BufferedReader(
                    new InputStreamReader(socket.getInputStream())
            );
            writer = new BufferedWriter(
                    new OutputStreamWriter(socket.getOutputStream())
            );

            //处理用户的输入 TODO
            new Thread(new UserInputHandler(this)).start();
            //读取服务器转发的消息
            String msg = null;
            while ((msg = receive()) !=null){
                System.out.println(msg);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            close();
        }
    }

UserInputHandler里面不仅仅监听用户的输入,还要负责将消息发送给服务器。
同时还要判断用户是否要退出客户端。

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

接下来实现还没有完成的代码:
receive方法:接收服务器的消息

 public String receive() throws IOException {
        String msg = null;
        if (!socket.isInputShutdown()){
            msg = reader.readLine();
        }
        return msg;
    }

send方法,发消息给服务器:

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

主要代码就是这样,可以改进的地方可以使用线程池代替单独创建线程。
跑起试试:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
附赠BIO编程模型一张图:
在这里插入图片描述
下篇:JavaIO之NIO,BIO的复制文件的简单比较(3)

发布了11 篇原创文章 · 获赞 1 · 访问量 605

猜你喜欢

转载自blog.csdn.net/Fujie1997/article/details/104760672
今日推荐