对于 tomcat 请求数据的读取来说,可以分为请求行的读取,请求头的读取,请求体的读取,三个部分方法调用序列图如下:
读取请求行读取请求头读取请求体综合上面三个序列图,对于请求行,请求头,请求体的读取都最终调用了NioSocketWrapper 对象实例的 fillReadBuffer() 方法。只不过请求行和请求头读取参数传递为 true,请求体读取参数传递为 false,该方法核心代码如下:
private int fillReadBuffer(boolean block, ByteBuffer to) throws IOException { int nRead; NioChannel socket = getSocket(); if (socket instanceof ClosedNioChannel) { throw new ClosedChannelException(); } if (block) { Selector selector = null; try { selector = pool.get(); } catch (IOException x) { // Ignore } try { nRead = pool.read(to, socket, selector, getReadTimeout()); } finally { if (selector != null) { pool.put(selector); } } } else { nRead = socket.read(to); if (nRead == -1) { throw new EOFException(); } } return nRead;}
- 根据上述方法分析,对于请求行和请求头的读取采用非阻塞的方式,用到了以前文章介绍的 NioChannel 对象的 read(ByteBuffer) 方法,其核心代码如下:
//NioChannelprotected SocketChannel scpublic int read(ByteBuffer dst) throws IOException { return sc.read(dst); }
- 根据上述代码,请求行和请求头的读取本质是调用 java NIO api SocketChannel 的 read() 方法,该方法为非阻塞方法。如果读不到数据就直接返回,继续由以前文章介绍的 poller 线程监测是否有数据可读。
对于请求体的读取采用阻塞的方式,调用以前文章中介绍的 NioSelectorPool 的read(ByteBuffer buf, NioChannel socket, Selector selector, long readTimeout) 方法。在该方法中又会调用 NioBlockingSelector 的 read() 方法,核心代码如下:
public int read(ByteBuffer buf, NioChannel socket, long readTimeout) 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 read = 0; boolean timedout = false; int keycount = 1; //assume we can read long time = System.currentTimeMillis(); //start the timeout timer try { while (!timedout) { if (keycount > 0) { //only read if we were registered for a read read = socket.read(buf); if (read != 0) { break; } } try { if (att.getReadLatch()==null || att.getReadLatch().getCount()==0) { att.startReadLatch(1); } poller.add(att,SelectionKey.OP_READ, reference); att.awaitReadLatch(AbstractEndpoint.toTimeout(readTimeout), TimeUnit.MILLISECONDS); } catch (InterruptedException ignore) { // Ignore } if ( att.getReadLatch()!=null && att.getReadLatch().getCount()> 0) { //we got interrupted, but we haven't received notification from the poller. keycount = 0; }else { //latch countdown has happened keycount = 1; att.resetReadLatch(); } if (readTimeout >= 0 && (keycount == 0)) { timedout = (System.currentTimeMillis() - time) >= readTimeout; } } if (timedout) { throw new SocketTimeoutException(); } } finally { poller.remove(att,SelectionKey.OP_READ); if (timedout && reference.key != null) { poller.cancelKey(reference.key); } reference.key = null; keyReferenceStack.push(reference); } return read;}
- 根据以上代码整个读数据逻辑在一个循环里进行,如果有数据读到就跳出循环,返回数据。
- 如果没有数据,则调用 BlockPoller 的 add() 方法,该方法将封装的 OP_READ 事件添加到 BlockPoller 的事件队列里。关于 BlockPoller 我们在后面文章里详细讲解。
- 然后在调用以前文章介绍的 NioSocketWrapper 中的 CountDownLatch 类型 readLatch 属性的 await() 方法,使当前线程(一般是tomcat io线程)在 readLatch 上等待。
当前线程在 readLatch 上等待一般有超时时间(读超时时间),默认不配置为 -1,这时超时时间为 Long.MAX_VALUE 毫秒。
对于 tomcat 数据读取总结如下:
对于请求行,请求头和请求体的读取默认(不开启异步)都在 tomcat io 线程中进行。
对于请求行和请求头的读取是非阻塞读取,即不阻塞 tomcat io 线程,如果没有读取到数据,则由 poll 线程继续监测下次数据的到来。
对于请求体的读取是阻塞的读取,如果发现请求体数据不可读,那么首先注册封装的 OP_READ 事件到 BlockPoller 对象实例的事件队列里。然后利用 NioSocketWrapper 对象中的 readLatch 来阻塞 tomcat io 线程。
对于 tomcat io 线程阻塞时间为读超时,默认不配置为 -1,这时超时时间为 Long.MAX_VALUE 毫秒。
如果超时,则抛出 SocketTimeoutException,并取消上面注册的读事件。
最后将该事件从 selector 中移除(一般是可读事件)。
目前先写到这里,下一篇文章里我们继续介绍 tomcat 中的响应数据的写入。