Java网络编程:以一个基于BIO模型的多线程网络聊天室为例
网络编程的目的在于远程发送数据,发送接收数据就涉及到I/O的操作,这里因为涉及到比较底层字节和字符的操作,所以不可以使用java.nio.file.Files 操作文件。那就先说说I/O吧,I/O流分为字节流和字符流。字节即Byte,包含8位二进制数,一个二进制数就是1bit,中文名称叫位。字符即一个字母或者一个汉字。一个字母由一个字节组成,而汉字根据编码多字节个组成。
Java I/O类的实现原理是装饰者设计模式:
FileInputStream fileInputStream = new FileInputStream(filePath); BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
优点:装饰类和被装饰类可以独立发展,不会相互耦合,装饰模式是继承的一个替代模式,装饰模式可以动态扩展一个实现类的功能。
使用场景: 1、扩展一个类的功能。 2、动态增加功能,动态撤销。
Java BIO:同步阻塞
Java NIO:同步非阻塞
多个的进程的IO可以注册到一个复用器(select)上,然后用一个进程调用该select, select会监听所有注册进来的IO。如果select监听的IO在内核缓冲区都没有可读数据,select调用进程会被阻塞;而当任一IO在内核缓冲区中有可数据时,select调用就会返回。而后select调用进程可以自己或通知另外的进程(注册进程)来再次发起读取IO,读取内核中准备好的数据。可以看到,多个进程注册IO后,只有另一个select调用进程被阻塞。
Java AIO:异步非阻塞
BIO通信模型
所谓BIO,就是Block IO,阻塞式的IO。这个阻塞主要发生在:ServerSocket接收请求时、InputStream、OutputStream(输入输出流的读和写)都是阻塞的。
服务端ChatServer类:
1 package top.rqfreefly.chatroom.bio; 2 3 /** 4 * 功能实现:服务器端,这个类有两个关键的全局变量,一个就是存储在线用户信息的Map,一个就是线程池 5 * 这个类会监听端口,接收客户端的请求,然后为客户端分配工作线程 6 * 还会提供一些常用的工具方法给每个工作线程调用,比如:添加和移除用户,发送消息。 7 */ 8 9 import java.io.*; 10 import java.net.*; 11 import java.util.Map; 12 import java.util.concurrent.*; 13 14 public class ChatServer { 15 16 private int SEVER_PORT = 8888; 17 /** 18 * 创建一个Map存储在线用户的信息。这个userMap可以统计在线用户、针对这些用户可以转发其他用户发送的消息 19 * 因为会有多个线程操作这个userMap,所以为了安全起见用ConcurrentHashMap 20 * 在这里key就是客户端的端口号,但在实际中肯定不会用端口号区分用户,如果是web的话一般用session 21 * value是用户的IO的Writer,用以给客户端发送消息 22 */ 23 private Map<Integer, Writer> userMap = new ConcurrentHashMap<>(); 24 /** 25 * 创建线程池,线程上限为40个,如果第41个客户端请求进来,服务器会接收但是不会去分配线程处理它。 26 */ 27 private ExecutorService executorService = Executors.newFixedThreadPool(40); 28 29 //客户端连接时往map添加客户端 30 public void addClient(Socket socket) throws IOException { 31 if (socket != null) { 32 BufferedWriter writer = new BufferedWriter( 33 new OutputStreamWriter(socket.getOutputStream()) 34 ); 35 userMap.put(socket.getPort(), writer); 36 System.out.println("Client["+socket.getPort()+"]:Online"); 37 } 38 } 39 40 //断开连接时map里移除客户端 41 public void removeClient(Socket socket) throws Exception { 42 if (socket != null) { 43 if (userMap.containsKey(socket.getPort())) { 44 userMap.get(socket.getPort()).close(); 45 userMap.remove(socket.getPort()); 46 } 47 System.out.println("Client [" + socket.getPort() + "] Offline"); 48 } 49 } 50 51 //转发客户端消息,这个方法就是把消息发送给在线的其他的所有客户端 52 public void sendMessage(Socket socket, String msg) throws IOException { 53 //遍历在线客户端 54 for (Integer port : userMap.keySet()) { 55 //发送给在线的其他客户端 56 if (port != socket.getPort()) { 57 Writer writer = userMap.get(port); 58 writer.write(msg); 59 writer.flush(); 60 } 61 } 62 } 63 64 //接收客户端请求,并分配Handler去处理请求 65 public void start() { 66 try (ServerSocket serverSocket = new ServerSocket(SEVER_PORT)) { 67 System.out.println("Server Start,The Port is:" + SEVER_PORT); 68 while (true) { 69 //等待客户端连接 70 Socket socket = serverSocket.accept(); 71 //为客户端分配一个ChatHandler线程 72 executorService.execute(new ChatHandler(this, socket)); 73 } 74 } catch (IOException e) { 75 e.printStackTrace(); 76 } 77 } 78 79 public static void main(String[] args) { 80 ChatServer server = new ChatServer(); 81 server.start(); 82 } 83 84 }
服务端的ChatHandler类:
1 package top.rqfreefly.chatroom.bio; 2 3 /** 4 * 功能实现:这个类就是工作线程类,把接收到的消息转发给其他客户端,还可以添加和移除用户。 5 */ 6 7 import java.io.*; 8 import java.net.*; 9 10 public class ChatHandler implements Runnable { 11 private ChatServer server; 12 private Socket socket; 13 BufferedReader reader; 14 15 //构造函数,ChatServer通过这个分配Handler线程 16 public ChatHandler(ChatServer server, Socket socket) { 17 this.server = server; 18 this.socket = socket; 19 } 20 21 @Override 22 public void run() { 23 try { 24 //往map里添加这个客户端 25 server.addClient(socket); 26 //读取这个客户端发送的消息 27 reader = new BufferedReader( 28 new InputStreamReader(socket.getInputStream()) 29 ); 30 String msg = null; 31 while ((msg = reader.readLine()) != null) { 32 //这样拼接是为了让其他客户端也能看清是谁发送的消息 33 String sendmsg = "Client [" + socket.getPort() + "]: " + msg; 34 //服务器打印这个消息 35 System.out.println(sendmsg); 36 //将收到的消息转发给其他在线客户端 37 server.sendMessage(socket, sendmsg + "\n"); 38 if (msg.equals("exit")) { 39 break; 40 } 41 } 42 } catch (IOException e) { 43 e.printStackTrace(); 44 } finally { 45 //如果用户退出或者发生异常,就在map中移除该客户端 46 try { 47 server.removeClient(socket); 48 } catch (Exception e) { 49 e.printStackTrace(); 50 } 51 try { 52 if(reader != null) { 53 reader.close(); 54 } 55 } catch (IOException e) { 56 e.printStackTrace(); 57 } 58 } 59 } 60 }
客户端ChatClient类:
1 package top.rqfreefly.chatroom.bio; 2 3 /** 4 * 功能实现:客户端,会通过Socket和服务器连接。提供了两个工具方法:发送消息和接收消息。 5 */ 6 7 import java.io.*; 8 import java.net.*; 9 10 public class ChatClient { 11 12 private BufferedReader reader; 13 private BufferedWriter writer; 14 private Socket socket; 15 16 //发送消息给服务器 17 public void sendToServer(String msg) throws IOException { 18 //发送之前,判断socket的输出流是否关闭 19 if (!socket.isOutputShutdown()) { 20 //如果没有关闭就把用户键入的消息放到writer里面 21 writer.write(msg + "\n"); 22 writer.flush(); 23 } 24 } 25 26 //从服务器接收消息 27 public String receive() throws IOException { 28 String msg = null; 29 //判断socket的输入流是否关闭 30 if (!socket.isInputShutdown()) { 31 //没有关闭的话就可以通过reader读取服务器发送来的消息。注意:如果没有读取到消息线程会阻塞在这里 32 msg = reader.readLine(); 33 } 34 return msg; 35 } 36 37 public void start() { 38 //和服务创建连接 39 try { 40 socket = new Socket("127.0.0.1", 8888); 41 reader = new BufferedReader( 42 new InputStreamReader(socket.getInputStream()) 43 ); 44 writer = new BufferedWriter( 45 new OutputStreamWriter(socket.getOutputStream()) 46 ); 47 //新建一个线程去监听用户输入的消息 48 new Thread(new UserInputHandler(this)).start(); 49 50 //不停的读取服务器转发的其他客户端的信息 51 String msg=null; 52 while ((msg=receive())!=null){ 53 System.out.println(msg); 54 } 55 } catch (IOException e) { 56 e.printStackTrace(); 57 }finally { 58 try { 59 if(writer != null) { 60 writer.close(); 61 } 62 } catch (IOException e) { 63 e.printStackTrace(); 64 } 65 try { 66 if(reader != null) { 67 reader.close(); 68 } 69 } catch (IOException e) { 70 e.printStackTrace(); 71 } 72 if (socket != null) { 73 try { 74 socket.close(); 75 } catch (IOException e) { 76 e.printStackTrace(); 77 } 78 } 79 } 80 } 81 82 public static void main(String[] args) { 83 ChatClient chatClient = new ChatClient(); 84 chatClient.start(); 85 } 86 87 }
客户端UserInputHandler类:
1 package top.rqfreefly.chatroom.bio; 2 3 /** 4 * 功能实现:专门负责监听用户输入信息,一旦有信息键入,就马上发送给服务器。 5 */ 6 7 import java.io.*; 8 9 public class UserInputHandler implements Runnable { 10 11 private ChatClient client; 12 13 public UserInputHandler(ChatClient client) { 14 this.client = client; 15 } 16 17 @Override 18 public void run() { 19 try { 20 //接收用户输入的消息 21 BufferedReader reader = new BufferedReader( 22 new InputStreamReader(System.in) 23 ); 24 //不停的获取reader中的System.in,实现了等待用户输入的效果 25 while (true) { 26 String input = reader.readLine(); 27 //向服务器发送消息 28 client.sendToServer(input); 29 if (input.equals("exit")) 30 break; 31 } 32 } catch (IOException e) { 33 e.printStackTrace(); 34 } 35 } 36 37 }