在上一篇文章里我们主要介绍了 tomcat 中请求数据的读取,这里主要介绍对于响应数据的写入。
响应数据写入的流程
上图中的 CoyoteOutputStream 实例对象就是 ServletOutputStream 的实现,我们平时调用 servlet API 向 OutputStream 中写数据的时候就是走的这个调用图。
最终调用了 NioSocketWrapper 的 doWrite() 方法,对于第一个参数一般为true 。该值由 isBlocking() 方法决定,因为一般情况下不会设置 writeListener ,所以返回 ture ,源码如下:
//Http11OutputBuffer protected final boolean isBlocking() { return response.getWriteListener() == null; }
对于 NioSocketWrapper 的 doWrite() 方法核心代码如下:
protected void doWrite(boolean block, ByteBuffer from) throws IOException { NioChannel socket = getSocket(); if (socket instanceof ClosedNioChannel) { throw new ClosedChannelException(); } if (block) { long writeTimeout = getWriteTimeout(); Selector selector = null; try { selector = pool.get(); } catch (IOException x) { // Ignore } try { pool.write(from, socket, selector, writeTimeout); if (block) { // Make sure we are flushed do { if (socket.flush(true, selector, writeTimeout)) { break; } } while (true); } } finally { if (selector != null) { pool.put(selector); } } // If there is data left in the buffer the socket will be registered for // write further up the stack. This is to ensure the socket is only // registered for write once as both container and user code can trigger // write registration. } else { if (socket.write(from) == -1) { throw new EOFException(); } } updateLastWrite(); }
对于请求体的读取采用阻塞的方式,调用以前文章介绍的 NioSelectorPool 的 write(ByteBuffer buf, NioChannel socket, Selector selector, long writeTimeout) 方法。在该方法中又会调用 NioBlockingSelector 的 write() 方法,核心代码如下:
public int write(ByteBuffer buf, NioChannel socket, long writeTimeout) throws IOException { SelectionKey key = socket.getIOChannel().keyFor(socket.getSocketWrapper().getPoller().getSelector()); if (key == null) { throw new IOException(sm.getString("nioBlockingSelector.keyNotRegistered")); } KeyReference reference = keyReferenceStack.pop(); if (reference == null) { reference = new KeyReference(); } NioSocketWrapper att = (NioSocketWrapper) key.attachment(); int written = 0; boolean timedout = false; int keycount = 1; //assume we can write long time = System.currentTimeMillis(); //start the timeout timer try { while (!timedout && buf.hasRemaining()) { if (keycount > 0) { //only write if we were registered for a write int cnt = socket.write(buf); //write the data if (cnt == -1) { throw new EOFException(); } written += cnt; if (cnt > 0) { time = System.currentTimeMillis(); //reset our timeout timer continue; //we successfully wrote, try again without a selector } } try { if (att.getWriteLatch() == null || att.getWriteLatch().getCount() == 0) { att.startWriteLatch(1); } poller.add(att, SelectionKey.OP_WRITE, reference); att.awaitWriteLatch(AbstractEndpoint.toTimeout(writeTimeout), TimeUnit.MILLISECONDS); } catch (InterruptedException ignore) { // Ignore } if (att.getWriteLatch() != null && att.getWriteLatch().getCount() > 0) { //we got interrupted, but we haven't received notification from the poller. keycount = 0; } else { //latch countdown has happened keycount = 1; att.resetWriteLatch(); } if (writeTimeout > 0 && (keycount == 0)) { timedout = (System.currentTimeMillis() - time) >= writeTimeout; } } if (timedout) { throw new SocketTimeoutException(); } } finally { poller.remove(att, SelectionKey.OP_WRITE); if (timedout && reference.key != null) { poller.cancelKey(reference.key); } reference.key = null; keyReferenceStack.push(reference); } return written; }
根据以上代码整个读数据逻辑在一个循环里进行,如果有数据写入就跳出循环,返回写入数据的长度。
如果数据不可写(例如写缓冲已满),则调用 BlockPoller 实例的 add() 方法,将封装的 OP_WRITE 事件添加到 BlockPoller 的事件队列里。关于 BlockPoller 我们在后面文章里详细讲解。
然后在调用之前文章介绍的 NioSocketWrapper 中的 CountDownLatch 类型 writeLatch 属性的 await() 方法,使当前线程(一般是tomcat io线程)在 writeLatch 上等待。
当前线程在 writeLatch 上等待一般有超时时间(写超时时间),默认不配置为 -1,这时超时时间为 Long.MAX_VALUE 毫秒。
响应数据写入的总结
响应数据的写入是阻塞的,如果发现数据不可写(例如写缓冲已满),那么首先注册封装的 OP_WRITE 事件到 BlockPoller 的事件队列里。然后会利用 NioSocketWrapper 对象中的 writeLatch 来阻塞当前线程。
对于线程阻塞时间为写超时,默认不配置为 -1,这时写时时间为 Long.MAX_VALUE 毫秒。
如果超时,则抛出 SocketTimeoutException,并取消上面注册的读事件。
最后将该事件从 selector 中移除(一般是可写事件)。
目前先写到这里,下一篇文章里我们继续介绍 tomcat nio 中的 BlockPoller 线程。