精华内容
下载资源
问答
  • IO通信之多路复用

    2021-03-02 11:51:48
      聊多路复用器之前呢,先回归昨天的NIO,NIO的出现解决了BIO阻塞线程、一连接一线线程问题。但是它有缺点吗,答案是肯定的。 NIO的缺点   我们把问题放大,如果有一万个连接但是只有一个连接是有数据的,但是...

    什么是多路复用器

      聊多路复用器之前呢,先回归昨天的NIO,NIO的出现解决了BIO阻塞线程、一连接一线线程问题。但是它有缺点吗,答案是肯定的。
    NIO的缺点
      我们把问题放大,如果有一万个连接但是只有一个连接是有数据的,但是对于我们的NIO来说,他每次都会遍历所有连接并且去调用内核,我们都是到用户态切换内核态的消耗还是很大的,这里就会涉及到操作系统的知识,当用户态切换到内核态时会有一个80中断,需要经历的过程大致为,保存用户线程的现场信息,根据中断描述符从中断向量表中查找指令,调用指令,恢复现场,如果我们只有一个连接有数据但是会进行9999次无效的80中断是不是会造成不必要的资源消耗。
    如何解决这一个问题
     第一种多路复用器selector横空出世,他从根本问题出发,NIO不是80中断消耗大吗,那直接去内核去遍历所有的连接不就好了,这也是selector的做法,大致流程是这样的。
    在这里插入图片描述
    还有一个和selector 逻辑想用的多路复用器poll,他和selector不同之处在于selector是有连接限制的。

    下面是重头戏epoll

    在追踪内核之前我们先把epoll的大致流程讲一下,然后我们去追踪内核去证明。
    在这里插入图片描述
    图片优点笼统,笔者下面一一解释
      首先要说一点,目前我说认知的所有IO模型都需要socket、bind、listen这几个指令。
      首先还是需要生成一个socket然后通过bind命令将socket与目标端口绑定然后去监听。
      接下来就是epoll特有的指令了
      第一个epoll_create(size)这个指令会在内存开辟两个空间,一个空间用来存放需要监听的文件描述符以及需要监听对应文件描述符的事件,第二个空间是用来当有事件到来时,将对应的文件描述符以及数据放入这个缓冲区中,当程序调用accept、read时就回去缓冲区中找有没有对应的事件。
      第二个指令epoll_ctl(epfds,op,fds,event),这个指令是将对应的文件描述符以及需要监听的事件交由epoll去管理,下面解释一下参数,第一个参数epoll的文件描述符,第二个参数表示需要啥操作,第三个文件描述符表示需要监听的对象,第四个参数为需要监听的事件。
      第三个指令epoll_wait(epfds,events,maxevent,timeout),这个指令是来监听epoll中所有需要监听的事件有没有返回,如果有返回就将对应的fds与数据放入缓冲区中。参数对应的意思为,epoll的文件描述符、需要监听的事件、最大返回事件个数、超时时间。
      笔者认为有了这些基础我们可以更好的去理解epoll的原理。参数的意思可以使用man int 对应的命令,这个命令来查看。

    重点注意

      epoll有一个非常了不起的地方就是他对于收到的事件数据放入缓冲区的操作与我们程序accept/read能达到异步,这点是epoll非常厉害的地方。
    下面我们来一一证明
     笔者写了一个简陋的java代码如下
    server端

    public class NIOServer {
        private static ByteBuffer bb = ByteBuffer.allocate(1024);
        private static ServerSocketChannel ssc = null;
        private static Selector selector = null;
        private static  int port = 8888;
        public static void main(String[] args) throws IOException {
            ssc = ServerSocketChannel.open();
            ssc.configureBlocking(false);
            ssc.bind(new InetSocketAddress(port));
            selector = Selector.open();
            ssc.register(selector, SelectionKey.OP_ACCEPT);
            System.out.println("服务启动。。");
            while (true){
                while (selector.select(500)>0){
                    Set<SelectionKey> keys = selector.selectedKeys();
                    Iterator<SelectionKey> it = keys.iterator();
                    while (it.hasNext()){
                        SelectionKey selectionKey = it.next();
                        it.remove();
                        if(selectionKey.isAcceptable()){
                            handlekey(selectionKey);
                        }else if(selectionKey.isReadable()){
                            handleRead(selectionKey);
                        }
                    }
                }
            }
        }
        public static void  handlekey(SelectionKey selectionKey) throws IOException {
            ServerSocketChannel cssc = (ServerSocketChannel) selectionKey.channel();
            SocketChannel client = cssc.accept();
            client.configureBlocking(false);
            client.register(selector,SelectionKey.OP_READ);
            System.out.println("新的客户端加入"+client.getRemoteAddress());
        }
        public static void handleRead(SelectionKey selectionKey) throws IOException {
            SocketChannel cssc = (SocketChannel) selectionKey.channel();
            int read = cssc.read(bb);
            if(read>0){
                System.out.println(new String(bb.array(),0,read));
            }
        }
    }
    

    client端

    public class NIOClient {
        public static void main(String[] args) {
            try (SocketChannel socketChannel = SocketChannel.open();){
                socketChannel.connect(new InetSocketAddress("localhost", 8888));
                ByteBuffer sendBuffer = ByteBuffer.allocate(1024);
                String str = "你好,服务器我是客户端";
                sendBuffer.put(str.getBytes());
                sendBuffer.flip();
                socketChannel.write(sendBuffer);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    

      JavaNIO默认的是使用epoll,如果发现使用的不是epoll我们可以加虚拟机参数。如果没问题参数可以不加。
    在这里插入图片描述
     我们通过strace命令来对追踪我们的内核调用。打开我们的out文件。
    在这里插入图片描述
      我们先来关注这两条指令,第一个socket是对应生成socket的,并返回了一个文件描述符5,接下来看第二个指令,注意参数,第一个参数就是对应的我们socket的文件描述符,第三个参数很重要NONBLOCK表示设置我们的socket为非阻塞的。我们继续向下看。
    在这里插入图片描述
    在这里插入图片描述
      这两个指令应该很熟悉,就是bind端口,然后去监听。就不过多解释了。
    下面就是我们要重点关注的epoll的指令了。
    在这里插入图片描述
      这个指令上面已经解释过了就是用来创建我们的epoll的并分配资源。我们可以使用man命令来证实一下,如下图
    在这里插入图片描述
      这个就是epoll_create命令的解释,感兴趣可以自己去看一下。我们继续看下一条指令。
    在这里插入图片描述
      下一个指令时epoll_ctl;这些参数大家应该可以看个大概。第一个就是我们创建epoll的时候返回的文件描述符8,第二个字面意思就是添加吗,就是往epoll中去添加socket的文件描述符5,最后的参数是需要监听的事件,以及对应的文件描述符。
      下一个指令就是epoll_wait命令,第一个参数就是我们epoll的文件描述符8。而且有没有注意到超时时间,这个是不是和我们java程序中设置的超时时间是一样的,这点我们需要注意,我们先使用man 命令来看一下,指令的介绍。
    在这里插入图片描述
    在这里插入图片描述
      我们只关注返回值,有兴趣的读者可以自己翻阅一下,返回值这里说,成功时,epoll_wait()返回为请求的I/O准备的文件描述符的数量,如果在请求的超时毫秒内没有文件描述符准备就绪,则返回零。当发生错误时,epoll_wait()返回-1,我们这里的返回值为0表示当前还没有事件。
     下面我们启动客户端jar包
    在这里插入图片描述
      我们的日志文件中出现了变化,图中画红线的指令,就是表示epoll_wait收到了事件,然后发现是一个连接事件,然后就去创建连接并返回文件描述符9,我们继续向下看。
    在这里插入图片描述
      我们看到又调用了epoll_ctl将文件描述符9就是对应的我们的连接交由epoll去管理,然后再调用epoll_wait又获取了事件,这个事件就是我们客户端给服务器端发的数据。我们继续向下看。
    在这里插入图片描述
      发现后面的wait总是能返回事件,而且都一样,这说明我们程序中有很大的bug,我打断点调试后发现每次都会进到可读的逻辑中,我自己在处理读数据的时候最后 selectionKey.cancel();我不知道合理不合理,但是确实解决了问题。

      自己学习的心得希望能帮到大家,也许这种东西只有在面试的时候才有用,那就拿他来跟面试官说吧。

    展开全文
  • 干货|自定义Angular路由复用策略

    千次阅读 2018-01-31 00:00:00
    点击上方“中兴开发者社区”,关注我们每天读一篇一线开发者原创好文【摘要】 在Angular应用中,路由是大家耳熟能详的用来页面导航的技术。本文描述了Angular的路由复用策略以及如何自定义路由复用策略。【关键词】...
        

    点击上方“中兴开发者社区”,关注我们

    每天读一篇一线开发者原创好文

    640?wx_fmt=png&wxfrom=5&wx_lazy=1

    【摘要】 在Angular应用中,路由是大家耳熟能详的用来页面导航的技术。本文描述了Angular的路由复用策略以及如何自定义路由复用策略。

    【关键词】Angular  Angular2  Angular4  Angular5  路由  RouteReuseStrategy


    1 问题提出

    在基于Angular的SPA应用中,应用通过路由在各个页面之间进行导航。 默认情况下,用户在离开一个页面时,这个页面(组件)会被Angular销毁,用户的输入信息也随之丢失,当用户再次进入这个页面时,看到的是一个新生成的页面(组件),之前的输入信息都没了。

     

    配置的前端项目就是基于Angular的,工作中遇到了这样的问题,部分页面需要保存用户的输入信息,用户再次进入页面时需要回到上一次离开时的状态,部分页面每次都要刷新页面,不需要保存用户信息。而页面间的导航正是通过路由实现的,Angular的默认行为不能满足我们的需求!


    2 解决思路

    针对以上问题,通过查阅Angular的相关资料可以发现,Angular提供了RouteReuseStrategy接口,通过实现这个接口,可以让开发者自定义路由复用策略。


    2.1 RouteReuseStrategy接口

    我们先来看看RouteReuseStrategy的接口定义:

    640?wx_fmt=png&wxfrom=5&wx_lazy=1

    这个接口只定义了5个方法,每个方法的作用如下:

    • shouldDetach

    路由离开时是否需要保存页面,这是实现自定义路由复用策略最重要的一个方法。

    其中:

    返回值为true时,路由离开时保存页面信息,当路由再次激活时,会直接显示保存的页面。

    返回值为false时,路由离开时直接销毁组件,当路由再次激活时,直接初始化为新页面。

    •  store

    如果shouldDetach方法返回true,会调用这个方法来保存页面。

    •  shouldAttach

    路由进入页面时是否有页面可以重用。 true: 重用页面,false:生成新的页面

    • retrieve

    路由激活时获取保存的页面,如果返回null,则生成新页面

    • shouldReuseRout

    决定跳转后是否可以使用跳转前的路由页面,即跳转前后跳转后使用相同的页面

     

    2.2 默认的路由复用策略

    Angular实现了一个默认的路由重用策略:DefaultRouteReuseStrategy,其实现如下:

    640?wx_fmt=png

    在这个默认的路由复用策略中,只有当跳转前和跳转后的路由一致时,才会复用页面。只要跳转前和跳转后的路由不一致,页面就会被销毁。

     

    有鉴于此,我们需要实现一个自定义的路由复用策略,实现针对不同的路由,能够有不同的行为。同时,也要能兼容现有代码,不能对现有代码做大规模的修改。


    3 实践情况

    3.1 自定义路由复用策略:

    自定义路由复用策略实现如下:

    640?wx_fmt=png

    在这个路由复用策略中,有两个关键点:

    1.我们使用了一个handlers对象来保存页面。

    2.通过路由配置的data对象中的reload属性来判断一个页面是否需要保存,并且只有reload属性为false时,才会保存页面。如果不配置reload属性,或者reload属性不为false,则不会保存页面。

     

     

    3.2 配置路由重用策略为自定义策略

    为了使用自定义的路由复用策略,需要在应用的根路由模块providers中使用自定义的路由复用策略。

    640?wx_fmt=png

    3.3 配置路由

    在路由配置中,按需配置路由的data属性。如需要保存页面,则设置reload值为false,如不需要保存页面,不配置该属性。例如:

    640?wx_fmt=png

    此路由配置下,访问/foo页面始终会生成一个新的页面,而/bar页面会在路由离开时会被保存,再次进入该页面都会恢复到上一次离开该页面时的状态。

      

    4 效果评价

    在自定义了路由复用策略,并通过路由配置的data对象,可以和现有代码无缝集成,也不会对原有功能有触动,又大幅增强了原有的路由复用功能,完美解决了默认路由复用策略不足的问题

      

    5 推广建议

    在使用Angular路由的时候,路由激活时会自动生成组件的新实例,路由离开时会销毁组件实例,如果希望改变这种默认行为,就可以通过定制合适的路由复用策略来满足使用场景。同时,对路由的data属性的灵活使用,可以完美实现对现有代码的兼容。

    640?wx_fmt=png

    展开全文
  • IO多路复用

    2017-08-23 09:33:39
    为解决这问题,我们发现元凶处在“一线程一请求”上,如果一个线程能同时处理多个请求,那么在高并发下性能上会大大改善。这里就借住 JAVA 中的 nio 技术来实现这一模型。 nio 的阻塞实现 关于什么是 ...

    摘要:本文属于原创,欢迎转载,转载请保留出处:https://github.com/jasonGeng88/blog

    当前环境

    1. jdk == 1.8

    代码地址

    git 地址:https://github.com/jasonGeng88/java-network-programming

    知识点

    • nio 下 I/O 阻塞与非阻塞实现
    • SocketChannel 介绍
    • I/O 多路复用的原理
    • 事件选择器与 SocketChannel 的关系
    • 事件监听类型
    • 字节缓冲 ByteBuffer 数据结构

    场景

    接着上一篇中的站点访问问题,如果我们需要并发访问10个不同的网站,我们该如何处理?

    在上一篇中,我们使用了java.net.socket类来实现了这样的需求,以一线程处理一连接的方式,并配以线程池的控制,貌似得到了当前的最优解。可是这里也存在一个问题,连接处理是同步的,也就是并发数量增大后,大量请求会在队列中等待,或直接异常抛出。

    为解决这问题,我们发现元凶处在“一线程一请求”上,如果一个线程能同时处理多个请求,那么在高并发下性能上会大大改善。这里就借住 JAVA 中的 nio 技术来实现这一模型。

    nio 的阻塞实现

    关于什么是 nio,从字面上理解为 New IO,就是为了弥补原本 I/O 上的不足,而在 JDK 1.4 中引入的一种新的 I/O 实现方式。简单理解,就是它提供了 I/O 的阻塞与非阻塞的两种实现方式(当然,默认实现方式是阻塞的。)。

    下面,我们先来看下 nio 以阻塞方式是如何处理的。

    建立连接

    有了上一篇 socket 的经验,我们的第一步一定也是建立 socket 连接。只不过,这里不是采用 new socket() 的方式,而是引入了一个新的概念 SocketChannel。它可以看作是 socket 的一个完善类,除了提供 Socket 的相关功能外,还提供了许多其他特性,如后面要讲到的向选择器注册的功能。

    类图如下: 

    建立连接代码实现:

    // 初始化 socket,建立 socket 与 channel 的绑定关系
    SocketChannel socketChannel = SocketChannel.open();
    // 初始化远程连接地址
    SocketAddress remote = new InetSocketAddress(this.host, port);
    // I/O 处理设置阻塞,这也是默认的方式,可不设置
    socketChannel.configureBlocking(true);
    // 建立连接
    socketChannel.connect(remote);

    获取 socket 连接

    因为是同样是 I/O 阻塞的实现,所以后面的关于 socket 输入输出流的处理,和上一篇的基本相同。唯一差别是,这里需要通过 channel 来获取 socket 连接。

    • 获取 socket 连接
    Socket socket = socketChannel.socket();
    • 处理输入输出流
    PrintWriter pw = getWriter(socketChannel.socket());
    BufferedReader br = getReader(socketChannel.socket());

    完整示例

    package com.jason.network.mode.nio;
    
    import com.jason.network.constant.HttpConstant;
    import com.jason.network.util.HttpUtil;
    
    import java.io.*;
    import java.net.InetSocketAddress;
    import java.net.Socket;
    import java.net.SocketAddress;
    import java.nio.channels.SocketChannel;
    
    public class NioBlockingHttpClient {
    
        private SocketChannel socketChannel;
        private String host;
    
    
        public static void main(String[] args) throws IOException {
    
            for (String host: HttpConstant.HOSTS) {
    
                NioBlockingHttpClient client = new NioBlockingHttpClient(host, HttpConstant.PORT);
                client.request();
    
            }
    
        }
    
        public NioBlockingHttpClient(String host, int port) throws IOException {
            this.host = host;
            socketChannel = SocketChannel.open();
            socketChannel.socket().setSoTimeout(5000);
            SocketAddress remote = new InetSocketAddress(this.host, port);
            this.socketChannel.connect(remote);
        }
    
        public void request() throws IOException {
            PrintWriter pw = getWriter(socketChannel.socket());
            BufferedReader br = getReader(socketChannel.socket());
    
            pw.write(HttpUtil.compositeRequest(host));
            pw.flush();
            String msg;
            while ((msg = br.readLine()) != null){
                System.out.println(msg);
            }
        }
    
        private PrintWriter getWriter(Socket socket) throws IOException {
            OutputStream out = socket.getOutputStream();
            return new PrintWriter(out);
        }
    
        private BufferedReader getReader(Socket socket) throws IOException {
            InputStream in = socket.getInputStream();
            return new BufferedReader(new InputStreamReader(in));
        }
    }

    nio 的非阻塞实现

    原理分析

    nio 的阻塞实现,基本与使用原生的 socket 类似,没有什么特别大的差别。

    下面我们来看看它真正强大的地方。到目前为止,我们将的都是阻塞 I/O。何为阻塞 I/O,看下图:

    我们主要观察图中的前三种 I/O 模型,关于异步 I/O,一般需要依靠操作系统的支持,这里不讨论。

    从图中可以发现,阻塞过程主要发生在两个阶段上:

    • 第一阶段:等待数据就绪;
    • 第二阶段:将已就绪的数据从内核缓冲区拷贝到用户空间;

    这里产生了一个从内核到用户空间的拷贝,主要是为了系统的性能优化考虑。假设,从网卡读到的数据直接返回给用户空间,那势必会造成频繁的系统中断,因为从网卡读到的数据不一定是完整的,可能断断续续的过来。通过内核缓冲区作为缓冲,等待缓冲区有足够的数据,或者读取完结后,进行一次的系统中断,将数据返回给用户,这样就能避免频繁的中断产生。

    了解了 I/O 阻塞的两个阶段,下面我们进入正题。看看一个线程是如何实现同时处理多个 I/O 调用的。从上图中的非阻塞 I/O 可以看出,仅仅只有第二阶段需要阻塞,第一阶段的数据等待过程,我们是不需要关心的。不过该模型是频繁地去检查是否就绪,造成了 CPU 无效的处理,反而效果不好。如果有一种类似的好莱坞原则— “不要给我们打电话,我们会打给你” 。这样一个线程可以同时发起多个 I/O 调用,并且不需要同步等待数据就绪。在数据就绪完成的时候,会以事件的机制,来通知我们。这样不就实现了单线程同时处理多个 IO 调用的问题了吗?即所说的“I/O 多路复用模型”。


    废话讲了一大堆,下面就来实际操刀一下。

    创建选择器

    由上面分析可以,我们得有一个选择器,它能监听所有的 I/O 操作,并且以事件的方式通知我们哪些 I/O 已经就绪了。

    代码如下:

    import java.nio.channels.Selector;
    
    ...
    
    private static Selector selector;
    static {
        try {
            selector = Selector.open();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    

    创建非阻塞 I/O

    下面,我们来创建一个非阻塞的 SocketChannel,代码与阻塞实现类型,唯一不同是socketChannel.configureBlocking(false)

    注意:只有在socketChannel.configureBlocking(false)之后的代码,才是非阻塞的,如果socketChannel.connect()在设置非阻塞模式之前,那么连接操作依旧是阻塞调用的。

    SocketChannel socketChannel = SocketChannel.open();
    SocketAddress remote = new InetSocketAddress(host, port);
    // 设置非阻塞模式
    socketChannel.configureBlocking(false);
    socketChannel.connect(remote);

    建立选择器与 socket 的关联

    选择器与 socket 都创建好了,下一步就是将两者进行关联,好让选择器和监听到 Socket 的变化。这里采用了以 SocketChannel 主动注册到选择器的方式进行关联绑定,这也就解释了,为什么不直接new Socket(),而是以SocketChannel的方式来创建 socket。

    代码如下:

    socketChannel.register(selector,
                            SelectionKey.OP_CONNECT
                            | SelectionKey.OP_READ
                            | SelectionKey.OP_WRITE);

    上面代码,我们将 socketChannel 注册到了选择器中,并且对它的连接、可读、可写事件进行了监听。

    具体的事件监听类型如下:

    操作类型 描述 所属对象
    OP_READ 1 << 0 读操作 SocketChannel
    OP_WRITE 1 << 2 写操作 SocketChannel
    OP_CONNECT 1 << 3 连接socket操作 SocketChannel
    OP_ACCEPT 1 << 4 接受socket操作 ServerSocketChannel

    选择器监听 socket 变化

    现在,选择器已经与我们关心的 socket 进行了关联。下面就是感知事件的变化,然后调用相应的处理机制。

    这里与 Linux 下的 selector 有点不同,nio 下的 selecotr 不会去遍历所有关联的 socket。我们在注册时设置了我们关心的事件类型,每次从选择器中获取的,只会是那些符合事件类型,并且完成就绪操作的 socket,减少了大量无效的遍历操作。

    public void select() throws IOException {
    	// 获取就绪的 socket 个数
        while (selector.select() > 0){
        
        	// 获取符合的 socket 在选择器中对应的事件句柄 key
            Set keys = selector.selectedKeys();
    
    		// 遍历所有的key
            Iterator it = keys.iterator();
            while (it.hasNext()){
    
    			// 获取对应的 key,并从已选择的集合中移除
                SelectionKey key = (SelectionKey)it.next();
                it.remove();
    
                if (key.isConnectable()){
                	// 进行连接操作
                    connect(key);
                }
                else if (key.isWritable()){
                	// 进行写操作
                    write(key);
                }
                else if (key.isReadable()){
                	// 进行读操作
                    receive(key);
                }
            }
        }
    }
    

    注意:这里的selector.select()是同步阻塞的,等待有事件发生后,才会被唤醒。这就防止了 CPU 空转的产生。当然,我们也可以给它设置超时时间,selector.select(long timeout)来结束阻塞过程。

    处理连接就绪事件

    下面,我们分别来看下,一个 socket 是如何来处理连接、写入数据和读取数据的(这些操作都是阻塞的过程,只是我们将等待就绪的过程变成了非阻塞的了)。

    处理连接代码:

    // SelectionKey 代表 SocketChannel 在选择器中注册的事件句柄
    private void connect(SelectionKey key) throws IOException {
    	// 获取事件句柄对应的 SocketChannel
        SocketChannel channel = (SocketChannel) key.channel();
        
       // 真正的完成 socket 连接
        channel.finishConnect();
        
       // 打印连接信息
        InetSocketAddress remote = (InetSocketAddress) channel.socket().getRemoteSocketAddress();
        String host = remote.getHostName();
        int port = remote.getPort();
        System.out.println(String.format("访问地址: %s:%s 连接成功!", host, port));
    }

    处理写入就绪事件

    // 字符集处理类
    private Charset charset = Charset.forName("utf8");
    
    private void write(SelectionKey key) throws IOException {
        SocketChannel channel = (SocketChannel) key.channel();
        InetSocketAddress remote = (InetSocketAddress) channel.socket().getRemoteSocketAddress();
        String host = remote.getHostName();
    
    	// 获取 HTTP 请求,同上一篇
        String request = HttpUtil.compositeRequest(host);
    
    	// 向 SocketChannel 写入事件 
        channel.write(charset.encode(request));
        
        // 修改 SocketChannel 所关心的事件
        key.interestOps(SelectionKey.OP_READ);
    }

    这里有两个地方需要注意:

    • 第一个是使用 channel.write(charset.encode(request)); 进行数据写入。有人会说,为什么不能像上面同步阻塞那样,通过PrintWriter包装类进行操作。因为PrintWriter的 write() 方法是阻塞的,也就是说要等数据真正从 socket 发送出去后才返回。

    这与我们这里所讲的阻塞是不一致的,这里的操作虽然也是阻塞的,但它发生的过程是在数据从用户空间到内核缓冲区拷贝过程。至于系统将缓冲区的数据通过 socket 发送出去,这不在阻塞范围内。也解释了为什么要用 Charset 对写入内容进行编码了,因为缓冲区接收的格式是ByteBuffer

    • 第二,选择器用来监听事件变化的两个参数是 interestOps 与 readyOps

      • interestOps:表示 SocketChannel 所关心的事件类型,也就是告诉选择器,当有这几种事件发生时,才来通知我。这里通过key.interestOps(SelectionKey.OP_READ);告诉选择器,之后我只关心“读就绪”事件,其他的不用通知我了。

      • readyOps:表示 SocketChannel 当前就绪的事件类型。以key.isReadable()为例,判断依据就是:return (readyOps() & OP_READ) != 0;

    处理读取就绪事件

    private void receive(SelectionKey key) throws IOException {
        SocketChannel channel = (SocketChannel) key.channel();
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        channel.read(buffer);
        buffer.flip();
        String receiveData = charset.decode(buffer).toString();
    
    	// 当再没有数据可读时,取消在选择器中的关联,并关闭 socket 连接
        if ("".equals(receiveData)) {
            key.cancel();
            channel.close();
            return;
        }
    
        System.out.println(receiveData);
    }

    这里的处理基本与写入一致,唯一要注意的是,这里我们需要自行处理去缓冲区读取数据的操作。首先会分配一个固定大小的缓冲区,然后从内核缓冲区中,拷贝数据至我们刚分配固定缓冲区上。这里存在两种情况:

    • 我们分配的缓冲区过大,那多余的部分以0补充(初始化时,其实会自动补0)。
    • 我们分配的缓冲去过小,因为选择器会不停的遍历。只要 SocketChannel 处理读就绪状态,那下一次会继续读取。当然,分配过小,会增加遍历次数。

    最后,将一下 ByteBuffer 的结构,它主要有 position, limit,capacity 以及 mark 属性。以 buffer.flip(); 为例,讲下各属性的作用(mark 主要是用来标记之前 position 的位置,是在当前 postion 无法满足的情况下使用的,这里不作讨论)。

    从图中看出,

    • 容量(capacity):表示缓冲区可以保存的数据容量;
    • 极限(limit):表示缓冲区的当前终点,即写入、读取都不可超过该重点;
    • 位置(position):表示缓冲区下一个读写单元的位置;

    完整代码

    package com.jason.network.mode.nio;
    
    import com.jason.network.constant.HttpConstant;
    import com.jason.network.util.HttpUtil;
    
    import java.io.IOException;
    import java.net.InetSocketAddress;
    import java.net.SocketAddress;
    import java.nio.ByteBuffer;
    import java.nio.channels.SelectionKey;
    import java.nio.channels.Selector;
    import java.nio.channels.SocketChannel;
    import java.nio.charset.Charset;
    import java.util.Iterator;
    import java.util.Set;
    
    public class NioNonBlockingHttpClient {
    
        private static Selector selector;
        private Charset charset = Charset.forName("utf8");
    
        static {
            try {
                selector = Selector.open();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
    
        public static void main(String[] args) throws IOException {
    
            NioNonBlockingHttpClient client = new NioNonBlockingHttpClient();
    
            for (String host: HttpConstant.HOSTS) {
    
                client.request(host, HttpConstant.PORT);
    
            }
    
            client.select();
    
        }
    
        public void request(String host, int port) throws IOException {
            SocketChannel socketChannel = SocketChannel.open();
            socketChannel.socket().setSoTimeout(5000);
            SocketAddress remote = new InetSocketAddress(host, port);
            socketChannel.configureBlocking(false);
            socketChannel.connect(remote);
            socketChannel.register(selector,
                            SelectionKey.OP_CONNECT
                            | SelectionKey.OP_READ
                            | SelectionKey.OP_WRITE);
        }
    
        public void select() throws IOException {
            while (selector.select(500) > 0){
                Set keys = selector.selectedKeys();
    
                Iterator it = keys.iterator();
    
                while (it.hasNext()){
    
                    SelectionKey key = (SelectionKey)it.next();
                    it.remove();
    
                    if (key.isConnectable()){
                        connect(key);
                    }
                    else if (key.isWritable()){
                        write(key);
                    }
                    else if (key.isReadable()){
                        receive(key);
                    }
                }
            }
        }
    
        private void connect(SelectionKey key) throws IOException {
            SocketChannel channel = (SocketChannel) key.channel();
            channel.finishConnect();
            InetSocketAddress remote = (InetSocketAddress) channel.socket().getRemoteSocketAddress();
            String host = remote.getHostName();
            int port = remote.getPort();
            System.out.println(String.format("访问地址: %s:%s 连接成功!", host, port));
        }
    
        private void write(SelectionKey key) throws IOException {
            SocketChannel channel = (SocketChannel) key.channel();
            InetSocketAddress remote = (InetSocketAddress) channel.socket().getRemoteSocketAddress();
            String host = remote.getHostName();
    
            String request = HttpUtil.compositeRequest(host);
            System.out.println(request);
    
            channel.write(charset.encode(request));
            key.interestOps(SelectionKey.OP_READ);
        }
    
        private void receive(SelectionKey key) throws IOException {
            SocketChannel channel = (SocketChannel) key.channel();
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            channel.read(buffer);
            buffer.flip();
            String receiveData = charset.decode(buffer).toString();
    
            if ("".equals(receiveData)) {
                key.cancel();
                channel.close();
                return;
            }
    
            System.out.println(receiveData);
        }
    }
    

    示例效果

    总结

    本文从 nio 的阻塞方式讲起,介绍了阻塞 I/O 与非阻塞 I/O 的区别,以及在 nio 下是如何一步步构建一个 IO 多路复用的模型的客户端。文中需要理解的内容比较多,如果有理解错误的地方,欢迎指正~

    后续

    • Netty 下的异步请求实现
    展开全文
  • 一线互联网工程师,专注于后端技术栈,推送算法、Go、Python、Linux、分布式系统等相关技术总结与实践 公众号后台回复“面试”,获取精品学习资料 扫描下方二维码了解详情,试听课程 本文来源:蔡蔡技术记 ...

     

    看完下面这些,高频面试题你都会答了吧

    目录

    1、什么是IO多路复用?
    2、为什么出现IO多路复用机制?
    3、IO多路复用的三种实现方式
    4、select函数接口
    5、select使用示例
    6、select缺点
    7、poll函数接口
    8、poll使用示例
    9、poll缺点
    10、epoll函数接口
    11、epoll使用示例
    12、epoll缺点
    13、epoll LT 与 ET模式的区别
    14、epoll应用
    15、select/poll/epoll之间的区别
    16、IO多路复用完整代码实现
    17、高频面试题

    1、什么是IO多路复用

    「定义」

    • IO多路复用是一种同步IO模型,实现一个线程可以监视多个文件句柄;一旦某个文件句柄就绪,就能够通知应用程序进行相应的读写操作;没有文件句柄就绪时会阻塞应用程序,交出cpu。多路是指网络连接,复用指的是同一个线程

    2、为什么有IO多路复用机制?

    没有IO多路复用机制时,有BIO、NIO两种实现方式,但有一些问题

    同步阻塞(BIO)

    • 服务端采用单线程,当accept一个请求后,在recv或send调用阻塞时,将无法accept其他请求(必须等上一个请求处recv或send完),无法处理并发

    // 伪代码描述
    while(1) {
      // accept阻塞
      client_fd = accept(listen_fd)
      fds.append(client_fd)
      for (fd in fds) {
        // recv阻塞(会影响上面的accept)
        if (recv(fd)) {
          // logic
        }
      }  
    }
    
    • 服务器端采用多线程,当accept一个请求后,开启线程进行recv,可以完成并发处理,但随着请求数增加需要增加系统线程,大量的线程占用很大的内存空间,并且线程切换会带来很大的开销,10000个线程真正发生读写事件的线程数不会超过20%,每次accept都开一个线程也是一种资源浪费

    // 伪代码描述
    while(1) {
      // accept阻塞
      client_fd = accept(listen_fd)
      // 开启线程read数据(fd增多导致线程数增多)
      new Thread func() {
        // recv阻塞(多线程不影响上面的accept)
        if (recv(fd)) {
          // logic
        }
      }  
    }
    
    

    同步非阻塞(NIO)

    • 服务器端当accept一个请求后,加入fds集合,每次轮询一遍fds集合recv(非阻塞)数据,没有数据则立即返回错误,每次轮询所有fd(包括没有发生读写事件的fd)会很浪费cpu

    setNonblocking(listen_fd)
    // 伪代码描述
    while(1) {
      // accept非阻塞(cpu一直忙轮询)
      client_fd = accept(listen_fd)
      if (client_fd != null) {
        // 有人连接
        fds.append(client_fd)
      } else {
        // 无人连接
      }  
      for (fd in fds) {
        // recv非阻塞
        setNonblocking(client_fd)
        // recv 为非阻塞命令
        if (len = recv(fd) && len > 0) {
          // 有读写数据
          // logic
        } else {
           无读写数据
        }
      }  
    }
    

    IO多路复用(现在的做法)

    • 服务器端采用单线程通过select/epoll等系统调用获取fd列表,遍历有事件的fd进行accept/recv/send,使其能支持更多的并发连接请求

    fds = [listen_fd]
    // 伪代码描述
    while(1) {
      // 通过内核获取有读写事件发生的fd,只要有一个则返回,无则阻塞
      // 整个过程只在调用select、poll、epoll这些调用的时候才会阻塞,accept/recv是不会阻塞
      for (fd in select(fds)) {
        if (fd == listen_fd) {
            client_fd = accept(listen_fd)
            fds.append(client_fd)
        } elseif (len = recv(fd) && len != -1) { 
          // logic
        }
      }  
    }
    

    3、IO多路复用的三种实现方式

    • select

    • poll

    • epoll

    4、select函数接口

    #include <sys/select.h>
    #include <sys/time.h>
    
    #define FD_SETSIZE 1024
    #define NFDBITS (8 * sizeof(unsigned long))
    #define __FDSET_LONGS (FD_SETSIZE/NFDBITS)
    
    // 数据结构 (bitmap)
    typedef struct {
        unsigned long fds_bits[__FDSET_LONGS];
    } fd_set;
    
    // API
    int select(
        int max_fd, 
        fd_set *readset, 
        fd_set *writeset, 
        fd_set *exceptset, 
        struct timeval *timeout
    )                              // 返回值就绪描述符的数目
    
    FD_ZERO(int fd, fd_set* fds)   // 清空集合
    FD_SET(int fd, fd_set* fds)    // 将给定的描述符加入集合
    FD_ISSET(int fd, fd_set* fds)  // 判断指定描述符是否在集合中 
    FD_CLR(int fd, fd_set* fds)    // 将给定的描述符从文件中删除  
    

    5、select使用示例

    int main() {
      /*
       * 这里进行一些初始化的设置,
       * 包括socket建立,地址的设置等,
       */
    
      fd_set read_fs, write_fs;
      struct timeval timeout;
      int max = 0;  // 用于记录最大的fd,在轮询中时刻更新即可
    
      // 初始化比特位
      FD_ZERO(&read_fs);
      FD_ZERO(&write_fs);
    
      int nfds = 0; // 记录就绪的事件,可以减少遍历的次数
      while (1) {
        // 阻塞获取
        // 每次需要把fd从用户态拷贝到内核态
        nfds = select(max + 1, &read_fd, &write_fd, NULL, &timeout);
        // 每次需要遍历所有fd,判断有无读写事件发生
        for (int i = 0; i <= max && nfds; ++i) {
          if (i == listenfd) {
             --nfds;
             // 这里处理accept事件
             FD_SET(i, &read_fd);//将客户端socket加入到集合中
          }
          if (FD_ISSET(i, &read_fd)) {
            --nfds;
            // 这里处理read事件
          }
          if (FD_ISSET(i, &write_fd)) {
             --nfds;
            // 这里处理write事件
          }
        }
      }
    

    6、select缺点

    • 单个进程所打开的FD是有限制的,通过FD_SETSIZE设置,默认1024

    • 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大

    • 对socket扫描时是线性扫描,采用轮询的方法,效率较低(高并发时)

    7、poll函数接口

    poll与select相比,只是没有fd的限制,其它基本一样

    #include <poll.h>
    // 数据结构
    struct pollfd {
        int fd;                         // 需要监视的文件描述符
        short events;                   // 需要内核监视的事件
        short revents;                  // 实际发生的事件
    };
    
    // API
    int poll(struct pollfd fds[], nfds_t nfds, int timeout);
    

    8、poll使用示例

    // 先宏定义长度
    #define MAX_POLLFD_LEN 4096  
    
    int main() {
      /*
       * 在这里进行一些初始化的操作,
       * 比如初始化数据和socket等。
       */
    
      int nfds = 0;
      pollfd fds[MAX_POLLFD_LEN];
      memset(fds, 0, sizeof(fds));
      fds[0].fd = listenfd;
      fds[0].events = POLLRDNORM;
      int max  = 0;  // 队列的实际长度,是一个随时更新的,也可以自定义其他的
      int timeout = 0;
    
      int current_size = max;
      while (1) {
        // 阻塞获取
        // 每次需要把fd从用户态拷贝到内核态
        nfds = poll(fds, max+1, timeout);
        if (fds[0].revents & POLLRDNORM) {
            // 这里处理accept事件
            connfd = accept(listenfd);
            //将新的描述符添加到读描述符集合中
        }
        // 每次需要遍历所有fd,判断有无读写事件发生
        for (int i = 1; i < max; ++i) {     
          if (fds[i].revents & POLLRDNORM) { 
             sockfd = fds[i].fd
             if ((n = read(sockfd, buf, MAXLINE)) <= 0) {
                // 这里处理read事件
                if (n == 0) {
                    close(sockfd);
                    fds[i].fd = -1;
                }
             } else {
                 // 这里处理write事件     
             }
             if (--nfds <= 0) {
                break;       
             }   
          }
        }
      }
    

    9、poll缺点

    • 每次调用poll,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大

    • 对socket扫描时是线性扫描,采用轮询的方法,效率较低(高并发时)

    10、epoll函数接口

    #include <sys/epoll.h>
    
    // 数据结构
    // 每一个epoll对象都有一个独立的eventpoll结构体
    // 用于存放通过epoll_ctl方法向epoll对象中添加进来的事件
    // epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可
    struct eventpoll {
        /*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/
        struct rb_root  rbr;
        /*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/
        struct list_head rdlist;
    };
    
    // API
    
    int epoll_create(int size); // 内核中间加一个 ep 对象,把所有需要监听的 socket 都放到 ep 对象中
    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); // epoll_ctl 负责把 socket 增加、删除到内核红黑树
    int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);// epoll_wait 负责检测可读队列,没有可读 socket 则阻塞进程
    

    11、epoll使用示例

    int main(int argc, char* argv[])
    {
       /*
       * 在这里进行一些初始化的操作,
       * 比如初始化数据和socket等。
       */
    
        // 内核中创建ep对象
        epfd=epoll_create(256);
        // 需要监听的socket放到ep中
        epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);
     
        while(1) {
          // 阻塞获取
          nfds = epoll_wait(epfd,events,20,0);
          for(i=0;i<nfds;++i) {
              if(events[i].data.fd==listenfd) {
                  // 这里处理accept事件
                  connfd = accept(listenfd);
                  // 接收新连接写到内核对象中
                  epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);
              } else if (events[i].events&EPOLLIN) {
                  // 这里处理read事件
                  read(sockfd, BUF, MAXLINE);
                  //读完后准备写
                  epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);
              } else if(events[i].events&EPOLLOUT) {
                  // 这里处理write事件
                  write(sockfd, BUF, n);
                  //写完后准备读
                  epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);
              }
          }
        }
        return 0;
    }
    

    12、epoll缺点

    • epoll只能工作在linux下

    13、epoll LT 与 ET模式的区别

    • epoll有EPOLLLT和EPOLLET两种触发模式,LT是默认的模式,ET是“高速”模式。

    • LT模式下,只要这个fd还有数据可读,每次 epoll_wait都会返回它的事件,提醒用户程序去操作

    • ET模式下,它只会提示一次,直到下次再有数据流入之前都不会再提示了,无论fd中是否还有数据可读。所以在ET模式下,read一个fd的时候一定要把它的buffer读完,或者遇到EAGAIN错误

    14、epoll应用

    • redis

    • nginx

    15、select/poll/epoll之间的区别

      select poll epoll
    数据结构 bitmap 数组 红黑树
    最大连接数 1024 无上限 无上限
    fd拷贝 每次调用select拷贝 每次调用poll拷贝 fd首次调用epoll_ctl拷贝,每次调用epoll_wait不拷贝
    工作效率 轮询:O(n) 轮询:O(n) 回调:O(1)

    16、完整代码示例

    https://github.com/caijinlin/learning-pratice/tree/master/linux/io

    17、高频面试题

    • 什么是IO多路复用?

    • nginx/redis 所使用的IO模型是什么?

    • select、poll、epoll之间的区别

    • epoll 水平触发(LT)与 边缘触发(ET)的区别?

     

     

     

    展开全文
  • 具体到每个公司时,则必须把以前所做的项目中包含的个性化的内容归纳出来,提取共性,提高项目中可复用的程度。最终使项目能满足起百分之七八十的企业的需求,预留部分容易修改需求给客户,满足客户...
  • BIO与NIO与多路复用

    2020-09-05 08:28:10
    回复"面试"获取一线大厂面试资料 7.回复"进阶之路"获取Java进阶之路的思维导图 8.回复"手册"获取阿里巴巴Java开发手册(嵩山终极版) 9.回复"总结"获取Java后端面试经验总结PDF版 10.回复"Redis"获取Redis命令手册,和...
  • K8s已经成为一线大厂分布式平台的标配技术。你是不是还在惆怅怎么掌握它?来这里,大型互联网公司一线工程师亲授,不来虚的,直接上手实战,3天时间带你搭建K8s平台,快速学会K8s,点击下方...
  • JAVA NIO 一步步构建I/O多路复用的请求模型 转自:https://github.com/jasonGeng88...
  • JAVA NIO 一步步构建I/O多路复用的请求模型 转自:https://github.com/jasonGeng88/blog 摘要:本文属于原创,欢迎转载,转载请保留出处:https://github.com/jasonGeng88/blog 文章一:JAVA 中原生的 socket ...
  • 学习了java,自然知道了面向对象编程,找出有共性的类创建父类实现代码的复用也成了敲代码的家常便饭。继承的使用极大的方便了程序员的开发,你可以抽取出共有的代码放在父类中,避免了子类中大量的重复代码,而且更...
  • Web Component 是一种 W3C标准支持的组件化方案,通过它可以编写可复用的组件,同时也可以对自己的组件做更精细化的控制。更牛的是,Web Component可以在任何一种框...
  • Web Component 是一种 W3C标准支持的组件化方案,通过它可以编写可复用的组件,同时也可以对自己的组件做更精细化的控制。更牛的是,Web Component 可以在任何一种框...
  • 我们如何根据目前一线大厂前端生存现状以及业务模式发展的方向来优化自己呢?如何实现技术思维开拓以及如何调整自己的开发模式,突破自己的技术瓶颈呢?所以呢,Vue的学习任重而道远,希望小伙伴们都可以静下心来...
  • JAVA NIO 一步步构建I/O多路复用的请求模型 摘要:本文属于原创,欢迎转载,转载请保留出处:https://github.com/jasonGeng88/blog 文章一:JAVA 中原生的 socket 通信机制 当前环境 jdk == 1.8 代码地址 git 地址...
  • 单点的小规模尝试,导致团队内部技术栈以及实现方案出现分化,间接造成了知识库之间的隔离、项目之间模块复用率下降,人员在不同项目中的学习成本大大增加,技术管理成本被大量转嫁到人员管理上。
  • 模板方法模式是所有模式中最为常见的几个模式之一,是基于继承的代码复用的基本技术,我们再看下类图 模板方法模式就是用来创建一个算法的模板,这个模板就是方法,该方法将算法定义成一组步骤,其中的任意步骤都...
  • 全网极具参考价值的Python面试题,从创业公司到一线大厂的面经汇总整理。作者会持续维护更新!
  • Java网络编程与NIO详解2:JAVA NIO一步步构建IO多路复用的请求模型 知识点 nio 下 I/O 阻塞与非阻塞实现 SocketChannel 介绍 I/O 多路复用的原理 事件选择器与 SocketChannel 的关系 事件...
  • JAVA NIO 一步步构建I/O多路复用的请求模型 摘要:本文属于原创,欢迎转载,转载请保留出处:https://github.com/jasonGeng88/blog 文章一:JAVA 中原生的 socket 通信机制 当前环境 jdk == 1.8 代码地址 git ...
  • I/O 多路复用的原理 事件选择器与 SocketChannel 的关系 事件监听类型 字节缓冲 ByteBuffer 数据结构 场景 接着上一篇中的站点访问问题,如果我们需要并发访问10个不同的网站,我们该如何处理? 在上一篇中..
  • 统一架构优化之后迁移成本整体控制在 30% 以下,其实平均是低于 20% 的,在这个阶段下,美团多应用同时支持需求迭代的情况就变得可行了,而且是一个常态化的过程,代码复用率也从一开始不复用,到后面整体复用率在 ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 6,074
精华内容 2,429
关键字:

一线复用