Java BIO
- Java BIO 就是传统的 java io编程,其相关的类和接口在 java.io包下
- BIO(blocking I/O) : 同步阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善
- BIO 方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,程序简单易理解。
BIO 代码示例
package nio;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class BIOServer {
public static void main(String[] args) throws Exception {
//线程池机制
//思路
//1. 创建一个线程池
//2. 如果有客户端连接,就创建一个线程,与之通讯(单独写一个方法)
//ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
ExecutorService newCachedThreadPool = Executors.newSingleThreadExecutor();
//创建ServerSocket
ServerSocket serverSocket = new ServerSocket(6666);
System.out.println("服务器启动了");
while (true) {
System.out.println("线程信息 id =" + Thread.currentThread().getId() + " 名字=" + Thread.currentThread().getName()+"等待连接....");
//监听,等待客户端连接,阻塞
final Socket socket = serverSocket.accept();
System.out.println("连接到一个客户端");
//就创建一个线程,与之通讯(单独写一个方法)
newCachedThreadPool.execute(new Runnable() {
public void run() { //我们重写
//可以和客户端通讯
handler(socket);
}
});
}
}
//编写一个handler方法,和客户端通讯
public static void handler(Socket socket) {
try {
byte[] bytes = new byte[1024];
//通过socket 获取输入流
InputStream inputStream = socket.getInputStream();
//循环的读取客户端发送的数据
while (true) {
//read也是阻塞的
int read = inputStream.read(bytes);
if (read != -1) {
System.out.println("线程信息 id =" + Thread.currentThread().getId() + " 名字=" + Thread.currentThread().getName()+"读到:"+
new String(bytes, 0, read)); //输出客户端发送的数据
} else {
break;
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println("关闭和client的连接");
try {
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
java BIO的缺点
- 每个请求都需要创建独立的线程,与对应的客户端进行数据 Read,业务处理,数据 Write 。当并发数较大时,需要创建大量线程来处理连接,系统资源占用较大。
- 连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在 Read 操作上,造成线程资源浪费。其他请求不能使用当前被挂起的线程。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IIFgERKi-1587207564828)(http://www.txjava.cn/wordpress/wp-content/uploads/2020/04/image-1586499972233.png)]
我们可以通过telnet来测试
我们能看到,每一个请求都需要一个一个线程来处理
此时我们如果采用单例线程池来做多个请求,我们发现程序无法同时处理多个请求,只有关闭正在阻塞的请求才能去处理其他的请求。显然不合理。
此种方式符合 同步阻塞IO模型
NIO
- Java NIO 全称 java non-blocking IO,是指 JDK提供的新 API。从JDK1.4 开始,Java 提供了一系列改进的输入/输出的新特性,被统称为NIO(即New IO),是同步非阻塞的
- NIO 相关类都被放在 java.nio 包及子包下,并且对原 java.io 包中的很多类进行改写。
- NIO有三大核心部分:Channel(通道),Buffer(缓冲区), Selector(选择器)
- NIO 是面向缓冲区编程的。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络
- Java NIO 的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。
- 通俗理解:NIO 是可以做到用一个线程来处理多个操作的。假设有 10000 个请求过来,根据实际情况,可以分配50 或者 100 个线程来处理。不像之前的阻塞 IO 那样,非得分配 10000 个。
- HTTP2.0 使用了多路复用的技术,做到同一个连接并发处理多个请求,而且并发请求的数量比 HTTP1.1 大了好几个数量级
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ef2wNLD3-1587207564837)(http://www.txjava.cn/wordpress/wp-content/uploads/2020/04/image-1586502329862.png)]
简单NIO示例
服务端代码:
package nio.demo1;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
public class NIOServer {
/**
* 创建一个缓存区
* @param args
* @throws Exception
*/
static ByteBuffer buffer = ByteBuffer.allocate(1024);
public static void main(String[] args) throws Exception {
//获得服务器断点的服务管道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//绑定端口
serverSocketChannel.socket().bind(new InetSocketAddress(6666));
//设置成异步
serverSocketChannel.configureBlocking(false);
while (true){
//获得客户端连接过来的管道
SocketChannel socketChannel = serverSocketChannel.accept();
if(socketChannel != null){
socketChannel.configureBlocking(false);
int length = socketChannel.read(buffer);
if(length != -1){
System.out.println("接收到的数据是:"+new String(buffer.array(), 0, length));
}
}
}
}
}
客户端代码
public class NIOClient {
public static void main(String[] args) throws Exception {
InetSocketAddress socketAddress = new InetSocketAddress(InetAddress.getLocalHost(), 6666);
Socket socket = new Socket();
socket.connect(socketAddress);
socket.getOutputStream().write("hello".getBytes());
}
}
在服务端由于是非阻塞IO,那么我们无法监控到客户端的连接,所以我们采用不断轮询的方式来监控(显然这是一种性能的巨大消耗),此种方法是同步非阻塞IO模型的一种方式
NIO多路复用解决方案
我们可以看到,我们刚刚手动的方式来做了一个很不合理的方案。那么到底什么样的方案是完美的方案呢?
选择器
- Java 的 NIO,用非阻塞的 IO 方式。可以用一个线程,处理多个的客户端连接,就会使用到 Selector(选择器)
- Selector 能够检测多个注册的通道上是否有事件发生(注意:多个 Channel 以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。
- 只有在连接/通道 真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程, 避免了多线程之间的上下文切换导致的开销
NIO多路复用实现代码
服务端:
package nio.demo2;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
public class NIOServer1 {
public static void main(String[] args) throws Exception {
//获得服务器断点的服务管道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//绑定端口
serverSocketChannel.socket().bind(new InetSocketAddress(6666));
//设置成异步
serverSocketChannel.configureBlocking(false);
//创建Selector
Selector selector = Selector.open();
//注册
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true){
while(selector.select(2000) == 0){
System.out.println("等待连接");
continue;
}
//获得准备就绪的key
Set<SelectionKey> keys = selector.selectedKeys();
//遍历keys
Iterator<SelectionKey> iterator = keys.iterator();
while (iterator.hasNext()){
//获得key
SelectionKey key = iterator.next();
if(key.isAcceptable()){
//获得通道
SocketChannel socketChannel = serverSocketChannel.accept();
if(socketChannel != null){
System.out.println(Thread.currentThread().getName()+" 接收到一个连接:"+socketChannel.getRemoteAddress());
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
}
}
if(key.isReadable()){
//获得通道
SocketChannel channel = (SocketChannel) key.channel();
//获得缓冲区
ByteBuffer buffer = (ByteBuffer) key.attachment();
int length = channel.read(buffer);
if(length != -1){
System.out.println(Thread.currentThread().getName()+" 发送的数据是:"+new String(buffer.array(), 0, length)+" 客户端的地址:"+channel.getRemoteAddress());
}
buffer.clear();
}
iterator.remove();
}
}
}
}
程序流程:
- 服务端启动时候获得Selector,注册给ServerSocketChannel
- Selector 调用select方法, 监听请求和读写事件,返回有事件发生的通道的个数.
- 通过selector获得就绪状态的键集合,然后遍历这个集合判断这个键的类型
- 当客户端连接时key类型是OP_ACCEPT,会通过 ServerSocketChannel 得到 SocketChannel, 把该通道连同buffer注册到selector选择器上。一个 selector 上可以注册多个 SocketChannel
- 注册后返回一个 SelectionKey, 会和该 Selector 关联(集合)
- 进一步得到各个 SelectionKey (有事件发生)
- 如果key的类型是OP_READ或者OP_WRITE(读写),通过 SelectionKey反向获取 SocketChannel , 方法 channel()
- 可以通过得到的channel, 完成业务处理
此刻我会发现我们的NIO使用的就是IO多路复用的模型。
更详细的Spring源码解析请关注:java架构师免费课程
每晚20:00直播分享高级java架构技术
扫描加入QQ交流群264572737