关于nio:大话网络通信

2次阅读

共计 4732 个字符,预计需要花费 12 分钟才能阅读完成。

1、术语

并发 vs 并行

  • 并发和并行是相干的概念,但有一些小的区别。并发意味着两个或多个工作正在获得停顿,即便它们可能不会同时执行。例如,这能够通过工夫切片来实现,其中局部工作按程序执行,并与其余工作的局部混合。另一方面,当执行的工作能够真正同时进行时,就会呈现并行
    简略说启动一个线程在一个 core 上就是并行,启动两个线程在一个 core 上就是并发

异步 vs 同步

  • 如果调用者在办法返回值或引发异样之前无奈获得停顿,则认为办法调用是同步的。另一方面,异步调用容许调用者在无限的步骤之后持续进行,并且能够通过一些附加机制 (它可能是已注册的回调、Future 或音讯)来告诉办法的实现
    简略来说 Java API 层来说的,如下 :
ExecutorService executorService = Executors.newFixedThreadPool(2);
        Future<Boolean> future = executorService.submit(new Callable<Boolean>() {
            @Override
            public Boolean call() throws Exception {System.out.println("执行业务逻辑");
                
                // 依据业务逻辑判断给定返回
                return true;
            }
        });

        future.get(); // 同步 API,必须等到返回
        if(future.isDone()) {future.get();// 异步 API,只有执行完,再 get 后果
        }  
  • 同步 API 能够应用阻塞来实现同步,但这不是必要的。CPU 密集型工作可能会产生相似 于阻塞的行为。一般来说,最好应用异步 API,因为它们保证系统可能进行

非阻塞 vs 阻塞

  • 如果一个线程的提早能够无限期地提早其余一些线程,这就是咱们探讨的阻塞。一个很好的例子是,一个线程能够应用互斥来独占应用一个资源。如果一个线程无限期地占用资源(例如意外运行有限循环),则期待该资源的其余线程将无奈进行。相同,非阻塞意味着没有线程可能无限期地提早其余线程
  • 非阻塞操作优先于阻塞操作,因为当零碎蕴含阻塞操作时,零碎的总体进度并不能失去很好的保障

    死锁 vs 饥饿 vs 活锁

  • 当多个线程在期待对方达到某个特定的状态以便可能获得停顿时,就会呈现死锁。因为没有其余线程达到某种状态,所有受影响的子系统都无奈持续运行。死锁与阻塞密切相关,因为线程可能无限期地提早其余线程的过程
  • 在死锁的状况下,没有线程能够获得停顿,相同,当有线程能够获得停顿,但可能有一个或多个线程不能获得停顿时,就会产生饥饿。典型的场景是一个调度算法,它总是抉择高优先级的工作而不是低优先级的工作。如果传入的高优先级工作的数量始终足够多,那么低优先级工作将永远不会实现
  • 活锁相似于死锁,因为没有线程获得停顿。不同之处在于,线程不会被解冻在期待别人停顿的状态中,而是一直地扭转本人的状态。一个示例场景是,两个线程有两个雷同资源可用时。他们每一个都试图取得资源,但他们也会查看对方是否也须要资源。如果资源是由另一个线程申请的,他们会尝试获取该资源的另一个实例。在可怜的情 况下,两个线程可能会在两种资源之间“反弹”,从不获取资源,但总是屈服于另一种资源

2、BIO vs NIO

BIO

serverSocket.accept(),这里会阻塞
socket.getInputStream.read(),也会阻塞

尽管能够应用了线程池,因为 read()办法的阻塞,其实线程池也是不能复用的,说白了,就是须要一个客户端一个线程进行服务

思考:那 BIO 就没有应用场景了吗?
其实不是,BIO 在建设长连贯的流式传输场景还是很有用的,比如说 HDSF,客户端向 DataNode 传输数据应用的就是建设一个 BIO 的管道,流式上传数据的。此时引入一个问题,那 HDFS DataNode 就不思考到线程阻塞么?是这样的,其实要晓得你不可能多个客户端上传文件都是针对某个 DataNode(NameNode 会进行抉择 DataNode),所以线程阻塞的压力是会摊派的。NIO 还是善于小数据量的 RPC 申请,能承受百万客户端的连贯

NIO

NIO 中有三个重要组件 : Buffer(ByteBuffer 次要应用)、Channel(双向通道,可读可写)和 Selector(多路复用选择器)

  1. Buffer
    罕用的就是 ByteBuffer,缓冲池,能够作为 channel 写的单位,也能够承受 channel 读取的返回外面重要的属性 :position、capacity、flip、limit 和 hasRemain

    每个 channel 都须要记录可能切分的音讯,因为 ByteBuffer 不能被多个 channel 应用,因而须要为每个 channel 保护一个独立的 ByteBuffer。ByteBuffer 不能太大,比方一个 ByteBuffer 1M 的话,须要反对百万连贯要 1TB 内存,因而须要设计大小可变的 ByteBuffer
    1、首先调配一个较小的 buffer,比方 4k,如果发现不够的话,再调配 8kb 的 buffer,将 4kb buffer 内容拷贝到 8kb buffer,有点是音讯间断容易解决,毛病是数据拷贝消耗性能
    2、多个数组组成 buffer,一个数组不够,把多进去的内容写入新的数组,毛病不间断解析简单,有点防止了拷贝引起的性能损耗

  2. FileChannel
    FileChannel 在同一个 JVM 中是线程平安的,多个线程写也没有问题,然而在不同的 JVM 中同时写一个文件就会有问题须要的是 FileLock 对文件进行加锁,有独占锁和共享锁
    channel.lock(0, Integer.MAX_VALUE, true),能够锁肯定的区间

    RandomAccessFile 能够反对读写文件,Channel 自身是反对读写的,只是看源头是不是反对读写,比如说 FileInputStream 流获取的 channel 只能反对读,RandomAccessFile 获取的流反对读写

    channel.force(true); 强制将 os cache 数据刷入到磁盘上

    from.transferTo(position,count,dest);从 from channel 写到 dest channel,从 position 开始写,写了 count 长度
    比如说从本地文件向网络中进行传输
    to.transferFrom(src,position,count); 比如说从网络中写到本地文件,from 就是从外界到,src 读取,写到 to 中

    transferTo & transferFrom 底层应用的是零拷贝,零拷贝简略来说其实不走应用层数据复制,然而也是有数据复制的,是在 Linux 内核层

  3. Selector & SocketChannel
    服务端 ServerSocketChannel
    是通过 ServerSocketChannel 和 Selector 来获取多个连贯,每个连贯是一个 SocketChannel
    将 ServerSocketChannel 注册到 Selector 上,如果有连贯,selector 的 select 阻塞办法会有事件,生成 SelectionKey,每个 SelectionKey 其实对应一个 SocketChannel

    Selectionkey 是能够 attach 对象的,也能够通过 Selectionkey 通过 attachment 进行对象的获取, 这很重要,个别会创立一个对象并和 SelectioKey 进行关联

    照样是 bind,监听 OP_ACCEPT,进行 isAcceptable、read 和 write 事件(write 事件是一次没有写结束,持续要写)

    客户端 SocketChannel
    是进行 connect,监听 OP_CONNECT 事件,isConnectable,read 和 write 事件

    要留神,SelectionKey,每次迭代是须要删除的,否则反复申请,然而曾经解决,就会有问题

    写数据的时候肯定要留神,最好不要 while(buffer.hasRemaining()) 始终写,这样会阻塞网络带宽的,影响读取
    写一部分数据,而后关注 SelectionKey.OP_WRITE 事件,一直 selector.select() 持续写,写结束勾销写事件的关注

    socketChannel.write(buffer); 写一下,也不肯定会把 buffer 中都写结束
    if(buffer.hasRemaining()) {selectionKey.interestOps(selectionKey.interestOps() | SelectionKey.OP_WRITE);
        selectionKey.attach(buffer);
    }

3、零拷贝

传统 IO 问题
比如说要将本地磁盘文件往网络中写,磁盘 -> 内核缓冲区 -> 用户缓冲区 -> socket 缓冲区 -> 网卡

读磁盘数据 : 用户态 -> 内核态
内核数据写到用户缓冲区 : 内核态 -> 用户态
网卡写数据 : 用户态 -> 内核态

4 次数据复制,3 次内核切换

通过 DirectByteBuffer,MappedByteBuffer,为什么快?
因为他应用 direct buffer 的形式读写文件内容,称为内存映射。这种形式间接调用零碎底层的缓存,没有 JVM 和零碎之间的复制操作,所以效率大大的晋升
将堆外内存映射到 JVM 内存中间接拜访
缩小一次数据拷贝,用户态与内核态的切换次数没有缩小

Linux2.4
Java 调用 transferTo,要从 Java 程序的用户态到内核态,磁盘 -> 内核缓冲区 -> 网卡,一次内核切换,两次数据复制

4、Socket 参数

SocketChannel 参数
SO_RCVBUF 和 SO_SNDBUF : Socket 参数,TCP 数据接收缓冲区大小,发送和承受缓冲区,128kb 或者 256kb
CONNECT_TIMEOUT_MILLIS : 用户在客户端建设连贯时,如果在指定毫秒内无奈建设连贯,会抛出 timeout 异样
TCP_NODELAY TCP 参数,立刻发送数据,默认值为 Ture(敞开 nagle 算法)
SO_KEEPALIVE Socket 参数,连贯保活,默认值为 False。启用该性能时,TCP 会被动探测闲暇连贯的有效性(2 个小时)
SO_REUSEADDR : 其实就是比如说 ServerSocketChannel 连贯敞开了,此时跟其余客户端的连贯都处于一个 timeout 状态,重启 Netty Server,如果设置了
SO_REUSEADDR 为 true,则会让 ServerSocketChannel 从新地址端口绑定,否则失败

ServerSocketChannel 参数
SO_BACKLOG Socket 参数,服务端承受连贯的队列长度,如果队列已满,客户端连贯将被回绝。默认值,Windows 为 200,其余为 128
TCP 三次握手是在 ACCEPT 之前产生的
1、第一次握手,client 发送 SYN 到 server,状态批改为 SYN_SEND,server 收到,状态批改为 SYN_REVD,并将申请放入 sync queu 队列
2、第二次握手,server 回复 SYN + ACK 给 client,client 收到,状态批改为 ESTABLISHED,并发送给 ACK 给 server
3、第三次握手,server 收到 ack,状态批改为 ESTABLISHED,将申请从 sync queue 放入 accept queue

所以当初呈现了半连贯队列和全连贯队列
在 Centos Linux 下对应着 /proc/sys/net/ipv4/tcp_max_syn_backlog(512),/proc/sys/net/core/somaxconn(128)

SO_BACKLOG 设置的是全连贯

TCP SYNC FLOOD 歹意 DOS 攻击方式就是建设大量的半连贯状态的申请,而后抛弃

正文完
 0