Java IO

Java IO

BIO

{
 ExecutorService executor = Excutors.newFixedThreadPollExecutor(100);//线程池

 ServerSocket serverSocket = new ServerSocket();
 serverSocket.bind(8088);
 while(!Thread.currentThread.isInturrupted()){//主线程死循环等待新连接到来
    Socket socket = serverSocket.accept();
    executor.submit(new ConnectIOnHandler(socket));//为新的连接创建新的线程
}

class ConnectIOnHandler extends Thread{
    private Socket socket;
    public ConnectIOnHandler(Socket socket){
       this.socket = socket;
    }
    public void run(){
      while(!Thread.currentThread.isInturrupted()&&!socket.isClosed()){死循环处理读写事件
          String someThing = socket.read()....//读取数据
          if(someThing!=null){
             ......//处理数据
             socket.write()....//写数据
          }

      }
    }
}

这个模型最本质的问题在于,严重依赖于线程。但线程是很”贵”的资源,主要表现在:

    1. 线程的创建和销毁成本很高,在Linux这样的操作系统中,线程本质上就是一个进程。创建和销毁都是重量级的系统函数。
    1. 线程本身占用较大内存,像Java的线程栈,一般至少分配512K~1M的空间,如果系统中的线程数过千,恐怕整个JVM的内存都会被吃掉一半。
    1. 线程的切换成本是很高的。操作系统发生线程切换的时候,需要保留线程的上下文,然后执行系统调用。如果线程数过高,可能执行线程切换的时间甚至会大于线程执行的时间,这时候带来的表现往往是系统load偏高、CPU sy使用率特别高(超过20%以上),导致系统几乎陷入不可用的状态。
    1. 容易造成锯齿状的系统负载。因为系统负载是用活动线程数或CPU核心数,一旦线程数量高但外部网络环境不是很稳定,就很容易造成大量请求的结果同时返回,激活大量阻塞线程从而使系统负载压力过大。

NIO

Channels

打开 SocketChannel

SocketChannel ch = SocketChannel.open();  
ch.connect(new InetSocketAddress("somehost", someport));  

Server 监听在某个端口上:

ServerSocketChannel ch = ServerSocketChannel.open();  
ch.socket().bind (new InetSocketAddress (somelocalport));  

Buffers

Selectors

NIO 编程示例

下面具体看下如何利用事件模型单线程处理所有I/O请求:

NIO 的主要事件有几个:读就绪、写就绪、有新连接到来

我们首先需要注册当这几个事件到来的时候所对应的处理器。然后在合适的时机告诉事件选择器:我对这个事件感兴趣。对于写操作,就是写不出去的时候对写事件感兴趣;对于读操作,就是完成连接和系统没有办法承载新读入的数据的时;对于accept,一般是服务器刚启动的时候;而对于connect,一般是connect失败需要重连或者直接异步调用connect的时候。

其次,用一个死循环选择就绪的事件,会执行系统调用(Linux 2.6之前是selectpoll,2.6之后是epoll,Windows是IOCP),还会阻塞的等待新事件的到来。新事件到来的时候,会在selector上注册标记位,标示可读、可写或者有连接到来。

注意,select是阻塞的,无论是通过操作系统的通知(epoll)还是不停的轮询(selectpoll),这个函数是阻塞的。所以你可以放心大胆地在一个while(true)里面调用这个函数而不用担心CPU空转。

interface ChannelHandler{
    void channelReadable(Channel channel);
    void channelWritable(Channel channel);
}
class Channel{
    Socket socket;
    Event event;//读,写或者连接
}

//IO线程主循环:
class IoThread extends Thread{

    @Override
    public void run() {
        Channel channel;
        while(channel=Selector.select()){//选择就绪的事件和对应的连接
            if(channel.event==accept){
                registerNewChannelHandler(channel);//如果是新连接,则注册一个新的读写处理器
            }
            if(channel.event==write){
                getChannelHandler(channel).channelWritable(channel);//如果可以写,则执行写事件
            }
            if(channel.event==read){
                getChannelHandler(channel).channelReadable(channel);//如果可以读,则执行读事件
            }
        }
    }
    Map<ChannelChannelHandler> handlerMap;//所有channel的对应事件处理器
}

select

select 不断轮循去监听 socket,socket 个数有限制,一般是 1024/2048FD_SETSIZE 控制,需要重新编译内核才可以修改宏。线性扫描全部 socket 集合。

select 缺点:每次调用 select 都需要把 fd 集合从用户态拷贝到内核态epoll 对于每个文件描述符只会拷贝一次到内核,不用重复拷贝。

poll

poll 轮循方式监听,只不过没有最大连接数的限制。线性扫描

epoll

epoll 不是轮循监听,而是 socket 有变化时通过回调的方式主动告知用户进程。

epoll 缺点:

  • 大部分套接字活跃的情况下,性能比不上 select/poll。它更适合处理大量的 fd,且活跃 fd 不是很多的情,反之如果处理的 fd 量不大,且基本都是活跃的,过多使用 epoll_ctl,效率相比还有稍微的下降。
  • 跨平台性差

epoll 原理:

epoll 由 epoll_createepoll_ctlepoll_wait 这三个底层函数支持,使用的时候用 epoll_create 创建对象,参数 site 是保证内核保证能正确处理的最大句柄数。epoll_ctl 可以添加/移除 socket。epoll_wait 在给定的 timeout 时间内,如果有事件发生,那么就返回变化的文件句柄。

socket 在 epoll 内部以红黑树形式存储,支持快速查找、删除和插入。除了这个红黑树,还会建立一个链表,用来存储准备就绪的事件,当 epoll_wait 调用时,就查看这个链表里是不是为空就可以。所以 epoll_wait 仅仅拷贝少量的句柄返回给内核态,所以比较高效。

当调用 epoll_ctl 时候,除了把 socket 放入红黑树,还会向内核注册一个回调函数,中断到了,那么就将它放到这个就绪链表里面。

另外 epoll 有 ET 和 LT 两种触发模式,LT 的话,只要一个句柄上的事件没有处理完,以后调用 epoll_wait 还会返回这个句柄,而 ET 仅仅在第一次返回。

epoll 使用流程:

int epoll_fd = epoll_create(0);
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, 0, &event);
while(running) 
    event_count = epoll_wait(epoll_fd, events, MAX_EVENTS, 30000);

AIO

参考