Netty(一)基础知识

前言

首先介绍一下本文的学习步骤

  1. 同步,异步,阻塞和非阻塞
  2. BIO,NIO模型
  3. BIO,NIO各种实现以及问题(JAVA)
  4. NIO与多路复用
  5. 拓展

最开始的实现永远是最朴素的,而架构演进是跟着需求自然而然进行的。

起因

进程调用系统函数(System Call)会引发中断。
计算机执行逻辑主体是CPU,进程只是一种对CPU工作流抽象的数据结构。在中断发生时,CPU的工作空间从用户内存转换到内核内存并保存当前工作流的上下文信息(现场保护),当系统函数调用完成时需要恢复上下文信息(恢复现场),相对于程序逻辑的执行来说中断的耗时很长。

阻塞模型(Blocking)

阻塞模型,系统函数执行完毕后才会返回结果,这段时间请求线程一直是阻塞(Blocking)的。就互联网应用来说,请求打到服务器会产生对应工作线程,当线程进行I/O操作会造成线程处于阻塞状态,造成大量资源浪费。
在这里插入图片描述

非阻塞模型(Nonblocking)

非阻塞模型,内核函数执行会立即返回当前执行状态。这为在用户态运行的程序提供了优化效率的可能,比如让一个线程负责发起系统调用,另一个线程负责监控函数执行状态,最后再让一个线程池去执行回调后的操作。
在这里插入图片描述

同步(Synchronous)和异步(Asynchronous)

同步和异步的争论一直没有停止过,有的人说:“同步就是阻塞,异步就是非阻塞。”,也有人说:“同步是一个线程执行,异步是两个线程执行。”
上面的说法都没有问题,只是看待问题的角度不同,实际上关于这个争论可以引用知乎的一个回答。

同步/异步关注的是消息通信机制 (synchronous communication/ asynchronous communication) 。

  • 所谓同步,就是在发出一个调用时,在没有得到结果之前, 该调用就不返回。
  • 异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果。

阻塞/非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态

  • 阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
  • 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。
小结
  • 阻塞和非阻塞是操作系统级的概念,底层的结构决定了上层的实现。
  • 在阻塞模型下就算上层接口立即返回,也并没有解决操作系统资源消耗的问题。
  • 非阻塞模型为实现I/O多路复用技术,异步提供了可能性。
BIO、NIO

现在我们再来理解一下BIO、NIO模型可能更为清晰。

BIONIO
IO模型阻塞非阻塞
同步、异步同步支持异步
优点简单多路复用
BIO(Blocking IO)
单线程阻塞IO的实现
 public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(8080);
        System.out.println("Server Start!");
        while (true) {
        	//发生阻塞1
            Socket socket = serverSocket.accept();
            System.out.println("Client connect:");
            byte[] bytes = new byte[1024];
            //发生阻塞2
            socket.getInputStream().read(bytes);
            System.out.println("receive data :" + new String(bytes));
        }
    }

让我们来看看两次阻塞都发生了什么

accept()

我们在rt.jar下找到了这段代码

InetSocketAddress[] isaa = new InetSocketAddress[1];
if (timeout <= 0) {
     newfd = accept0(nativefd, isaa);
}

看到fd我们基本就知道这是一个系统调用了,因为在linux系统中所有的资源都有对应的fd描述符。新建一个int类型newfd变量,将accept0()的结果赋值给newfd。accept0是个native方法,具体逻辑由jdk源码实现,这边我们不讨论JDK源码直接查看对应的操作系统命令。

$ man  accept
NAME
       accept - accept a connection on a socket

SYNOPSIS
       #include <sys/types.h>          /* See NOTES */
       #include <sys/socket.h>

       int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

       #define _GNU_SOURCE
       #include <sys/socket.h>

       int accept4(int sockfd, struct sockaddr *addr,
                   socklen_t *addrlen, int flags);

DESCRIPTION
       The  accept()  system call is used with connection-based socket types (SOCK_STREAM,
       SOCK_SEQPACKET).  It extracts the first connection request on the queue of  pending
       connections  for  the listening socket, sockfd, creates a new connected socket, and
       returns a new file descriptor referring to that socket.  The newly  created  socket
       is  not  in  the listening state.  The original socket sockfd is unaffected by this
       call.

       The argument sockfd is a socket that has been created with socket(2),  bound  to  a
       local address with bind(2), and is listening for connections after a listen(2).

       The  argument  addr is a pointer to a sockaddr structure.  This structure is filled
       in with the address of the peer socket, as known to the communications layer.   The
       exact  format  of  the  address returned addr is determined by the socket’s address
       family (see socket(2) and the respective protocol man pages).  When addr  is  NULL,
       nothing is filled in; in this case, addrlen is not used, and should also be NULL.
RETURN VALUE
       On success, these system calls return a non-negative integer that is  a  descriptor
       for the accepted socket.  On error, -1 is returned, and errno is set appropriately.

简单翻译一下,accept是socket的系统调用,只有第一次连接会触发,返回当前socket的文件描述符。

read()

将accept渠道取到的文件描述符作为属性注入,然后socketRead0就完事了

	//注入属性
	SocketInputStream(AbstractPlainSocketImpl impl) throws IOException {
        super(impl.getFileDescriptor());
        this.impl = impl;
        socket = impl.getSocket();
    }
    //调用native方法读取
 	private int socketRead(FileDescriptor fd,
                           byte b[], int off, int len,
                           int timeout)
        throws IOException {
        return socketRead0(fd, b, off, len, timeout);
    }

由于两次阻塞都比较耗时,单线程实现的处理效率很低。

多线程阻塞IO的实现
	public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(8080);
        System.out.println("Server Start!");
        while (true) {
            Socket socket = serverSocket.accept();
            //开启线程执行
            new Thread(()-> {
                System.out.println("Client connect:");
                byte[] bytes = new byte[1024];
                try {
                    socket.getInputStream().read(bytes);
                } catch (IOException e) {
                    e.printStackTrace();
                }
                System.out.println("receive data :" + new String(bytes));
            }).start();
        }
    }

在这里插入图片描述
虽然多线程提高了Server响应速度,但是实际上大部分线程都同时处于阻塞状态。

NIO(Nonblocking IO)
	public static void main(String[] args) throws IOException {
        //启动服务器,设置非阻塞
        ServerSocketChannel serverSocket = ServerSocketChannel.open();
        serverSocket.bind(new InetSocketAddress(8080));
        serverSocket.configureBlocking(false);
        while (true) {
            //保存连接
            SocketChannel socketChannel = serverSocket.accept();
            if (socketChannel != null) {
                socketChannel.configureBlocking(false);
                channelList.add(socketChannel);
            }
            // 遍历连接
            Iterator<SocketChannel> iterator = channelList.iterator();
            while (iterator.hasNext()) {
                doWork(iterator.next(),channelList);
            }
        }
    }

    //执行工作
    private static void doWork(SocketChannel socketChannel,List<SocketChannel> channelList) throws IOException {
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        int read = socketChannel.read(byteBuffer);
        if(read > 0){
            //do something
        }else if (read == -1){
            //连接已断开
            channelList.remove(socketChannel);
        }
    }

Server服务通过容器保存当前连接,并轮询当前连接通过read()获取当前连接是否有写数据,这样Server就实现了单线程处理并发连接的能力。但是如果连接集合中只有一小部分在传输数据,每次都遍历所有连接显然不是一个高效的策略。

NIO多路复用

操作系统提供了非阻塞模型来支持单线程处理并发请求,同样也提供了多路复用的通信方式来支持优化无效的遍历。

 //启动服务器,设置非阻塞
        ServerSocketChannel serverSocket = ServerSocketChannel.open();
        serverSocket.bind(new InetSocketAddress(8080));
        serverSocket.configureBlocking(false);
        //注册accept事件
        Selector selector = Selector.open();
        serverSocket.register(selector, SelectionKey.OP_ACCEPT);
        while (true) {
            selector.select();
            //获取所有事件
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            //遍历accept时间
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                iterator.remove();
                // 如果是OP_ACCEPT事件,则注册读事件
                if (key.isAcceptable()) {
                    ServerSocketChannel server = (ServerSocketChannel) key.channel();
                    SocketChannel socketChannel = server.accept();
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector, SelectionKey.OP_READ);
                }
                // 如果是OP_READ事件
                if (key.isReadable()) {
                    //do something
                }
            }
        }

查看源码我们发现,windows系统NIO的select()方法最终调用的是poll函数

 private native int poll0(long var1, int var3, int[] var4, int[] var5, int[] var6, long var7);

继续$man poll查看功能

NAME
       poll, ppoll - wait for some event on a file descriptor

SYNOPSIS
       #include <poll.h>
       int poll(struct pollfd *fds, nfds_t nfds, int timeout);
       #define _GNU_SOURCE
       #include <poll.h>
       int ppoll(struct pollfd *fds, nfds_t nfds,
               const struct timespec *timeout, const sigset_t *sigmask);
DESCRIPTION
       poll()  performs  a  similar  task  to select(2): it waits for one of a set of file
       descriptors to become ready to perform I/O.
       The set of file descriptors to be monitored is specified in the fds argument, which is an array of nfds structures of the following form:
           struct pollfd {
               int   fd;         /* file descriptor */
               short events;     /* requested events */
               short revents;    /* returned events */
           };
       The field fd contains a file descriptor for an open file.
       The field events is an input parameter, a bit mask specifying the events the application is interested in.
       The field revents is an output parameter, filled by the kernel with the events that actually occurred.  The bits returned in revents can include any of those specified
in events, or one of the values POLLERR, POLLHUP, or POLLNVAL.  (These  three  bits are  meaningless in the events field, and will be set in the revents field whenever
the corresponding condition is true.)
RETURN VALUE
       On success, a positive number is returned; this is the number of  structures  which have  non-zero  revents  fields  (in  other words, those descriptors with events or
errors reported).  A value of 0 indicates that the  call  timed  out  and  no  file descriptors  were ready.  On error, -1 is returned, and errno is set appropriately.

在这里插入图片描述
poll监听一个集合中的事件是否准备好进行IO操作,实现原理类似于上面代码中的遍历集合判断状态,只不过是让操作系统来执行轮询操作。

扩展

在linux中的select()源码和windows有着明显的区别

Windows

	this.subSelector.poll();

Linux

	pollWrapper.poll(timeout);

pollWrapper封装了epoll函数
执行man epoll

NAME
       epoll - I/O event notification facility

SYNOPSIS
       #include <sys/epoll.h>

DESCRIPTION
       epoll  is  a  variant  of poll(2) that can be used either as an edge-triggered or a
       level-triggered interface and scales well to large numbers of watched file descrip-
       tors.   The  following  system  calls  are  provided  to create and manage an epoll
       instance:

       *  An epoll instance created by epoll_create(2), which returns  a  file  descriptor
          referring  to the epoll instance.  (The more recent epoll_create1(2) extends the
          functionality of epoll_create(2).)

       *  Interest in particular file descriptors is  then  registered  via  epoll_ctl(2).
          The  set  of file descriptors currently registered on an epoll instance is some-
          times called an epoll set.

       *  Finally, the actual wait is started by epoll_wait(2).

IO事件通知器,epoll是poll的升级版本用来监控文件触发,一般通过epoll_create(创建函数),epoll_ctl(管理函数),epoll_wait(监控函数)配合使用。
epoll与poll的区别可以参考这篇文章《深入理解select、poll和epoll及区别》

总结

IO模型优化并不是提升IO的效率,而是尽量减少无效的IO和系统中断的次数,提高资源利用率。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值