Tomcat源码学习笔记 - Connector组件(一)

Connector组件

Tomcat作为一款web容器,响应处理请求,需要与底层数据做交互,而Connector组件就是Service服务与Socket套接字之间的桥梁。Coyote框架是Tomcat默认的Connector,在org.apache.coyote包下,当然我们也可以自己实现自定义的Connector适配。

Connector数据结构

关于Connector,有两个非常重要的接口,ProtocolHandler和Adapter,前者用于处理请求,而后者则是Connector与Container容器之间的一个连接器。

protocolHandler是Connector的一个主要属性,是Coyote用来处理协议的主要接口。

public class Connector extends LifecycleMBeanBase  {
    /* 协议Handler */
    protected final ProtocolHandler protocolHandler;
    public Connector() {
        // 默认使用nio
        this("org.apache.coyote.http11.Http11NioProtocol");
    }

    public Connector(String protocol) { // 传入协议名称
        boolean aprConnector = AprLifecycleListener.isAprAvailable() &&
                AprLifecycleListener.getUseAprConnector(); // 是否开启Apr

        if ("HTTP/1.1".equals(protocol) || protocol == null) { // HTTP/1.1
            if (aprConnector) {
                protocolHandlerClassName = "org.apache.coyote.http11.Http11AprProtocol";
            } else {
                protocolHandlerClassName = "org.apache.coyote.http11.Http11NioProtocol";
            }
        } else if ("AJP/1.3".equals(protocol)) { // AJP/1.3
            if (aprConnector) {
                protocolHandlerClassName = "org.apache.coyote.ajp.AjpAprProtocol";
            } else {
                protocolHandlerClassName = "org.apache.coyote.ajp.AjpNioProtocol";
            }
        } else {
            protocolHandlerClassName = protocol; // 其他自定义协议实现
        }
        ProtocolHandler p = null;
        try {
            // 反射获取协议处理类
            Class<?> clazz = Class.forName(protocolHandlerClassName);
            // 实例化
            p = (ProtocolHandler) clazz.getConstructor().newInstance();
        } catch (Exception e) {
            log.error(sm.getString(
                    "coyoteConnector.protocolHandlerInstantiationFailed"), e);
        } finally {
            this.protocolHandler = p;
        }
        ...
    }
}

下图是protocolHandler在不同的IO处理及不同协议下的各种实现。

可以看到Coyote默认支持HTTP/1.1、HTTP/2、AJP协议,AJP协议是面向包的协议,因为虽然Tomcat可以作为独立的java web容器,但是它对静态资源的处理速度远不如其他专业的HTTP服务器比如Nginx,所以通常在实际应用中需要与其他HTTP服务器集成,而这个集成由AJP完成,默认监听8009端口。

另外Coyote默认支持NIO、NIO2(AIO)、APR三种I/O处理方式(本文Tomcat版本为9.0.17,Tomcat9删除了BIO的默认支持),APR表示Apache可移植运行库,是Aapche Http服务器的支持库,开启这个模式Tomcat将以JNI的形式调用Apache HTTP服务器的核心链接库来处理文件或网络传输操作,从操作系统级别解决异步IO问题,大幅度的提高服务器的处理和响应性能,是Tomcat运行高并发应用的首选。

1

那么以HTTP/1.1、Tomcat默认使用的nio处理方式为例,先来看看这个Http11NioProtocol,它的唯一构造方法,传入了一个的NioEndpoint对象。

public Http11NioProtocol() {
    super(new NioEndpoint());
}

public AbstractProtocol(AbstractEndpoint<S,?> endpoint) {
    // 每个endpoint对应一种IO策略
    this.endpoint = endpoint;
    // 设置一些默认的属性
    setConnectionLinger(Constants.DEFAULT_CONNECTION_LINGER);
    setTcpNoDelay(Constants.DEFAULT_TCP_NO_DELAY);
}
NioEndpoint

这个NioEndpoint作为Tomcat NIO的IO处理策略,主要提供工作线程和线程池:

  • Socket接收者Acceptor
  • Socket轮询者Poller
  • 工作线程池
NioEndPoint数据结构

从NioEndpoint和其上层的抽象类中可以了解到一些比较重要的属性。

public class NioEndpoint extends AbstractJsseEndpoint<NioChannel, SocketChannel> {
    /* 线程安全的Selector池 */
    private NioSelectorPool selectorPool = new NioSelectorPool();
    /* 轮询事件的缓存栈 */
    private SynchronizedStack<PollerEvent> eventCache;
    /* NioChannel的缓存栈,NioChannel对SocketChannel封装,使SSL与非SSL对外提供相同的处理方式 */
    private SynchronizedStack<NioChannel> nioChannels;
    /* 轮询线程的优先级 */
    private int pollerThreadPriority = Thread.NORM_PRIORITY;
    /* 轮询线程数量,2与JVM可利用线程中取小值 */
    private int pollerThreadCount =                          Math.min(2,Runtime.getRuntime().availableProcessors());
    /* 轮询线程池 */
    private Poller[] pollers = null;
    ...
}
public abstract class AbstractEndpoint<S,U> {
    /* Endpoint的运行状态 */
    protected volatile boolean running = false;
    /* Endpoint暂停时设置为true */
    protected volatile boolean paused = false;
    /* 标记使用的是否为默认线程池 */
    protected volatile boolean internalExecutor = true;
    /* 流量控制阀门,Socket连接数限制,达到阈值之后放入FIFO队列 */
    private volatile LimitLatch connectionLimitLatch = null;
    /* 代表在Server.xml中可设置的一些Socket相关属性 */
    protected final SocketProperties socketProperties = new SocketProperties();
    /* 接收线程,接收连接并交给工作线程 */
    protected List<Acceptor<U>> acceptors;
    /* Socket处理对象的缓存栈 */
    protected SynchronizedStack<SocketProcessorBase<S>> processorCache;
    /* 接收线程数量,默认为1 */
    protected int acceptorThreadCount = 1;
    /* 接收线程的优先级 */
    protected int acceptorThreadPriority = Thread.NORM_PRIORITY;
    /* 最大连接数nio模式默认10000,Apr模式默认8*1024 */
    private int maxConnections = 10000;
    /* keepAlive时间,没有设置则使用soTimeout */
    private Integer keepAliveTimeout = null;
    /* 是否开启ssl */
    private boolean SSLEnabled = false;
    /* request的keepAlive时间 */
    private int maxKeepAliveRequests = 100;
    /* ConnectionHandler */
    private Handler<S> handler = null;
    /* 用于存放传递给子组件的信息 */
    protected HashMap<String, Object> attributes = new HashMap<>();
}

主要包含LimitLatch、Acceptor、Poller、SocketProcessor、Excutor5个部分:

  • LimitLatch是一个连接控制器,负责连接限制,nio模式下默认10000,达到阈值则拒绝连接请求。
  • Acceptor负责接收请求,默认由1个线程负责,将请求的事件注册到事件列表中。
  • Poller负责轮询上述产生的事件,将就绪的事件生成SokcetProcessor,交给Excutor去执行。
  • SokcetProcessor里面的doRun方法,封装了Socket的读写,完成Container调用逻辑。
  • Excutor用来执行Poller创建的SokcetProcessor,线程池大小Connector节点配置的maxThreads决定。

看到Endpoint的属性中出现了很多SynchronizedStack,这个数据结构为Tomcat量身定做,是ConcurrentLinkedQueue一个GC-free的轻量级替代方案,提供扩容方案,最大128,但没有提供减少容量的方法。减少容量必然带来数组对象的回收,适用于数据量比较固定的场景,另外这个数据结构本身由数组维护,减少了维护节点的开销。

public class SynchronizedStack<T> {

    public static final int DEFAULT_SIZE = 128; // 上限128
    private static final int DEFAULT_LIMIT = -1;
    private int size;
    private final int limit;
    private int index = -1;
    private Object[] stack; // 由一个数组维护

    public SynchronizedStack() {
        this(DEFAULT_SIZE, DEFAULT_LIMIT); // 初始化数组大小
    }
    public SynchronizedStack(int size, int limit) {
        if (limit > -1 && size > limit) {
            this.size = limit;
        } else {
            this.size = size;
        }
        this.limit = limit;
        stack = new Object[size];
    }
    public synchronized boolean push(T obj) { // 放入数据
        index++;
        if (index == size) { // 达到数组最大值,若小于上限值则扩容,否则直接返回放入失败
            if (limit == -1 || size < limit) {
                expand();
            } else {
                index--;
                return false;
            }
        }
        stack[index] = obj;
        return true;
    }

    @SuppressWarnings("unchecked")
    public synchronized T pop() {
        if (index == -1) { // 没有数据返回null
            return null;
        }
        T result = (T) stack[index]; // 返回最后一个数据
        stack[index--] = null;
        return result;
    }

    public synchronized void clear() { // 清空stack
        if (index > -1) {
            for (int i = 0; i < index + 1; i++) {
                stack[i] = null;
            }
        }
        index = -1;
    }

    private void expand() { // 扩容
        int newSize = size * 2;
        if (limit != -1 && newSize > limit) {
            newSize = limit;
        }
        Object[] newStack = new Object[newSize];
        // System.arraycopy是对内存直接进行复制,减少了for循环过程中的寻址时间,从而提高了效能
        System.arraycopy(stack, 0, newStack, 0, size);
        // 这里是唯一产生垃圾的地方,旧数组对象被GC,注意只是数组对象,而不是数组内容对象
        stack = newStack;
        size = newSize;
    }
}

而另一个SynchronizedQueue则是一个GC-free的容器,只不过这个是一个FIFO无界容器。

NioEndPoint启动

来看下NioEndPoint的startInternal方法,这个方法为Nio Endpoint初始化状态,并创建接收和轮询线程。

public void startInternal() throws Exception {

    if (!running) {
        // 标记运行状态:running
        running = true;
        // 将暂停标志位掷为false
        paused = false;
        // 创建三个缓存栈,Socket处理缓存栈、轮询事件缓存栈、SocketChannel包装类缓存栈
        processorCache = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,
                socketProperties.getProcessorCache());
        eventCache = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,
                socketProperties.getEventCache());
        nioChannels = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,
                socketProperties.getBufferPool());
        // 先获取外部线程池(用户自定义),没有则创建一个内置默认的线程池
        if (getExecutor() == null) {
            createExecutor();
        }
        // 初始化可重入锁LimitLatch,用于限制最大连接数
        initializeConnectionLatch();
        // 根据pollerThreadCount,初始化轮询线程池
        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); // 设置为守护线程
            pollerThread.start(); // 启动线程
        }
        // 创建接收线程
        startAcceptorThreads();
    }
}

protected void startAcceptorThreads() {
    int count = getAcceptorThreadCount(); // 接收线程数量
    acceptors = new ArrayList<>(count);

    for (int i = 0; i < count; i++) {
        Acceptor<U> acceptor = new Acceptor<>(this); 
        String threadName = getName() + "-Acceptor-" + i;
        acceptor.setThreadName(threadName);
        acceptors.add(acceptor);
        // 创建接收线程
        Thread t = new Thread(acceptor, threadName); 
        // 设置线程优先级
        t.setPriority(getAcceptorThreadPriority());
        // 设置接收线程是否为守护,默认为true,可以设为非守护线程
        t.setDaemon(getDaemon());
        // 启动线程
        t.start();
    }
}

这个方法比较简单,我们从调用栈中可以看到,这个方法会在Connector的startInternal方法中被调用,而最终在Catilina启动时中被调用。

2
Acceptor和Poller

Acceptor和Poller是一个典型的生产者-消费者模式。

Acceptor
public class Acceptor<U> implements Runnable {    
    private static final int INITIAL_ERROR_DELAY = 50;
    private static final int MAX_ERROR_DELAY = 1600;
    
    @Override
    public void run() {
        int errorDelay = 0;
        // 循环,直到接收到一个关闭命令
        while (endpoint.isRunning()) {  
            // 循环,如果Endpoint被暂停则循环sleep
            while (endpoint.isPaused() && endpoint.isRunning()) { 
                state = AcceptorState.PAUSED;
                try {
                    Thread.sleep(50); // 50毫秒拉取一次endpoint运行状态
                } catch (InterruptedException e) {
                    // Ignore
                }
            }
            if (!endpoint.isRunning()) {
                break;
            }
            state = AcceptorState.RUNNING;

            try {
                endpoint.countUpOrAwaitConnection(); // 如果达到最大连接数则等待
                if (endpoint.isPaused()) { // 如果等到连接数限制的时候endpoint被暂停,则停止接收
                    continue;
                }
                U socket = null;
                try {
                    socket = endpoint.serverSocketAccept(); // 创建一个socketChannel接收连接
                } catch (Exception ioe) {
                    endpoint.countDownConnection();
                    if (endpoint.isRunning()) {
                        errorDelay = handleExceptionWithDelay(errorDelay); // 延迟异常处理
                        throw ioe; // 重新扔出异常给c1处捕获
                    } else {
                        break;
                    }
                }
                errorDelay = 0; // 成功接收之后重置延时处理异常时间
                if (endpoint.isRunning() && !endpoint.isPaused()) {
                    // setSocketOptions()将Socket传给相应processor处理
                    if (!endpoint.setSocketOptions(socket)) {
                        endpoint.closeSocket(socket);
                    }
                } else {
                    endpoint.destroySocket(socket); // 否则destroy掉该socketChannel
                }
            } catch (Throwable t) { // c1
                ExceptionUtils.handleThrowable(t); // 处理延迟异常
                String msg = sm.getString("endpoint.accept.fail");
                if (t instanceof Error) {
                    ... // 日志记录
                }
            }
        }
        state = AcceptorState.ENDED; // 标记状态为ENDED
    }
       
        protected int handleExceptionWithDelay(int currentErrorDelay) {
        if (currentErrorDelay > 0) {
            try {
                Thread.sleep(currentErrorDelay);
            } catch (InterruptedException e) {
                // Ignore
            }
        }
        // 第一次异常不延迟处理,直接在c2处返回,默认第一次返回50毫秒,后续每次加倍,但最大1.6秒
        if (currentErrorDelay == 0) {
            return INITIAL_ERROR_DELAY; // c2
        } else if (currentErrorDelay < MAX_ERROR_DELAY) {
            return currentErrorDelay * 2;
        } else {
            return MAX_ERROR_DELAY;
        }
    }
}

Acceptor在接收请求时非常有意思的一点是采用了一个延时的异常处理机制。在catch一个异常时,第一次不做延迟处理,直接re-throw给下一个catch日志记录,第二次由于currentErrorDelay已经变成50毫秒,该线程睡50毫秒之后继续执行,后续还抛异常则double延迟时间,最大到1.6s,直到正常接收重置该时间。这个机制有效的防止了Acceptor线程不断循环同时抛错,导致cpu资源占用飙升,并且大量重复日志记录,在一些情况下有显著效果,比如:Linux,一个文件的ulimit达到上限,这时线程不断访问该文件,导致连续报错。

而setSocketOptions方法则是处理Acceptor传过来的指定连接。

    protected boolean setSocketOptions(SocketChannel socket) {
        try {
            // 不同于BIO、ARP模式,NIO会采用轮询的方式
            socket.configureBlocking(false);
            Socket sock = socket.socket();
            socketProperties.setProperties(sock); // 设置Socket属性

            NioChannel channel = nioChannels.pop(); // 取出一个NioChannel
            if (channel == null) {
                // 初始化一个BufferHandler的基本属性
                SocketBufferHandler bufhandler = new SocketBufferHandler(
                        socketProperties.getAppReadBufSize(),
                        socketProperties.getAppWriteBufSize(),
                        socketProperties.getDirectBuffer()); 
                if (isSSLEnabled()) { // 根据是不是SSL包装成不同NioChannel
                    channel = new SecureNioChannel(socket, bufhandler, selectorPool, this);
                } else {
                    channel = new NioChannel(socket, bufhandler);
                }
            } else {
                channel.setIOChannel(socket); // 如果缓存中有Niochannel对象可用,则直接复用
                channel.reset();
            }
            getPoller0().register(channel); // 获取可用的循环器,并将nioChannel注册到该Poll上
        } catch (Throwable t) {
            ... // 异常处理
            return false;
        }
        return true;
    }
    // 注册过程
    public void register(final NioChannel socket) {
        socket.setPoller(this);
        // 将NioChannel包装起来
        NioSocketWrapper ka = new NioSocketWrapper(socket, NioEndpoint.this);
        socket.setSocketWrapper(ka);
        ka.setPoller(this);
        ka.setReadTimeout(getConnectionTimeout()); // 设置基本属性
        ka.setWriteTimeout(getConnectionTimeout());
        ka.setKeepAliveLeft(NioEndpoint.this.getMaxKeepAliveRequests());
        ka.setSecure(isSSLEnabled());
        PollerEvent r = eventCache.pop(); // 取出一个PollerEvent
        ka.interestOps(SelectionKey.OP_READ);// 注册读就绪事件
        if (r == null) r = new PollerEvent(socket, ka, OP_REGISTER); // 兴趣事件OP_REGISTER
        else r.reset(socket, ka, OP_REGISTER); // PollerEvent不为空则覆盖
        // 调用Poller的addEvent方法,将该事件扔到Poller的
        // SynchronizedQueue<PollerEvent> events这个待处理队列中
        addEvent(r);
    }
    private void addEvent(PollerEvent event) {
        events.offer(event);
        if (wakeupCounter.incrementAndGet() == 0) selector.wakeup(); // 唤醒selector
    }

调用selector的wakeup()方法是为了唤醒selector,从Selector的选择方式来说,分为selectNow()、select()、select(timeout),第一个是非阻塞的,跟wakeup没有关系,但是后两者会等待,select一直等待,select(timeout)设置等待时间。Selector阻塞式选择除了超时、中断之外,只有等到感兴趣事件ready时才会返回,那么这个wakeup()就是构造这样的一个ready场景。

现在,Acceptor已经完成了任务,接下去便是由Poller去处理事件了。

Poller
public class Poller implements Runnable {
    /* nio的多路复用选择器 */
    private Selector selector;
    /* 轮询事件的同步队列 */
    private final SynchronizedQueue<PollerEvent> events =
        new SynchronizedQueue<>();
    /* 线程开启状态标记位 */
    private volatile boolean close = false;
    /* 优化过期处理机制 */
    private long nextExpiration = 0;
    /* Selector唤醒计数 */
    private AtomicLong wakeupCounter = new AtomicLong(0);
    /* 就绪通道的数量 */
    private volatile int keyCount = 0;
    
    @Override
    public void run() {
        while (true) { // 循环直到destroy()被调用
            boolean hasEvents = false; // 是否有事件标识
            try {
                if (!close) {
                    // 将events队列,将每个事件中的通道感兴趣的事件注册到Selector中
                    hasEvents = events(); 
                    // 在
                    if (wakeupCounter.getAndSet(-1) > 0) {
                    //如果走到了这里,代表已经有就绪的IO通道
                    //调用非阻塞的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(sm.getString("endpoint.nio.selectorLoopError"), x);
                continue;
            }
            // 如果上面select方法超时,或者被唤醒,则将events队列中的通道注册到Selector上。
            if (keyCount == 0) hasEvents = (hasEvents | events());

            Iterator<SelectionKey> iterator =
                keyCount > 0 ? selector.selectedKeys().iterator() : null;
            // 遍历SelectionKey,并调度活动事件,这个过程跟普通的nio类似
            while (iterator != null && iterator.hasNext()) {
                SelectionKey sk = iterator.next();
                // 获取NioSocketWrapper,如果获取为空,说明其他线程调用了cancelKey()
                // 则去除这个Key,否则调用processKey()
                NioSocketWrapper attachment = (NioSocketWrapper) sk.attachment();
                if (attachment == null) {
                    iterator.remove();
                } else {
                    iterator.remove();
                    processKey(sk, attachment); // 处理SelectKey
                }
            }
            timeout(keyCount, hasEvents);
        }
        getStopLatch().countDown();
    }
}

processKey()这个方法主要通过调用processSocket()方法创建一个SocketProcessor,然后丢到Tomcat线程池中去执行。每个Endpoint都有自己的SocketProcessor实现,从Endpoint的属性中可以看到,这个Processor也有缓存机制。
总结一下Poller所做的事:遍历PollerEvents队列,将每个事件中的通道感兴趣的事件注册到Selector,当事件就绪时,创建一个SocketProcessor或者从缓存中取出一个SocketProcessor,然后放到线程池执行或者直接执行它的run方法执行。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,639评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,277评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,221评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,474评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,570评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,816评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,957评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,718评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,176评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,511评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,646评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,322评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,934评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,755评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,987评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,358评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,514评论 2 348

推荐阅读更多精彩内容

  • 1.Tomcat总体架构 Tomcat有Connector和Container两大核心组件,Connector组件...
    monkey01阅读 11,948评论 6 23
  • 前言 在使用tomcat时,经常会遇到连接数、线程数之类的配置问题,要真正理解这些概念,必须先了解Tomcat的连...
    shaolin79阅读 769评论 0 0
  • 前言 在使用tomcat时,经常会遇到连接数、线程数之类的配置问题,要真正理解这些概念,必须先了解Tomcat的连...
    Java大生阅读 1,068评论 0 2
  • 一个人,倘若懂得生命的珍贵,也许才能珍惜。 一个人,倘若识得生命的无常,也许就能从容。 一个人,倘若勘破生命的本质...
    慕槐阅读 425评论 0 3
  • Kafka简介 Apache Kafka是分布式发布-订阅消息系统。它最初由LinkedIn公司开发,之后成为Ap...
    Radom7阅读 6,323评论 0 23