系列文章目录
NIO之bio、nio、多路复用器发展历程(一)
NIO之select、poll、epoll 内核触发模型对比(二)
NIO之epoll(三)
前言
ps:如果你是一个每天都在镜子面前给自己磕头硬核男人,可以忽略下面的建议
各位老表,如果是新来的,建议先看下,操作系统相关的知识:深层次详解同步IO、异步IO、阻塞IO、非阻塞IO
各位看官,这边看起。
那得从 long long ago谈起了,当时限于业务和pg智力,最开始流行的是BIO ,每次为新的连接分配一个线程,连接激增时会导致堆栈溢出、线程无法分配、系统崩溃现象。程序员一看,这是要背锅的节奏呀,三下五除二改成了伪异步IO(bio +线程池+任务队列),这下不用担心资源一直分配了,但是换汤不换药,进程阻塞、服务超时、系统崩溃等问题依然存在。dang dang dang 主角来了,NIO (非阻塞io)基于同步非阻塞io模型,在获取连接和从client 获取数据时实现了非阻塞(也可以设置为阻塞),虽然实现了非阻塞,但是还是需要程序循环连接(循环fds-文件描述符列表)一个个发生系统调用(会发生用户态、内核态切换),消耗资源、效率低。那怎么才能不一 一个循环的系统调用呢,你一次性给他1000个,内核自己去循环判断不得了,聪明,多路复用就是起到这个作用,多路复用是一种规则,实现方式有select>poll>epoll ,性能从低到高。
bio(阻塞,流操作)–>nio(只是非阻塞、块操作)–>多路复用器
一、BIO
包: java.io.*
类别
- 传统的bio: 通常有一个独立的线程负责监听客户端的连接,会为每个新来的连接请求创建一个新的线程进行读写操作,由于jvm的资源有限,当访问量激增后,系统性能急剧下降,可能发生堆栈溢出、无法创建新线程等,最终导致进程宕机。
- 伪异步io: 在bio基础上,采用了线程池+任务队列的方式,代替了为每个连接创建一个线程的方式。做到占用的资源可控,保证了服务的可用性。但底层依然采用的同步阻塞模型,由此导致的进程阻塞或者服务超时、系统崩溃等都无法破解。
特点
- 同步堵塞io模型
- accept()获取连接阻塞,方法返回值-1=无数据,只能等着有数据返回,所以抛到新的一个线程处理,耗资源
- client.read() 读取数据阻塞,方法返回值-1=无数据,只能等着有数据返回,
- 系统调用次数多,应用程序循环fds 一次次的调用内核,效率极低
BIO创建连接慢原因
> 三次握手,所有io模型是必走的,
> server端, 主线程,accept(系统调用)发生阻塞(没有连接进入一直阻塞),有连接请求,发生clone (系统调用)每次开启一个子线程,主要慢在clone阶段
例子:使用bio方式创建5W连接 可以观察创建的速度。
客户端代码
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;
import java.util.LinkedList;
/**
* @author: di
* @create: 2020-06-06 15:12
*/
public class C10Kclient {
public static void main(String[] args) {
LinkedList<SocketChannel> clients = new LinkedList<>();
InetSocketAddress serverAddr = new InetSocketAddress("192.168.150.11", 9090);
for (int i = 10000; i < 65000; i++) {
try {
SocketChannel client1 = SocketChannel.open();
client1.bind(new InetSocketAddress("192.168.150.1", i));
// 192.168.150.1:10000 192.168.150.11:9090
client1.connect(serverAddr);
clients.add(client1);
} catch (IOException e) {
e.printStackTrace();
}
}
System.out.println("clients "+ clients.size());
try {
System.in.read();
} catch (IOException e) {
e.printStackTrace();
}
}
}
服务端代码
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
public class SocketIO {
public static void main(String[] args) throws Exception {
ServerSocket server = new ServerSocket(9090,20);
System.out.println("step1: new ServerSocket(9090) ");
while (true) {
Socket client = server.accept(); //阻塞1
System.out.println("step2:client\t" + client.getPort());
new Thread(() -> {
InputStream in = null;
try {
in = client.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
while(true){
String dataline = reader.readLine(); //阻塞2
if(null != dataline){
System.out.println(dataline);
}else{
client.close();
break;
}
}
System.out.println("客户端断开");
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
}
}
可以忽略这部分
/**
可以忽略这里,只是处理本地网卡和虚拟网卡网络互通问题
window机器,本地会有一块网卡(例如ip:192.168.110.100),安装了vmware后,以后用一个虚拟网卡net8
(例如,虚拟网络为:192.168.150.0,net网关服务为:192.168.150.2),在vm上安装虚拟机node1(ip为192.168.150.11),
创建11W 个连接,通过本地的两个ip(192.168.150.1、192.168.110.100)+port,循环5.5W次,
调用server端(192.168.150.11:9090)创建连接,需要增加一个route关系,不然client可以往虚拟机server发送,
server无法给192.169.110.100的发送ack,网络不通。
route -n:
*/
二、NIO
定义
- NIO: 官方称为 New I/O,但称为非阻塞io,更能体现NIO的特点,同步非阻塞。
包: java.nio.*
特点
- 非阻塞 (可以设置阻塞或者非阻塞)
- accept() 获取连接非阻塞。该方法立即返回,底层返回-1,方法返回null,无连接;有连接会返回连接 client
- client.read(buffer),读取数据不阻塞,有数据返回正数,无数据返回非正数。
- 可以在一个线程里面处理接收连接和读取数据,
缺点
- 一个线程可以处理连接和数据读取等,假如10W连接,但是进行数据读取时,循环一次,无论有无数据都会进行10W次的系统调用,很多调用无意义,消耗很大。
- read是没错的,但是无用无效的read 在被不断调用,用户态、内核态不断切换。
NIO和传统IO(简称IO)之间最大的区别
- IO是面向流的,Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方,操作简单,但慢;NIO是面向缓冲区的。 NIO 以块的方式处理数据,可以前后移动的操作数据,每一个操作都在一步中产生或者消费一个数据块,按块处理数据比按(流式的)字节处理数据要快得多,但缺少IO的优雅性和简单性。
- IO同步阻塞;NIO同步非阻塞
例子:同nio方式创建5W连接,注意观察速度
客服端代码
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;
import java.util.LinkedList;
/**
* @author: di
* @create: 2020-06-06 15:12
*/
public class C10Kclient {
public static void main(String[] args) {
LinkedList<SocketChannel> clients = new LinkedList<>();
InetSocketAddress serverAddr = new InetSocketAddress("192.168.150.11", 9090);
for (int i = 10000; i < 65000; i++) {
try {
SocketChannel client1 = SocketChannel.open();
client1.bind(new InetSocketAddress("192.168.150.1", i));
// 192.168.150.1:10000 192.168.150.11:9090
client1.connect(serverAddr);
clients.add(client1);
} catch (IOException e) {
e.printStackTrace();
}
}
System.out.println("clients "+ clients.size());
try {
System.in.read();
} catch (IOException e) {
e.printStackTrace();
}
}
}
服务端代码
import java.net.InetSocketAddress;
import java.net.StandardSocketOptions;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.LinkedList;
public class SocketNIO {
public static void main(String[] args) throws Exception {
LinkedList<SocketChannel> clients = new LinkedList<>();
ServerSocketChannel ss = ServerSocketChannel.open();// 相当于fd
ss.bind(new InetSocketAddress(9090));// 绑定端口
ss.configureBlocking(false); //重点 OS NONBLOCKING!!!
ss.setOption(StandardSocketOptions.TCP_NODELAY, false);
/**
主线程里面负责接收连接及遍历连接获取数据
*/
while (true) {
Thread.sleep(1000);
//不会阻塞,返回 -1 =null 以为着无连接
// NONBLOCKING 的时候,非阻塞,没有连接返回为null;设置Blocking 为阻塞 ,直到有连接进来,accept() 才往下走
SocketChannel client = ss.accept();
/** accept 调用了内核,
1、没有客户端连接进来,返回值? BIO 的时候,一直卡着,但在NIO不卡着,内部返回0,accept返回null。
2、如果有客户端的连接,内部是返回的是客服端的fd ,该方法client 为 object
3、NONBLOCKING 就是代码能往下走了,只不过有不同的情况
*/
if (client == null) {
System.out.println("null.....");
} else {
client.configureBlocking(false);// 重点 socket(服务端的listen socket<双方三次握手后,往这里扔,我去通过accept得到后面的连接socket>,,连接的socket<连接后的数据传递使用> ),指的下面的c.read(buffer) 读取不阻塞
int port = client.socket().getPort();
System.out.println("client...port: " + port);
clients.add(client);
}
// ByteBuffer buffer = ByteBuffer.allocateDirect(4096); //可以在堆里 堆外
for (SocketChannel c : clients) { //串行化!!!! or 多线程!!
int num = c.read(buffer); // >0 -1 0 //不会阻塞,有无数都会返回,但会发生系统态切换,影响速度
if (num > 0) {
buffer.flip();
byte[] aaa = new byte[buffer.limit()];
buffer.get(aaa);
String b = new String(aaa);
System.out.println(c.socket().getPort() + " : " + b);
buffer.clear();
}
}
}
}
}
错误
1、Exception in thread “main” java.io.IOException:Too many open file
ulimit -a : 可以查看open files 等全部配置
ulimit -n : 只查看 open files 限制大小 (一般用户是根据这个限制来的,root会突破限制),1024 一个进程最大可以打开的fd数量。
ulimit -sha 50000 : 把openfiles 设置为5W
netstat -natp: 网络连接状态
三、多路复用器
定义
多条路(下方图中多个io链路):通过一个系统调用(就是复用),把一批fd 传给kernel,内核获取fds其中的io状态,然后由程序自己对有状态的io进行读写。
多路复用器是一种规范,会有select、poll、epoll (三者都是同步非阻塞模型)实现。三种方式体现到java就是selector<多路开关选择器>,一个选择器能够管理多个信道上的I/O操作,
nio:直接采用和bio一样的获取连接方式accept(),
nio+多路复用:也可以通过将socketChannel 注册到selector 上的方式,使用selector 多路复用器获取有状态连接,
与Selector一起使用时,Channel必须处于非阻塞模式下。这意味着不能将FileChannel与Selector一起使用,因为FileChannel不能切换到非阻塞模式。而套接字通道都可以。
**例子:**通过创建5W连接,可以看到速度明显快了很多。
客户端代码
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;
import java.util.LinkedList;
/**
* @author: di
* @create: 2020-06-06 15:12
*/
public class C10Kclient {
public static void main(String[] args) {
LinkedList<SocketChannel> clients = new LinkedList<>();
InetSocketAddress serverAddr = new InetSocketAddress("192.168.150.11", 9090);
for (int i = 10000; i < 65000; i++) {
try {
SocketChannel client1 = SocketChannel.open();
client1.bind(new InetSocketAddress("192.168.150.1", i));
// 192.168.150.1:10000 192.168.150.11:9090
client1.connect(serverAddr);
clients.add(client1);
} catch (IOException e) {
e.printStackTrace();
}
}
System.out.println("clients "+ clients.size());
try {
System.in.read();
} catch (IOException e) {
e.printStackTrace();
}
}
}
服务端代码
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
public class SocketMultiplexingSingleThread {
private ServerSocketChannel server = null;
//linux 多路复用器(select>poll>epoll)
private Selector selector = null;
int port = 9090;
public void initServer() {
try {
server = ServerSocketChannel.open();//fd4
server.configureBlocking(false);
server.bind(new InetSocketAddress(port));
//如果在epoll模型下,open--》 epoll_create -> fd3
// select poll epoll 优先选择:epoll , 但是可以 -D修正指定使用哪个多路复用器
selector = Selector.open();
//server 约等于 listen状态的fd4
/*
register
如果:
select,poll:jvm里开辟一个数组 fd4 放进去
epoll:epoll_ctl(fd3,ADD,fd4,EPOLLIN) 红黑树
*/
server.register(selector, SelectionKey.OP_ACCEPT);
} catch (IOException e) {
e.printStackTrace();
}
}
public void start() {
initServer();
System.out.println("服务器启动了。。。。。");
try {
while (true) { //死循环
Set<SelectionKey> keys = selector.keys();
System.out.println(keys.size()+" size");
//1,调用多路复用器(select,poll or epoll (epoll_wait))
/*
select()是啥意思:
1,select、poll :其实是内核的select(fd4)、 poll(fd4)
2,epoll: 其实内核的epoll_wait()
参数可以带时间:没有时间,0 : 阻塞,有时间设置一个超时
selector.wakeup() 结果返回0
懒加载:
其实在触碰到selector.select()调用的时候才触发了epoll_ctl的调用
*/
while (selector.select() > 0) {
Set<SelectionKey> selectionKeys = selector.selectedKeys(); //返回的有状态的fd集合
Iterator<SelectionKey> iter = selectionKeys.iterator();
//管你啥多路复用器,你只能给我状态,我还得一个一个的去处理他们的R/W。同步好辛苦!
// NIO 自己对着每一个fd调用系统调用,浪费资源,那么你看,这里是不是调用了一次select方法,知道具体的那些可以R/W了?
//前边强调过,socket: listen 通信 R/W
while (iter.hasNext()) {
SelectionKey key = iter.next();
iter.remove(); //set 不移除会重复循环处理
if (key.isAcceptable()) {
//看代码的时候,这里是重点,如果要去接受一个新的连接
//语义上,accept接受连接且返回新连接的FD对吧?
//那新的FD怎么办?
//select,poll,因为他们内核没有空间,那么在jvm中保存和前边的fd4那个listen的一起
//epoll: 我们希望通过epoll_ctl把新的客户端fd注册到内核空间
acceptHandler(key);
} else if (key.isReadable()) {
readHandler(key); //连read 还有 write都处理了
//在当前线程,这个方法可能会阻塞 ,如果阻塞了十年,其他的IO早就没电了。。。
//所以,为什么提出了 IO THREADS
//redis 是不是用了epoll,redis是不是有个io threads的概念 ,redis是不是单线程的
//tomcat 8,9 异步的处理方式 IO 和 处理上 解耦
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public void acceptHandler(SelectionKey key) {
try {
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
SocketChannel client = ssc.accept(); //来啦,目的是调用accept接受客户端 fd7
client.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.allocate(8192); //前边讲过了
//你看,调用了register
/*
select,poll:jvm里开辟一个数组 fd7 放进去
epoll: epoll_ctl(fd3,ADD,fd7,EPOLLIN)
*/
client.register(selector, SelectionKey.OP_READ, buffer);
System.out.println("-------------------------------------------");
System.out.println("新客户端:" + client.getRemoteAddress());
System.out.println("-------------------------------------------");
} catch (IOException e) {
e.printStackTrace();
}
}
public void readHandler(SelectionKey key) {
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
buffer.clear();
int read = 0;
try {
while (true) {
read = client.read(buffer);
if (read > 0) {
buffer.flip();
while (buffer.hasRemaining()) {
client.write(buffer);
}
buffer.clear();
} else if (read == 0) {
break;
} else {
client.close();
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
SocketMultiplexingSingleThreadv1 service = new SocketMultiplexingSingleThreadv1();
service.start();
}
}
附加----------------------------------------------------------------
IO分两阶段:
- 1.数据准备阶段:数据从网卡、磁盘复制到内核缓存区
- 2.内核空间复制到用户进程缓冲区、
阻塞、非阻塞、同步、异步 只关注io,不关注io读写之后的事情
- 同步: 强调io的第二步,需要程序自己去完成R/W
- 异步: 强调io的第二步,需要kerner自己去完成R/W,不依赖程序
- 阻塞: 程序执行没有等到结果,一直等着
- 非阻塞: 无论有无数据都直接返回,不会让当前线程一直等着。有数据返回数据,没数据返回空。
可以参考此文:[深层次详解同步IO、异步IO、阻塞IO、非阻塞IO]