Java网络编程-Socket编程初涉二(基于BIO模型的简易多人聊天室)

Java网络编程-Socket编程初涉二(基于BIO模型的简易多人聊天室)

要求

我们要实现一个基于BIO模型的简易多人聊天室,不能像上一个版本一样(Java网络编程-Socket编程初涉一(简易客户端-服务器)),当服务器与某个客户端连接成功后,它们在进行数据交互过程中,其他客户端的连接请求,服务器是不会响应的,我们这里要使用BIO模型来改善这一点。

所谓多人聊天室,就是某个用户发送的消息,其他用户也是可以看见的。

什么是BIO模型?

BIO模型与伪异步I/O模型

多人聊天室的时序图

时序图如下图所示,先不纠结时序图是否标准,等下解释代码时,可以与时序图结合起来进行理解。
在这里插入图片描述
代码

服务器代码

先给出全部代码,然后分模块进行解释。
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相当于存储在线用户的容器(实际上并没有存储用户),而是以用户的portkey,以服务器向该port的用户发送消息的Writervalue,组成的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是客户端与服务器建立连接后创建的Socketreader是用于客户端读取服务器回复的消息,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模型的简易多人聊天室,大家可以自己去实现一下。

测试
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
测试是没问题的。

如果有说错的地方,请大家不吝赐教(记得留言哦~~~~)。

发布了288 篇原创文章 · 获赞 325 · 访问量 65万+

猜你喜欢

转载自blog.csdn.net/qq_37960603/article/details/104157914