网络IO模型演进02——NIO模型示例

1. 简介

NIO 全称 Non Blocking IO , 也就是非阻塞IO,这里的阻塞体现在两个方面,

  • accept内核分配的Socket资源后,非阻塞的等待客户端的连接
  • 读取客户端发送的数据时是非阻塞的

对于Java 来讲,nio 指的是new io,也就是说java.nio包里的类,可以是阻塞的,也可以是非阻塞的。

2. 笔者环境
  • Ubuntu 18.04
  • JDK1.8
3. 示例代码
  • vim SocketNIONonBlocking.java
  • javac SocketNIONonBlocking.java
  • strace -ff -o out java SocketNIONonBlocking
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.LinkedList;

public class SocketNIONonBlocking {

    public static void main(String[] args) throws Exception {

        LinkedList clients = new LinkedList<>();

        ServerSocketChannel ss = ServerSocketChannel.open();
        ss.bind(new InetSocketAddress(9090));
        ss.configureBlocking(false); //重点


        while (true) {
            Thread.sleep(2000);//为了方便日志分析,避免过多的系统调用
            SocketChannel client = ss.accept(); //不会阻塞

            if (client == null) {
                System.out.println("client is null");
            } else {
                client.configureBlocking(false); //重点
                int port = client.socket().getPort();
                System.out.println("client port: " + port);
                clients.add(client);
            }

            ByteBuffer buffer = ByteBuffer.allocateDirect(4096);  //可以在堆里   堆外

            for (SocketChannel c : clients) {
                int num = c.read(buffer);
                if (num > 0) {
                    buffer.flip();
                    byte[] aaa = new byte[buffer.limit()];
                    buffer.get(aaa);

                    String b = new String(aaa);
                    System.out.println(c.socket().getPort() + " : " + b);
                    buffer.clear();
                }

            }
        }
    }

}

  • 这里strace 是 查看系统调用命令,会将结果输出到out为前缀的文件中,后面的id是线程id
  • 可以看到此时控制台不断的输出 client is null,证明这里的accept是非阻塞的,不同于BIO中,没有客户端连接时阻塞在这里
$ strace -ff -o out java  SocketNIONonBlocking
client is null
client is null
client is null
client is null
client is null
......
4. 查看系统调用
  • 打开一个新的命令行,查看系统调用日志,查找输出 client is null 的文件
$ grep 'client is null' out.*
......
out.5136:write(1, "client is null", 14)          = 14
out.5136:write(1, "client is null", 14)          = 14
out.5136:write(1, "client is null", 14)          = 14
out.5136:write(1, "client is null", 14)          = 14
......

  • 打开out.5136文件,查找上面 write(1, “client is null”, 14) = 14这一行
  • vim out.5136
accept(4, 0x7f37300eae10, [28])         = -1 EAGAIN (Resource temporarily unavailable)
write(1, "client is null", 14)          = 14
write(1, "\n", 1)                       = 1
accept(4, 0x7f37300eae10, [28])         = -1 EAGAIN (Resource temporarily unavailable)
write(1, "client is null", 14)          = 14
write(1, "\n", 1)                       = 1
accept(4, 0x7f37300eae10, [28])         = -1 EAGAIN (Resource temporarily unavailable)
write(1, "client is null", 14)          = 14
write(1, "\n", 1)                       = 1
accept(4, 0x7f37300eae10, [28])         = -1 EAGAIN (Resource temporarily unavailable)
write(1, "client is null", 14)          = 14
write(1, "\n", 1
  • 可以看到进程在不断的进行accept系统调用,返回值是-1,Resource temporarily unavailable 资源暂时不可用
5. 客户端连接
  • 新打开一个命令行,进行连接
  • nc localhost 9090
  • grep ‘client port’ out.*
  • 继续查看系统调用 vim out.5136
accept(4, {sa_family=AF_INET6, sin6_port=htons(58014), inet_pton(AF_INET6, "::ffff:127.0.0.1", &sin6_addr), sin6_flowinfo=htonl(0), sin6_scope_id=0}, [28]) = 5    

......

fcntl(5, F_GETFL)                       = 0x2 (flags O_RDWR)
fcntl(5, F_SETFL, O_RDWR|O_NONBLOCK)    = 0
.....

write(1, "client port: 58014", 19)     = 19
write(1, "\n", 1)                       = 1
  
  • 这里可以看到,accept系统调用,已经完成返回值是文件描述符5
  • 这里使用fcntl系统调用把文件描述符5,设置为非阻塞的
  • 继续往下看
read(5, 0x7f6dd7c03cb0, 4096)           = -1 EAGAIN (Resource temporarily unavailable)
accept(4, 0x7f6e100f8680, [28])         = -1 EAGAIN (Resource temporarily unavailable)
write(1, "client is null", 14)          = 14
write(1, "\n", 1)                       = 1

  • 这里发现 read 系统调用读的就是accept 返回的文件描述符
  • 发现资源临时不可用后,继续accept,对应了Java代码中进入下一轮循环
6. 客户端发送数据
  • 在nc 窗口下发送数据
$ nc localhost 9090
abcdefg
  • 查找包含abcdeeg的文件
grep abcdefg out.*
  • vim out.8359 在vim中使用 / 来查找abcdefg
......

accept(4, 0x7fefc40dae40, [28])         = -1 EAGAIN (Resource temporarily unavailable)
write(1, "client is null", 14)          = 14
write(1, "\n", 1)                       = 1
mprotect(0x7fefc40e9000, 4096, PROT_READ|PROT_WRITE) = 0
read(5, "abcdefg\n", 4096)              = 8
write(1, "59968 : abcdefg\n", 16)       = 16
write(1, "\n", 1)                       = 1
futex(0x7fefc400be78, FUTEX_WAIT_PRIVATE, 0, {tv_sec=1, tv_nsec=999997296}) = -1 ETIMEDOUT (Connection timed out)
futex(0x7fefc400be28, FUTEX_WAKE_PRIVATE, 1) = 0
accept(4, 0x7fefc40dae40, [28])         = -1 EAGAIN (Resource temporarily unavailable)
write(1, "client is null", 14)          = 14
write(1, "\n", 1)                       = 1
.......
  • 这里可以发现,read系统调用 读到了文件描述符5 的内容,和上文是一样的
  • 客户端未发送数据时, read(5) 返回 -1,
  • 客户端发送了数据, read(5) 返回的是 字符个数
  • read完数据之后,继续走accept系统调用
7.总结
  1. 非阻塞IO涉及的系统调用
  • socket
  • bind
  • listen
  • accept
  • read
  1. 非阻塞IO accept 和 read 系统调用都是非阻塞的,不会阻塞当前线程
  • 非阻塞的属性,这里和fcntl系统调用有关
  1. Java 的 nio包指的是 New io , Java nio 包也可以将IO设置为阻塞IO
ss.configureBlocking(true); 


client.configureBlocking(true);

向示例代码中的 configureBlocking方法传入true,这里就变成了阻塞IO,大家可以自行验证。

你可能感兴趣的:(io,nio)