网络编程03:BIO chat room服务端

BIO模型

jdk1.4 之前对 IO 的支持比较简单,都是使用 BIO,就是 Block IO 的模型来进行编码。
上面看到,accept 是阻塞的,那么新来的客户端是怎么处理的?就是新建一个 Handler 的线程去处理,这样就不阻塞了当前的客户端了,所以第二个客户端就可以与服务器端发送消息。
acceptor线程就是专门用来接收客户端发起的连接请求。它不是用来处理输入输出的数据流,只是用accept函数接受发来的请求的。
handler处理数据

chat room的UML建模之时序图

多人在线,每个人的发言都被转发给其它在线用户

  • ChatHandler 是启动的一个新的线程,首先在这个线程里面添加客户端到客户端集合里面,这个时候客户端输入的是一个阻塞的函数,为了同时能够收到消息,新建一个 UserInputhandler的线程,专门处理用户输入的线程。如果有输入,就发送给服务器端发送消息
  • 用户发送 “quit” 消息的时候,这个时候服务器端就知道某个客户端要退出了,首先要做的事情就是先移除客户端列表,然后客户端关闭 socket
  • 最外面的 loop 循环的大框,就是服务器的一个主要的循环,一直在等待 accept 函数的执行,如果有就新建一个 Handler 去进行处理
    在这里插入图片描述

所谓bio,一个client对应一个handler线程。
客户1——>服务端——>客户端2,3,4
服务端需要存储在线用户的集合。
客户端需要两条线程,一个等待控制台输入的消息,一个收到服务器发来的其它用户的信息。

实现服务端:ChatServer

  • 不停的监听和接收客户端的消息:while循环、chatHandler线程
  • 转发给在线的所有的客户端
  • 需要保存所有的在线客户端列表:hashmap存放、端口作为id
  • 当用户输入 quit 的时候,这个客户就不想留在服务器端,就需要将它从在线用户列表中移除
    ChatServer.java:
    构造函数、加入新上线的客户端、转发消息、检查是否退出、关闭、启动
public class ChatServer {
	//默认服务器监听的端口
    private int DEFAULT_PORT = 888;
    private final String QUIT = "quit";
   
    private ServerSocket serverSocket;
    // 连接的客户端列表,value 是需要写消息的 writer
    private Map<Integer, Writer> connectedClients;
    
    public ChatServer() {
       connectedClients = new HashMap();
    }
    
    // 添加客户端
    public synchronized void addClient(Socket socket) throws IOException {
        if(null != socket) {
            int port = socket.getPort();
            OutputStream out = scoket.getOutputStream();
            BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out));
            connectedClients.put(port, writer); // 线程安全问题,用synchronized关键字解决
            System.out.println("客户端[" + port + "已经连接到服务器");
        }
    }
    // 移除客户端,还需要关闭
    public synchronized void removeClient(Socket socket) throws IOException {
       	if(null != socket) {
           	int port = socket.getPort();
           	//找到socket最外层的writer删除,等于删除这个socket
           	if(connectedClients.containsKey(port)) {
              	Writer writer = connectedClients.get(port);
              	writer.close();
           	}
           	connectedClients.remove(port);
		   	System.out.println("客户端[" + port + "已经断开连接");
       	}
    }
    // 转发消息给所有的客户端
    public synchronized void forwardMessage(Socket socket, String fwdMsg) 
                                       throws IOException {
        for(Map.Entry<Integer, Writer> entry: connectedClients.entrySet()) {
           	int port = entry.getKey();
           	//排除掉发送者:
           	if((socket.getPort != port)) {
               	Writer writer = entry.getValue();
               	writer.writer(fwdMsg);
               	writer.flush();
           	}
        }
    }

    // 检查是否退出
    public boolean readToQuit(String msg) {
         return QUIT.equals(msg);
    }
   
    // 关闭
    public synchronized void close() {
       if(null != serverSocket) {
           try {
               serverSocket.close();
           }catch(Exception e) {
               e.printStackTrace();
           }
       }
    }

    // 启动逻辑
    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 (Exception e) {
           e.printStackTrace();
        } finally {
           close();
        }
   }

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

实现监听器:ChatHandler

一个handler对象为一个客户端提供服务,里面只有一个run方法,由server开辟该线程。

  • 需要 ChatServer 的对象:很多的操作都是依赖于 ChatServer 的connectedClients对象
  • 需要客户端的 Socket 对象
  • 前面的ChatServer 代码中添加readToQuit的方法代码
  • 最后需要注意线程安全问题,需要注意一下
    ChatHandler.java
public class ChatHandler implements Runable{
   private ChatServer server;
   private Socket socket;
   
   public ChatHandler(ChatServer server, Socket socket) {
      this. server = server;
      this.socket = socket;
   }
   
   @Ovverride
   public void run() {
      try {
         // 存储新上线用户
         server.addClient(socket);
         // 读取用户发送的消息
         BufferedReader reader = 
               new BufferedReader(new InputStreamReader(socket.getInputStream()))
         // readLine是一直阻塞的,除非收到换行符就会读取
         String msg = null;
         while ((msg = reader.readLine()) != null) {
             String fwdMsg = "客户端[" + socket.getPort + "]:" + msg + "\n";
             System.out.println(fwdMsg );
             // 将消息转发给聊天室在线的其他用户
             server.forwadMessge(socket, fwdMsg);
             // 检查是否准备退出
             if(server.readToQuit()) {
                 break; // 如果收到退出命令,那么就直接退出
             }
         }
      }catch(IOException e) {
         e.printStachTrace();
      } finall{
         try {
         	//把已经不需要继续进行通信活动的客户从列表删除。
         	//如果不移除,会发给已经下线的客户端。
           server.removeClient(socket); 
         }catch(Exception e) {
           e.printStachTrace();
         }
      }
   }
}

实现客户端:ChatClient

先完成主线程:将消息发送给服务器,接收服务器返回的消息

ChatClient.java

public class ChatClient {
   private String DEFAULT_SERVER_HOST = "127.0.0.1";
   private int DEFAULT_SERVER_PORT = 8888;
   private final String QUIT = "quit";
   
   private Socket socket; 
   private BufferedReader reader;
   private BufferedWriter writer;
   
   public ChatClinet() {
      
   }

   // 发送消息给服务器
   public void send(String msg) throws IOException {
     if(!socket.isOuputShutdown()) {
        writer.write(msg + "\n");
        writer.flush();
     }
   }  
 
   // 接收消息从服务器
   public String receive() {
      String msg = null;
      if(!socket.IsInputShutdown()){
        try{
          msg = reader.readLine();  
}catch(IOException e){
  e.printStackTrace();
}       
      }
      return msg;
    }

    // 检查是否退出
    public boolean readyToQuit(String msg) {
        return QUIT.equals(msg);
    }

    public void close() {
       if(null != writer) {
          try {
             wirter.close();
          } catch (Exceptio e) {
             e.printStackTrace();
          }
       }
    }	
     
    public void start() {
       
       try {
          // 创建 Socket
          socket = new Socket( DEFAULT_SERVER_HOST, DEFAULT_SERVER_PORT);
          // 创建 IO 流
          reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
          writer = new BufferedWriter(new OutStreamWriter(socket.getOutputStream));

          // 处理用户的输入(需要额外开线程来进行处理)
          new Thread(new UserInputHanderl(this)).start();
          
          // 时刻监听是否有从服务器其他用户发过来的消息
          String msg = null;
          while ((msg = receive()) != null) {
             System.out.println(msg);
          }
       } catch (Exception e) {
         e.printStackTrace();
       } finally {
          close();
       }
       
    }
    
    public static void main(String[] args) {
         ChatClient chatClient = new ChatClient();
         chatClient.start();
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值