Chinaunix首页 | 论坛 | 博客
  • 博客访问: 712148
  • 博文数量: 31
  • 博客积分: 330
  • 博客等级: 一等列兵
  • 技术积分: 3004
  • 用 户 组: 普通用户
  • 注册时间: 2012-09-05 22:38
个人简介

java开发工程师,专注于内核源码,算法,数据结构。 qq:630501400

文章分类
文章存档

2014年(2)

2013年(22)

2012年(7)

分类: Java

2013-07-19 15:32:22

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

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

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

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

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

  
  2.buffer flush一次可以容下输出数据:
 
    3.buffer flush多次可以容下数据:

     看下byteChunk write 数据的代码:

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

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

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

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

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

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

  42.         flushBuffer();

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

  52.     }

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

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

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

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

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

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

  3.         if (!committed) {

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

  8.         }

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

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

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

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

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

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


  1. protected void commit()
  2.         throws IOException {

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

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

  14.     }

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

  1. protected void doFlush(boolean realFlush)
  2.         throws IOException {

  3.         if (suspended) {
  4.             return;
  5.         }

  6.         try {
  7.             doFlush = true;
  8.             if (initial) {
  9.                 coyoteResponse.sendHeaders(); //先flush header
  10.                 initial = false; //flush一次header就够了
  11.             }
  12.             if (cb.getLength() > 0) { //flush charChunk
  13.                 cb.flushBuffer();
  14.             }
  15.             if (bb.getLength() > 0) { //flush byteChunk
  16.                 bb.flushBuffer();
  17.             }
  18.         } finally {
  19.             doFlush = false;
  20.         }
  21.        //这个就是被动flush和主动flush的区别,realFlush就是主动flush,这个保证了那个gzip filter flush全部数据
  22.         if (realFlush) {
  23.             coyoteResponse.action(ActionCode.CLIENT_FLUSH,
  24.                                   coyoteResponse);
  25.             // If some exception occurred earlier, or if some IOE occurred
  26.             // here, notify the servlet with an IOE
  27.             if (coyoteResponse.isExceptionPresent()) {
  28.                 throw new ClientAbortException
  29.                     (coyoteResponse.getErrorException());
  30.             }
  31.         }

  32.     }

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

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

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

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

  1. public void endRequest()
  2.         throws IOException {

  3.         if (!committed) {

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

  8.         }

  9.         if (finished)
  10.             return;

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

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

  1. public long end()
  2.         throws IOException {
  3.         if (compressionStream == null) {
  4.             compressionStream = new FlushableGZIPOutputStream(fakeOutputStream);
  5.         }
  6.         compressionStream.finish();
  7.         compressionStream.close();
  8.         return ((OutputFilter) buffer).end();
  9.     }

总结:

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



阅读(7905) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~