精华内容
下载资源
问答
  • 本文使用jdk1.8.0_45和spring boot 2.1.4.RELEASE涉及源码都放在https://github.com/sabersword/Nio前因这周遇到一个连接断开的问题,便沿着这条线学习了一下Java NIO,顺便验证一下Tomcat作为spring boot默认的web...

    本文使用jdk1.8.0_45和spring boot 2.1.4.RELEASE

    涉及源码都放在https://github.com/sabersword/Nio

    前因

    这周遇到一个连接断开的问题,便沿着这条线学习了一下Java NIO,顺便验证一下Tomcat作为spring boot默认的web容器,是怎样管理空闲连接的。

    Java NIO(new IO/non-blocking IO)不同于BIO,BIO是堵塞型的,并且每一条学习路线的IO章节都会从BIO说起,因此大家非常熟悉。而NIO涉及Linux底层的select,poll,epoll等,要求对Linux的网络编程有扎实功底,反正我是没有搞清楚,在此推荐一篇通俗易懂的入门文章聊聊BIO,NIO和AIO

    此处先引用文章的结论:

    对于socket的文件描述符才有所谓BIO和NIO。

    多线程+BIO模式会带来大量的资源浪费,而NIO+IO多路复用可以解决这个问题。

    在Linux下,基于epoll的IO多路复用是解决这个问题的最佳方案;epoll相比select和poll有很大的性能优势和功能优势,适合实现高性能网络服务。

    底层的技术先交给大神们解决,我们着重从Java上层应用的角度了解一下。

    从JDK 1.5起使用epoll代替了传统的select/poll,极大提升了NIO的通信性能,因此下文提到Java NIO都是使用epoll的。

    Java NIO涉及到的三大核心部分Channel、Buffer、Selector,它们都十分复杂,单单其中一部分都能写成一篇文章,就不班门弄斧了。此处贴上一个自己学习NIO时设计的样例,功能是服务器发布服务,客户端连上服务器,客户端向服务器发送若干次请求,达到若干次答复后,服务器率先断开连接,随后客户端也断开连接。

    NIO服务器核心代码

    public void handleRead(SelectionKey key){

    SocketChannel sc = (SocketChannel) key.channel();

    ByteBuffer buf = (ByteBuffer) key.attachment();

    try {

    long bytesRead = sc.read(buf);

    StringBuffer sb = new StringBuffer();

    while (bytesRead > 0) {

    buf.flip();

    while (buf.hasRemaining()) {

    sb.append((char) buf.get());

    }

    buf.clear();

    bytesRead = sc.read(buf);

    }

    LOGGER.info("收到客户端的消息:{}", sb.toString());

    writeResponse(sc, sb.toString());

    if (sb.toString().contains("3")) {

    sc.close();

    }

    } catch (IOException e) {

    key.cancel();

    e.printStackTrace();

    LOGGER.info("疑似一个客户端断开连接");

    try {

    sc.close();

    } catch (IOException e1) {

    LOGGER.info("SocketChannel 关闭异常");

    }

    }

    }

    复制代码

    NIO客户端核心代码

    Iterator iter = selector.selectedKeys().iterator();

    while (iter.hasNext()) {

    SelectionKey key = iter.next();

    if (key.isConnectable()) {

    while (!socketChannel.finishConnect()) ;

    socketChannel.configureBlocking(false);

    socketChannel.register(key.selector(), SelectionKey.OP_READ, ByteBuffer.allocateDirect(1024));

    LOGGER.info("与服务器连接成功,使用本地端口{}", socketChannel.socket().getLocalPort());

    }

    if (key.isReadable()) {

    SocketChannel sc = (SocketChannel) key.channel();

    ByteBuffer buf = (ByteBuffer) key.attachment();

    long bytesRead;

    try {

    bytesRead = sc.read(buf);

    } catch (IOException e) {

    e.printStackTrace();

    LOGGER.info("远程服务器断开了与本机的连接,本机也进行断开");

    sc.close();

    continue;

    }

    while (bytesRead > 0) {

    buf.flip();

    while (buf.hasRemaining()) {

    System.out.print((char) buf.get());

    }

    System.out.println();

    buf.clear();

    bytesRead = sc.read(buf);

    }

    TimeUnit.SECONDS.sleep(2);

    String info = "I'm " + i++ + "-th information from client";

    buffer.clear();

    buffer.put(info.getBytes());

    buffer.flip();

    while (buffer.hasRemaining()) {

    sc.write(buffer);

    }

    }

    iter.remove();

    }

    复制代码

    服务器日志a2ad3fcb80a117fb9dccbbe50fa66c90.png

    客户端日志52120a580d55ba34c4becb40fa77b9c7.png

    从这个样例可以看到,客户端和服务器都能根据自身的策略,与对端断开连接,本例中是服务器首先断开连接,根据TCP协议,必然有一个时刻服务器处于FIN_WAIT_2状态,而客户端处于CLOSE_WAIT状态

    27bf56690d4e30493f7810a6c86b35d1.png

    我们通过netstat命令找出这个状态,果不其然。

    107f3d4b1d8c357e104a848190fe9e86.png

    但是JDK提供的NIO接口还是很复杂很难写的,要用好它就必须借助于Netty、Mina等第三方库的封装,这部分就先不写了。

    接下来考虑另外一个问题,在大并发的场景下,成千上万的客户端涌入与服务器连接,连接成功后不发送请求,浪费了服务器宝贵的资源,这时服务器该如何应对?

    答案当然是设计合适的连接池来管理这些宝贵的资源,为此我们选用Tomcat作为学习对象,了解一下它是如何管理空闲连接的。

    Tomcat的Connector组件用于管理连接,Tomcat8默认使用Http11NioProtocol,它有一个属性ConnectionTimeout,注释如下:

    /*

    * When Tomcat expects data from the client, this is the time Tomcat will

    * wait for that data to arrive before closing the connection.

    */

    复制代码

    可以简单理解成空闲超时时间,超时后Tomcat会主动关闭该连接来回收资源。

    我们将它修改为10秒,得到如下配置类,并将该spring boot应用打包成tomcat-server.jar

    @Component

    public class MyEmbeddedServletContainerFactory extends TomcatServletWebServerFactory{

    public WebServer getWebServer(ServletContextInitializer... initializers){

    // 设置端口

    this.setPort(8080);

    return super.getWebServer(initializers);

    }

    protected void customizeConnector(Connector connector){

    super.customizeConnector(connector);

    Http11NioProtocol protocol = (Http11NioProtocol) connector.getProtocolHandler();

    // 设置最大连接数

    protocol.setMaxConnections(2000);

    // 设置最大线程数

    protocol.setMaxThreads(2000);

    // 设置连接空闲超时

    protocol.setConnectionTimeout(10 * 1000);

    }

    }

    复制代码

    我们将上文的NIO客户端略微修改一下形成TomcatClient,功能就是连上服务器后什么都不做。

    Iterator iter = selector.selectedKeys().iterator();

    while (iter.hasNext()) {

    SelectionKey key = iter.next();

    if (key.isConnectable()) {

    while (!socketChannel.finishConnect()) ;

    socketChannel.configureBlocking(false);

    socketChannel.register(key.selector(), SelectionKey.OP_READ, ByteBuffer.allocateDirect(1024));

    LOGGER.info("与远程服务器连接成功,使用本地端口{}", socketChannel.socket().getLocalPort());

    }

    if (key.isReadable()) {

    SocketChannel sc = (SocketChannel) key.channel();

    ByteBuffer buf = (ByteBuffer) key.attachment();

    long readCount;

    readCount = sc.read(buf);

    while (readCount > 0) {

    buf.flip();

    while (buf.hasRemaining()) {

    System.out.print((char) buf.get());

    }

    System.out.println();

    buf.clear();

    readCount = sc.read(buf);

    }

    // 远程服务器断开连接后会不停触发OP_READ,并收到-1代表End-Of-Stream

    if (readCount == -1) {

    LOGGER.info("远程服务器断开了与本机的连接,本机也进行断开");

    sc.close();

    }

    }

    iter.remove();

    }

    复制代码

    分别运行服务器和客户端,可以看到客户端打印如下日志

    f8226a9fe3c88b1885353a9175e349c1.png

    30:27连上服务器,不进行任何请求,经过10秒后到30:37被服务器断开了连接。

    此时netstat会发现还有一个TIME_WAIT的连接

    e2e8cc4ee4b29822373589252764369a.png

    2a93bcb2342bf886d1e1560cbc1e50cf.png

    根据TCP协议主动断开方必须等待2MSL才能关闭连接,Linux默认的2MSL=60秒(顺带说一句网上很多资料说CentOS的/proc/sys/net/ipv4/tcp_fin_timeout能修改2MSL的时间,实际并没有效果,这个参数应该是被写进内核,必须重新编译内核才能修改2MSL)。持续观察netstat发现31:36的时候TIME_WAIT连接还在,到了31:38连接消失了,可以认为是31:37关闭连接,对比上文30:37刚好经过了2MSL(默认60秒)的时间。

    35f290096fc38cf64fbde7bad4ca53c7.png

    展开全文
  • NIO是同步非阻塞,NIO已经足够好了,Java为什么还要NIO.2呢? NIONIO.2最大的区别? 一个是同步一个是异步。而异步最大特点是,应用程序无需自己触发数据从内核空间到用户空间的拷贝。 为何是应用程序去“触发...

    NIO是同步非阻塞,NIO已经足够好了,Java为什么还要NIO.2呢?

    NIO和NIO.2最大的区别?

    一个是同步一个是异步。而异步最大特点是,应用程序无需自己触发数据从内核空间到用户空间的拷贝

    为何是应用程序去“触发”数据拷贝,而非直接从内核拷贝数据?

    应用程序无法访问内核空间,数据拷贝必须由内核负责,问题是谁来触发?

    • 内核主动将数据拷贝到用户空间并通知应用程序
    • 还是等待应用程序通过Selector来查询,当数据就绪后,应用程序再发起一个read调用,这时内核再把数据从内核空间拷贝到用户空间。

    数据从内核空间拷贝 =》 用户空间这段时间,应用程序还是阻塞的。所以异步效率高于同步,因为异步模式下应用程序始终不会被阻塞。

    • ServerSocket:用于在本机(Server端)开一个端口,被动的等待数据(用accept()方法),与 Client 端端建立连接后可以进行数据交换
    • Socket:用于连接远端机器(Server端)上的一个端口,主动发出数据,建立连接后也可以接收数据。

    网络数据读取在异步模式下的工作过程

    应用程序调用read API,同时告诉内核:

    • 数据准备好了后,拷贝到哪个Buffer
    • 调用哪个回调函数去处理这些数据

    之后,内核接到该read指令,等待网卡数据到达。
    数据到达后,产生硬件中断,内核在中断程序把数据从网卡拷贝到内核空间
    接着做TCP/IP协议层的数据解包和重组,
    再把数据拷贝到应用程序指定的Buffer,
    最后调用应用程序指定的回调函数。

    异步模式下,应用程序当了“需求甲方”,内核则忙前忙后,但最大限度提高了I/O通信效率。
    Linux内核2.6的AIO都提供了异步I/O的支持,但还不完善,详情可以看这里:http://lse.sourceforge.net/io/aio.html。
    Java的NIO.2 API是对os异步I/O API的封装,通过epoll实现的。

    Java NIO.2

    服务端程序

    为什么需要创建一个线程池?

    异步I/O模型下,应用程序不知道数据何时到达,因此向内核注册回调方法,当数据到达时,内核就会调用该回调方法。
    同时为提高处理速度,会提供一个线程池给内核使用,这样不会耽误内核线程工作,内核只需把工作交给线程池就立即返回了。

    回调类AcceptHandler

    它实现了CompletionHandler接口

    两个模板参数V和A,分别表示

    • I/O调用的返回值
      比如accept的返回值就是AsynchronousSocketChannel
    • 附件类
      附件类由用户自己决定。

    在accept的调用中,我们传入一个Nio2Server。因此AcceptHandler带有了两个模板参数:AsynchronousSocketChannel和Nio2Server。

    CompletionHandler有两个方法:completed和failed,分别在I/O操作成功和失败时调用。completed方法有两个参数,其实就是前面说的两个模板参数。也就是说,Java的NIO.2在调用回调方法时,会把返回值和附件类当作参数传给NIO.2的使用者。

    处理读的回调类ReadHandler

    public class ReadHandler implements CompletionHandler<Integer, ByteBuffer> {   
        // 读取到消息后的处理  
        @Override  
        public void completed(Integer result, ByteBuffer attachment) {  
            // attachment就是数据,调用flip操作,其实就是把读的位置移动最前面
            attachment.flip();  
            // 读取数据
            ... 
        }  
    
        void failed(Throwable exc, A attachment){
            ...
        }
    }
    

    read调用的返回值是一个整型,所以回调方法里:

    • 第一个参数是个整型
      表示有多少数据被读取到了Buffer中
    • 第二个参数是一个ByteBuffer
      因为调用read方法时,把用来存放数据的ByteBuffer当作附件类传进去了,所以在回调方法有ByteBuffer类型参数,直接从该ByteBuffer获取数据

    Nio2Endpoint

    Nio2Endpoint的组件

    总体工作流程类似NioEndpoint。

    Nio2Acceptor扩展Acceptor,用异步I/O接收连接,跑在一个单独线程,也是一个线程组。

    Nio2Acceptor接收新的连接后,得到一个AsynchronousSocketChannel,Nio2Acceptor把AsynchronousSocketChannel封装成一个Nio2SocketWrapper,并创建一个SocketProcessor任务类交给线程池处理,并且SocketProcessor持有Nio2SocketWrapper对象。

    Executor在执行SocketProcessor时,SocketProcessor的run方法会调用Http11Processor来处理请求,Http11Processor会通过Nio2SocketWrapper读取和解析请求数据,请求经过容器处理后,再把响应通过Nio2SocketWrapper写出。

    需要你注意Nio2Endpoint跟NioEndpoint的一个明显不同点是,Nio2Endpoint中没有Poller组件,也就是没有Selector。这是为什么呢?因为在异步I/O模式下,Selector的工作交给内核来做了。

    Nio2Endpoint各组件设计

    Nio2Acceptor

    和NioEndpint一样,Nio2Endpoint用LimitLatch控制连接数,但Nio2Acceptor监听连接的过程不是在一个死循环里不断地调accept,而是回调方法。

    连接监听方法:

    serverSock.accept(null, this);
    

    第二个参数this,表明Nio2Acceptor自己就是处理连接的回调类,因此Nio2Acceptor实现了CompletionHandler接口。

    @Override
    public void completed(AsynchronousSocketChannel socket,
            Void attachment) {
            
        if (isRunning() && !isPaused()) {
            if (getMaxConnections() == -1) {
                // 若无连接限制,则继续接收新连接
                serverSock.accept(null, this);
            } else {
                // 若有连接限制,就在线程池里执行run,run会检查连接数
                getExecutor().execute(this);
            }
            // 处理请求
            if (!setSocketOptions(socket)) {
                closeSocket(socket);
            }
        } 
    

    为什么要执行run方法?

    因为在run方法里会检查连接数,当连接达到最大数时,线程可能会被LimitLatch阻塞。

    为什么要放在线程池里跑?

    若放在当前线程里执行,completed方法可能被阻塞,导致该回调方法一直无法返回。
    接着completed方法会调用setSocketOptions方法,在这个方法里,会创建Nio2SocketWrapper和SocketProcessor,并交给线程池处理。

    Nio2SocketWrapper

    封装Channel,并提供接口给Http11Processor读写数据。

    Http11Processor无法阻塞等待数据的,按异步I/O模式,Http11Processor在调用Nio2SocketWrapper#read时需注册回调类,调用read后会立即返回。

    可若立即返回后Http11Processor还没有读到数据,怎么办?该请求的处理不就失败了?
    为解决这个问题,Http11Processor通过2次read调用完成数据读取操作:

    • 第一次read调用
      连接刚刚建立好后,Acceptor创建SocketProcessor任务类交给线程池去处理,Http11Processor在处理请求的过程中,会调用Nio2SocketWrapper#read发出第一次读请求,同时注册回调类readCompletionHandler,因为数据没读到,Http11Processor把当前的Nio2SocketWrapper标记为数据不完整。

      接着SocketProcessor线程被回收,Http11Processor并未阻塞等待数据。
      Http11Processor维护了一个Nio2SocketWrapper列表,也就是维护了连接的状态。
    • 第二次read调用
      当数据到达后,内核已经把数据拷贝到Http11Processor指定的Buffer里,同时回调类readCompletionHandler被调用,在这个回调处理方法里会重新创建一个新的SocketProcessor任务来继续处理这个连接,而这个新的SocketProcessor任务类持有原来那个Nio2SocketWrapper,这一次Http11Processor可以通过Nio2SocketWrapper读取数据了,因为数据已经到了应用层的Buffer。

    Nio2SocketWrapper#read会被调用两次,但不是串行调两次,而是Poller会先后创建两个SocketProcessor任务类,在两个线程中执行,执行过程中每次Http11Processor都会调Nio2SocketWrapper#read。

    public int read(boolean block, ByteBuffer to){
    
    //第二次调用时直接通过这个方法取数据
    int nRead = populateReadBuffer(to);
    
    ...
    
    //第一次时数据没取到,会调用下面这个方法去真正执行I/O操作并注册回调函数:
    nRead = fillReadBuffer(block);
    
    ...
    }
    

    两次read可以简单理解为,连接被保留着,数据没就绪处理的线程资源先释放了。收到异步数据就绪通知后,根据原有连接重建处理线程,继续处理。阻塞期间线程可复用。

    回调类readCompletionHandler

    Nio2SocketWrapper是作为附件类传递的,这样在回调函数里能拿到所有上下文。

    this.readCompletionHandler = new CompletionHandler<Integer, SocketWrapperBase<Nio2Channel>>() {
        public void completed(Integer nBytes, SocketWrapperBase<Nio2Channel> attachment) {
            ...
            // 通过附件类SocketWrapper拿到所有的上下文
            Nio2SocketWrapper.this.getEndpoint().processSocket(attachment, SocketEvent.OPEN_READ, false);
        }
    
        public void failed(Throwable exc, SocketWrapperBase<Nio2Channel> attachment) {
            ...
        }
    }
    

    总结

    在异步I/O模型里,内核做了很多事情,它把数据准备好,并拷贝到用户空间,再通知应用程序去处理,也就是调用应用程序注册的回调函数。Java在操作系统 异步IO API的基础上进行了封装,提供了Java NIO.2 API,而Tomcat的异步I/O模型就是基于Java NIO.2 实现的。

    由于NIO和NIO.2的API接口和使用方法完全不同,可以想象一个系统中如果已经支持同步I/O,要再支持异步I/O,改动是比较大的,很有可能不得不重新设计组件之间的接口。但是Tomcat通过充分的抽象,比如SocketWrapper对Channel的封装,再加上Http11Processor的两次read调用,巧妙地解决了这个问题,使得协议处理器Http11Processor和I/O通信处理器Endpoint之间的接口保持不变。

    FAQ

    • Tomcat里NIO为什么不参考netty,通过使用堆外内存来避免零拷贝问题?
      主要还是堆外内存管理起来没有JVM堆那么方便,为了稳定性的考虑吧,另外APR就是堆外内存的方案,也就是已经提供了这个选项。

    tomcat 在哪里配置 使用nioendpoint 还是nio2endpoint呢?
    server.xml中:

    <Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol"
    maxThreads="150" SSLEnabled="true">
    
    </Connector>
    

    参考

    • https://docs.spring.io/spring-boot/docs/current/reference/html/howto-embedded-web-servers.html
    • http://lse.sourceforge.net/io/aio.html
    展开全文
  • 笔者这几天被NIO、BIO搞的头昏脑胀的,不过最后还是记住了两幅图,在此围绕这两幅图,阐述一下Tomcat中的BIO模式和NIO模式的运行原理。 目录一、Tomcat中的BIO模式二、Tomcat中的NIO模式 一、Tomcat中的BIO模式 ...

    笔者这几天被NIO、BIO搞的头昏脑胀的,不过最后还是记住了两幅图,在此围绕这两幅图,阐述一下Tomcat中的BIO模式和NIO模式的运行原理。


    一、Tomcat中的BIO模式

    在这里插入图片描述

    Tomcat通过Acceptor接收到一个socket链接请求后,会将该请求封装成一个SocketProcessor连接线程,然后将它放入到连接池中。SocketProcessor负责从socket中阻塞读取数据,并且向socket中阻塞写入数据。每个SocketProcessor对应了一个Http11Processor负责解析请求数据。

    二、Tomcat中的NIO模式

    在这里插入图片描述

    Tomcat利用Acceptor来阻塞获取socket连接,NIO中叫socketChannel,Acceptor接收到socketChannel后,需要将socketChannel绑定到一个Selector中,并注册读事件,另外,基于NIO还需要一个线程来轮询Selector中是否存在就绪事件,如果存在就将就绪事件查出来,并处理该事件,在Tomcat中支持多个线程同时查询是否存在就绪事件,该线程对象为Poller,每个Poller中都包含一个Selector,这样每个Poller线程就负责轮询自己的Selector上就绪的事件,然后处理事件。处理事件就是把socketChannel和当前要做的事情(读或写)封装为SocketProcessor对象,然后将它放入连接池中,后续步骤和BIO类似。

    要注意的地方是:NIO中对请求行、请求头、请求体的读取是非阻塞的,而BIO是阻塞的。

    展开全文
  • Tomcat NIO模式源码解读

    2021-04-12 13:39:09
    Tomcat解读前提NIO概述引用总览Spring Boot内置tomcat源码NIO解读启动流程请求返回流程 前提 这里提及前提,那是因为如果没有前提知识,要读懂tomcat是有一定的困难,个人认为要解读tomcat源码之前,最好具备以下...


    做那么久web开发工程师,却一直没时间亲自研究tomcat中间件到底是个啥,网络传输怎么玩法,对于程序员的好奇心非常重要,针对于此,个人亲自研究一下tomcat NIO启动模式(tomcat9具有四种模式NIO、NIO2、BIO、Apr)

    前提

    这里提及前提,那是因为如果没有前提知识,要读懂tomcat是有一定的困难,个人认为要解读tomcat源码之前,最好具备以下前提知识(可自行选择阅读,若有时间均阅读更佳)

    • 三次握手和TCP原理 https://blog.csdn.net/lijin_12456/article/details/84887878
    • Linux下网络编程 https://blog.csdn.net/weixin_44895651/article/details/108163533
    • http协议 https://www.cnblogs.com/an-wen/p/11180076.html
    • 网络拓扑和安全 https://blog.csdn.net/soft_z1302/article/details/114678095
    • Java NIO Tutorial http://tutorials.jenkov.com/java-nio/index.html
    • TCP和NIO流程 https://blog.csdn.net/u011381576/article/details/79876754
    • java常见各种锁 https://www.jianshu.com/p/e6e794b64f80
    • tomcat简单配置和spring mvc以及过滤器拦截器的基本理念
    • 线程池多线程 https://blog.csdn.net/soft_z1302/article/details/110440449

    Non-blocking Server讲解

    为了增加阅读兴趣,个人来看一下Jakob Jenkov写的文章描述样例源码,Jakob Jenkov编写的源码服务器,由两个线程协同处理socket请求,一个接收socket,存放到队列,一个处理socket队列,并输出流(图片引用Jakob Jenkov描述
    在这里插入图片描述
    创建两个线程
    1.线程监听serversocketChannel,存放socketChannel到queue中
    2.线程不停轮询queue,若有则注册SelectionKey.OP_READ中,通过selectedKeys读取通信(信号读半消息,存放到list中,最终合并),
    然后注册到读代理队列,从读代理队列获取注册到写中,写入返回数据
    3.buffer缓存循环利用,每个线程占用一段byte【begin,end】,不相干扰。
    接下来执行一下代码(源码可自行下载,上面链接),查看请求报文和返回报文
    在com.jenkov.nioserver.Message#writePartialMessageToMessage方法中增加输出读取的缓存数据

     public void writePartialMessageToMessage(Message message, int endIndex) {
            int startIndexOfPartialMessage = message.offset + endIndex;
            int lengthOfPartialMessage = (message.offset + message.length) - endIndex;
    
            // 读取报文数据
            System.arraycopy(message.sharedArray, startIndexOfPartialMessage, this.sharedArray, this.offset, lengthOfPartialMessage);
            for (int i = message.offset; i < endIndex; i++) {
                System.out.print((char)message.sharedArray[i]);
            }
            System.out.println();
        }
    

    启动程序com.jenkov.nioserver.example.Main#main
    使用postman或者curl请求http://localhost:9999/ ,报文体随便填写,这里只是样例报文

    {
      "password": "string",
      "phone": "string"
    }
    

    查看查看请求结果
    在这里插入图片描述
    可以看到服务器能接受前端请求,同时查看返回数据易能解析报文。
    在这里插入图片描述
    综合上面可知,其实我们所有网络传输都只是使用了操作系统的tcp三次握手建立连接,即tcp_connect,传输符合http协议报文的二进制数据,最终按照http协议处理数据和返回数据,即网络通讯流程,其实Tomcat亦如此,不过tomcat设计比这个复杂一些,有以上理念,更加容易读懂源码

    Tomcat总览

    在这里插入图片描述

    我们来看一个简单tomcat配置文件server.xml如上图,由图看见,tomcat不是一个工具,是一个容器也是一个中间件。个人总结tomcat组成为

    1. Server全局服务,也就是服务器
    2. Listener 监听器,一序列全局监听器
    3. GlobalNamingResources全局配置文件
    4. Service对外提供服务,也就是我们web服务关注地方
    5. Connector连接,对于tomcat来说,每次请求过来一个socket处理连接,配置端口协议等
    6. Engine 主机,可配置域名访问等
    7. Host应用host,若没有,使用engine
    8. Context 应用,全局应用,一个context一个应用(源码解读得知
    9. Wrapper即包装的servlet,记录所有实例化的servlet(源码解读得知
    10. Pipeline(value),其中Engine、Host、Context 均具有通讯管道。(源码解读得知
    11. tomcat应用部署方式四种:war、文件夹、jar包、节点Context。不管是哪一种均需要servlet,遵循servlet协议。

    Spring Boot内置tomcat源码NIO解读

    启动流程

    个人比较懒,这里不详细编写图,大体流程如下(整个流程中Context传递bean工厂):
    初始化beanFactory-》onRefresh()-》createWebServer()-》Tomcat-》StandardServer-》StandardService-》Connector-》Http11NioProtocol-》TomcatWebServer tomcat.start()-》startInternal()->NioEndpoint#startInternal()->
    createExecutor()->PollerEvent->Poller->startAcceptorThread()

    请求返回流程

    在这里插入图片描述

    NIO有buffer、channel、selector组成。channel网络传输用ServerSocketChannel,调用accept()方法,获取SocketChannel,通过通过注册到selector监控数据到达,完成以及传输过程,因此tomcat设计也因此设计

    1. Acceptor监听网络传输
    2. 添加处理同步队列事件PollerEvent
    3. 监听同步队列事件若有只则注册到Channel到Selector中,操作方式为SelectionKey.OP_READ
    4. selector.selectedKeys()处理信号指令,拿到通讯socketChannel
    5. 拿到SocketProcessor,通过ThreadPoolExecutor执行SocketProcessor线程
    6. Http11Processor处理协议,处理Connector、Engine 、Host、Context 、Wrapper、Pipeline
    7. 处理完后执行完请求和返回,返回Http11OutputBuffer,执行输出流NioSocketWrapper#doWrite,终止请求request.finishRequest()和返回response.finishResponse()

    总结

    总而言之,学会tomcat,你将会明白计算机如何传输数据,网络如何通讯,以及web服务器如何处理请求数据,解析数据,包装请求设计等。
    1.了解tomcat只不过是针对网络传输协议http进行解析和包装而已。
    2.tomcat的NIO处理请求数据时,最终交给线程池去处理,这里可以优化tomcat线程池配置,至于线程池优化,可查阅java多线程-学习总结(完整版)

    server:
      tomcat:
        min-spare-threads: 16
        max-threads: 150
        max-connections: 200000
        accept-count: 128
        accesslog:
          enabled: true
          pattern: "%h %l %u %t %r %s %b %D"
    

    3.部署方式有四种,也就是最终都是tomcat的nioendpoint处理,即可以设置部署路径

    management:
      endpoints:
        web:
          base-path: /
    

    4.tomcat其实就是ServerSocketChannel使用Poller和worker进行双线程处理。
    5.可以自定义请求方法,自定义servlet,指定请求方法。
    6.你将会了解,spring boot的web应用,不仅仅是一个spring的封装而已,也做了大量tomcat和启动相关协议封装。此外,程序都是一个规范,规范的好坏要经过数年岁月的验证。

    笔记

    tomcat底层原理
    1.tomcat超强的容器也就是一个中间件
    2.四大组件servlet和context
    3.wrapper容器详解
    4.tomcat的bio和nio
    5.tomcat和socket的关系
    
    应用部署方式:war、文件夹、jar包、节点Context
    
    org.apache.catalina.mbeans.MBeanFactory#createStandardServiceEngine
    
    servlet容器
    org.apache.catalina.startup.Tomcat#getServer
    
    启动流程
    onRefresh()-》createWebServer()-》Tomcat-》StandardServer-》StandardService-》Connector-》
    Http11NioProtocol-》TomcatWebServer tomcat.start()-》startInternal()->NioEndpoint#startInternal()->
    createExecutor()->PollerEvent->Poller->startAcceptorThread()
    
    请求接受
    Acceptor<Thread> ->  NioEndpoint.serverSocketAccept()
    NioEndpoint#setSocketOptions 设置SocketChannel
    NioSocketWrapper#register(SelectionKey.OP_READ) 作为标志,超时使用
    Poller#wakeupCounter increment 增加记录
    selector.wakeup() 唤醒selector
    
    Poller#run#events()(loop检查队列,若有信号,则sc.register注册读管道到selector)
    Poller#events#eventCache.push(PollerEvent)
    NIO(selector.selectedKeys())
    
    Executor.execute(SocketProcessor)处理请求数据
    AbstractProtocol.ConnectionHandler#process
    coyote.AbstractProcessorLight#process
    
    coyote.http11.Http11Processor#service
    ==================
    setSocketWrapper(socketWrapper);  设置缓存大小8 * 1024开始 headerBufferSize,若超限,则扩容
    	SocketProperties.appReadBufSize和SocketProperties.appWriteBufSize
    protected final void setSocketWrapper(SocketWrapperBase<?> socketWrapper) {
            super.setSocketWrapper(socketWrapper);
            inputBuffer.init(socketWrapper);
            outputBuffer.init(socketWrapper);
    }
    Http11InputBuffer#parseRequestLine 处理请求行,读取数据,只操作头,包装request
    NioSocketWrapper#fillReadBuffer(boolean, java.nio.ByteBuffer) 读取全部数据
    prepareRequestProtocol() 设置请求响应信息
    prepareRequest() 解析主机域名等相关信息
    getAdapter().service(request, response) 调用servelt
    
    connector-》StandardService[Tomcat]-》StandardEngine[Tomcat]-》StandardPipeline[StandardEngine[Tomcat]]
    -》StandardEngineValve[StandardEngine[Tomcat]].invoke()-》StandardEngine[Tomcat].StandardHost[localhost]
    ->StandardPipeline[StandardEngine[Tomcat].StandardHost[localhost]]
    ->ErrorReportValve[StandardEngine[Tomcat].StandardHost[localhost]]->
    StandardHostValve.invoke()->Context->
    org.apache.catalina.core.StandardWrapperValve#invoke() -------------------------- tomcat调用servlet
    StandardWrapper[dispatcherServlet]
    DispatcherServlet(servlet)
    
    ApplicationFilterChain.doFilter().doFilterInternal() -------------------------- 处理过滤器开始
    OncePerRequestFilter
    CharacterEncodingFilter
    WebMvcMetricsFilter
    FormContentFilter
    RequestContextFilter
    WsFilter
    internalDoFilter(servlet.service(request, response))
    servlet
    controller
    输出结果
    org.apache.catalina.connector.OutputBuffer#close()关闭输出流
    org.apache.coyote.http11.Http11OutputBuffer.SocketOutputBuffer#end() socketWrapper.flush(true);
    org.apache.tomcat.util.net.SocketWrapperBase#doWrite(boolean)
    org.apache.tomcat.util.net.NioEndpoint.NioSocketWrapper#doWrite
     
    --------------------------处理过滤器结束
    
    ------------------
    
    ================
    
    
    
    Pipeline
    	List<Value> value;
    Engine
    	List<Host> hosts;
    Host
    	List<Context> contexts;
    
    Context
    	list<Wrapper> wrappers;
    Wrapper
    	List<Servlet> servlets
    
    讲解NIO
    http://tutorials.jenkov.com/java-nio/index.html
    TCP和NIO流程
    https://blog.csdn.net/u011381576/article/details/79876754
    
    创建两个线程
    1。线程监听serversocketChannel,存放socketChannel到queue中
    2。线程不停轮询queue,若有则注册SelectionKey.OP_READ中,通过selectedKeys读取通信(信号半读取,存放到list中,最终合并),
    然后注册到读代理队列,从读代理队列获取注册到写中,写入返回数据
    每个线程占用一段byte【begin,end】
    https://github.com/jjenkov/java-nio-server
    
    需要三次握手,tcp传输原理、https协议组成、操作系统流程、IO、NIO、java一些理念和数据结构
    

    参考文献

    【1】tomcat接受、分配连接(socket)解析
    【2】三次握手和TCP原理
    【3】Linux下网络编程
    【4】Java NIO Tutorial
    【5】TCP和NIO流程
    【6】java常见各种锁

    展开全文
  • 浅析Tomcat NIO 配置

    2021-03-14 19:36:46
    NIO (No-blocking I/O)从JDK 1.4起,NIO API作为一个基于缓冲区,并能提供非阻塞I/O操作的API被引入。作为开源web服务器的java实现,tomcat几乎就是web开发者开发、测试的***,有很多其他商业服务...
  • 使用命令行telnet和Mac上的一个tcp通信软件socket debugger,对tomcat8.5的nio模式做了源代码调试,解开了之前自己关于nio模式的一个疑惑:nio模式下tcp通信是非阻塞的,即客户端有信息发往tomcat,对该信息处理完毕...
  • TomcatNIO线程模型

    2021-04-22 18:18:16
    前言 关于Tomcat的线程模型,网上也介绍了很多了,我也不搬过来了(主要是因为懒) 至于原理嘛,我听一个老师这么说过,框架工具都...Tomcat有三种模型,BIO、NIO、APR,Tomcat8以上版本,默认使用的就是NIO模式,所以
  • 文将介绍 Tomcat 中的 NIO 使用,使大家对 Java NIO 的生产使用有更加直观的认识。虽然本文的源码篇幅也不短,但是 Tomcat 的源码毕竟不像 Doug Lea 的并发源码那么“变态”,对于大部分读者来说,阅读难度比之前...
  • System.out.println("bx nio tomcat 已经启动 监听的端口:"+this.port); f.channel().closeFuture().sync(); } catch (Exception e) { e.printStackTrace(); } finally { bossGroup.shutdownGracefully(); ...
  • 摘要: I/O复用模型,是同步非...TomcatNIO是基于I/O复用来实现的。对这点一定要清楚,不然我们的讨论就不在一个逻辑线上。下面这张图学习过I/O模型知识的一般都见过,出自《UNIX网络编程》,I/O模型一共有阻塞式I/O.
  • Tomcat NIO王新栋 作者王新栋。2014年加入京东,一直从事京麦平台的架构设计与开发工作,熟悉各种开源软件架构。在Web开发、架构优化上有较丰富实战经历。有多年NIO领域的设计、开发经验,对HTTP、TCP长连接技术有...
  • 文章目录 一、I/O复用模型解读 二、TOMCAT对IO模型的支持 三、TOMCATNIO的配置与使用 四、NioEndpoint组件关系图解读 六、NioEndpoint源码解读 6.1、初始化 6.2、步骤解读 6.3、NioBlockingSelector和BlockPoller...
  • java的NIO大行其道,其高效的处理机制利用了目前linux和Windows中的IO多路复用技术,目前已经在各个web服务器中,如tomcat,jetty作为主要通道提供服务,而很多的网络框架如Netty,Mina等也利用NIO的API进行编程;...
  • 转载 :https://blog.csdn.net/liufuwu1/article/details/54890834可以通过设置tomcat下conf文件夹的server.xml文件,对请求连接数和请求超时时间进行设置。connectionTimeout="20000"redirectPort="8443" ...
  • 一、BIO、NIO、AIO先了解四个概念:同步 : 自己亲自出马持银行卡到银行取钱(使用同步IO时,Java自己处理IO读写)。异步 : 委托一小弟拿银行卡到银行取钱,然后给你(使用异步IO时,Java将IO读写委托给OS处理,需要将...
  • TomcatNIO模型

    2021-07-23 10:03:19
    TomcatNIO模型 总结: 在BIO中,会发生两次阻塞: 第一次阻塞 connect调用:等待客户端的连接请求,如果没有客户端连接,服务端将一直阻塞等待 第二次阻塞 accept调用:客户端连接后,服务器会等待客户端发送数据...
  • 概述Tomcat8.0起已经默认nio模式,不需要做修改,BIO模式也已经抛弃了,今天主要介绍下tomcat的三种运行模式:BIO、NIO、ARP。简述及配置运行模式1、bio:没经过任何优化和处理,几百并发性能极低下。 配置server....
  • tomcat启动报错:java.lang.NoClassDefFoundError 解决办法:杀掉几个再重启tomcat,这次就拉起来了。
  • 基于官方文档,我们尝试在tomcat源码中寻找NIO/NIO2相关的类信息: 经过一番搜寻可以很明显的看到NIO相关的信息。channel,selector,pool经验告诉我们这些都不可能是业务处理的核心逻辑,剩下的就只有这个end
  • NIO优化原理和Tomcat线程模型 1、I/O阻塞 书上说BIO、NIO等都属于I/O模型,但是I/O模型这个范围有点含糊,我为此走了不少弯路。我们日常开发过程中涉及到NIO模型应用,如Tomcat、Netty中等线程模型,可以直接将其视...
  • 本文主要包括tomcat服务器的目录结构、工作模式、整体架构、I/O模型以及NIONIO2、APR三者的对比介绍。 1、Tomcat的目录结构 我们先来看一下tomcat8.5和tomcat9中的home目录中的文件: 可以看到除掉一些说明文件...
  • 最近在编写springboot项目,需要配置外部tomcat。然而在配置好后启动测试时总是出现 org.yaml.snakeyaml.error.YAMLException: java.nio.charset.MalformedInputException: Input length = 1 at org.yaml.snakeyaml....
  • 背景 11.11活动当...
  • 二、Tomcat使用的IO模式Tomcat 有三种 IO 模式,BIO、NIO、APR。在 Tomcat7 及以下 Linux 中默认启用的是 BIO 模式,Tomcat8 及以上使用的是 NIO 模式,利用 Java 的异步 IO 处理,可以通过少量的线程处理大量的连接...
  • tomcat BIO 、NIO 、AIO

    2021-12-07 06:34:43
    11.11活动当天,服务器负载过大,导致部分页面出现了不可访问的状态、那后来主管就要求调优了,下面是tomcat bio、nio、apr模式以及后来自己测试的一些性能结果。 原理方面的资料都是从网上找的,并且把多个地方的...
  • Tomcat NIO(19)-开启异步

    2021-01-09 08:00:00
    在上一篇文章中我们主要介绍 tomcat nio 中的服务端事件 SSE,在这里我们主要介绍 tomcat 中的异步。对于异步处理来说,主要包括异步的开启,异步的执行,异步的结束,异步的...
  • 在上一篇文章中我们主要介绍 tomcat nio 中异步的开启,主要包括以下的总结:开启异步的时候不会执行 start 事件函数的,因为此时构造的异步上下文对象的源码中并没有设置事件监听...
  • 不同的Tomcat版本支持的protocol不同,其中最典型的protocol包括BIO、NIO,NIO2和APR(Tomcat7中支持这3种,Tomcat8增加了对NIO2的支持,而到了Tomcat8.5和Tomcat9.0,则去掉了对BIO的支持)。 2、如何指定protocol ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 40,344
精华内容 16,137
关键字:

nio2tomcat