/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.tomcat.util.net; import java.io.EOFException; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Socket; import java.net.SocketTimeoutException; import java.nio.ByteBuffer; import java.nio.channels.CancelledKeyException; import java.nio.channels.Channel; import java.nio.channels.FileChannel; import java.nio.channels.NetworkChannel; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.nio.channels.WritableByteChannel; import java.util.ConcurrentModificationException; import java.util.Iterator; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLSession; import org.apache.coyote.AbstractProtocol; import org.apache.coyote.http11.Http11Processor; import org.apache.juli.logging.Log; import org.apache.juli.logging.LogFactory; import org.apache.tomcat.util.ExceptionUtils; import org.apache.tomcat.util.IntrospectionUtils; import org.apache.tomcat.util.collections.SynchronizedQueue; import org.apache.tomcat.util.collections.SynchronizedStack; import org.apache.tomcat.util.net.AbstractEndpoint.Handler.SocketState; import org.apache.tomcat.util.net.jsse.JSSESupport; /** * 负责接收处理底层麻烦的Socket网络连接(TCP/IP协议) * NIO tailored thread pool, providing the following services: * NIO定义的线程池,可以实现以下功能:监听线程 Acceptor、socket NIO poller 线程、以及请求处理线程池。 * <ul> * <li>Socket acceptor thread</li> * <li>Socket poller thread</li> * <li>Worker threads pool</li> * </ul> * * When switching to Java 5, there's an opportunity to use the virtual * machine's thread pool. * 当选择了JAVA5以后,我们使用的就是虚拟机的线程池了(虚拟机的线程如何来的,后续分享)。 * * 1. Acceptor监听到Socket后,将SocketChannel封装成对应的NioChannel。 * 2. 将NioChannel注册到Selector.(getPoller.register(nioChannel)) * 大致流程: * Acceptor(接收请求并将SocketChannel封装成NioChannel) ----> (queue(event queue)) --->Poller(Selector )--->(Worker) * Acceptor接收socket线程封装成NioChannel并放入队列,Poller负责从队列中取出对应的Channel对象。 * 然后Poller通过Selector对象遍历数据并传递给Worker中的线程。 * Worker线程转发对应的协议处理器去解析成Request对象. * * 当我们keys host:8080/TestTomcatServlet/FirstServlet NioEndPoint的内部类开始工作了。 * @author Mladen Turk * @author Remy MaucheratNioChannel * @translator chenchen6([email protected]/[email protected]) */ public class NioEndpoint extends AbstractJsseEndpoint<NioChannel> { // -------------------------------------------------------------- Constants private static final Log log = LogFactory.getLog(NioEndpoint.class); /** * 16进制的Ox100 2^8 = 256. */ public static final int OP_REGISTER = 0x100; //register interest op // ----------------------------------------------------------------- Fields /** * */ private NioSelectorPool selectorPool = new NioSelectorPool(); /** * Server socket "pointer". * ServerSocketChannel,用于监听客户端的连接, * 它是所有客户端连接的父管道。 */ private volatile ServerSocketChannel serverSock = null; /** * */ private volatile CountDownLatch stopLatch = null; /** * Cache for poller events * 缓存了Poller的事件。 * 缓存的作用:不用多说,缓存起来,提高利用率和速率。 */ private SynchronizedStack<PollerEvent> eventCache; /** * Bytebuffer cache, each channel holds a set of buffers (two, except for SSL holds four) */ private SynchronizedStack<NioChannel> nioChannels; // ------------------------------------------------------------- Properties /** * Generic properties, introspected */ @Override public boolean setProperty(String name, String value) { final String selectorPoolName = "selectorPool."; try { if (name.startsWith(selectorPoolName)) { return IntrospectionUtils.setProperty(selectorPool, name.substring(selectorPoolName.length()), value); } else { return super.setProperty(name, value); } }catch ( Exception x ) { log.error("Unable to set attribute \""+name+"\" to \""+value+"\"",x); return false; } } /** * Use System.inheritableChannel to obtain channel from stdin/stdout. */ private boolean useInheritedChannel = false; public void setUseInheritedChannel(boolean useInheritedChannel) { this.useInheritedChannel = useInheritedChannel; } public boolean getUseInheritedChannel() { return useInheritedChannel; } /** * Priority of the poller threads. */ private int pollerThreadPriority = Thread.NORM_PRIORITY; public void setPollerThreadPriority(int pollerThreadPriority) { this.pollerThreadPriority = pollerThreadPriority; } public int getPollerThreadPriority() { return pollerThreadPriority; } /** * Poller thread count. * 轮询器的线程数和你的机器cpu核数是有关系的。 */ private int pollerThreadCount = Math.min(2,Runtime.getRuntime().availableProcessors()); public void setPollerThreadCount(int pollerThreadCount) { this.pollerThreadCount = pollerThreadCount; } public int getPollerThreadCount() { return pollerThreadCount; } private long selectorTimeout = 1000; public void setSelectorTimeout(long timeout){ this.selectorTimeout = timeout;} public long getSelectorTimeout(){ return this.selectorTimeout; } /** * The socket poller. */ private Poller[] pollers = null; private AtomicInteger pollerRotater = new AtomicInteger(0); /** * Return an available poller in true round robin fashion. * * @return The next poller in sequence */ public Poller getPoller0() { int idx = Math.abs(pollerRotater.incrementAndGet()) % pollers.length; return pollers[idx]; } public void setSelectorPool(NioSelectorPool selectorPool) { this.selectorPool = selectorPool; } public void setSocketProperties(SocketProperties socketProperties) { this.socketProperties = socketProperties; } /** * Is deferAccept supported? */ @Override public boolean getDeferAccept() { // Not supported return false; } // --------------------------------------------------------- Public Methods /** * Number of keep-alive sockets. * * @return The number of sockets currently in the keep-alive state waiting * for the next request to be received on the socket */ public int getKeepAliveCount() { if (pollers == null) { return 0; } else { int sum = 0; for (int i=0; i<pollers.length; i++) { sum += pollers[i].getKeyCount(); } return sum; } } // ----------------------------------------------- Public Lifecycle Methods /** * Initialize the endpoint. * 1.绑定ServerSocketChannel的端口和Ip。以及允许链接的数量默认为100. * 2.改变Channel的阻塞模式。 * 3.接收器和轮询器的线程数设置。 * 4.轮询器池的开启。 */ @Override public void bind() throws Exception { /** * set ServerSocket。设置Socket相关,并绑定对应接口。 */ if (!getUseInheritedChannel()) { /** * 1.打开 ServerSocketChannel,用于监听客户端的连接(在初始化的时候。) */ serverSock = ServerSocketChannel.open(); socketProperties.setProperties(serverSock.socket()); InetSocketAddress addr = (getAddress()!=null?new InetSocketAddress(getAddress(),getPort()):new InetSocketAddress(getPort())); /** * 2.绑定指定监听端口以及设置最大连接数默认为100。 */ serverSock.socket().bind(addr,getAcceptCount()); } else { // Retrieve the channel provided by the OS Channel ic = System.inheritedChannel(); if (ic instanceof ServerSocketChannel) { serverSock = (ServerSocketChannel) ic; } if (serverSock == null) { throw new IllegalArgumentException(sm.getString("endpoint.init.bind.inherited")); } } /** * 3.设置链接为阻塞模式。 * 模拟ARP行为。。。。 */ serverSock.configureBlocking(true); //mimic APR behavior // Initialize thread count defaults for acceptor, poller /** * 初始化Acceptor和Poller的线程数都是1. */ if (acceptorThreadCount == 0) { // FIXME: Doesn't seem to work that well with multiple accept threads //多个线程的时候不能很好的去工作? acceptorThreadCount = 1; } if (pollerThreadCount <= 0) { //minimum one poller thread pollerThreadCount = 1; } setStopLatch(new CountDownLatch(pollerThreadCount)); // Initialize SSL if needed initialiseSsl(); /** * 4.打开主Selector,并创建和打开辅Selector。 */ selectorPool.open(); } /** * Start the NIO endpoint, creating acceptor, poller threads. * * 1.直接启动NioEndPoint。 * 2.实例化对应数据结构.(Processor, PollerEvent, NioChannel) * 3.对连接数做限制。 * 4.创建I/O密集型的线程池. * 5.创建Acceptor 和 Poller的线程组。 * * * Connector的Accptor,Poller,PollerEvent都是继承Runnable. * 所以关注其run方法是必然的. * 先搞定全局的大致流程.再去细挑其逻辑.这才是读源码的最佳途径. * 一边读一边debug. */ @Override public void startInternal() throws Exception { if (!running) { /** * 设置NidEndPoint的可运行状态为true.便于在下面启动的时候进行判断。 */ running = true; paused = false; /** * 初始化数据结构. */ processorCache = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE, socketProperties.getProcessorCache()); eventCache = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE, socketProperties.getEventCache()); nioChannels = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE, socketProperties.getBufferPool()); // Create worker collection //创建I/O密集型的线程池. if ( getExecutor() == null ) { createExecutor(); } /** * 初始化连的限制器个数。 * 避免处理请求过多导致服务器崩掉. */ initializeConnectionLatch(); // Start poller threads pollers = new Poller[getPollerThreadCount()]; for (int i=0; i<pollers.length; i++) { pollers[i] = new Poller(); Thread pollerThread = new Thread(pollers[i], getName() + "-ClientPoller-"+i); pollerThread.setPriority(threadPriority); pollerThread.setDaemon(true); /** * 轮询器线程的启动 * {@link Poller#run()} */ pollerThread.start(); } /** * {@link AbstractEndpoint#startAcceptorThreads()} */ startAcceptorThreads(); } } /** * Stop the endpoint. This will cause all processing threads to stop. * EndPoint的停止。会导致所有的正在处理请求的线程也会停止。 * 当此方法被调用的时候,说明tomcat已关闭。 * 安全的关闭线程池是一种优雅的操作。 * 所以作者给定了当前处理请求的线程也会被停止。 */ @Override public void stopInternal() { releaseConnectionLatch(); if (!paused) { pause(); } if (running) { running = false; unlockAccept(); for (int i=0; pollers!=null && i<pollers.length; i++) { if (pollers[i]==null) continue; pollers[i].destroy(); pollers[i] = null; } try { if (!getStopLatch().await(selectorTimeout + 100, TimeUnit.MILLISECONDS)) { log.warn(sm.getString("endpoint.nio.stopLatchAwaitFail")); } } catch (InterruptedException e) { log.warn(sm.getString("endpoint.nio.stopLatchAwaitInterrupted"), e); } /** * 关闭线程池。 */ shutdownExecutor(); eventCache.clear(); nioChannels.clear(); processorCache.clear(); } } /** * Deallocate NIO memory pools, and close server socket. */ @Override public void unbind() throws Exception { if (log.isDebugEnabled()) { log.debug("Destroy initiated for "+new InetSocketAddress(getAddress(),getPort())); } if (running) { stop(); } doCloseServerSocket(); destroySsl(); super.unbind(); if (getHandler() != null ) { getHandler().recycle(); } selectorPool.close(); if (log.isDebugEnabled()) { log.debug("Destroy completed for "+new InetSocketAddress(getAddress(),getPort())); } } @Override protected void doCloseServerSocket() throws IOException { if (!getUseInheritedChannel() && serverSock != null) { // Close server socket serverSock.socket().close(); serverSock.close(); } serverSock = null; } // ------------------------------------------------------ Protected Methods public int getWriteBufSize() { return socketProperties.getTxBufSize(); } public int getReadBufSize() { return socketProperties.getRxBufSize(); } public NioSelectorPool getSelectorPool() { return selectorPool; } @Override protected AbstractEndpoint.Acceptor createAcceptor() { return new Acceptor(); } protected CountDownLatch getStopLatch() { return stopLatch; } protected void setStopLatch(CountDownLatch stopLatch) { this.stopLatch = stopLatch; } /** * * 处理特别的连接。 * Process the specified connection. * @param socket The socket channel * @return <code>true</code> if the socket was correctly configured * and processing may continue, <code>false</code> if the socket needs to be * close immediately * true 和 false 取决于socket是否能够正确的执行。 * * * 在这里的意图:将SocketChannel 转换成 NioChannel. * 然后将NioChannel注册到轮询器。 * 1.设置SocketChannel属性和对应的Socket并转换为NioChannel。 * 2.将NioChannel注册到Poller上去。 */ protected boolean setSocketOptions(SocketChannel socket) { // Process the connection try { //disable blocking, APR style, we are gonna be polling it //禁止用阻塞,ARP的风格。我门要轮询他。 socket.configureBlocking(false); Socket sock = socket.socket(); socketProperties.setProperties(sock); NioChannel channel = nioChannels.pop(); if (channel == null) { /** * 初始化SocketBufferHandler。 */ SocketBufferHandler bufhandler = new SocketBufferHandler( socketProperties.getAppReadBufSize(), socketProperties.getAppWriteBufSize(), socketProperties.getDirectBuffer()); /** * 创建Channel对象。 */ if (isSSLEnabled()) { channel = new SecureNioChannel(socket, bufhandler, selectorPool, this); } else { channel = new NioChannel(socket, bufhandler); } } else { channel.setIOChannel(socket); channel.reset(); } /** * 将NioChannel注册到Poller内。 */ getPoller0().register(channel); } catch (Throwable t) { ExceptionUtils.handleThrowable(t); try { log.error("",t); } catch (Throwable tt) { ExceptionUtils.handleThrowable(tt); } // Tell to close the socket return false; } return true; } @Override protected Log getLog() { return log; } @Override protected NetworkChannel getServerSocket() { return serverSock; } // --------------------------------------------------- Acceptor Inner Class /** * The background thread that listens for incoming TCP/IP connections and * hands them off to an appropriate processor. * 生产者消费者模型利用后台线程将TCP/IP请求传递给对应的处理器去处理. */ protected class Acceptor extends AbstractEndpoint.Acceptor { /** * 典型的生产者消费者模型. * 1.轮询的去接收ServerSocketChannel对象. * 2.将返回的SocketChannel对象转换成NioChannel. * 3.再将NioChannel注册到Poller中. * 4.生成对应的PollerEvent.放置队列中供Poller去获取. */ @Override public void run() { log.info("NioEndPoint -> Acceptor -> run() -> start!"); int errorDelay = 0; log.info("NioEndPoint -> Acceptor -> run() -> running state is : " + running); // Loop until we receive a shutdown command while (running) { // Loop if endpoint is paused /** * 自旋 */ while (paused && running) { state = AcceptorState.PAUSED; try { Thread.sleep(50); } catch (InterruptedException e) { // Ignore } } if (!running) { break; } state = AcceptorState.RUNNING; try { //if we have reached max connections, wait /** * 当到达最大连接数的时候就等待。 * 点进源码看一看。 */ countUpOrAwaitConnection(); SocketChannel socket = null; try { // Accept the next incoming connection from the server // socket /** * 监听来自ServerSocket中进来的下一个请求. * 阻塞式代码走到这儿的时候说明获得了共享锁。 * 5. Acceptor并没有将Channel注册到Selector上。 * 这里通过阻塞的方式获取了对应的SocketChannel. */ socket = serverSock.accept(); log.info("NioEndPoint -> Acceptor -> run() -> socket value is : " + socket.toString()); } catch (IOException ioe) { // We didn't get a socket countDownConnection(); if (running) { // Introduce delay if necessary errorDelay = handleExceptionWithDelay(errorDelay); // re-throw throw ioe; } else { break; } } log.info("NioEndPoint -> Acceptor -> run() -> errorDelay value is : " + errorDelay); // Successful accept, reset the error delay //保证错误的处理时间为0。 errorDelay = 0; // Configure the socket if (running && !paused) { // setSocketOptions() will hand the socket off to // an appropriate processor if successful //转换socket为NioChannel,注册进轮询器。 /** * 6.将SocketChannel注册到Poller上的Seletor(read事件)。 */ if (!setSocketOptions(socket)) { closeSocket(socket); } } else { closeSocket(socket); } } catch (Throwable t) { ExceptionUtils.handleThrowable(t); log.error(sm.getString("endpoint.accept.fail"), t); } } state = AcceptorState.ENDED; log.info("NioEndPoint -> Acceptor -> run() -> end!!!"); } private void closeSocket(SocketChannel socket) { countDownConnection(); try { socket.socket().close(); } catch (IOException ioe) { if (log.isDebugEnabled()) { log.debug(sm.getString("endpoint.err.close"), ioe); } } try { socket.close(); } catch (IOException ioe) { if (log.isDebugEnabled()) { log.debug(sm.getString("endpoint.err.close"), ioe); } } } } @Override protected SocketProcessorBase<NioChannel> createSocketProcessor( SocketWrapperBase<NioChannel> socketWrapper, SocketEvent event) { /** * {@link SocketProcessor#doRun()} * and * {@link SocketProcessorBase#run()} */ return new SocketProcessor(socketWrapper, event); } private void close(NioChannel socket, SelectionKey key) { try { if (socket.getPoller().cancelledKey(key) != null) { // SocketWrapper (attachment) was removed from the // key - recycle the key. This can only happen once // per attempted closure so it is used to determine // whether or not to return the key to the cache. // We do NOT want to do this more than once - see BZ // 57340 / 57943. if (log.isDebugEnabled()) { log.debug("Socket: [" + socket + "] closed"); } if (running && !paused) { if (!nioChannels.push(socket)) { socket.free(); } } } } catch (Exception x) { log.error("",x); } } // ----------------------------------------------------- Poller Inner Classes /** * * PollerEvent, cacheable object for poller events to avoid GC * 可以缓存的轮询器事件,避免GC频繁回收。 * NioSocketWrapper将将NioChannel包装成此类. * 此类会被放置到队列中去. */ public static class PollerEvent implements Runnable { /** * */ private NioChannel socket; /** * */ private int interestOps; /** * */ private NioSocketWrapper socketWrapper; public PollerEvent(NioChannel ch, NioSocketWrapper w, int intOps) { reset(ch, w, intOps); } public void reset(NioChannel ch, NioSocketWrapper w, int intOps) { socket = ch; interestOps = intOps; socketWrapper = w; } public void reset() { reset(null, null, 0); } /** * 轮询器事件:还是个线程,最重要的仍然是run方法做了什么。 */ @Override public void run() { if (interestOps == OP_REGISTER) { try { socket.getIOChannel().register( socket.getPoller().getSelector(), SelectionKey.OP_READ, socketWrapper); } catch (Exception x) { log.error(sm.getString("endpoint.nio.registerFail"), x); } } else { final SelectionKey key = socket.getIOChannel().keyFor(socket.getPoller().getSelector()); try { if (key == null) { // The key was cancelled (e.g. due to socket closure) // and removed from the selector while it was being // processed. Count down the connections at this point // since it won't have been counted down when the socket // closed. socket.socketWrapper.getEndpoint().countDownConnection(); ((NioSocketWrapper) socket.socketWrapper).closed = true; } else { final NioSocketWrapper socketWrapper = (NioSocketWrapper) key.attachment(); if (socketWrapper != null) { //we are registering the key to start with, reset the fairness counter. int ops = key.interestOps() | interestOps; socketWrapper.interestOps(ops); key.interestOps(ops); } else { socket.getPoller().cancelledKey(key); } } } catch (CancelledKeyException ckx) { try { socket.getPoller().cancelledKey(key); } catch (Exception ignore) {} } } } @Override public String toString() { return "Poller event: socket [" + socket + "], socketWrapper [" + socketWrapper + "], interestOps [" + interestOps + "]"; } } /** * Poller class.只有这个类是非阻塞。Acceptor和对应的Worker线程都是阻塞的。 * 轮询器.(核心仍然在run方法内) * Acceptor获取到对应的Socket转换成NioChannel后注册到Poller内。 * Poller从队列中可拿到事件.并将事件到已经持有的Selector上. * 1.从eventsQueue中拿到之前Acceptor放置的PollerEvent. * 2.将PollerEvent放置在Selector上. * 3.将NioSocketWrapper绑定在对应的SelectionKey上. * 4.将已就绪的Event通过SocketProcesser用线程池中的线程去处理(I/O密集线程池). * 5.SocketProcessor 取得ConnectionHandler. * 6.ConnectionHandler将NioSocketWrapper放置在Http11Processor中去处理(Http11Processor). */ public class Poller implements Runnable { /** * nio多路复用的重要组件(主Selector。)。 * 可以只使用一个线程去控制多个Channel。 * 减少线程的切换。达到性能要求。 * * Thread * | * Selector * | * ------------------------------ * | | | * Channel Channel Channel */ private Selector selector; /** * 存储轮询器事件的Queue。 * 生产者(Acceptor) ==========>消费者(Poller)的缓冲区。 * 匹配二者速度不一致的问题。达到生产和消费平衡的作用。 * 典型的生产者消费者模型。 */ private final SynchronizedQueue<PollerEvent> events = new SynchronizedQueue<>(); private volatile boolean close = false; private long nextExpiration = 0;//optimize expiration handling /** * 用于记录唤醒的Channel的个数来判断获取channel的方式。 */ private AtomicLong wakeupCounter = new AtomicLong(0); /** * Selector内就绪状态Channel的个数。 */ private volatile int keyCount = 0; /** * 构造器内初始化管道。 * @throws IOException */ public Poller() throws IOException { this.selector = Selector.open(); } public int getKeyCount() { return keyCount; } public Selector getSelector() { return selector;} /** * Destroy the poller. */ protected void destroy() { // Wait for polltime before doing anything, so that the poller threads // exit, otherwise parallel closure of sockets which are still // in the poller can cause problems close = true; selector.wakeup(); } private void addEvent(PollerEvent event) { events.offer(event); if ( wakeupCounter.incrementAndGet() == 0 ) selector.wakeup(); } /** * Add specified socket and associated pool to the poller. The socket will * be added to a temporary array, and polled first after a maximum amount * of time equal to pollTime (in most cases, latency will be much lower, * however). * 向Poller中添加指定的socket和 * @param socket to add to the poller * @param interestOps Operations for which to register this socket with * the Poller */ public void add(final NioChannel socket, final int interestOps) { PollerEvent r = eventCache.pop(); if ( r==null) { r = new PollerEvent(socket,null,interestOps); }else { r.reset(socket,null,interestOps); } addEvent(r); if (close) { NioEndpoint.NioSocketWrapper ka = (NioEndpoint.NioSocketWrapper)socket.getAttachment(); processSocket(ka, SocketEvent.STOP, false); } } /** * Processes events in the event queue of the Poller. * 去处理Poller(轮询器)事件队列中的事件。 * * @return <code>true</code> if some events were processed, * <code>false</code> if queue was empty */ public boolean events() { boolean result = false; PollerEvent pe = null; /** * 咦。这里怎么是遍历了..... * 看一看看一看,处理轮询器队列的事件。 * 首先遍历每个事件,然后重置,最后判断状态放置在Cache中去。 */ for (int i = 0, size = events.size(); i < size && (pe = events.poll()) != null; i++ ) { result = true; try { /** * 启动对应的轮询事件。 */ pe.run(); /** * 设置空的channel,wrapper。 */ pe.reset(); if (running && !paused) { eventCache.push(pe); } } catch ( Throwable x ) { log.error("",x); } } return result; } /** * Registers a newly created socket with the poller. * 轮询器注册新的Socket。 * 1. 设置当前的Poller为NioChannel的Poller。 * 2. 设置对应的NioSocketWrapper。 * 3. 拿到事件对象(从缓存或new)。 * 4. 并将事件对象放置队列内。 * * @param socket The newly created socket */ public void register(final NioChannel socket) { socket.setPoller(this); /** * 关注这里。 */ NioSocketWrapper ka = new NioSocketWrapper(socket, NioEndpoint.this); socket.setSocketWrapper(ka); ka.setPoller(this); ka.setReadTimeout(getSocketProperties().getSoTimeout()); ka.setWriteTimeout(getSocketProperties().getSoTimeout()); ka.setKeepAliveLeft(NioEndpoint.this.getMaxKeepAliveRequests()); ka.setSecure(isSSLEnabled()); ka.setReadTimeout(getConnectionTimeout()); ka.setWriteTimeout(getConnectionTimeout()); PollerEvent r = eventCache.pop(); ka.interestOps(SelectionKey.OP_READ);//this is what OP_REGISTER turns into. if ( r==null) r = new PollerEvent(socket,ka,OP_REGISTER); else r.reset(socket,ka,OP_REGISTER); addEvent(r); } public NioSocketWrapper cancelledKey(SelectionKey key) { NioSocketWrapper ka = null; try { if ( key == null ) return null;//nothing to do ka = (NioSocketWrapper) key.attach(null); if (ka != null) { // If attachment is non-null then there may be a current // connection with an associated processor. getHandler().release(ka); } if (key.isValid()) key.cancel(); // If it is available, close the NioChannel first which should // in turn close the underlying SocketChannel. The NioChannel // needs to be closed first, if available, to ensure that TLS // connections are shut down cleanly. if (ka != null) { try { ka.getSocket().close(true); } catch (Exception e){ if (log.isDebugEnabled()) { log.debug(sm.getString( "endpoint.debug.socketCloseFail"), e); } } } // The SocketChannel is also available via the SelectionKey. If // it hasn't been closed in the block above, close it now. if (key.channel().isOpen()) { try { key.channel().close(); } catch (Exception e) { if (log.isDebugEnabled()) { log.debug(sm.getString( "endpoint.debug.channelCloseFail"), e); } } } try { if (ka != null && ka.getSendfileData() != null && ka.getSendfileData().fchannel != null && ka.getSendfileData().fchannel.isOpen()) { ka.getSendfileData().fchannel.close(); } } catch (Exception ignore) { } if (ka != null) { countDownConnection(); ka.closed = true; } } catch (Throwable e) { ExceptionUtils.handleThrowable(e); if (log.isDebugEnabled()) log.error("",e); } return ka; } /** * The background thread that adds sockets to the Poller, checks the * poller for triggered events and hands the associated socket off to an * appropriate processor as events occur. * * 选择对应的处理器。processKey() */ @Override public void run() { // Loop until destroy() is called while (true) { boolean hasEvents = false; try { if (!close) { /** * //关注events()方法做了什么。 * 启动了对应的PollerEvent事件run方法。 */ hasEvents = events(); /** * Poller去处理的时候非阻塞。 * 获取对应的Channel。 */ if (wakeupCounter.getAndSet(-1) > 0) { //if we are here, means we have other stuff to do //do a non blocking select keyCount = selector.selectNow(); } else { keyCount = selector.select(selectorTimeout); } wakeupCounter.set(0); } if (close) { events(); timeout(0, false); try { selector.close(); } catch (IOException ioe) { log.error(sm.getString("endpoint.nio.selectorCloseFail"), ioe); } break; } } catch (Throwable x) { ExceptionUtils.handleThrowable(x); log.error("",x); continue; } //either we timed out or we woke up, process events first if ( keyCount == 0 ) hasEvents = (hasEvents | events()); /** * 获取对应的selector内SelectKeys的迭代器。 */ Iterator<SelectionKey> iterator = keyCount > 0 ? selector.selectedKeys().iterator() : null; // Walk through the collection of ready keys and dispatch // any active event. /** * 遍历处于read状态的keys集合和分发处于活动状态的任何事件。 */ while (iterator != null && iterator.hasNext()) { SelectionKey sk = iterator.next(); NioSocketWrapper attachment = (NioSocketWrapper)sk.attachment(); // Attachment may be null if another thread has called // cancelledKey() if (attachment == null) { iterator.remove(); } else { iterator.remove(); /** * 处理对应的SelectKey。 * {@link Poller#processKey(java.nio.channels.SelectionKey, org.apache.tomcat.util.net.NioEndpoint.NioSocketWrapper)} */ processKey(sk, attachment); } }//while //process timeouts timeout(keyCount,hasEvents); }//while getStopLatch().countDown(); } protected void processKey(SelectionKey sk, NioSocketWrapper attachment) { try { if ( close ) { cancelledKey(sk); } else if ( sk.isValid() && attachment != null ) { if (sk.isReadable() || sk.isWritable() ) { if ( attachment.getSendfileData() != null ) { processSendfile(sk,attachment, false); } else { unreg(sk, attachment, sk.readyOps()); boolean closeSocket = false; // Read goes before write /** * 写之前先读。 */ if (sk.isReadable()) { //读事件。 /** * {@link AbstractEndpoint#processSocket(org.apache.tomcat.util.net.SocketWrapperBase, org.apache.tomcat.util.net.SocketEvent, boolean)} */ if (!processSocket(attachment, SocketEvent.OPEN_READ, true)) { closeSocket = true; } } if (!closeSocket && sk.isWritable()) { //写事件 if (!processSocket(attachment, SocketEvent.OPEN_WRITE, true)) { closeSocket = true; } } if (closeSocket) { cancelledKey(sk); } } } } else { //invalid key cancelledKey(sk); } } catch ( CancelledKeyException ckx ) { cancelledKey(sk); } catch (Throwable t) { ExceptionUtils.handleThrowable(t); log.error("",t); } } public SendfileState processSendfile(SelectionKey sk, NioSocketWrapper socketWrapper, boolean calledByProcessor) { NioChannel sc = null; try { unreg(sk, socketWrapper, sk.readyOps()); SendfileData sd = socketWrapper.getSendfileData(); if (log.isTraceEnabled()) { log.trace("Processing send file for: " + sd.fileName); } if (sd.fchannel == null) { // Setup the file channel File f = new File(sd.fileName); @SuppressWarnings("resource") // Closed when channel is closed FileInputStream fis = new FileInputStream(f); sd.fchannel = fis.getChannel(); } // Configure output channel sc = socketWrapper.getSocket(); // TLS/SSL channel is slightly different WritableByteChannel wc = ((sc instanceof SecureNioChannel)?sc:sc.getIOChannel()); // We still have data in the buffer if (sc.getOutboundRemaining()>0) { if (sc.flushOutbound()) { socketWrapper.updateLastWrite(); } } else { long written = sd.fchannel.transferTo(sd.pos,sd.length,wc); if (written > 0) { sd.pos += written; sd.length -= written; socketWrapper.updateLastWrite(); } else { // Unusual not to be able to transfer any bytes // Check the length was set correctly if (sd.fchannel.size() <= sd.pos) { throw new IOException("Sendfile configured to " + "send more data than was available"); } } } if (sd.length <= 0 && sc.getOutboundRemaining()<=0) { if (log.isDebugEnabled()) { log.debug("Send file complete for: "+sd.fileName); } socketWrapper.setSendfileData(null); try { sd.fchannel.close(); } catch (Exception ignore) { } // For calls from outside the Poller, the caller is // responsible for registering the socket for the // appropriate event(s) if sendfile completes. if (!calledByProcessor) { switch (sd.keepAliveState) { case NONE: { if (log.isDebugEnabled()) { log.debug("Send file connection is being closed"); } close(sc, sk); break; } case PIPELINED: { if (log.isDebugEnabled()) { log.debug("Connection is keep alive, processing pipe-lined data"); } if (!processSocket(socketWrapper, SocketEvent.OPEN_READ, true)) { close(sc, sk); } break; } case OPEN: { if (log.isDebugEnabled()) { log.debug("Connection is keep alive, registering back for OP_READ"); } reg(sk,socketWrapper,SelectionKey.OP_READ); break; } } } return SendfileState.DONE; } else { if (log.isDebugEnabled()) { log.debug("OP_WRITE for sendfile: " + sd.fileName); } if (calledByProcessor) { add(socketWrapper.getSocket(),SelectionKey.OP_WRITE); } else { reg(sk,socketWrapper,SelectionKey.OP_WRITE); } return SendfileState.PENDING; } } catch (IOException x) { if (log.isDebugEnabled()) log.debug("Unable to complete sendfile request:", x); if (!calledByProcessor && sc != null) { close(sc, sk); } return SendfileState.ERROR; } catch (Throwable t) { log.error("", t); if (!calledByProcessor && sc != null) { close(sc, sk); } return SendfileState.ERROR; } } protected void unreg(SelectionKey sk, NioSocketWrapper attachment, int readyOps) { //this is a must, so that we don't have multiple threads messing with the socket reg(sk,attachment,sk.interestOps()& (~readyOps)); } protected void reg(SelectionKey sk, NioSocketWrapper attachment, int intops) { sk.interestOps(intops); attachment.interestOps(intops); } protected void timeout(int keyCount, boolean hasEvents) { long now = System.currentTimeMillis(); // This method is called on every loop of the Poller. Don't process // timeouts on every loop of the Poller since that would create too // much load and timeouts can afford to wait a few seconds. // However, do process timeouts if any of the following are true: // - the selector simply timed out (suggests there isn't much load) // - the nextExpiration time has passed // - the server socket is being closed if (nextExpiration > 0 && (keyCount > 0 || hasEvents) && (now < nextExpiration) && !close) { return; } //timeout int keycount = 0; try { for (SelectionKey key : selector.keys()) { keycount++; try { NioSocketWrapper ka = (NioSocketWrapper) key.attachment(); if ( ka == null ) { cancelledKey(key); //we don't support any keys without attachments } else if (close) { key.interestOps(0); ka.interestOps(0); //avoid duplicate stop calls processKey(key,ka); } else if ((ka.interestOps()&SelectionKey.OP_READ) == SelectionKey.OP_READ || (ka.interestOps()&SelectionKey.OP_WRITE) == SelectionKey.OP_WRITE) { boolean isTimedOut = false; // Check for read timeout if ((ka.interestOps() & SelectionKey.OP_READ) == SelectionKey.OP_READ) { long delta = now - ka.getLastRead(); long timeout = ka.getReadTimeout(); isTimedOut = timeout > 0 && delta > timeout; } // Check for write timeout if (!isTimedOut && (ka.interestOps() & SelectionKey.OP_WRITE) == SelectionKey.OP_WRITE) { long delta = now - ka.getLastWrite(); long timeout = ka.getWriteTimeout(); isTimedOut = timeout > 0 && delta > timeout; } if (isTimedOut) { key.interestOps(0); ka.interestOps(0); //avoid duplicate timeout calls ka.setError(new SocketTimeoutException()); if (!processSocket(ka, SocketEvent.ERROR, true)) { cancelledKey(key); } } } }catch ( CancelledKeyException ckx ) { cancelledKey(key); } }//for } catch (ConcurrentModificationException cme) { // See https://bz.apache.org/bugzilla/show_bug.cgi?id=57943 log.warn(sm.getString("endpoint.nio.timeoutCme"), cme); } long prevExp = nextExpiration; //for logging purposes only nextExpiration = System.currentTimeMillis() + socketProperties.getTimeoutInterval(); if (log.isTraceEnabled()) { log.trace("timeout completed: keys processed=" + keycount + "; now=" + now + "; nextExpiration=" + prevExp + "; keyCount=" + keyCount + "; hasEvents=" + hasEvents + "; eval=" + ((now < prevExp) && (keyCount>0 || hasEvents) && (!close) )); } } } // ---------------------------------------------------- Key Attachment Class 关键附件类.几把. /** * Acceptor接收请求拿到的SocketChannel包装成NioChannel. * 此类便是在NioChannel注册到Poller的时候,去包装NioChannel,并生成PollerEvent. * 然后将事件放置在EventQueue内.供Poller去使用. */ public static class NioSocketWrapper extends SocketWrapperBase<NioChannel> { /** * NioSelectorPool */ private final NioSelectorPool pool; /** * 对应的轮询器Poller。 */ private Poller poller = null; /** * 公平计数器。 */ private int interestOps = 0; private CountDownLatch readLatch = null; private CountDownLatch writeLatch = null; private volatile SendfileData sendfileData = null; private volatile long lastRead = System.currentTimeMillis(); private volatile long lastWrite = lastRead; private volatile boolean closed = false; public NioSocketWrapper(NioChannel channel, NioEndpoint endpoint) { super(channel, endpoint); pool = endpoint.getSelectorPool(); socketBufferHandler = channel.getBufHandler(); } public Poller getPoller() { return poller;} public void setPoller(Poller poller){this.poller = poller;} public int interestOps() { return interestOps;} public int interestOps(int ops) { this.interestOps = ops; return ops; } public CountDownLatch getReadLatch() { return readLatch; } public CountDownLatch getWriteLatch() { return writeLatch; } protected CountDownLatch resetLatch(CountDownLatch latch) { if ( latch==null || latch.getCount() == 0 ) return null; else throw new IllegalStateException("Latch must be at count 0"); } public void resetReadLatch() { readLatch = resetLatch(readLatch); } public void resetWriteLatch() { writeLatch = resetLatch(writeLatch); } protected CountDownLatch startLatch(CountDownLatch latch, int cnt) { if ( latch == null || latch.getCount() == 0 ) { return new CountDownLatch(cnt); } else throw new IllegalStateException("Latch must be at count 0 or null."); } public void startReadLatch(int cnt) { readLatch = startLatch(readLatch,cnt);} public void startWriteLatch(int cnt) { writeLatch = startLatch(writeLatch,cnt);} protected void awaitLatch(CountDownLatch latch, long timeout, TimeUnit unit) throws InterruptedException { if ( latch == null ) throw new IllegalStateException("Latch cannot be null"); // Note: While the return value is ignored if the latch does time // out, logic further up the call stack will trigger a // SocketTimeoutException latch.await(timeout,unit); } public void awaitReadLatch(long timeout, TimeUnit unit) throws InterruptedException { awaitLatch(readLatch,timeout,unit);} public void awaitWriteLatch(long timeout, TimeUnit unit) throws InterruptedException { awaitLatch(writeLatch,timeout,unit);} public void setSendfileData(SendfileData sf) { this.sendfileData = sf;} public SendfileData getSendfileData() { return this.sendfileData;} public void updateLastWrite() { lastWrite = System.currentTimeMillis(); } public long getLastWrite() { return lastWrite; } public void updateLastRead() { lastRead = System.currentTimeMillis(); } public long getLastRead() { return lastRead; } @Override public boolean isReadyForRead() throws IOException { socketBufferHandler.configureReadBufferForRead(); if (socketBufferHandler.getReadBuffer().remaining() > 0) { return true; } fillReadBuffer(false); boolean isReady = socketBufferHandler.getReadBuffer().position() > 0; return isReady; } @Override public int read(boolean block, byte[] b, int off, int len) throws IOException { int nRead = populateReadBuffer(b, off, len); if (nRead > 0) { return nRead; /* * Since more bytes may have arrived since the buffer was last * filled, it is an option at this point to perform a * non-blocking read. However correctly handling the case if * that read returns end of stream adds complexity. Therefore, * at the moment, the preference is for simplicity. */ } // Fill the read buffer as best we can. nRead = fillReadBuffer(block); updateLastRead(); // Fill as much of the remaining byte array as possible with the // data that was just read if (nRead > 0) { socketBufferHandler.configureReadBufferForRead(); nRead = Math.min(nRead, len); socketBufferHandler.getReadBuffer().get(b, off, nRead); } return nRead; } @Override public int read(boolean block, ByteBuffer to) throws IOException { int nRead = populateReadBuffer(to); if (nRead > 0) { return nRead; /* * Since more bytes may have arrived since the buffer was last * filled, it is an option at this point to perform a * non-blocking read. However correctly handling the case if * that read returns end of stream adds complexity. Therefore, * at the moment, the preference is for simplicity. */ } // The socket read buffer capacity is socket.appReadBufSize int limit = socketBufferHandler.getReadBuffer().capacity(); if (to.remaining() >= limit) { to.limit(to.position() + limit); nRead = fillReadBuffer(block, to); if (log.isDebugEnabled()) { log.debug("Socket: [" + this + "], Read direct from socket: [" + nRead + "]"); } updateLastRead(); } else { // Fill the read buffer as best we can. nRead = fillReadBuffer(block); if (log.isDebugEnabled()) { log.debug("Socket: [" + this + "], Read into buffer: [" + nRead + "]"); } updateLastRead(); // Fill as much of the remaining byte array as possible with the // data that was just read if (nRead > 0) { nRead = populateReadBuffer(to); } } return nRead; } @Override public void close() throws IOException { getSocket().close(); } @Override public boolean isClosed() { return closed; } private int fillReadBuffer(boolean block) throws IOException { socketBufferHandler.configureReadBufferForWrite(); return fillReadBuffer(block, socketBufferHandler.getReadBuffer()); } private int fillReadBuffer(boolean block, ByteBuffer to) throws IOException { int nRead; NioChannel channel = getSocket(); if (block) { Selector selector = null; try { selector = pool.get(); } catch (IOException x) { // Ignore } try { NioEndpoint.NioSocketWrapper att = (NioEndpoint.NioSocketWrapper) channel .getAttachment(); if (att == null) { throw new IOException("Key must be cancelled."); } nRead = pool.read(to, channel, selector, att.getReadTimeout()); } finally { if (selector != null) { pool.put(selector); } } } else { nRead = channel.read(to); if (nRead == -1) { throw new EOFException(); } } return nRead; } /** * * @param block Should the write be blocking or not? * @param from the ByteBuffer containing the data to be written * 数据刷新到页面。 * @throws IOException */ @Override protected void doWrite(boolean block, ByteBuffer from) throws IOException { long writeTimeout = getWriteTimeout(); Selector selector = null; try { selector = pool.get(); } catch (IOException x) { // Ignore } try { /** * 将数据返回给页面。 * {@link NioSelectorPool#write(java.nio.ByteBuffer, org.apache.tomcat.util.net.NioChannel, java.nio.channels.Selector, long, boolean)} */ pool.write(from, getSocket(), selector, writeTimeout, block); if (block) { // Make sure we are flushed do { if (getSocket().flush(true, selector, writeTimeout)) { break; } } while (true); } updateLastWrite(); } 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. } @Override public void registerReadInterest() { if (log.isDebugEnabled()) { log.debug(sm.getString("endpoint.debug.registerRead", this)); } getPoller().add(getSocket(), SelectionKey.OP_READ); } @Override public void registerWriteInterest() { if (log.isDebugEnabled()) { log.debug(sm.getString("endpoint.debug.registerWrite", this)); } getPoller().add(getSocket(), SelectionKey.OP_WRITE); } @Override public SendfileDataBase createSendfileData(String filename, long pos, long length) { return new SendfileData(filename, pos, length); } @Override public SendfileState processSendfile(SendfileDataBase sendfileData) { setSendfileData((SendfileData) sendfileData); SelectionKey key = getSocket().getIOChannel().keyFor( getSocket().getPoller().getSelector()); // Might as well do the first write on this thread return getSocket().getPoller().processSendfile(key, this, true); } @Override protected void populateRemoteAddr() { InetAddress inetAddr = getSocket().getIOChannel().socket().getInetAddress(); if (inetAddr != null) { remoteAddr = inetAddr.getHostAddress(); } } @Override protected void populateRemoteHost() { InetAddress inetAddr = getSocket().getIOChannel().socket().getInetAddress(); if (inetAddr != null) { remoteHost = inetAddr.getHostName(); if (remoteAddr == null) { remoteAddr = inetAddr.getHostAddress(); } } } @Override protected void populateRemotePort() { remotePort = getSocket().getIOChannel().socket().getPort(); } @Override protected void populateLocalName() { InetAddress inetAddr = getSocket().getIOChannel().socket().getLocalAddress(); if (inetAddr != null) { localName = inetAddr.getHostName(); } } @Override protected void populateLocalAddr() { InetAddress inetAddr = getSocket().getIOChannel().socket().getLocalAddress(); if (inetAddr != null) { localAddr = inetAddr.getHostAddress(); } } @Override protected void populateLocalPort() { localPort = getSocket().getIOChannel().socket().getLocalPort(); } /** * {@inheritDoc} * @param clientCertProvider Ignored for this implementation */ @Override public SSLSupport getSslSupport(String clientCertProvider) { if (getSocket() instanceof SecureNioChannel) { SecureNioChannel ch = (SecureNioChannel) getSocket(); SSLEngine sslEngine = ch.getSslEngine(); if (sslEngine != null) { SSLSession session = sslEngine.getSession(); return ((NioEndpoint) getEndpoint()).getSslImplementation().getSSLSupport(session); } } return null; } @Override public void doClientAuth(SSLSupport sslSupport) throws IOException { SecureNioChannel sslChannel = (SecureNioChannel) getSocket(); SSLEngine engine = sslChannel.getSslEngine(); if (!engine.getNeedClientAuth()) { // Need to re-negotiate SSL connection engine.setNeedClientAuth(true); sslChannel.rehandshake(getEndpoint().getConnectionTimeout()); ((JSSESupport) sslSupport).setSession(engine.getSession()); } } @Override public void setAppReadBufHandler(ApplicationBufferHandler handler) { getSocket().setAppReadBufHandler(handler); } } // ---------------------------------------------- SocketProcessor Inner Class /** * This class is the equivalent of the Worker, but will simply use in an * external Executor thread pool. * * Processor 用于将 Endpoint 接收到的 Socket 封装成 Request。 * Tomcat自定义的I/O密集线程池就是用于在这里工作。这里的doRun就是任务类型。 * 线程池调用的任务处理者.(重点): * 1.拿到NioEndpoint的ConnectionHandler. * 2.ConnectionHandler使用Http11Processor去处理NioSocketWrapper. */ protected class SocketProcessor extends SocketProcessorBase<NioChannel> { public SocketProcessor(SocketWrapperBase<NioChannel> socketWrapper, SocketEvent event) { super(socketWrapper, event); } /** * 所以我们能看到,线程池中线程大部分都在等待I/O操作。 * 故此线程池应该被优化。Tomcat就在JDK基础上进行了优化。 * 1.握手,建立对应链接。 * 2.调用对应的连接器去处理此类请求。{@link Http11Processor#service(org.apache.tomcat.util.net.SocketWrapperBase)} */ @Override protected void doRun() { NioChannel socket = socketWrapper.getSocket(); SelectionKey key = socket.getIOChannel().keyFor(socket.getPoller().getSelector()); try { //握手? int handshake = -1; try { if (key != null) { if (socket.isHandshakeComplete()) { // No TLS handshaking required. Let the handler // process this socket / event combination. handshake = 0; } else if (event == SocketEvent.STOP || event == SocketEvent.DISCONNECT || event == SocketEvent.ERROR) { // Unable to complete the TLS handshake. Treat it as // if the handshake failed. handshake = -1; } else { handshake = socket.handshake(key.isReadable(), key.isWritable()); // The handshake process reads/writes from/to the // socket. status may therefore be OPEN_WRITE once // the handshake completes. However, the handshake // happens when the socket is opened so the status // must always be OPEN_READ after it completes. It // is OK to always set this as it is only used if // the handshake completes. event = SocketEvent.OPEN_READ; } } } catch (IOException x) { handshake = -1; if (log.isDebugEnabled()) log.debug("Error during SSL handshake",x); } catch (CancelledKeyException ckx) { handshake = -1; } /** * 握手成功? */ if (handshake == 0) { SocketState state = SocketState.OPEN; // Process the request from this socket if (event == null) { /** * 处理关键点 ConnectionHandler调用处理方法. * {@link AbstractProtocol.ConnectionHandler#process(org.apache.tomcat.util.net.SocketWrapperBase, org.apache.tomcat.util.net.SocketEvent)} */ state = getHandler().process(socketWrapper, SocketEvent.OPEN_READ); } else { state = getHandler().process(socketWrapper, event); } if (state == SocketState.CLOSED) { close(socket, key); } } else if (handshake == -1 ) { getHandler().process(socketWrapper, SocketEvent.CONNECT_FAIL); close(socket, key); } else if (handshake == SelectionKey.OP_READ){ socketWrapper.registerReadInterest(); } else if (handshake == SelectionKey.OP_WRITE){ socketWrapper.registerWriteInterest(); } } catch (CancelledKeyException cx) { socket.getPoller().cancelledKey(key); } catch (VirtualMachineError vme) { ExceptionUtils.handleThrowable(vme); } catch (Throwable t) { log.error("", t); socket.getPoller().cancelledKey(key); } finally { socketWrapper = null; event = null; //return to cache if (running && !paused) { processorCache.push(this); } } } } // ----------------------------------------------- SendfileData Inner Class /** * SendfileData class. */ public static class SendfileData extends SendfileDataBase { public SendfileData(String filename, long pos, long length) { super(filename, pos, length); } protected volatile FileChannel fchannel; } }