Tomcat中的输出buffer

以前一直没搞清楚tomcat输出的buffer的结构,在tomcat中有两对responserequest,其中一组是对http请求header的最基本的封装,另一组是对servlet规范中responserequest的实现,这组是调用servletservice方法的requestresponse。在这两对requestresponse都有一个内在的buffer,其中servlet规范那组是缓冲servlet中的输出的,缓冲response.getWriter().write()buffer,另外一组responserequestbuffer是缓冲httpheader的一些信息的。


上图就是整体tomcat的输出框架,coyoteResponsebuffer来控制httpheader的存储,这个header buffer是不可以扩充的,因为在配置时会设置它的默认大小默认8192字节,对于httpheader(无论是输出还是输入)都不可以超出这个大小。servletResponsebuffer存储servlet的输出,即http响应信息。这个buffer大小时可以扩充的,默认大小时8192字节。它的实现是byteChunk

     tomcat输出分两个部分一个是header的输出,另一个http消息体的输出,对于servletResponsebufferflush操作都是直接把byteChunk传入outputFilter,这些filter是关于chunked输出和gzip输出或者计算content-lengthfilter,在gzipfilter的时候,输出会buffer住一些,filter最后面连接的是socketoutputStream

     byteChunk的实现肯定是byte数组,tomcat在初始化的时候会设置默认8192大小的byte数组。在写数据的时候,判断byte数组的大小,如果输出数据小于byte数组剩余容量,直接把数据追加到byte数组中,如果byte数组不足以容纳输出数据,那么直接flush8192字节大小的数据,剩下的数据继续存在buffer中。

     看下buffer写数据的几种情况:
     1. buffer 可以容下输出数据得时候:

buffer flush一次可以容下输出数据:

3.buffer flush多次可以容下数据:

   看下byteChunkwrite 数据的代码:

    public void append( byte src[], int off, int len )
            throws IOException
        {
            // will grow, up to limit
            makeSpace( len );

            // if we don't have limit: makeSpace can grow as it wants
            if( limit < 0 ) { 
            //limit是byte数组的最大值,如果小于0,代表limit无限大,直接copy src数据到byteChunk的byte数组里面就可以了
                // assert: makeSpace made enough space
                System.arraycopy( src, off, buff, end, len );
                end+=len;
                return;
            }

            // Optimize on a common case.
            // If the buffer is empty and the source is going to fill up all the
            // space in buffer, may as well write it directly to the output,
            // and avoid an extra copy
            //这里面是个优化方法,如果当时这个buffer是空的,而且src的数据大小恰好等于limit,那么就不用再次copy src数组到byte数组了,直接flush就可以了
            if ( optimizedWrite && len == limit && end == start && out != null ) {
                out.realWriteBytes( src, off, len );
                return;
            }
            //如果byte数组足够放下src数组,直接copy
            // if we have limit and we're below
            if( len <= limit - end ) {
                // makeSpace will grow the buffer to the limit,
                // so we have space
                System.arraycopy( src, off, buff, end, len );
                end+=len;
                return;
            }

            // need more space than we can afford, need to flush
            // buffer

            // the buffer is already at ( or bigger than ) limit

            // We chunk the data into slices fitting in the buffer limit, although
            // if the data is written directly if it doesn't fit
           
            int avail=limit-end;
            //先把byte数组填满,flush一次
            System.arraycopy(src, off, buff, end, avail);
            end += avail;

            flushBuffer();

            int remain = len - avail;
            //再看下剩余数据是否能够填满8192,只要能填满,直接flush
            while (remain > (limit - end)) {
                out.realWriteBytes( src, (off + len) - remain, limit - end );
                remain = remain - (limit - end);
            }
            //最后剩下一些不够8192了,放在byte数组中,等待flush操作或者等待再次写数据
            System.arraycopy(src, (off + len) - remain, buff, end, remain);
            end += remain;

        }

 上面说了 tomcat 中存储 servlet 输出的 buffer ,这个 buffer byteChunk ,写操作和 flush 操作都可以激发 byteChunkflush 数据到 outputFilter 中或者直接到网络中。但是这里面有问题,如果数据 flush 到网络,如果这个数据前面不加上 header 的数据,是不对的,因为 response 数据前面肯定是 httpheader 数据,如果直接将 byteChunk 数据 flush 到网络,就不是 http 响应数据了,对于客户端程序肯定会出错。所以 byteChunkflush 数据肯定要通知 coyoteResponse 准备 header 数据,然后在 flushbyteChunk 之前,把 header 数据全部 flush 到网络中。

    看下tomcatflush数据到网络的时机有这么几个:

    1.servletservice方法中,write了数据,但是这个数据比byteChunk buffer8192默认)大,这个时候byteChunk就开始write一部分数据到客户端,并且在这个数据之前先write全部header数据。注意这个header数据是确定全部write到客户端,但是对于write信息体数据,如果在没有outputfilter的情况下,这些数据肯定会全部输出到客户端,但是如果有gzipfilter,那么这个write操作并不能保证所有数据都gzip完成后输出到客户端,上面这个操作并不能flushgzip中的数据。所以说这种被动的flush数据,并不能保证flush的消息体数据全部到达客户端。

    从触发的情况来说,servlet也并没有主动flush,也就说servlet也并不想flush出去的数据全部让客户端收到,只是说responsebuffer不够了,腾出一部分空间就行了,其他的不管了。

    byteChunkflush数据的时候会调用coyoteResponsedoWrite方法:


    public int doWrite(ByteChunk chunk, Response res) //chunk就是需要flush的数据
            throws IOException {

            if (!committed) {

                // Send the connector a request for commit. The connector should
                // then validate the headers, send them (using sendHeaders) and
                // set the filters accordingly.
                response.action(ActionCode.COMMIT, null); //这个地方就是先flush header数据

            }

            if (lastActiveFilter == -1)
                return outputStreamOutputBuffer.doWrite(chunk, res);       //flush数据到网络中
            else
                return activeFilters[lastActiveFilter].doWrite(chunk, res);//或者把数据写入filter中,注意这个地方不会flush这些filter。
        }


 这个 commit 操作很重要,后面 commitheader 数据到网络都是这一套逻辑,首先要把 add header 生成 bytestream ,然后 flush 出去。


    else if (actionCode == ActionCode.COMMIT) {
                // Commit current response

                if (response.isCommitted()) { 
                //记录这个header数据是否flush出去了,注意,如果之前flush了一次,那就证明了header已经全部flush出去了,后面在有header数据也不flush了,因为之前
    flush header之后,已经flush 响应数据了,再flush header就会出错(不是http响应了)
                    return;
                }

                // Validate and write response headers
                try {
                    prepareResponse();           //先生成header数据,然后放到coyoteResponse的buffer中
                    getOutputBuffer().commit(); //把上面buffer中生成的数据commit出去
                } catch (IOException e) {
                    // Set error flag
                    error = true;
                }

     看下commit函数的代码,写的很清楚

    protected void commit()
            throws IOException {

            // The response is now committed
            committed = true;          //设置header已经commit了,后面就不会再commit这个http header数据了
            response.setCommitted(true);

            if (pos > 0) {
                // Sending the response header buffer
                if (useSocketBuffer) {
                    socketBuffer.append(buf, 0, pos);
                } else {
                    outputStream.write(buf, 0, pos); //不管那种方式,header反正是直接flush到网络中了
                }
            }

        }

     2.当在service方法中调用了flush类方法。这种方法如果之后在addHeader是不管用的,因为flush数据的时候已经先flush header完了,已经开始flush数据阶段,这种情况是servlet主动flush,这个操作是必须保证buffer中的数据一定会write到客户端的。
 

    protected void doFlush(boolean realFlush)
            throws IOException {

            if (suspended) {
                return;
            }

            try {
                doFlush = true;
                if (initial) {
                    coyoteResponse.sendHeaders(); //先flush header
                    initial = false; //flush一次header就够了
                }
                if (cb.getLength() > 0) { //flush charChunk
                    cb.flushBuffer();
                }
                if (bb.getLength() > 0) { //flush byteChunk
                    bb.flushBuffer();
                }
            } finally {
                doFlush = false;
            }
           //这个就是被动flush和主动flush的区别,realFlush就是主动flush,这个保证了那个gzip filter flush全部数据
            if (realFlush) {
                coyoteResponse.action(ActionCode.CLIENT_FLUSH,
                                      coyoteResponse);
                // If some exception occurred earlier, or if some IOE occurred
                // here, notify the servlet with an IOE
                if (coyoteResponse.isExceptionPresent()) {
                    throw new ClientAbortException
                        (coyoteResponse.getErrorException());
                }
            }

        }

    这个 CLIENT_FLUSH主要处理逻辑如下:

    for (int i = 0; i <= lastActiveFilter; i++) {
                if (activeFilters[i] instanceof GzipOutputFilter) { //直接flush Gzip的filter数据
                    if (log.isDebugEnabled()) {
                        log.debug("Flushing the gzip filter at position " + i +
                                " of the filter chain...");
                    }
                    ((GzipOutputFilter) activeFilters[i]).flush();
                    break;
                }
            }

     从这个主动flush数据看来,servlet是愿意flush出全部数据的,所以这个过程比被动flush多了一个 flush gzip filter的过程。

    3.在coyoteAdapter的处理完service方法之后,最后会flush全部数据到客户端,close那些stream,回收request,response,重新设置状态等等,我们关心的就是flush数据的过程。前面的逻辑过程和时机1的代码基本类似,都是先flush http header数据,然后在flush http entity数据到outputFilter 中,但是这个最后还会调用endRequest函数,这个函数就是flush 那些outputFilter的。

    public void endRequest()
            throws IOException {

            if (!committed) {

                // Send the connector a request for commit. The connector should
                // then validate the headers, send them (using sendHeader) and
                // set the filters accordingly.
                response.action(ActionCode.COMMIT, null);

            }

            if (finished)
                return;

            if (lastActiveFilter != -1)
                activeFilters[lastActiveFilter].end(); //flush output filter
            finished = true;
        }

     这个end函数就是关闭filter中打开的stream,然后重置buffer等等,列举一下GzipOutputFilter的end函数代码:

    public long end()
            throws IOException {
            if (compressionStream == null) {
                compressionStream = new FlushableGZIPOutputStream(fakeOutputStream);
            }
            compressionStream.finish();
            compressionStream.close();
            return ((OutputFilter) buffer).end();
        }


总结:

       tomcat中存在两个buffer一个是缓冲httpheader的,另一个是缓冲httpentity的,当httpheader满的时候,就会报错,当httpentitybuffer满的时候就会flush数据到客户端,这个flush数据之前只会flush一次的全部的http headerflush数据的时机会有3个,第一个是entityheader满的时候被动flush数据到客户端,flush的数据不保证全部到客户端,因为有GzipoutputFilter的存在,会缓冲一些数据。第二个时机是servlet主动flush数据,这个过程除了flushhttp headerentity数据,还会把GzipoutputFilter的缓冲数据flush出去。第三个就是在servlet service方法完成后,也会flush全部数据,并且关闭buffer中的stream还有重置那些buffer的状态。



  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
最近学习Nginx+tomcat实现 负载均衡。 首先大家注意: 本文章没有session共享,关于session共享我会在下一篇讲解,先实现Nginx+tomcat负载均衡再实现session共享。 从网上查了好多资料,多走了很多弯路,现在把自己成功的方法拿出来与大家分享。 Window7 我是在Win7上做的。不是什么Linux,网上好多资料,特别麻烦。 Nginx Nginx 比较好找到,直接去网上下载 网址: http://nginx.org/en/download.html 版本不作要求了,(比如1.2.9版本),都有。 JDK JAVA的各种环境都要有。 版本不要求 Tomcat 这里我给大家提供tomcat6 Tomcat各种版本的下载地址我也提供给大家:http://tomcat.apache.org/download-60.cgi 大家可以先用我tomcat6 学会了,在下载自己需要的版本。 词条科普 另外我把实现过程遇到的知识点都总结好了,一起提供给大家学习。 步骤: 注:本例程以一台win7机器为例子,即同一台机器上装一个nginx和2个Tomcat。 且安装了JDK。 便于管理将用到的资料放在一个文件夹下 我在D盘 创建 server 文件夹 . 1. Nginx 下载直接解压缩到server,点nginx.exe 执行 安装后如果可用,可在任务管理其找到如图类似,并且在浏览器输入 http://localhost/ 浏览器显示如下两个图 说明成功 2.Tomcat 同样将自己下载的或者我提供的tomcat 放到D盘的server下不过要复制成两份或者多份。 命名如:(便于区别 我们只用两个来讲解 ,多个tomcat和两个原理是一样的) 1、server.xml配置 我们需要在一台机器上跑 2 个不同的 tomcat ,避免出现端口被占用的情况,为了规范统一,我们修改全部tomca端口。分别找到tomcat6的1和2 的conf下的 server.xml。 修改Server端口 找到Server将: 改为 XXXX 在这里表示不同的端口:我的两个 tomcat 分别使用 8005和8006; 2.1.2、修改Connector端口 找到Connector将: 改为 XXXX 在这里表示不同的端口:我的两个 tomcat 分别使用 8081和8082; 2.1.3、修改Engine端口 找到Engine将: 改为 tomcatX 在这里表示不同的tomcat,我的两个 tomcat 分别使用 tomcat1和tomcat2;来区分。 这个设置是主要用以tomcat的集群。 如果看不懂可以去看我提供的tomcat我已经改好了。 启动tomcat服务 分别到两个tomcat下,直接双击D:\server\apache-tomcat-6.0.39_1\bin\startup.bat启动tomcat1 D:\server\apache-tomcat-6.0.39_2\bin\startup.bat启动tomcat2 出现以下页面表示启动成功 在浏览器输入 http://localhost:8081 http://localhost:8082 出现 标示成功 3、Nginx+Tomcat负载均衡配置 首先创建两个文件,这两个文件 我来提供,将这两个文件拷入Nginx的conf文件夹下 1.proxy.conf 文件内容 #负责代理转发 proxy_redirect off; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值