Tomcat NIO(12)-响应数据写入

上一篇文章里我们主要介绍了 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 线程。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值