Java 中的网络IO(2)

252 阅读5分钟

在 Java 中的网络IO(1) 中说到JDK中的NIO 的弊端,因为 即使NIO没有了BIO的阻塞,可以让accept read write 都在一个单线程中可以完成处理。
但是!!!
每一次每一次遍历 socketList 时 我们都会调用read而read 在是底层操作系统提供的一个系统调用,我们每一次调用都要在用户态和内核态的切换,而每一次群换都要遍历一次socketList都会调用一遍系统调用,这样子带来的开销 ,是巨大的。同样还有同时多个连接进来一次只能接收一个连接就要继续去遍历socket。

这时候操作系统提供了一个新的系统调用,select 多路复用器。select 是POSIX标准中所提出的。

来看看什么是多路复用器

为什么会出现: 如上所说,NIO 要想知道socket中是否有数据必须去遍历读取 如果有数据就返回没有就返回-1。
这样每一次都去遍历查看的系统调用极其耗费资源。因此多路复用就出现了。只要调用 select()这个系统调用就能知道各个IO通路的状态 ,注意只是状态不是获得IO中的数据。将各个IO通路的状态返回给用户程序后就可以对有状态的IO进行相应的处理。这样子是不是就节省了大量的开销 。

也就是说每一次只要一个select的系统调用就可以获得各个IO通路的状态,然后对对有状态的IO进行相应的处理,比每次都对所有连接进行遍历节省了大量开销。

来自周志明老师

而在Linux中对多路复用器的实现有三个,**select poll epoll **

首先来看看 select 和 poll

select 和 poll 在Linux中的功能基本完全一样,但是select 限制了最大连接数为1024个,而poll没有限制。

这也可以从linux 手册中看到这两个系统调用的签名中也可以看出,
都是传入所有要查看状态的文件描述符 ,然后返回对应的各个FD 的状态

弊端此时我们可以看出select 和 poll 的弊端,那就是
1.遍历,在实质上 select 和 poll还是对所有 IO通路进行遍历再返回对应的状态,虽然这样子已经将遍历放入内核中省去了很多的内核态切换带来的开销,但是遍历本身的开销也是有的。
2.要重复传递FDS也就是每一次调用都要重复传递要检测的IO通路的文件描述符。

由于select 和 poll的这些弊端 Linux 提出了 Epoll Event poll

更好的多路复用器 Epoll

Epoll 不再去遍历所有FDS ,而是基于事件触发的形式(中断触发) (我的理解)

首先我们要知道计算机的外设可以发出 中断信号,而网络是基于网卡这个外设的,当网络中有数据传来网卡就会发出中断信号告诉cpu ,此时 操作系统就会对网卡中的数据通过网络协议栈最终关联到 kernel中 FD 的buffer 上。而这之间 os 还可以执行callback 也就是在把数据放入内核之前,os 还可以做一些手脚。

epoll 就是基于此,数据来之后kernel 不只是把数据放入内核,还将FD的 状态更新到一个 FDlist上 调用 Epoll 时不再遍历而是直接取出上一次 网卡中断时 所调用的callback所更新的 FDlist;

而Epoll 的实现是基于 Epoll_create Epoll_ctl Epoll_wait

这样子就极大的提高了处理性能

Java中的Selector使用

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;

public class SocketMultiplexingSingleThreadv1 {


    private ServerSocketChannel server = null;
    private Selector selector = null;   //linux 多路复用器(select poll    epoll kqueue) nginx  event{}
    int port = 9090;

    public void initServer() {
        try {
            server = ServerSocketChannel.open();
            server.configureBlocking(false);
            server.bind(new InetSocketAddress(port));


            //如果在epoll模型下,open--》  epoll_create -> fd3
            selector = Selector.open();  //  select  poll  *epoll  优先选择:epoll  但是可以 -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))
                /*
                select()是啥意思:
                1,select,poll  其实  内核的select(fd4)  poll(fd4)
                2,epoll:  其实 内核的 epoll_wait()
                *, 参数可以带时间:没有时间,0  :  阻塞,有时间设置一个超时
                selector.wakeup()  结果返回0

                懒加载:
                其实再触碰到selector.select()调用的时候触发了epoll_ctl的调用

                 */
                while (selector.select(50) > 0) {
                    Set<SelectionKey> selectionKeys = selector.selectedKeys();  //返回的有状态的fd集合
                    Iterator<SelectionKey> iter = selectionKeys.iterator();
                    //so,管你啥多路复用器,你呀只能给我状态,我还得一个一个的去处理他们的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);  //前边讲过了

            // 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) {
        SocketMultiplexingSingleThreadv1 service = new SocketMultiplexingSingleThreadv1();
        service.start();
    }
}

默认为Epoll

指定为Poll java -Djava.nio.channels.spi.SelectorProvider=sun.nio.ch.PollSelectorProvider