認證授權方案之JwtBearer認證

1.前言

回顧:認證方案之初步認識JWT

在現代Web應用程序中,即分為前端與後端兩大部分。當前前後端的趨勢日益劇增,前端設備(手機、平板、電腦、及其他設備)層出不窮。因此,為了方便滿足前端設備與後端進行通訊,就必須有一種統一的機制。所以導致API架構的流行。而RESTful API這個API設計思想理論也就成為目前互聯網應用程序比較歡迎的一套方式。

這種API架構思想的引入,因此,我們就需要考慮用一種標準的,通用的,無狀態的,與語言無關的身份認證方式來實現API接口的認證。

HTTP提供了一套標準的身份驗證框架:服務端可以用來針對客戶端的請求發送質詢(challenge),客戶端根據質詢提供應答身份驗證憑證。

質詢與應答的工作流程如下:服務端向客戶端返回401(Unauthorized,未授權)狀態碼,並在WWW-Authenticate頭中添加如何進行驗證的信息,其中至少包含有一種質詢方式。然後客戶端可以在請求中添加Authorization頭進行驗證,其Value為身份驗證的憑證信息。

在本文中,將要介紹的是以Jwt Bearer方式進行認證。

2.Bearer認證

本文要介紹的Bearer驗證也屬於HTTP協議標準驗證,它隨着OAuth協議而開始流行,詳細定義見: RFC 6570。

     +--------+                               +---------------+
     |        |--(A)- Authorization Request ->|   Resource    |
     |        |                               |     Owner     |
     |        |<-(B)-- Authorization Grant ---|               |
     |        |                               +---------------+
     |        |
     |        |                               +---------------+
     |        |--(C)-- Authorization Grant -->| Authorization |
     | Client |                               |     Server    |
     |        |<-(D)----- Access Token -------|               |
     |        |                               +---------------+
     |        |
     |        |                               +---------------+
     |        |--(E)----- Access Token ------>|    Resource   |
     |        |                               |     Server    |
     |        |<-(F)--- Protected Resource ---|               |
     +--------+                               +---------------+

A security token with the property that any party in possession of the token (a “bearer”) can use the token in any way that any other party in possession of it can. Using a bearer token does not require a bearer to prove possession of cryptographic key material (proof-of-possession).

因此Bearer認證的核心是Token,Bearer驗證中的憑證稱為BEARER_TOKEN,或者是access_token,它的頒發和驗證完全由我們自己的應用程序來控制,而不依賴於系統和Web服務器,Bearer驗證的標準請求方式如下:

Authorization: Bearer [BEARER_TOKEN] 

那麼使用Bearer驗證有什麼好處呢?

  • CORS: cookies + CORS 並不能跨不同的域名。而Bearer驗證在任何域名下都可以使用HTTP header頭部來傳輸用戶信息。
  • 對移動端友好: 當你在一個原生平台(iOS, Android, WindowsPhone等)時,使用Cookie驗證並不是一個好主意,因為你得和Cookie容器打交道,而使用Bearer驗證則簡單的多。
  • CSRF: 因為Bearer驗證不再依賴於cookies, 也就避免了跨站請求攻擊。
  • 標準:在Cookie認證中,用戶未登錄時,返回一個302到登錄頁面,這在非瀏覽器情況下很難處理,而Bearer驗證則返回的是標準的401 challenge

3.JWT

上面介紹的Bearer認證,其核心便是BEARER_TOKEN,那麼,如何確保Token的安全是重中之重。一種是通過HTTPS的方式,另一種是通過對Token進行加密編碼簽名,而最流行的Token編碼簽名方式便是:JSON WEB TOKEN。

Json web token (Jwt), 是為了在網絡應用環境間傳遞聲明而執行的一種基於JSON的開放標準(RFC 7519)。該token被設計為緊湊且安全的,特別適用於分佈式站點的單點登錄(SSO)場景。JWT的聲明一般被用來在身份提供者和服務提供者間傳遞被認證的用戶身份信息,以便於從資源服務器獲取資源,也可以增加一些額外的其它業務邏輯所必須的聲明信息,該token也可直接被用於認證,也可被加密。

JWT是由.分割的如下三部分組成:

Header.Payload.Signature

還記得之前說個的一篇認證方案之初步認識JWT嗎?沒有的,可以看看,對JWT的特點和基本原理介紹,可以進一步的了解。

學習了之前的文章后,我們可以發現使用JWT的好處在於通用性、緊湊性和可拓展性。

  • 通用性:因為json的通用性,所以JWT是可以進行跨語言支持的,像JAVA,JavaScript,NodeJS,PHP等很多語言都可以使用。
  • 緊湊性:JWT的構成非常簡單,字節佔用很小,通過 GET、POST 等放在 HTTP 的 header 中,便於傳輸。
  • 可擴展性:JWT是自我包涵的,因為有了payload部分,包含了必要的一些其他業務邏輯所必要的非敏感信息,自身存儲,不需要在服務端保存會話信息, 非常易於應用的擴展。

4.開始

1. 註冊認證服務

在這裏,我們用微軟給我們提供的JwtBearer認證方式,實現認證服務註冊 。

引入nuget包:Microsoft.AspNetCore.Authentication.JwtBearer

註冊服務,將服務添加到容器中,

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();

        var Issurer = "JWTBearer.Auth";  //發行人
        var Audience = "api.auth";       //受眾人
        var secretCredentials = "q2xiARx$4x3TKqBJ";   //密鑰

        //配置認證服務
        services.AddAuthentication(x =>
        {
            x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        }).AddJwtBearer(o=>{
            o.TokenValidationParameters = new TokenValidationParameters
            {
                //是否驗證發行人
                ValidateIssuer = true,
                ValidIssuer = Issurer,//發行人
                //是否驗證受眾人
                ValidateAudience = true,
                ValidAudience = Audience,//受眾人
                //是否驗證密鑰
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(secretCredentials)),
                
                ValidateLifetime = true, //驗證生命周期
                RequireExpirationTime = true, //過期時間
            };
        });
    }

注意說明:

一. TokenValidationParameters的參數默認值:
1. ValidateAudience = true,  ----- 如果設置為false,則不驗證Audience受眾人
2. ValidateIssuer = true ,   ----- 如果設置為false,則不驗證Issuer發布人,但建議不建議這樣設置
3. ValidateIssuerSigningKey = false,
4. ValidateLifetime = true,  ----- 是否驗證Token有效期,使用當前時間與Token的Claims中的NotBefore和Expires對比
5. RequireExpirationTime = true, ----- 是否要求Token的Claims中必須包含Expires
6. ClockSkew = TimeSpan.FromSeconds(300), ----- 允許服務器時間偏移量300秒,即我們配置的過期時間加上這個允許偏移的時間值,才是真正過期的時間(過期時間 +偏移值)你也可以設置為0,ClockSkew = TimeSpan.Zero

調用方法,配置Http請求管道:

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseRouting();
        //1.先開啟認證
        app.UseAuthentication();
        //2.再開啟授權
        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }

JwtBearerOptions的配置中,通常IssuerSigningKey(簽名秘鑰), ValidIssuer(Token頒發機構), ValidAudience(頒發給誰) 三個參數是必須的,后兩者用於與TokenClaims中的IssuerAudience進行對比,不一致則驗證失敗。

2.接口資源保護

創建一個需要授權保護的資源控制器,這裏我們用建立API生成項目自帶的控制器,WeatherForecastController.cs, 在控制器上使用Authorize即可

[ApiController]
[Route("[controller]")]
[Authorize]
public class WeatherForecastController : ControllerBase
{
    private static readonly string[] Summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

    private readonly ILogger<WeatherForecastController> _logger;

    public WeatherForecastController(ILogger<WeatherForecastController> logger)
    {
        _logger = logger;
    }

    [HttpGet]
    public IEnumerable<WeatherForecast> Get()
    {
        var rng = new Random();
        return Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = rng.Next(-20, 55),
            Summary = Summaries[rng.Next(Summaries.Length)]
        })
        .ToArray();
    }
}

3. 生成Token

因為微軟為我們內置了JwtBearer驗證,但是沒有提供Token的發放,所以這裏我們要實現生成Token的方法

引入Nugets包:System.IdentityModel.Tokens.Jwt

這裏我們根據IdentityModel.Tokens.Jwt文檔給我們提供的幫助類,提供了方法WriteToken創建Token,根據參數SecurityToken,可以實例化,JwtSecurityToken,指定可選參數的類。

        /// <summary>
        /// Initializes a new instance of the <see cref="JwtSecurityToken"/> class specifying optional parameters.
        /// </summary>
        /// <param name="issuer">If this value is not null, a { iss, 'issuer' } claim will be added, overwriting any 'iss' claim in 'claims' if present.</param>
        /// <param name="audience">If this value is not null, a { aud, 'audience' } claim will be added, appending to any 'aud' claims in 'claims' if present.</param>
        /// <param name="claims">If this value is not null then for each <see cref="Claim"/> a { 'Claim.Type', 'Claim.Value' } is added. If duplicate claims are found then a { 'Claim.Type', List&lt;object&gt; } will be created to contain the duplicate values.</param>
        /// <param name="expires">If expires.HasValue a { exp, 'value' } claim is added, overwriting any 'exp' claim in 'claims' if present.</param>
        /// <param name="notBefore">If notbefore.HasValue a { nbf, 'value' } claim is added, overwriting any 'nbf' claim in 'claims' if present.</param>
        /// <param name="signingCredentials">The <see cref="SigningCredentials"/> that will be used to sign the <see cref="JwtSecurityToken"/>. See <see cref="JwtHeader(SigningCredentials)"/> for details pertaining to the Header Parameter(s).</param>
        /// <exception cref="ArgumentException">If 'expires' &lt;= 'notbefore'.</exception>
        public JwtSecurityToken(string issuer = null, string audience = null, IEnumerable<Claim> claims = null, DateTime? notBefore = null, DateTime? expires = null, SigningCredentials signingCredentials = null)
        {
            if (expires.HasValue && notBefore.HasValue)
            {
                if (notBefore >= expires)
                    throw LogHelper.LogExceptionMessage(new ArgumentException(LogHelper.FormatInvariant(LogMessages.IDX12401, expires.Value, notBefore.Value)));
            }

            Payload = new JwtPayload(issuer, audience, claims, notBefore, expires);
            Header = new JwtHeader(signingCredentials);
            RawSignature = string.Empty;
        }

這樣,我們可以根據參數指定內容:

1. string iss = "JWTBearer.Auth";  // 定義發行人
2. string aud = "api.auth";       //定義受眾人audience
3. IEnumerable<Claim> claims = new Claim[]
{
new Claim(JwtClaimTypes.Id,"1"),
new Claim(JwtClaimTypes.Name,"i3yuan"),
};//定義許多種的聲明Claim,信息存儲部分,Claims的實體一般包含用戶和一些元數據
4. var nbf = DateTime.UtcNow;  //notBefore  生效時間
5. var Exp = DateTime.UtcNow.AddSeconds(1000);  //expires 過期時間
6. string sign = "q2xiARx$4x3TKqBJ"; //SecurityKey 的長度必須 大於等於 16個字符
 var secret = Encoding.UTF8.GetBytes(sign);
 var key = new SymmetricSecurityKey(secret);
 var signcreds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

好了,通過以上填充參數內容,進行傳參賦值得到,完整代碼如下:

新增AuthController.cs控制器:

    [HttpGet]
    public IActionResult GetToken()
    {
        try
        {
            //定義發行人issuer
            string iss = "JWTBearer.Auth";
            //定義受眾人audience
            string aud = "api.auth";

            //定義許多種的聲明Claim,信息存儲部分,Claims的實體一般包含用戶和一些元數據
            IEnumerable<Claim> claims = new Claim[]
            {
                new Claim(JwtClaimTypes.Id,"1"),
                new Claim(JwtClaimTypes.Name,"i3yuan"),
            };
            //notBefore  生效時間
            // long nbf =new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds();
            var nbf = DateTime.UtcNow;
            //expires   //過期時間
            // long Exp = new DateTimeOffset(DateTime.Now.AddSeconds(1000)).ToUnixTimeSeconds();
            var Exp = DateTime.UtcNow.AddSeconds(1000);
            //signingCredentials  簽名憑證
            string sign = "q2xiARx$4x3TKqBJ"; //SecurityKey 的長度必須 大於等於 16個字符
            var secret = Encoding.UTF8.GetBytes(sign);
            var key = new SymmetricSecurityKey(secret);
            var signcreds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
            var jwt = new JwtSecurityToken(issuer: iss, audience: aud, claims:claims,notBefore:nbf,expires:Exp, signingCredentials: signcreds);
		    var JwtHander = new JwtSecurityTokenHandler();
            var token = JwtHander.WriteToken(jwt);
            return Ok(new
            {
                access_token = token,
                token_type = "Bearer",
            });
        }
        catch (Exception ex)
        {
            throw;
        }
    }
注意:
1.SecurityKey 的長度必須 大於等於 16個字符,否則生成會報錯。(可通過在線隨機生成密鑰)

5. 運行

訪問獲取Token方法,獲取得到access_token:

再訪問,授權資源接口,可以發現,再沒有添加請求頭token值的情況下,返回了401沒有權限。

這次,在請求頭通過Authorization加上之前獲取的token值后,再次進行訪問,發現已經可以獲取訪問資源控制器,並返回對應的數據。

6.擴展說明

在HTTP標準驗證方案中,我們比較熟悉的是”Basic”和”Digest”,前者將用戶名密碼使用BASE64編碼後作為驗證憑證,後者是Basic的升級版,更加安全,因為Basic是明文傳輸密碼信息,而Digest是加密後傳輸。

一、Basic基礎認證

Basic認證是一種較為簡單的HTTP認證方式,客戶端通過明文(Base64編碼格式)傳輸用戶名和密碼到服務端進行認證,通常需要配合HTTPS來保證信息傳輸的安全。

客戶端請求需要帶Authorization請求頭,值為“Basic xxx”,xxx為“用戶名:密碼”進行Base64編碼後生成的值。 若客戶端是瀏覽器,則瀏覽器會提供一個輸入用戶名和密碼的對話框,用戶輸入用戶名和密碼后,瀏覽器會保存用戶名和密碼,用於構造Authorization值。當關閉瀏覽器后,用戶名和密碼將不再保存。

憑證為“YWxhzGRpbjpvcGVuc2VzYWl1”,是通過將“用戶名:密碼”格式的字符串經過的Base64編碼得到的。而Base64不屬於加密範疇,可以被逆向解碼,等同於明文,因此Basic傳輸認證信息是不安全的。

Basic基礎認證圖示:

缺陷匯總
1.用戶名和密碼明文(Base64)傳輸,需要配合HTTPS來保證信息傳輸的安全。
2.即使密碼被強加密,第三方仍可通過加密后的用戶名和密碼進行重放攻擊。
3.沒有提供任何針對代理和中間節點的防護措施。
4.假冒服務器很容易騙過認證,誘導用戶輸入用戶名和密碼。

二、Digest摘要認證

Digest認證是為了修復基本認證協議的嚴重缺陷而設計的,秉承“絕不通過明文在網絡發送密碼”的原則,通過“密碼摘要”進行認證,大大提高了安全性。

Digest認證步驟如下:
第一步:客戶端訪問Http資源服務器。由於需要Digest認證,服務器返回了兩個重要字段nonce(隨機數)和realm。
第二步:客戶端構造Authorization請求頭,值包含username、realm、nouce、uri和response的字段信息。其中,realm和nouce就是第一步返回的值。nouce只能被服務端使用一次。uri(digest-uri)即Request-URI的值,但考慮到經代理轉發后Request-URI的值可能被修改、因此實現會複製一份副本保存在uri內。response也可叫做Request-digest,存放經過MD5運算后的密碼字符串,形成響應碼。
第三步:服務器驗證包含Authorization值的請求,若驗證通過則可訪問資源。
Digest認證可以防止密碼泄露和請求重放,但沒辦法防假冒。所以安全級別較低。
Digest和Basic認證一樣,每次都會發送Authorization請求頭,也就相當於重新構造此值。所以兩者易用性都較差。

Digest認證圖示:

7.注意

  1. 在進行JwtBearer認證時,在生成token之後,還需要與刷新token配合使用,因為當用戶執行了退出,修改密碼等操作時,需要讓該token無效,無法再次使用,所以,會給access_token設置一個較短的有效期間,(JwtBearer認證默認會驗證有效期,通過notBeforeexpires來驗證),當access_token過期后,可以在用戶無感知的情況下,使用refresh_token重新獲取access_token,但這就不屬於Bearer認證的範疇了,但是我們可以通過另一種方式通過IdentityServer的方式來實現,在後續中會對IdentityServer進行詳細講解。
  2. 在生成token的時候,需要用的secret,主要是用來防止token被偽造與篡改。因為當token被劫取的時候,可以得到你的令牌中帶的一些個人不重要的信息明文,但不用擔心,只要你不在生成token里把私密的個人信息放出去的話,就算被動機不良的人得到,也做不了什麼事情。但是你可能會想,如果用戶自己隨便的生成一個 token ,帶上你的信息,那不就可以隨便訪問你的資源服務器了,因此這個時候就需要利用secret 來生成 token,來確保数字簽名的正確性。而且在認證授權資源,進行token解析的時候,通過微軟的源碼發現,已經幫我們封裝了方法,對secret進行了校驗了,確保了token的安全性,從而保證api資源的安全。

8.總結

  1. JwtToken在認證時,無需Security token service安全令牌服務器的參与,都是基於Claim的,默認會驗證有效期,通過notBeforeexpires來驗證,這在分佈式中提供給了極大便利。
  2. JwtToken與平台、無言無關,在前端也可以直接解析出Claims。
  3. 如果有不對的或不理解的地方,希望大家可以多多指正,提出問題,一起討論,不斷學習,共同進步。
  4. 後面會對認證授權方案中的授權這一塊進行說明分享。
  5. 本示例源碼地址
    參考JwtBearer源碼

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

網頁設計最專業,超強功能平台可客製化

聚甘新

特性速覽| Apache Hudi 0.5.3版本正式發布

1. 下載連接

  • 源代碼下載:Apache Hudi 0.5.3 Source Release (asc, sha512)
  • 0.5.3版本相關jar包地址:https://repository.apache.org/#nexus-search;quick~hudi

2. 遷移指南

  • 這是一個bugfix版本,從0.5.2升級時不需要任何特殊的遷移步驟。如果要從早期版本”X”升級,請閱讀”X”和0.5.3之間的每個後續版本的遷移指南。
  • 0.5.3是Hudi畢業后的第一個版本,因此所有hudi jar的版本名稱中不再帶有”-incubating”。在所有提及hudi版本的地方,請確保不再存在”-incubating”。

例如,hudi-spark-bundle pom依賴如下所示:

<dependency>
	<groupId>org.apache.hudi</groupId>
	<artifactId>hudi-spark-bundle_2.12</artifactId>
	<version>0.5.3</version>
</dependency>

3. 關鍵特性

  • Hudi內置支持 aliyun OSS 對象存儲。

  • 默認情況下將為delta-streamer和spark datasource寫入啟用Embedded Timeline Server。在此版本之前,此功能處於實驗模式,embeddedTimeline Server在Spark Driver中緩存文件列表,並提供Restful接口給Spark Writer任務調用來減少了每次寫入時的list文件列表的操作,此優化對雲上對象存儲非常友好。

  • 默認情況下為delta-streamer和Spark datasource寫入均啟用”增量清理(incremental cleaning)”。在此版本之前,此功能還處於實驗模式,在穩定狀態下,增量清理避免了掃描所有分區的昂貴步驟,而是使用Hudi元數據來查找要清理的文件,此優化也對雲上對象存儲非常友好。

  • 支持將Delta-Streamer配置文件放置在與實際數據不同的文件系統中。

  • Hudi Hive Sync現在支持按日期類型列分區的表。

  • Hudi Hive Sync現在支持直接通過Hive MetaStore進行同步。您只需要設置hoodie.datasource.hive_sync.use_jdbc = false。Hive Metastore Uri將從environment中隱式讀取。例如當通過Spark datasource寫入時,

     spark.write.format(“hudi”)
     .option(…)
     .option(“hoodie.datasource.hive_sync.username”, “<user>”)
     .option(“hoodie.datasource.hive_sync.password”, “<password>”)
     .option(“hoodie.datasource.hive_sync.partition_fields”, “<partition_fields>”)
     .option(“hoodie.datasource.hive_sync.database”, “<db_name>”)
     .option(“hoodie.datasource.hive_sync.table”, “<table_name>”)
     .option(“hoodie.datasource.hive_sync.use_jdbc”, “false”)
     .mode(APPEND)
     .save(“/path/to/dataset”)
    
  • 支持Presto查詢MoR表時Hudi側的改造。

  • 其他與Writer Performance相關的缺陷修復。

    • 現在DataSource Writer避免了寫入后不必要的數據加載。
    • Hudi Writer現在利用spark的併發來加速小文件查找。

4. 感謝

感謝如下貢獻者(排名不分先後): @bhasudha,@yanghua ,@ddong ,@smarthi ,@afilipchik,@zhedoubushishi,@umehrot2,@varadar,@ffcchi,@bschell,@vinothchandar ,@shenh062326,@lamber-ken,@zhaomin1423,@EdwinGuo,@prashantwason ,@pratyakshsharma,@dengziming ,@AakashPradeep,@Jecarm ,@xushiyan ,@cxzl25,@garyli1019 ,@rolandjohann ,@nsivabalan,@leesf ,@jfrazee

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※別再煩惱如何寫文案,掌握八大原則!

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

※教你寫出一流的銷售文案?

網頁設計最專業,超強功能平台可客製化

聚甘新

從linux源碼看epoll

從linux源碼看epoll

前言

在linux的高性能網絡編程中,繞不開的就是epoll。和select、poll等系統調用相比,epoll在需要監視大量文件描述符並且其中只有少數活躍的時候,表現出無可比擬的優勢。epoll能讓內核記住所關注的描述符,並在對應的描述符事件就緒的時候,在epoll的就緒鏈表中添加這些就緒元素,並喚醒對應的epoll等待進程。
本文就是筆者在探究epoll源碼過程中,對kernel將就緒描述符添加到epoll並喚醒對應進程的一次源碼分析(基於linux-2.6.32內核版本)。由於篇幅所限,筆者聚焦於tcp協議下socket可讀事件的源碼分析。

簡單的epoll例子

下面的例子,是從筆者本人用c語言寫的dbproxy中的一段代碼。由於細節過多,所以做了一些刪減。

int init_reactor(int listen_fd,int worker_count){
	......
	// 創建多個epoll fd,以充分利用多核
	for(i=0;i<worker_count;i++){
		reactor->worker_fd = epoll_create(EPOLL_MAX_EVENTS);
	}
	/* epoll add listen_fd and accept */
	// 將accept后的事件加入到對應的epoll fd中
	int client_fd = accept(listen_fd,(struct sockaddr *)&client_addr,&client_len)));
	// 將連接描述符註冊到對應的worker裏面
	epoll_ctl(reactor->client_fd,EPOLL_CTL_ADD,epifd,&event);
}
// reactor的worker線程
static void* rw_thread_func(void* arg){
	......

	for(;;){
		  // epoll_wait等待事件觸發
        int retval = epoll_wait(epfd,events,EPOLL_MAX_EVENTS,500);
        if(retval > 0){
        	for(j=0; j < retval; j++){
        		// 處理讀事件
        	   if(event & EPOLLIN){
                 handle_ready_read_connection(conn);
                 continue;
             }
             /* 處理其它事件 */
        	}
        }
	}
	......
}

上述代碼事實上就是實現了一個reactor模式中的accept與read/write處理線程,如下圖所示:

epoll_create

Unix的萬物皆文件的思想在epoll裏面也有體現,epoll_create調用返回一個文件描述符,此描述符掛載在anon_inode_fs(匿名inode文件系統)的根目錄下面。讓我們看下具體的epoll_create系統調用源碼:

SYSCALL_DEFINE1(epoll_create, int, size)
{
	if (size <= 0)
		return -EINVAL;

	return sys_epoll_create1(0);
}

由上述源碼可見,epoll_create的參數是基本沒有意義的,kernel簡單的判斷是否為0,然後就直接就調用了sys_epoll_create1。由於linux的系統調用是通過(SYSCALL_DEFINE1,SYSCALL_DEFINE2……SYSCALL_DEFINE6)定義的,那麼sys_epoll_create1對應的源碼即是SYSCALL_DEFINE(epoll_create1)。
(注:受限於寄存器數量的限制,(80×86下的)kernel限制系統調用最多有6個參數。據ulk3所述,這是由於32位80×86寄存器的限制)
接下來,我們就看下epoll_create1的源碼:

SYSCALL_DEFINE1(epoll_create1, int, flags)
{
	// kzalloc(sizeof(*ep), GFP_KERNEL),用的是內核空間
	error = ep_alloc(&ep);
	// 獲取尚未被使用的文件描述符,即描述符數組的槽位
	fd = get_unused_fd_flags(O_RDWR | (flags & O_CLOEXEC));
	// 在匿名inode文件系統中分配一個inode,並得到其file結構體
	// 且file->f_op = &eventpoll_fops
	// 且file->private_data = ep;
	file = anon_inode_getfile("[eventpoll]", &eventpoll_fops, ep,
				 O_RDWR | (flags & O_CLOEXEC));
	// 將file填入到對應的文件描述符數組的槽裏面
	fd_install(fd,file);			 
	ep->file = file;
	return fd;
}

最後epoll_create生成的文件描述符如下圖所示:

struct eventpoll

所有的epoll系統調用都是圍繞eventpoll結構體做操作,現簡要描述下其中的成員:

/*
 * 此結構體存儲在file->private_data中
 */
struct eventpoll {
	// 自旋鎖,在kernel內部用自旋鎖加鎖,就可以同時多線(進)程對此結構體進行操作
	// 主要是保護ready_list
	spinlock_t lock;
	// 這個互斥鎖是為了保證在eventloop使用對應的文件描述符的時候,文件描述符不會被移除掉
	struct mutex mtx;
	// epoll_wait使用的等待隊列,和進程喚醒有關
	wait_queue_head_t wq;
	// file->poll使用的等待隊列,和進程喚醒有關
	wait_queue_head_t poll_wait;
	// 就緒的描述符隊列
	struct list_head rdllist;
	// 通過紅黑樹來組織當前epoll關注的文件描述符
	struct rb_root rbr;
	// 在向用戶空間傳輸就緒事件的時候,將同時發生事件的文件描述符鏈入到這個鏈表裡面
	struct epitem *ovflist;
	// 對應的user
	struct user_struct *user;
	// 對應的文件描述符
	struct file *file;
	// 下面兩個是用於環路檢測的優化
	int visited;
	struct list_head visited_list_link;
};

本文講述的是kernel是如何將就緒事件傳遞給epoll並喚醒對應進程上,因此在這裏主要聚焦於(wait_queue_head_t wq)等成員。

epoll_ctl(add)

我們看下epoll_ctl(EPOLL_CTL_ADD)是如何將對應的文件描述符插入到eventpoll中的。
藉助於spin_lock(自旋鎖)和mutex(互斥鎖),epoll_ctl調用可以在多個KSE(內核調度實體,即進程/線程)中併發執行。

SYSCALL_DEFINE4(epoll_ctl, int, epfd, int, op, int, fd,
		struct epoll_event __user *, event)
{
	/* 校驗epfd是否是epoll的描述符 */
	// 此處的互斥鎖是為了防止併發調用epoll_ctl,即保護內部數據結構
	// 不會被併發的添加修改刪除破壞
	mutex_lock_nested(&ep->mtx, 0);
	switch (op) {
		case EPOLL_CTL_ADD:
			...
			// 插入到紅黑樹中
			error = ep_insert(ep, &epds, tfile, fd);
			...
			break;
		......
	}
	mutex_unlock(&ep->mtx);	
}		

上述過程如下圖所示:

ep_insert

在ep_insert中初始化了epitem,然後初始化了本文關注的焦點,即事件就緒時候的回調函數,代碼如下所示:

static int ep_insert(struct eventpoll *ep, struct epoll_event *event,
		     struct file *tfile, int fd)
{
	/* 初始化epitem */
	// &epq.pt->qproc = ep_ptable_queue_proc
	init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);
	// 在這裏將回調函數注入
	revents = tfile->f_op->poll(tfile, &epq.pt);
	// 如果當前有事件已經就緒,那麼一開始就會被加入到ready list
	// 例如可寫事件
	// 另外,在tcp內部ack之後調用tcp_check_space,最終調用sock_def_write_space來喚醒對應的epoll_wait下的進程
	if ((revents & event->events) && !ep_is_linked(&epi->rdllink)) {
		list_add_tail(&epi->rdllink, &ep->rdllist);
		// wake_up ep對應在epoll_wait下的進程
		if (waitqueue_active(&ep->wq)){
			wake_up_locked(&ep->wq);
		}
		......
	}	
	// 將epitem插入紅黑樹
	ep_rbtree_insert(ep, epi);
	......
}

tfile->f_op->poll的實現

向kernel更底層註冊回調函數的是tfile->f_op->poll(tfile, &epq.pt)這一句,我們來看一下對於對應的socket文件描述符,其fd=>file->f_op->poll的初始化過程:

    // 將accept后的事件加入到對應的epoll fd中
    int client_fd = accept(listen_fd,(struct sockaddr *)&client_addr,&client_len)));
    // 將連接描述符註冊到對應的worker裏面
    epoll_ctl(reactor->client_fd,EPOLL_CTL_ADD,epifd,&event);

回顧一下上述user space代碼,fd即client_fd是由tcp的listen_fd通過accept調用而來,那麼我們看下accept調用鏈的關鍵路徑:

accept
      |->accept4
            |->sock_attach_fd(newsock, newfile, flags & O_NONBLOCK);
                  |->init_file(file,...,&socket_file_ops);
                        |->file->f_op = fop;
                              /* file->f_op = &socket_file_ops */
            |->fd_install(newfd, newfile); // 安裝fd

那麼,由accept獲得的client_fd的結構如下圖所示:

(注:由於是tcp socket,所以這邊sock->ops=inet_stream_ops,這個初始化的過程在我的另一篇博客<<從linux源碼看socket的阻塞和非阻塞>>中,博客地址如下:
https://my.oschina.net/alchemystar/blog/1791017)
既然知道了tfile->f_op->poll的實現,我們就可以看下此poll是如何將安裝回調函數的。

回調函數的安裝

kernel的調用路徑如下:

sock_poll /*tfile->f_op->poll(tfile, &epq.pt)*/;
	|->sock->ops->poll
		|->tcp_poll
			/* 這邊重要的是拿到了sk_sleep用於KSE(進程/線程)的喚醒 */
			|->sock_poll_wait(file, sk->sk_sleep, wait);
				|->poll_wait
					|->p->qproc(filp, wait_address, p);
					/* p為&epq.pt,而且&epq.pt->qproc= ep_ptable_queue_proc*/
						|-> ep_ptable_queue_proc(filp,wait_address,p);

繞了一大圈之後,我們的回調函數的安裝其實就是調用了eventpoll.c中的ep_ptable_queue_proc,而且向其中傳遞了sk->sk_sleep作為其waitqueue的head,其源碼如下所示:

static void ep_ptable_queue_proc(struct file *file, wait_queue_head_t *whead,
				 poll_table *pt)
{
	// 取出當前client_fd對應的epitem
	struct epitem *epi = ep_item_from_epqueue(pt);
	// &pwq->wait->func=ep_poll_callback,用於回調喚醒
	// 注意,這邊不是init_waitqueue_entry,即沒有將當前KSE(current,當前進程/線程)寫入到
	// wait_queue當中,因為不一定是從當前安裝的KSE喚醒,而應該是喚醒epoll\_wait的KSE
	init_waitqueue_func_entry(&pwq->wait, ep_poll_callback);
	// 這邊的whead是sk->sk_sleep,將當前的waitqueue鏈入到socket對應的sleep列表
	add_wait_queue(whead, &pwq->wait);	
}	

這樣client_fd的結構進一步完善,如下圖所示:

ep_poll_callback函數是喚醒對應epoll_wait的地方,我們將在後面一起講述。

epoll_wait

epoll_wait主要是調用了ep_poll:

SYSCALL_DEFINE4(epoll_wait, int, epfd, struct epoll_event __user *, events,
		int, maxevents, int, timeout)
{
	/* 檢查epfd是否是epoll\_create創建的fd */
	// 調用ep_poll
	error = ep_poll(ep, events, maxevents, timeout);
	...
}

緊接着,我們看下ep_poll函數:

static int ep_poll(struct eventpoll *ep, struct epoll_event __user *events,
		   int maxevents, long timeout)
{
	......
retry:
	// 獲取spinlock
	spin_lock_irqsave(&ep->lock, flags);
	// 將當前task_struct寫入到waitqueue中以便喚醒
	// wq_entry->func = default_wake_function;
	init_waitqueue_entry(&wait, current);
	// WQ_FLAG_EXCLUSIVE,排他性喚醒,配合SO_REUSEPORT從而解決accept驚群問題
	wait.flags |= WQ_FLAG_EXCLUSIVE;
	// 鏈入到ep的waitqueue中
	__add_wait_queue(&ep->wq, &wait);
	for (;;) {
		// 設置當前進程狀態為可打斷
		set_current_state(TASK_INTERRUPTIBLE);
		// 檢查當前線程是否有信號要處理,有則返回-EINTR
		if (signal_pending(current)) {
			res = -EINTR;
			break;
		}
		spin_unlock_irqrestore(&ep->lock, flags);
		// schedule調度,讓出CPU
		jtimeout = schedule_timeout(jtimeout);
		spin_lock_irqsave(&ep->lock, flags);
	}
	// 到這裏,表明超時或者有事件觸發等動作導致進程重新調度
	__remove_wait_queue(&ep->wq, &wait);
	// 設置進程狀態為running
	set_current_state(TASK_RUNNING);
	......
	// 檢查是否有可用事件
	eavail = !list_empty(&ep->rdllist) || ep->ovflist != EP_UNACTIVE_PTR;
	......
	// 向用戶空間拷貝就緒事件
	ep_send_events(ep, events, maxevents)
}		   

上述邏輯如下圖所示:

ep_send_events

ep_send_events函數主要就是調用了ep_scan_ready_list,顧名思義ep_scan_ready_list就是掃描就緒列表:

static int ep_scan_ready_list(struct eventpoll *ep,
			      int (*sproc)(struct eventpoll *,
					   struct list_head *, void *),
			      void *priv,
			      int depth)
{
	...
	// 將epfd的rdllist鏈入到txlist
	list_splice_init(&ep->rdllist, &txlist);
	...
	/* sproc = ep_send_events_proc */
	error = (*sproc)(ep, &txlist, priv);
	...
	// 處理ovflist,即在上面sproc過程中又到來的事件
	...
}

其主要調用了ep_send_events_proc:

static int ep_send_events_proc(struct eventpoll *ep, struct list_head *head,
			       void *priv)
{
	for (eventcnt = 0, uevent = esed->events;
	     !list_empty(head) && eventcnt < esed->maxevents;) {
	   // 遍歷ready list 
		epi = list_first_entry(head, struct epitem, rdllink);
		list_del_init(&epi->rdllink);
		// readylist只是表明當前epi有事件,具體的事件信息還是得調用對應file的poll
		// 這邊的poll即是tcp_poll,根據tcp本身的信息設置掩碼(mask)等信息 & 上興趣事件掩碼,則可以得知當前事件是否是epoll_wait感興趣的事件
		revents = epi->ffd.file->f_op->poll(epi->ffd.file, NULL) &
			epi->event.events;
		if(revents){
			/* 將event放入到用戶空間 */
			/* 處理ONESHOT邏輯 */
			// 如果不是邊緣觸發,則將當前的epi重新加回到可用列表中,這樣就可以下一次繼續觸發poll,如果下一次poll的revents不為0,那麼用戶空間依舊能感知 */
			else if (!(epi->event.events & EPOLLET)){
				list_add_tail(&epi->rdllink, &ep->rdllist);
			}
			/* 如果是邊緣觸發,那麼就不加回可用列表,因此只能等到下一個可用事件觸發的時候才會將對應的epi放到可用列表裡面*/
			eventcnt++
		}
		/* 如poll出來的revents事件epoll_wait不感興趣(或者本來就沒有事件),那麼也不會加回到可用列表 */
		......
	}
	return eventcnt;
}			    

上述代碼邏輯如下所示:

事件到來添加到epoll就緒隊列(rdllist)的過程

經過上述章節的詳述之後,我們終於可以闡述,tcp在數據到來時是怎麼加入到epoll的就緒隊列的了。

可讀事件到來

首先我們看下tcp數據包從網卡驅動到kernel內部tcp協議處理調用鏈:

step1:

網絡分組到來的內核路徑,網卡發起中斷後調用netif_rx將事件掛入CPU的等待隊列,並喚起軟中斷(soft_irq),再通過linux的軟中斷機制調用net_rx_action,如下圖所示:

注:上圖來自PLKA(<<深入Linux內核架構>>)

step2:

緊接着跟蹤next_rx_action

next_rx_action
	|-process_backlog
		......
			|->packet_type->func 在這裏我們考慮ip_rcv
					|->ipprot->handler 在這裏ipprot重載為tcp_protocol
						(handler 即為tcp_v4_rcv)					

我們再看下對應的tcp_v4_rcv

tcp_v4_rcv
      |->tcp_v4_do_rcv
            |->tcp_rcv_state_process
                  |->tcp_data_queue
                        |-> sk->sk_data_ready(sock_def_readable)
                              |->wake_up_interruptible_sync_poll(sk->sleep,...)
                                    |->__wake_up
                                          |->__wake_up_common
                                                |->curr->func
                                                /* 這裏已經被ep_insert添加為ep_poll_callback,而且設定了排它標識WQ_FLAG_EXCLUSIVE*/
                                                      |->ep_poll_callback

這樣,我們就看下最終喚醒epoll_wait的ep_poll_callback函數:

static int ep_poll_callback(wait_queue_t *wait, unsigned mode, int sync, void *key)
{
	// 獲取wait對應的epitem	
	struct epitem *epi = ep_item_from_wait(wait);
	// epitem對應的eventpoll結構體
	struct eventpoll *ep = epi->ep;
	// 獲取自旋鎖,保護ready_list等結構
	spin_lock_irqsave(&ep->lock, flags);
	// 如果當前epi沒有被鏈入ep的ready list,則鏈入
	// 這樣,就把當前的可用事件加入到epoll的可用列表了
	if (!ep_is_linked(&epi->rdllink))
		list_add_tail(&epi->rdllink, &ep->rdllist);
	// 如果有epoll_wait在等待的話,則喚醒這個epoll_wait進程
	// 對應的&ep->wq是在epoll_wait調用的時候通過init_waitqueue_entry(&wait, current)而生成的
	// 其中的current即是對應調用epoll_wait的進程信息task_struct
	if (waitqueue_active(&ep->wq))
		wake_up_locked(&ep->wq);
}

上述過程如下圖所示:

最後wake_up_locked調用__wake_up_common,然後調用了在init_waitqueue_entry註冊的default_wake_function,調用路徑為:

wake_up_locked
	|->__wake_up_common
		|->default_wake_function
			|->try_wake_up (wake up a thread)
				|->activate_task
					|->enqueue_task    running

將epoll_wait進程推入可運行隊列,等待內核重新調度進程,然後epoll_wait對應的這個進程重新運行后,就從schedule恢復,繼續下面的ep_send_events(向用戶空間拷貝事件並返回)。
wake_up過程如下圖所示:

可寫事件到來

可寫事件的運行過程和可讀事件大同小異:
首先,在epoll_ctl_add的時候預先會調用一次對應文件描述符的poll,如果返回事件里有可寫掩碼的時候直接調用wake_up_locked以喚醒對應的epoll_wait進程。
然後,在tcp在底層驅動有數據到來的時候可能攜帶了ack從而可以釋放部分已經被對端接收的數據,於是觸發可寫事件,這一部分的調用鏈為:

tcp_input.c
tcp_v4_rcv
	|-tcp_v4_do_rcv
		|-tcp_rcv_state_process
			|-tcp_data_snd_check
				|->tcp_check_space
					|->tcp_new_space
						|->sk->sk_write_space
						/* tcp下即是sk_stream_write_space*/

最後在此函數裏面sk_stream_write_space喚醒對應的epoll_wait進程

void sk_stream_write_space(struct sock *sk)
{
	// 即有1/3可寫空間的時候才觸發可寫事件
	if (sk_stream_wspace(sk) >= sk_stream_min_wspace(sk) && sock) {
		clear_bit(SOCK_NOSPACE, &sock->flags);

		if (sk->sk_sleep && waitqueue_active(sk->sk_sleep))
			wake_up_interruptible_poll(sk->sk_sleep, POLLOUT |
						POLLWRNORM | POLLWRBAND)
		......
	}
}

關閉描述符(close fd)

值得注意的是,我們在close對應的文件描述符的時候,會自動調用eventpoll_release將對應的file從其關聯的epoll_fd中刪除,kernel關鍵路徑如下:

close fd
      |->filp_close
            |->fput
                  |->__fput
                        |->eventpoll_release
                              |->ep_remove

所以我們在關閉對應的文件描述符后,並不需要通過epoll_ctl_del來刪掉對應epoll中相應的描述符。

總結

epoll作為linux下非常優秀的事件觸發機製得到了廣泛的運用。其源碼還是比較複雜的,本文只是闡述了epoll讀寫事件的觸發機制,探究linux kernel源碼的過程非常快樂_

公眾號

關注筆者公眾號,獲取更多乾貨文章:

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

網頁設計最專業,超強功能平台可客製化

聚甘新

二叉樹的創建與遍歷(遞歸實現)

在樹的基本概念和術語總結一文中介紹了二叉樹的基本結構。

在不知道怎樣用遞歸?按步驟來!一文中介紹了如何使用遞歸。

二叉樹的結構是遞歸的,所以創建、遍歷也可以通過遞歸實現。

下面是一顆二叉樹:

結點的定義:

public class Node {
    Integer value;
    Node leftChild;
    Node rightChild;

    public Node(Integer value) {
        this.value = value;
    }
}

創建

各個結點的值用一個ArrayList集合來保存,根據該集合來創建二叉樹。

按照不知道怎樣用遞歸?按步驟來!中的方法分析如何遞歸地創建一顆二叉樹。

第一步:找到大問題是什麼?

創建一顆二叉樹。

private Node createBinaryTree(ArrayList<Integer> inputList) {
        
}

第二步:找到最簡單問題是什麼?滿足最簡單問題時應該做什麼?

「創建一個空二叉樹」是最簡單的問題,當滿足時,直接返回null

private Node createBinaryTree(ArrayList<Integer> inputList) {   
    if (inputList == null || inputList.isEmpty()) {//最簡單問題
        return null;
    }
}

第三步:找到重複邏輯是什麼?

因為我們把每個結點的值都放在ArrayList集合中了,所以,每創建一個二叉樹結點,都需要從集合中拿值。

對於每個結點而言,它一定有左孩子和右孩子(上圖中結點3的左孩子和右孩子可以看成「值為null的結點」),

所以要確定每個結點的左孩子和右孩子是誰。

所以重複邏輯是:

  1. 從集合中拿值,創建結點。
  2. 確定該結點的左孩子和右孩子。
//大問題
private Node createBinaryTree(ArrayList<Integer> inputList) {
    if (inputList == null || inputList.isEmpty()) {//最簡單問題
        return null;
    }
    Node node = null;//重複邏輯
    Integer value = inputList.remove(0);//重複邏輯
    if (value != null) {
        node = new Node(value);//重複邏輯
        node.leftChild = ?;//重複邏輯
        node.rightChild = ?;//重複邏輯
    }

}

第四步:自己調用自己

先解釋一下上個代碼片段中的問號。

要確定一個結點的左孩子和右孩子是誰,其實就是一個賦值操作,那麼就一定要先有一些可選的結點

比如說,如果我們要確定結點1的左右孩子,那麼結點2、結點5就必須已經被創建出來了,這樣才能進行賦值操作。

那麼如何在進行賦值操作之前創建結點2、結點5呢?答案是自己調用自己。

我們可以把結點2、結點5看成另一顆二叉樹的根結點,只要我們創建好以結點2或結點5為根結點的二叉樹,那麼結點2和結點5自然就被創建出來了。

確定結點2和結點5的左右孩子同理,這樣一直分解下去,直到分解成最簡單問題,或者從集合中拿到null為止。

注意:自己調用自己時參數的變小是通過inputList.remove(0)實現的。

//大問題
private Node createBinaryTree(ArrayList<Integer> inputList) {
    if (inputList == null || inputList.isEmpty()) {//最簡單問題
        return null;
    }
    Node node = null;//重複邏輯
    Integer value = inputList.remove(0);//重複邏輯
    if (value != null) {
        node = new Node(value);//重複邏輯
        node.leftChild = createBinaryTree(inputList);//重複邏輯,自己調用自己
        node.rightChild = createBinaryTree(inputList);//重複邏輯,自己調用自己
    }

}

第五步:返回

返回的是根結點,該根結點被確定為左孩子或右孩子,從而構成一顆更大的二叉樹,直到滿足最大問題的那顆二叉樹被創建成功,此時返回的根結點是真正的解。

//大問題
private Node createBinaryTree(ArrayList<Integer> inputList) {
    if (inputList == null || inputList.isEmpty()) {//最簡單問題
        return null;
    }
    Node node = null;//重複邏輯
    Integer value = inputList.remove(0);//重複邏輯
    if (value != null) {
        node = new Node(value);//重複邏輯
        node.leftChild = createBinaryTree(inputList);//重複邏輯,自己調用自己
        node.rightChild = createBinaryTree(inputList);//重複邏輯,自己調用自己
    }
	return node;//返回
}

遍歷

先序遍歷

第一步:找到大問題是什麼?

先序遍歷一顆二叉樹,打印出每個結點的值。

public void preOrderTraveral(Node node) {
    
}

第二步:找到最簡單問題是什麼?滿足最簡單問題時應該做什麼?

「遍歷一顆空二叉樹」是最簡單問題,此時任何操作都不用做。

public void preOrderTraveral(Node node) {
    if (node == null) {//最簡單問題
        return;
    }
}

第三步:找到重複邏輯是什麼?

打印每個結點的值

public void preOrderTraveral(Node node) {
    if (node == null) {//最簡單問題
        return;
    }
    System.out.print(node.value);//重複邏輯
}

第四步:自己調用自己

先序遍歷的過程:

  1. 遍歷根結點
  2. 先序遍歷左子樹
  3. 先序遍歷右子樹
public void preOrderTraveral(Node node) {
    if (node == null) {//最簡單問題
        return;
    }
    System.out.print(node.value);//重複邏輯
    preOrderTraversal(node.leftChild);//自己調用自己
    preOrderTraversal(node.rightChild);//自己調用自己
}

自己調用自己時參數通過node.leftChildnode.rightChild不斷變小

第五步:返回

不需要返回值。

中序遍歷和後序遍歷同理

完整代碼

//二叉樹結點
public class Node {
    Integer value;
    Node leftChild;
    Node rightChild;

    public Node(Integer value) {
        this.value = value;
    }
}
//二叉樹
public class BinaryTree {

    private Node root;

    public Node getRoot() {
        return root;
    }

    public BinaryTree(ArrayList<Integer> inputList) {
        Node root = createBinaryTree(inputList);
        this.root = root;
    }

	//創建二叉樹
    private Node createBinaryTree(ArrayList<Integer> inputList) {
        if (inputList == null || inputList.isEmpty()) {
            return null;
        }
        Node node = null;
        Integer value = inputList.remove(0);
        if (value != null) {
            node = new Node(value);
            node.leftChild = createBinaryTree(inputList);
            node.rightChild = createBinaryTree(inputList);
        }
        return node;
    }

    //先序遍歷
    public void preOrderTraversal(Node node) {
        if (node == null) {
            return;
        }
        System.out.print(node.value);
        preOrderTraversal(node.leftChild);
        preOrderTraversal(node.rightChild);
    }
	
    //中序遍歷
    public void inOrderTraversal(Node node) {
        if (node == null) {
            return;
        }
        inOrderTraversal(node.leftChild);
        System.out.print(node.value);
        inOrderTraversal(node.rightChild);
    }

    //後序遍歷
    public void postOrderTraversal(Node node) {
        if (node == null) {
            return;
        }
        postOrderTraversal(node.leftChild);
        postOrderTraversal(node.rightChild);
        System.out.print(node.value);
    }
}
//測試
public static void main(String[] args) {
    List<Integer> list = Arrays.asList(new Integer[]{1, 2, 3, null, null, 4, null, null, 5, null, 6});
    ArrayList inputList = new ArrayList(list);

    BinaryTree binaryTree = new BinaryTree(inputList);
    Node root = binaryTree.getRoot();
    System.out.print("先序遍歷:");
    binaryTree.preOrderTraversal(root);
    System.out.print("\n中序遍歷:");
    binaryTree.inOrderTraversal(root);
    System.out.print("\n後序遍歷:");
    binaryTree.postOrderTraversal(root);
}

如有錯誤,還請指正。

文章首發於微信公眾號『行人觀學』

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

網頁設計最專業,超強功能平台可客製化

聚甘新

昶洧將於桃園建電池包場,望2018可推首款量產車

昶洧日前舉行法說會說明近期電動車發展成果與展望,公司董事長沈瑋表示,即將在桃園觀音興建電池包廠,並預計第一台試製樣車 (Beta Car) 可在今年三月出爐,最快 2018 年底發表首款量產車。

針對電動車研發近況,沈瑋表示,公司測試路車 (Alpha Car) 進入路測階段,目前已測試 200 多次,仍持續地蒐集路測數據進行分析,最佳測試風阻係數為 0.23,比 FF 推出的新車還低,是目前風阻係數最低的電動轎車。

他也指出,公司與大陸贛州發改委合作的第一期廠房已建設完成,預計四月份設備將會安裝完畢,而贛州廠試製車間也已完工並開始運作。此外,也已與西班牙巴塞隆納區洽談合作建廠計劃,蘇格蘭政府也看中昶洧技術並邀請去當地設立研發中心,顯見公司卓越的技術已深獲大陸與歐洲政府的看重並積極提出合作的方案。

昶洧也指出,公司計劃在台灣桃園觀音鄉自有土地 2 千多坪上設置電池包廠,另投資新台幣 3 億元建廠房及購置機器設備,因技術均為自有研發技術及自有土地,預計毛利率相對較高,有助於業務開展,電池包量產銷售後可維持台灣地區的營運開支所需;電池包廠將於今年動工,年產量約計 8,000~12,000 片,這部分雖然只能供應贛州昶洧新能源公司十分之一所需,但不會全數供應自家車廠,仍會考慮部份出售給其他車廠。

關於昶洧新車展示及上市計畫,沈瑋則指出,除了一至二月在中國贛州廠展示外,三月底會在香港展出,待香港展覽完成後預計將會安排來台灣展示。昶洧也預計於今年九月時參加法蘭克福車展, 2018 年三月份參加日內瓦車展,超級跑車廠預計今年底完成建廠作業;計畫超級跑車及轎車於今年下半年即可開始預購。

在製車技術方面,沈瑋表示,昶洧輕量化技術是全球獨一無二,目前已申請專利,創新底盤及電池包等多項核心技術都是自行研發,且使用專有航太模擬器進行數據模擬與測試,故節省相當多製作模具的費用,因此領先其他廠商且在開發經費上較其他廠商減少成本約 40~60%,主因是大多數的技術都是自有的。

沈瑋並表示,昶洧一直戮力於發展新興電動車產業,尤其以自行研發的電池管理系統、熱管理系統及創新底盤設計等三大核心專業技術,期許帶動台灣電動車產業供應鏈的升級,惟受台灣保守的投資環境及相關電動車產業政策不明確與未積極推動下,對於公司有心從海外募集資金回流於台灣設廠,提升台灣電動車產業發展的計畫,屢受阻礙;為了不影響公司中、長期發展及創造股東的利益,董事會已通過出售所投資之Flash Hope Holdings Limited部份股權,以獲取資金進行後續投資計劃,且昶洧所屬企業在海外募資的成果截至目前為止,在大陸已募得人民幣 12.3 億元(合台幣56.58億元),將有助全力發展當地的電動車事業。

(本文內容由授權使用。圖片出處)

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

網頁設計最專業,超強功能平台可客製化

台灣盼衝電動車款,2017電動機車目標4萬輛

台灣有2,300萬人口與1,400萬輛掛牌機車,機車普及率極高。而電動機車在其中只佔6萬輛,還有龐大的市場空間。台灣政府希望提高電動車充電站的設置密度,藉此推動電動車需求成長,目標在今年銷售四萬輛、2021年達到20萬輛。

台灣在1999年開始推動電動機車,但整體市場需求在2015年本土品牌Gogoro加入後才逐漸明朗化。2016年,台灣電動機車的銷售量較2015年增加了一倍,達2萬輛,其中有1.3萬輛是Gogoro車款;其次則是中華汽車的電動機車,2016年銷量4,600輛。在需求類別方面,從自用車到物流、外送等,都有分布。

電動車充電設施若能廣泛鋪設,就能吸引更多駕駛加入電動車款行列,在世界各地都是不變的原則。台灣經濟部長李世光對此表示,未來將繼續推動電動車充電網絡建設,目標六都每1公里就設置一座電池交換站或充電站,同時也將加強偏鄉與觀光區的充電設施建設。

目前,台灣政府補助輕型電動機車每輛新台幣7,200元,重型電動機車每輛10,000元;各地方政府也有金額不等的加碼補助。優惠措施加上充電設施普及,預期將能更有效推動需求。2017年的銷售目標是較2016年再倍增的4萬輛

電動汽車銷量如何?先看Tesla

雖然台灣2016年電動機車的銷量達到2萬輛,但對比機車總銷量80萬輛而言,只佔2.5%,還有很大的發展空間。

而2016年台灣電動車市的另一大消息,非美商TESLA來台莫屬。Tesla電動汽車已經正式在台交車,一月有80輛完成交車手續,均為Model S。其中,又以 Model S 90D車款銷量最好。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

網頁設計最專業,超強功能平台可客製化

豐田新Prius PHV可搭載太陽能面版

路透社、每日新聞報導,日本汽車業龍頭豐田汽車(Toyota)15日推出插電式油電混合車(PHV)「Prius PHV(見附圖)」新型車款,並將自當日(15日)起於日本市場開賣,售價為326萬1,600日圓-422萬2,800日圓,日本月銷售量目標為2,500台。   新款Prius PHV藉由搭載大容量鋰離子電池,在EV模式(不使用汽油)下,充飽一次電可行駛68.2km(現行車款為26.4km),且時速最高可達135km(現行車款為100km);在使用汽油行駛的情況下,每公升汽油可跑37.2km(現行車款為31.6km)。   另外,新款Prius PHV能以選配的方式搭載太陽能電池面板,在行駛時也能用太陽能充電,且新款Prius PHV支援快速充電,僅需20分鐘就可充到80%電力。

豐田於14日發布新聞稿宣布,截至2017年1月底為止旗下油電混合車(HV、包含PHV)全球累計銷售量正式突破1,000萬台大關、達1,004.85萬台,為自1997年12月開賣全球首款量產版油電車「Prius」之後、歷時約20年時間達成累計銷售量破1,000萬台的里程碑;其中日本國內銷售量為485.27萬台、北美319.04萬台、歐洲133.60萬台。   豐田指出,2016年該公司油電車全球銷售量達140.06萬台、年銷售量創史上新高紀錄。   根據嘉實XQ全球贏家系統報價,截至台北時間15日13點35分為止,豐田汽車上揚0.64%至6,495日圓。   (本文內容由授權使用。圖片出處 :Toyota)  

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

網頁設計最專業,超強功能平台可客製化

特斯拉 Model 3 估 7 月投產,拚下半年如期交車

美國電動車廠特斯拉(Tesla)周三盤後公布前季財報再度由盈轉虧,不過這並不打緊,因為按照特斯拉說法,Model 3 平價電動車有望如期交車,這才是投資人所最關心的。

特斯拉獲利表現不理想,去年第四季淨損 1.21 億美元,或相當於每股 0.78 美元,若扣除一次性費用,每股虧損降低至 0.69 美元,但還是高於分析師原預估每股虧 0.53 美元。

當季營收來到 22.8 億美元,較去年同期成長88%,且優於分析師預估的 22.2 億美元。特斯拉去年 11 月 21 日已完成太陽能面板廠 SolarCity 的收購作業,SolarCity 當季貢獻營收 1.31 億美元。

特斯拉當日宣布,Model 3 預計2017年 7 月可限量投產,9 月產量可開始放大,預估 2018 年每周可產出 10,000 輛。特斯拉原預期 Model 3 可在下半年開始交車,現在兌現承諾的機率大大提高,這激勵特斯拉股價盤後大漲 2.49%、暫報 280.02 美元。

第四季 Model S、Model X 產量合計來到 24,882 輛,年增 77%;銷售量合計為 22,252 輛,成長 27%。(紐約時報)

展望 2017 年,特斯拉預期 Model S 與 Model X 交車量將介於 47,000 至 50,000 輛,換算年成長率最高為 71%。另外,特斯拉還說太陽能車頂下半年就能開始量產。

(本文內容由授權使用。圖片出處:Public Domain CC0)

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

網頁設計最專業,超強功能平台可客製化

docker鏡像瘦身思路

docker鏡像瘦身思路

一、簡介

      docker鏡像太大,帶來了以下幾個問題:

  • 存儲開銷

      這塊影響其實不算很大,因為對服務器磁盤來說,15GB的存儲空間並不算大,除非用戶服務器的磁盤空間很緊張

  • 部署時間

      這塊影響真的很大,交付件zip包太大,導致用戶部署該產品時,花費的時間變長,客戶現場中反饋部署時間超過1.5小時,這嚴重影響用戶的體驗,降低滿意度

  • 性能不穩定

      如果客戶的服務器規格不夠(特別是磁盤讀寫性能不夠),會增大部署失敗的概率。

二、瘦身思路

       以下思路是我在該任務中嘗試使用用於鏡像瘦身的方法,均可以不同程度的降低DOcker鏡像的尺寸。

  • 清理Docker鏡像中的無用安裝包

      在Dockerfile構建Docker鏡像過程中,有可能引入臨時文件,比如:安裝包i、文件壓縮包。這些臨時文件忘記清理,導致佔據了一定的尺寸,有必要對其進行清理。

        如下Dockerfile:   

FROM xxxx/xxxx-jdk:1.0.0RUN apt-get update && apt-get install -y git maven 
mysql-client nodejs nodejs-legacy python-pip graphviz npm unzip  

Dockerfile裏面經常安裝很多工具,安裝完后,需要及時刪除安裝包緩存

(alpine) apk del openssh vim:刪除包及其依賴包

(Ubuntu) Apt-get clean:刪除所有已下載的包文件

(centos) Yum clean all: yum 會把下載的軟件包和header存儲在cache中,而不自動刪除。如果覺得佔用磁盤空間,可以使用yum clean指令進行清除,更精確 的用法是yum clean headers清除header,yum clean packages清除下載的rpm包,yum clean all一全部清除

      上面的dockerfile中在安裝工具后應該執行下: && apt-get clean && rm -rf /var/lib/apt/lists/*

RUN apt-get update && apt-get install -y git maven 
    mysql-client nodejs nodejs-legacy python-pip graphviz npm unzip && apt-get clean && rm -rf /var/lib/apt/lists/*

 

   實例:

      

FROM centos:7
RUN yum update -y RUN yum install -y wget unzip socat java-1.8.0-openjdk-headless
# Set permissions
RUN yum clean all
EXPOSE 8486

 

   修改:將黃色標示的部分改寫成如下,大小從691Mb下降到583Mb

RUN yum update -y  && yum install -y wget unzip socat java-1.8.0-openjdk-headless && yum clean all
  • 避免不必要的工具安裝

     有的Dockerfile中安裝了很多工具,這個工具的加在一起尺寸比較大,這塊需要挨個排查:客戶環境下,需不需要安裝該工具?很多工具其實是面向開發使用的,而用戶根本不會使用,那麼就沒有必要在客戶環境上使用安裝這麼工具的鏡像,應該仔細排除工具的必要性,會給鏡像瘦身帶來比較大的收益。比如, dockerfile中安裝了JDK。 這個有些情況下,完全沒必要,實際上可能jre就能搞定。

     總之,能不安裝,就不安裝;能少安裝,就少安裝;能用輕量級的工具,盡量用輕量級的工具!!!

  • 多階段構建

      Docker多階段構建是17.05以後引入的新特性,旨在解決編譯、構建複雜和鏡像大小的問題。對於多階段構建,可以在Dockerfile中使用多個FROM語句。每個FROM指令可以使用不同的基礎,並且每個指令都開始一個新的構建。您可以選擇性地將工件從一個階段複製到另一個階段,從而在最終image中只留下您想要的內容。

      如下圖所示為多階段構建的使用示例:

把多個Dockerfile合併在一塊,每個Dockerfile單獨作為一個“階段”,“階段”之間可以互相聯繫,讓后一個階段構建可以使用前一個階段構建的產物,形成一條構建階段的chain,最終結果僅產生一個image,避免產生冗餘的多個臨時images或臨時容器對象。

       1)多階段構建使用之前

      針對多階段構建的特點,分析我們產品裏面的dockerfile,如下面所示,該操作的目的是將tar包拷貝值容器內的路徑下,並解壓、執行後續操作。然而COPY層具有一定的大小,只起到臨時層的作用。(特別是這個tar包足足幾百MB!)。

 

FROM xxxx:${project.version}COPY karaf-${ccsdk.opendaylight.version}.tar.gz /tmp/
RUN mkdir /opt/opendaylight \
      && tar zxvf /tmp/karaf-${ccsdk.opendaylight.version}.tar.gz --directory /opt/opendaylight \&& rm -rf /tmp/karaf-${ccsdk.opendaylight.version}.tar.gz \ 
      && mv /opt/opendaylight/karaf-${ccsdk.opendaylight.version} /opt/opendaylight/current && mkdir -p  /opt/opendaylight/current  && ln -s  /opt/opendaylight/current /opt/opendaylight/karaf-${ccsdk.opendaylight.version}
RUN mkdir -p /opt/opendaylight/current/system/org/mariadb/jdbc/mariadb-java-client/${ccsdk.mariadb-connector-java.version}
COPY mariadb-java-client-${ccsdk.mariadb-connector-java.version}.jar /opt/opendaylight/current/system/org/mariadb/jdbc/mariadb-java-client/${ccsdk.mariadb-connector-java.version}
EXPOSE 8181   

     2)  使用多階段構建

       使用多階段構建,修改后的dockerfile如下圖所示,修改實現將第一階段拷貝並解壓好的文件複製過來即可,少了拷貝tar包的環節,這樣使得最終形成的鏡像中鏡像層數得到有效的降低,也一定程度上降低了鏡像尺寸。

FROM xxxx:${project.version} as baseFirst
COPY karaf-${ccsdk.opendaylight.version}.tar.gz /tmp/
RUN mkdir /opt/opendaylight \
      && tar zxvf /tmp/karaf-${ccsdk.opendaylight.version}.tar.gz --directory /opt/opendaylight \&& rm -rf /tmp/karaf-${ccsdk.opendaylight.version}.tar.gz \ 
      && mv /opt/opendaylight/karaf-${ccsdk.opendaylight.version} /opt/opendaylight/current 

FROM xxxxxe:${project.version} as baseSecondRUN mkdir -p  /opt/opendaylight/current  && ln -s  /opt/opendaylight/current /opt/opendaylight/karaf-${ccsdk.opendaylight.version}
COPY --from=baseFirst /opt/opendaylight/current  /opt/opendaylight/current
RUN mkdir -p /opt/opendaylight/current/system/org/mariadb/jdbc/mariadb-java-client/${ccsdk.mariadb-connector-java.version}
COPY mariadb-java-client-${ccsdk.mariadb-connector-java.version}.jar /opt/opendaylight/current/system/org/mariadb/jdbc/mariadb-java-client/${ccsdk.mariadb-connector-java.version}
EXPOSE 8181
  • Copy和賦權同時執行
FROM ubuntu:16.04
# Copy APIKeys
COPY ./messageservice/ /tmp/zookeeper/gerrit  ------A 
EXPOSE 2181 2888 3888
B------> RUN useradd $ZK_USER && [ `id -u $ZK_USER` -eq 1000 ] && [ `id -g $ZK_USER` -eq 1000 ] && chown -R $ZK_USER:$ZK_USER /opt/$ZK_DIST/ /opt/zookeeper/ /var/lib/ /var/log/ /tmp/zookeeper/    
USER $ZK_USER

      問題排查如下:A處copy的文件700MB太大,很多文件沒用到

                               B處給/tmp/zookeeper添加屬組和屬主,該層也很大

       修改:使用 COPY –chown=1000:1000   ./messageservice/ /tmp/zookeeper/gerrit, 鏡像大小從1.4GB 下降到700Mb

  • 鏡像層的復用

      這一塊修改得當的話,得到的收益是最大的!!!最大的!!!最大的!!!

      我們知道docker鏡像具有層級結構,如果很多鏡像具有相同的層,則這些相同的層就能得到復用(把多個鏡像生成一個tar),docker不會保存兩份相同放入層文件,通過提高層得復用能顯著降低整體的鏡像尺寸。比如常見方法有:替換統一的基礎鏡像、創建出統一的基礎鏡像、調整層的順序等等。這裏東西沒有整理,就不上圖了,可以自行腦補,查閱資料即可

我這邊經過這一步調整后,zip產品包從11.45GB下降到6.96GB

 

最終zip包從15GB下降到7GB

    

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

網頁設計最專業,超強功能平台可客製化

報告指亞馬遜雨林去年火災次數飆升三成

摘錄自2020年1月9日星島日報報導

巴西國家太空研究院(INPE)周三發表一份報告顯示,2019年全年在南美洲亞馬遜雨林內發生的火災,比對上一年大幅上升30%。

根據 INPE 提供的數據,在2019年,在亞馬遜地區偵測到的火災多達8萬9178宗,比2018年的6萬8345宗大幅增加。不過,這個數字仍然低於10萬9630宗的歷史性平均紀錄。

亞馬遜森林去年發生的特大山火,引起國際社會廣泛關注。巴西總統博爾索納羅的政策和立場受到各方抨擊,指他上台後只顧經濟利益,把環保及全球氣候暖化的問題置之不理。巴西在2019年斬伐林木及開墾土地的數字,創下10年來新高。如何保護亞馬遜森林成為一個緊急議題。

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

網頁設計最專業,超強功能平台可客製化