JAVA盖楼——IO

这里对IO的学习做一个记录:

什么是I/O?

简单的将就是input和output,通常我们对I/O的操作可分为文件I/O操作,这时候可以使用文件流方式进行。而大部分的应用系统之间的交互则是用到网络I/O,因此这里也针对网络I/O进行学习。

I/O有几种模型?

I/O的概念,其实主体是针对当前应用系统所运行的操作系统而言(开发过网络编程的同学如果不清楚其概念的话很容易将它与网络通讯搞混淆)
这里沿用几张图: tcp连接以及网络I/O的几个问题

应用程序通过网络获取到数据通常有2个步骤:

  1. 数据通过网络获取到分组的数据后先方在内核的某个缓冲区中
  2. 准备好后再从内核拷贝到用户空间
    在这里插入图片描述
    在这里插入图片描述
    从阻塞和非阻塞的两张图中可以看出,阻塞就是线程交出cpu处于阻塞状态等待数据准备好后再继续执行,而非阻塞则是进程不交出cpu不断尝试获取数据。这样的情况如果数据准备时常不长的话,则可以避免因为上下文切换而引起的系统消耗。
    阻塞I/O是在I/O的操作上进行了阻塞。
    前面两种方式都是同步方式。
    在这里插入图片描述
    I/O复用模型,是调用系统函数,select、poll去进行io资源的监控,所以其阻塞是在调用select等函数上,而不是具体的i/o操作。它的优势是在于select函数可以同时监控多个连接请求,而不必没有请求线程都去执行操作。其他对于当个连接而言并没有加快它的访问速度

这里有几个概念理清楚了会更好的理解多路复用:
(1) 首先是系统的并发量,是只同一时间请求的数量。
(2)系统的并行量,是只同一时间能处理的并发数。可以直到针对程序某一个时间能处理的并发量是有限的,可以根据cpu核数分配最适合的并行数。
(3)假设我们的应用系统是单线程运行的,如redis
(4)如果不是多路复用,则一个i/o请求过来之后,单线程系统就会阻塞在i/o上等待本地i/o结束,才能处理下一个请求。而采用多路复用后就等于单线程可以一次处理多个i/o请求,大大增加了系统的并发和资源的利用。 这里可能有小伙伴会问那是不是可以采用多线程的方式去处理i/o请求?理论上应该是可以的但要直到系统的线程资源是很有限的,增加线程为增加很多额外的开销。
(5) 多路复用调用的select等方法使用的是操作系统内核函数,速度上会快很多

这也是为什么redis采用,多路复用技术。

在这里插入图片描述
信号驱动IO是指进程事先告诉内核,在某个描述符上发生某事的时候通知进程,所以在数据准备阶段进程不进行阻塞

针对使用套接字连接的应用,需要下面的步骤

//设置sgio信号处理函数,用于处理sigio信号
signal(SIGIO, sig_io);
// 设置套接字的属主进程,因该在设置套接字属主之前建立信号处理函数,因为在调用fcntl后调用signal之前有较小的机会产生SIGIO信号,此时信号被丢弃
fcntl(sockfd, F_SETOWN, getpid());
//开启该套接字的信号驱动式I/O
const int on = 1;
ioctl(sockfd,  O_ASYNC,  &on);

信号驱动IO模式其中一个主要的部分是针对接收到信号的处理,针对TCP的套接字而言,下列情况会产生信号:

  1. 监听套接字上某个连接请求已经完成
  2. 某个断连请求已经发起
  3. 某个断连请求已经完成
  4. 某个连接之半已经关闭
  5. 数据到达套接字
  6. 数据已经从套接字发送走
  7. 发生某个异步错误

由于信号产生的过于频繁,并且它的出现并没有告诉我们发生了什么事情。因此tcp模式下信号驱动io近乎无用, 因此应该只针对监听TCP套接字使用SIGIO,因为对于监听套接字产生SIGIO的唯一条件是某个新的连接已完成(这里不知道怎么只针对监听tcp使用sigio?)

针对UDP模式下列两种情况会产生信号:

  1. 数据报到达套接字
  2. 套接字上发生异步错误
    因此在接收到信号时,调用recvfrom拷贝内核缓存数据,最后处理数据报。或处理异常

在这里插入图片描述
异步IO下,进程在接收到i/o操作后,则调用aio_read方法,无论系统是否准备完毕,该方法都会立即返回数据。此时用户进程就可以做其他事情继续执行,内核会自动将数据拷贝到用户空间中,同发送信号。应用程序进行信号处理,程序处理,数据报处理。

这里linux系统针对信号的处理有下列三种情况:

  1. 如果这个进程正在用户态忙着做别的事(例如在计算两个矩阵的乘积),那就强行打断之,调用事先注册的信号处理函数,这个函数可以决定何时以及如何处理这个异步任务。由于信号处理函数是突然闯进来的,因此跟中断处理程序一样,有很多事情是不能做的,因此保险起见,一般是把事件 “登记” 一下放进队列,然后返回该进程原来在做的事。
  2. 如果这个进程正在内核态忙着做别的事,例如以同步阻塞方式读写磁盘,那就只好把这个通知挂起来了,等到内核态的事情忙完了,快要回到用户态的时候,再触发信号通知。
  3. 如果这个进程现在被挂起了,例如无事可做 sleep 了,那就把这个进程唤醒,下次有 CPU 空闲的时候,就会调度到这个进程,触发信号通知。

前面把IO的五种模型了解了一下。接下来我们来看下JAVA中对于IO的支持。

Java的IO支持

Java针对IO提供了两种支持方案,一种是IO在java.io包中,NIO在java.nio中
先来看下两者的区别:

IONIO
面向字节流面向缓冲区
阻塞IO非阻塞IO
Selectors选择器

java 的io直接对流进行操作,并是阻塞式的io也就式在对io进行读写时线程式阻塞的。

Java NIO

NIO的三个核心是Channel(通道),Buffer(缓冲区),Selector(选择器)

一.Channel

channel通道是io操作的载体,相当与流,但流是单向的而通道是双向的,并且读取到channel中的数据总是buffer

二.Buffer缓冲区

Buffer用于与channel进行交互,buffer实质上是一块被包装成NIO Buffer对象的内存,可以对其进行数据的写入和读取。

三. Selectors 选择器

selectors 是Java NIO的核心内容。
selector 有选择已就绪任务的能力,它会轮训注册在上面的channel并选择有读写时间的channel进行后续的读写任务操作。NIO的非阻塞模式使用的是多路复用模式

我们先来写一个基于socket的nio应用

package com.heroschool.study;

import com.sun.xml.internal.ws.policy.privateutil.PolicyUtils;
import org.junit.Test;

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 NioTest {

    @Test
    public void server() throws IOException {
        // 获取通道
        ServerSocketChannel server = ServerSocketChannel.open();
        // 切换到非阻塞模式
        server.configureBlocking(false);
        server.bind(new InetSocketAddress(8880));
        // 获取选择器
        Selector selector = Selector.open();
        // 注册接收事件
        server.register(selector, SelectionKey.OP_ACCEPT);
        // 如果有准备就绪的事件(接收到客户端数据)
        while (true) {
            selector.select();
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> keys = selectionKeys.iterator();
            while (keys.hasNext()) {
                SelectionKey key = keys.next();
                if (key.isAcceptable()) {
                    System.out.println("客户端连接完毕");
                    SocketChannel client = server.accept();
                    client.configureBlocking(false);
                    // 注册客户端的读事件
                    client.register(selector, SelectionKey.OP_READ);
                } else if (key.isReadable()) {
                    System.out.println("客户端数据传输准备完成");
                    SocketChannel client = (SocketChannel) key.channel();
                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                    int len = 0;
                    // 读取客户端数据
                    while ((len = client.read(byteBuffer)) > 0) {
                        byteBuffer.flip();
                        System.out.println(new String(byteBuffer.array(), 0, len, "utf-8"));
                        byteBuffer.clear();
                    }
                    if(len == -1) { // 关闭客户端连接
                        key.cancel();
                        client.close();
                    }
                }
                keys.remove();
            }
        }
    }

    @Test
    public void client() throws IOException {
        // 获取通道
        SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 8880));
        // 切换非阻塞模式
        socketChannel.configureBlocking(false);
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        String s = "this is test";
        byteBuffer.put(s.getBytes("utf-8"));
        byteBuffer.flip();
        socketChannel.write(byteBuffer);
        byteBuffer.clear();
        socketChannel.close();
    }
}

在这里插入图片描述
这里服务端选择器会调用select()方法,该方法底层便是使用到操作系统的select或epoll方法。我们可以稍微来看下代码:
在这里插入图片描述
最终实现的类是:
在这里插入图片描述
实现的方法为:
在这里插入图片描述
poll方法即调用底层的操作系统方法,获取到数据后更新selectKeys

这里在提一下,这种多路复用的方法使用的是Reactor模式:
(1)采用事件驱动
(2)可以处理一个或多个输入源
(3)通过service Handler将事件分发给request handler

Java NIO的AIO模式

JAVA1.7之后nio支持AIO异步IO的模式,可以让客户端或服务端不需要等待操作系统层面上的io等待。这里就不进行详细的看了。后面会专门看下Netty框架,该框架对这种异步需求有着很好的支持。

参考资料

信号驱动式I/O
信号驱动式I/O
JAVA NIO编程介绍

用户名,Email地址,昵称 ajax无刷新验证是否已注册 非空验证 用户名验证:由英文字母和数字组成的4-16位字符,以字母开头! 邮箱验证:Email格式不正确,例如web@sohu.com 昵称验证:由汉字组成的2-8位字符! 密码验证:密码不能含有非法字符,长度在4-10之间 再次输入密码验证:两次输入密码不一致! 注册成功后用户直接保存在session里进入登录页面 搜索框实现智能联想(根据关键字联想贴吧名搜索) 登录成功用户保存在session中显示用户昵称 登录失败显示(登录失败!请重新) 注销可以销毁session中的用户对象 横条广告用flash制作替换效果 个人中心显示用户头像及昵称 奇吧朵朵展示留个贴吧信息(滚动文字) 最新话题展示最新发帖标题并可点击进入 常逛贴吧由用户登录后显示 贴吧分类显示不同类型贴吧鼠标移动上去展示贴吧名 热帖分类展示四个贴吧四条回复 热帖排行榜显示回复最多三个帖子 顶部展示贴吧名称,会员数,帖子数,以及帖吧简介 帖子显示回复次数,标题,内容25字符发帖昵称以及发帖时间 签到按钮可以判断是否登录,签到成功次数加一,提示以签到 显示用户头像和昵称 恢复按钮AJAX判断是否登录,登录后方可发帖 右侧游戏动态加载 翻页可实现看第一页,向前和向后翻页 顶部显示广告,帖子标题 只看楼主可实现只看楼主发表内容,取消只看楼主查看全部回复 显示回复用户头像和昵称,回复内容签名档,楼层,发表时间 关注AJAX判断用户是否已登录,关注成功次数加一,已关注提示 鼠标移动到用户头像可查看用户信息 实现翻页功能 回复条数,返回当前贴吧 回复按钮AJAX判断是否已登录,登陆后方可发帖 我的帖子显示吧名,标题,回复次数,发帖时间,删除提示,删除失败提示 我关注的帖吧显示把名,标题,回复次数,发帖时间,删除提示,删除失败提示
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值