2.BIO编程模型实现群聊

在第一章中运用Socket和ServerSocket简单的实现了网络通信。这一章,利用BIO编程模型进行升级改造,实现群聊聊天室
所谓BIO,就是Block IO,阻塞式的IO。这个阻塞主要发生在:ServerSocket接受请求时(accept()方法)、InputStream、OutputStream(输入输出流的读和写)都是阻塞的。这个可以在下面代码的调试中发现,比如在客户端接受服务器消息的输入流处打上断点,除非服务器发来消息,不然断点是一直停在这个地方的。也就是说这个线程在这时间是被阻塞的。
在这里插入图片描述
如图:当一个苦读段请求进来是,接收器会为这个客户端分配一个工作线程,这个工作线程专职处理客户端的操作。在上一章中,服务器收到客户端请求后就跑去专门服务之歌客户端了,所以当其他请求进来时,是处理不到的。
看到这个图,很容易就会想到线程池,BIO是一个相对简单的模型,实现它的关键也在于线程池。
在写代码之前,先大概说清楚每个类的作用。

服务器端

ChatServer:这个类的作用就像图中的Acceptor。它有两个比较关键的全局变量,一个就是存储在线用户信息的Map,一个就是线程池。这个类会监听端口,接受客户端的请求,然后为客户端分配工作线程。还会提供一些常用的工具方法给每个工作线程调用,比如:发送消息,添加在线用户等。
ChatHandler:这个类就是工作线程类。在这个项目中,它的工作很简单:把接受到的消息转发给其他客户端,当然还有一些小功能,比如添加、移除在线用户

客户端

相较于服务器,客户端的改动较小,主要是把等待用户输入信息这个功能分到其他线程做,不然这个功能会一直阻塞主线程,导致无法接受其他客户端的信息。
ChatClient:客户端启动类,也就是主线程,会通过Socket和服务器连接。也提供了两个工具方法:发送消息和接受消息
UserInputHandler:专门负责等待用户输入信息的线程,一旦有信息键入,就马上发送给服务器。

服务端ChatServer

package com.yjn.learn.bio;

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.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
* @Author yjn
* @Date 2021/10/12 3:24 下午
*/
public class ChatServer {

   //定义默认端口号
   private final int DEFAULT_PORT = 8888;
   /**
    * 创建一个map存储在线用户的信息,这个map可以统计在线用户,针对这些用户可以转发其他用户的信息
    * 因为会有多个线程操作这个map,为了安全性使用ConcurrentHashMap
    * 在这里key就是客户端的端口,但在实际中肯定不会以端口号区分用户,如果是web的话一般采用session
    * value是IO的Writer,用来存储客户端发送的消息
    */
   private Map<Integer, Writer> map = new ConcurrentHashMap<>();

   /**
    * 创建线程池,线程上限为10个,如果地11个客户端请求进来,服务器会接受但是不会去分配线程处理它
    * 前10个客户端的聊天记录他看不见,当有一个客户端下线时,这第十一个客户端就会被分配线程,服务器显示在线
    */
   private ExecutorService executorService = Executors.newFixedThreadPool(10);

   //客户端链接时往map中添加客户端
   public void addClient(Socket socket) throws IOException {
       if (socket != null) {
           Writer out;
           BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
           map.put(socket.getPort(), writer);
           System.out.println("Client [" + socket.getPort() + "] :Online");
       }
   }

   //链接断开是移除客户端
   public void removeClient(Socket socket) throws IOException {
       if (socket != null) {
           if (map.containsKey(socket.getPort())) {
               map.get(socket.getPort()).close();
               map.remove(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 sock = serverSocket.accept();
               //为客户端分配一个ChatHandler线程
               executorService.execute(new ChatHanlder(this, sock));
           }
       } catch (IOException e) {
           e.printStackTrace();
       }
   }

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

服务器端ChatHandler

package com.yjn.learn.bio;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.Socket;

/**
* @Author yjn
* @Date 2021/10/12 3:44 下午
*/
public class ChatHanlder implements Runnable {

   private ChatServer chatServer;

   private Socket socket;

   //构造函数 ChatServer通过这个分配Handler线程
   public ChatHanlder(ChatServer chatServer, Socket sock) {
       this.chatServer = chatServer;
       this.socket = sock;
   }

   @Override
   public void run() {
       try {
           //往map中添加客户端
           chatServer.addClient(socket);
           //读取这个客户端发送的消息
           BufferedReader reader=new BufferedReader(new InputStreamReader(socket.getInputStream()));
           String msg;
           while ((msg=reader.readLine())!=null){
               //这样拼接是为了让其他客户端异能看到是谁发送的消息
               String sendMsg="Client ["+socket.getPort()+" ]" +msg;
               //服务端打印这个消息
               System.out.println(sendMsg);
               //将消息转发到其他在线的客户端
               chatServer.sendMessage(socket,sendMsg+"\n");
               if ("quit".equals(msg)){
                   break;
               }
           }
       } catch (IOException e) {
           e.printStackTrace();
       } finally {
           //如果用户退出或者发生异常,就在map中移除改客户端
           try {
               chatServer.removeClient(socket);
           } catch (IOException e) {
               e.printStackTrace();
           }
       }
   }
}

客户端ChatClient

package com.yjn.learn.bio;

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

/**
* @Author yjn
* @Date 2021/10/12 3:54 下午
*/
public class ChatClient {

   private BufferedReader reader;
   private BufferedWriter writer;
   private Socket socket;

   //发送消息给服务端
   public void sendToServer(String msg) throws IOException {
       //发送之前,判断Socket的输出流是否关闭
       if (!socket.isOutputShutdown()) {
           //如果没有关闭就把用户输入的信息放到write中
           writer.write(msg + "\n");
           writer.flush();
       }
   }

   //从服务端接受请求
   public String receive() throws IOException {
       String msg = null;
       //判断socket 的输入流是否关闭
       if (!socket.isOutputShutdown()) {
           //如果没有关闭的话就可以通过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) {
       ChatClient chatClient=new ChatClient();
       chatClient.start();
   }
}

客户端UserInputHandler

package com.yjn.learn.bio;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

/**
* @Author yjn
* @Date 2021/10/12 4:21 下午
*/
public class UserInputHandler implements Runnable {

   private ChatClient chatClient;

   public UserInputHandler(ChatClient chatClient) {
       this.chatClient = chatClient;
   }

   @Override
   public void run() {
       try {
           //接受用户输入的消息
           BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
           //不停的获取reader中的System.in,实现了等待用户输入的效果
           while (true) {
               String input = reader.readLine();
               //向服务器发送消息
               chatClient.sendToServer(input);
               if ("quit".equals(input)) {
                   break;
               }
           }
       } catch (IOException e) {
           e.printStackTrace();
       }
   }
}

测试效果

1.首先运行ChatServer

在这里插入图片描述

2.接着启动ChatClient

在这里插入图片描述
在这里插入图片描述
可以看到在一个终端输入之后,服务器和另外的客户端都能接受到消息

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值