分類
發燒車訊

這些國產車都用寶馬發動機了!賣不好因為我們不識貨?

還是那句話,用什麼發動機並不是決定銷量好壞的首要因素。有“東瀛寶馬”之稱的馬自達,之所以被不少車迷們津津樂道,除了擁有媲美寶馬的操控,更是因為馬自達掌握不少令“黑科技”眾多的本田都羡慕的技術。創馳藍天(SKYACTIV Technology)則是其中的代表。

叫獸常說選車主要看三大件 – 發動機、變速箱和底盤。作為汽車的“心臟”,發動機的重要性不言而喻,如今各品牌之間都會選擇共享發動機的戰略,以實現共贏。

不少自主品牌車型搭載了其它合資品牌發動機,甚至還有用上寶馬發動機的,但大部分只是空有一顆“最強心”,銷量並沒有達到預期,一起來看看吧。

華頌,這個非常陌生的名字,是華晨集團旗下一個新的自主品牌,其首款車型是進軍中高檔市場的MpV – 華頌7。這款車搭載的引擎相信每一位汽車愛好者都不會陌生,來自BMW非常經典的N20系列發動機,如今居然“下放”到一款國產MpV身上,的確出乎意料。

在這裏就不過多介紹N20的技術亮點,大家只需要知道這款曾榮獲“2011年全球十佳引擎”的發動機近幾年為寶馬立下了汗馬功勞,不過從去年開始BMW的新車型開始陸續換裝全新的“B系列”發動機。

華頌7能搭載BMW的發動機無疑是令華頌非常得意的宣傳點,但在一個全新的國產品牌面前,23.77 – 28.77萬的指導價顯然是蒼白無力的,這也註定了其銷量慘淡的命運。

叫獸說

既然有這樣的好背景,更應該腳踏實地從基礎做起,飯得一口一口的吃,路要一步一步的走。一個全新的國產品牌想憑一台發動機走天下未免有些天真。

說到BMW發動機,就不得不提到由pSA集團(標緻雪鐵龍)與BMW共同研發的THp發動機。

雪鐵龍C4L、標緻408以及MINI等車型均搭載了這款1.6THp發動機,似乎是裝上了有BMW基因的發動機令雪鐵龍很自信,宣傳C4L的時候喜歡弄“百公里加速”、“汽車拔河”這些營銷手段,眼球是賺到了,但對銷量和口碑似乎並沒有起到太大作用。

合作都是為了實現共贏,這款發動機的確融合了兩大集團眾多技術優勢。站在市場角度,BMW減少了研發入門發動機的成本,而能攀上BMW這個“高枝兒”對於標緻雪鐵龍自然是件求之不得的事情。

叫獸說

豪華品牌與普通品牌之間合作本是一件互補的事情,但例如C4L,有一個勁吹噓發動機的功夫,倒不如在其它地方多花心思,以做到全面發展從而贏得銷量。還是那句話,用什麼發動機並不是決定銷量好壞的首要因素。

有“東瀛寶馬”之稱的馬自達,之所以被不少車迷們津津樂道,除了擁有媲美寶馬的操控,更是因為馬自達掌握不少令“黑科技”眾多的本田都羡慕的技術。創馳藍天(SKYACTIV Technology)則是其中的代表。

來自一汽的奔騰便與馬自達有着扯不斷的關係,估計你很難想到奔騰B70這款毫無特色的車居然與非常經典的馬自達6同平台打造。除了發動機之外(非創馳藍天),變速箱、底盤等等均出自馬6,可以說B70就是一款換殼換標的馬6。但事實上從行駛質感到做工品質以及油耗等方面B70並沒有表現出馬6的水準,在不少一線自主品牌對手面前,與馬6同平台打造這個“老掉牙”的賣點也顯得底氣不足。

叫獸說

作為自主品牌里資源最豐富的一汽,如果能少些“腐氣”,多些用心造車的士氣,未來在競爭中自然更有底氣。

說到一汽,不免想到另一位“皇城下”的北汽。北汽紳寶高價收購了瀕臨破產的薩博的大部分技術,隨後推出了自主品牌中又一款進軍中高級車市場的紳寶D70。

D70全系搭載的均是渦輪增壓發動機,從動力參數上看在眾多自主品牌競品里算得上是優秀水平,但隨之而來較高的油耗是網友普遍反映的問題。除此外,較高的售價、無特色的外觀和較為粗糙的做工註定了這款車的命運。

叫獸說

看看捷豹路虎之於奇瑞、沃爾沃之於吉利,反觀薩博之於紳寶,自主品牌在收購國外品牌之後究竟應該怎樣將價值發揮到最大是北汽需要深思並解決的問題。

除了上述幾個品牌外,還有不少自主車型直接向國外品牌採購發動機的例子。來自瀋陽航天三菱發動機出品的“4A”系列和“4G”系列發動機普遍搭載於自主品牌車型上。

(搭載於陸風X5上的三菱4G63S4T發動機)

不僅有陸風、眾泰、海馬、東南等等品牌車型採購過三菱發動機外,哈弗H6是最典型也是最成功的例子。憑藉中庸耐看的外觀、合理的售價以及較可靠的品質,H6成為月銷超5萬的“神車”。為何同樣都是“三菱心”,命運卻有如此大的差別,這是其它品牌需要思考和學習的地方。

希望中國品牌能早日研發出真正屬於自己的優秀髮動機,屆時掌核心技術后,才能挺直了腰板說一聲“Made in China”而不是“OEM in China”。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

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

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

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

分類
發燒車訊

發動機不磨合?機油燒死你!

還說買了性能車,直接來一發暴力磨合。然而事實是,出廠前就磨合好的車只是佔少數,同時暴力磨合也是一種不合理的磨合方式。所以,作為普通車主,還是不要搞什麼幺蛾子了。另外,磨合的時候多跑高速也是有特定條件的。

大家常聽老司機說新車要磨合,有所謂的磨合期,那麼磨合到底是什麼鬼?

發動機是磨合過程中最核心的部分,為了讓內部的活塞和發動機缸體達到完美的活塞運動,這時候就要在它們之間放置活塞環和缸套。

而磨合的作用,是要讓活塞環與缸套的內壁完美地磨合密封。

但是因為活塞環有彈性,如果金屬內壁不夠平滑,那就很容易出現各種磨損,最明顯的一個傷害就是燒機油。所以,所謂磨合期,就是要慢慢的讓發動機進行活塞運動,然後讓零件間的接合面打磨得更加貼合,達到最佳狀態。

不過長期以來,各種鍵盤車神和老司機間都流傳着一些以訛傳訛的說法,比如說新車早就在廠里磨合好,買回來就儘管飆。還說買了性能車,直接來一發暴力磨合。

然而事實是,出廠前就磨合好的車只是佔少數,同時暴力磨合也是一種不合理的磨合方式。所以,作為普通車主,還是不要搞什麼幺蛾子了。

另外,磨合的時候多跑高速也是有特定條件的。在跑的過程中多切換不同的擋位和油門深淺,同是不要貪新鮮開個幾百米就算,這樣各類油溫沒起來,會對車輛造成一定損害。

不過說了那麼多,千言萬語合萬一句,就是要多看看車主手冊。看完這期節目,大家記得要等着看下一期,什麼是燒機油!本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

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

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

分類
發燒車訊

澳洲發現無齒恐龍 與迅猛龍是近親

摘錄自2020年5月19日自由時報報導

澳洲發現新種無齒恐龍,跟暴龍、迅猛龍是近親!澳洲古生物學家日前發布了新品種恐龍,此種新恐龍雖然與暴龍和迅猛龍同屬於一個亞目,但卻有著長脖子、沒有牙齒、飲食習性也不相同等特點。學界尚未給予正式名稱,研究人員暫時將其稱作「伊拉夫羅龍(Era the Elaphrosaur)」。

根據《BBC》報導,伊拉夫羅龍在維多利亞州奧特韋角出土,作為新品種的獸腳亞目,這種新恐龍具有許多古代捕食者的熟悉體型,牠採用雙足站立,加上兩隻些微笨拙退化的前爪(小胳膊),甚至可能披覆著一層羽毛,但從鼻子到尾巴的身長只有約2公尺左右。

伊拉夫羅龍與其他獸腳亞目的最大區別是,其脖子較大多數獸腳類要長得多,且似乎只有小時候才有牙齒。隨著年齡的增長,伊拉夫羅龍的牙齒會逐漸退化,並留下角質的喙。該物種可能小時候吃肉、長大後又轉向吃素。不過研究人員暫時還無法確定,因為目前暫缺頭骨部分的化石(只有頸骨部分)。

生活環境
國際新聞
澳洲
恐龍

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

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

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

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

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

分類
發燒車訊

皮實耐用家轎王終於迎來新款 7.99萬起!

新車的動力系統為1。4L 90馬力+5擋手動、1。5L 110馬力+5擋手動/6擋自動、1。4T 131馬力+7擋雙離合。老款的1。6L發動機將被新款的1。5L所取代。1。5L發動機最大馬力和1。6L相同,只是最大扭矩由160牛·米變為150牛·米,在保持動力基本不變的前提下可以降低油耗,這也是消費者喜歡的。

捷達自從2013年上市以來,一直沒有經過什麼大的改變。這一點讓消費者不太滿意。不過這一情況如今得到了改善,目前新捷達已經上市,外觀改變較大,配置提升,1.5L發動機取代1.6L發動機。下面就一起看看改款后的捷達到底性價比如何。

一汽-大眾新捷達在12月7日晚上上市,新車售價為7.99-13.49萬,共推出9個車型供消費者選擇。全新捷達上市,老款捷達處於停產在售狀態,同時1.6L車型被1.5L車型取代。

既然是新款車型,最起碼外觀還是要做出一點改變的吧!要不然就太對不起觀眾了。捷達也不例外,雖然還是遵從着大眾的家族式設計,但是不管是外觀還是內飾,還是有所改變的。

首先是前臉的變化,這也是最明的變化。新車的前進氣格柵採用了網格狀造型,同時增加了鍍鉻元素,前大燈造型和保險杠也有較大的改變,整體來看新車看起來比老款要年輕時尚許多。

前臉看完了來看側面,側面的變化非常小,新車的車窗下方會有鍍鉻裝飾條,只是高配車型的輪轂造型有一些變化。

尾部造型變化也比較大,新款車型的尾部看起來“機靈”了許多,造型也向大哥寶來靠攏。

新款車型的顏色增加了胡桃棕、鈦光灰兩種顏色。同時還有深黑、甜蜜金、反射銀、糖果白、板岩灰五種顏色。新車一共有七種車身配色供消費者選擇。

同時新車的車尺寸為4501*1704*1469mm,軸距為2604mm,老款車型的車身尺寸4487*1706*1470mm,軸距為2603mm。尺寸略有增大。

內飾有所升級,整體質感比老款車型有所提升。整車的配置也有不小的升級,全系車型除了時尚版,都標配了車身穩定系統,要知道,這可是一款捷達,大眾能給它裝上ESp算是有點意外了,老款車型只有頂配才有。同時新車也會有座椅加熱、胎壓監測、真皮坐椅等,有了這些配置,捷達終於有點上檔次了。

新車的動力系統為1.4L 90馬力+5擋手動、1.5L 110馬力+5擋手動/6擋自動、1.4T 131馬力+7擋雙離合。老款的1.6L發動機將被新款的1.5L所取代。1.5L發動機最大馬力和1.6L相同,只是最大扭矩由160牛·米變為150牛·米,在保持動力基本不變的前提下可以降低油耗,這也是消費者喜歡的。

競爭對手:

可以看出新捷達的配置有所提升,外觀也變得更加年輕了,競爭力也會隨之增強。但是捷達所處的這個級別競爭真的是特別激烈。他面臨的競爭對手也不是吃素的,不少車型性價要比捷達高。

吉利汽車-帝豪

帝豪的指導價為6.98-24.98萬,作為最暢銷的自主品牌緊湊型轎車,同價位的帝豪要比捷達的配置高了幾個檔次。不管是做工還是用料都不輸捷達,差的只是“車標”吧!

奇瑞汽車-艾瑞澤5

艾5的指導價為5.89-9.79萬,作為自主品牌性價比最高的車型之一,艾瑞澤5的實力一點也不差,同等配置,選擇艾5,省下的幾萬塊錢可以買多少升汽油了,可以多跑多少公里了…

上汽通用雪佛蘭-科沃茲

科沃茲的指導價為7.99-10.99萬,就算你避過了自主品牌的鋒芒,同級別合資車這一關你也不過好過吧!科沃茲的配置比你好,車身也比你大,外觀也很時尚。

除了這些還有鋒范、標緻301、桑塔納等,所以捷達如果還想維持較高的銷量,就必須和老款一樣,大力降價促銷,畢竟薄利多銷也是一個很好的選擇么!本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

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

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

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

分類
發燒車訊

Java編程技術之淺析JVM內存

JVM

JVM->Java Virtual Machine:Java虛擬機,是一種用於計算設備的規範,它是一個虛構出來的計算機,是通過在實際的計算機上仿真模擬各種計算機功能來實現的。

基本認知:

  • 1.JVM是用於運行Java代碼的假象計算機,主要有一套字節碼指令集,一組寄存器,一個棧,一個垃圾回收,堆 和 一個存儲方法域。
  • 2.JVM運行在操作系統之上,與硬件沒有直接的交互。

Java程序執行過程:

  • 1.編譯->源文件由編譯器編譯成字節碼[ByteCode]

Java 源文件—->編譯器—->字節碼文件

  • 2.運行->字節碼由java虛擬機解釋運行

字節碼文件—->JVM—->機器碼

Java類的加載步驟:

  • 1.加載->主要是完成3個階段的提交:

通過類的全限定名來獲取定義類的二進制字節流
將字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構
在內存中生成一個代表這個類的java.lang.Class對象,作為方法區這個類的各種數據的訪問入口。

  • 2.驗證->四個階段的檢驗動作:

文件格式驗證
元數據驗證
字節碼驗證
符號引用驗證

  • 3.準備->為類變量(static)分配內存並設置類變量的初始值。
  • 4.解析->將常量池內的符號引用轉為直接的引用
  • 5.初始化->按照static塊和static變量在文件中的出現順序,合併到 ()方法中。實例變量由 ()函數賦值。

JVM線程實體:

JVM線程->程序執行過程中的一個線程實體,JVM 允許一個應用併發執行多個線程。

從此,我們應該意識到,在Java中,當提到線程就應該是指JVM線程和Java線程。其中JVM線程指的是Hotspot JVM 後台運行的系統線程,而且Hotspot JVM 中的 Java 線程與原生操作系統線程有直接的映射關係。

️[注意事項]:
[1].當線程本地存儲、緩衝區分配、同步對象、棧、程序計數器等準備好以後,就會創建一個操作系統原生線程。
[2].Java 線程結束,原生線程隨之被回收。操作系統負責調度所有線程,並把它們分配到任何可用的 CPU 上。
[3].當原生線程初始化完畢,就會調用 Java 線程的 run() 方法。當線程結束時,會釋放原生線程和 Java 線程的所有資源

特別需要知道的是,Hotspot JVM 後台運行的系統線程主要是:

  • 虛擬機線程->VM thread:等待 JVM 到達安全點操作出現。這些操作必須要在獨立的線程里執行,因為當堆修改無法進行時,線程都需要 JVM 位於安全點。這些操作的類型有:stop-theworld垃圾回收、線程棧 dump、線程暫停、線程偏向鎖(biased locking)解除。
  • 周期性任務線程->負責定時器事件(也就是中斷),用來調度周期性操作的執行
  • GC線程->支持 JVM 中不同的垃圾回收活動
  • 編譯器線程->在運行時將字節碼動態編譯成本地平台相關的機器碼
  • 信號分發線程->程接收發送到 JVM 的信號並調用適當的 JVM 方法處理

JVM內存

版權聲明:本文為博主原創文章,遵循相關版權協議,如若轉載或者分享請附上原文出處鏈接和鏈接來源。

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

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

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

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

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

分類
發燒車訊

面試官:換人!他連 TCP 這幾個參數都不懂

每日一句英語學習,每天進步一點點:

前言

TCP 性能的提升不僅考察 TCP 的理論知識,還考察了對於操作系統提供的內核參數的理解與應用。

TCP 協議是由操作系統實現,所以操作系統提供了不少調節 TCP 的參數。

Linux TCP 參數

如何正確有效的使用這些參數,來提高 TCP 性能是一個不那麼簡單事情。我們需要針對 TCP 每個階段的問題來對症下藥,而不是病急亂投醫。

接下來,將以三個角度來闡述提升 TCP 的策略,分別是:

  • TCP 三次握手的性能提升;
  • TCP 四次揮手的性能提升;
  • TCP 數據傳輸的性能提升;

本節提綱

正文

01 TCP 三次握手的性能提升

TCP 是面向連接的、可靠的、雙向傳輸的傳輸層通信協議,所以在傳輸數據之前需要經過三次握手才能建立連接。

三次握手與數據傳輸

那麼,三次握手的過程在一個 HTTP 請求的平均時間佔比 10% 以上,在網絡狀態不佳、高併發或者遭遇 SYN 攻擊等場景中,如果不能有效正確的調節三次握手中的參數,就會對性能產生很多的影響。

如何正確有效的使用這些參數,來提高 TCP 三次握手的性能,這就需要理解「三次握手的狀態變遷」,這樣當出現問題時,先用 netstat 命令查看是哪個握手階段出現了問題,再來對症下藥,而不是病急亂投醫。

TCP 三次握手的狀態變遷

客戶端和服務端都可以針對三次握手優化性能。主動發起連接的客戶端優化相對簡單些,而服務端需要監聽端口,屬於被動連接方,其間保持許多的中間狀態,優化方法相對複雜一些。

所以,客戶端(主動發起連接方)和服務端(被動連接方)優化的方式是不同的,接下來分別針對客戶端和服務端優化。

客戶端優化

三次握手建立連接的首要目的是「同步序列號」。

只有同步了序列號才有可靠傳輸,TCP 許多特性都依賴於序列號實現,比如流量控制、丟包重傳等,這也是三次握手中的報文稱為 SYN 的原因,SYN 的全稱就叫 Synchronize Sequence Numbers(同步序列號)。

TCP 頭部

SYN_SENT 狀態的優化

客戶端作為主動發起連接方,首先它將發送 SYN 包,於是客戶端的連接就會處於 SYN_SENT 狀態。

客戶端在等待服務端回復的 ACK 報文,正常情況下,服務器會在幾毫秒內返回 SYN+ACK ,但如果客戶端長時間沒有收到 SYN+ACK 報文,則會重發 SYN 包,重發的次數由 tcp_syn_retries 參數控制,默認是 5 次:

通常,第一次超時重傳是在 1 秒后,第二次超時重傳是在 2 秒,第三次超時重傳是在 4 秒后,第四次超時重傳是在 8 秒后,第五次是在超時重傳 16 秒后。沒錯,每次超時的時間是上一次的 2 倍

當第五次超時重傳后,會繼續等待 32 秒,如果仍然服務端沒有回應 ACK,客戶端就會終止三次握手。

所以,總耗時是 1+2+4+8+16+32=63 秒,大約 1 分鐘左右。

SYN 超時重傳

你可以根據網絡的穩定性和目標服務器的繁忙程度修改 SYN 的重傳次數,調整客戶端的三次握手時間上限。比如內網中通訊時,就可以適當調低重試次數,儘快把錯誤暴露給應用程序。

服務端優化

當服務端收到 SYN 包后,服務端會立馬回復 SYN+ACK 包,表明確認收到了客戶端的序列號,同時也把自己的序列號發給對方。

此時,服務端出現了新連接,狀態是 SYN_RCV。在這個狀態下,Linux 內核就會建立一個「半連接隊列」來維護「未完成」的握手信息,當半連接隊列溢出后,服務端就無法再建立新的連接。

半連接隊列與全連接隊列

SYN 攻擊,攻擊的是就是這個半連接隊列。

如何查看由於 SYN 半連接隊列已滿,而被丟棄連接的情況?

我們可以通過該 netstat -s 命令給出的統計結果中, 可以得到由於半連接隊列已滿,引發的失敗次數:

上面輸出的數值是累計值,表示共有多少個 TCP 連接因為半連接隊列溢出而被丟棄。隔幾秒執行幾次,如果有上升的趨勢,說明當前存在半連接隊列溢出的現象

如何調整 SYN 半連接隊列大小?

要想增大半連接隊列,不能只單純增大 tcp_max_syn_backlog 的值,還需一同增大 somaxconn 和 backlog,也就是增大 accept 隊列。否則,只單純增大 tcp_max_syn_backlog 是無效的。

增大 tcp_max_syn_backlog 和 somaxconn 的方法是修改 Linux 內核參數:

增大 backlog 的方式,每個 Web 服務都不同,比如 Nginx 增大 backlog 的方法如下:

最後,改變了如上這些參數后,要重啟 Nginx 服務,因為 SYN 半連接隊列和 accept 隊列都是在 listen() 初始化的。

如果 SYN 半連接隊列已滿,只能丟棄連接嗎?

並不是這樣,開啟 syncookies 功能就可以在不使用 SYN 半連接隊列的情況下成功建立連接

syncookies 的工作原理:服務器根據當前狀態計算出一個值,放在己方發出的 SYN+ACK 報文中發出,當客戶端返回 ACK 報文時,取出該值驗證,如果合法,就認為連接建立成功,如下圖所示。

開啟 syncookies 功能

syncookies 參數主要有以下三個值:

  • 0 值,表示關閉該功能;
  • 1 值,表示僅當 SYN 半連接隊列放不下時,再啟用它;
  • 2 值,表示無條件開啟功能;

那麼在應對 SYN 攻擊時,只需要設置為 1 即可:

SYN_RCV 狀態的優化

當客戶端接收到服務器發來的 SYN+ACK 報文後,就會回復 ACK 給服務器,同時客戶端連接狀態從 SYN_SENT 轉換為 ESTABLISHED,表示連接建立成功。

服務器端連接成功建立的時間還要再往後,等到服務端收到客戶端的 ACK 后,服務端的連接狀態才變為 ESTABLISHED。

如果服務器沒有收到 ACK,就會重發 SYN+ACK 報文,同時一直處於 SYN_RCV 狀態。

當網絡繁忙、不穩定時,報文丟失就會變嚴重,此時應該調大重發次數。反之則可以調小重發次數。修改重發次數的方法是,調整 tcp_synack_retries 參數

tcp_synack_retries 的默認重試次數是 5 次,與客戶端重傳 SYN 類似,它的重傳會經歷 1、2、4、8、16 秒,最後一次重傳後會繼續等待 32 秒,如果服務端仍然沒有收到 ACK,才會關閉連接,故共需要等待 63 秒。

服務器收到 ACK 后連接建立成功,此時,內核會把連接從半連接隊列移除,然後創建新的完全的連接,並將其添加到 accept 隊列,等待進程調用 accept 函數時把連接取出來。

如果進程不能及時地調用 accept 函數,就會造成 accept 隊列(也稱全連接隊列)溢出,最終導致建立好的 TCP 連接被丟棄。

accept 隊列溢出

accept 隊列已滿,只能丟棄連接嗎?

丟棄連接只是 Linux 的默認行為,我們還可以選擇向客戶端發送 RST 複位報文,告訴客戶端連接已經建立失敗。打開這一功能需要將 tcp_abort_on_overflow 參數設置為 1。

tcp_abort_on_overflow 共有兩個值分別是 0 和 1,其分別表示:

  • 0 :如果 accept 隊列滿了,那麼 server 扔掉 client 發過來的 ack ;
  • 1 :如果 accept 隊列滿了,server 發送一個 RST 包給 client,表示廢掉這個握手過程和這個連接;

如果要想知道客戶端連接不上服務端,是不是服務端 TCP 全連接隊列滿的原因,那麼可以把 tcp_abort_on_overflow 設置為 1,這時如果在客戶端異常中可以看到很多 connection reset by peer 的錯誤,那麼就可以證明是由於服務端 TCP 全連接隊列溢出的問題。

通常情況下,應當把 tcp_abort_on_overflow 設置為 0,因為這樣更有利於應對突發流量。

舉個例子,當 accept 隊列滿導致服務器丟掉了 ACK,與此同時,客戶端的連接狀態卻是 ESTABLISHED,客戶端進程就在建立好的連接上發送請求。只要服務器沒有為請求回復 ACK,客戶端的請求就會被多次「重發」。如果服務器上的進程只是短暫的繁忙造成 accept 隊列滿,那麼當 accept 隊列有空位時,再次接收到的請求報文由於含有 ACK,仍然會觸發服務器端成功建立連接。

tcp_abort_on_overflow 為 0 可以應對突發流量

所以,tcp_abort_on_overflow 設為 0 可以提高連接建立的成功率,只有你非常肯定 TCP 全連接隊列會長期溢出時,才能設置為 1 以儘快通知客戶端。

如何調整 accept 隊列的長度呢?

accept 隊列的長度取決於 somaxconn 和 backlog 之間的最小值,也就是 min(somaxconn, backlog),其中:

  • somaxconn 是 Linux 內核的參數,默認值是 128,可以通過 net.core.somaxconn 來設置其值;
  • backlog 是 listen(int sockfd, int backlog) 函數中的 backlog 大小;

Tomcat、Nginx、Apache 常見的 Web 服務的 backlog 默認值都是 511。

如何查看服務端進程 accept 隊列的長度?

可以通過 ss -ltn 命令查看:

  • Recv-Q:當前 accept 隊列的大小,也就是當前已完成三次握手並等待服務端 accept() 的 TCP 連接;
  • Send-Q:accept 隊列最大長度,上面的輸出結果說明監聽 8088 端口的 TCP 服務,accept 隊列的最大長度為 128;

如何查看由於 accept 連接隊列已滿,而被丟棄的連接?

當超過了 accept 連接隊列,服務端則會丟掉後續進來的 TCP 連接,丟掉的 TCP 連接的個數會被統計起來,我們可以使用 netstat -s 命令來查看:

上面看到的 41150 times ,表示 accept 隊列溢出的次數,注意這個是累計值。可以隔幾秒鐘執行下,如果這個数字一直在增加的話,說明 accept 連接隊列偶爾滿了。

如果持續不斷地有連接因為 accept 隊列溢出被丟棄,就應該調大 backlog 以及 somaxconn 參數。

如何繞過三次握手?

以上我們只是在對三次握手的過程進行優化,接下來我們看看如何繞過三次握手發送數據。

三次握手建立連接造成的後果就是,HTTP 請求必須在一個 RTT(從客戶端到服務器一個往返的時間)后才能發送。

常規 HTTP 請求

在 Linux 3.7 內核版本之後,提供了 TCP Fast Open 功能,這個功能可以減少 TCP 連接建立的時延。

接下來說說,TCP Fast Open 功能的工作方式。

開啟 TCP Fast Open 功能

在客戶端首次建立連接時的過程:

  1. 客戶端發送 SYN 報文,該報文包含 Fast Open 選項,且該選項的 Cookie 為空,這表明客戶端請求 Fast Open Cookie;
  2. 支持 TCP Fast Open 的服務器生成 Cookie,並將其置於 SYN-ACK 數據包中的 Fast Open 選項以發回客戶端;
  3. 客戶端收到 SYN-ACK 后,本地緩存 Fast Open 選項中的 Cookie。

所以,第一次發起 HTTP GET 請求的時候,還是需要正常的三次握手流程。

之後,如果客戶端再次向服務器建立連接時的過程:

  1. 客戶端發送 SYN 報文,該報文包含「數據」(對於非 TFO 的普通 TCP 握手過程,SYN 報文中不包含「數據」)以及此前記錄的 Cookie;
  2. 支持 TCP Fast Open 的服務器會對收到 Cookie 進行校驗:如果 Cookie 有效,服務器將在 SYN-ACK 報文中對 SYN 和「數據」進行確認,服務器隨後將「數據」遞送至相應的應用程序;如果 Cookie 無效,服務器將丟棄 SYN 報文中包含的「數據」,且其隨後發出的 SYN-ACK 報文將只確認 SYN 的對應序列號;
  3. 如果服務器接受了 SYN 報文中的「數據」,服務器可在握手完成之前發送「數據」,這就減少了握手帶來的 1 個 RTT 的時間消耗
  4. 客戶端將發送 ACK 確認服務器發回的 SYN 以及「數據」,但如果客戶端在初始的 SYN 報文中發送的「數據」沒有被確認,則客戶端將重新發送「數據」;
  5. 此後的 TCP 連接的數據傳輸過程和非 TFO 的正常情況一致。

所以,之後發起 HTTP GET 請求的時候,可以繞過三次握手,這就減少了握手帶來的 1 個 RTT 的時間消耗。

開啟了 TFO 功能,cookie 的值是存放到 TCP option 字段里的:

TCP option 字段 – TFO

注:客戶端在請求並存儲了 Fast Open Cookie 之後,可以不斷重複 TCP Fast Open 直至服務器認為 Cookie 無效(通常為過期)。

Linux 下怎麼打開 TCP Fast Open 功能呢?

在 Linux 系統中,可以通過設置 tcp_fastopn 內核參數,來打開 Fast Open 功能

tcp_fastopn 各個值的意義:

  • 0 關閉
  • 1 作為客戶端使用 Fast Open 功能
  • 2 作為服務端使用 Fast Open 功能
  • 3 無論作為客戶端還是服務器,都可以使用 Fast Open 功能

TCP Fast Open 功能需要客戶端和服務端同時支持,才有效果。

小結

本小結主要介紹了關於優化 TCP 三次握手的幾個 TCP 參數。

三次握手優化策略

客戶端的優化

當客戶端發起 SYN 包時,可以通過 tcp_syn_retries 控制其重傳的次數。

服務端的優化

當服務端 SYN 半連接隊列溢出后,會導致後續連接被丟棄,可以通過 netstat -s 觀察半連接隊列溢出的情況,如果 SYN 半連接隊列溢出情況比較嚴重,可以通過 tcp_max_syn_backlog、somaxconn、backlog 參數來調整 SYN 半連接隊列的大小。

服務端回復 SYN+ACK 的重傳次數由 tcp_synack_retries 參數控制。如果遭受 SYN 攻擊,應把 tcp_syncookies 參數設置為 1,表示僅在 SYN 隊列滿后開啟 syncookie 功能,可以保證正常的連接成功建立。

服務端收到客戶端返回的 ACK,會把連接移入 accpet 隊列,等待進行調用 accpet() 函數取出連接。

可以通過 ss -lnt 查看服務端進程的 accept 隊列長度,如果 accept 隊列溢出,系統默認丟棄 ACK,如果可以把 tcp_abort_on_overflow 設置為 1 ,表示用 RST 通知客戶端連接建立失敗。

如果 accpet 隊列溢出嚴重,可以通過 listen 函數的 backlog 參數和 somaxconn 系統參數提高隊列大小,accept 隊列長度取決於 min(backlog, somaxconn)。

繞過三次握手

TCP Fast Open 功能可以繞過三次握手,使得 HTTP 請求減少了 1 個 RTT 的時間,Linux 下可以通過 tcp_fastopen 開啟該功能,同時必須保證服務端和客戶端同時支持。

02 TCP 四次揮手的性能提升

接下來,我們一起看看針對 TCP 四次揮手關不連接時,如何優化性能。

在開始之前,我們得先了解四次揮手狀態變遷的過程。

客戶端和服務端雙方都可以主動斷開連接,通常先關閉連接的一方稱為主動方,后關閉連接的一方稱為被動方。

客戶端主動關閉

可以看到,四次揮手過程只涉及了兩種報文,分別是 FIN 和 ACK

  • FIN 就是結束連接的意思,誰發出 FIN 報文,就表示它將不會再發送任何數據,關閉這一方向上的傳輸通道;
  • ACK 就是確認的意思,用來通知對方:你方的發送通道已經關閉;

四次揮手的過程:

  • 當主動方關閉連接時,會發送 FIN 報文,此時發送方的 TCP 連接將從 ESTABLISHED 變成 FIN_WAIT1。
  • 當被動方收到 FIN 報文後,內核會自動回復 ACK 報文,連接狀態將從 ESTABLISHED 變成 CLOSE_WAIT,表示被動方在等待進程調用 close 函數關閉連接。
  • 當主動方收到這個 ACK 后,連接狀態由 FIN_WAIT1 變為 FIN_WAIT2,也就是表示主動方的發送通道就關閉了
  • 當被動方進入 CLOSE_WAIT 時,被動方還會繼續處理數據,等到進程的 read 函數返回 0 后,應用程序就會調用 close 函數,進而觸發內核發送 FIN 報文,此時被動方的連接狀態變為 LAST_ACK。
  • 當主動方收到這個 FIN 報文後,內核會回復 ACK 報文給被動方,同時主動方的連接狀態由 FIN_WAIT2 變為 TIME_WAIT,在 Linux 系統下大約等待 1 分鐘后,TIME_WAIT 狀態的連接才會徹底關閉
  • 當被動方收到最後的 ACK 報文後,被動方的連接就會關閉

你可以看到,每個方向都需要一個 FIN 和一個 ACK,因此通常被稱為四次揮手

這裏一點需要注意是:主動關閉連接的,才有 TIME_WAIT 狀態。

主動關閉方和被動關閉方優化的思路也不同,接下來分別說說如何優化他們。

主動方的優化

關閉的連接的方式通常有兩種,分別是 RST 報文關閉和 FIN 報文關閉。

如果進程異常退出了,內核就會發送 RST 報文來關閉,它可以不走四次揮手流程,是一個暴力關閉連接的方式。

安全關閉連接的方式必須通過四次揮手,它由進程調用 closeshutdown 函數發起 FIN 報文(shutdown 參數須傳入 SHUT_WR 或者 SHUT_RDWR 才會發送 FIN)。

調用 close 函數 和 shutdown 函數有什麼區別?

調用了 close 函數意味着完全斷開連接,完全斷開不僅指無法傳輸數據,而且也不能發送數據。 此時,調用了 close 函數的一方的連接叫做「孤兒連接」,如果你用 netstat -p 命令,會發現連接對應的進程名為空。

使用 close 函數關閉連接是不優雅的。於是,就出現了一種優雅關閉連接的 shutdown 函數,它可以控制只關閉一個方向的連接

第二個參數決定斷開連接的方式,主要有以下三種方式:

  • SHUT_RD(0):關閉連接的「讀」這個方向,如果接收緩衝區有已接收的數據,則將會被丟棄,並且後續再收到新的數據,會對數據進行 ACK,然後悄悄地丟棄。也就是說,對端還是會接收到 ACK,在這種情況下根本不知道數據已經被丟棄了。
  • SHUT_WR(1):關閉連接的「寫」這個方向,這就是常被稱為「半關閉」的連接。如果發送緩衝區還有未發送的數據,將被立即發送出去,併發送一個 FIN 報文給對端。
  • SHUT_RDWR(2):相當於 SHUT_RD 和 SHUT_WR 操作各一次,關閉套接字的讀和寫兩個方向

close 和 shutdown 函數都可以關閉連接,但這兩種方式關閉的連接,不只功能上有差異,控制它們的 Linux 參數也不相同。

FIN_WAIT1 狀態的優化

主動方發送 FIN 報文後,連接就處於 FIN_WAIT1 狀態,正常情況下,如果能及時收到被動方的 ACK,則會很快變為 FIN_WAIT2 狀態。

但是當遲遲收不到對方返回的 ACK 時,連接就會一直處於 FIN_WAIT1 狀態。此時,內核會定時重發 FIN 報文,其中重發次數由 tcp_orphan_retries 參數控制(注意,orphan 雖然是孤兒的意思,該參數卻不只對孤兒連接有效,事實上,它對所有 FIN_WAIT1 狀態下的連接都有效),默認值是 0。

你可能會好奇,這 0 表示幾次?實際上當為 0 時,特指 8 次,從下面的內核源碼可知:

如果 FIN_WAIT1 狀態連接很多,我們就需要考慮降低 tcp_orphan_retries 的值,當重傳次數超過 tcp_orphan_retries 時,連接就會直接關閉掉。

對於普遍正常情況時,調低 tcp_orphan_retries 就已經可以了。如果遇到惡意攻擊,FIN 報文根本無法發送出去,這由 TCP 兩個特性導致的:

  • 首先,TCP 必須保證報文是有序發送的,FIN 報文也不例外,當發送緩衝區還有數據沒有發送時,FIN 報文也不能提前發送。
  • 其次,TCP 有流量控制功能,當接收方接收窗口為 0 時,發送方就不能再發送數據。所以,當攻擊者下載大文件時,就可以通過接收窗口設為 0 ,這就會使得 FIN 報文都無法發送出去,那麼連接會一直處於 FIN_WAIT1 狀態。

解決這種問題的方法,是調整 tcp_max_orphans 參數,它定義了「孤兒連接」的最大數量

當進程調用了 close 函數關閉連接,此時連接就會是「孤兒連接」,因為它無法在發送和接收數據。Linux 系統為了防止孤兒連接過多,導致系統資源長時間被佔用,就提供了 tcp_max_orphans 參數。如果孤兒連接數量大於它,新增的孤兒連接將不再走四次揮手,而是直接發送 RST 複位報文強制關閉。

FIN_WAIT2 狀態的優化

當主動方收到 ACK 報文後,會處於 FIN_WAIT2 狀態,就表示主動方的發送通道已經關閉,接下來將等待對方發送 FIN 報文,關閉對方的發送通道。

這時,如果連接是用 shutdown 函數關閉的,連接可以一直處於 FIN_WAIT2 狀態,因為它可能還可以發送或接收數據。但對於 close 函數關閉的孤兒連接,由於無法在發送和接收數據,所以這個狀態不可以持續太久,而 tcp_fin_timeout 控制了這個狀態下連接的持續時長,默認值是 60 秒:

它意味着對於孤兒連接(調用 close 關閉的連接),如果在 60 秒后還沒有收到 FIN 報文,連接就會直接關閉。

這個 60 秒不是隨便決定的,它與 TIME_WAIT 狀態持續的時間是相同的,後面我們在來說說為什麼是 60 秒。

TIME_WAIT 狀態的優化

TIME_WAIT 是主動方四次揮手的最後一個狀態,也是最常遇見的狀態。

當收到被動方發來的 FIN 報文後,主動方會立刻回復 ACK,表示確認對方的發送通道已經關閉,接着就處於 TIME_WAIT 狀態。在 Linux 系統,TIME_WAIT 狀態會持續 60 秒后才會進入關閉狀態。

TIME_WAIT 狀態的連接,在主動方看來確實快已經關閉了。然後,被動方沒有收到 ACK 報文前,還是處於 LAST_ACK 狀態。如果這個 ACK 報文沒有到達被動方,被動方就會重發 FIN 報文。重發次數仍然由前面介紹過的 tcp_orphan_retries 參數控制。

TIME-WAIT 的狀態尤其重要,主要是兩個原因:

  • 防止具有相同「四元組」的「舊」數據包被收到;
  • 保證「被動關閉連接」的一方能被正確的關閉,即保證最後的 ACK 能讓被動關閉方接收,從而幫助其正常關閉;

原因一:防止舊連接的數據包

TIME-WAIT 的一個作用是防止收到歷史數據,從而導致數據錯亂的問題。

假設 TIME-WAIT 沒有等待時間或時間過短,被延遲的數據包抵達後會發生什麼呢?

接收到歷史數據的異常

  • 如上圖黃色框框服務端在關閉連接之前發送的 SEQ = 301 報文,被網絡延遲了。
  • 這時有相同端口的 TCP 連接被複用后,被延遲的 SEQ = 301 抵達了客戶端,那麼客戶端是有可能正常接收這個過期的報文,這就會產生數據錯亂等嚴重的問題。

所以,TCP 就設計出了這麼一個機制,經過 2MSL 這個時間,足以讓兩個方向上的數據包都被丟棄,使得原來連接的數據包在網絡中都自然消失,再出現的數據包一定都是新建立連接所產生的。

原因二:保證連接正確關閉

TIME-WAIT 的另外一個作用是等待足夠的時間以確保最後的 ACK 能讓被動關閉方接收,從而幫助其正常關閉。

假設 TIME-WAIT 沒有等待時間或時間過短,斷開連接會造成什麼問題呢?

沒有確保正常斷開的異常

  • 如上圖紅色框框客戶端四次揮手的最後一個 ACK 報文如果在網絡中被丟失了,此時如果客戶端 TIME-WAIT 過短或沒有,則就直接進入了 CLOSE 狀態了,那麼服務端則會一直處在 LASE-ACK 狀態。
  • 當客戶端發起建立連接的 SYN 請求報文後,服務端會發送 RST 報文給客戶端,連接建立的過程就會被終止。

我們再回過頭來看看,為什麼 TIME_WAIT 狀態要保持 60 秒呢?這與孤兒連接 FIN_WAIT2 狀態默認保留 60 秒的原理是一樣的,因為這兩個狀態都需要保持 2MSL 時長。MSL 全稱是 Maximum Segment Lifetime,它定義了一個報文在網絡中的最長生存時間(報文每經過一次路由器的轉發,IP 頭部的 TTL 字段就會減 1,減到 0 時報文就被丟棄,這就限制了報文的最長存活時間)。

為什麼是 2 MSL 的時長呢?這其實是相當於至少允許報文丟失一次。比如,若 ACK 在一個 MSL 內丟失,這樣被動方重發的 FIN 會在第 2 個 MSL 內到達,TIME_WAIT 狀態的連接可以應對。

為什麼不是 4 或者 8 MSL 的時長呢?你可以想象一個丟包率達到百分之一的糟糕網絡,連續兩次丟包的概率只有萬分之一,這個概率實在是太小了,忽略它比解決它更具性價比。

因此,TIME_WAIT 和 FIN_WAIT2 狀態的最大時長都是 2 MSL,由於在 Linux 系統中,MSL 的值固定為 30 秒,所以它們都是 60 秒。

雖然 TIME_WAIT 狀態有存在的必要,但它畢竟會消耗系統資源。如果發起連接一方的 TIME_WAIT 狀態過多,佔滿了所有端口資源,則會導致無法創建新連接。

  • 客戶端受端口資源限制:如果客戶端 TIME_WAIT 過多,就會導致端口資源被佔用,因為端口就65536個,被佔滿就會導致無法創建新的連接;
  • 服務端受系統資源限制:由於一個 四元組表示TCP連接,理論上服務端可以建立很多連接,服務端確實只監聽一個端口 但是會把連接扔給處理線程,所以理論上監聽的端口可以繼續監聽。但是線程池處理不了那麼多一直不斷的連接了。所以當服務端出現大量 TIME_WAIT 時,系統資源被佔滿時,會導致處理不過來新的連接;

另外,Linux 提供了 tcp_max_tw_buckets 參數,當 TIME_WAIT 的連接數量超過該參數時,新關閉的連接就不再經歷 TIME_WAIT 而直接關閉:

當服務器的併發連接增多時,相應地,同時處於 TIME_WAIT 狀態的連接數量也會變多,此時就應當調大 tcp_max_tw_buckets 參數,減少不同連接間數據錯亂的概率。

tcp_max_tw_buckets 也不是越大越好,畢竟內存和端口都是有限的。

有一種方式可以在建立新連接時,復用處於 TIME_WAIT 狀態的連接,那就是打開 tcp_tw_reuse 參數。但是需要注意,該參數是只用於客戶端(建立連接的發起方),因為是在調用 connect() 時起作用的,而對於服務端(被動連接方)是沒有用的。

tcp_tw_reuse 從協議角度理解是安全可控的,可以復用處於 TIME_WAIT 的端口為新的連接所用。

什麼是協議角度理解的安全可控呢?主要有兩點:

  • 只適用於連接發起方,也就是 C/S 模型中的客戶端;
  • 對應的 TIME_WAIT 狀態的連接創建時間超過 1 秒才可以被複用。

使用這個選項,還有一個前提,需要打開對 TCP 時間戳的支持(對方也要打開 ):

由於引入了時間戳,它能帶來了些好處:

  • 我們在前面提到的 2MSL 問題就不復存在了,因為重複的數據包會因為時間戳過期被自然丟棄;
  • 同時,它還可以防止序列號繞回,也是因為重複的數據包會由於時間戳過期被自然丟棄;

時間戳是在 TCP 的選擇字段里定義的,開啟了時間戳功能,在 TCP 報文傳輸的時候會帶上發送報文的時間戳。

TCP option 字段 – 時間戳

我們來看看開啟了 tcp_tw_reuse 功能,如果四次揮手中的最後一次 ACK 在網絡中丟失了,會發生什麼?

四次揮手中的最後一次 ACK 在網絡中丟失

上圖的流程:

  • 四次揮手中的最後一次 ACK 在網絡中丟失了,服務端一直處於 LAST_ACK 狀態;
  • 客戶端由於開啟了 tcp_tw_reuse 功能,客戶再次發起新連接的時候,會復用超過 1 秒后的 time_wait 狀態的連接。但客戶端新發的 SYN 包會被忽略(由於時間戳),因為服務端比較了客戶端的上一個報文與 SYN 報文的時間戳,過期的報文就會被服務端丟棄
  • 服務端 FIN 報文遲遲沒有收到四次揮手的最後一次 ACK,於是超時重發了 FIN 報文給客戶端;
  • 處於 SYN_SENT 狀態的客戶,由於收到了 FIN 報文,則會回 RST 給服務端,於是服務端就離開了 LAST_ACK 狀態;
  • 最初的客戶端 SYN 報文超時重發了( 1 秒鐘后),此時就與服務端能正確的三次握手了。

所以大家都會說開啟了 tcp_tw_reuse,可以在復用了 time_wait 狀態的 1 秒過後成功建立連接,這 1 秒主要是花費在 SYN 包重傳。

另外,老版本的 Linux 還提供了 tcp_tw_recycle 參數,但是當開啟了它,就有兩個坑:

  • Linux 會加快客戶端和服務端 TIME_WAIT 狀態的時間,也就是它會使得 TIME_WAIT 狀態會小於 60 秒,很容易導致數據錯亂;
  • 另外,Linux 會丟棄所有來自遠端時間戳小於上次記錄的時間戳(由同一個遠端發送的)的任何數據包。就是說要使用該選項,則必須保證數據包的時間戳是單調遞增的。那麼,問題在於,此處的時間戳並不是我們通常意義上面的絕對時間,而是一個相對時間。很多情況下,我們是沒法保證時間戳單調遞增的,比如使用了 NAT、LVS 等情況;

所以,不建議設置為 1 ,在 Linux 4.12 版本后,Linux 內核直接取消了這一參數,建議關閉它:

另外,我們可以在程序中設置 socket 選項,來設置調用 close 關閉連接行為。

如果 l_onoff 為非 0, 且 l_linger 值為 0,那麼調用 close 后,會立該發送一個 RST 標誌給對端,該 TCP 連接將跳過四次揮手,也就跳過了 TIME_WAIT 狀態,直接關閉。

但這為跨越 TIME_WAIT 狀態提供了一個可能,不過是一個非常危險的行為,不值得提倡。

被動方的優化

當被動方收到 FIN 報文時,內核會自動回復 ACK,同時連接處於 CLOSE_WAIT 狀態,顧名思義,它表示等待應用進程調用 close 函數關閉連接。

內核沒有權利替代進程去關閉連接,因為如果主動方是通過 shutdown 關閉連接,那麼它就是想在半關閉連接上接收數據或發送數據。因此,Linux 並沒有限制 CLOSE_WAIT 狀態的持續時間。

當然,大多數應用程序並不使用 shutdown 函數關閉連接。所以,當你用 netstat 命令發現大量 CLOSE_WAIT 狀態。就需要排查你的應用程序,因為可能因為應用程序出現了 Bug,read 函數返回 0 時,沒有調用 close 函數。

處於 CLOSE_WAIT 狀態時,調用了 close 函數,內核就會發出 FIN 報文關閉發送通道,同時連接進入 LAST_ACK 狀態,等待主動方返回 ACK 來確認連接關閉。

如果遲遲收不到這個 ACK,內核就會重發 FIN 報文,重發次數仍然由 tcp_orphan_retries 參數控制,這與主動方重發 FIN 報文的優化策略一致。

還有一點我們需要注意的,如果被動方迅速調用 close 函數,那麼被動方的 ACK 和 FIN 有可能在一個報文中發送,這樣看起來,四次揮手會變成三次揮手,這隻是一種特殊情況,不用在意。

如果連接雙方同時關閉連接,會怎麼樣?

由於 TCP 是雙全工的協議,所以是會出現兩方同時關閉連接的現象,也就是同時發送了 FIN 報文。

此時,上面介紹的優化策略仍然適用。兩方發送 FIN 報文時,都認為自己是主動方,所以都進入了 FIN_WAIT1 狀態,FIN 報文的重發次數仍由 tcp_orphan_retries 參數控制。

同時關閉

接下來,雙方在等待 ACK 報文的過程中,都等來了 FIN 報文。這是一種新情況,所以連接會進入一種叫做 CLOSING 的新狀態,它替代了 FIN_WAIT2 狀態。接着,雙方內核回復 ACK 確認對方發送通道的關閉后,進入 TIME_WAIT 狀態,等待 2MSL 的時間后,連接自動關閉。

小結

針對 TCP 四次揮手的優化,我們需要根據主動方和被動方四次揮手狀態變化來調整系統 TCP 內核參數。

四次揮手的優化策略

主動方的優化

主動發起 FIN 報文斷開連接的一方,如果遲遲沒收到對方的 ACK 回復,則會重傳 FIN 報文,重傳的次數由 tcp_orphan_retries 參數決定。

當主動方收到 ACK 報文後,連接就進入 FIN_WAIT2 狀態,根據關閉的方式不同,優化的方式也不同:

  • 如果這是 close 函數關閉的連接,那麼它就是孤兒連接。如果 tcp_fin_timeout 秒內沒有收到對方的 FIN 報文,連接就直接關閉。同時,為了應對孤兒連接佔用太多的資源,tcp_max_orphans 定義了最大孤兒連接的數量,超過時連接就會直接釋放。
  • 反之是 shutdown 函數關閉的連接,則不受此參數限制;

當主動方接收到 FIN 報文,並返回 ACK 后,主動方的連接進入 TIME_WAIT 狀態。這一狀態會持續 1 分鐘,為了防止 TIME_WAIT 狀態佔用太多的資源,tcp_max_tw_buckets 定義了最大數量,超過時連接也會直接釋放。

當 TIME_WAIT 狀態過多時,還可以通過設置 tcp_tw_reusetcp_timestamps 為 1 ,將 TIME_WAIT 狀態的端口復用於作為客戶端的新連接,注意該參數只適用於客戶端。

被動方的優化

被動關閉的連接方應對非常簡單,它在回復 ACK 后就進入了 CLOSE_WAIT 狀態,等待進程調用 close 函數關閉連接。因此,出現大量 CLOSE_WAIT 狀態的連接時,應當從應用程序中找問題。

當被動方發送 FIN 報文後,連接就進入 LAST_ACK 狀態,在未等到 ACK 時,會在 tcp_orphan_retries 參數的控制下重發 FIN 報文。

03 TCP 傳輸數據的性能提升

在前面介紹的是三次握手和四次揮手的優化策略,接下來主要介紹的是 TCP 傳輸數據時的優化策略。

TCP 連接是由內核維護的,內核會為每個連接建立內存緩衝區:

  • 如果連接的內存配置過小,就無法充分使用網絡帶寬,TCP 傳輸效率就會降低;
  • 如果連接的內存配置過大,很容易把服務器資源耗盡,這樣就會導致新連接無法建立;

因此,我們必須理解 Linux 下 TCP 內存的用途,才能正確地配置內存大小。

滑動窗口是如何影響傳輸速度的?

TCP 會保證每一個報文都能夠抵達對方,它的機制是這樣:報文發出去后,必須接收到對方返回的確認報文 ACK,如果遲遲未收到,就會超時重發該報文,直到收到對方的 ACK 為止。

所以,TCP 報文發出去后,並不會立馬從內存中刪除,因為重傳時還需要用到它。

由於 TCP 是內核維護的,所以報文存放在內核緩衝區。如果連接非常多,我們可以通過 free 命令觀察到 buff/cache 內存是會增大。

如果 TCP 是每發送一個數據,都要進行一次確認應答。當上一個數據包收到了應答了, 再發送下一個。這個模式就有點像我和你面對面聊天,你一句我一句,但這種方式的缺點是效率比較低的。

按數據包進行確認應答

所以,這樣的傳輸方式有一個缺點:數據包的往返時間越長,通信的效率就越低

要解決這一問題不難,并行批量發送報文,再批量確認報文即刻。

并行處理

然而,這引出了另一個問題,發送方可以隨心所欲的發送報文嗎?當然這不現實,我們還得考慮接收方的處理能力。

當接收方硬件不如發送方,或者系統繁忙、資源緊張時,是無法瞬間處理這麼多報文的。於是,這些報文只能被丟掉,使得網絡效率非常低。

為了解決這種現象發生,TCP 提供一種機制可以讓「發送方」根據「接收方」的實際接收能力控制發送的數據量,這就是滑動窗口的由來。

接收方根據它的緩衝區,可以計算出後續能夠接收多少字節的報文,這個数字叫做接收窗口。當內核接收到報文時,必須用緩衝區存放它們,這樣剩餘緩衝區空間變小,接收窗口也就變小了;當進程調用 read 函數后,數據被讀入了用戶空間,內核緩衝區就被清空,這意味着主機可以接收更多的報文,接收窗口就會變大。

因此,接收窗口並不是恆定不變的,接收方會把當前可接收的大小放在 TCP 報文頭部中的窗口字段,這樣就可以起到窗口大小通知的作用。

發送方的窗口等價於接收方的窗口嗎?如果不考慮擁塞控制,發送方的窗口大小「約等於」接收方的窗口大小,因為窗口通知報文在網絡傳輸是存在時延的,所以是約等於的關係。

TCP 頭部

從上圖中可以看到,窗口字段只有 2 個字節,因此它最多能表達 65535 字節大小的窗口,也就是 64KB 大小。

這個窗口大小最大值,在當今高速網絡下,很明顯是不夠用的。所以後續有了擴充窗口的方法:在 TCP 選項(option)字段定義了窗口擴大因子,用於擴大 TCP 通告窗口,其值大小是 2^14,這樣就使 TCP 的窗口大小從 16 位擴大為 30 位(2^16 * 2^ 14 = 2^30),所以此時窗口的最大值可以達到 1GB。

TCP option 選項 – 窗口擴展

Linux 中打開這一功能,需要把 tcp_window_scaling 配置設為 1(默認打開):

要使用窗口擴大選項,通訊雙方必須在各自的 SYN 報文中發送這個選項:

  • 主動建立連接的一方在 SYN 報文中發送這個選項;
  • 而被動建立連接的一方只有在收到帶窗口擴大選項的 SYN 報文之後才能發送這個選項。

這樣看來,只要進程能及時地調用 read 函數讀取數據,並且接收緩衝區配置得足夠大,那麼接收窗口就可以無限地放大,發送方也就無限地提升發送速度。

這是不可能的,因為網絡的傳輸能力是有限的,當發送方依據發送窗口,發送超過網絡處理能力的報文時,路由器會直接丟棄這些報文。因此,緩衝區的內存並不是越大越好。

如果確定最大傳輸速度?

在前面我們知道了 TCP 的傳輸速度,受制於發送窗口與接收窗口,以及網絡設備傳輸能力。其中,窗口大小由內核緩衝區大小決定。如果緩衝區與網絡傳輸能力匹配,那麼緩衝區的利用率就達到了最大化。

問題來了,如何計算網絡的傳輸能力呢?

相信大家都知道網絡是有「帶寬」限制的,帶寬描述的是網絡傳輸能力,它與內核緩衝區的計量單位不同:

  • 帶寬是單位時間內的流量,表達是「速度」,比如常見的帶寬 100 MB/s;
  • 緩衝區單位是字節,當網絡速度乘以時間才能得到字節數;

這裏需要說一個概念,就是帶寬時延積,它決定網絡中飛行報文的大小,它的計算方式:

比如最大帶寬是 100 MB/s,網絡時延(RTT)是 10ms 時,意味着客戶端到服務端的網絡一共可以存放 100MB/s * 0.01s = 1MB 的字節。

這個 1MB 是帶寬和時延的乘積,所以它就叫「帶寬時延積」(縮寫為 BDP,Bandwidth Delay Product)。同時,這 1MB 也表示「飛行中」的 TCP 報文大小,它們就在網絡線路、路由器等網絡設備上。如果飛行報文超過了 1 MB,就會導致網絡過載,容易丟包。

由於發送緩衝區大小決定了發送窗口的上限,而發送窗口又決定了「已發送未確認」的飛行報文的上限。因此,發送緩衝區不能超過「帶寬時延積」。

發送緩衝區與帶寬時延積的關係:

  • 如果發送緩衝區「超過」帶寬時延積,超出的部分就沒辦法有效的網絡傳輸,同時導致網絡過載,容易丟包;
  • 如果發送緩衝區「小於」帶寬時延積,就不能很好的發揮出網絡的傳輸效率。

所以,發送緩衝區的大小最好是往帶寬時延積靠近。

怎樣調整緩衝區大小?

在 Linux 中發送緩衝區和接收緩衝都是可以用參數調節的。設置完后,Linux 會根據你設置的緩衝區進行動態調節

調節發送緩衝區範圍

先來看看發送緩衝區,它的範圍通過 tcp_wmem 參數配置;

上面三個数字單位都是字節,它們分別表示:

  • 第一個數值是動態範圍的最小值,4096 byte = 4K;
  • 第二個數值是初始默認值,87380 byte ≈ 86K;
  • 第三個數值是動態範圍的最大值,4194304 byte = 4096K(4M);

發送緩衝區是自行調節的,當發送方發送的數據被確認后,並且沒有新的數據要發送,就會把發送緩衝區的內存釋放掉。

調節接收緩衝區範圍

而接收緩衝區的調整就比較複雜一些,先來看看設置接收緩衝區範圍的 tcp_rmem 參數:

上面三個数字單位都是字節,它們分別表示:

  • 第一個數值是動態範圍的最小值,表示即使在內存壓力下也可以保證的最小接收緩衝區大小,4096 byte = 4K;
  • 第二個數值是初始默認值,87380 byte ≈ 86K;
  • 第三個數值是動態範圍的最大值,6291456 byte = 6144K(6M);

接收緩衝區可以根據系統空閑內存的大小來調節接收窗口:

  • 如果系統的空閑內存很多,就可以自動把緩衝區增大一些,這樣傳給對方的接收窗口也會變大,因而提升發送方發送的傳輸數據數量;
  • 反正,如果系統的內存很緊張,就會減少緩衝區,這雖然會降低傳輸效率,可以保證更多的併發連接正常工作;

發送緩衝區的調節功能是自動開啟的,而接收緩衝區則需要配置 tcp_moderate_rcvbuf 為 1 來開啟調節功能

調節 TCP 內存範圍

接收緩衝區調節時,怎麼知道當前內存是否緊張或充分呢?這是通過 tcp_mem 配置完成的:

上面三個数字單位不是字節,而是「頁面大小」,1 頁表示 4KB,它們分別表示:

  • 當 TCP 內存小於第 1 個值時,不需要進行自動調節;
  • 在第 1 和第 2 個值之間時,內核開始調節接收緩衝區的大小;
  • 大於第 3 個值時,內核不再為 TCP 分配新內存,此時新連接是無法建立的;

一般情況下這些值是在系統啟動時根據系統內存數量計算得到的。根據當前 tcp_mem 最大內存頁面數是 177120,當內存為 (177120 * 4) / 1024K ≈ 692M 時,系統將無法為新的 TCP 連接分配內存,即 TCP 連接將被拒絕。

根據實際場景調節的策略

在高併發服務器中,為了兼顧網速與大量的併發連接,我們應當保證緩衝區的動態調整的最大值達到帶寬時延積,而最小值保持默認的 4K 不變即可。而對於內存緊張的服務而言,調低默認值是提高併發的有效手段。

同時,如果這是網絡 IO 型服務器,那麼,調大 tcp_mem 的上限可以讓 TCP 連接使用更多的系統內存,這有利於提升併發能力。需要注意的是,tcp_wmem 和 tcp_rmem 的單位是字節,而 tcp_mem 的單位是頁面大小。而且,千萬不要在 socket 上直接設置 SO_SNDBUF 或者 SO_RCVBUF,這樣會關閉緩衝區的動態調整功能。

小結

本節針對 TCP 優化數據傳輸的方式,做了一些介紹。

數據傳輸的優化策略

TCP 可靠性是通過 ACK 確認報文實現的,又依賴滑動窗口提升了發送速度也兼顧了接收方的處理能力。

可是,默認的滑動窗口最大值只有 64 KB,不滿足當今的高速網絡的要求,要想要想提升發送速度必須提升滑動窗口的上限,在 Linux 下是通過設置 tcp_window_scaling 為 1 做到的,此時最大值可高達 1GB。

滑動窗口定義了網絡中飛行報文的最大字節數,當它超過帶寬時延積時,網絡過載,就會發生丟包。而當它小於帶寬時延積時,就無法充分利用網絡帶寬。因此,滑動窗口的設置,必須參考帶寬時延積。

內核緩衝區決定了滑動窗口的上限,緩衝區可分為:發送緩衝區 tcp_wmem 和接收緩衝區 tcp_rmem。

Linux 會對緩衝區動態調節,我們應該把緩衝區的上限設置為帶寬時延積。發送緩衝區的調節功能是自動打開的,而接收緩衝區需要把 tcp_moderate_rcvbuf 設置為 1 來開啟。其中,調節的依據是 TCP 內存範圍 tcp_mem。

但需要注意的是,如果程序中的 socket 設置 SO_SNDBUF 和 SO_RCVBUF,則會關閉緩衝區的動態整功能,所以不建議在程序設置它倆,而是交給內核自動調整比較好。

有效配置這些參數后,既能夠最大程度地保持併發性,也能讓資源充裕時連接傳輸速度達到最大值。

巨人的肩膀

[1] 系統性能調優必知必會.陶輝.極客時間.

[2] 網絡編程實戰專欄.盛延敏.極客時間.

[3] http://www.blogjava.net/yongboy/archive/2013/04/11/397677.html

[4] http://blog.itpub.net/31559359/viewspace-2284113/

[5] https://blog.51cto.com/professor/1909022

[6] https://vincent.bernat.ch/en/blog/2014-tcp-time-wait-state-linux

嘮嗑嘮嗑

跟大家說個沉痛的事情。

我想大部分小夥伴都發現了,最近公眾號改版,訂閱號里的信息流不再是以時間順序了,而是以推薦算法方式显示順序。

這對小林這種「周更」的作者,真的一次重重打擊,非常的不友好。

因為長時間沒發文,公眾號可能會把推薦的權重降低,這就會導致很多讀者,會收不到我的「最新」的推文,如此下去,那小林文章不就無人問津了?(抱頭痛哭 …)

另外,小林更文時間長的原因,不是因為偷懶。

而是為了把知識點「寫的更清楚,畫的更清晰」,所以這必然會花費更多更長的時間。

如果你認可和喜歡小林的文章,不想錯過文章的第一時間推送,可以動動你的小手手,給小林公眾號一個「星標」。

平時沒事,就讓「小林coding」靜靜地躺在你的訂閱號底部,但是你要知道它在這其間並非無所事事,而是在努力地準備着更好的內容,等準備好了,它自然會「蹦出」在你面前。

小林是專為大家圖解的工具人,Goodbye,我們下次見!

讀者問答

讀者問:“小林,請教個問題,somaxconn和backlog是不是都是指的是accept隊列?然後somaxconn是內核參數,backlog是通過系統調用間隔地修改somaxconn,比如Linux中listen()函數?”

兩者取最小值才是 accpet 隊列。

讀者問:“小林,還有個問題要請教下,“如果 accept 隊列滿了,那麼 server 扔掉 client 發過來的 ack”,也就是說該TCP連接還是位於半連接隊列中,沒有丟棄嗎?”

  1. 當 accept 隊列滿了,後續新進來的syn包都會被丟失
  2. 我文章的突發流量例子是,那個連接進來的時候 accept 隊列還沒滿,但是在第三次握手的時候,accept 隊列突然滿了,就會導致 ack 被丟棄,就一直處於半連接隊列。

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

【其他文章推薦】

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

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心

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

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

分類
發燒車訊

20萬大空間/高顏值轎車 媳婦喜歡丈母娘滿意

國內消費者最關心的乘坐空間,2805mm軸距的起亞表現的很好,以175cm的體驗者為例,前後排可活的一拳的頭部空間,後排頭部空間超兩拳,提供了1。6T/2。0L/2。0T三種排量可供選擇,搭配6擋手自一體或者7擋雙離合變速箱,選擇性很強。

國人買東西都有一個共同的特點

就是大大大大大大大大大大大大

手機必須上大屏的

鑽戒要買大的

那車子當然是越大越好

前段時間一條火爆的廣告刷爆了朋友圈

致25歲還一無是處的本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

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

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

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

分類
發燒車訊

LeetCode 75,90%的人想不出最佳解的簡單題

本文始發於個人公眾號:TechFlow,原創不易,求個關注

今天是LeetCode專題的44篇文章,我們一起來看下LeetCode的75題,顏色排序 Sort Colors。

這題的官方難度是Medium,通過率是45%,點贊2955,反對209(國際版數據),從這份數據上我們大概能看得出來,這題的難度不大,並且點贊遠遠高於反對,說明題目的質量很不錯。事實上也的確如此,這題足夠簡單也足夠有趣,值得一做。

題意

給定一個n個元素的數組,數組當中的每一個元素表示一個顏色。一共有紅白藍三種顏色,分別用0,1和2來表示。要求將這些顏色按照大小進行排序,返回排序之後的結果。

要求不能調用排序庫sort來解決問題。

桶排序

看完題目應該感受到了,如果沒有不能使用sort的限制,這題毫無難度。即使加上了限制難度也不大,我們既然不能調用sort,難道還不能自己寫個sort嗎?Python寫個快排也才幾行而已。

自己寫sort當然是可以的,顯然這是下下策。因為元素只有3個值,互相之間的大小關係也就只有那麼幾種,排序完全沒有必要。比較容易想到,我們可以統計一下這三個數值出現的次數,幾個0幾個1幾個2,我們再把這些數拼在一起,還原之前的數據不就可以了嗎?

這樣的確可行,但實際上這也是一種排序方案,叫做基數排序,也稱為桶排序,還有些地方稱為小學生排序(大概是小學生都能懂的意思吧)。基數排序的思想非常簡單,我們創建一個數組,用它的每一位來表示某個元素是否在原數組當中出現過。出現過則+1,沒出現過則一直是0。我們標記完原數組之後,再遍歷一遍標記的數組,由於下標天然有序,所以我們就可以得到排序之後的結果了。

如果你還有些迷糊也沒有關係,我們把代碼寫出來就明白了,由於這題讓我們提供一個inplace的方法,所以我們在最後的時候需要對nums當中的元素重新賦值。

class Solution:
    def sortColors(self, nums: List[int]) -> None:
        """  Do not return anything, modify nums in-place instead.  """
        bucket = [0 for _ in range(3)]
        for i in nums:
            bucket[i] += 1

        ret = []
        for i in range(3):
            ret += [i] * bucket[i]

        nums[:] = ret[:]

和排序相比,我們只是遍歷了兩次數據,第一次是遍歷了原數組獲得了其中0,1和2的數量,第二次是將獲得的數據重新填充回原數組當中。相比於快排或者是其他一些排序算法的耗時,桶排序只遍歷了兩次數組,明顯要快得多。但遺憾的是這並不是最佳的方法,題目當中明確說了,還有隻需要遍歷一次原數組的方法。

two pointers

在我們介紹具體的算法之前,我們先來分析一下問題。既然顏色只有三種,那麼當我們排完序之後,整個數組會被分成三個部分,頭部是0,中間是1,尾部是2。

我們可以用一個區間來收縮1的範圍,假設我們當前區間的首尾元素分別是l和r。當我們讀到0的時候,我們就將它和l交換,然後將l向後移動一位。當我們讀到2的時候,則將它和r進行交換,將r向左移動一位。也就是說我們保證l和r之間的元素只有1。

我們之前曾經介紹過這種維護一個區間的做法,雖然都是維護了一個區間,但是操作上是有一些區別的。之前介紹的two pointers算法,也叫做尺取法,本質上是通過移動區間的右側邊界來容納新的元素,通過移動左邊界彈出數據的方式來維護區間內所有元素的合法性。而當前的做法中,一開始獲得的就是一個非法的區間,我們通過元素的遍歷以及區間的移動,最後讓它變得合法。兩者的思路上有一些細微的差別,但形式是一樣的,就是通過移動左右兩側的邊界來維護或者是達到合法。

class Solution:
    def sortColors(self, nums: List[int]) -> None:
        """  Do not return anything, modify nums in-place instead.  """
        l, r = 0, len(nums)-1
        i = 0
        while i < len(nums):
            if i > r:
                break
   # 如果遇到0,則和左邊交換
            if nums[i] == 0:
                nums[l], nums[i] = nums[i], nums[l]
                l += 1
            # 如果遇到2,則和右邊交換
            # 交換之後i需要-1,因為可能換來一個0
            elif nums[i] == 2:
                nums[r], nums[i] = nums[i], nums[r]
                r -= 1
                continue
            i += 1

這種方法我們雖然只遍歷了數組一次,但是由於交換的次數過多,整體運行的速度比上面的方法還要慢。所以遍歷兩次數組並不一定就比只遍歷一次要差,畢竟兩者都是的算法,相差的只是一個常數。遍歷的次數只是構成常數的部分之一。

除了這個方法之外,我們還有其他維護區間的方法。

維護區間

接下來要說的方法非常巧妙,我個人覺得甚至要比上面的方法還有巧妙。

我們來假想一下這麼一個場景,假設我們不是在原數組上操作數據,而是從其中讀出數據放到新的數組當中。我們先不去想應該怎麼擺放這個問題,我們就來假設我們原數組當中的數據已經放好了若干個,那麼這個時候的新數組會是什麼樣?顯然,應該是排好序的,前面若干個0,中間若干個1,最後若干個2。

那麼問題來了,假設這個時候我們讀到一個0,那麼應該怎麼放呢?為了簡化敘述我們把它畫成圖:

我們假設藍色部分是0,綠色部分是1,粉色部分是2。a是0最右側的下標,b是1部分最右側的下標,c是2部分最右側的下標。那麼這個時候,當我們需要放入一個0的時候,應該怎麼辦?

我們結合圖很容易想明白,我們需要把0放在a+1的位置,那麼我們需要把後面1和2的部分都往右側移動一格,讓出一格位置出來放0。我們移動數組顯然帶來的開銷會過於大,實際上沒有必要移動整個部分,只需要移動頭尾元素即可。比如1的部分左側被0佔掉了一格,那麼為了保持長度不變,右側也需要延伸一格。同理,2的部分右側也需要延伸一格。那麼整個操作用代碼來表示就是:nums[a+1] = 0,nums[b+1] = 1, nums[c+1] = 2。

假設我們讀入的數是1,那麼我們需要把b延長一個單位,但是這樣帶來的後果是2的部分被侵佔,所以需要將2也延長,補上被1侵佔的一個單位。如果讀到的是2,那麼直接延長2即可,因為2後面沒有其他顏色了。

假設我們有一個空白的數組,我們可以這麼操作,但其實我們沒有必要專門創建一個數組,我們完全可以用原數組自己填充自己。因為我們從原數組上讀取的數和擺放的數是一樣的,我們直接把数字擺放在原數組的頭部,佔用之前讀取的數即可。

光說可能還有些迷糊,看下代碼馬上就清楚了:

class Solution:
    def sortColors(self, nums: List[int]) -> None:
        """  Do not return anything, modify nums in-place instead.  """
        # 記錄0,1和2的末尾位置
        zero, one, two = -1, -1, -1
        n = len(nums)
        for i in range(n):
            # 如果擺放0
            # 那麼1和2都往後平移一位,讓一個位置出來擺放0
            if nums[i] == 0:
                nums[two+1] = 2
                nums[one+1] = 1
                nums[zero+1] = 0
                zero += 1
                one += 1
                two += 1
            elif nums[i] == 1:
                nums[two+1] = 2
                nums[one+1] = 1
                one += 1
                two += 1
            else:
                nums[two+1] = 2
                two += 1

總結

到這裏,這道題的解法基本上都講完了。

相信大家也都看出來了,從難度上來說這題真的不難,相信大家都能想出解法來,但是要想到最優解還是有些困難的。一方面需要我們對題目有非常深入的理解,一方面也需要大量的思考。這類題目沒有固定的解法,需要我們根據題目的要求以及實際情況自行設計解法,這也是最考驗思維能力以及算法設計能力的問題,比考察某個算法會不會的問題要有意思得多。

希望大家都能從這題當中獲得樂趣,如果喜歡本文,可以的話,請點個關注,給我一點鼓勵,也方便獲取更多文章。

本文使用 mdnice 排版

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

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

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

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心

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

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

分類
發燒車訊

Action的三種實現方式,struts.xml配置的詳細解釋及其簡單執行過程(二)

勿以惡小而為之,勿以善小而不為————————–劉備

勸諸君,多行善事積福報,莫作惡

上一章簡單介紹了Struts2的’兩個蝴蝶飛,你好’ (一),如果沒有看過,請觀看上一章

一 Action的三種實現方式

上一章開發的HelloAction和HelloAction2,並沒有繼承任何類或者實現任何接口,但是必須有一個execute() 方法,方法返回值是String類型。

這樣的代碼不容易理解,更並不能使人看得出這個類是干什麼的,甚至不能區分這個控制器類與普通的Java類有什麼區別,通常開發中不這樣做。

我們開發者在開發Struts2框架的時候,希望自己寫的這個Action類能夠具有易理解性,且已經支持某些功能,如參數接收,文件上傳等。

一.一 第一種實現方式(普通Java類,裏面只包含execute()方法)

package com.yjl.web.action;
import org.apache.log4j.Logger;
/**
* @author 兩個蝴蝶飛
* @version 創建時間:2018年8月23日 上午9:41:32
* @description 第一種實現方式,普通java類,
* 有一個execute()方法,也可以多寫幾個方法,用action中的標籤method來控制,可以正常訪問。
*/
public class Hello1Action {
	private static Logger logger=Logger.getLogger(Hello1Action.class);
	public String execute() {
		logger.info("兩個蝴蝶飛,web層你好");
		return "success";
	}
}

不具有開發時要求的規範性,且不支持某些struts2自身提供的功能。

方法名稱只有一個 execute()

一.二 第二種實現方式(實現Action接口)

package com.yjl.web.action;
import com.opensymphony.xwork2.Action;
/**
* @author 兩個蝴蝶飛
* @version 創建時間:2018年8月23日 上午10:54:03
* @description 第二種實現方式,實現Action接口,重寫裏面的execute()方法
* 有一個execute()方法和五個String類型的常量
*/
public class Hello2Action implements Action{
	@Override
	public String execute() throws Exception {
		return Action.SUCCESS;
		//return Action.ERROR;
		//return Action.LOGIN;
		//return Action.NONE;
		//return Action.INPUT;
	}
}

注意,Action接口是xwork2包下的接口。

實現了Action接口,使開發者能夠看出來這是一個Action,具有了一定程度上的開發規範,

但是實現了Action接口,所以必須要重寫execute()方法。

一般自己寫Action,構思好之後上來就直接add(), edit(), delete(). select() 這些業務方法,

每次都要重寫execute()方法,不太方便。 而且這種方式不具有struts2中某些功能,如驗證框架和國際化。

Action中接口中有五個常用的結果字符串(好多方法都返回success,error,login,input,none,故將其封裝了一下) .

這些字符串雖然是大寫,然而真實的值是全部小寫.

package com.opensymphony.xwork2;

public abstract interface Action
{
  public static final String SUCCESS = "success";
  public static final String NONE = "none";
  public static final String ERROR = "error";
  public static final String INPUT = "input";
  public static final String LOGIN = "login";
  
  public abstract String execute()
    throws Exception;
}

一.三 繼承ActionSupport類(官方推薦)

	package com.yjl.web.action;
	import com.opensymphony.xwork2.ActionSupport;
	/**
	* @author 兩個蝴蝶飛
	* @version 創建時間:2018年8月23日 上午11:04:20
	* @description 第三種方式,繼承ActionSupport類。
	* ActionSupport類實現了Action接口,也有Action中的五個常量.
	*/
	public class Hello3Action extends ActionSupport{
		public String list() {
			return "list";
		}
	}

繼承了ActionSupport類,不需要重新寫execute()方法,直接寫業務方法即可。

ActionSupport類,已經實現了 Action接口。 其具備Action中的五個常量,並且該類還實現了其他接口,

源代碼:

	public class ActionSupport implements Action, Validateable, ValidationAware, TextProvider, LocaleProvider, Serializable{
    ...
	public String execute() throws Exception
  	{
		//默認返回的是 success 字符串 
  		 return "success";
 	}
 	...
}

如驗證框架(Validateable,ValidationAware),國際化(LocaleProvider)。

以後開發中,使用 繼承 ActionSupport 類的形式。

二 配置文件 struts.xml中節點的詳細解釋

在src下有一個struts.xml的配置文件,它配置了開發者自己編寫實現的Action,是struts2框架的核心,不能改變文件名稱。(注意,是struts.xml,並不是struts2.xml,並沒有那個2)。

在struts.xml中,最上面是一個約束, 是一個根節點。

二.一 修改常量節點

在struts-core.jar核心包下,有一個包org.apache.struts2包下,有一個default.properties屬性文件,裏面記錄了很多常用的常量,

其中常見的有:

struts.i18n.encoding=UTF-8 
struts.multipart.maxSize=2097152
struts.action.extension=action,,
struts.enable.DynamicMethodInvocation = false
struts.devMode = false
struts.ui.theme=xhtml
struts.ognl.allowStaticMethodAccess=false

建議修改后的值為:

###國際化操作,編碼格式為UTF-8
struts.i18n.encoding=UTF-8
###上傳文件時最大的上傳大小,默認為2M. 根據項目情況具體填寫值,建議後面加兩個00
struts.multipart.maxSize=209715200
###struts的訪問後綴名, struts1框架默認的是 .do 
struts.action.extension=action,,
###struts是否可以訪問靜態方法
struts.enable.DynamicMethodInvocation =true
###struts是否是開發者模式
struts.devMode =true
###struts中ui標籤的主題,建議為simple
struts.ui.theme=simple
###ognl中是否可以訪問靜態方法,為true
struts.ognl.allowStaticMethodAccess=true

可以在struts.xml中進行相應的修改,如

 <!--修改國際化編碼 -->
<constant name="struts.i18n.encoding" value="UTF-8"></constant>
<!--修改是否為開發者模式 -->
<constant name="struts.devMode" value="true"></constant>

按照name,value值的形式進行填寫。

也可以在src下新建一個struts.properties,然後將這些值放置進去,struts也會自動struts.propeties中的常量值的。

也可以在web.xml中,在 中,以 局部參數的形式傳遞進去。

建議使用第一種方式,在struts.xml中用 ,畢竟這個文件常常打開,出錯了也容易發現。

二.二 分模塊開發

在實際的項目中,有很多的模塊,如果所有的配置都放在一個struts.xml,那麼一旦這個struts.xml被其他人誤操作導致了錯誤,那麼其他人的項目將無法運行的,當配置內容過多時,struts.xml的內容太長,不便於維護,所以最好是分模塊開發,一個模塊用一個配置文件,然後再利用 進行導入, 類似 於jsp中的 靜態包含一樣。

所以建議每一個模塊都寫一個模塊.xml,然後在struts.xml中引入即可。如有三個模塊 User模塊和Class,Course,那麼可以將User的配置放置在user.xml中,Class配置放置在class.xml中,course模塊放置在course.xml,在struts.xml中只需要

	<include file="user.xml"></include>
	<include file="class.xml"></include>
	<include file="course.xml"></include>

靜態包含即可。 注意,file的文件路徑引用是否輸入正確。

正確的位置引用,點擊ctrl+模塊.xml時,可以跳轉到相應的.xml文件中。如果沒有跳轉和反應,那說明位置引用錯誤,需要重新檢查一下。

二.三 包節點

在struts.xml配置文件中,最重要的節點就是package節點。 package,分包。 可以將action進行分包處理。

這樣每一個action或者每一組action用package進行隔開,便於維護,類似於java中package的概念。

二.三.一 <package> 節點的使用

<package name="hello" extends="struts-default" namespace="/">
        <!--具體的Action-->
</package>

package中name節點是package的名字,是獨一無二的,不能夠重複。 最好與模塊名相同或者起一個有意義的名稱。

extends節點表示繼承,即package之間可以相互的繼承,來避免重複化功能的編寫。 默認為struts-default。

struts-default中struts已經定義了很多功能,開發者自己寫的包只需要extends 這個包名struts-default,

就擁有了struts已經定義好的功能。 如攔截器功能,文件上傳功能。

用戶也可以自己繼承自己所寫的包 。如父包名為

那麼子包只需要 , 這樣child包不但擁有struts-default的功能,也擁有parent包中的特殊功能,這也是Java的多重繼承的體現。 所以package的name 要符合標識符的規範,具有可讀性。

namespace節點表示命名空間,以/開頭,默認是”/” 。是為了在訪問路徑和訪問請求url方面體現package的分包作用. package中的name是在配置文件中體現分包,namespace是在url中體現分包。 建議開發中,namespace的路徑名與name保持一致。 package中的namespace的值與子節點action中name的值,共同構成了完整的訪問請求路徑。

二.三.二 <package></package> 子節點<action></action>節點的使用

在Hello3Action中定義兩個方法,一個是list()查詢,一個是add()添加的方法。

package com.yjl.web.action;
import org.apache.log4j.Logger;
import com.opensymphony.xwork2.ActionSupport;
/**
* @author 兩個蝴蝶飛
* @version 創建時間:2018年8月23日 上午11:04:20
* @description 測試action標籤中method的方法訪問
*/
public class Hello3Action extends ActionSupport{
	private static final long serialVersionUID = 8737138848863458260L;
	Logger logger=Logger.getLogger(Hello3Action.class);
	public String list() {
		logger.info("執行list方法");
		return "list";
	}
	public String add() {
		logger.info("執行add方法");
		return "add";
	}
}

標籤,有三個基本的屬性,

	<action name="list" class="com.yjl.web.action.Hello3Action"
        method="list">

</action>

其中name為action的名字,表示區別一個package包下的不同的action。 其中這個name的值,不應該隨便取,應該是要訪問的方法名。

在瀏覽器客戶端請求的url為 /項目名/package的namespace名稱/action的name名稱.action;

class為要訪問的那個Action的全限定名稱,是class,用.(點)進行分隔。

其中,class 可以省略, 省略默認為 ActionSupport 類, 全限定名稱為: com.opensymphony.xwork2.ActionSupport
method為要訪問的那個方法名稱,類 extends ActionSupport 后,有很多很多的方法,如list(), add(), delete()等,那麼怎麼知道具體要訪問哪個方法呢? 用method這個屬性. method=”要方法的方法名” ,是方法名。

action還有一個節點是converter,表示所用的是哪一個類型轉換器。(後面會有相應的解釋)

很清楚, action 中的 class指定了訪問的是哪一個action, method 指定了訪問的是哪一個具體的方法, 利用了反射技術實現。

在本實例了有兩個方法,所以要進行寫兩個Action, 一個Action類中會有多個方法,難道要一個個配置多個Action嗎?

Struts2提供了一些簡單的方式

二.三.三 配置Action的三種形式

二.三.三.一 通過配置method的屬性完成

簡單舉例如下:

	<action name="list" class="com.yjl.web.action.Hello3Action"
		method="list">
			
	</action>
  <action name="add" class="com.yjl.web.action.Hello3Action"
		method="add">
            
    </action>

缺點: 有幾個方法,就要配置有幾個action,當方法過多時,不易維護。

二.三.三.二 通過配置 通配符完成。

簡單舉例如下:

	<action name="Hello3_*" class="com.yjl.web.action.Hello3Action"
		method="{1}">
			
		</action>

name的值為: 類簡寫名(去掉Action后)_* method中的值取第一個{1},從1開始,不是從0開始。

這樣訪問Hello3Action中的list方法,訪問路徑就是 Hello3_list

訪問Hello3Action中的add方法,訪問路徑就是Hello3_add

簡化了action的相關配置。

也有的人配置的更狠, 會配置成_, 即:

	<action name="*_*" class="com.yjl.web.action.{1}Action"
		method="{2}">
			
		</action>

User類中的list就是User_list, User類中的add就是User_add,

Class類中的list就是Class_list,Class類中的add就是Class_add

這樣雖說簡化了開發,但卻不利用 result 節點的維護 ,不建議這樣配置。

好多類的好多方法返回值,都寫在這一個action 下面,會亂。

二.三.三.三 動態方法訪問

不是用 * 通配符,而是用! 號。 即:

想訪問UserAction中list方法() 前端寫url為 userAction!list.action
想訪問UserAction中add方法() 前端寫url為 userAction!add.action
想訪問ClassAction中list方法() 前端寫url為 classAction!list.action
想訪問ClassAction中add方法() 前端寫url為 classAction!add.action

這樣訪問也特別的方便。

這樣的話, action中只需要配置name和class即可。 method已經由外部指定了,不需要寫method的值了。

需要先添加變量 struts.enable.DynamicMethodInvocation, 使其變成 true,開啟。

	<constant name="struts.enable.DynamicMethodInvocation" value="true"></constant>

如果是UserAction的話,配置應該是:

<action name="userAction" class="com.yjl.web.action.UserAction" >
			
</action>

ClassAction的話,配置應該是

<action name="classAction" class="com.yjl.web.action.ClassAction" >
			
</action>

二.三.四 action子節點result的配置

result表示結果,是對方法的返回值進行相應的分析。有兩個屬性,name和type

	<result name="success" type="dispatcher">/index.jsp</result>

其中name的值要與方法的返回值保持一致。

如 list方法返回值是return SUCCESS,那麼這個list方法的返回值對應的result的值就是 ,

如果返回是”hello”, 那麼這個name的返回值就是

如果在action中配置通配符, name=Hello3_*形式,method=”{1}”, 那麼為了簡化result的配置,可以將result配置成 name={1},

相應的.jsp,可以變成 /{1}.jsp。

但這樣必須保證Action中方法的名稱與返回值的名稱相同,並且與跳轉到的jsp的名稱也要相同, 這樣不太好。

result中type五種常見的形式, dispatcher(轉發到jsp),redirect(重定向到jsp), chain(轉發到另外一個方法),redirectAction(重定向到另外一個方法),stream(上傳和下載流)

其中dispathcer和redirect是跳轉到jsp,如果想要傳遞數據,用dispather,

如果不想傳遞數據,用redirect (dispathcer是轉發,redirect是重定向)

chain,redirectAction是跳轉到action的操作,一般用於這同一個類中兩個方法之間的跳轉,

如add()添加成功之後,需要跳轉到list()方法進行显示結果,這時就可以配置成:

	<result name="add" type="redirectAction">Hello3_list</result>

地址url也會相應的改變,如果是chain的話,地址欄是不會改變的。 chain是轉發到action, redirectAction是重定向到action.

也可以在不同包之間的action進行的跳轉 。

如 add 方法 想到跳轉到 /class 命名空間下的 Hello2Action 的 list 方法。

<result name="add" type="redirectAction">
	<!-- 要跳轉到哪一個命名空間,即哪一個包 -->
	<param name="namespace">/class</param>
	<!-- 要跳轉到哪一個Action 不加後綴 -->
	<param name="actionName">Hello2Action</param>
	<!-- 跳轉到哪一個方法 -->
	<param name="method">list</param>
	<!-- 可能要傳遞的參數. 用ognl表達式,根據情況添加 -->
	<param name="id">${id}</param>
</result>

通過 param 標籤來配置帶參還是不帶參。

二.四 全局結果頁面與局部結果頁面。

這個全局是相對於package來說的,是package中的全局,並不是所有的struts.xml中的全局,所以全局結果的節點位置應該放在package節點裏面,與action節點平行。 用 節點。

常用的全局結果頁面有兩種:

error錯誤頁面,頁面出錯了都显示這個頁面,

login 登錄頁面, 如果沒有登錄,輸入任何url都會跳轉到login頁面(認證時用)

noprivilege 沒有權限頁面,如果用戶沒有權限訪問了某一個頁面,會給出相應的提示(授權時用)

<global-results>
			<result name="error">/error/error.jsp</result>
			<result name="login">/login.jsp</result>
            <result name="noprivilege">/noprivilege.jsp</result>
</global-results>

當全局結果頁面與局部結果頁面發生衝突時,以局部結果頁面為準。

全局配置時:

<global-results>
			<result name="success">/successGlobal.jsp</result>
</global-results>

在該包下的某個action 的方法result 也返回了 success

	<result name='success'>success.jsp</result>

那麼,當邏輯視圖為 success時,最終將返回 success.jsp

二.五 配置跳轉頁面

在開發中,常常有這麼一種情況,

請求login.jsp 時,為 /login, 那麼就跳轉到 login.jsp 頁面,

語法為 register.jsp 時,為 /register, 那麼就跳轉到 register 頁面。

這個時候,配置 為:

	<action name="*">
			<result>/WEB-INF/content/{1}.jsp</result>
	</action>

將頁面放置在 content 文件夾下面,避免用戶直接訪問 jsp頁面。

注意,要將此 action 放置在最後, 當所有上面的action都不匹配時,才匹配這一個action.

三 Struts2的執行流程

當用戶在客戶端發送一個請求后,如常用的標準的http://localhost:8080/Struts_Hello/user/User_add.action時,

會經過前端控制器(StrutsPrepareAndExecuteFilter) 過濾器,執行一連串的過濾器鏈,然後根據user 找到了對應的package的namespape,進入到具體的package包下。 利用通配符的方式進行訪問,User_add會進行匹配相應的action,根據class和method找到是哪一個類的哪一個方法,在實例化類Action之前,會先執行攔截器。通過反射實例化類,運行方法, 方法運行成功之後,有一個返回值,這個返回值會與剛才action下的 中的name進行相應的匹配,匹配到哪一個,就執行哪一個result。 如果是diapatcher或者redirect,就显示到相應的.jsp頁面(帶有數據), 如果是chain或者redirectAction,那麼就去執行那一個方法,之後進行返回具體的視圖。

執行過程圖如下:

謝謝您的觀看!!!

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

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

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

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

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

分類
發燒車訊

微前端與項目實施方案研究

一、前言

微前端(micro-frontends)是近幾年在前端領域出現的一個新概念,主要內容是將前端應用分解成一些更小、更簡單的能夠獨立開發、測試、部署的小塊,而在用戶看來仍然是內聚的單個產品。微前端的理念源於微服務,是將龐大的整體拆成可控的小塊,並明確它們之間的依賴關係,而它的價值在於能將低耦合的代碼與組件進行組合,基座+基礎協議模式能接入大量應用,進行統一的管理和輸出,許多公司與團隊也都在不斷嘗試和優化相關解決技術與設計方案,為這一概念的落地和推廣添磚加瓦。結合自身遇到的問題,適時引用微前端架構能起到明顯的提效賦能作用。

二、背景

目前我司擁有大量的內部系統,這些系統採用相同的技術棧,在實際開發和使用過程中,逐漸暴露出如下幾個問題:

1.有大量可復用的部分,雖然有組件庫,但是依賴版本難統一;
2.靜態資源體積過大,影響頁面加載和渲染速度;
3.應用切換目前是通過鏈接跳轉的方式實現,會有白屏和等待時長的問題,對用戶體驗不夠友好;
針對上述幾個問題,決定採用微前端架構對內部系統進行統一的管理,本文也是圍繞微前端落地的技術預研方案。

三、方案調研

目前業界有多種解決方案,有各自的優缺點,具體如下:

  • 路由轉發:路由轉發嚴格意義上不屬於微前端,多個子模塊之間共享一個導航即可 簡單,易實現 體驗不好,切換應用整個頁面刷新;

  • 嵌套 iframe:每個子應用一個 iframe 嵌套 應用之間自帶沙箱隔離 重複加載腳本和樣式;

  • 構建時組合:獨立倉儲,獨立開發,構建時整體打包,合併應用 方便依賴管理,抽取公共模塊 無法獨立部署,技術棧,依賴版本必須統一;

  • 運行時組合:每個子應用獨立構建,運行時由主應用負責應用管理,加載,啟動,卸載,通信機制 良好的體驗,真正的獨立開發,獨立部署 複雜,需要設計加載,通信機制,無法做到徹底隔離,需要解決依賴衝突,樣式衝突問題;

    開源微前端框架也有多種,例如阿里出品的qiankun,icestark,還有針對angular提出的mooa等,都能快速接入項目,但結合公司內部系統的特點,直接採用會有有些限制,例如要實現定製界面,無刷新加載應用,且不能對現有項目的開發和部署造成影響,因此決定自研相關技術。

四、架構設計

4.1 應用層

應用層包括所有接入微服務工作台的內部系統,他們各自開發與部署,接入前後沒有多大影響,只是需要針對微服務層單獨輸出打包一份靜態資源;

4.2 微服務層

微服務層作為核心模塊,擁有資源加載、路由管理、狀態管理和用戶認證管理幾大功能,具體內容將在後面詳細闡述,架構整體工作流程如下:

4.3 基礎支撐層

基礎支撐層作為基座,提供微服務運行的環境和容器,同時接入其他後端服務,豐富實用場景和業務功能;

五、技術重難點

要實現自定義微前端架構,難點在於需要管理和整合多個應用,確保應用之間獨立運行,彼此不受影響,需要解決如下幾個問題:

5.1 資源管理

5.1.1資源加載

每個應用有一個應用資源管理和註冊的文件(app.regiser.js),其中包含路由信息,應用配置信息(configs.js)和靜態資源清單,當首次切換到某應用時,首先加載app.register.js文件,完成路由和應用信息的註冊,然後根據當前瀏覽器路由地址加載對應的靜態文件,完成頁面渲染,從而將各應用的靜態資源串聯起來,其中註冊入口文件通過webpack插件來實現,具體實現如下:
FuluAppRegisterPlugin.prototype.apply = function(compiler) {
   appId = extraAppId();
   var entry = compiler.options.entry;
   if (isArray(entry)) {
            for (var i = 0; i &lt; entry.length; i++) {
                if (isIndexFile(entry[i])) { // 入口文件
                    indexFileEdit(entry[i]);
                    entry[i] = entry[i].replace(indexEntryRegx, indeEntryTemp); // 替換入口文件
                    i = entry.length;
                }
            }
    } else {
            if (isIndexFile(entry)) { // 入口文件
                indexFileEdit(entry); // 重新生成和編輯入口文件
                compiler.options.entry = compiler.options.entry.replace(indexEntryRegx, indeEntryTemp); // 替換入口文件
            }
    }
    compiler.hooks.done.tap('fulu-app-register-done', function(compilation) {
            fs.unlinkSync(tempFilePath); // 刪除臨時文件
            return compilation;
    });
    compiler.hooks.emit.tap('fulu-app-register', function(compilation) {
        var contentStr = 'window.register("'+ appId + '", {\nrouter: [ \n ' + extraRouters() + ' \n],\nentry: {\n'; // 全局註冊方法
        var entryCssArr = [];
        var entryJsArr = [];
        for (var filename in compilation.assets) {
            if (filename.match(mainCssRegx)) { // 提取css文件
                entryCssArr.push('\"' + filename + '\"');
            } else if (filename.match(mainJsRegx) || filename.match(manifestJsRegx) || filename.match(vendorsJsRegx)) { // 提取js文件
                entryJsArr.push('\"' + filename + '\"');
            }
        }
        contentStr += ('css: ['+ entryCssArr.join(', ') +'],\n'); // css資源清單
        contentStr += ('js: ['+ entryJsArr.join(', ') +'],\n }\n});\n'); // js資源清單
        compilation.assets['resources/js/' + appId + '-app-register.js'] = { // 生成appid-app-register.js入口文件
            source: function() {
                return contentStr;
            },
            size: function() {
                return contentStr.length;
            }
        };
        return compilation;
    });
};
5.1.2資源文件名
微服務輸出打包模式下,靜態資源統一打包形式以項目id開頭,形如10000092-main.js, 文件名稱的修改通過webpack的插件實現;

核心實現代碼如下:

FuluAppRegisterPlugin.prototype.apply = function(compiler) {
    ......
    compiler.options.output.filename = addIdToFileName(compiler.options.output.filename, appId);
    compiler.options.output.chunkFilename = addIdToFileName(compiler.options.output.chunkFilename, appId);
    compiler.options.plugins.forEach((c) =&gt; {
        if (c.options) {
            if (c.options.filename) {
                c.options.filename = addIdToFileName(c.options.filename, appId);
            }
            if (c.options.chunkFilename) {
                c.options.chunkFilename = addIdToFileName(c.options.chunkFilename, appId);
            }
        }
    });
   ......
};

5.2 路由管理

路由分為應用級和菜單級兩大類,應用類以應用id為前綴,將各應用區分開,避免路由地址重名的情況,菜單級的路由由各應用的路由系統自行管理,結構如下:

5.3 狀態分隔

前端項目通過狀態管理庫來進行數據的管理,為了保證各應用彼此間獨立,因此需要修改狀態庫的映射關係,這一部分需要藉助於webpack插件來進行統一的代碼層面調整,包括model和view兩部分代碼,model定義了狀態對象,view藉助工具完成狀態對象的映射,調整規則為【應用id+舊狀態對象名稱】,下面來講解一下插件的實現;

插件的實現原理是藉助AST的搜索語法匹配源代碼中的狀態編寫和綁定的相關代碼,然後加上應用編號前綴,變成符合預期的AST,最後輸出成目標代碼:
module.exports = function(source) {
      var options = loaderUtils.getOptions(this);
	stuff = 'app' + options.appId;
	isView = !!~source.indexOf('React.createElement'); // 是否是視圖層
	allFunc = [];
	var connectFn = "function connect(state) {return Object.keys(state).reduce(function (obj, k) { var nk = k.startsWith('"+stuff+"') ? k.replace('"+stuff+"', '') : k; obj[nk] = state[k]; return obj;}, {});}";
	connctFnAst = parser.parse(connectFn);
	const ast = parser.parse(source, { sourceType: "module", plugins: ['dynamicImport'] });
	traverse(ast, {
		CallExpression: function(path) {
			if (path.node.callee && path.node.callee.name === 'connect') { // export default connext(...)
				if (isArray(path.node.arguments)) {
					var argNode = path.node.arguments[0];
					if (argNode.type === 'FunctionExpression') { // connect(() => {...})
						traverseMatchFunc(argNode);
					} else if (argNode.type === 'Identifier' && argNode.name !== 'mapStateToProps') { // connect(zk)
						var temp_node = allFunc.find((fnNode) => {
							return fnNode.id.name === argNode.name;
						});
						if (temp_node) {
							traverseMatchFunc(temp_node);
						}
					}
				}
			} else if (path.node.callee && path.node.callee.type === 'SequenceExpression') {
				if (isArray(path.node.callee.expressions)) {
					for (var i = 0; i < path.node.callee.expressions.length; i++) {
						if (path.node.callee.expressions[i].type === 'MemberExpression'
							&& path.node.callee.expressions[i].object.name === '_dva'
							&& path.node.callee.expressions[i].property.name === 'connect') {
								traverseMatchFunc(path.node.arguments[0]);
								i = path.node.callee.expressions.length;
						}
					}
				}
			}
		},
		FunctionDeclaration: function(path) {
			if (path.node.id.name === 'mapStateToProps' && path.node.body.type === 'BlockStatement') {
				traverseMatchFunc(path.node);
			}
			allFunc.push(path.node);
		},
		ObjectExpression: function(path) {
			if (isView) {
				return;
			}
			if (isArray(path.node.properties)) {
				var temp = path.node.properties;
				for (var i = 0; i < temp.length; i++) {
					if (temp[i].type === 'ObjectProperty' && temp[i].key.name === 'namespace') {
						temp[i].value.value = stuff + temp[i].value.value;
						i = temp.length;
					}
				}
			}
		}
	});
	return core.transformFromAstSync(ast).code;
};

5.4 框架容器渲染

完成以上步驟的改造,就可以實現容器中的頁面渲染,這一部分涉及到組件庫框架層面的調整,大流程如下圖:

六、構建流程

6.1 使用插件

構建過程中涉及到兩款自開發的插件,分別是fulu-app-register-plugin和fulu-app-loader;

6.1.1 安裝
npm i fulu-app-register-plugin fulu-app-loader -D;
6.1.2 配置

webpack配置修改:

const FuluAppRegisterPlugin = require('fulu-app-register-plugin');
module: {
   rules: [{
         test: /\.jsx?$/,
         loader: 'fulu-app-loader',
      }
   ]
}
plugins: [
    new FuluAppRegisterPlugin(),
    ......
]

6.2.編譯

編譯過程與目前項目保持一致,相比以前,多輸出了一份微前端項目編譯代碼,流程如下:

七、遺留問題

7.1 js環境隔離

由於各應用都加載到同一個運行環境,因此如果修改了公共的部分,則會對其他系統產生不可預知的影響,目前沒有比較好的辦法來解決,後續將持續關注這方面的內容,逐漸優化達到風險可制的效果。

7.2.獲取token

目前應用切換使用重定向來完成token獲取,要實現如上所述的微前端效果,需要放棄這種方式,改用接口調用異步獲取,或者其他解決方案。

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

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

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

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

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