手动搭建I/O网络通信框架(二):BIO编程模型实现群聊

手动搭建I/O网络通信框架(一):Socket和ServerSocket实现单聊
本节——手动搭建I/O网络通信框架(二):BIO编程模型实现群聊

手动搭建I/O网络通信框架(三):NIO编程模型升级改造聊天室
所谓BIO,就是Block IO,阻塞式的IO。这个阻塞主要发生在:ServerSocket接收请求时(accept()方法)、InputStream、OutputStream(输入输出流的读和写)都是阻塞的。这个可以在下面代码的调试中发现,比如在客户端接收服务器消息的输入流处打上断点,除非服务器发来消息,不然断点是一直停在这个地方的。也就是说这个线程在这时间是被阻塞的。

在这里插入图片描述
如图:当一个客户端请求进来时,接收器会为这个客户端分配一个工作线程,这个工作线程专职处理客户端的操作。在上一章中,服务器接收到客户端请求后就跑去专门服务这个客户端了,所以当其他请求进来时,是处理不到的。

看到这个图,很容易就会想到线程池,BIO是一个相对简单的模型,实现它的关键之处也在于线程池

在上代码之前,先大概说清楚每个类的作用,以免弄混淆。更详细的说明,都写在注释当中。

服务器端:
**ChatServer:**这个类的作用就像图中的Acceptor。它有两个比较关键的全局变量,一个就是存储在线用户信息的Map,一个就是线程池。这个类会监听端口,接收客户端的请求,然后为客户端分配工作线程。还会提供一些常用的工具方法给每个工作线程调用,比如:发送消息、添加在线用户等。

**ChatHandler:**这个类就是工作线程的类。在这个项目中,它的工作很简单:把接收到的消息转发给其他客户端,当然还有一些小功能,比如添加\移除在线用户。
  
客户端:
**ChatClient:**客户端启动类,也就是主线程,会通过Socket和服务器连接。也提供了两个工具方法:发送消息和接收消息。
**UserInputHandler:**专门负责等待用户输入信息的线程,一旦有信息键入,就马上发送给服务器。

服务端ChatServer


/**
 * @Package com.csdn.bio
 * @ClassName ChatServer
 * @Description TODO
 * @Author LangShengJie
 * @Date Created in 2020/11/24 10:39
 */
public class ChatServer {
    private int DEFAULT_PORT = 8888;
    /**
     * 创建一个Map存储在线用户信息。map可以统计在线用户,针对这些用户转发其他用户发送的消息。
     * 因为会有多个线程操作这个map,所以为了安全起见用ConcurrentHashMap
     * 在这里key就是客户端的端口号,但是实际中肯定不会用端口号区分用户,如果是web的话一般用session。
     * value是IO的Writer,用来存储客户端发送的消息。
     */

    private Map<Integer, Writer> map =  new ConcurrentHashMap<Integer, Writer>();

    /**
     * 创建线程池,线程上限为10个,如果第11个客户端请求进来,服务器会接收但是不会去分配线程处理它。
     * 前10个客户端的聊天记录,它看不见。当有一个客户端下线时,这第11个客户端就会被分配线程,服务器显示在线
     * 大家可以把10再设置小一点,测试看看
     * */

    private ExecutorService executorService= Executors.newFixedThreadPool(10);

    //客户端连接时往map添加客户端
    public void  addClient(Socket socket) throws IOException {
        if (socket!=null){
            BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
            map.put(socket.getPort(),writer);
            System.out.println(socket.getPort()+"已连接");
        }
    }
    //断开连接时map里移除客户端
    public void removeClient(Socket socket) throws Exception {
        if (socket != null) {
            if (map.containsKey(socket.getPort())) {
                map.get(socket.getPort()).close();
                map.remove(socket.getPort());
            }
            System.out.println(socket.getPort() + "断开连接");
        }
    }
    //转发客户端消息,这个方法就是把消息发送给在线的其他的所有客户端
    public void sendMessage(Socket socket, String msg) throws IOException {
        //遍历在线客户端
        for (Integer port : map.keySet()) {
            //发送给在线的其他客户端
            if (port != socket.getPort()) {
                Writer writer = map.get(port);
                writer.write(msg);
                writer.flush();
            }
        }
    }
    //接收客户端请求,并分配Handler去处理请求
    public void start() {
        try (ServerSocket serverSocket = new ServerSocket(DEFAULT_PORT)) {
            System.out.println("Server Start,The Port is:"+DEFAULT_PORT);
            while (true){
                //等待客户端连接
                Socket socket=serverSocket.accept();
                //为客户端分配一个ChatHandler线程
                executorService.execute(new ChatHandler(this,socket));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

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

服务端ChatHandler

/**
 * @Package com.csdn.bio
 * @ClassName ChatHandler
 * @Description TODO
 * @Author LangShengJie
 * @Date Created in 2020/11/24 10:52
 */
public class ChatHandler implements Runnable {
    private ChatServer server;
    private Socket socket;
    //构造函数,ChatServer通过这个分配Handler线程
    public ChatHandler(ChatServer server, Socket socket) {
        this.server = server;
        this.socket = socket;
    }
    @Override
    public void run() {
        try {
            //往map里添加客户端
            server.addClient(socket);
            //读取此客户端发送的消息
            BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            String msg = null;
            while ((msg = reader.readLine())!=null){
                //拼接一个端口号,方便看是那个客户端发送的消息
                String sendmsg  = socket.getPort()+":"+msg;
                //打印消息
                System.out.println(sendmsg);
                //将收到的消息转发给其他在线客户端
                server.sendMessage(socket,sendmsg+"\n");
                if (msg.equals("quit")) {
                    break;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
                try {
                    server.removeClient(socket);
                } catch (Exception e) {
                    e.printStackTrace();
                }
        }
    }
}

客户端ChatClient

/**
 * @Package com.csdn.bio
 * @ClassName ChatClient
 * @Description TODO
 * @Author LangShengJie
 * @Date Created in 2020/11/24 11:00
 */
public class ChatClient {
    private BufferedReader reader;
    private BufferedWriter writer;
    private Socket socket;
    //给服务端发送消息
    public void sendToServer(String msg) throws IOException {
        //发送之前,判断socket的输出流是否关闭
        if (!socket.isOutputShutdown()) {
            //如果没有关闭就把用户键入的消息放到writer里面
            writer.write(msg + "\n");
            writer.flush();
        }
    }
    //从服务器接收消息
    public String receive() throws IOException {
        String msg = null;
        //判断socket的输入流是否关闭
        if (!socket.isInputShutdown()) {
            //没有关闭的话就可以通过reader读取服务器发送来的消息。注意:如果没有读取到消息线程会阻塞在这里
            msg = reader.readLine();
        }
        return msg;
    }
    public void start() {
        //和服务创建连接
        try {
            socket = new Socket("127.0.0.1", 8888);
            reader=new BufferedReader(
                    new InputStreamReader(socket.getInputStream())
            );
            writer=new BufferedWriter(
                    new OutputStreamWriter(socket.getOutputStream())
            );
            //新建一个线程去监听用户输入的消息
            new Thread(new UserInputHandler(this)).start();
            /**
             * 不停的读取服务器转发的其他客户端的信息
             * 记录一下之前踩过的小坑:
             * 这里一定要创建一个msg接收信息,如果直接用receive()方法判断和输出receive()的话会造成有的消息不会显示
             * 因为receive()获取时,在返回之前是阻塞的,一旦接收到消息才会返回,也就是while这里是阻塞的,一旦有消息就会进入到while里面
             * 这时候如果输出的是receive(),那么上次获取的信息就会丢失,然后阻塞在System.out.println
             * */
            String msg=null;
            while ((msg=receive())!=null){
                System.out.println(msg);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                if(writer!=null){
                    writer.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

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

客户端UserInputHandler

/**
 * @Package com.csdn.bio
 * @ClassName UserInputHandler
 * @Description TODO
 * @Author LangShengJie
 * @Date Created in 2020/11/24 11:15
 */
public class UserInputHandler implements Runnable {
    private ChatClient client;
    public UserInputHandler(ChatClient client) {
        this.client = client;
    }
    @Override
    public void run() {
        try {
            //接收用户输入的消息
            BufferedReader reader = new BufferedReader(
                    new InputStreamReader(System.in)
            );
            //不停的获取reader中的System.in,实现了等待用户输入的效果
            while (true) {
                String input = reader.readLine();
                //向服务器发送消息
                client.sendToServer(input);
                if (input.equals("quit")){
                    break;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值