• Socket与IO

    • I/O模型

      • 概述
      • 阻塞I/O
      • 非阻塞I/O
      • I/O复用
      • 信号驱动I/O
      • 异步I/O
      • 五大I/O模型比较
    • I/O复用
      • 概述IO复用
      • select
      • poll
      • epoll
    • Java中的NIO
      • 组件1:Buffer
      • 组件2:Channel
      • 组件3:Selector
    • 联系实际:I/O
      • 基于Tomcat9.0.21

        • Tomcat的IO模型
        • 组件与框架概述
        • 生命周期、启动、停止
        • 请求过程
          • Acceptor
          • Poller和PollerEvent
          • SocketProcessor和ConnectionHandler
          • 小结
        • Container
        • Mapper
      • Redis的IO模型

Socket与IO

I/O模型

概述

  1. 输入操作包括两个阶段1.等待数据准备好2.从内核向进程复制数据
  2. 比如Socket套接字的输入操作,第一步等级数据从网络中送达,数据到达后被复制到内核中的缓冲区。第二部,数据从内核缓冲区复制到应用进程缓冲区。
  3. UNIX有五种I/O模型
    • 阻塞I/O
    • 非阻塞I/O
    • I/O复用:select和poll
    • 信号驱动式I/O:SIGIO
    • 异步I/O:AIO

阻塞I/O

  • 概述:应用进程被阻塞,直到数据从内核缓冲区复制到引用缓冲区才返回,注意这个阻塞的进程是不会响应中断的。如果想提高吞吐量,必须通过多线程。

  • 同步阻塞

非阻塞I/O

  • 概述:应用进程发出需要I/O数据的请求然后就做别的事去了,内核在等待数据,进程不断轮询(polling)来判断内核缓冲区数据是否准备完毕。因为要不断轮询,因此消耗大量CPU时间。

  • 同步非阻塞

I/O复用

  • 概述:如何让单线程获得处理多个I/O的能力呢?事件驱动I/O,和非阻塞相比优点是可以等待多个描述符。非阻塞是一个线程等待一个描述符,通过反复轮询确认内核是否准备好缓冲区数据,IO复用是一个线程等待多个描述符,只要有描述符就返回相应的IO数据。 方法有select,poll和epoll,在使用这些方法的时候是阻塞的。相较于多线程技术,IO复用开销更小,redis就使用了这种方式。

  • 对比阻塞式IO,IO复用模型还需要多调用一次select,然后在select中阻塞,因此易用性上略显不足。但是IO复用最大的亮点是监听多个文件描述符,大大减小了阻塞线程的个数。

  • 同步非阻塞(select层面上阻塞,但是应用进程不阻塞,即IO是非阻塞的(比如说read方法是不阻塞的,但是select是阻塞的),select是阻塞的)

信号驱动I/O

  • 概述:开启套接字的信号驱动式IO功能,通过sigaction系统调用安装一个信号处理函数,当内核接收到数据时,sigaction这个异步方法会被执行,给引用线程发送SIGIO信号,然后应用线程从内核中读取数据。

  • 同步非阻塞

异步I/O

  • 概述:应用进程调用一个方法(aio_read)后就返回,等内核缓冲区的数据准备好后,内核将数据复制到应用进程缓冲区,完成这一步就通知应用程序。和信号驱动IO的区别是,信号驱动IO是由内核通知应用线程可以开始从内核缓冲区中复制了,而异步直接是把数据已经放进了应用进程的缓冲区里。

  • 异步非阻塞

五大I/O模型比较

  • 同步IO:将内核缓冲区数据复制到应用进程缓冲区这个操作是应用进程做的,就是同步。
  • 异步IO:内核执行缓存区复制,就是异步.
  • 阻塞:read方法阻塞
  • 非阻塞:read方法立即返回

I/O复用

概述IO复用

https://segmentfault.com/q/1010000006694787

  • 在不用I/O复用的情况下,如何处理多请求:

    1. 主线程循环执行accept,等待客户端连接。
    2. 当客户端connect后,新建一个线程用于处理请求。当然也可以交给线程池去处理。
  • 在用I/O服用的情况下。
    1. 主线程执行select,select中注册多个文件描述符,轮询这些文件描述符,如果发现connect,可以根据不同的策略执行,比如在select当前线程执行,新建线程或者交给线程池处理。
  • 区别:如果有10000并发连接数,但是只有100个活动请求,如果用方案1就要创建100000个线程处理,如果是方案2就可以用1个线程处理,或者用少量线程处理。

select

  1. int select(int maxfdp1, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
  2. maxfdp1表示待测试的描述符个数,也就是要轮询的数量,从0开始到maxfdp1-1。readset,writeset和exceptset指定我们要内核测试读,写和异常的描述符。这个应该相当于是向select进程中注册事件。
  3. 宏命令:FD_ISSET(int fd,fd_set *fdset)检查集合中指定的文件描述符是否可以读写。
  4. timeout参数告知内核等待所指定描述符中的任何一个就绪可花多少事件。可以指定s或者ms。该参数可以指定为NULL即永远等待下去,timeout等待一段固定时间,0根本不等待。****
  5. select线程遍历所有的readfds,writefds和exceptfds。重要的是了解fd_set的数据结构,他是类似bitmap的,FD_ZERO(&set)则set为00000000,若注册fd=1,fd=2择优set为00000011,若还有fd=5则00010011。 遍历结束后,会改写set的值,然后再遍历所有的fd,看fd对应的位是否为1,那对应的fd就是已经准备就绪的,因此要有两次遍历。
  • 缺点

    1. 单个进程打开的fd有限值,FD_SETSIZE默认为1024,64位为2048。cat /proc/sys/fs/file-max
    2. fd_set返回时会被内核修改,因此每次调用都要重新修改,不够优雅
    3. 两次遍历,内核遍历一次查询注册的fd是否准备好缓冲区数据,用户再遍历一次查看自己的fd是否准备好,效率较低
    4. 需要维护存放大量fd的数据结构(xxxset),在内核态和用户态传递该结构开销大。

poll

  1. int poll(struct pollfd *fds, unsigned int nfds, int timeout);nfds表示maxfdp1,就是待测试的描述符个数。fds结构是变程度数组,代替了之前所有的set,这个fds中定义了很多事件。
  2. 与select的区别
    • 没有最大连接数的限制。- poll的timeout事件精度是ms,select的pselect方法精度可以到us,一般的是s和ms。
    • polldf中有一个events和一个revents,前者表示等待的事件,后者表示实际发生的事件,用户修改前者,内核修改后者,解决了每次调用时都要重新修改fd_set的问题,更加优雅
    • select的timeout为null表示无线等待,poll是-1.
    • poll可以监听更多事件,比如POLLIN,POLLOUT,POLLERR,POLLMSG等,在poll.h中有定义。
    • select几乎所有系统都支持。
  3. 水平触发特点,当通知程序fd就绪后,如果本次未被处理,那下次poll的时候会再次通知同个fd已经就绪。水平触发的意思是,只要内核缓冲区就数据就通知fd,边沿触发的意思是,只有内核缓冲区接收到新数据请求才通知fd,区别是如果客户端没有接收完毕数据,水平触发第二次仍会通知,但是边沿触发要等到新的数据到来(中断)才通知。 select也支持水平触发

epoll

  1. int epoll_create(int size);
    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
    
    • epoll_create在内核专属epoll的高速cache区建立红黑树和就绪链表,用户传入的句柄将被放入红黑树。(全部传入,第一次拷贝) select监控的fd在用户态,然后拷贝fd到内核态,内核态处理完set后再返回用户,轮询N次复制N次,但是epoll只复制一次,之后就等待回调。
    • epoll_ctl执行add动作时,将fd放入红黑树,向内核注册该fd的回调函数。当fd可读可写时调用回调函数,回调函数将fd放入就绪链表。
    • epoll_wait是阻塞的,监控就绪链表,如果就绪链表有文件句柄,则表示该文件句柄可读可写,并返回到用户态。(这里只有少量拷贝)
    • 由于内核不修改fd的位,因此只需要第一次传入就可以重复监控,直到epoll_ctl删除。
  2. epoll是linux系统独有的,具有水平触发和边沿触发两种,上面已经解释过了。边沿触发的好处是有效减少了触发次数。
    问题:

  3. 为什么要用红黑树?红黑树用于存放socket,当ctl中添加的时候,可以通过红黑树快速查找是否存在,如果用线性表就是O(n)时间复杂度了。

  4. 为什么epoll看上去是异步调用,但是还说叫同步IO?因为从微观来看,epoll是异步的,但是对于epoll_wait方法是阻塞的,根据判断标准应用进程实现的缓存区拷贝,因此是同步

Java中的NIO

组件1:Buffer

  1. NIO是面向缓冲区的,本质上是一块内存区域,核心属性是capacity,limit和position。capacity指的是当前缓冲区最大字节数,limit在写入时等于capacity,在读取时等于能读取的最大数据量,positon代表当前指针,读了多少就向后移动多少。
  2. 关键方法
    • ByteBuffer buffer = ByteBuffer.allocate(1024),分配容量为1024字节的缓冲区。
    • channel.read(buffer) 从channel中读数据并写入buffer buffer.put(byte[])向buffer中写入字节
    • channel.write(buffer) 向channel中写数据,即从buffer中读数据
    • flip 从写模式转换为读模式
    • rewind,将position置为0,这样可以重复读取缓冲区数据
    • clear和compact clear全部清除,compact保留还未读的缓冲区部分
    • mark,reset mark可以标记当前position,然后reset回到标记的mark

组件2:Channel

  1. 有几个重要的实现

    • FileChannel,这个类无法非阻塞读写
    • DatagramChannel 用于UDP的读写
    • SocketChannel 用于TCP的数据读写,客户端实现
    • ServerSocketChannel 服务器实现,但是为了和客户端对接,因此还需要用SocketChannel接收和发送给客户端数据,ServerSocketChannel通过accept方法会返回一个SocketChannel对象,用语和客户端之间进行读写。
  2. 可以通过configureBlocking(false)设置为非阻塞,默认为阻塞。当设置为非阻塞的时候要注意,比如你channel.read(buffer),但是别人没发过来,因为是非阻塞,就直接返回执行后面的代码了,因此需要用while。这也引出了下面的问题:
    • 如果用阻塞I/O,需要多线程(浪费内存),如果用非阻塞I/O,需要不断重试(耗费CPU)。Selector的出现解决了这尴尬的问题,非阻塞模式下,通过Selector,我们的线程只为已就绪的通道工作,不用盲目的重试了。比如,当所有通道都没有数据到达时,也就没有Read事件发生,我们的线程会在select()方法处被挂起,从而让出了CPU资源。

组件3:Selector

  1. 可以用较少的线程处理更多的通道。
  2. 要将channel注册到selector中,ServerSocketChannel ssc = ServerSocketChannel.open();ssc.register(selector, SelectionKey.OP_ACCEPT); 事件可以选择如下的:Connect,Accept,Write,Read。SelectionKey.OP_CONNECT,OP_ACEPT,OP_READ,OP_WRITE。
  3. 一个SelectionKey表示某个通道对象和某个selector之间的注册关系。关键方法如下
    • interestOps,获得key感兴趣的事件,返回值是一个掩码,和OP_XXX按位与后可以得到状态。
    • readyOps,返回一个掩码,可以用于判断哪些状态ready了。但是一般用isWritable,isConnetable,isAcceptable等方法直接判断,这下方法内部就是用的readOops。
    • key.channel(),key.selector()可以获取key上关联的通道和复用器。
    • attach方法可以在key上附着一个对象,比如附着一个Buffer。
  4. int nReady = selector.select();可能会阻塞在此处,这也是遵循IO复用模型的。select()方法返回的int值表示有多少通道已经就绪,是自上次调用select()方法后有多少通道变成就绪状态。之前在select()调用时进入就绪的通道不会在本次调用中被记入,而在前一次select()调用进入就绪但现在已经不在处于就绪的通道也不会被记入。本质上用的是linux中epoll的边沿触发。
  5. Set<SelectionKey> keys = selector.selectedKeys(); 可以获得所有已注册的健,然后用迭代器遍历这些健,判断状态,根据状态处理。

联系实际:I/O

基于Tomcat9.0.21

Tomcat的IO模型

  1. BIO:一个连接对应一个线程。(在tomcat8.5后被淘汰)
  2. NIO:一个请求对应一个线程。(多个长连接,有连接,不一定有请求,如果用BIO就会有大量闲置线程。)
  3. APR:Apache Portable Runtime。Apache可移植运行库,以JNI的形式调用阿帕奇HTTP服务器的核心动态链接库,调用系统底层的IO接口。(不通过java的nio包)
  • NIO概述:Tomcat的NIO是基于I/O复用模型。

    可以发现,tomcat6以后利用java对read request body与write response使用了BIO,对read request headers和wait for next request实现了NIO。Tomcat8.5后全面取消BIO

    • 为什么要这样设置?nio在网络不好的时候,处理http request head的时候根本不会不浪费socketProcesser线程去等待读(bio是阻塞的,nio直接继续丢到poller池轮询,就绪了再分配socketProcesser线程处理)。另外bio在处理长连接的时候天生就弱势,必须要浪费一个socketProcesser等待读下一次http请求,指导超时才会释放socket(然而tomcat一般默认200个socketProcesser线程,bio超过75%长连接后面就直接当短连接处理就知道bio对长连接处理多弱),而nio是poller处理TCP连接,有数据就绪就分配到线程池处理http;单线程优势啊,几乎无内核用户态切换,不浪费cpu。
  • (Tomcat6.x)执行过程:客户端连接到达 -> nio接收连接 -> nio使用轮询方式读取文本并且解析HTTP协议(单线程) -> 生成ServletRequest、ServletResponse,取出请求的Servlet -> 从线程池取出线程,并在该线程执行这个Servlet -> 把ServletResponse的内容发送到客户端连接 -> 关闭连接。

组件与框架概述

  • 以下内容参考:

    https://blog.csdn.net/linxdcn/article/details/73321304

    https://blog.csdn.net/sunyunjie361/article/details/60126398

  1. server对应多个service,service中具有connectors(多个connector)和一个contaioner,在tomcat9.x源码中,connector和engine持有service的引用,但是engine和connector之间并没有引用关系。
    图中server表示tomcat启动的服务,service表示webapp,每个webapp都包括n组engine和connector,connector负责接收用户请求,engine是逻辑容器。host为主机名,因为虚拟主机的存在,因此host也可能有多个,context就是web.xml转化而来的,wrapper就是servlet。

  2. 组件详细介绍

    • server 读取server.xml中的配置文件,将配置文件加载到Server的实现类StandardServer中。若关闭server,将关闭全部组件。包含方法有获取端口,地址,持有多个service引用。
    • service Service接口实现类standardService,逻辑上持有Connector和Container。并且持有向server的引用。service和server是相互关联的,server向外提供访问这些service的接口
    • connector,负责处理客户端发来的连接,默认协议有http,https和AJP。主要作用为根据不同请求解析客户端请求,将解析好的请求转发给connector关联的engine容器,即container。这个组件就是IO模型应用的地方,本质上是为了解决请求等待的问题。100000个长连接,不可能开那么多线程,那就开少量的selector线程轮询,将请求放到线程池中执行即可。

    以下四个都继承自Container接口,都属于Container

    manger用于管理session,resources对每个webaap对应部署结构的封装,loader对每个webapp自有的classloader的封装,mapper封装了请求资源uri和相对应处理wrapper容器的映射关系。

    • 以上组件中的cluster用于tomcat管理,Realm实现用户权限管理,pipeline和value利用职责链模式处理pipeline上的各个value。

生命周期、启动、停止

  1. tomcat中所有组件都继承LifeCycle接口,lifecycle的抽象实现类中有initt方法,如果当前状态为NEW,那么就将状态转换为INITIALIZING状态并进行初始化,初始化完毕后状态为INITIALIZED。

    //LifeCycleBase.java
    //init方法
    @Override
    public final synchronized void init() throws LifecycleException {if (!state.equals(LifecycleState.NEW)) {invalidTransition(Lifecycle.BEFORE_INIT_EVENT);}try {setStateInternal(LifecycleState.INITIALIZING, null, false);initInternal();setStateInternal(LifecycleState.INITIALIZED, null, false);} catch (Throwable t) {handleSubClassException(t, "lifecycleBase.initFail", toString());}
    }
    ``
    
  2. 以StandardServer为例,他的initInternal方法中调用类加载器加载jar包,这是双亲委派的,然后遍历services组件并init。service组件对其中的connectors和engine还有Executors进行init。

    //StandardServer.java
    //initInternal方法
    for (int i = 0; i < services.length; i++) {services[i].init();
    }
    
  3. init之后是start,以StandardServer为例,让内部的services调用start,以此类推。

    //LifeCycleBase.java
    //start方法节选
    try {setStateInternal(LifecycleState.STARTING_PREP, null, false);startInternal();if (state.equals(LifecycleState.FAILED)) {// This is a 'controlled' failure. The component put itself into the// FAILED state so call stop() to complete the clean-up.stop();} else if (!state.equals(LifecycleState.STARTING)) {// Shouldn't be necessary but acts as a check that sub-classes are// doing what they are supposed to.invalidTransition(Lifecycle.AFTER_START_EVENT);} else {setStateInternal(LifecycleState.STARTED, null, false);}
    }
    
  4. 小结:每个组件的生命周期是统一的,init->start->stop->destory,这是由LifeCycleBase统一通知的,每个组件单独实现initInternal、startInternal、stopInternal、destoryInternal,使用方法前先判断LifeCycle枚举类状态,然后根据状态执行不同的生命周期方法,执行完成后改变状态。

  5. 观察者设计模式。为什么要用观察者设计模式呢?多个观察者对象同时监听一个主体对象,当主体对象作出某种操作时,主体对象会通知观察者,然后观察者作出相应动作。当一个对象改变的同时需要改变其他对象时,可以采用观察者模式,易于扩展且降低耦合。 所以狼来了的故事里,小孩是通知者,也就是subject,大人们是监听者,在大话设计模式里,前台是通知者,同事们是监听者。

    //监听者(观察者)
    //public class EngineConfig implements LifecycleListener
    @Override
    public void lifecycleEvent(LifecycleEvent event) {// Identify the engine we are associated withtry {engine = (Engine) event.getLifecycle();} catch (ClassCastException e) {log.error(sm.getString("engineConfig.cce", event.getLifecycle()), e);return;}// Process the event that has occurredif (event.getType().equals(Lifecycle.START_EVENT))start();else if (event.getType().equals(Lifecycle.STOP_EVENT))stop();}//被监听者(被观察者) server组件
    protected void startInternal() throws LifecycleException {fireLifecycleEvent(CONFIGURE_START_EVENT, null);setState(LifecycleState.STARTING);//当被监听者start的时候,监听器会通知所有
    protected void fireLifecycleEvent(String type, Object data) {LifecycleEvent event = new LifecycleEvent(this, type, data);for (LifecycleListener listener : lifecycleListeners) {listener.lifecycleEvent(event);}
    }
    
  6. EngineConfig,HostConfig,ContextConfig都是通过监听器来实现的,达到了配置逻辑和容器的解耦,修改配置逻辑不需要改动容器,修改容器也不用改动配置逻辑。

  7. 当我们运行tomcat/bin/startup的时候发生了什么?我们相当于调用Bootstrap类的main方法,并传入一个“start”参数,Bootstrap会根据这个字符串反射调用方法。

    //Bootstrap.java
    public static void main(String args[]) {synchronized (daemonLock) {if (daemon == null) {// Don't set daemon until init() has completedBootstrap bootstrap = new Bootstrap();try {bootstrap.init();} catch (Throwable t) {handleThrowable(t);t.printStackTrace();return;}daemon = bootstrap;} else {// When running as a service the call to stop will be on a new// thread so make sure the correct class loader is used to// prevent a range of class not found exceptions.Thread.currentThread().setContextClassLoader(daemon.catalinaLoader);}}try {String command = "start";if (args.length > 0) {command = args[args.length - 1];}if (command.equals("startd")) {args[args.length - 1] = "start";daemon.load(args);daemon.start();} else if (command.equals("stopd")) {args[args.length - 1] = "stop";daemon.stop();} else if (command.equals("start")) {daemon.setAwait(true);daemon.load(args);daemon.start();if (null == daemon.getServer()) {System.exit(1);}} else if (command.equals("stop")) {daemon.stopServer(args);} else if (command.equals("configtest")) {daemon.load(args);if (null == daemon.getServer()) {System.exit(1);}System.exit(0);} else {log.warn("Bootstrap: command \"" + command + "\" does not exist.");}} catch (Throwable t) {// Unwrap the Exception for clearer error reportingif (t instanceof InvocationTargetException &&t.getCause() != null) {t = t.getCause();}handleThrowable(t);t.printStackTrace();System.exit(1);}
    }
    

  1. 在这个这个类中还值得关注一点类加载器。根据我之前的了解,除了webapp的类加载器默认子加载器优先外,其他都还是执行双亲委派的。

    private void initClassLoaders() {try {commonLoader = createClassLoader("common", null);if (commonLoader == null) {// no config file, default to this loader - we might be in a 'single' env.commonLoader = this.getClass().getClassLoader();}//commonLoader是catalinaLoader和sharedLoader的父加载器。根据上面的英文注释,默认情况下这三个加载器的加载范围是同一个,通过查阅配置文件也可以得知,只有common有加载路径,shared和catalina都是没有加载路径的。catalinaLoader = createClassLoader("server", commonLoader);sharedLoader = createClassLoader("shared", commonLoader);} catch (Throwable t) {handleThrowable(t);log.error("Class loader creation threw exception", t);System.exit(1);}
    }

    catalina.properties关于类加载器配置如下:
    common.loader="catalina.base/lib&quot;,&quot;{catalina.base}/lib&quot;,&quot;catalina.base/lib","{catalina.base}/lib/.jar","catalina.home/lib&quot;,&quot;{catalina.home}/lib&quot;,&quot;catalina.home/lib","{catalina.home}/lib/.jar"
    server.loader=
    shared.loader=

    源码中解析路径的时候还要做${字符判断的原因就在这里。

  2. 上文中看到了catalina_base和catalina_home,有什么区别呢?home是安装目录,base是工作目录。home主要包括bin和lib,base包括conf,logs,temps,webapps,work和shared。所以IDEA运行tomcat其实是为每个webapp都复制了一份catalina_base(work,logs,conf),但是公用同一个tomcat的bin和lib,同一个版本但是可以多实例的运行,部署难度降低,不然你就要安装多个tomcat。然后还要再conf->catalina->localhost中放一个工程.xml以表示工程位置在哪里。



  3. 总结

启动过程为:main方法接收到参数start交给bootstrap类,boostrap类委托给catalina类,catalina类中initserver,一旦开始组件的初始化就一发不可收拾了,就链式将该组件下的所有组件init。然后执行boostrap的start,委托给catalina进行start,然后server先start,之后所有组件start。

请求过程

  1. tomcat9.x的IO模型

    • nio模式是基于Java nio包实现的,能提供非阻塞I/O操作,拥有比传统I/O操作(bio)更好的并发运行性能。在Tomcat9中是默认模式。
    • apr模式(Apache Portable Runtime/Apache可移植运行时),是Apache HTTP服务器的支持库。可以简单地理解为Tomcat将以JNI的形式调用Apache HTTP服务器的核心动态链接库来处理文件读取或网络传输操作,从而大大地提高了对静态文件的处理性能。
  2. 概述
  3. 启动顺序
    • Connector组件的构造方法通过反射创建Http11NioProtocol类实例,这个实例内部有一个NioEndpoint(负责接收请求)和一个ConnectorHandler(负责处理请求)。NioEndpoint和ConnectorHandler在创建Http11NioProtocol的时候被创建出来。
  4. NioEndPoint包含三个组件:Acceptor(负责监听请求)、Poller(接收监听到的请求socket)、SocketProcessor(Worker,处理socket,本质上委托给ConnectionHandler处理)。
Acceptor
  1. Acceptor在NioEndpoint中被创建出来,Acceptor这个类本身是实现Runnable的,因此是一个线程。在9.0.21版本中,这里是直接运行的,在9.x的早期版本中应该还有一个acceptors保存了所有的acceptor。

    AbstractNioEndPoint.java
    protected void startAcceptorThread() {acceptor = new Acceptor<>(this);String threadName = getName() + "-Acceptor";acceptor.setThreadName(threadName);Thread t = new Thread(acceptor, threadName);t.setPriority(getAcceptorThreadPriority());t.setDaemon(getDaemon());t.start();
    }//public class Acceptor<U> implements Runnable
    

    在Acceptor.java -> run()中,socket = endpoint.serverSocketAccept(); 这里的endpoint.serverSocketAccept本质上是调用的serverSocketChannel.accept。再看下一段代码,NioEndpoint->initServerSocket()中,serverSock.configureBlocking(true); 说明是阻塞的,因此Acceptor在监听连接的时候是阻塞的。当有请求过来时,endpoint.setSocketOptions(socket),这里是将socket包装起来生成secketWrapper(设置socket发送、接收缓存大小,心跳检测),将SocketChannel包装成NioChannel,调用poller池中的register方法提交给poller。

    public class NioEndpoint {protected boolean setSocketOptions(SocketChannel socket) {// Process the connectiontry {// Disable blocking, polling will be usedsocket.configureBlocking(false);Socket sock = socket.socket();socketProperties.setProperties(sock);NioChannel channel = null;if (nioChannels != null) {channel = nioChannels.pop();}if (channel == null) {SocketBufferHandler bufhandler = new SocketBufferHandler(socketProperties.getAppReadBufSize(),socketProperties.getAppWriteBufSize(),socketProperties.getDirectBuffer());//根据是否是HTTPS协议返回不同的channel的包装类if (isSSLEnabled()) {channel = new SecureNioChannel(socket, bufhandler, selectorPool, this);} else {channel = new NioChannel(socket, bufhandler);}} else {channel.setIOChannel(socket);channel.reset();}NioSocketWrapper socketWrapper = new NioSocketWrapper(channel, this);channel.setSocketWrapper(socketWrapper);socketWrapper.setReadTimeout(getConnectionTimeout());socketWrapper.setWriteTimeout(getConnectionTimeout());socketWrapper.setKeepAliveLeft(NioEndpoint.this.getMaxKeepAliveRequests());socketWrapper.setSecure(isSSLEnabled());poller.register(channel, socketWrapper);return true;} catch (Throwable t) {ExceptionUtils.handleThrowable(t);try {log.error(sm.getString("endpoint.socketOptionsError"), t);} catch (Throwable tt) {ExceptionUtils.handleThrowable(tt);}}// Tell to close the socketreturn false;}
    }
    
Poller和PollerEvent
  1. public class Poller implements Runnable,而且每个poller都持有一个selector。那PollerEvent呢?这个类的run方法中进行socketChannel.register(selector,OP_XX)。Poller的run方法中调用events()方法,这个方法就是遍历一个元素的PollerEvent的队列,然后依次run,即依次register。这样每个poller上就注册了全部socketChannel,任何一个socketChannel发来请求的时候,poller都能通过select获得。根据之前JAVA NIO的那一节,select结束阻塞后,可以通过Set keys = selector.SelectedKeys获得selector上所有selectionkey,每个key都可以获得selector和channel。换而言之,poller可以通过select获得所有因为请求而触发的事件,而每个事件都对应着一个NioSocketChannel,这个channel可以读取请求的数据(因为持有SocketWrapper引用)。下一步就是交给SocketProcessor。
SocketProcessor和ConnectionHandler
  1. 这个小组件主要任务是吧NioSocket这个Channel的socket交给ConnectionHandler这个组件去处理。ConnectionHandler主要是解析http协议,并封装成request和response对象交给CoyoteAdapter。
小结
  • 9.0.21版本IO模型的特点为,acceptor和poller都是单线程的,早期版本acceptor是有数组的,9.x早期版本poller也是有数组的,但是9.0.21都换成了单线程,且不交给线程池运行,这两个线程是从EndPoint.startInternal开始就创建并运行的。Acceptor组件负责以阻塞的方式监听连接,监听到后交给NioEndpoint封装NioSocketChannel和SocketWrapper,poller负责创建PointEvent实例(NioSocketChannel是核心)并加入events这个队列中。P.S.虽然PointEvent也是Runnable的实现类,但是他从来没有当做线程执行过,而是在poller的select前,遍历events并全部run一遍来保证注册所有事件。select后,遍历所有的SelectionKey,然后找到对应的NioSocketChannel,并取出socket,交给SocketProcessor。SocketProcessor先尝试从缓存中拿,缓存中不够的话会new一个,缓存的目的是为了避免反复的生成对象和GC,然后交给线程池去执行。这个线程池的阻塞队列是TaskQueue extends LinkedBlockingQueue,核心池为10,最大池为200。但是值得注意的是,虽然acceptor是单线程的,但是一个service有多个connector,因此用于接收客户端连接还是有多个NIO线程,不同的connector的SocketProcessor应该是交给不同线程池去运行,因为无论是executor还是acceptor都是属于某一个connector组件的。
  • socketprocessor交给connectionHandler的process方法,处理socket。代码中是socketprocessor的run方法中调用doRun方法,doRun方法中先getHandler,然后connectionHandler.process()->AbstractProcessorLight.process(socketWrapper)->Http11Processor.service()
  • reactor模式:事件驱动,有多个并发请求,有一个(或多个)Selector响应请求,并分发给其他线程处理。

Container

  1. 概览


  2. 书接上回,SocketProcessor.doRun()->ConnectionHandler.process()->Http11Processor.process()->Http11Processor.service(socketWrapper)->Adapter.service(request,)。根据这个执行链,到了CoytoteAdapter,此时已经封装好了request和response,并且作为参数传到了adapter的service中。这里的request和response还不是servlet中的,但是有转换关系,具体来说,adapter中解析出来的request和response比servlet中的更强

  3. PipeLine之间使用valve连接,valve之间又通过单链表的形式组织而成。

    PipeLine之间就是职责链,通过父子节点的方式连接。这里有个疑问,父节点如何找到自己的子节点呢?Tomcat的解决方案是,Engine与Host之间被一个Valve连接,这个Valve用于指明下一个子节点是谁,Engine有StrandEngineVavle,关系为:

    上图所有的Value都拼错了,应该是Valve。这些Valve可以用户自己写,然后配置在xml中即可,相当于起到了拦截器的作用。Valve的组织形式是单链表。

    (1)每个Pipeline都有特定的Valve,而且是在管道的最后一个执行,这个Valve叫做BasicValve,BasicValve是不可删除的;(2)在上层容器的管道的BasicValve中会调用下层容器的管道。

  4. 每个Valve的作用:

    • StandardEngineValve:传递到Host容器中
    • AccessLogValve:Host容器1号阀门,用于记录日志
    • ErrorReportValve:Host容器2号阀门用于记录异常并封装到response中
    • StandardHostValve:从request中找到映射的context容器(说明1个host有多个context),更新session的访问时间
    • StandardContextValve:禁止对WEB-INF/META-INF目录下资源的重定向,从request中获取Wrapper(Mapper组件将request映射到正确的servlet)
    • StandardServerWrapperValve:调用StandardWrapper的loadServlet方法生成servlet,调用ApplicationFilterFactory生成filter链条。
  5. 在Wrapper组件中,每个wrapper对应这样一个servlet和一条filterChain。每个请求会通过Mapper组件映射到一个Wrapper中,然后这个请求进入StandardWrapperValve中,先用filterChain进行处理,然后执行service方法。

Mapper

  1. Mapper组件由service管理,具有MappedHost,MappedContext,MappedWrapper。我们熟知的servlet的映射匹配方式,比如精确匹配,通配符匹配,扩展名匹配就是通过wrapperContext中不同的数组实现的。
  2. CoyoteAdapter中,之前说到用service方法处理request和response,在9.0.21版本中,service()方法会调用postParseRequest()方法,在此方法中,connector.getService().getMapper().map(serverName, decodedURI, version, request.getMappingData()); 来找到对应的host,context和wrapper,用于之后BasicValve向下一层容器派发,这些都是保存在request中的。

Redis的IO模型

  1. Redis中两类事件,一是文件事件比如客户端发送get请求,二是时间事件,服务器定时或周期性的执行,比如RDB持久化。后一个事件通过fork一个IO线程去做,前一个事件通过IO多路复用机制,将事件派发给不同的handler处理。
  2. MySQL的IO模型还是阻塞的,用的是线程池模型,一个连接一个线程,因为mysql的瓶颈主要在与硬盘的IO交互。
  3. 为什么MySQL之类的数据库不采用NIO呢?简单来说就是因为现在DB的线程池技术成熟了(JDBC就是BIO的),如果再加入NIO就会使得代码特别复杂,但是收益不大。

Socket与IO复习相关推荐

  1. Socket重叠IO

    1.为什么到现在才弄懂这个 不知道这个Socket重叠IO这种模型是不是socket IO完成端口的基础,不过我感觉,学习一下这个再去学习socket IO完成端口是比较有好处的. 这个Scoket重 ...

  2. java 大文件上传 断点续传(Socket、IO流)

    java两台服务器之间,大文件上传(续传),采用了Socket通信机制以及JavaIO流两个技术点,具体思路如下: 实现思路: 1.服:利用ServerSocket搭建服务器,开启相应端口,进行长连接 ...

  3. php socket select IO复用

    此篇博客是接着上篇php socekt阻塞模型PHP代码(php socket IO阻塞方式的Server/Client)的进阶,IO阻塞模型只能是同一个时刻只能由一个客户端进行访问,除非利用多进程或 ...

  4. SOCKET编程 IO与NIO

    socket是网络编程的基础,通过socket不同的计算机之间可以进行数据的交互. JAVA中IO操作是阻塞的,每个操作都要创建一个线程,容易对资源造成浪费.而NIO的出现解决了这一点,NIO可以通过 ...

  5. 第三章 文件IO复习

          open(const char * path, int flag.../*mode_t*/) #include <fcntl.h> path:绝对路径 flag:O_RDONL ...

  6. node mongoose_如何使用Express,Mongoose和Socket.io在Node.js中构建实时聊天应用程序

    node mongoose by Arun Mathew Kurian 通过阿伦·马修·库里安(Arun Mathew Kurian) 如何使用Express,Mongoose和Socket.io在N ...

  7. Socket.IO 客户端 API IO

    IO 创建方式 <script src="/socket.io/socket.io.js"></script> <script>const so ...

  8. 你知道socket.io中connect事件和connection事件的区别吗?

    server端的socket.io中有两个连接事件.一个是.on('connect'),一个是.on('connection'). 官网上没有对这两个事件的区别进行解释. 那么这两个事件有什么区别呢? ...

  9. java 网络io详解_Java网络socket编程详解

    或许有点长 但是一步步教你 我想你也愿意看7.2面向套接字编程 我们已经通过了解Socket的接口,知其所以然,下面我们就将通过具体的案例,来熟悉Socket的具体工作方式7.2.1使用套接字实现基于 ...

最新文章

  1. Microbiome:南京农大团队在粘细菌捕食的生态学功能方面取得重要进展
  2. VM8不能安装64位操作系统原因解析
  3. micropython解释器原理_了解一下 MicroPython 的项目整体架构
  4. iOS iOS9下实现app间的跳转
  5. float在python_如何在python中读取.float文件? - python
  6. Java关键字new和newInstance的区别
  7. 如何才能让项目团队高效稳定?
  8. linux mud 游戏,一笑天涯MUD游戏
  9. 软件测试 | 测试计划包含什么内容
  10. 使用WINPE制作U盘启动
  11. 什么是智能合约安全审计
  12. edge 打开PDF文件显示无法加载插件
  13. Axure 8 网页滚动效果+APP上下垂直拖动效果
  14. Word 2010 中的 VBA 入门
  15. 1309460-27-2,Ald-Ph-PEG4-acid苯甲醛基与酰肼和氨基氧基
  16. Qt在VS中的使用方法详解
  17. 五年北京,这个改变我命运的城市,终于要离开了(转)
  18. sql-lab 通关Less1 -65(深入学习)
  19. 白盒与黑盒测试什么区分
  20. python安装osgeo及shapefile库、is not a supported wheel on this platform 的问题

热门文章

  1. MybatisPlus--3.5 指定查询列
  2. 《微信小程序-进阶篇》package.json版本说明及各类版本符号详解(一)
  3. cad之画多边形的一些注意事项
  4. log里面汉字乱码问题
  5. 自我学习java心得
  6. 撸了个商城系统,已在Github上开源了,快来看看吧!
  7. 幻想FPGA人工智能的未来世界
  8. c#---三只小猪童话实现
  9. 怎么样可以搭建自己的腾讯云服务器
  10. Serializable接口的意义