中國出臺新能源汽車補貼新政 純電動汽車直補6萬

9月17日,中國財政部、科技部、工信部、發改委聯合出臺的《關于繼續開展新能源汽車推廣應用工作的通知》(以下簡稱“通知”), 新能源家用汽車每臺直補3.5萬~6萬元(人民幣),被經濟界稱為“節能家電補貼”嫁接出了新能源汽車版。

同時,今年購買新能源汽車將最為實惠。2014年、2015年,會按照補貼標準依次下降補貼10%、20%。此次“通知”補貼的主要范疇為純電動乘用車、插電式混合動力(含增程式)乘用車、純電動專用車、燃料電池汽車四類。相比2010年4部委首次頒布的“新能源汽車補貼標準”,“通知”的最大特點是對全國普惠、優化補貼流程,並兼顧惠及生產、銷售、消費三方利益。

一步到位的購車補貼流程對消費市場是重大利好,消費者到4S店購車,即可從車款中直接減掉對應的補貼款。此後,由中央財政與車企完成結算。同時,“通知”也採取了分級補貼的形式:純電動汽車續航裏程在80~150公裏補貼3.5萬元/臺;150~250公裏補貼5萬元/臺;250公裏以上補貼6萬元/臺。插電式混合動力(含增程式)乘用車,一次直補3.5萬元。

但是,也許政策補貼可讓純電動汽車裸車購買價更低,但其綜合經濟性並不能完全超過傳統汽車。由於購車價格較高,充電不方便,以及消費者對於新能源汽車的安全係數與技術問題的擔憂,導致目前新能源汽車在中國仍沒有很大的需求。

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

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

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

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

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

※幫你省時又省力,新北清潔一流服務好口碑

※回頭車貨運收費標準

在運行時生成C# .NET類

​本文譯自​:​Generating C# .NET Classes at Runtime
作者:WedPort

在我的C#職業生涯中,有幾次我不得不在運行時生成新的類型。希望把它寫下來能幫助有相同應用需求的人。這也意味着我以後不必在查找相同問題的StackOverflow文章了。我最初是在.NET 4.6.2中這樣做的,但我已經更新到為.NET Core 3.0提供了示例。所有代碼都可以在我的GitHub上面找到。
GitHub:https://github.com/cheungt6/public/tree/master/ReflectionEmitClassGeneration

為什麼我需要在運行時生成類?

在運行時生產新類型的需求通常是由於運行時才知道類屬性,滿足性能要求以及需要在新類型中添加功能。當你嘗試這樣做的時候,你應該考慮的第一件事是:這是否真的是一個明智的解決方案。在深入思考之前,還有很多其他事情可以嘗試,問你自己這樣的問題:

  1. 我可以使用普通的類嗎
  2. 我可以使用Dictionary、Tuple或者對象數組(Array)?
  3. 我是否可以使用擴展對象
  4. 我確定我不能使用一個普通的類嗎?

如果你認為這仍然是必要的,請繼續閱讀下面的內容。

示例用例

作為一名開發人員,我將大量數據綁定到各種WPF Grids中。大多數時候屬性是固定的,我可以使用預定義的類。有時候,我不得不動態的構建網格,並且能夠在應用程序運行時更改數據。採取以下显示ID和一些財務數據的類(FTSE和CAC是指數,其屬性代表指數價格):

public class PriceHolderViewModel : ViewModelBase
{
    public long Id { get; set; }
    public decimal FTSE100 { get; set; }
    public decimal CAC40 { get; set; }
}

如果我們僅對其中的屬性感興趣,該類定義的非常棒。但是,如果要使用更多屬性擴展此類,則需要在代碼中添加它,重新編譯並在新版本中進行部署。

相反的,我們可以做的是跟蹤對象所需的屬性,並在運行時構建類。這將允許我們在需要是不斷的添加和刪除屬性,並使用反射來更新它們的值。

// Keep track of my properties
var _properties = new Dictionary<string, Type>(new[]{
   new KeyValuePair<string, Type>( "FTSE100", typeof(Decimal) ),
   new KeyValuePair<string, Type>( "CAC40", typeof(Decimal) ) });

創建你的類型

下面的示例向您展示了如何在運行時構建新類型。你需要使用**System.Reflection.Emit**庫來構造一個新的動態程序集,您的類將在其中創建,然後是模塊和類型。與舊的** .NET Framework**框架不同,在舊的版本中,你需要在當前程序的AppDomain中創建程序集 ,而在** .NET Core** 中,AppDomain不再可用。你將看到我使用GUID創建了一個新類型名稱,以便於跟蹤類型的版本。在以前,你不能創建具有相同名稱的兩個類型,但是現在似乎不是這樣了。

public Type GeneratedType { private set; get; }

private void Initialise()
{
    var newTypeName = Guid.NewGuid().ToString();
    var assemblyName = new AssemblyName(newTypeName);
    var dynamicAssembly = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
    var dynamicModule = dynamicAssembly.DefineDynamicModule("Main");
    var dynamicType = dynamicModule.DefineType(newTypeName,
            TypeAttributes.Public |
            TypeAttributes.Class |
            TypeAttributes.AutoClass |
            TypeAttributes.AnsiClass |
            TypeAttributes.BeforeFieldInit |
            TypeAttributes.AutoLayout,
            typeof(T));     // This is the type of class to derive from. Use null if there isn't one
    dynamicType.DefineDefaultConstructor(MethodAttributes.Public |
                                        MethodAttributes.SpecialName |
                                        MethodAttributes.RTSpecialName);
    foreach (var property in Properties)
        AddProperty(dynamicType, property.Key, property.Value);

    GeneratedType = dynamicType.CreateType();
}

在定義類型時,你可以提供一種類型,從中派生新的類型。如果你的基類具有要包含在新類型中的某些功能或屬性,這將非常有用。之前,我曾使用它在運行時擴展ViewModelSerializable類型。

在你創建了TypeBuilder后,你可以使用下面提供的代碼開始添加屬性。它創建了支持字段和所需的中間語言,以便通過GetterSetter訪問它們。為每個屬性完成此操作后,可以使用CreateType()創建類型的實例。

private static void AddProperty(TypeBuilder typeBuilder, string propertyName, Type propertyType)
{
    var fieldBuilder = typeBuilder.DefineField("_" + propertyName, propertyType, FieldAttributes.Private);
    var propertyBuilder = typeBuilder.DefineProperty(propertyName, PropertyAttributes.HasDefault, propertyType, null);
    
    var getMethod = typeBuilder.DefineMethod("get_" + propertyName,
        MethodAttributes.Public |
        MethodAttributes.SpecialName |
        MethodAttributes.HideBySig, propertyType, Type.EmptyTypes);
    var getMethodIL = getMethod.GetILGenerator();
    getMethodIL.Emit(OpCodes.Ldarg_0);
    getMethodIL.Emit(OpCodes.Ldfld, fieldBuilder);
    getMethodIL.Emit(OpCodes.Ret);

    var setMethod = typeBuilder.DefineMethod("set_" + propertyName,
          MethodAttributes.Public |
          MethodAttributes.SpecialName |
          MethodAttributes.HideBySig,
          null, new[] { propertyType });
    var setMethodIL = setMethod.GetILGenerator();
    Label modifyProperty = setMethodIL.DefineLabel();
    Label exitSet = setMethodIL.DefineLabel();

    setMethodIL.MarkLabel(modifyProperty);
    setMethodIL.Emit(OpCodes.Ldarg_0);
    setMethodIL.Emit(OpCodes.Ldarg_1);
    setMethodIL.Emit(OpCodes.Stfld, fieldBuilder);
    setMethodIL.Emit(OpCodes.Nop);
    setMethodIL.MarkLabel(exitSet);
    setMethodIL.Emit(OpCodes.Ret);

    propertyBuilder.SetGetMethod(getMethod);
    propertyBuilder.SetSetMethod(setMethod);
}

有了類型后,就很容易通過使用Activator.CreateInstance()來創建它的實例。但是,你希望能夠更改已創建的屬性的值,為了做到這一點,你可以再次使用反射來獲取propertyInfos並提取Set方法。一旦有了這些屬性,電影它們類設置屬性值就相對簡單了。

foreach (var property in Properties)
{
    var propertyInfo = GeneratedType.GetProperty(property.Key);
    var setMethod = propertyInfo.GetSetMethod();
    setMethod.Invoke(objectInstance, new[] { propertyValue });
}

現在,您可以在運行時使用自定義屬性來創建自己的類型,並具有更新其值的功能,一切就緒。 我發現的唯一障礙是創建一個可以存儲新類型實例的列表。 WPF中的DataGrid傾向於只讀取List的常規參數類型的屬性。 這意味着即使您使用新屬性擴展了基類,使用AutoGenerateProperties也只能看到基類中的屬性。 解決方案是使用生成的類型顯式創建一個新的List。 我在下面提供了如何執行此操作的示例:

var listGenericType = typeof(List<>);
var list = listGenericType.MakeGenericType(GeneratedType);
var constructor = list.GetConstructor(new Type[] { });
var newList = (IList)constructor.Invoke(new object[] { });
foreach (var value in values)
    newList.Add(value);

結論

我已經在GitHub中創建了一個示例應用程序。它包含一個UI來幫助您調試和理解運行時新類型的創建,以及如何更新值。如果您有任何問題或意見,請隨時與我們聯繫。

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

【其他文章推薦】

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

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

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

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

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

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

一個線上問題的思考:Eureka註冊中心集群如何實現客戶端請求負載及故障轉移?

前言

先拋一個問題給我聰明的讀者,如果你們使用微服務SpringCloud-Netflix進行業務開發,那麼線上註冊中心肯定也是用了集群部署,問題來了:

你了解Eureka註冊中心集群如何實現客戶端請求負載及故障轉移嗎?

可以先思考一分鐘,我希望你能夠帶着問題來閱讀此篇文章,也希望你看完文章後會有所收穫!

背景

前段時間線上Sentry平台報警,多個業務服務在和註冊中心交互時,例如續約註冊表增量拉取等都報了Request execution failed with message : Connection refused 的警告:

緊接着又看到 Request execution succeeded on retry #2 的日誌。

看到這裏,表明我們的服務在嘗試兩次重連后和註冊中心交互正常了。

一切都顯得那麼有驚無險,這裏報Connection refused 是註冊中心網絡抖動導致的,接着觸發了我們服務的重連,重連成功后一切又恢復正常。

這次的報警雖然沒有對我們線上業務造成影響,並且也在第一時間恢復了正常,但作為一個愛思考的小火雞,我很好奇這背後的一系列邏輯:Eureka註冊中心集群如何實現客戶端請求負載及故障轉移?

註冊中心集群負載測試

線上註冊中心是由三台機器組成的集群,都是4c8g的配置,業務端配置註冊中心地址如下(這裏的peer來代替具體的ip地址):

eureka.client.serviceUrl.defaultZone=http://peer1:8080/eureka/,http://peer2:8080/eureka/,http://peer3:8080/eureka/

我們可以寫了一個Demo進行測試:

註冊中心集群負載測試

1、本地通過修改EurekaServer服務的端口號來模擬註冊中心集群部署,分別以87618762兩個端口進行啟動
2、啟動客戶端SeviceA,配置註冊中心地址為:http://localhost:8761/eureka,http://localhost:8762/eureka

3、啟動SeviceA時在發送註冊請求的地方打斷點:AbstractJerseyEurekaHttpClient.register(),如下圖所示:

這裏看到請求註冊中心時,連接的是8761這個端口的服務。

4、更改ServiceA中註冊中心的配置:http://localhost:8762/eureka,http://localhost:8761/eureka
5、重新啟動SeviceA然後查看端口,如下圖所示:

此時看到請求註冊中心是,連接的是8762這個端口的服務。

註冊中心故障轉移測試

以兩個端口分別啟動EurekaServer服務,再啟動一個客戶端ServiceA。啟動成功后,關閉一個8761端口對應的服務,查看此時客戶端是否會自動遷移請求到8762端口對應的服務:

1、以87618762兩個端口號啟動EurekaServer
2、啟動ServiceA,配置註冊中心地址為:http://localhost:8761/eureka,http://localhost:8762/eureka
3、啟動成功后,關閉8761端口的EurekaServer
4、在EurekaClient發送心跳請求的地方打上斷點:AbstractJerseyEurekaHttpClient.sendHeartBeat()
5、查看斷點處數據,第一次請求的EurekaServer8761端口的服務,因為該服務已經關閉,所以返回的responsenull

6、第二次會重新請求8762端口的服務,返回的response為狀態為200,故障轉移成功,如下圖:

思考

通過這兩個測試Demo,我以為EurekaClient每次都會取defaultZone配置的第一個host作為請求EurekaServer的請求的地址,如果該節點故障時,會自動切換配置中的下一個EurekaServer進行重新請求。

那麼疑問來了,EurekaClient每次請求真的是以配置的defaultZone配置的第一個服務節點作為請求的嗎?這似乎也太弱了!!?

EurekaServer集群不就成了偽集群!!?除了客戶端配置的第一個節點,其它註冊中心的節點都只能作為備份和故障轉移來使用!!?

真相是這樣嗎?NO!我們眼見也不一定為實,源碼面前毫無秘密!

翠花,上乾貨!

客戶端請求負載原理

原理圖解

還是先上結論,負載原理如圖所示:

這裡會以EurekaClient端的IP作為隨機的種子,然後隨機打亂serverList,例如我們在商品服務(192.168.10.56)中配置的註冊中心集群地址為:peer1,peer2,peer3,打亂后的地址可能變成peer3,peer2,peer1

用戶服務(192.168.22.31)中配置的註冊中心集群地址為:peer1,peer2,peer3,打亂后的地址可能變成peer2,peer1,peer3

EurekaClient每次請求serverList中的第一個服務,從而達到負載的目的。

代碼實現

我們直接看最底層負載代碼的實現,具體代碼在
com.netflix.discovery.shared.resolver.ResolverUtils.randomize() 中:

這裏面random 是通過我們EurekaClient端的ipv4做為隨機的種子,生成一個重新排序的serverList,也就是對應代碼中的randomList,所以每個EurekaClient獲取到的serverList順序可能不同,在使用過程中,取列表的第一個元素作為serverhost,從而達到負載的目的。

思考

原來代碼是通過EurekaClientIP進行負載的,所以剛才通過DEMO程序結果就能解釋的通了,因為我們做實驗都是用的同一個IP,所以每次都是會訪問同一個Server節點。

既然說到了負載,這裏肯定會有另一個疑問:

通過IP進行的負載均衡,每次請求都會均勻分散到每一個Server節點嗎?

比如第一次訪問Peer1,第二次訪問Peer2,第三次訪問Peer3,第四次繼續訪問Peer1等,循環往複……

我們可以繼續做個試驗,假如我們有10000個EurekaClient節點,3個EurekaServer節點。

Client節點的IP區間為:192.168.0.0 ~ 192.168.255.255,這裏面共覆蓋6w多個ip段,測試代碼如下:

/**
 * 模擬註冊中心集群負載,驗證負載散列算法
 *
 *  @author 一枝花算不算浪漫
 *  @date 2020/6/21 23:36
 */
public class EurekaClusterLoadBalanceTest {

    public static void main(String[] args) {
        testEurekaClusterBalance();
    }

    /**
     * 模擬ip段測試註冊中心負載集群
     */
    private static void testEurekaClusterBalance() {
        int ipLoopSize = 65000;
        String ipFormat = "192.168.%s.%s";
        TreeMap<String, Integer> ipMap = Maps.newTreeMap();
        int netIndex = 0;
        int lastIndex = 0;
        for (int i = 0; i < ipLoopSize; i++) {
            if (lastIndex == 256) {
                netIndex += 1;
                lastIndex = 0;
            }

            String ip = String.format(ipFormat, netIndex, lastIndex);
            randomize(ip, ipMap);
            System.out.println("IP: " + ip);
            lastIndex += 1;
        }

        printIpResult(ipMap, ipLoopSize);
    }

    /**
     * 模擬指定ip地址獲取對應註冊中心負載
     */
    private static void randomize(String eurekaClientIp, TreeMap<String, Integer> ipMap) {
        List<String> eurekaServerUrlList = Lists.newArrayList();
        eurekaServerUrlList.add("http://peer1:8080/eureka/");
        eurekaServerUrlList.add("http://peer2:8080/eureka/");
        eurekaServerUrlList.add("http://peer3:8080/eureka/");

        List<String> randomList = new ArrayList<>(eurekaServerUrlList);
        Random random = new Random(eurekaClientIp.hashCode());
        int last = randomList.size() - 1;
        for (int i = 0; i < last; i++) {
            int pos = random.nextInt(randomList.size() - i);
            if (pos != i) {
                Collections.swap(randomList, i, pos);
            }
        }

        for (String eurekaHost : randomList) {
            int ipCount = ipMap.get(eurekaHost) == null ? 0 : ipMap.get(eurekaHost);
            ipMap.put(eurekaHost, ipCount + 1);
            break;
        }
    }

    private static void printIpResult(TreeMap<String, Integer> ipMap, int totalCount) {
        for (Map.Entry<String, Integer> entry : ipMap.entrySet()) {
            Integer count = entry.getValue();
            BigDecimal rate = new BigDecimal(count).divide(new BigDecimal(totalCount), 2, BigDecimal.ROUND_HALF_UP);
            System.out.println(entry.getKey() + ":" + count + ":" + rate.multiply(new BigDecimal(100)).setScale(0, BigDecimal.ROUND_HALF_UP) + "%");
        }
    }
}

負載測試結果如下:

可以看到第二個機器會有50%的請求,最後一台機器只有17%的請求,負載的情況並不是很均勻,我認為通過IP負載並不是一個好的方案。

還記得我們之前講過Ribbon默認的輪詢算法RoundRobinRule,【一起學源碼-微服務】Ribbon 源碼四:進一步探究Ribbon的IRule和IPing 。

這種算法就是一個很好的散列算法,可以保證每次請求都很均勻,原理如下圖:

故障轉移原理

原理圖解

還是先上結論,如下圖:

我們的serverList按照client端的ip進行重排序后,每次都會請求第一個元素作為和Server端交互的host,如果請求失敗,會嘗試請求serverList列表中的第二個元素繼續請求,這次請求成功后,會將此次請求的host放到全局的一個變量中保存起來,下次client端再次請求 就會直接使用這個host

這裏最多會重試請求兩次。

代碼實現

直接看底層交互的代碼,位置在
com.netflix.discovery.shared.transport.decorator.RetryableEurekaHttpClient.execute() 中:

我們來分析下這個代碼:

  1. 第101行,獲取client上次成功server端的host,如果有值則直接使用這個host
  2. 第105行,getHostCandidates()是獲取client端配置的serverList數據,且通過ip進行重排序的列表
  3. 第114行,candidateHosts.get(endpointIdx++),初始endpointIdx=0,獲取列表中第1個元素作為host請求
  4. 第120行,獲取返回的response結果,如果返回的狀態碼是200,則將此次請求的host設置到全局的delegate變量中
  5. 第133行,執行到這裏說明第120行執行的response返回的狀態碼不是200,也就是執行失敗,將全局變量delegate中的數據清空
  6. 再次循環第一步,此時endpointIdx=1,獲取列表中的第二個元素作為host請求
  7. 依次執行,第100行的循環條件numberOfRetries=3,最多重試2次就會跳出循環

我們還可以第123和129行,這也正是我們業務拋出來的日誌信息,所有的一切都對應上了。

總結

感謝你看到這裏,相信你已經清楚了開頭提問的問題。

上面已經分析完了Eureka集群下Client端請求時負載均衡的選擇以及集群故障時自動重試請求的實現原理。

如果還有不懂的問題,可以添加我的微信或者給我公眾號留言,我會單獨和你討論交流。

本文首發自:一枝花算不算浪漫 公眾號,如若轉載請在文章開頭標明出處,如需開白可直接公眾號回復即可。

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

【其他文章推薦】

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

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

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

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

新北清潔公司,居家、辦公、裝潢細清專業服務

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

說說TCP的三次握手和四次揮手

一、傳輸控制協議TCP簡介

1.1 簡介

TCP(Transmission Control Protocol) 傳輸控制協議,是一種 面向連接的、可靠的、基於字節流的傳輸層 通信協議。

TCP是一種面向連接(連接導向)的、可靠的基於字節流的傳輸層通信協議。TCP將用戶數據打包成報文段,它發送后啟動一個定時器,另一端收到的數據進行確認、對失序的數據重新排序、丟棄重複數據。

TCP把連接作為最基本的對象,每一條TCP連接都有兩個端點,這種端點我們叫作套接字(socket),將端口號拼接到IP地址即構成了套接字,例如 192.1.1.6:50030

1.2 特點

  • 面向連接的、可靠的、基於字節流的 傳輸層 通信協議
  • 將應用層的數據流分割成文段併發送給目標節點的TCP層
  • 數據包都有序號,對方收到則發送ACK確認,未收到則重傳
  • 使用校驗和來檢驗數據在傳輸過程中是否有誤

二、TCP報文頭

1、源端口(Source Port)/ 目的端口(Destination Port):他們各佔2個字節,標示該段報文來自哪裡(源端口)以及要傳給哪個上層協議或應用程序(目的端口)。進行tcp通信時,一般client是通過系統自動選擇的臨時端口號,而服務器一般是使用知名服務端口號或者自己指定的端口號(比如DNS協議對應端口53,HTTP協議對應80)

2、序號(Sequence Number):佔據四個字節,TCP是面向字節流的,TCP連接中傳送的字節流中的每個字節都按順序編號,例如如一段報文的序號字段值是107,而攜帶的數據共有100個字段,如果有下一個報文過來,那麼序號就從207(100+107)開始,整個要傳送的字節流的起始序號必須要在連接建立時設置。首部中的序號字段值指的是本報文段所發送的數據的第一個字節的序號

3、確認序號(Acknowledgment Number):4個字節,是期望收到對方下一個報文段的第一個數據字節的序號,若確認號=N,則表明:到序號N-1為止的所有數據都已正確收到,例如:B收到A發送過來的報文,其序列號字段是301,而數據長度是200字節,這表明了B正確的收到了A到序號500(301+200-1)為止的數據,因此B希望收到A的下一個數據序號是501,於是B在發送給A的確認報文段中,會把ACK確認號設置為501

4、數據偏移(Offset):4個字節。指出TCP報文段的數據起始處距離報文段的起始處有多遠,這個字段實際上是指出TCP報文段的首部長度。由於首部中還有長度不確定的選項字段,因此數據偏移字段是必要的。單位是32位字,也就是4字節,4位二進制最大表示15,所以數據偏移也就是TCP首部最大60字節

5、保留(Reserved):6個字節。保留域

6、TCP Flags:控制位,由八個標誌位組成,每個標誌位表示控制的功能,我們主要來介紹TCP Flags中常用的六個,

  • URG(緊急指針標誌):當URG=1時,表明緊急指針字段有效。它告訴系統此報文段中有緊急數據,應儘快傳送(相當於高優先級的數據),而不要按原來的排隊順序來傳送。例如,已經發送了很長的一個程序在主機上運行。但後來發現了一些問題,需要取消該程序的運行。因此用戶從鍵盤發出中斷命令。如果不使用緊急數據,那麼這兩個字符將存儲在接收TCP的緩存末尾。只有在所有的數據被處理完畢后這兩個字符才被交付接收方的應用進程。這樣做就浪費了許多時間

  • ACK(確認序號標誌):當ACK=1時確認號字段有效。當ACK=0時,確認號無效。TCP規定,在連接建立后所有的傳送的報文段都必須把ACK置1

  • PSH(push標誌):當兩個應用進程進行交互式的通信時,有時在一端的應用進程希望在鍵入一個命令后立即就能收到對方的響應。在這種情況下,TCP就可以使用推送操作。這時,發送方TCP把PSH置1,並立即創建一個報文段發送出去。接收方TCP收到PSH=1的報文段,就儘快地交付接收應用進程,而不再等到整個緩存都填滿了後向上交付

  • RST(重置連接標誌):TCP連接中出現嚴重差錯(如由於主機崩潰或其他原因),必須釋放連接,然後再重新建立運輸連接,可以用來拒絕一個非法的報文段或拒絕打開一個連接

  • SYN(同步序號,用於建立連接過程):在連接建立時用來同步序號。當SYN=1而ACK=0時,表明這是一個連接請求報文段。對方若同意建立連接,則應在相應的報文段中使用SYN=1和ACK=1。因此,SYN置為1就表示這是一個連接請求或連接接受保溫。

  • FIN(finish標誌,用於釋放連接):當FIN=1時,表明此報文段的發送方的數據已發送完畢,並要求釋放運輸連接

7、窗口(Window)是TCP流量控制的一個手段。這裏說的窗口,指的是接收通告窗口(Receiver Window,RWND)。它告訴對方本端的TCP接收緩衝區還能容納多少字節的數據,這樣就可以控制發送數據的速度

8、檢驗和(Checksum):檢驗範圍包括首部和數據兩部分,由發送端填充,接收端對TCP報文段執行CRC算法以檢驗TCP報文段在傳輸過程中是否損壞。這也是TCP可靠傳輸的一個重要保障

9、緊急指針(Urgent Pointer):緊急指針僅在URG=1時才有意義,它指出本報文段中的緊急數據的字節數(緊急數據結束后就是普通數據)。因此,緊急指針指出了緊急數據的末尾在報文段中的位置。當所有緊急數據都處理完時,TCP就告訴應用程序恢復到正常操作。值得注意的是,即使窗口為零時也可發送緊急數據。

10、TCP可選項(TCP Options):長度可變,最長可達40字節。當沒有使用“選項”時,TCP的首部長度是20字節。

三、TCP的三次握手

所謂三次握手(Three-Way Handshake)即建立TCP連接,就是指建立一個TCP連接時,需要客戶端和服務端總共發送3個包以確認連接的建立。在socket編程中,這一過程由客戶端執行connect來觸發,整個流程如下圖所示:

在TCP/IP協議中,TCP協議提供可靠的連接服務,採用三次握手建立一個連接。

第一次握手: 建立連接時,客戶端發送SYN包(syn=j)到服務器,並進入SYN_SEND狀態,等待服務器確認,SYN:同步序列編號(Synchronize Sequence Numbers)。

第二次握手: 服務器收到 SYN 包,必須確認客戶的 SYN(ack=j+1),同時自己也發送一個SYN包(syn=k),即SYN+ACK包,此時服務器進入SYN_RECV狀態;

第三次握手: 客戶端收到服務器的SYN + ACK包,向服務器發送確認包ACK(ack=k+1),此包發送完畢,客戶端和服務器進入ESTABLISHED(TCP連接成功)狀態,完成三次握手。

3.1 為什麼需要三次握手才能建立連接

  • 為了初始化Sequence Number 的初始值,實現可靠數據傳輸, TCP 協議的通信雙方, 都必須維護一個序列號, 以標識發送出去的數據包中, 哪些是已經被對方收到的。 三次握手的過程即是通信雙方相互告知序列號起始值, 並確認對方已經收到了序列號起始值的必經步驟
  • 如果只是兩次握手, 至多只有連接發起方的起始序列號能被確認, 另一方選擇的序列號則得不到確認

3.2 首次握手的隱患——SYN超時

一、問題起因分析:
  1. 服務器收到客戶端的SYN,回復SYN和ACK的時候未收到ACK確認
  2. 服務器不斷重試直至超時,Linux默認等待63秒才斷開連接;(重複5次【不包括第一次】,從1秒開始,每次重試都翻倍:1+2+4+8+16+32=63秒)
二、針對SYN Flood的防護措施:
  1. SYN隊列滿后,通過tcp_syncookies參數會發SYN cookie【源端口+目標端口+時間戳組成】
  2. 若為正常連接則Client會回發SYN Cookie,直接建立連接;

3.3 保活機制:

當我們建立連接后,Client出現故障怎麼辦?

  1. 向對方發送保活探測報文,如果未收到響應則繼續發送;
  2. 嘗試次數達到保活探測數仍未收到相應則中斷連接;

四、TCP的四次揮手

所謂四次揮手(Four-Way Wavehand)即終止TCP連接,就是指斷開一個TCP連接時,需要客戶端和服務端總共發送4個包以確認連接的斷開。在socket編程中,這一過程由客戶端或服務端任一方執行close來觸發,整個流程如下圖所示:

由於TCP連接時全雙工的,因此,每個方向都必須要單獨進行關閉,這一原則是當一方完成數據發送任務后,發送一個FIN來終止這一方向的連接,收到一個FIN只是意味着這一方向上沒有數據流動了,即不會再收到數據了,但是在這個TCP連接上仍然能夠發送數據,直到這一方向也發送了FIN。首先進行關閉的一方將執行主動關閉,而另一方則執行被動關閉。

  • 第一次揮手: Client發送一個FIN,用來關閉Client到Server的數據傳送,Client進入FIN_WAIT_1狀態
  • 第二次揮手: Server收到FIN后,發送一個ACK給Client,確認序號為收到序號+1(與SYN相同,一個FIN佔用一個序號),Server進入CLOSE_WAIT狀態
  • 第三次揮手: Server發送一個FIN,用來關閉Server到Client的數據傳送,Server進入LAST_ACK狀態
  • 第四次揮手: Client收到FIN后,Client進入TIME_WAIT狀態,接着發送一個ACK給Server,確認序號為收到序號+1,Server進入CLOSED狀態,完成四次揮手
一、為什麼會有TIME_WAIT狀態

客戶端連接在收到服務器的結束報文段之後,不會直接進入CLOSED狀態,而是轉移到TIME_WAIT狀態。在這個狀態,客戶端連接要等待一段長為2MSL,即兩倍的報文段最大生存時間,才能完全關閉,其原因主要有兩點:

  • 確保有足夠的時間放對方收到ACK包
  • 避免新舊連接混淆
二、為什麼需要四次握手才能斷開連接

因為TCP連接是全雙工的網絡協議,允許同時通信的雙方同時進行數據的收發,同樣也允許收發兩個方向的連接被獨立關閉,以避免client數據發送完畢,向server發送FIN關閉連接,而server還有發送到client的數據沒有發送完畢的情況。所以關閉TCP連接需要進行四次握手,每次關閉一個方向上的連接需要FIN和ACK兩次握手,發送發和接收方都需要FIN報文和ACK報文

三、服務器出現大量CLOSE_WAIT狀態的原因

是由於對方關閉socket連接,我方忙於讀或寫,沒有及時關閉連接

當客戶端因為某種原因先於服務端發出了FIN信號,就會導致服務端被動關閉,若服務端不主動關閉socket發FIN給Client,此時服務端Socket會處於CLOSE_WAIT狀態(而不是LAST_ACK狀態)。通常來說,一個CLOSE_WAIT會維持至少2個小時的時間(系統默認超時時間的是7200秒,也就是2小時)。如果服務端程序因某個原因導致系統造成一堆CLOSE_WAIT消耗資源,那麼通常是等不到釋放那一刻,系統就已崩潰

解決:
1、檢查代碼,特別是釋放資源的代碼
2、檢查配置,特別是處理請求的線程配置

Linux的檢查代碼:netstat -n | awk '/^tcp/{++S[$NF]}END{for(a in S) print a,S[a]}'

五、總結

到這裏TCP的三次握手四次揮手就講完了,好久都沒有寫技術文章了,寫了一下,感覺還挺好的,上面是博主的認識,有寫的不好的地方,大家可以在評論區討論或者提問,博主看到了會第一時間回復大家,最近也準備開始面試了,先好好準備一下,希望今年可以找到心滿意足的工作,也希望今年面試的小夥伴們都有一個好的office,大家一起加油,我是牧小農,我喂自己帶鹽,大家加油。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

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

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

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

※幫你省時又省力,新北清潔一流服務好口碑

※回頭車貨運收費標準

Java多線程之volatile詳解

本文目錄

  • 從多線程交替打印A和B開始
  • Java 內存模型中的可見性、原子性和有序性
  • Volatile原理
    • volatile的特性
    • volatile happens-before規則
    • volatile 內存語義
    • volatile 內存語義的實現
  • CPU對於Volatile的支持
    • 緩存一致性協議
  • 工作內存(本地內存)並不存在
  • 總結
  • 參考資料

從多線程交替打印A和B開始

面試中經常會有一道多線程交替打印A和B的問題,可以通過使用Lock和一個共享變量來完成這一操作,代碼如下,其中使用num來決定當前線程是否打印

public class ABTread {

    private static int num=0;
    private static Lock lock=new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {

        Thread A=new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    lock.lock();
                    if (num==0){
                        System.out.println("A");
                        num=1;
                    }
                    lock.unlock();
                }
            }
        },"A");
        Thread B=new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    lock.lock();
                    if (num==1){
                        System.out.println("B");
                        num=0;
                    }
                    lock.unlock();
                }
            }
        },"B");
        A.start();
        B.start();
    }
}

這一過程使用了一個可重入鎖,在以前可重入鎖的獲取流程中有分析到,當鎖被一個線程持有時,後繼的線程想要再獲取鎖就需要進入同步隊列還有可能會被阻塞。
現在假設當A線程獲取了鎖,B線程再來獲取鎖且B線程獲取失敗則會調用LockSupport.park()導致線程B阻塞,線程A釋放鎖時再還行線程B
是否會經常存在阻塞線程和還行線程的操作呢,阻塞和喚醒的操作是比較費時間的。是否存在一個線程剛釋放鎖之後這一個線程又再一次獲取鎖,由於共享變量的存在,
則獲取鎖的線程一直在做着毫無意義的事情。

可以使用volatile關鍵字來修飾共享變量來解決,代碼如下:

public class ABTread {

    private static volatile  int num=0;
    public static void main(String[] args) throws InterruptedException {

        Thread A=new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    if (num==0){        //讀取num過程記作1
                        System.out.println("A");
                        num=1;          //寫入num記位2
                    }
                }
            }
        },"A");
        Thread B=new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    if (num==1){        //讀取num過程記作3
                        System.out.println("B");
                        num=0;          ////寫入num記位4
                    }
                }
            }
        },"B");
        A.start();
        B.start();
    }
}

Lock可以通過阻止同時訪問來完成對共享變量的同時訪問和修改,必要的時候阻塞其他嘗試獲取鎖的線程,那麼volatile關鍵字又是如何工作,
在這個例子中,是否效果會優於Lock呢。

Java 內存模型中的可見性、原子性和有序性

  • 可見性:指線程之間的可見性,一個線程對於狀態的修改對另一個線程是可見的,也就是說一個線程修改的結果對於其他線程是實時可見的。
    可見性是一個複雜的屬性,因為可見性中的錯誤總是會違背我們的直覺(JMM決定),通常情況下,我們無法保證執行讀操作的線程能實時的看到其他線程的寫入的值。
    為了保證線程的可見性必須使用同步機制。退一步說,最少應該保證當一個線程修改某個狀態時,而這個修改時程序員希望能被其他線程實時可見的,
    那麼應該保證這個狀態實時可見,而不需要保證所有狀態的可見。在 Javavolatilesynchronizedfinal 實現可見性。

  • 原子性:如果一個操作是不可以再被分割的,那麼我們說這個操作是一個原子操作,即具有原子性。但是例如i++實際上是i=i+1這個操作是可分割的,他不是一個原子操作。
    非原子操作在多線程的情況下會存在線程安全性問題,需要是我們使用同步技術將其變為一個原子操作。javaconcurrent包下提供了一些原子類,
    我們可以通過閱讀API來了解這些原子類的用法。比如:AtomicIntegerAtomicLongAtomicReference等。在 Javasynchronized 和在 lockunlock 中操作保證原子性

  • 有序性:一系列操作是按照規定的順序發生的。如果在本線程之內觀察,所有的操作都是有序的,如果在其他線程觀察,所有的操作都是無序的;前半句指“線程內表現為串行語義”後半句指“指令重排序”和“工作內存和主存同步延遲”
    Java 語言提供了 volatilesynchronized 兩個關鍵字來保證線程之間操作的有序性。volatile 是因為其本身包含“禁止指令重排序”的語義,
    synchronized 是由“一個變量在同一個時刻只允許一條線程對其進行 lock 操作”這條規則獲得的,此規則決定了持有同一個對象鎖的兩個同步塊只能串行執行。

Volatile原理

volatile定義:Java編程語言允許線程訪問共享變量,為了確保共享變量能被準確和一致的更新,線程應該通過獲取排他鎖單獨獲取這個變量;
java提供了volatile關鍵字在某些情況下比鎖更好用。

  • Java語言提供了volatile了關鍵字來提供一種稍弱的同步機制,他能保證操作的可見性和有序性。當把變量聲明為volatile類型后,
    編譯器與運行時都會注意到這個變量是一個共享變量,並且這個變量的操作禁止與其他的變量的操作重排序。

  • 訪問volatile變量時不會執行加鎖操作。因此也不會存在阻塞競爭的線程,因此volatile變量是一種比sychronized關鍵字更輕量級的同步機制。

volatile的特性

volatile具有以下特性:

  • 可見性:對於一個volatile的讀總能看到最後一次對於這個volatile變量的寫
  • 原子性:對任意單個volatile變量的讀/寫具有原子性,但對於類似於i++這種複合操作不具有原子性。
  • 有序性:

volatile happens-before規則

根據JMM要求,共享變量存儲在共享內存當中,工作內存存儲一個共享變量的副本,
線程對於共享變量的修改其實是對於工作內存中變量的修改,如下圖所示:

從多線程交替打印A和B開始章節中使用volatile關鍵字的實現為例來研究volatile關鍵字實現了什麼:
假設線程A在執行num=1之後B線程讀取num指,則存在以下happens-before關係

1)  1 happens-before 2,3 happens-before 4
2)  根據volatile規則有:2 happens-before 3
3)  根據heppens-before傳遞規則有: 1 happens-before 4

至此線程的執行順序是符合我們的期望的,那麼volatile是如何保證一個線程對於共享變量的修改對於其他線程可見的呢?

volatile 內存語義

根據JMM要求,對於一個變量的獨寫存在8個原子操作。對於一個共享變量的獨寫過程如下圖所示:

對於一個沒有進行同步的共享變量,對其的使用過程分為readloaduseassign以及不確定的storewrite過程。
整個過程的語言描述如下:

- 第一步:從共享內存中讀取變量放入工作內存中(`read`、`load`)
- 第二步:當執行引擎需要使用這個共享變量時從本地內存中加載至**CPU**中(`use`)
- 第三步:值被更改后使用(`assign`)寫回工作內存。
- 第四步:若之後執行引擎還需要這個值,那麼就會直接從工作內存中讀取這個值,不會再去共享內存讀取,除非工作內存中的值出於某些原因丟失。
- 第五步:在不確定的某個時間使用`store`、`write`將工作內存中的值回寫至共享內存。

由於沒有使用鎖操作,兩個線程可能同時讀取或者向共享內存中寫入同一個變量。或者在一個線程使用這個變量的過程中另一個線程讀取或者寫入變量。
上圖中1和6兩個操作可能會同時執行,或者在線程1使用num過程中6過程執行,那麼就會有很嚴重的線程安全問題,
一個線程可能會讀取到一個並不是我們期望的值。

那麼如果希望一個線程的修改對後續線程的讀立刻可見,那麼只需要將修改后存儲在本地內存中的值回寫到共享內存
並且在另一個線程讀的時候從共享內存重新讀取而不是從本地內存中直接讀取即可;事實上
當寫一個volatile變量時,JMM會把該線程對應的本地內存中共享變量值刷新會共享內存;
而當讀取一個volatile變量時,JMM會從主存中讀取共享變量
,這也就是volatile的寫-讀內存語義。

volatile的寫-讀內存語義:

  • volatile寫的內存語義:當寫一個volatile變量時,JMM會把該線程對應的本地內存中共享變量值刷新會共享內存
  • volatile讀的內存語義:當讀一個volatile變量時,JMM會把該線程對應的本地內存置為無效,線程接下來將從主內存中讀取共享變量。

如果將這兩個步驟綜合起來,那麼線程3讀取一個volatile變量后,寫線程1在寫這個volatile變量之前所有可見的共享變量的值都將樂客變得對線程3可見。

volatile變量的讀寫過程如下圖:

需要注意的是:在各個線程的工作內存中是存在volatile變量的值不一致的情況的,只是每次使用都會從共享內存讀取並刷新,執行引擎看不到不一致的情況,
所以認為volatile變量在本地內存中不存在不一致問題。

volatile 內存語義的實現

在前文Java內存模型中有提到重排序。為了實現volatile的內存語義,JMM會限制重排序的行為,具體限制如下錶:

是否可以重排序 第二個操作 第二個操作 第二個操作
第一個操作 普通讀/寫 volatile volatile
普通讀/寫 NO
volatile NO NO NO
volatile NO NO

說明:

- 若第一個操作時普通變量的讀寫,第二個操作時volatile變量的寫操作,則編譯器不能重排序這兩個操作
- 若第一個操作是volatile變量的讀操作,不論第二個變量是什麼操作不餓能重排序這兩個操作
- 若第一個操作時volatile變量的寫操作,除非第二個操作是普通變量的獨寫,否則不能重排序這兩個操作

為了實現volatile變量的內存語義,編譯器生成字節碼文件時會在指令序列中插入內存屏障來禁止特定類型的處理器排序。
為了實現volatile變量的內存語義,插入了以下內存屏障,並且在實際執行過程中,只要不改變volatile的內存語義,
編譯器可以根據實際情況省略部分不必要的內存屏障

- 在每個volatile寫操作前面插入StoreStore屏障
- 在每個volatile寫操作後面插入StoreLoad屏障
- 在每個volatile讀操作後面插入LoadLoad屏障
- 在每個volatile讀操作後面插入LoadStore屏障

插入內存屏障后volatile寫操作過程如下圖:

插入內存屏障后volatile讀操作過程如下圖:

至此在共享內存和工作內存中的volatile的寫-讀的工作過程全部完成

但是現在的CPU中存在一個緩存,CPU讀取或者修改數據的時候是從緩存中獲取並修改數據,那麼如何保證CPU緩存中的數據與共享內存中的一致,並且修改后寫回共享內存呢?

CPU對於Volatile的支持

緩存行:cpu緩存存儲數據的基本單位,cpu不能使數據失效,但是可以使緩存行失效。

對於CPU來說,CPU直接操作的內存時高速緩存,而每一個CPU都有自己L1、L2以及共享的L3級緩存,如下圖:

那麼當CPU修改自身緩存中的被volatile修飾的共享變量時,如何保證對其他CPU的可見性。

緩存一致性協議

在多處理器的情況下,每個處理器總是嗅探總線上傳播的數據來檢查自己的緩存是否過期,當處理器發現自己對應的緩存對應的地址被修改,
就會將當前處理器的緩存行設置為無效狀態,當處理器對這個數據進行操作的時候,會重新從系統中把數據督導處理器的緩存里。這個協議被稱之為緩存一致性協議。

緩存一致性協議的實現又MEIMESIMOSI等等。

MESI協議緩存狀態

狀態 描述
M(modified)修改 該緩存指被緩存在該CPU的緩存中並且是被修改過的,即與主存中的數據不一致,該緩存行中的數據需要在未來的某個時間點寫回主存,當寫回註冊年之後,該緩存行的狀態會變成E(獨享)
E(exclusive)獨享 該緩存行只被緩存在該CPU的緩存中,他是未被修改過的,與主存中數據一致,該狀態可以在任何時候,當其他的CPU讀取該內存時編程共享狀態,同樣的,當CPU修改該緩存行中的內容時,該狀態可以變為M(修改)
S(share)共享 該狀態意味着該緩存行可能被多個CPU緩存,並且各個緩存中的數據與主存中的數據一致,當有一個CPU修改自身對應的緩存的數據,其它CPU中該數據對應的緩存行被作廢
I(Invalid)無效 該緩存行無效

MESI協議可以防止緩存不一致的情況,但是當一個CPU修改了緩存中的數據,但是沒有寫入主存,也會存在問題,那麼如何保證CPU修改共享被volatile修飾的共享變量后立刻寫回主存呢。

在有volatile修飾的共享變量進行寫操作的時候會多出一條帶有lock前綴的彙編代碼,而這個lock操作會做兩件事:

  1. 將當前處理器的緩存行的數據協會到系統內存。lock信號確保聲言該信號期間CPU可以獨佔共享內存。在之前通過鎖總線的方式,現在採用鎖緩存的方式。
  2. 這個寫回操作會使其他處理器的緩存中緩存了該地址的緩存行無效。在下一次這些CPU需要使用這些地址的值時,強制要求去共享內存中讀取。

如果對聲明了volatile的共享變量進行寫,JVM會向CPU發送一條lock指令,使得將這個變量所在的緩存行緩存的數據寫回到內存中。而其他CPU通過嗅探總線上傳播的數據,
使得自身緩存行失效,下一次使用時會從主存中獲取對應的變量。

工作內存(本地內存)並不存在

根據JAVA內存模型描述,各個線程使用自身的工作內存來保存共享變量,那麼是不是每個CPU緩存的數據就是從工作內存中獲取的。這樣的話,在CPU緩存寫回主存時,
協會的是自己的工作內存地址,而各個線程的工作內存地址並不一樣。CPU嗅探總線時就嗅探不到自身的緩存中緩存有對應的共享變量,從而導致錯誤?

事實上,工作內存並不真實存在,只是JMM為了便於理解抽象出來的概念,它涵蓋了緩存,寫緩衝區、寄存器及其他的硬件編譯器優化。所以緩存是直接和共享內存交互的。
每個CPU緩存的共享數據的地址是一致的。

總結

  • volatile提供了一種輕量級同步機制來完成同步,它可以保操作的可見性、有序性以及對於單個volatile變量的讀/寫具有原子性,對於符合操作等非原子操作不具有原子性。

  • volatile通過添加內存屏障及緩存一致性協議來完成對可見性的保證。

最後Lock#lock()是如何保證可見性的呢??

Lock#lock()使用了AQSstate來標識鎖狀態,而statevolatile標記的,由於對於volatile的獨寫操作時添加了內存屏障的,所以在修改鎖狀態之前,
一定會將之前的修改寫回共享內存。

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

【其他文章推薦】

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

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

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

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

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

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

mysql定時備份任務

簡介

在生產環境上,為了避免數據的丟失,通常情況下都會定時的對數據庫進行備份。而Linux的crontab指令則可以幫助我們實現對數據庫定時進行備份。首先我們來簡單了解crontab指令,如果你會了請跳到下一個內容mysql備份
本文章的mysql數據庫是安裝在docker容器當中,以此為例進行講解。沒有安裝到docker容器當中也可以參照參照。

contab定時任務

使用crontab -e來編寫我們的定時任務。

0 5 * * 1 [command]

前面的5個数字分別代表分、時、日、月、周,後面的 command為你的執行命令。
假如你需要在每天晚上8點整執行定時任務,那麼可以這麼寫

0 8 * * * [command]

擴展:
crontab -l 可以查看自己的定時任務
crontab -r 刪除當前用戶的所有定時任務

mysql備份

快速上手

這裏我的mysql數據庫是docker容器。假如你需要在每天晚上8點整執行定時任務,那麼可以這麼寫。
首先執行命令crontab -e

0 8 * * * docker exec mysql_container mysqldump -uroot -proot_password database_name > /var/backups/mysql/$(date +%Y%m%d_%H%M%S).sql

mysql_container 為你的數據庫容器名
mysqldump 是mysql數據庫導出數據的指令
-u 填寫root賬號
-p 填寫root密碼
database_name 需要備份的數據庫名
/var/backups/mysql/$(date +%Y%m%d_%H%M%S).sql 備份文件,後面是文件名的格式

如果你沒什麼要求,單純的只是想要備份,那麼上面那個命令就可以幫你進行定時備份。

小坑: mysql備份的時候我使用了docker exec -it mysqldump ... 這樣的命令去做bash腳本,因為-i參數是有互動的意思,導致在crontab中執行定時任務的時候,沒有輸出數據到sql文件當中。所以使用crontab定時的對docker容器進行備份命令的時候不要添加-i參數。

crontab優化

我不建議直接在crontab -e裏面寫要執行的命令,任務多了就把這個文件寫的亂七八招了。
建議把數據庫備份的命令寫成一個bash腳本。在crontab這裏調用就好了
如:建立一個/var/backups/mysql/mysqldump.sh文件,內容如下

docker exec mysql_container mysqldump -uroot -pmypassword database_name > /var/backups/mysql/$(date +%Y%m%d_%H%M%S).sql

然後把文件改為當前用戶可執行的:

chmod 711 /var/backups/mysql/mysqldump.sh

執行crontab -e 命令修改成如下:

0 20 * * * /var/backups/mysql/mysqldump.sh

那麼這樣就比較規範了。

mysql備份優化

因為sql文件比較大,所以一般情況下都會對sql文件進行壓縮,不然的話磁盤佔用就太大了。
假設你做了上面這一步 crontab優化,我們可以把mysqldump.sh腳本改成下面這樣:

export mysqldump_date=$(date +%Y%m%d_%H%M%S) && \
docker exec mysql_container mysqldump -uroot -pmypassword database_name> /var/backups/mysql/$mysqldump_date.sql && \
gzip /var/backups/mysql/$mysqldump_date.sql
find /var/backups/mysql/ -name "*.sql" -mtime +15 -exec rm -f {} \;

export 在系統中自定義了個變量mysqldump_date,給備份和壓縮命令使用
gzip 為壓縮命令,默認壓縮了之後會把源文件刪除,壓縮成.gz文件
find ... 這行命令的意思為,查詢 /var/backups/mysql/目錄下,創建時間15天之前(-mtime +15),文件名後綴為.sql的所有文件 執行刪除命令-exec rm -f {} \;。總的意思就是:mysql的備份文件只保留15天之內的。15天之前的都刪除掉。

數據恢復

若一不小心你執行drop database,穩住,淡定。我們首先要創建數據庫被刪除的數據庫。

>mysql create database database_name;

然後恢復最近備份的數據。恢復備份的命令:

docker exec -i mysql_container mysql -uroot -proot_password database_name < /var/backups/mysql/20200619_120012.sql

雖然恢復了備份文件的數據,但是備份時間點之後的數據我們卻沒有恢復回來。
如:晚上8點進行定時備份,但是卻在晚上9點drop database,那麼晚上8點到晚上9點這一個小時之內的數據卻沒有備份到。這時候就要使用binlog日誌了。

binlog日誌

binlog 是mysql的一個歸檔日誌,記錄的數據修改的邏輯,如:給 ID = 3 的這一行的 money 字段 + 1。
首先登錄mysql后查詢當前有多少個binlog文件:

> mysql show binary logs;
+---------------+-----------+-----------+
| Log_name      | File_size | Encrypted |
+---------------+-----------+-----------+
| binlog.000001 |       729 | No        |
| binlog.000002 |      1749 | No        |
| binlog.000003 |      1087 | No        |
+---------------+-----------+-----------+

查看當前正在寫入的binlog

mysql> show master status\G;

生成新的binlog文件,mysql的後續操作都會寫入到新的binlog文件當中,一般在恢複數據都時候都會先執行這個命令。

mysql> flush logs

查看binlog日誌

mysql> show binlog events in 'binlog.000003';

小知識點:初始化mysql容器時,添加參數--binlog-rows-query-log-events=ON。或者到容器當中修改/etc/mysql/my.cnf文件,添加參數binlog_rows_query_log_events=ON,然後重啟mysql容器。這樣可以把原始的SQL添加到binlog文件當中。

恢複數據

拿回上面例子的這段話。

晚上8點進行定時備份,但是卻在晚上9點drop database,那麼晚上8點到晚上9點這一個小時之內的數據卻沒有備份到。。

首先進入到mysql容器后,切換到/var/lib/mysql目錄下,查看binlog文件的創建日期

cd /var/lib/mysql
ls -l
...
-rw-r----- 1 mysql mysql      729 Jun 19 15:54  binlog.000001
-rw-r----- 1 mysql mysql     1749 Jun 19 18:45  binlog.000002
-rw-r----- 1 mysql mysql     1087 Jun 19 20:58  binlog.000003
...

從文件日期可以看出:當天時間為2020-06-21,binlog.000002文件的最後更新時間是 18:45 分,那麼晚上8點的備份肯定包含了binlog.000002的數據;
binlog.000003的最後更新日期為 20:58 分,那麼我們需要恢復的數據 = 晚上8點的全量備份 + binlog.000003的 20:00 – 執行drop database命令時間前的數據。

恢復命令格式:

mysqlbinlog [options] file | mysql -uroot -proot_password database_name

mysqlbinlog常用參數:

–start-datetime 開始時間,格式 2020-06-19 18:00:00
–stop-datetime 結束時間,格式同上
–start-positon 開始位置,(需要查看binlog文件)
–stop-position 結束位置,同上

恢復備份數據和binlog數據前建議先登錄mysql后執行flush logs生成新的binlog日誌,這樣可以專註需要恢複數據的binlog文件。
首先我們需要查看binlog日誌,在哪個位置進行了drop database操作:

mysql> show binlog events in 'binlog.000003';
+---------------+-----+----------------+-----------+-------------+---------------------------------------------------------------------------------------------------------------------------------------------+
| Log_name      | Pos | Event_type     | Server_id | End_log_pos | Info                                                                                                                                        |
+---------------+-----+----------------+-----------+-------------+---------------------------------------------------------------------------------------------------------------------------------------------+
| binlog.000003 |   4 | Format_desc    |         1 |         125 | Server ver: 8.0.20, Binlog ver: 4                                                                                                           |
| binlog.000003 | 125 | Previous_gtids |         1 |         156 |                                                                                                                                             |
| binlog.000003 | 156 | Anonymous_Gtid |         1 |         235 | SET @@SESSION.GTID_NEXT= 'ANONYMOUS'                                                                                                        |
| binlog.000003 | 235 | Query          |         1 |         318 | BEGIN                                                                                                                                       |
| binlog.000003 | 318 | Rows_query     |         1 |         479 | # INSERT INTO `product_category` SET `name` = '床上用品' , `create_time` = 1592707634 , `update_time` = 1592707634 , `lock_version` = 0      |
| binlog.000003 | 479 | Table_map      |         1 |         559 | table_id: 139 (hotel_server.product_category)                                                                                               |
| binlog.000003 | 559 | Write_rows     |         1 |         629 | table_id: 139 flags: STMT_END_F                                                                                                             |
| binlog.000003 | 629 | Xid            |         1 |         660 | COMMIT /* xid=2021 */                                                                                                                       |
| binlog.000004 | 660 | Anonymous_Gtid |         1 |         739 | SET @@SESSION.GTID_NEXT= 'ANONYMOUS'                                                                                                        |
| binlog.000004 | 739 | Query          |         1 |         822 | drop database hotel_server /* xid=26 */                                                                                                     |
+---------------+-----+----------------+-----------+-------------+---------------------------------------------------------------------------------------------------------------------------------------------+

根據上面的日誌,我們可以看到,在End_log_pos = 822 的位置執行了drop database操作,那麼使用binlog恢復的範圍就在2020-06-19 20:00:00 – 660 的位置。為什麼是660?因為drop database的上一個事務的提交是660的位置,命令如下:

mysqlbinlog --start-datetime=2020-06-19 20:00:00 --stop-position=660 /var/lib/mysql/binlog.000003 | mysql -uroot -proot_password datbase_name

如果你的範圍包括了822的位置,那麼就會幫你執行drop database命令了。不信你試試?
執行完上面的命令,你的數據就會恢復到drop database前啦!開不開心,激不激動!

總結

因為mysql定時備份是在生產環境上必須的任務。是很常用的。所以我就迫不及待的寫博客。當然也很感謝我同事的幫助。這篇文章已經寫了三天了,因為我也是在不斷地試錯,不斷的更新文章。避免把錯誤的知識點寫出來。如果幫到你了,關注我一波唄!謝謝。

個人博客網址: https://colablog.cn/

如果我的文章幫助到您,可以關注我的微信公眾號,第一時間分享文章給您

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

【其他文章推薦】

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

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

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

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

新北清潔公司,居家、辦公、裝潢細清專業服務

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

快速打造屬於你的接口自動化測試框架

1 接口測試

接口測試是對系統或組件之間的接口進行測試,主要是校驗數據的交換,傳遞和控制管理過程,以及相互邏輯依賴關係。
接口自動化相對於UI自動化來說,屬於更底層的測試,這樣帶來的好處就是測試收益更大,且維護成本相對來說較低,是我們進行自動化測試的首選

2 框架選型

目前接口自動化的框架比較多,比如jmeter,就可以集接口自動化和性能測試於一體,該工具編寫用例效率不高;還有我們常用的postman,結合newman也可以實現接口自動化;Python+unittest+requests+HTMLTestRunner 是目前比較主流的測試框架,對python有一定的編碼要求;
本期我們選擇robotframework(文中後續統一簡稱為RF)這一個比較老牌的測試框架進行介紹,RF是一個完全基於 關鍵字 測試驅動的框架,它即能夠基於它的一定規則,導入你需要的測試庫(例如:其集成了selenium的測試庫,即可以理解為操作控件的測試底層庫),然後基於這些測試庫,你能應用TXT形式編寫自己的關鍵字(支持python和java語言,這些關鍵字即你的庫組成),之後,再編寫(測試用例由測試關鍵字組成)進行測試;他支持移動端、UI自動化和接口自動化的測試

3 環境搭建

  • python的安裝:目前選取的python3以上的版本,RF的運行依賴python
  • robotframework:參考https://www.jianshu.com/p/9dcb4242b8f2
  • jenkins:用於調度RF的用例執行環境
  • gitlab:代碼倉庫

4 需求

4.1 需求內容
接口內容:實現一個下單,並檢查訂單狀態是否正常的場景;該需求涉及到如下三個接口

  • 下單接口
  • 訂單結果查詢接口
  • 下單必須帶上認證標識,生成token的接口

環境覆蓋:需要支持能在多套環境運行,比如測試和預發布環境
系統集成:需要能夠集成在CICD中,實現版本更新后的自動檢測

4.2 用例設計
4.2.1 用例設計,根據業務場景設計測試用例,方便後續實現

4.2.2 測試數據構造,預置不同環境的測試數據,供實現調用

5 整體實現架構

接口測試實現層:在RF,通過引用默認關鍵字 RequestsLibrary (實現http請求)和通過python自定義關鍵字來完成用例實現的需求;
jenkins調度:在jenkins上配置一個job,設置好RF用例執行的服務器和發送給服務器相關的RF執行的指令,並且在jenkins中配置好測試報告模板,這樣用例便可以通過jenkins完成執行併發送測試結果給項目干係人;
生成用例執行的API:上圖中藍色部分,就是為了將jenkins的job生成一個可訪問api接口,方便被測項目的CICD集成;
集成到被測系統CICD流程:將上面步驟中封裝的API配置在被測應用的__gitlab-ci.yml__中,完成整個接口自動化的閉環

6 RF用例實現

6.1 引用的內置關鍵字

  • RequestsLibrary 構造http的請求,get|post等請求
getRequests
# get請求的入參
    [Arguments]    ${url_domain}    ${getbody}    ${geturl}    ${getToken}
    Create session    postmain    ${url_domain}
# 定義header的內容
    ${head}    createdictionary    content-type=application/json    Authorization=${getToken}    MerchantId=${s_merchant_id}
# get請求
    ${addr}    getRequest    postmain    ${geturl}    params=${getbody}    headers=${head}
# 請求狀態碼斷言
    Should Be Equal As Strings    ${addr.status_code}    200
    ${response_get_data}    To Json    ${addr.content}
# 返回http_get請求結果
    Set Test Variable    ${response_get_data}	 
    Delete All Sessions

6.2 自定義關鍵字

  • getEnvDomain 用於從自定義的configs.ini文件獲取對應環境的微服務的請求域名
    configs.ini的內容
# 獲取configs.ini的內容
import configparser
def getEnv(path,env):
    config = configparser.ConfigParser()
    config.read(path)
    passport = config[env]['passport']
    stock=config[env]['stock']
    finance=config[env]['finance']
    SUP = config[env]['SUP']
    publicApi = config[env]['publicApi']
    publicOrder = config[env]['publicOrder']
    data_dict={'passport':passport,'stock':stock,'finance':finance,'SUP':SUP,'publicApi':publicApi,'publicOrder':publicOrder}
    return data_dict
  • excelTodict 用戶將excel中的內容作為字典返回
import xlrd

'''
通用獲取excel數據
@:param path excel文件路徑
@:param sheet_name excel文件裏面sheet的名稱 如:Sheet1
@:env 環境,是IT還是PRE
'''
def getExcelDate(path, sheet_name,env):
    bk = xlrd.open_workbook(path)
    sh = bk.sheet_by_name(sheet_name)
    row_num = sh.nrows
    data_list = []
    for i in range(1, row_num):
        row_data = sh.row_values(i)
        data = {}
        for index, key in enumerate(sh.row_values(0)):
            data[key] = row_data[index]
        data_list.append(data)
    data_list1 = []
    for x in data_list:
        #print('這是'+str(x))
        if(x.get('env')==env):
            data_list1.append(x)
    return data_list1
  • getToken 提供接口下單的授權token
*** Keywords ***
# 根據傳入的clientid、secret生成對應的token
getToken
    [Arguments]    ${client_id}    ${client_secret}    ${url_domain}
    Create session    postmain    ${url_domain}
    ${auth}    createdictionary    grant_type=client_credentials    client_id=${client_id}    client_secret=${client_secret}
    ${header}    createdictionary    content-type=application/x-www-form-urlencoded
    ${addr}    postRequest    postmain    /oauth/token    data=${auth}    headers=${header}
    Should Be Equal As Strings    ${addr.status_code}    200
    ${responsedata}    To Json    ${addr.content}
    ${access}    Get From Dictionary    ${responsedata}    access_token
    ${token}    set variable    bearer ${access}
    Set Test Variable    ${token}
    Delete All Sessions
  • getAllDate 獲取該用例下的所有數據
getAllData
    [Arguments]    ${row_no}
    getEnvDomain
    getBalance    ${row_no}
    getStockNum    ${row_no}
    getSupProPrice    ${row_no}
    getProPrice    ${row_no}
    Set Test Variable    ${publicOrderUrl}
    Set Test Variable    ${FPbalance}
    Set Test Variable    ${Pbalance}
    Set Test Variable    ${Sbalance}
    Set Test Variable    ${Jbalance}
    Set Test Variable    ${Cardnum}
    Set Test Variable    ${sprice}
    Set Test Variable    ${price}
    Set Test Variable    ${j_merchant_id}
    Set Test Variable    ${s_merchant_id}
    Set Test Variable    ${stock_id}
    Set Test Variable    ${p_product_id}
    Set Test Variable    ${s_product_id}

  • 實現demo
*** Settings ***
Test Template
Resource          引用所有資源.txt

*** Test Cases ***
*** Settings ***
Test Template
Resource          引用所有資源.txt

*** Test Cases ***
01 下單卡密直儲商品
    [Tags]    order
    LOG    ---------------------獲取下單前的數量、餘額------------------------------------------
    getAllData    0
    ${Cardnum1}    set variable    ${Cardnum}
    ${FPbalance1}    set variable    ${FPbalance}
    ${Pbalance1}    set variable    ${Pbalance}
    ${Sbalance1}    set variable    ${Sbalance}
    ${Jbalance1}    set variable    ${Jbalance}
    ${CustomerOrderNo1}    Evaluate    random.randint(1000000, 9999999)    random
    ${Time}    Get Time
    log    ------------------------下單操作-------------------------------------------------------
    getToken    100xxxx    295dab07a9xxxx9780be0eb95xxxx   ${casUrl}
    ${input_cs}    create dictionary    memberId=${j_merchant_id}    clientId=1xxx079    userId=string    shopType=string    customerOrderNo=${CustomerOrderNo1}
    ...    productId=${p_product_id}    buyNum=1    chargeAccount=otest888888    notifyUrl=string    chargeIp=string    chargePassword=string
    ...    chargeGameName=string    chargeGameRole=string    chargeGameRegion=string    chargeGameSrv=string    chargeType=string    remainingNumber=0
    ...    contactTel=string    contactQQ=string    customerPrice=0    poundage=0    batchNumber=    originalOrderId=string
    ...    shopName=string    appointSupProductId=0    stemFromSubOrderId=123456    externalBizId=456789
    postRequests    ${publicOrderUrl}    ${input_cs}    /api/Order    ${token}
    ${data}    get from dictionary    ${responsedata}    data
    ${orderid}    get from dictionary    ${data}    id
    sleep    6
    ${getdata}    create dictionary    Id=${orderid}    PageIndex=1    PageSize=1
    getRequests    ${publicOrderUrl}    ${getdata}    /api/Order/GetList    ${token}
    ${datalist}    get from dictionary    ${response_get_data}    data
    ${data}    get from dictionary    ${datalist}    list
    ${dict}    set variable    ${data}[0]
    ${orderOuterStatus}    get from dictionary    ${dict}    orderOuterStatus
    LOG    ---------------------獲取下單后的數量、餘額----------------------------------------------
    getAllData    0
    ${Cardnum2}    set variable    ${Cardnum}
    ${FPbalance2}    set variable    ${FPbalance}
    ${Pbalance2}    set variable    ${Pbalance}
    ${Sbalance2}    set variable    ${Sbalance}
    ${Jbalance2}    set variable    ${Jbalance}
    ${sprice}    set variable    ${sprice}
    ${price}    set variable    ${price}
    log    ------------------斷言-----------------------------------------------------------------
    ${Cardnum3}    Evaluate    ${Cardnum1}
    ${Jbalance3}    Evaluate    ${Jbalance1}
    ${Sbalance3}    Evaluate    ${Sbalance1}
    ${Pbalance3}    Evaluate    ${Pbalance1}
    should be true    ${orderOuterStatus}==90
    should be true    ${Cardnum3}==${Cardnum2}
    should be true    ${Jbalance3}==${Jbalance2}
    should be true    ${Sbalance3}==${Sbalance2}
    should be true    ${Pbalance3}==${Pbalance2}

7 集成到CICD流程

7.1 jenkins配置job
通過jenkins的參數化構建,定義it和pre兩套環境

jenkins發送RF執行的命令

7.2 封裝的jenkins_job的執行接口地址
通過python的flask框架,根據測試和pre兩套環境包一層jenkins的job執行接口

__author__ = 'paul'

# !/usr/bin/env python
# -*- coding: utf-8 -*-
from flask import Flask, abort, request, jsonify
import jenkins

server = jenkins.Jenkins('http://10.0.1.xxx:80', username='xxx', password='fuluxxxx')

app = Flask(__name__)

tasks = []

# it的測試集合http請求接口
@app.route('/test/it', methods=['get'])
def robot_Test_It():
    server.build_job('CI_FuluOrder', {'environment': 'IT'})
    return jsonify({'result': 'success'})

# pre的測試集合http請求接口
@app.route('/test/pre', methods=['get'])
def robot_Test_Pre():
    server.build_job('CI_FuluOrder', {'environment': 'PRE'})
    return jsonify({'result': 'success'})

if __name__ == "__main__":
    # 將host設置為0.0.0.0,則外網用戶也可以訪問到這個服務
    app.run(host="0.0.0.0", port=80, debug=True)

7.3 將上述flask封裝的接口打包成鏡像
根據dockerfile生成鏡像

FROM python:3.6
WORKDIR /app
EXPOSE 80
COPY .	.
RUN pip install -r requirements.txt 
ENTRYPOINT ["python","robotTestApi.py"]

7.4 將鏡像部署到kubernetes,對外提供服務
供觸發測試執行的調用入口 ,這部分封裝的接口部署在本地的k8s集群下ordermiddle

IT: http://ordermiddle.xxx.cn/test/it
pre:http://ordermiddle.xxx.cn/test/pre

7.5 被測項目的CICD集成接口自動化測試
gitlab目前採取直接對CICD腳本加入測試步驟,在部署到容器30秒后(考慮到容器在K8S啟動時間)調用測試接口

7.6 發送測試報告

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

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

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

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

※幫你省時又省力,新北清潔一流服務好口碑

※回頭車貨運收費標準

聯合國示警:氣候變遷加劇阿拉伯地區衝突局勢

環境資訊中心綜合外電;姜唯 編譯;林大利 審校

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

【其他文章推薦】

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

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

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

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

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

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

美國迪士尼致力環保 將停用一次用塑膠吸管

摘錄自2018年7月27日蘋果日報美國報導

美國娛樂巨頭華特迪士尼公司( Walt Disney Company)昨天(26日)宣示,明年中期以前,迪士尼樂園等將停止使用一次性塑膠吸管。鑑於塑料垃圾造成的海洋污染日益嚴重,為保護地球環境,歐美正在推廣相同的措施。

迪士尼指出,此舉將可每年減少1.75億根以上的吸管、1.3億根攪拌棒,強調本次嘗試是迪士尼履行環保責任的一環。

共同社則報導,據與迪士尼方面簽訂許可協議的東方樂園公司稱,由於位於千葉縣浦安市的東京迪士尼度假區,運營母體不同,因此不受此次華特迪士尼公司決定的影響。然而該公司也指,「正在研究減少塑料廢棄物」。

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

【其他文章推薦】

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

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

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

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

新北清潔公司,居家、辦公、裝潢細清專業服務

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

葡西法三國簽署能源互聯協議 法國將關閉所有煤電廠

摘錄自2018年7月28日新華社報導

葡萄牙、西班牙和法國27日在葡萄牙首都里斯本舉行的第二屆能源互聯峰會上正式簽署了三國能源互聯協議。

根據協議,西葡兩國同歐洲的能源互聯水平到2020年達到10%,2030年達到15%。此外,歐盟委員會將投資5.7億歐元在西班牙以北的比斯開灣建造一個用於連接西班牙、葡萄牙和法國的電力互聯項目。

葡萄牙總理科斯塔、西班牙首相桑切斯和法國總統馬克宏在會後舉行了聯合記者會。馬克宏表示,最晚到2022年,法國將關閉所有煤電廠。

科斯塔說,葡萄牙計劃到2020年使清潔能源占比超過60%,葡萄牙在逐步減少煤電行業投入的同時尋求清潔能源出口。

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

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

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

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

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

※幫你省時又省力,新北清潔一流服務好口碑

※回頭車貨運收費標準