课程介绍
1 网络模型概述
2 Channel 详解
3 Buffer 详解
4 Selector 详解
5 NIO综合案例-聊天室
6 AIO概念及实现
1 网络编程IO 模型介绍
1.1 BLockingIO
Blocking IO也称BIO,及同步阻塞IO。Java 的 io 包基于流模型实现,提供了FIle,FileInputStream,FileOutputStream等输入输出流的功能。Java io 包下提供的基于流操作,交互方式是同步且阻塞的方式,在输入输出流进行读写操作之前,线程一直阻塞。因此io 包中对流的操作容易造成性能的瓶颈。
同样,在java.net 包鞋提供的部分网络API,如 Socket、ServerSocket、HttpURLConnection 等,进行网络通信时,用到的也是java。io下的流操作,也是同步阻塞IO。
1.2 Non Blocking IO
Non Blocking IO 也称NIO,即同步非阻塞编程IO。Java 1.4中引入NIO框架,在java.nio 包中提供了Channer、Selector、Buffer 等抽象类,可以快速构建多路复用的IO程序,用于提供更接近操作系统底层高性能数据操作的方式。
1.3 Asynchronous IO
Asynchronous IO,即异步非阻塞IO。Java 7 提供了改进版的NIO,引入异步非阻塞的IO,由操作系统完成后,回调通知服务端启动现场去处理。
2 BIO 实现逻辑以及局限性
在BIO同步阻塞模式下,一个服务器可以开启多个线程来处理客户端的连接,但是一个处理线程只能对应一个客户端的连接。
如果大量客户端来连接服务器,服务端将开启大量的线程处理连接, 开启线程非常消耗资源,很容易达到性能瓶颈。
2.1 案例 单线程服务端
服务端
package com.spqin.nio.newstudy.bio; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; /** * 单线程服务段 */ public class ServerSocketSingleThread { public static void main(String[] args) { try { ServerSocket serverSocket = new ServerSocket(9090); while(true){ System.out.println("阻塞等待客户端的连接"); // accept 将一直阻塞直到客户端的连接到来 Socket socket = serverSocket.accept(); System.out.println("客户端已经连接了"); //开始处理客户端的读写请求 InputStream inputStream = socket.getInputStream(); //用于接受客户端的数据 byte[] data = new byte[1024]; //读取到的数据长度 int len = inputStream.read(data); // 把接收端到的数据转为字符串显示 System.out.println("收到客户端发来的数据:"+new String(data,0,len)); OutputStream outputStream = socket.getOutputStream(); outputStream.write("收到连接,可以聊天了".getBytes("utf-8")); outputStream.flush(); } } catch (IOException e) { throw new RuntimeException(e); } } }
连个客户端发起请求
客户端1
package com.spqin.nio.newstudy.bio; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; import java.util.concurrent.TimeUnit; /** * @author ChaoMing * @description 客户端 * @date 2024-12-22 11:01 */ public class SocketClient { public static void main(String[] args) { // 连接服务 System.out.println("连接服务端"); try { Socket socket = new Socket("127.0.0.1", 9090); //获得输出流 OutputStream outputStream = socket.getOutputStream(); System.out.println("睡眠一段时间,模拟客户端实际发消息的场景"); TimeUnit.SECONDS.sleep(10); outputStream.write("张三 向客户端发起连接!".getBytes("utf-8")); outputStream.flush(); InputStream inputStream = socket.getInputStream(); byte[] data = new byte[1024]; int len = inputStream.read(data); System.out.println("接收到服务端的相应数据:"+new String (data,0,len)); //关闭连接 socket.close(); } catch (IOException e) { throw new RuntimeException(e); } catch (InterruptedException e) { throw new RuntimeException(e); } } }
客户端2
package com.spqin.nio.newstudy.bio; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; import java.util.concurrent.TimeUnit; /** * @author ChaoMing * @description 客户端 * @date 2024-12-22 11:01 */ public class SocketClient2 { public static void main(String[] args) { // 连接服务 System.out.println("连接服务端"); try { Socket socket = new Socket("127.0.0.1", 9090); //获得输出流 OutputStream outputStream = socket.getOutputStream(); TimeUnit.SECONDS.sleep(10); outputStream.write("李四 向客户端发起连接!".getBytes("utf-8")); outputStream.flush(); InputStream inputStream = socket.getInputStream(); byte[] data = new byte[1024]; int len = inputStream.read(data); System.out.println("接收到服务端的相应数据:"+new String (data,0,len)); //关闭连接 socket.close(); } catch (IOException e) { throw new RuntimeException(e); } catch (InterruptedException e) { throw new RuntimeException(e); } } }
运行调试
服务端打印消息:
阻塞等待客户端的连接
客户端已经连接了
收到客户端发来的数据:张三 向客户端发起连接!
阻塞等待客户端的连接
客户端已经连接了
收到客户端发来的数据:李四 向客户端发起连接!
阻塞等待客户端的连接
客户端1打印消息:
连接服务端
睡眠一段时间,模拟客户端实际发消息的场景
接收到服务端的相应数据:收到连接,可以聊天了
客户端2打印消息:
连接服务端
睡眠一段时间,模拟客户端实际发消息的场景
接收到服务端的相应数据:收到连接,可以聊天了
可以看出这个服务端一次只能处理一个客户端的请求,请求处理完之前不能接受第二个客户端的请求。
说明服务端可以开启线程处理客户端的连接,但是一个线程只能处理一个客户端的来连接,不能并行处理两个客户端的连接。
要处理2个客户端连接必须另外开启1个线程。
2.2 案例 多线程服务端
只需要改进服务端,让其支持多个客户端的连接(每个客户端开启一个子线程处理),客户端不需要改动
package com.spqin.nio.newstudy.bio.multithreadserver; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; /** * 多线程服务段 */ public class ServerSocketMultiThread { public static void main(String[] args) { try { ServerSocket serverSocket = new ServerSocket(9090); while(true){ System.out.println("阻塞等待客户端的连接"); // accept 将一直阻塞直到客户端的连接到来 Socket socket = serverSocket.accept(); System.out.println("客户端已经连接了,开启子线程处理客户端的连接"); new Thread(new ConnectHandler(socket),"客户端Socket 端口"+socket.getPort()).start(); } } catch (IOException e) { throw new RuntimeException(e); } } }
客户连接处理器
package com.spqin.nio.newstudy.bio.multithreadserver; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; /** * @author ChaoMing * @description 处理来自客户端的连接 * @date 2024-12-22 11:29 */ public class ConnectHandler implements Runnable{ private Socket socket; public ConnectHandler (Socket socket) { this.socket = socket; } @Override public void run() { try { //开始处理客户端的读写请求 InputStream inputStream = socket.getInputStream(); //用于接受客户端的数据 byte[] data = new byte[1024]; //读取到的数据长度 int len = inputStream.read(data); // 把接收端到的数据转为字符串显示 System.out.println(Thread.currentThread().getName()+"线程收到客户端发来的数据:"+new String(data,0,len)); OutputStream outputStream = socket.getOutputStream(); outputStream.write("收到连接,可以聊天了".getBytes("utf-8")); outputStream.flush(); } catch (IOException e) { throw new RuntimeException(e); } } }
运行结果
服务端打印消息:
阻塞等待客户端的连接
客户端已经连接了,开启子线程处理客户端的连接
阻塞等待客户端的连接
客户端已经连接了,开启子线程处理客户端的连接
阻塞等待客户端的连接
客户端Socket 端口54643线程收到客户端发来的数据:张三 向客户端发起连接!
客户端Socket 端口54646线程收到客户端发来的数据:李四 向客户端发起连接!
客户端信息不变,略。
2.3 案例 线程池服务端
设计一个线程池,最大线程数为2,最大能持支 4个任务,任务时长设定为10s(让任务不能理解结束),等第5个任务提交时,线程池默认的线程处理策略是拒绝提交。
服务端代码
package com.spqin.nio.newstudy.bio.threadpoolserver; import com.spqin.nio.newstudy.bio.multithreadserver.ConnectHandler; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; /** * 线程池服务端 */ public class ServerSocketThreadPool { public static void main(String[] args) { // 创建一个最大线程数为2的线程池, // 每个队列设置为2,则 最大能支持2 + 2 = 4个线程 // 创建 10个客户端,每个客户端,睡眠 10秒,这样理论上再地5个任务提交时,线程池会采取默认的拒绝策略, ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor( 2, 2, 1, TimeUnit.MINUTES, new LinkedBlockingDeque<>(2), new ThreadFactory() { @Override public Thread newThread(Runnable r) { Thread t = new Thread(r,"服务端线程城池-"); if (t.isDaemon()) t.setDaemon(false); if (t.getPriority() != Thread.NORM_PRIORITY) t.setPriority(Thread.NORM_PRIORITY); return t; } }); try { ServerSocket serverSocket = new ServerSocket(9090); while(true){ System.out.println("阻塞等待客户端的连接"); // accept 将一直阻塞直到客户端的连接到来 Socket socket = serverSocket.accept(); System.out.println("客户端已经连接了,开启子线程处理客户端的连接"); try{ //线程池拒绝异常 threadPoolExecutor.execute(new ConnectHandler(socket)); }catch (Exception e) { System.out.println("线程池异常:"+e.getMessage()); } } } catch (IOException e) { throw new RuntimeException(e); } } }
客户端代码
package com.spqin.nio.newstudy.bio.threadpoolserver; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; import java.util.concurrent.TimeUnit; /** * @author ChaoMing * @description 线程池客户端(由于服务端是线程池,所以要增加客户端连接数) * @date 2024-12-22 11:01 */ public class SocketClientThreadPool { static int num = 0; public static void main(String[] args) { //创建 10个客户端 while (num < 10) { num++; //循环开启子线程徐连接客户端 new Thread(()->{ int no = num; // 连接服务 System.out.println("连接服务端"); try { Socket socket = new Socket("127.0.0.1", 9090); //获得输出流 OutputStream outputStream = socket.getOutputStream(); System.out.println("睡眠一段时间,模拟客户端实际发消息的场景"); TimeUnit.SECONDS.sleep(10); outputStream.write(("用户" + no + "向客户端发起连接!").getBytes("utf-8")); outputStream.flush(); InputStream inputStream = socket.getInputStream(); byte[] data = new byte[1024]; int len = inputStream.read(data); System.out.println("接收到服务端的相应数据:"+new String (data,0,len)); //关闭连接 socket.close(); } catch (IOException e) { throw new RuntimeException(e); } catch (InterruptedException e) { throw new RuntimeException(e); } }).start(); //延迟一下主线程的提交速度,让子线程尽快创建 try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { throw new RuntimeException(e); } } } }
运行结果
服务端结果:
阻塞等待客户端的连接 客户端已经连接了,开启子线程处理客户端的连接 阻塞等待客户端的连接 客户端已经连接了,开启子线程处理客户端的连接 阻塞等待客户端的连接 客户端已经连接了,开启子线程处理客户端的连接 阻塞等待客户端的连接 客户端已经连接了,开启子线程处理客户端的连接 阻塞等待客户端的连接 客户端已经连接了,开启子线程处理客户端的连接 线程池异常:Task com.spqin.nio.newstudy.bio.multithreadserver.ConnectHandler@24d46ca6 rejected from java.util.concurrent.ThreadPoolExecutor@4517d9a3[Running, pool size = 2, active threads = 2, queued tasks = 2, completed tasks = 0] 阻塞等待客户端的连接 客户端已经连接了,开启子线程处理客户端的连接 线程池异常:Task com.spqin.nio.newstudy.bio.multithreadserver.ConnectHandler@372f7a8d rejected from java.util.concurrent.ThreadPoolExecutor@4517d9a3[Running, pool size = 2, active threads = 2, queued tasks = 2, completed tasks = 0] 阻塞等待客户端的连接 客户端已经连接了,开启子线程处理客户端的连接 线程池异常:Task com.spqin.nio.newstudy.bio.multithreadserver.ConnectHandler@2f92e0f4 rejected from java.util.concurrent.ThreadPoolExecutor@4517d9a3[Running, pool size = 2, active threads = 2, queued tasks = 2, completed tasks = 0] 阻塞等待客户端的连接 客户端已经连接了,开启子线程处理客户端的连接 线程池异常:Task com.spqin.nio.newstudy.bio.multithreadserver.ConnectHandler@28a418fc rejected from java.util.concurrent.ThreadPoolExecutor@4517d9a3[Running, pool size = 2, active threads = 2, queued tasks = 2, completed tasks = 0] 阻塞等待客户端的连接 客户端已经连接了,开启子线程处理客户端的连接 线程池异常:Task com.spqin.nio.newstudy.bio.multithreadserver.ConnectHandler@5305068a rejected from java.util.concurrent.ThreadPoolExecutor@4517d9a3[Running, pool size = 2, active threads = 2, queued tasks = 2, completed tasks = 0] 阻塞等待客户端的连接 客户端已经连接了,开启子线程处理客户端的连接 线程池异常:Task com.spqin.nio.newstudy.bio.multithreadserver.ConnectHandler@1f32e575 rejected from java.util.concurrent.ThreadPoolExecutor@4517d9a3[Running, pool size = 2, active threads = 2, queued tasks = 2, completed tasks = 0] 阻塞等待客户端的连接 服务端线程城池-线程收到客户端发来的数据:用户1向客户端发起连接! 服务端线程城池-线程收到客户端发来的数据:用户2向客户端发起连接! 服务端线程城池-线程收到客户端发来的数据:用户3向客户端发起连接! 服务端线程城池-线程收到客户端发来的数据:用户4向客户端发起连接!
客户端打印结果
连接服务端 睡眠一段时间,模拟客户端实际发消息的场景 连接服务端 睡眠一段时间,模拟客户端实际发消息的场景 连接服务端 睡眠一段时间,模拟客户端实际发消息的场景 连接服务端 睡眠一段时间,模拟客户端实际发消息的场景 连接服务端 睡眠一段时间,模拟客户端实际发消息的场景 连接服务端 睡眠一段时间,模拟客户端实际发消息的场景 连接服务端 睡眠一段时间,模拟客户端实际发消息的场景 连接服务端 睡眠一段时间,模拟客户端实际发消息的场景 连接服务端 睡眠一段时间,模拟客户端实际发消息的场景 连接服务端 睡眠一段时间,模拟客户端实际发消息的场景 接收到服务端的相应数据:收到连接,可以聊天了 接收到服务端的相应数据:收到连接,可以聊天了 接收到服务端的相应数据:收到连接,可以聊天了 接收到服务端的相应数据:收到连接,可以聊天了
可以看到在第五个客户端连接时,线程池抛出异常,服务端没有处理对应的一场,服务端挂掉了,不再向线程池提交任务,已经提交到线程池的任务继续执行。
2.4 BIO 局限总结
在上面的例子中可以看出,IO代码里Read操作是阻塞操作,如果连接读数据时数据没有准备好,会导致线程阻塞,浪费线程资源。
如果采用多线程处理客户端请求,如果客户端请求过多,将会导致服务端不断创建出新的线程,对服务器造成过大的压力。
采用线程池实现服务端,是对多线程资源浪费的一种优化,解决不了线程阻塞的问题,如果请求过多会出现提交任务被拒绝(即任务队列满了,不支持新任务提交了)。
BIO的使用和维护相对 比较简单,适用于较小且稳定的框架
标签:BIO,java,线程,import,多线程,连接,服务端,客户端 From: https://www.cnblogs.com/spqin/p/18621874