Jason_Geng

Jason_Geng 查看完整档案

上海编辑东华大学  |  工业设计 编辑新美大  |  JAVA 编辑 github.com/jasonGeng88/blog 编辑
编辑

趁年轻,折腾!

个人动态

Jason_Geng 收藏了文章 · 2019-04-24

Spring-MVC理解之一:应用上下文webApplicationContext

一、先说ServletContext

  javaee标准规定了,servlet容器需要在应用项目启动时,给应用项目初始化一个ServletContext作为公共环境容器存放公共信息。ServletContext中的信息都是由容器提供的。

举例:

通过自定义contextListener获取web.xml中配置的参数
1.容器启动时,找到配置文件中的context-param作为键值对放到ServletContext中
2.然后找到listener,容器调用它的contextInitialized(ServletContextEvent event)方法,执行其中的操作
例如:在web.xml中配置
复制代码

<context-param>
   <param-name>key</param-name>
   <param-value>value123</param-value>
</context-param>
<listener> 
   <listener-class>com.brolanda.contextlistener.listener.ContextListenerTest</listener-class>
</listener>

配置好之后,在该类中获取对应的参数信息
复制代码

package com.brolanda.contextlistener.listener;

import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

public class ContextListenerTest implements ServletContextListener {
    
    public void contextDestroyed(ServletContextEvent event) {
        System.out.println("*************destroy ContextListener*************");
    }
    
    @SuppressWarnings("unused")
    public void contextInitialized(ServletContextEvent event) {
        System.out.println("*************init ContextListener*************");
        ServletContext servletContext = event.getServletContext();
        System.out.println("key:"+servletContext.getInitParameter("key"));
    }
    
}

复制代码
执行流程:

  web.xml在<context-param></context-param>标签中声明应用范围内的初始化参数

1.启动一个WEB项目的时候,容器(如:Tomcat)会去读它的配置文件web.xml.读两个节点: <listener></listener> 和 <context-param></context-param>
2.紧接着,容器创建一个ServletContext(上下文)。在该应用内全局共享。
3.容器将<context-param></context-param>转化为键值对,并交给ServletContext.

4.容器创建<listener></listener>中的类实例,即创建监听.该监听器必须实现自ServletContextListener接口
5.在监听中会有contextInitialized(ServletContextEvent event)初始化方法
在这个方法中获得ServletContext = ServletContextEvent.getServletContext();

        “context-param的值” = ServletContext.getInitParameter("context-param的键");

6.得到这个context-param的值之后,你就可以做一些操作了.注意,这个时候你的WEB项目还没有完全启动完成.这个动作会比所有的Servlet都要早.换句话说,这个时候,你对<context-param>中的键值做的操作,将在你的WEB项目完全启动之前被执行.

web.xml中可以定义两种参数:

一个是全局参数(ServletContext),通过<context-param></context-param>
一个是servlet参数,通过在servlet中声明

<init-param>
 <param-name>param1</param-name>
   <param-value>avalible in servlet init()</param-value>   
     </init-param> 
第一种参数在servlet里面可以通过getServletContext().getInitParameter("context/param")得到

第二种参数只能在servlet的init()方法中通过this.getInitParameter("param1")取得

二、spring上下文容器配置

  spring为我们提供了实现ServletContextListener接口的上下文初始化监听器:org.springframework.web.context.ContextLoaderListener

  spring为我们提供的IOC容器,需要我们指定容器的配置文件,然后由该监听器初始化并创建该容器。要求你指定配置文件的地址及文件名称,一定要使用:contextConfigLocation作为参数名称。

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/applicationContext.xml,/WEB-INF/action-servlet.xml,/WEB-INF/jason-servlet.xml</param-value>
</context-param>
<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

复制代码
该监听器,默认读取/WEB-INF/下的applicationContext.xml文件。但是通过context-param指定配置文件路径后,便会去你指定的路径下读取对应的配置文件,并进行初始化。

三、spring上下文容器配置后,初始化了什么?

  既然,ServletContext是由Servlet容器初始化的,那spring的ContextLoaderListener又做了什么初始化呢?

    1、servlet容器启动,为应用创建一个“全局上下文环境”:ServletContext
    2、容器调用web.xml中配置的contextLoaderListener,初始化WebApplicationContext上下文环境(即IOC容器),加载context-param指定的配置文件信息到IOC容器中。WebApplicationContext在ServletContext中以键值对的形式保存
    3、容器初始化web.xml中配置的servlet,为其初始化自己的上下文信息servletContext,并加载其设置的配置信息到该上下文中。将WebApplicationContext设置为它的父容器。
    4、此后的所有servlet的初始化都按照3步中方式创建,初始化自己的上下文环境,将WebApplicationContext设置为自己的父上下文环境。

clipboard.png

对于作用范围而言,在DispatcherServlet中可以引用由ContextLoaderListener所创建的ApplicationContext中的内容,而反过来不行。

   当Spring在执行ApplicationContext的getBean时,如果在自己context中找不到对应的bean,则会在父ApplicationContext中去找。这也解释了为什么我们可以在DispatcherServlet中获取到由ContextLoaderListener对应的ApplicationContext中的bean。

四、spring配置时:<context:exclude-filter>的使用原因,为什么在applicationContext.xml中排除controller,而在spring-mvc.xml中incloud这个controller

既然知道了spring的启动流程,那么web容器初始化webApplicationContext时作为公共的上下文环境,只需要将service、dao等的配置信息在这里加载,而servlet自己的上下文环境信息不需要加载。故,在applicationContext.xml中将@Controller注释的组件排除在外,而在dispatcherServlet加载的配置文件中将@Controller注释的组件加载进来,方便dispatcherServlet进行控制和查找。故,配置如下:

applicationContext.mxl中:

 <context:component-scan base-package="com.linkage.edumanage">
      <context:exclude-filter expression="org.springframework.stereotype.Controller"    type="annotation" /> 
 </context:component-scan>
 
spring-mvc.xml中:
  <context:component-scan base-package="com.brolanda.cloud"   use-default-filters="false"> 
      <context:include-filter expression="org.springframework.stereotype.Controller"    type="annotation" /> 
 </context:component-scan>

原文转至 http://www.cnblogs.com/brolan...

查看原文

Jason_Geng 赞了文章 · 2019-04-24

Spring-MVC理解之一:应用上下文webApplicationContext

一、先说ServletContext

  javaee标准规定了,servlet容器需要在应用项目启动时,给应用项目初始化一个ServletContext作为公共环境容器存放公共信息。ServletContext中的信息都是由容器提供的。

举例:

通过自定义contextListener获取web.xml中配置的参数
1.容器启动时,找到配置文件中的context-param作为键值对放到ServletContext中
2.然后找到listener,容器调用它的contextInitialized(ServletContextEvent event)方法,执行其中的操作
例如:在web.xml中配置
复制代码

<context-param>
   <param-name>key</param-name>
   <param-value>value123</param-value>
</context-param>
<listener> 
   <listener-class>com.brolanda.contextlistener.listener.ContextListenerTest</listener-class>
</listener>

配置好之后,在该类中获取对应的参数信息
复制代码

package com.brolanda.contextlistener.listener;

import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

public class ContextListenerTest implements ServletContextListener {
    
    public void contextDestroyed(ServletContextEvent event) {
        System.out.println("*************destroy ContextListener*************");
    }
    
    @SuppressWarnings("unused")
    public void contextInitialized(ServletContextEvent event) {
        System.out.println("*************init ContextListener*************");
        ServletContext servletContext = event.getServletContext();
        System.out.println("key:"+servletContext.getInitParameter("key"));
    }
    
}

复制代码
执行流程:

  web.xml在<context-param></context-param>标签中声明应用范围内的初始化参数

1.启动一个WEB项目的时候,容器(如:Tomcat)会去读它的配置文件web.xml.读两个节点: <listener></listener> 和 <context-param></context-param>
2.紧接着,容器创建一个ServletContext(上下文)。在该应用内全局共享。
3.容器将<context-param></context-param>转化为键值对,并交给ServletContext.

4.容器创建<listener></listener>中的类实例,即创建监听.该监听器必须实现自ServletContextListener接口
5.在监听中会有contextInitialized(ServletContextEvent event)初始化方法
在这个方法中获得ServletContext = ServletContextEvent.getServletContext();

        “context-param的值” = ServletContext.getInitParameter("context-param的键");

6.得到这个context-param的值之后,你就可以做一些操作了.注意,这个时候你的WEB项目还没有完全启动完成.这个动作会比所有的Servlet都要早.换句话说,这个时候,你对<context-param>中的键值做的操作,将在你的WEB项目完全启动之前被执行.

web.xml中可以定义两种参数:

一个是全局参数(ServletContext),通过<context-param></context-param>
一个是servlet参数,通过在servlet中声明

<init-param>
 <param-name>param1</param-name>
   <param-value>avalible in servlet init()</param-value>   
     </init-param> 
第一种参数在servlet里面可以通过getServletContext().getInitParameter("context/param")得到

第二种参数只能在servlet的init()方法中通过this.getInitParameter("param1")取得

二、spring上下文容器配置

  spring为我们提供了实现ServletContextListener接口的上下文初始化监听器:org.springframework.web.context.ContextLoaderListener

  spring为我们提供的IOC容器,需要我们指定容器的配置文件,然后由该监听器初始化并创建该容器。要求你指定配置文件的地址及文件名称,一定要使用:contextConfigLocation作为参数名称。

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/applicationContext.xml,/WEB-INF/action-servlet.xml,/WEB-INF/jason-servlet.xml</param-value>
</context-param>
<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

复制代码
该监听器,默认读取/WEB-INF/下的applicationContext.xml文件。但是通过context-param指定配置文件路径后,便会去你指定的路径下读取对应的配置文件,并进行初始化。

三、spring上下文容器配置后,初始化了什么?

  既然,ServletContext是由Servlet容器初始化的,那spring的ContextLoaderListener又做了什么初始化呢?

    1、servlet容器启动,为应用创建一个“全局上下文环境”:ServletContext
    2、容器调用web.xml中配置的contextLoaderListener,初始化WebApplicationContext上下文环境(即IOC容器),加载context-param指定的配置文件信息到IOC容器中。WebApplicationContext在ServletContext中以键值对的形式保存
    3、容器初始化web.xml中配置的servlet,为其初始化自己的上下文信息servletContext,并加载其设置的配置信息到该上下文中。将WebApplicationContext设置为它的父容器。
    4、此后的所有servlet的初始化都按照3步中方式创建,初始化自己的上下文环境,将WebApplicationContext设置为自己的父上下文环境。

clipboard.png

对于作用范围而言,在DispatcherServlet中可以引用由ContextLoaderListener所创建的ApplicationContext中的内容,而反过来不行。

   当Spring在执行ApplicationContext的getBean时,如果在自己context中找不到对应的bean,则会在父ApplicationContext中去找。这也解释了为什么我们可以在DispatcherServlet中获取到由ContextLoaderListener对应的ApplicationContext中的bean。

四、spring配置时:<context:exclude-filter>的使用原因,为什么在applicationContext.xml中排除controller,而在spring-mvc.xml中incloud这个controller

既然知道了spring的启动流程,那么web容器初始化webApplicationContext时作为公共的上下文环境,只需要将service、dao等的配置信息在这里加载,而servlet自己的上下文环境信息不需要加载。故,在applicationContext.xml中将@Controller注释的组件排除在外,而在dispatcherServlet加载的配置文件中将@Controller注释的组件加载进来,方便dispatcherServlet进行控制和查找。故,配置如下:

applicationContext.mxl中:

 <context:component-scan base-package="com.linkage.edumanage">
      <context:exclude-filter expression="org.springframework.stereotype.Controller"    type="annotation" /> 
 </context:component-scan>
 
spring-mvc.xml中:
  <context:component-scan base-package="com.brolanda.cloud"   use-default-filters="false"> 
      <context:include-filter expression="org.springframework.stereotype.Controller"    type="annotation" /> 
 </context:component-scan>

原文转至 http://www.cnblogs.com/brolan...

查看原文

赞 4 收藏 10 评论 4

Jason_Geng 赞了文章 · 2018-10-03

聊聊jdk httpclient的connect timeout异常

本文主要研究一下httpclient的connect timeout异常

实例代码

    @Test
    public void testConnectTimeout() throws IOException, InterruptedException {
        HttpClient client = HttpClient.newBuilder()
                .build();

        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://twitter.com"))
                .build();
        long start = System.currentTimeMillis();
        try{
            HttpResponse<String> result = client.send(request, HttpResponse.BodyHandlers.ofString());
            System.out.println(result.body());
        }finally {
            long cost = System.currentTimeMillis() - start;
            System.out.println("cost:"+cost);
        }
    }
异常日志如下:
cost:75814

java.net.ConnectException: Operation timed out

    at java.net.http/jdk.internal.net.http.HttpClientImpl.send(HttpClientImpl.java:561)
    at java.net.http/jdk.internal.net.http.HttpClientFacade.send(HttpClientFacade.java:119)
    at com.example.HttpClientTest.testConnectTimeout(HttpClientTest.java:464)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
    at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
    at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
    at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at com.intellij.rt.execution.application.AppMainV2.main(AppMainV2.java:131)
Caused by: java.net.ConnectException: Operation timed out
    at java.base/sun.nio.ch.SocketChannelImpl.checkConnect(Native Method)
    at java.base/sun.nio.ch.SocketChannelImpl.finishConnect(SocketChannelImpl.java:779)
    at java.net.http/jdk.internal.net.http.PlainHttpConnection$ConnectEvent.handle(PlainHttpConnection.java:128)
    at java.net.http/jdk.internal.net.http.HttpClientImpl$SelectorManager.handleEvent(HttpClientImpl.java:957)
    at java.net.http/jdk.internal.net.http.HttpClientImpl$SelectorManager.lambda$run$3(HttpClientImpl.java:912)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1540)
    at java.net.http/jdk.internal.net.http.HttpClientImpl$SelectorManager.run(HttpClientImpl.java:912)

Exchange.responseAsync

java.net.http/jdk/internal/net/http/Exchange.java

    public CompletableFuture<Response> responseAsync() {
        return responseAsyncImpl(null);
    }

    CompletableFuture<Response> responseAsyncImpl(HttpConnection connection) {
        SecurityException e = checkPermissions();
        if (e != null) {
            return MinimalFuture.failedFuture(e);
        } else {
            return responseAsyncImpl0(connection);
        }
    }

    CompletableFuture<Response> responseAsyncImpl0(HttpConnection connection) {
        Function<ExchangeImpl<T>, CompletableFuture<Response>> after407Check;
        bodyIgnored = null;
        if (request.expectContinue()) {
            request.addSystemHeader("Expect", "100-Continue");
            Log.logTrace("Sending Expect: 100-Continue");
            // wait for 100-Continue before sending body
            after407Check = this::expectContinue;
        } else {
            // send request body and proceed.
            after407Check = this::sendRequestBody;
        }
        // The ProxyAuthorizationRequired can be triggered either by
        // establishExchange (case of HTTP/2 SSL tunneling through HTTP/1.1 proxy
        // or by sendHeaderAsync (case of HTTP/1.1 SSL tunneling through HTTP/1.1 proxy
        // Therefore we handle it with a call to this checkFor407(...) after these
        // two places.
        Function<ExchangeImpl<T>, CompletableFuture<Response>> afterExch407Check =
                (ex) -> ex.sendHeadersAsync()
                        .handle((r,t) -> this.checkFor407(r, t, after407Check))
                        .thenCompose(Function.identity());
        return establishExchange(connection)
                .handle((r,t) -> this.checkFor407(r,t, afterExch407Check))
                .thenCompose(Function.identity());
    }

    // get/set the exchange impl, solving race condition issues with
    // potential concurrent calls to cancel() or cancel(IOException)
    private CompletableFuture<? extends ExchangeImpl<T>>
    establishExchange(HttpConnection connection) {
        if (debug.on()) {
            debug.log("establishing exchange for %s,%n\t proxy=%s",
                      request, request.proxy());
        }
        // check if we have been cancelled first.
        Throwable t = getCancelCause();
        checkCancelled();
        if (t != null) {
            return MinimalFuture.failedFuture(t);
        }

        CompletableFuture<? extends ExchangeImpl<T>> cf, res;
        cf = ExchangeImpl.get(this, connection);
        // We should probably use a VarHandle to get/set exchangeCF
        // instead - as we need CAS semantics.
        synchronized (this) { exchangeCF = cf; };
        res = cf.whenComplete((r,x) -> {
            synchronized(Exchange.this) {
                if (exchangeCF == cf) exchangeCF = null;
            }
        });
        checkCancelled();
        return res.thenCompose((eimpl) -> {
                    // recheck for cancelled, in case of race conditions
                    exchImpl = eimpl;
                    IOException tt = getCancelCause();
                    checkCancelled();
                    if (tt != null) {
                        return MinimalFuture.failedFuture(tt);
                    } else {
                        // Now we're good to go. Because exchImpl is no longer
                        // null cancel() will be able to propagate directly to
                        // the impl after this point ( if needed ).
                        return MinimalFuture.completedFuture(eimpl);
                    } });
    }
  • responseAsync最后调用ExchangeImpl.get(this, connection)

ExchangeImpl.get

java.net.http/jdk/internal/net/http/ExchangeImpl.java

    /**
     * Initiates a new exchange and assigns it to a connection if one exists
     * already. connection usually null.
     */
    static <U> CompletableFuture<? extends ExchangeImpl<U>>
    get(Exchange<U> exchange, HttpConnection connection)
    {
        if (exchange.version() == HTTP_1_1) {
            if (debug.on())
                debug.log("get: HTTP/1.1: new Http1Exchange");
            return createHttp1Exchange(exchange, connection);
        } else {
            Http2ClientImpl c2 = exchange.client().client2(); // #### improve
            HttpRequestImpl request = exchange.request();
            CompletableFuture<Http2Connection> c2f = c2.getConnectionFor(request, exchange);
            if (debug.on())
                debug.log("get: Trying to get HTTP/2 connection");
            return c2f.handle((h2c, t) -> createExchangeImpl(h2c, t, exchange, connection))
                    .thenCompose(Function.identity());
        }
    }
  • 这里调用Http2ClientImpl.getConnectionFor获取连接

Http2ClientImpl.getConnectionFor

java.net.http/jdk/internal/net/http/Http2ClientImpl.java

    /**
     * When HTTP/2 requested only. The following describes the aggregate behavior including the
     * calling code. In all cases, the HTTP2 connection cache
     * is checked first for a suitable connection and that is returned if available.
     * If not, a new connection is opened, except in https case when a previous negotiate failed.
     * In that case, we want to continue using http/1.1. When a connection is to be opened and
     * if multiple requests are sent in parallel then each will open a new connection.
     *
     * If negotiation/upgrade succeeds then
     * one connection will be put in the cache and the others will be closed
     * after the initial request completes (not strictly necessary for h2, only for h2c)
     *
     * If negotiate/upgrade fails, then any opened connections remain open (as http/1.1)
     * and will be used and cached in the http/1 cache. Note, this method handles the
     * https failure case only (by completing the CF with an ALPN exception, handled externally)
     * The h2c upgrade is handled externally also.
     *
     * Specific CF behavior of this method.
     * 1. completes with ALPN exception: h2 negotiate failed for first time. failure recorded.
     * 2. completes with other exception: failure not recorded. Caller must handle
     * 3. completes normally with null: no connection in cache for h2c or h2 failed previously
     * 4. completes normally with connection: h2 or h2c connection in cache. Use it.
     */
    CompletableFuture<Http2Connection> getConnectionFor(HttpRequestImpl req,
                                                        Exchange<?> exchange) {
        URI uri = req.uri();
        InetSocketAddress proxy = req.proxy();
        String key = Http2Connection.keyFor(uri, proxy);

        synchronized (this) {
            Http2Connection connection = connections.get(key);
            if (connection != null) {
                try {
                    if (connection.closed || !connection.reserveStream(true)) {
                        if (debug.on())
                            debug.log("removing found closed or closing connection: %s", connection);
                        deleteConnection(connection);
                    } else {
                        // fast path if connection already exists
                        if (debug.on())
                            debug.log("found connection in the pool: %s", connection);
                        return MinimalFuture.completedFuture(connection);
                    }
                } catch (IOException e) {
                    // thrown by connection.reserveStream()
                    return MinimalFuture.failedFuture(e);
                }
            }

            if (!req.secure() || failures.contains(key)) {
                // secure: negotiate failed before. Use http/1.1
                // !secure: no connection available in cache. Attempt upgrade
                if (debug.on()) debug.log("not found in connection pool");
                return MinimalFuture.completedFuture(null);
            }
        }
        return Http2Connection
                .createAsync(req, this, exchange)
                .whenComplete((conn, t) -> {
                    synchronized (Http2ClientImpl.this) {
                        if (conn != null) {
                            try {
                                conn.reserveStream(true);
                            } catch (IOException e) {
                                throw new UncheckedIOException(e); // shouldn't happen
                            }
                            offerConnection(conn);
                        } else {
                            Throwable cause = Utils.getCompletionCause(t);
                            if (cause instanceof Http2Connection.ALPNException)
                                failures.add(key);
                        }
                    }
                });
    }
  • 如果没有连接会新创建一个,走的是Http2Connection.createAsync

Http2Connection.createAsync

java.net.http/jdk/internal/net/http/Http2Connection.java

    // Requires TLS handshake. So, is really async
    static CompletableFuture<Http2Connection> createAsync(HttpRequestImpl request,
                                                          Http2ClientImpl h2client,
                                                          Exchange<?> exchange) {
        assert request.secure();
        AbstractAsyncSSLConnection connection = (AbstractAsyncSSLConnection)
        HttpConnection.getConnection(request.getAddress(),
                                     h2client.client(),
                                     request,
                                     HttpClient.Version.HTTP_2);

        // Expose the underlying connection to the exchange's aborter so it can
        // be closed if a timeout occurs.
        exchange.connectionAborter.connection(connection);

        return connection.connectAsync(exchange)
                  .thenCompose(unused -> connection.finishConnect())
                  .thenCompose(unused -> checkSSLConfig(connection))
                  .thenCompose(notused-> {
                      CompletableFuture<Http2Connection> cf = new MinimalFuture<>();
                      try {
                          Http2Connection hc = new Http2Connection(request, h2client, connection);
                          cf.complete(hc);
                      } catch (IOException e) {
                          cf.completeExceptionally(e);
                      }
                      return cf; } );
    }
  • 这里先是调用了HttpConnection.getConnection获取连接,然后调用connectAsync进行连接

AsyncSSLConnection

java.net.http/jdk/internal/net/http/AsyncSSLConnection.java

    @Override
    public CompletableFuture<Void> connectAsync(Exchange<?> exchange) {
        return plainConnection
                .connectAsync(exchange)
                .thenApply( unused -> {
                    // create the SSLTube wrapping the SocketTube, with the given engine
                    flow = new SSLTube(engine,
                                       client().theExecutor(),
                                       client().getSSLBufferSupplier()::recycle,
                                       plainConnection.getConnectionFlow());
                    return null; } );
    }
  • 这里委托给plainConnection.connectAsync

PlainHttpConnection.connectAsync

java.net.http/jdk/internal/net/http/PlainHttpConnection.java

    @Override
    public CompletableFuture<Void> connectAsync(Exchange<?> exchange) {
        CompletableFuture<Void> cf = new MinimalFuture<>();
        try {
            assert !connected : "Already connected";
            assert !chan.isBlocking() : "Unexpected blocking channel";
            boolean finished;

            connectTimerEvent = newConnectTimer(exchange, cf);
            if (connectTimerEvent != null) {
                if (debug.on())
                    debug.log("registering connect timer: " + connectTimerEvent);
                client().registerTimer(connectTimerEvent);
            }

            PrivilegedExceptionAction<Boolean> pa =
                    () -> chan.connect(Utils.resolveAddress(address));
            try {
                 finished = AccessController.doPrivileged(pa);
            } catch (PrivilegedActionException e) {
               throw e.getCause();
            }
            if (finished) {
                if (debug.on()) debug.log("connect finished without blocking");
                cf.complete(null);
            } else {
                if (debug.on()) debug.log("registering connect event");
                client().registerEvent(new ConnectEvent(cf));
            }
        } catch (Throwable throwable) {
            cf.completeExceptionally(Utils.toConnectException(throwable));
            try {
                close();
            } catch (Exception x) {
                if (debug.on())
                    debug.log("Failed to close channel after unsuccessful connect");
            }
        }
        return cf;
    }
  • 这里如果client有设置connectTimeout的话,则会创建一个connectTimerEvent
  • 调用chan.connect进行连接,如果连接未完成,则注册ConnectEvent

SocketChannelImpl.connect

java.base/sun/nio/ch/SocketChannelImpl.java

    @Override
    public boolean connect(SocketAddress sa) throws IOException {
        InetSocketAddress isa = Net.checkAddress(sa);
        SecurityManager sm = System.getSecurityManager();
        if (sm != null)
            sm.checkConnect(isa.getAddress().getHostAddress(), isa.getPort());

        InetAddress ia = isa.getAddress();
        if (ia.isAnyLocalAddress())
            ia = InetAddress.getLocalHost();

        try {
            readLock.lock();
            try {
                writeLock.lock();
                try {
                    int n = 0;
                    boolean blocking = isBlocking();
                    try {
                        beginConnect(blocking, isa);
                        do {
                            n = Net.connect(fd, ia, isa.getPort());
                        } while (n == IOStatus.INTERRUPTED && isOpen());
                    } finally {
                        endConnect(blocking, (n > 0));
                    }
                    assert IOStatus.check(n);
                    return n > 0;
                } finally {
                    writeLock.unlock();
                }
            } finally {
                readLock.unlock();
            }
        } catch (IOException ioe) {
            // connect failed, close the channel
            close();
            throw SocketExceptions.of(ioe, isa);
        }
    }
  • 通过Net.connect调用本地方法进行连接

ConnectEvent

java.net.http/jdk/internal/net/http/PlainHttpConnection.java

   final class ConnectEvent extends AsyncEvent {
        private final CompletableFuture<Void> cf;

        ConnectEvent(CompletableFuture<Void> cf) {
            this.cf = cf;
        }

        @Override
        public SelectableChannel channel() {
            return chan;
        }

        @Override
        public int interestOps() {
            return SelectionKey.OP_CONNECT;
        }

        @Override
        public void handle() {
            try {
                assert !connected : "Already connected";
                assert !chan.isBlocking() : "Unexpected blocking channel";
                if (debug.on())
                    debug.log("ConnectEvent: finishing connect");
                boolean finished = chan.finishConnect();
                assert finished : "Expected channel to be connected";
                if (debug.on())
                    debug.log("ConnectEvent: connect finished: %s Local addr: %s",
                              finished, chan.getLocalAddress());
                // complete async since the event runs on the SelectorManager thread
                cf.completeAsync(() -> null, client().theExecutor());
            } catch (Throwable e) {
                Throwable t = Utils.toConnectException(e);
                client().theExecutor().execute( () -> cf.completeExceptionally(t));
                close();
            }
        }

        @Override
        public void abort(IOException ioe) {
            client().theExecutor().execute( () -> cf.completeExceptionally(ioe));
            close();
        }
    }
  • SelectorManager对准备好的事件触发handle操作,对于ConnectEvent,就是调用ConnectEvent.handle
  • ConnectEvent的handle方法执行chan.finishConnect(),如果捕获到异常,则调用cf.completeExceptionally(t)

SocketChannelImpl.finishConnect

java.base/sun/nio/ch/SocketChannelImpl.java

    @Override
    public boolean finishConnect() throws IOException {
        try {
            readLock.lock();
            try {
                writeLock.lock();
                try {
                    // no-op if already connected
                    if (isConnected())
                        return true;

                    boolean blocking = isBlocking();
                    boolean connected = false;
                    try {
                        beginFinishConnect(blocking);
                        int n = 0;
                        if (blocking) {
                            do {
                                n = checkConnect(fd, true);
                            } while ((n == 0 || n == IOStatus.INTERRUPTED) && isOpen());
                        } else {
                            n = checkConnect(fd, false);
                        }
                        connected = (n > 0);
                    } finally {
                        endFinishConnect(blocking, connected);
                    }
                    assert (blocking && connected) ^ !blocking;
                    return connected;
                } finally {
                    writeLock.unlock();
                }
            } finally {
                readLock.unlock();
            }
        } catch (IOException ioe) {
            // connect failed, close the channel
            close();
            throw SocketExceptions.of(ioe, remoteAddress);
        }
    }
  • checkConnect是一个本地方法,如果是连接超时,则抛出java.net.ConnectException: Operation timed out

tcp连接syn超时(net.ipv4.tcp_syn_retries)


当client端与server端建立连接,client发出syn包,如果等待一定时间没有收到server端发来的SYN+ACK,则会进行重试,重试次数由具体由net.ipv4.tcp_syn_retries决定

/ # sysctl -a | grep tcp_syn_retries
sysctl: error reading key 'net.ipv6.conf.all.stable_secret': I/O error
net.ipv4.tcp_syn_retries = 6
sysctl: error reading key 'net.ipv6.conf.default.stable_secret': I/O error
sysctl: error reading key 'net.ipv6.conf.eth0.stable_secret': I/O error
sysctl: error reading key 'net.ipv6.conf.lo.stable_secret': I/O error

linux默认是6次,第一次发送等待2^0秒没收到回包则重试第一次,之后等待2^1,以此类推,第六次重试等待2^6秒,因此一共是1s+2s+4s+8s+16s+32s+64s=127s,因而在linux平台下,如果httpclient没有设置connect timeout,则依赖系统tcp的syn超时,即127s之后超时,java的本地调用抛出java.net.ConnectException: Operation timed out

如果是mac系统,根据Overriding the default Linux kernel 20-second TCP socket connect timeout的描述,超时是75s,与本实例代码输出的75814ms近似一致。

小结

  • 使用jdk httpclient进行连接,如果没有设置client的connectTimeout,则具体的超时时间依赖系统的tcp相关设置
  • 如果client端sync发送超时,则依赖tcp_syn_retries的配置来决定本地方法抛出java.net.ConnectException: Operation timed out异常的时间
  • linux下默认tcp_syn_retries默认为6,即重试6次,一共需要1s+2s+4s+8s+16s+32s+64s=127s,若再没有收到server端发来的SYN+ACK则抛出java.net.ConnectException: Operation timed out异常

doc

查看原文

赞 4 收藏 2 评论 0

Jason_Geng 收藏了文章 · 2018-06-11

读懂Netty的高性能架构之道

Netty是一个高性能、异步事件驱动的NIO框架,它提供了对TCP、UDP和文件传输的支持,作为一个异步NIO框架,Netty的所有IO操作都是异步非阻塞的,通过Future-Listener机制,用户可以方便的主动获取或者通过通知机制获得IO操作结果。

clipboard.png

作为当前最流行的NIO框架,Netty在互联网领域、大数据分布式计算领域、游戏行业、通信行业等获得了广泛的应用,一些业界著名的开源组件也基于Netty的NIO框架构建。载请注明出处。

为什么选择Netty

Netty是业界最流行的NIO框架之一,它的健壮性、功能、性能、可定制性和可扩展性在同类框架中都是首屈一指的,它已经得到成百上千的商用项目验证,例如Hadoop的RPC框架avro使用Netty作为底层通信框架;很多其他业界主流的RPC框架,也使用Netty来构建高性能的异步通信能力。

通过对Netty的分析,我们将它的优点总结如下:

1.API使用简单,开发门槛低;
2.功能强大,预置了多种编解码功能,支持多种主流协议;
3.定制能力强,可以通过ChannelHandler对通信框架进行灵活地扩展;
4.性能高,通过与其他业界主流的NIO框架对比,Netty的综合性能最优;
5.成熟、稳定,Netty修复了已经发现的所有JDK NIO BUG,业务开发人员不需要再为NIO的BUG而烦恼;
6.社区活跃,版本迭代周期短,发现的BUG可以被及时修复,同时,更多的新功能会加入;经历了大规模的商业应用考验,质量得到验证。

在互联网、大数据、网络游戏、企业应用、电信软件等众多行业得到成功商用,证明了它已经完全能够满足不同行业的商业应用了。

Netty架构分析

Netty 采用了比较典型的三层网络架构进行设计,逻辑架构图如下所示:

clipboard.png

第一层:Reactor 通信调度层,它由一系列辅助类完成,包括 Reactor 线程 NioEventLoop 以及其父类、NioSocketChannel/NioServerSocketChannel 以及其父 类、ByteBuffer 以及由其衍生出来的各种 Buffer、Unsafe 以及其衍生出的各种内 部类等。该层的主要职责就是监听网络的读写和连接操作,负责将网络层的数据 读取到内存缓冲区中,然后触发各种网络事件,例如连接创建、连接激活、读事 件、写事件等等,将这些事件触发到 PipeLine 中,由 PipeLine 充当的职责链来 进行后续的处理。

第二层:职责链 PipeLine,它负责事件在职责链中的有序传播,同时负责动态的 编排职责链,职责链可以选择监听和处理自己关心的事件,它可以拦截处理和向 后/向前传播事件,不同的应用的 Handler 节点的功能也不同,通常情况下,往往 会开发编解码 Hanlder 用于消息的编解码,它可以将外部的协议消息转换成内部 的 POJO 对象,这样上层业务侧只需要关心处理业务逻辑即可,不需要感知底层 的协议差异和线程模型差异,实现了架构层面的分层隔离。

第三层:业务逻辑处理层,可以分为两类:1.纯粹的业务逻辑 处理,例如订单处理。2.应用层协议管理,例如HTTP协议、FTP协议等。接下来,我从影响通信性能的三个方面(I/O模型、线程调度模型、序列化方式)来谈谈Netty的架构。

I/O模型

传统同步阻塞I/O模式如下图所示:

clipboard.png

它的弊端有很多:

1.性能问题:一连接一线程模型导致服务端的并发接入数和系统吞吐量受到极大限制;
2.可靠性问题:由于I/O操作采用同步阻塞模式,当网络拥塞或者通信对端处理缓慢会导致I/O线程被挂住,阻塞时间无法预测;
3.可维护性问题:I/O线程数无法有效控制、资源无法有效共享(多线程并发问题),系统可维护性差;几种I/O模型的功能和特性对比:

clipboard.png

Netty的I/O模型基于非阻塞I/O实现,底层依赖的是JDK NIO框架的Selector。

Selector提供选择已经就绪的任务的能力。简单来讲,Selector会不断地轮询注册在其上的Channel,如果某个Channel上面有新的TCP连接接入、读和写事件,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取就绪Channel的集合,进行后续的I/O操作。

一个多路复用器Selector可以同时轮询多个Channel,由于JDK1.5_update10版本(+)使用了epoll()代替传统的select实现,所以它并没有最大连接句柄1024/2048的限制。这也就意味着只需要一个线程负责Selector的轮询,就可以接入成千上万的客户端,这确实是个非常巨大的技术进步。

使用非阻塞I/O模型之后,Netty解决了传统同步阻塞I/O带来的性能、吞吐量和可靠性问题。

线程调度模型常用的Reactor线程模型有三种,分别如下:

1.Reactor单线程模型:Reactor单线程模型,指的是所有的I/O操作都在同一个NIO线程上面完成。对于一些小容量应用场景,可以使用单线程模型。

2.Reactor多线程模型:Rector多线程模型与单线程模型最大的区别就是有一组NIO线程处理I/O操作。主要用于高并发、大业务量场景。

3.主从Reactor多线程模型:主从Reactor线程模型的特点是服务端用于接收客户端连接的不再是个1个单独的NIO线程,而是一个独立的NIO线程池。利用主从NIO线程模型,可以解决1个服务端监听线程无法有效处理所有客户端连接的性能不足问题。

事实上,Netty的线程模型并非固定不变,通过在启动辅助类中创建不同的EventLoopGroup实例并通过适当的参数配置,就可以支持上述三种Reactor线程模型。

在大多数场景下,并行多线程处理可以提升系统的并发性能。但是,如果对于共享资源的并发访问处理不当,会带来严重的锁竞争,这最终会导致性能的下降。为了尽可能的避免锁竞争带来的性能损耗,可以通过串行化设计,即消息的处理尽可能在同一个线程内完成,期间不进行线程切换,这样就避免了多线程竞争和同步锁。

为了尽可能提升性能,Netty采用了串行无锁化设计,在I/O线程内部进行串行操作,避免多线程竞争导致的性能下降。表面上看,串行化设计似乎CPU利用率不高,并发程度不够。但是,通过调整NIO线程池的线程参数,可以同时启动多个串行化的线程并行运行,这种局部无锁化的串行线程设计相比一个队列-多个工作线程模型性能更优。

clipboard.png

序列化方式影响序列化性能的关键因素总结如下:

1.序列化后的码流大小(网络带宽占用)
2.序列化&反序列化的性能(CPU资源占用)
3.并发调用的性能表现:稳定性、线性增长、偶现的时延毛刺等

对Java序列化和二进制编码分别进行性能测试,编码100万次,测试结果表明:Java序列化的性能只有二进制编码的6.17%左右。

clipboard.png

Netty默认提供了对Google Protobuf的支持,通过扩展Netty的编解码接口,用户可以实现其它的高性能序列化框架,例如Thrift的压缩二进制编解码框架。

不同的应用场景对序列化框架的需求也不同,对于高性能应用场景Netty默认提供了Google的Protobuf二进制序列化框架,如果用户对其它二进制序列化框架有需求,也可以基于Netty提供的编解码框架扩展实现。

Netty架构剖析之可靠性Netty面临的可靠性挑战:

1.作为RPC框架的基础网络通信框架,一旦故障将导致无法进行远程服务(接口)调用。
2.作为应用层协议的基础通信框架,一旦故障将导致应用协议栈无法正常工作。
3.网络环境复杂(例如手游或者推送服务的GSM/3G/WIFI网络),故障不可避免,业务却不能中断。

从应用场景看,Netty是基础的通信框架,一旦出现Bug,轻则需要重启应用,重则可能导致整个业务中断。它的可靠性会影响整个业务集群的数据通信和交换,在当今以分布式为主的软件架构体系中,通信中断就意味着整个业务中断,分布式架构下对通信的可靠性要求非常高。

从运行环境看,Netty会面临恶劣的网络环境,这就要求它自身的可靠性要足够好,平台能够解决的可靠性问题需要由Netty自身来解决,否则会导致上层用户关注过多的底层故障,这将降低Netty的易用性,同时增加用户的开发和运维成本。

Netty的可靠性是如此重要,它的任何故障都可能会导致业务中断,蒙受巨大的经济损失。因此,Netty在版本的迭代中不断加入新的可靠性特性来满足用户日益增长的高可靠和健壮性需求。

在此我向大家推荐一个架构学习交流群。交流学习群号:478030634 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多

链路有效性检测

Netty提供的心跳检测机制分为三种:

1.读空闲,链路持续时间t没有读取到任何消息;

2.写空闲,链路持续时间t没有发送任何消息;

3.读写空闲,链路持续时间t没有接收或者发送任何消息。

clipboard.png

当网络发生单通、连接被防火墙Hang住、长时间GC或者通信线程发生非预期异常时,会导致链路不可用且不易被及时发现。特别是异常发生在凌晨业务低谷期间,当早晨业务高峰期到来时,由于链路不可用会导致瞬间的大批量业务失败或者超时,这将对系统的可靠性产生重大的威胁。

从技术层面看,要解决链路的可靠性问题,必须周期性的对链路进行有效性检测。目前最流行和通用的做法就是心跳检测。

心跳检测机制分为三个层面:
1.TCP层面的心跳检测,即TCP的Keep-Alive机制,它的作用域是整个TCP协议栈;
2.协议层的心跳检测,主要存在于长连接协议中。例如SMPP协议;
3.应用层的心跳检测,它主要由各业务产品通过约定方式定时给对方发送心跳消息实现。

心跳检测的目的就是确认当前链路可用,对方活着并且能够正常接收和发送消息。做为高可靠的NIO框架,Netty也提供了基于链路空闲的心跳检测机制:

1.读空闲,链路持续时间t没有读取到任何消息;
2.写空闲,链路持续时间t没有发送任何消息;
3.读写空闲,链路持续时间t没有接收或者发送任何消息。

流量整形

流量整形(Traffic Shaping)是一种主动调整流量输出速率的措施。Netty的流量整形有两个作用:

1.防止由于上下游网元性能不均衡导致下游网元被压垮,业务流程中断;
2.防止由于通信模块接收消息过快,后端业务线程处理不及时导致的“撑死”问题。

流量整形的原理示意图如下:

clipboard.png

流量整形(Traffic Shaping)是一种主动调整流量输出速率的措施。一个典型应用是基于下游网络结点的TP指标来控制本地流量的输出。流量整形与流量监管的主要区别在于,流量整形对流量监管中需要丢弃的报文进行缓存——通常是将它们放入缓冲区或队列内,也称流量整形(Traffic Shaping,简称TS)。当令牌桶有足够的令牌时,再均匀的向外发送这些被缓存的报文。流量整形与流量监管的另一区别是,整形可能会增加延迟,而监管几乎不引入额外的延迟。

Netty支持两种流量整形模式:

1.全局流量整形:全局流量整形的作用范围是进程级的,无论你创建了多少个Channel,它的作用域针对所有的Channel。用户可以通过参数设置:报文的接收速率、报文的发送速率、整形周期。
2.链路级流量整形:单链路流量整形与全局流量整形的最大区别就是它以单个链路为作用域,可以对不同的链路设置不同的整形策略。

优雅停机

Netty的优雅停机三部曲:

1.不再接收新消息

2.退出前的预处理操作

3.资源的释放操作

clipboard.png

Java的优雅停机通常通过注册JDK的ShutdownHook来实现,当系统接收到退出指令后,首先标记系统处于退出状态,不再接收新的消息,然后将积压的消息处理完,最后调用资源回收接口将资源销毁,最后各线程退出执行。

通常优雅退出需要有超时控制机制,例如30S,如果到达超时时间仍然没有完成退出前的资源回收等操作,则由停机脚本直接调用kill -9 pid,强制退出。

在实际项目中,Netty作为高性能的异步NIO通信框架,往往用作基础通信框架负责各种协议的接入、解析和调度等,例如在RPC和分布式服务框架中,往往会使用Netty作为内部私有协议的基础通信框架。 当应用进程优雅退出时,作为通信框架的Netty也需要优雅退出,主要原因如下:

  • 尽快的释放NIO线程、句柄等资源;
  • 如果使用flush做批量消息发送,需要将积攒在发送队列中的待发送消息发送完成;
  • 正在write或者read的消息,需要继续处理;
  • 设置在NioEventLoop线程调度器中的定时任务,需要执行或者清理。

Netty架构剖析之安全性

Netty面临的安全挑战:

对第三方开放

作为应用层协议的基础通信框架

clipboard.png

安全威胁场景分析:

对第三方开放的通信框架:如果使用Netty做RPC框架或者私有协议栈,RPC框架面向非授信的第三方开放,例如将内部的一些能力通过服务对外开放出去,此时就需要进行安全认证,如果开放的是公网IP,对于安全性要求非常高的一些服务,例如在线支付、订购等,需要通过SSL/TLS进行通信。

应用层协议的安全性。作为高性能、异步事件驱动的NIO框架,Netty非常适合构建上层的应用层协议。由于绝大多数应用层协议都是公有的,这意味着底层的Netty需要向上层提供通信层的安全传输功能。

SSL/TLSNetty安全传输特性:

  1. 支持SSL V2和V32.
  2. 支持TLS3.
  3. 支持SSL单向认证、双向认证和第三方CA认证。

SSL单向认证流程图如下:

clipboard.png

Netty通过SslHandler提供了对SSL的支持,它支持的SSL协议类型包括:SSL V2、SSL V3和TLS。

单向认证:单向认证,即客户端只验证服务端的合法性,服务端不验证客户端。

双向认证:与单向认证不同的是服务端也需要对客户端进行安全认证。这就意味着客户端的自签名证书也需要导入到服务端的数字证书仓库中。

CA认证:基于自签名的SSL双向认证,只要客户端或者服务端修改了密钥和证书,就需要重新进行签名和证书交换,这种调试和维护工作量是非常大的。因此,在实际的商用系统中往往会使用第三方CA证书颁发机构进行签名和验证。我们的浏览器就保存了几个常用的CA_ROOT。每次连接到网站时只要这个网站的证书是经过这些CA_ROOT签名过的。就可以通过验证了。

可扩展的安全特性

通过Netty的扩展特性,可以自定义安全策略:
1.IP地址黑名单机制
2.接入认证
3.敏感信息加密或者过滤机制

IP地址黑名单是比较常用的弱安全保护策略,它的特点就是服务端在与客户端通信的过程中,对客户端的IP地址进行校验,如果发现对方IP在黑名单列表中,则拒绝与其通信,关闭链路。

接入认证策略非常多,通常是较强的安全认证策略,例如基于用户名+密码的认证,认证内容往往采用加密的方式,例如Base64+AES等。

在此我向大家推荐一个架构学习交流群。交流学习群号:478030634 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多

Netty架构剖析之扩展性

通过Netty的扩展特性,可以自定义安全策略:
1.线程模型可扩展
2.序列化方式可扩展
3.上层协议栈可扩展
4.提供大量的网络事件切面,方便用户功能扩展

Netty的架构可扩展性设计理念如下:

5.判断扩展点,事先预留相关扩展接口,给用户二次定制和扩展使用;
6.主要功能点都基于接口编程,方便用户定制和扩展。

注:给大家分享一个作者专门聊Java架构等知识点的公众号。公众号:《Java烂猪皮》

查看原文

Jason_Geng 赞了文章 · 2018-06-11

Nginx 架构初探

引言:众所周知,Nginx 服务器是一个高性能的 Web 和反向代理服务器。Nginx 在激烈的 Web 服务器竞争中依旧保持良好的发展势头,一度成为 Web 服务器市场的后期之秀,这一切跟 Nginx 的架构设计是分不开的。

一. Nginx 模块化设计

高度模块化的设计是 Nginx 的架构基础。Nginx 服务器被分解为多个模块,每个模块就是一个功能模块,只负责自身的功能,模块之间严格遵循“高内聚,低耦合”的原则。

clipboard.png

  • 核心模块

核心模块是 Nginx 服务器正常运行必不可少的模块,提供错误日志记录、配置文件解析、事件驱动机制、进程管理等核心功能。

  • 标准 HTTP 模块

标准 HTTP 模块提供 HTTP 协议解析相关的功能,如:端口配置、网页编码设置、HTTP 响应头设置等。

  • 可选 HTTP 模块

可选 HTTP 模块主要用于扩展标准的 HTTP 功能,让 Nginx 能处理一些特殊的服务,如:Flash 多媒体传输、解析 GeoIP 请求、SSL 支持等。

  • 邮件服务模块

邮件服务模块主要用于支持 Nginx 的邮件服务,包括对 POP3 协议、IMAP 协议和 SMTP 协议的支持。

  • 第三方模块

第三方模块是为了扩展 Nginx 服务器应用,完成开发者自定义功能,如:Json 支持、Lua 支持等。

二. Nginx 请求处理方式

Nginx 是一个高性能的 Web 服务器,能够同时处理大量的并发请求。它结合多进程机制和异步机制,异步机制使用的是异步非阻塞方式,接下来就给大家介绍一下 Nginx 的多线程机制和异步非阻塞机制。

  • 多进程

服务器每当收到一个客户端时。就有服务器主进程(master process)生成一个子进程(worker process)出来和客户端建立连接进行交互,直到连接断开,该子进程就结束了。使用进程的好处是各个进程之间相互独立,不需要加锁,减少了使用锁对性能造成影响,同时降低编程的复杂度,降低开发成本。其次,采用独立的进程,可以让进程互相之间不会影响,如果一个进程发生异常退出时,其它进程正常工作,master 进程则很快启动新的 worker 进程,确保服务部中断,将风险降到最低。缺点是操作系统生成一个子进程需要进行内存复制等操作,在资源和时间上会产生一定的开销;当有大量请求时,会导致系统性能下降。

  • 异步非阻塞

每个工作进程使用异步非阻塞方式,可以处理多个客户端请求。当某个工作进程接收到客户端的请求以后,调用 IO 进行处理,如果不能立即得到结果,就去处理其他的请求(即为非阻塞);而客户端在此期间也无需等待响应,可以去处理其他事情(即为异步);当 IO 返回时,就会通知此工作进程;该进程得到通知,暂时挂起当前处理的事务去响应客户端请求。

三. Nginx 事件驱动模型

在 Nginx 的异步非阻塞机制中,工作进程在调用 IO 后,就去处理其他的请求,当 IO 调用返回后,会通知该工作进程。对于这样的系统调用,主要使用 Nginx 服务器的事件驱动模型来实现。

clipboard.png

如上图所示,Nginx 的事件驱动模型由事件收集器、事件发送器和事件处理器三部分基本单元组成。其中,事件收集器负责收集 worker 进程的各种 IO 请求,事件发送器负责将 IO 事件发送到事件处理器,而事件处理器负责各种事件的响应工作。

事件发送器将每个请求放入一个待处理事件的列表,使用非阻塞 I/O 方式调用“事件处理器”来处理该请求。其处理方式称为“多路 IO 复用方法”,常见的包括以下三种:select 模型、poll 模型、epoll 模型。

四. Nginx 设计架构

Nginx 服务器使用 master/worker 多进程模式。多线程启动和执行的流程如下:主程序 Master process 启动后,通过一个 for 循环来接收和处理外部信号;主进程通过 fork() 函数产生子进程,每个子进程执行一个 for 循环来实现 Nginx 服务器对事件的接收和处理。

一般推荐 worker 进程数与 cpu 内核数一致,这样一来不存在大量的子进程生成和管理任务,避免了进程之间竞争 CPU 资源和进程切换的开销。而且 Nginx 为了更好的利用多核特性,提供了 cpu 亲缘性的绑定选项,我们可以将某一个进程绑定在某一个核上,这样就不会因为进程的切换带来 cache 的失效。

对于每个请求,有且只有一个工作进程对其处理。首先,每个 worker 进程都是从 master 进程 fork 过来,在 master 进程里面,先建立好需要 listen 的 socket(listenfd)之后,然后再 fork 出多个 worker 进程。所有 worker 进程的 listenfd 会在新连接到来时变得可读,为保证只有一个进程处理该连接,所有 worker 进程在注册 listenfd 读事件前抢 accept_mutex,抢到互斥锁的那个进程注册 listenfd 读事件,在读事件里调用 accept 接受该连接。当一个 worker 进程在 accept 这个连接之后,就开始读取请求,解析请求,处理请求,产生数据后,再返回给客户端,最后才断开连接,这样一个完整的请求就是这样的了。我们可以看到,一个请求,完全由 worker 进程来处理,而且只在一个 worker 进程中处理。

clipboard.png

在 Nginx 服务器的运行过程中,主进程和工作进程需要进程交互。交互依赖于 Socket 实现的管道来实现。

  • Master-Worker 交互

这条管道与普通的管道不同,它是由主进程指向工作进程的单向管道,包含主进程向工作进程发出的指令,工作进程 ID 等;同时主进程与外界通过信号通信;每个子进程具备接收信号,并处理相应的事件的能力。

  • worker-worker 交互

这种交互是和 Master-Worker 交互是基本一致的,但是会通过主进程。工作进程之间是相互隔离的,所以当工作进程 W1 需要向工作进程 W2 发指令时,首先找到 W2 的进程 ID,然后将正确的指令写入指向 W2 的通道。W2 收到信号采取相应的措施。

在此我向大家推荐一个架构学习交流群。交流学习群号:478030634 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多

五. 总结

通过这篇文章,我们对 Nginx 服务器的整体架构有了一个整体的认识。包括其模块化的设计、

多进程和异步非阻塞的请求处理方式、事件驱动模型等。通过这些理论知识,对于我们以后学习 Nginx 的源码有很大的帮助;也推荐大家多看看 Nginx 的源码,才能更好地领悟 Nginx 的设计思想。

注:关注作者微信公众号,了解更多分布式架构、微服务、netty、MySQL、spring、性能优化、等知识点。

公众号:《Java烂猪皮》
图片描述

查看原文

赞 8 收藏 11 评论 1

Jason_Geng 关注了用户 · 2018-05-29

zuch @zuch

如果你连问题都描述不清楚,我怎么清楚的回答你的问题呢?
如果你只会伸手要答案,我帮助你有什么意义呢?

关注 1614

Jason_Geng 提出了问题 · 2018-05-28

解决struct 中成员的顺序不同,导致内存占用不同。那成员类型的定义顺序有什么遵守的原则吗?

背景:64位操作系统上

示例:定义2个成员相同的 stuct 对象(顺序不同),打印

package main

import "unsafe"

type A struct {
    X bool
    Y float64
    Z int16
}

type B struct {
    Y float64
    X bool
    Z int16
}

func main() {
    print("A: ")
    println(unsafe.Sizeof(A{}))
    print("B: ")
    println(unsafe.Sizeof(B{}))
}

输出结果:

A: 24
B: 16

结论:
所以在定义 sturct 成员的时候,随意的定义可能导致内存占用不同。那想问的是在定义时有什么可参考的吗?

关注 3 回答 1

Jason_Geng 发布了文章 · 2018-04-08

彻底搞懂 JS 中 this 机制

彻底搞懂 JS 中 this 机制

摘要:本文属于原创,欢迎转载,转载请保留出处:https://github.com/jasonGeng88/blog

目录

  • this 是什么
  • this 的四种绑定规则
  • 绑定规则的优先级
  • 绑定例外
  • 扩展:箭头函数

this 是什么

理解this之前, 先纠正一个观点,this 既不指向函数自身,也不指函数的词法作用域。如果仅通过this的英文解释,太容易产生误导了。它实际是在函数被调用时才发生的绑定,也就是说this具体指向什么,取决于你是怎么调用的函数。

this 的四种绑定规则

this的4种绑定规则分别是:默认绑定、隐式绑定、显示绑定、new 绑定。优先级从低到高。

默认绑定

什么叫默认绑定,即没有其他绑定规则存在时的默认规则。这也是函数调用中最常用的规则。

来看这段代码:

function foo() { 
}       console.log( this.a );

var a = 2; 
foo(); //打印的是什么?

foo() 打印的结果是2。

因为foo()是直接调用的(独立函数调用),没有应用其他的绑定规则,这里进行了默认绑定,将全局对象绑定this上,所以this.a 就解析成了全局变量中的a,即2。

注意:在严格模式下(strict mode),全局对象将无法使用默认绑定,即执行会报undefined的错误

function foo() { 
    "use strict";
   console.log( this.a );
}

var a = 2; 
foo(); // Uncaught TypeError: Cannot read property 'a' of undefined

隐式绑定

除了直接对函数进行调用外,有些情况是,函数的调用是在某个对象上触发的,即调用位置上存在上下文对象。

function foo() { 
    console.log( this.a );
}

var a = 2;

var obj = { 
    a: 3,
    foo: foo 
};

obj.foo(); // ?

obj.foo() 打印的结果是3。

这里foo函数被当做引用属性,被添加到obj对象上。这里的调用过程是这样的:

获取obj.foo属性 -> 根据引用关系找到foo函数,执行调用

所以这里对foo的调用存在上下文对象obj,this进行了隐式绑定,即this绑定到了obj上,所以this.a被解析成了obj.a,即3。

多层调用链

function foo() { 
    console.log( this.a );
}

var a = 2;

var obj1 = { 
    a: 4,
    foo: foo 
};

var obj2 = { 
    a: 3,
    obj1: obj1
};

obj2.obj1.foo(); //?

obj2.obj1.foo() 打印的结果是4。

同样,我们看下函数的调用过程:

先获取obj2.obj1 -> 通过引用获取到obj1对象,再访问 obj1.foo -> 最后执行foo函数调用

这里调用链不只一层,存在obj1、obj2两个对象,那么隐式绑定具体会绑哪个对象。这里原则是获取最后一层调用的上下文对象,即obj1,所以结果显然是4(obj1.a)。

隐式丢失(函数别名)

注意:这里存在一个陷阱,大家在分析调用过程时,要特别小心

先看个代码:

function foo() { 
    console.log( this.a );
}

var a = 2;

var obj = { 
    a: 3,
    foo: foo 
};

var bar = obj.foo;
bar(); //?

<font color="red">bar() 打印的结果是2。</font>

为什么会这样,obj.foo 赋值给bar,那调用bar()为什么没有触发隐式绑定,使用的是默认绑定呢。

这里有个概念要理解清楚,obj.foo 是引用属性,赋值给bar的实际上就是foo函数(即:bar指向foo本身)。

那么,实际的调用关系是:通过bar找到foo函数,进行调用。整个调用过程并没有obj的参数,所以是默认绑定,全局属性a。

隐式丢失(回调函数)

function foo() { 
    console.log( this.a );
}

var a = 2;

var obj = { 
    a: 3,
    foo: foo 
};

setTimeout( obj.foo, 100 ); // ?

<font color="red">打印的结果是2。</font>

同样的道理,虽然参传是obj.foo,因为是引用关系,所以传参实际上传的就是foo对象本身的引用。对于setTimeout的调用,还是 setTimeout -> 获取参数中foo的引用参数 -> 执行 foo 函数,中间没有obj的参与。这里依旧进行的是默认绑定。


显示绑定

相对隐式绑定,this值在调用过程中会动态变化,可是我们就想绑定指定的对象,这时就用到了显示绑定。

显示绑定主要是通过改变对象的prototype关联对象,这里不展开讲。具体使用上,可以通过这两个方法call(...)或apply(...)来实现(大多数函数及自己创建的函数默认都提供这两个方法)。

call与apply是同样的作用,区别只是其他参数的设置上

function foo() { 
    console.log( this.a );
}

var a = 2;

var obj1 = { 
    a: 3,
};

var obj2 = { 
    a: 4,
};
foo.call( obj1 ); // ?
foo.call( obj2 ); // ?

打印的结果是3, 4。

这里因为显示的申明了要绑定的对象,所以this就被绑定到了obj上,打印的结果自然就是obj1.a 和obj2.a。

硬绑定

function foo() { 
    console.log( this.a );
}

var a = 2;

var obj1 = { 
    a: 3,
};

var obj2 = { 
    a: 4,
};

var bar = function(){
    foo.call( obj1 );
}

bar(); // 3
setTimeout( bar, 100 ); // 3

bar.call( obj2 ); // 这是多少

前面两个(函数别名、回调函数)打印3,因为显示绑定了,没什么问题。

最后一个打印是3。

这里需要注意下,虽然bar被显示绑定到obj2上,对于bar,function(){...} 中的this确实被绑定到了obj2,而foo因为通过foo.call( obj1 )已经显示绑定了obj1,所以在foo函数内,this指向的是obj1,不会因为bar函数内指向obj2而改变自身。所以打印的是obj1.a(即3)。


new 绑定

js中的new操作符,和其他语言中(如JAVA)的new机制是不一样的。js中,它就是一个普通函数调用,只是被new修饰了而已。

使用new来调用函数,会自动执行如下操作:

  1. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。

从第三点可以看出,this指向的就是对象本身。

看个代码:

function foo(a) { 
    this.a = a;
}

var a = 2;

var bar1 = new foo(3);
console.log(bar1.a); // ?

var bar2 = new foo(4);
console.log(bar2.a); // ?

最后一个打印是3, 4。

因为每次调用生成的是全新的对象,该对象又会自动绑定到this上,所以答案显而易见。

绑定规则优先级

上面也说过,这里在重复一下。优先级是这样的,以按照下面的顺序来进行判断:

 数是否在new中调用(new绑定)?如果是的话this绑定的是新创建的对象。
 数是否通过call、apply(显式绑定)或者硬绑定调用?如果是的话,this绑定的是 指定的对象。
 数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this绑定的是那个上下文对象。
 果都不是的话,使用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到 全局对象。
 var bar = foo()

规则例外

在显示绑定中,对于null和undefined的绑定将不会生效。

代码如下:

function foo() { 
    console.log( this.a );
}
foo.call( null ); // 2
foo.call( undefined ); // 2

这种情况主要是用在不关心this的具体绑定对象(用来忽略this),而传入null实际上会进行默认绑定,导致函数中可能会使用到全局变量,与预期不符。

所以对于要忽略this的情况,可以传入一个空对象ø,该对象通过Object.create(null)创建。这里不用{}的原因是,ø是真正意义上的空对象,它不创建Object.prototype委托,{}和普通对象一样,有原型链委托关系。

1. 这里传null的一种具体使用场景是函数柯里化的使用

扩展:箭头函数

最后,介绍一下ES6中的箭头函数。通过“=>”而不是function创建的函数,叫做箭头函数。它的this绑定取决于外层(函数或全局)作用域。

case 1 (正常调用)

  • 普通函数
function foo(){     
    console.log( this.a );
}

var a = 2;

var obj = { 
    a: 3,
    foo: foo 
};

obj.foo(); //3
  • 箭头函数
var foo = () => {     
    console.log( this.a );
}

var a = 2;

var obj = { 
    a: 3,
    foo: foo 
};

obj.foo(); //2
foo.call(obj); //2 ,箭头函数中显示绑定不会生效

case 2 (函数回调)

  • 普通函数
function foo(){ 
    return function(){
        console.log( this.a );
    }    
}

var a = 2;

var obj = { 
    a: 3,
    foo: foo 
};

var bar = obj.foo();
bar(); //2
  • 箭头函数
function foo(){ 
    return () => {
        console.log( this.a );
    }    
}



var a = 2;

var obj = { 
    a: 3,
    foo: foo 
};

var bar = obj.foo();
bar(); //3

通过上面两个列子,我们看到箭头函数的this绑定<font color="red">只取决于外层(函数或全局)的作用域</font>,对于前面的4种绑定规则是不会生效的。它也是作为this机制的一种替换,解决之前this绑定过程各种规则带来的复杂性。

注意:对于ES6之前,箭头函数的替换版本是这样的

// es6
function foo(){ 
    return () => {
        console.log( this.a );
    }   
}

var a = 2;

var obj = { 
    a: 3,
    foo: foo 
};

var bar = obj.foo();
bar(); //3

通过上面两个列子,我们看到箭头函数的this绑定<font color="red">只取决于外层(函数或全局)的作用域</font>,对于前面的4种绑定规则是不会生效的。它也是作为this机制的一种替换,解决之前this绑定过程各种规则带来的复杂性。

注意:对于ES6之前,箭头函数的替换版本是这样的

// es6
function foo(){ 
    return () => {
        console.log( this.a );
    }   
}

// es6之前的替代方法
function foo(){ 
     var self = this;
    return () => {
        console.log( self.a );
    }   
}

总结

我们在使用js的过程中,对于this的理解往往觉得比较困难,再调试过程中有时也会出现一些不符合预期的现象。很多时候,我们都是通过一些变通的方式(如:使用具体对象替换this)来规避的问题。可问题一直存在那儿,我们没有真正的去理解和解决它。

本文主要参考了《你不知道的JavaScript(上卷)》,对this到底是什么,具体怎么绑定的,有什么例外情况以及ES6中的一个优化方向,来彻底搞清楚我们一直使用的this到底是怎么玩的。

查看原文

赞 96 收藏 386 评论 18

Jason_Geng 发布了文章 · 2018-02-14

JAVA 线程池的正确打开方式

JAVA 线程池的正确打开方式

摘要:本文属于原创,欢迎转载,转载请保留出处:https://github.com/jasonGeng88/blog

当前环境

  1. jdk == 1.8

Executors 使用的隐患

先来看一段代码,我们要创建一个固定线程池,假设固定线程数是4。代码如下:

Executors是JAVA并发包中提供的,用来快速创建不同类型的线程池。

是不是很简单,创建线程池只需一行代码。对于一些个人项目或临时性的项目,这样写确实没什么问题,而且开发速度很快。但在一些大型项目中,这种做法一般是禁止的。

WHY???

因为用Executors创建的线程池存在性能隐患,我们看一下源码就知道,用Executors创建线程池时,使用的队列是new LinkedBlockingQueue<Runnable>(),这是一个无边界队列,如果不断的往里加任务时,最终会导致内存问题,也就是说在项目中由于使用了无边界队列,导致的内存占用的不可控性。下图是不断添加线程任务导致老年代被占满的情况:

当然,除了内存问题,它还存在一些其他的问题,在下面对线程池参数的介绍中会具体说明。

线程池的正确创建方式

其实,问题很好解决。提供的简便方式有局限性,那我们自己new一个ThreadPoolExecutor,无非多写几行代码而已。

关于ThreadPoolExecutor的具体代码如下:

参数说明:

  • corePoolSize:核心线程数;
  • maximumPoolSize:最大线程数,即线程池中允许存在的最大线程数;
  • keepAliveTime:线程存活时间,对于超过核心线程数的线程,当线程处理空闲状态下,且维持时间达到keepAliveTime时,线程将被销毁;
  • unit:keepAliveTime的时间单位
  • workQueue:工作队列,用于存在待执行的线程任务;
  • threadFactory:创建线程的工厂,用于标记区分不同线程池所创建出来的线程;
  • handler:当到达线程数上限或工作队列已满时的拒绝处理逻辑;

具体代码

  • 自定义threadFactory。除了可以自定义创建的线程名称,方便问题排查,在newThread(Runnable r)创建线程的方法中,还可以进行定制化设置,如为线程设置特定上下文等。

  • 自定义RejectedExecutionHandler。记录异常信息,选择不同处理逻辑,有交由当前线程执行任务,有直接抛出异常,再或者等待后继续添加任务等。

  • 创建自定义线程池

线程池内在处理逻辑

我们通过一些例子,来观察一下其内部的处理逻辑。基于上述具体代码,我们已经创建了一个核心线程数4,最大线程数8,线程存活时间10s,工作队列最大容量为10的一个线程池。

  • 初始化线程池:未添加线程任务

    • 这时,线程池中不会创建任何线程,存活线程为0,工作队列为0.
  • 未达核心线程数:添加4个线程任务

    • 由于当前存活线程数 <= 核心线程数,所以会创建新的线程。即存活线程为4,工作队列为0.
  • 核心线程数已满:添加第5个线程任务

    • 若当前线程池中存在空闲线程,则交由该线程处理。即存活线程为4,工作队列为0.
    • 若当前所有线程处理运行状态,加入工作队列。即存活线程为4,工作队列为1.(注意:此时工作队列中的任务不会被执行,直到有线程空闲后,才能被处理
  • 工作队列未满:假设添加的任务都是耗时操作(短时间不会结束),再添加9个耗时任务

    • 即存活线程为4,工作队列为10.
  • 工作队列已满 & 未达最大线程数:再添加4个任务

    • 当工作队列已满,且不存在空闲线程,此时会创建额外线程来处理当前任务。此时存活线程为8,工作队列为10.
  • 工作队列已满 & 且最大线程数已满:再添加1个任务

    • 触发RejectedExecutionHandler,将当前任务交由自己设置的执行句柄进行处理。此时存活线程为8,工作队列为10.
  • 当任务执行完后,没有新增的任务,临时扩充的线程(大于核心线程数的)将在10s(keepAliveTime)后被销毁。

总结

最后,我们在使用线程池的时候,需要根据使用场景来自行选择。通过corePoolSize和maximumPoolSize的搭配,存活时间的选择,以及改变队列的实现方式,如:选择延迟队列,来实现定时任务的功能。并发包Executors中提供的一些方法确实好用,但我们仍需有保留地去使用,这样在项目中就不会挖太多的坑。

扩展

对于一些耗时的IO任务,盲目选择线程池往往不是最佳方案。通过异步+单线程轮询,上层再配合上一个固定的线程池,效果可能更好。类似与Reactor模型中selector轮询处理

查看原文

赞 4 收藏 7 评论 0

认证与成就

  • 获得 160 次点赞
  • 获得 5 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 5 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2015-10-20
个人主页被 1.4k 人浏览