1.传统的IO是堵塞模型。在接收客户端数据前一直是堵塞。服务端代码如下:
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 is;
try {
is = client.getInputStream();
byte[] buf = new byte[512];
while (true) {
int offset = is.read(buf); //堵塞等待客户端输入
if (offset == -1) {
is.close();
client.close();
break;
}
String data = new String(buf, 0, offset);
System.out.println(data);
}
System.out.println("客户端断开");
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
}
这样的服务端模型,客户端连接就会建立一个线程处理数据接收。如果客户端连接的数量多,创建过多的线程对系统的资源开销大,线程的切换频繁。服务器的性能降低。
2. NIO,系统是unblock-IO,JDK是new io。不堵塞的IO模型。
在调用accept和read的时候,不管有没有数据都返回。
public static void main(String[] args) throws Exception {
LinkedList<SocketChannel> clients = new LinkedList<>();
ServerSocketChannel ss = ServerSocketChannel.open();
ss.bind(new InetSocketAddress(9090));
ss.configureBlocking(false); // OS NONBLOCKING!!!
while (true) {
SocketChannel client = ss.accept(); //accept 调用内核了:1,没有客户端连接进来,也有返回值,在BIO 的时候一直卡着,但是在NIO,返回-1,NULL
//如果来客户端的连接,accept 返回的是这个客户端的fd 文件描述符
if (client == null) {
//System.out.println("null.....");
} else {
client.configureBlocking(false); //连接的socket,设置unblock
int port = client.socket().getPort();
System.out.println("client...port: " + port);
clients.add(client); //连接加入到容器
}
ByteBuffer buffer = ByteBuffer.allocate(4096); //可以在堆里 堆外
//遍历已经链接进来的客户端能不能读写数据
Iterator<SocketChannel> iterator = clients.iterator();
while (iterator.hasNext()) {
SocketChannel c = iterator.next();
int offset = c.read(buffer); // >0 -1 0 //不会阻塞
if (offset == -1) {
c.close();
iterator.remove();
} else if (offset == 0) {
// todo something..
} else if (offset > 0) {
buffer.flip(); //ByteBuffer操作接收的数据必须调用这个函数
byte[] aaa = new byte[buffer.limit()]; //limit表示接收到的数据大小
buffer.get(aaa);
String b = new String(aaa);
System.out.println(c.socket().getPort() + " : " + b);
buffer.clear();
}
}
}
}
这样服务端能不堵塞的处理多个连接请求。上一个堵塞IO模型,只能是一个一个接收,不然后面的连接会因为前面的连接没有建立完成,堵塞在哪里,连接慢。
这个io模型应该不是reactive模型,就是服务端接收大量的客户端连接,如果建立连接的,就返回这个socket,如果没有返回就不返回这个连接。也就是只返回连接成功的。
非堵塞模型,实现有select,poll和epoll实现也各不相同。
3.select,poll和epoll。
private ServerSocketChannel server = null;
private Selector selector = null; //linux 多路复用器(select poll epoll kqueue)
int port = 9090;
public void initServer() {
try {
server = ServerSocketChannel.open();
server.configureBlocking(false); // 设置成非阻塞
server.bind(new InetSocketAddress(port)); // 绑定监听的端口号
//如果在epoll模型下,Selector.open()其实完成了epoll_create,可能给你返回了一个 fd3
selector = Selector.open();
// 可以选择 select poll *epoll,在linux中会优先选择epoll 但是可以在JVM使用-D参数修正
//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))
/*
java中的select()是啥意思:
1,如果用select,poll 模型,其实调的是内核的select方法,并传入参数(fd4),或者poll(fd4)
2,如果用epoll模型,其实调用的是内核的epoll_wait()
注意:参数可以带时间。如果没有时间,或者时间是0,代表阻塞。如果有时间,则设置一个超时时间。
方法selector.wakeup()可以外部控制让它不阻塞。这时select的结果返回是0。
*/
while (selector.select(500) > 0) {
Set<SelectionKey> selectionKeys = selector.selectedKeys(); //拿到返回的有状态的fd集合
Iterator<SelectionKey> iter = selectionKeys.iterator(); // 转成迭代器
//所以,不管你是啥多路复用器,你只能告诉我fd的状态,我还得一个一个的去处理他们的R/W。同步好辛苦!!!
//我们之前用NIO的时候,需要自己对着每一个fd调用系统调用,浪费资源,那么你看,这里是不是调用了一次select方法,知道具体的那些可以R/W了?是不是很省力?
while (iter.hasNext()) {
SelectionKey key = iter.next();
iter.remove(); //这时一个set,不移除的话会重复循环处理
if (key.isAcceptable()) { //我前边强调过,socket分为两种,一种是listen的,一种是用于通信 R/W 的
//这里是重点,如果要去接受一个新的连接
//语义上,accept接受连接且返回新连接的FD,对吧?
//那新的FD怎么办?
//如果使用select,poll的时候,因为他们内核没有空间,那么在jvm中保存,和前边的fd4那个listen的放在一起
//如果使用epoll的话,我们希望通过epoll_ctl把新的客户端fd注册到内核空间
acceptHandler(key);
} else if (key.isReadable()) {
readHandler(key);
//在当前线程,这个方法可能会阻塞,如果阻塞了十年,其他的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); //前边讲过了
// 0.0 我类个去
//你看,调用了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) {
start();
}
和2.NIO不同代码不同。上面2中是程序用容器保存文件描述符,然后遍历文件描述符,进行读写。这个由用户自己控制。
3.NIO是用的内核select函数,每次调用,系统内核会帮你遍历文件描述符,状态变化了的,会重新设置好,返回给你。这样效率高。
4.select,poll和epoll的介绍。
select文件描述符存在数量限制,轮询所有的文件描述符性能消耗也大。内核/用户空间内存拷贝。触发方式是水平触发(这个需要多理解)应用程序如果没有完成对一个已经就绪的文件描述符进行IO,那么之后再次select调用还是会将这些文件描述符通知进程;
poll使用链表保存文件描述符,没有监视文件描述符数量的限制,其他问题一样有。
基于select模型,假设我们的服务器需要支持100万的并发连接,则在_FD_SETSIZE为1024的情况下,则我们至少需要开辟1k个进程才能实现100万的并发连接。除了进程间上下文切换的时间消耗外,从内核/用户空间大量的无脑内存拷贝、数组轮询等,是系统难以承受的。因此,基于select模型的服务器程序,要达到10万级别的并发访问,是一个很难完成的任务。
epoll没有select的那些缺点。用的堆外内存,红黑树和链表。红黑树保存所有的文件描述符,可以增删。链表保存有数据的文件描述符。
面试题:epoll怎么知道有数据?
内核中断处理程序注册一个回调函数,告诉内核,如果这个句柄的中断到了,就把它放到准备就绪list链表里。调用epoll_wait 获取链表。
5.总结
这个模型可以实现c10k,至于更大的并发连接,是否需要reactive编程模型(epoll就是这个模型),这个再探究。有在记录。
还有其他的io模型,比如信号驱动IO(SIGIO),和AIO 这个就不深究了,在实际中用到的少。
参考: