java nio 源码分析2 IO

Wesley13
• 阅读 607

目的

一直想知道

  • channel.write返回时, 到底这个数据是交给操作系统了, 还是说已经发出网卡了, 还是说已经发出去收到ACK了. (答案:只是说明它写入了内核的send_queue)
  • java nio是水平触发的, 而且缓冲区超过"低水位"就触发读事件, 不超过"高水位"就触发写事件, 那这个水位到底多高? 缓冲区只什么缓冲区? ByteBuffer还是内核管理的缓冲区?(没看native源码,但应该是socket的recv_queue和send_queue,反正我们只需要根据read和write的返回值、是否抛出异常来决定下一步行为即可。)
  • "低水位"一词来自<unix网络编程卷1>, 本文假设你已经读过.
  • read和write的返回值意义(写的返回值>=0, 读的返回值>= -1, 其中-1代表EOF)

参考

write

getTemporaryDirectBufferSocketChannelImpl值得分析

探究socketChannel原理提到了socket非直接内存的读写都是两次复制

读写的原理

上图便回答开文的第一个问题,当你写出成功返回时,只是说明它写入了内核的send_queue

SocketChannelImpl::write:

public int write(ByteBuffer buf) throws IOException {
        ...
        try {
            ...
            for (;;) {
                n = IOUtil.write(fd, buf, -1, nd);
                if ((n == IOStatus.INTERRUPTED) && isOpen())
                    continue;
                return IOStatus.normalize(n);
            }
        } finally {
            ...
        }
    }
}

依赖于IOUtil::write

static int write(FileDescriptor fd, ByteBuffer src, long position,
                     NativeDispatcher nd)
        throws IOException
{
    if (src instanceof DirectBuffer)
        return writeFromNativeBuffer(fd, src, position, nd);

    // Substitute a native buffer
    int pos = src.position();
    int lim = src.limit();
    assert (pos <= lim);
    int rem = (pos <= lim ? lim - pos : 0);  // 确定直接内存大小
    ByteBuffer bb = Util.getTemporaryDirectBuffer(rem);  // 分配直接内存
    try {
        bb.put(src);
        bb.flip();
        // Do not update src until we see how many bytes were written
        src.position(pos);

        int n = writeFromNativeBuffer(fd, bb, position, nd);  // 直接内存往外写
        if (n > 0) {
            // now update src
            src.position(pos + n);
        }
        return n;
    } finally {
        Util.offerFirstTemporaryDirectBuffer(bb);
    }
}

两个调用很重要:

  1. 通过Util.getTemporaryDirectBuffer分配直接内存
  2. 通过writeFromNativeBuffer(fd, bb, position, nd);从直接内存写数据

getTemporaryDirectBuffer

 public static ByteBuffer getTemporaryDirectBuffer(int size) {
    BufferCache cache = bufferCache.get(); // 获取线程私有变量
    ByteBuffer buf = cache.get(size);  // 关键,获取指定大小的内存
    if (buf != null) {
        return buf;
    } else {
        // 没有满足条件的直接内存
        // 把第一个内存给释放
        if (!cache.isEmpty()) {
            buf = cache.removeFirst();
            free(buf);
        }
        return ByteBuffer.allocateDirect(size);  // 关键,分配直接内存
    }
}
...
 private static ThreadLocal<BufferCache> bufferCache =
        new ThreadLocal<BufferCache>()
{
    @Override
    protected BufferCache initialValue() {
        return new BufferCache();
    }
};

这里我们易看出,线程私有变量bufferCache对直接内存进行了管理, 下面我们要分析该类

BufferCache::getTemporaryDirectBuffer

/**
 * A simple cache of direct buffers.
 */
private static class BufferCache {
    // the array of buffers
    private ByteBuffer[] buffers;

    // the number of buffers in the cache
    private int count;

    // the index of the first valid buffer (undefined if count == 0)
    private int start;

    private int next(int i) {
        return (i + 1) % TEMP_BUF_POOL_SIZE;
    }

    BufferCache() {
        buffers = new ByteBuffer[TEMP_BUF_POOL_SIZE];
    }
...

懒得分析了,挺简单的一个类,总之就是

  1. 用一个循环数组来管理一组空闲的直接内存,查找内存靠线性遍历查询
  2. next取余说明它是个循环数组
  3. 用start和count来记录有直接内存的元素界限。(buffer[start]是第一个有效的内存索引)
  4. get(int size)会找到第一个大小至少大于size的元素索引。由于将其返回后该位置会为空,就把第一个元素(buffer[start])置于此即可。
  5. getTemporaryDirectBuffer会尽量返回第一个

思考:多读读代码会发现,如果临时要输出的内存大小突然变大,会导致突然分配一个很大的ByteBuffer元素。如果之后又用不上这么大的内存,就比较浪费, 所以

《Hadoop技术内幕》提到了可将写入的数据分成固定大小(比如8KB)的chunk,并以chunk为单位写入DirectBuffer

IOUtil::writeFromNativeBuffer

没找到native源码,算了不分析了

read和write的返回值意义

我们看到继承链:

image.png

强烈建议阅读这两个类的注释

public interface WritableByteChannel
    extends Channel
{

    /**
     * @return The number of bytes written, possibly zero
     *
     * @throws  ...
     */
    public int write(ByteBuffer src) throws IOException;

}


public interface ReadableByteChannel extends Channel {

    /**
     *
     * @return  The number of bytes read, possibly zero, or <tt>-1</tt> if the
     *          channel has reached end-of-stream
     *
     * @throws  ...
     */
    public int read(ByteBuffer dst) throws IOException;

}

上面两个说明了,**write会返回的值是0或正数,read会返回的值可能是0, 正数, 或-1(当EOF时)**。
然后注释懒得贴上来翻译,我总结一下吧:

  1. 读是从socket的输入缓存(socket's input buffer)读,写是从socket的输出缓存写
  2. nio中,读不一定读满,写也不一定写满,根据返回值来决定下一步行为
  3. 根据Java NIO read() End of Stream read返回-1代表的EOF,应该是说明对方优雅关闭了连接(比如对方正常发出FIN并响应之类的)。如果不优雅关闭,应该会抛出异常。
  4. 引申地说,在nio编程时,如果ByteBuffer没有写完,不应当坚持循环等ByteBuffer写完。最好是注册写事件,等下一次写。

本文同步分享在 博客“不存在的里皮”(JianShu)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
5个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Wesley13 Wesley13
3年前
Java日期时间API系列31
  时间戳是指格林威治时间1970年01月01日00时00分00秒起至现在的总毫秒数,是所有时间的基础,其他时间可以通过时间戳转换得到。Java中本来已经有相关获取时间戳的方法,Java8后增加新的类Instant等专用于处理时间戳问题。 1获取时间戳的方法和性能对比1.1获取时间戳方法Java8以前
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
11个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这