IO NIO 实现demo

本文通过对比Java传统的IO与NIO的实现,深入解析NIO的工作原理。通过实例代码演示NIO的ServerSocketChannel、SocketChannel、Selector和SelectionKey的使用,阐述NIO如何在一个线程中处理多个连接,以及NIO非阻塞特性的理解。建议通过telnet配合调试加深理解。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

1.传统的io实现demo

2.nio实现demo


1.传统的io实现demo

public static void main(String[] args) throws IOException {
 ServerSocket server = new ServerSocket(8080);
 System.out.println("服务端已经开启");
 while (true) {
 Socket socket = server.accept();
 BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
 String buffer = null;
 while ((buffer = br.readLine()) != null && !buffer.equals("")) {
 System.out.println(buffer);
 }
 
 Charset charset = Charset.forName("UTF-8");
 BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), charset));
 bw.write("HTTP/1.1 200 OK\n");
 bw.write("Content-Type: text/html;charset=UTF-8\n\n");
 bw.write("BIO success");
 bw.flush();
 bw.close();
 br.close();
 socket.close();
 }
 }

这个demo可以用telnet或者浏览器去连接,但浏览器在连接的同时就发送了信息过去

首先,这是阻塞的,阻塞的地方有两个:一个是server.accept();另一个是br.readLine(),也就是说必须要连接进来,并且输出数据,否则就一直停在这里。换句话说就是,这个主线程只能处理这一个连接,在处理完这个连接之前,任何其他连接都别想进来。这就是阻塞。tomcat在新版本里已经完全不支持这种传统io模式了,之前的老版本里默认使用的是bio,因为tomcat使用的都是短连接,发送接收都很快,所以可以处理完这个连接后马上处理下一个连接,而且tomcat还使用了连接池,所以就可以支持更多连接的接入。但,这一切都没能阻止tomcat抛弃bio。(大家在启动tomcat时可以看到控制台的提示目前是使用nio还是bio连接)

2.nio实现demo

public class NIOServer {
 private Selector selector;
 
 public void init() throws IOException {
 // 创建一个选择器
 this.selector = Selector.open();
 // 使用通道计划,服务器
 ServerSocketChannel channel = ServerSocketChannel.open();
 // 这个通道非常高效,所以要非阻塞
 channel.configureBlocking(false);
 ServerSocket serverSocket = channel.socket();
 InetSocketAddress address = new InetSocketAddress(8080);
 serverSocket.bind(address);
 System.out.println("NIO抢票神器已经成功启动,端口8080");
 channel.register(selector, SelectionKey.OP_ACCEPT);// 注册事件
 }
 
 public void start() throws IOException {
 // 24小时营业,无限循环
 while (true) {
 // 选择器运行
 this.selector.select();
 Iterator<SelectionKey> iterator = this.selector.selectedKeys().iterator();
 // 抢票软件对每一个用户负责,帮大家处理事情
 while (iterator.hasNext()) {
 SelectionKey key = iterator.next();
 iterator.remove();// 移除,防止重复操作
 if (key.isAcceptable()) {// 这个是填写抢票的请求
 accept(key);
 } else if (key.isReadable()) {// 这个是抢票的操作--狂差12306
 read(key);
 }
 }
 }
 
 }
 
 private void accept(SelectionKey key) throws IOException {
 // 事件传过来的key
 ServerSocketChannel server = (ServerSocketChannel) key.channel();
 // 转成客户端的通道
 SocketChannel channel = server.accept();
 // 设置成非阻塞
 channel.configureBlocking(false);
 // 注册一个读事件,用作去12306抢票
 channel.register(this.selector, SelectionKey.OP_READ);// 注册事件
 }
 
 // 12306的抢票事件
 private void read(SelectionKey key) throws IOException {
 // 创建一个缓冲区,大家一起上咯,效率高
 ByteBuffer buffer = ByteBuffer.allocate(1024);
 SocketChannel channel = (SocketChannel) key.channel();
 // 在通道中读取大家的请求
 channel.read(buffer);
 String request = new String(buffer.array()).trim();// trim方法用于删除字符串两边的空格字符串
 System.out.println("抢票客户的请求" + request);
 String outString = "HTTP/1.1 200 OK\n" + "Content-Type: text/html;charset=UTF-8\n\n" + "NIOsuccess";
 ByteBuffer outBuffer = ByteBuffer.wrap(outString.getBytes());
 // 把我们的数据返回给通道(抢到票了)
 channel.write(outBuffer);
 channel.close();
 }
 
 public static void main(String[] args) throws Exception {
 NIOServer server = new NIOServer();
 server.init();
 server.start();
 
 }
 
}

这个demo建议大家打断点,配合telnet好好debug几遍,收获绝对很大。

其实我在想着大家如果学过单片机就好了,单片机里面的中断机制就和这个nio的思想一模一样。我理解起来就非常快,现在我用另一种方式和大家讲解。

首先要知道nio中使用的基本元素:ServerSocketChannel,这个就和bio中的ServerSocket类似,SocketChannel就和Socket类似,然后就是nio还有Selector(选择器)和SelectionKey这两个东西。(这里建议大家可以去看看API文档里的解释,预热一下)

然后我在宏观上比喻一下,大家在心里建立一个模型。首先,我们要知道这个nio是要一个线程处理多个连接的。就像一个人经营的咖啡店,当来客人的时候你得去招待一下,当客人下单的时候你也得快速做好咖啡然后才能才有空。但是在一个客人进来后和下单前这段时间,你可以招呼另一个客人进来,或者替另一个客人做咖啡。如果是bio的话,当你进来一位客人,你就必须要把他招待完毕才能接待下一个客人。咖啡店的老板就是那个Selector,SelectionKey是一个工具。

然后我们来仔细看程序,首先是init方法:

public void init() throws IOException {

// 创建一个选择器,就是那个咖啡店的老板

this.selector = Selector.open();

// 使用通道计划

ServerSocketChannel channel = ServerSocketChannel.open();

// 这个通道非常高效,所以要非阻塞,也只能是非阻塞

channel.configureBlocking(false);

ServerSocket serverSocket = channel.socket();

InetSocketAddress address = new InetSocketAddress(8080);

serverSocket.bind(address);//实现端口绑定

System.out.println("NIO抢票神器已经成功启动,端口8080");

channel.register(selector, SelectionKey.OP_ACCEPT);// 注册事件,也就是要老板在空闲的时候要看看门口有没有客人,有客人的话要来去开门迎接客人,查看API可知:OP_ACCEPT, 用于套接字接受操作的操作集位。

}

接着看下一段代码:

public void start() throws IOException {

// 24小时营业,无限循环

while (true) {

// 选择器运行,也就是说老板开始工作,程序第一次运行到这里会阻塞住,需要接入一个客户端才能运行后面的程序

this.selector.select();

Iterator<SelectionKey> iterator = this.selector.selectedKeys().iterator(); 这里就是SelectionKey的set集合,然后遍历这个set

while (iterator.hasNext()) {

SelectionKey key = iterator.next();

iterator.remove();// 移除,防止重复操作

if (key.isAcceptable()) {// 这个就表示客户端是刚连接进来的,前面的OP_ACCEPT就注册了连接进来的客户端会被监听到,所以选择器才会对此有反应

accept(key);//这个就是类似于打开门来迎接客人,然后表示后面我要留意这个人的下单。

} else if (key.isReadable()) {// 这个就表示客户端是已经连接进来的,所以接下来要接收客户端的字符数据

read(key);//这个就类似于处理客人的下单,去泡咖啡去

}}}}

然后来看accept方法:

private void accept(SelectionKey key) throws IOException {

// 事件传过来的key,从这里就可以看出这个SelectionKey其实就类似于保存了数据

ServerSocketChannel server = (ServerSocketChannel) key.channel();

// 转成客户端的通道

SocketChannel channel = server.accept();

// 设置成非阻塞

channel.configureBlocking(false);

// 注册一个读事件, 也就是去留意这个客户端是否有数据传输过来

channel.register(this.selector, SelectionKey.OP_READ);// 注册事件

}

然后看read方法:

private void read(SelectionKey key) throws IOException {

ByteBuffer buffer = ByteBuffer.allocate(1024);//一次性可以接收多少数据

SocketChannel channel = (SocketChannel) key.channel();

// 在通道中读取客户端的数据

channel.read(buffer);

String request = new String(buffer.array()).trim();// trim方法用于删除字符串两边的空格字符串

System.out.println("抢票客户的请求" + request);

String outString = "HTTP/1.1 200 OK\n" + "Content-Type: text/html;charset=UTF-8\n\n" + "NIOsuccess";

ByteBuffer outBuffer = ByteBuffer.wrap(outString.getBytes());

// ByteBuffer包装数据再返回给通道

channel.write(outBuffer);

channel.close();

}

默默表示自己的语言表达能力不行,不知道大家有没有理解,其实就是咖啡店的老板要留意每个客人的下单和从门口敲门的客人,当发生上面的任何一个事件,老板都会注意到,然后去做相应的处理。其实事件需要的数据都放在SelectionKey里面,取出key,然后判断key是什么类型的,然后做出相应的处理,前面的就是看到你是accept的,就帮你注册一个read事件。看到你是read的,就返回一个数据给你。

嗯,最后的建议就是,好好debug一下程序,然后开上几个telnet,一步一步去运行代码,相信你很快就能理解nio的思想了。

然后讨论一下为啥说nio是非阻塞的。这就涉及到对阻塞的定义了,this.selector.select();也是会停止在那里啊,为啥说非阻塞呢?我觉得是只要在这个选择器上注册的事件都能被监听,每来一个连接我都能去处理你(除非你处理上一个事件花了很久时间,一直不处于空闲状态,但是什么代码要运行这么久。。),然后再看看bio是只能完全处理完一个连接后才能接待下一个连接,然后你就能理解阻塞了。然后就是select还有其他方法,可以唤醒这个阻塞,可以定时唤醒。

//更新

看完epoll之后,我发现前面的认识真的是太浅了!不学操作系统,何以为家!强烈建议好好学习操作系统!

我再来总结一次:nio是基于epoll的(我觉得是epoll的边缘非阻塞模式,后面再去确认,因为基本一模一样)。select()方法就相当于epoll_wait方法,它会阻塞,也会自己唤醒自己去内核检查是否有就绪的文件描述符。 非阻塞,我想应该和文件描述符fd而言的有关,就类似于上面的channnel,把它设置为非阻塞的,这是为了在读取缓冲区数据时不会一直阻塞在那里。而bio中,不仅文件描述符是阻塞的,而且还是直接调用accept这个函数,这样就产生了多线程处理。现在想想,也是前面说的原因,tomcat之前基本都是短连接,又使用线程池,所以线程快速的产生和灭亡,尽量不会占用大量的线程资源。但是这都很极限了。而且线程的产生和灭亡本身也耗费很大的资源,这样服务器怎么能承受百万连接呢。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值