我们在用Tomcat响应数据给客户端的时候,一般会调用如下代码
OutputStreamoutputStream=resp.getOutputStream(); outputStream.write("test".getBytes());
Tomcat在响应客户端或者接受客户端消息的时候会用到门面模式,所以响应的时候Response的类型其实是ResponseFacade(接收的时候是RequestFacade),得到的socket.getOutputStream()得到的类型是CoyoteOutputStream类型(接受端也是,Coyote是核心类)。CoyoteOutPutStream中有一个属性ob(OutputBuffer),write方法调用的就是ob.writeBytes(),底层代码如下
private void writeBytes(byte b[], int off, int len) throws IOException { if (closed) { return; } append(b, off, len); bytesWritten += len; if (doFlush) { //每次把缓冲区的数据发送出去 flushByteBuffer(); } }
OutputBuffer中有个属性是bb(ByteChunk类型),里面有一个字节数组buff(8092kb),每次往缓冲区里添加数据的时候,就是往buff中写数据。代码中可以看到必须要满足doFlush的时候才会清空缓冲区,doFlush基本上就是在Servlet代码执行完前开发手动的调用OutputStream.flush()方法就会把它设置成true。这里就需要思考,什么时候会将buff中的数据放到socket中?
ByteChunk有一个属性out(ByteOutPutChannel),本质上就是一个管道,实现了数据流向到哪里,里面有一个realWriteBytes方法。当我们调用的时候就会把数据往下游(可以先理解为往socket发,实际是往socketbuff中发,第二层缓存)发送。
public void realWriteBytes(ByteBuffer buf) throws IOException { outputChunk.setBytes(buf,off,cnt); coyoteResponse.doWrite(buf); }
这里面主要就是通过一个outputChunk来标记这些数据是要传输下去的,真正实现发送功能的就是coyoteResponse.dowrite。
ByteChunk里面有append方法,来表示往buff中添加数据,当缓冲区大小满了之后,也会触发数据清空操作。还有一种情况就是上述说的,在缓冲区没满的时候,开发手动调用flush方法,也会执行数据清空下发操作。
当我们调用outputStream.flush()方法的时候
1.判断是否生成过响应头,有的话就不生了,没有的话说明是第一次发送,那么生成响应头。
2.因为调用了flush方法,所以会调用ByteChunk的flushBuffer方法,将buff中的数据发出去,然后清空
3.2中的数据其实是发送给另一个缓存(socketBuffer,但是这个缓存你是可以不用的,通过一些设置),SocketBuffer中也有一个ByteChunk,数据也是存放在这里面的,所以最后做的是将SocketBuffer中的数据发送到socket
有一个属性useSocketBuffer,默认是false(如果要用必须你将socketBuffer设置大小超过500才会启用,这个可以自己去配置)
我们最后调用的是coyoteResponse.doWrite方法,这里往下推调用的是outPutBuffer.doWrite方法(执行AbstractOutPutBuffer的逻辑)
1.doWrite方法会先判断响应头是否已经发送,如果还没有,那么会先构造响应头然后发给socketBuffer
2.发送完响应头,就会去判断你的数据到底是用Chunk还是ContentLength发送,对应着三个Filter(ContentLength->IdentityFilter,分块响应体->ChunkedFilter,不发送->VoidFilter)
所以需要确定的是,到底是执行哪个Filter的逻辑,Tomcat的选择逻辑如下:
Servlet中的所有代码执行完之后,就会调用response.finishResponse()->OutputBuffer.close()。close方法中会去判断响应体是否已经发送过了,如果一直没有发送过,说明缓冲区肯定是没满的,并且玩家没有执行过flush,那么第一缓冲区中的数据就是我们这次要发送的所有数据,所以缓冲区中的数据大小就是我们要的ContentLength大小,如果说在调用close之前已经发送过数据了,那么就会调用ChunkedFIlter的逻辑,比如我们自己调用flush,或者缓冲区满了,就会执行这个逻辑。
在执行close方法的时候,会先将响应头的数据发送到socketBuffer,然后再将响应体的数据通过对应的OutputFilter发送,最后会调用OutputFilter的end方法,分块发送的时候会循环掉(因为有很多块),IdentityFilter就会一次性全部发送出去。
上文就是Tomcat响应数据的过程