Alink漫談(五) : 迭代計算和Superstep_台中搬家公司

※推薦台中搬家公司優質服務,可到府估價

台中搬鋼琴,台中金庫搬運,中部廢棄物處理,南投縣搬家公司,好幫手搬家,西屯區搬家

Alink漫談(五) : 迭代計算和Superstep

目錄

  • Alink漫談(五) : 迭代計算和Superstep
    • 0x00 摘要
    • 0x01 緣由
    • 0x02 背景概念
      • 2.1 四層執行圖
      • 2.2 Task和SubTask
      • 2.3 如何劃分 Task 的依據
      • 2.4 JobGraph
      • 2.5 BSP模型和Superstep
        • BSP模型
        • BSP模型的實現
        • Flink-Gelly
    • 0x03 Flink的迭代算法(superstep-based)
      • 3.1 Bulk Iterate
      • 3.2 迭代機制
    • 0x04 Alink如何使用迭代
    • 0x05 深入Flink源碼和runtime來驗證
      • 5.1 向Flink提交Job
      • 5.2 生成JobGraph
      • 5.3 迭代對應的Task
        • 5.3.1 IterationHeadTask
        • 5.3.2 IterationIntermediateTask
        • 5.3.3 IterationTailTask
          • 如何和Head建立聯繫
          • 如何把用戶返回的數值傳給Head
        • 5.3.4 IterationSynchronizationSinkTask
      • 5.4 superstep
    • 0x06 結合KMeans代碼看superset
      • 6.1 K-means算法概要
      • 6.2 KMeansPreallocateCentroid
      • 6.3 KMeansAssignCluster 和 KMeansUpdateCentroids
      • 6.4 KMeansOutputModel
    • 0x07 參考

0x00 摘要

Alink 是阿里巴巴基於實時計算引擎 Flink 研發的新一代機器學習算法平台,是業界首個同時支持批式算法、流式算法的機器學習平台。迭代算法在很多數據分析領域會用到,比如機器學習或者圖計算。本文將通過Superstep入手看看Alink是如何利用Flink迭代API來實現具體算法。

因為Alink的公開資料太少,所以以下均為自行揣測,肯定會有疏漏錯誤,希望大家指出,我會隨時更新。

0x01 緣由

為什麼提到 Superstep 這個概念,是因為在擼KMeans代碼的時候,發現幾個很奇怪的地方,比如以下三個步驟中,都用到了context.getStepNo(),而且會根據其數值的不同進行不同業務操作:

public class KMeansPreallocateCentroid extends ComputeFunction {
    public void calc(ComContext context) {
        LOG.info("liuhao  KMeansPreallocateCentroid ");
        if (context.getStepNo() == 1) {
          /** 具體業務邏輯代碼
           * Allocate memory for pre-round centers and current centers.
           */        
        }
    }
}  

public class KMeansAssignCluster extends ComputeFunction {
    public void calc(ComContext context) {
        ......
        if (context.getStepNo() % 2 == 0) {
            stepNumCentroids = context.getObj(KMeansTrainBatchOp.CENTROID1);
        } else {
            stepNumCentroids = context.getObj(KMeansTrainBatchOp.CENTROID2);
        }
      /** 具體業務邏輯代碼
       * Find the closest cluster for every point and calculate the sums of the points belonging to the same cluster.
       */
    }
}

public class KMeansUpdateCentroids extends ComputeFunction {
    public void calc(ComContext context) {
        if (context.getStepNo() % 2 == 0) {
            stepNumCentroids = context.getObj(KMeansTrainBatchOp.CENTROID2);
        } else {
            stepNumCentroids = context.getObj(KMeansTrainBatchOp.CENTROID1);
        }
      /** 具體業務邏輯代碼
       * Update the centroids based on the sum of points and point number belonging to the same cluster.
       */
    }

查看ComContext的源碼,發現stepNo的來源居然是runtimeContext.getSuperstepNumber()

public class ComContext {
   private final int taskId;
   private final int numTask;
   private final int stepNo; // 對,就是這裏
   private final int sessionId;
	public ComContext(int sessionId, IterationRuntimeContext runtimeContext) {
		this.sessionId = sessionId;
		this.numTask = runtimeContext.getNumberOfParallelSubtasks();
		this.taskId = runtimeContext.getIndexOfThisSubtask();
		this.stepNo = runtimeContext.getSuperstepNumber(); // 這裏進行了變量初始化
	}  
	/**
	 * Get current iteration step number, the same as {@link IterationRuntimeContext#getSuperstepNumber()}.
	 * @return iteration step number.
	 */
	public int getStepNo() {
		return stepNo; // 這裡是使用
	}  
}

看到這裡有的兄弟可能會虎軀一震,這不是BSP模型的概念嘛。我就是想寫個KMeans算法,怎麼除了MPI模型,還要考慮BSP模型。下面就讓我們一步一步挖掘究竟Alink都做了什麼工作。

0x02 背景概念

2.1 四層執行圖

在 Flink 中的執行圖可以分為四層:StreamGraph -> JobGraph -> ExecutionGraph -> 物理執行圖

  • StreamGraph:Stream API 編寫的代碼生成的最初的圖。用來表示程序的拓撲結構。
  • JobGraph:StreamGraph 經過優化後生成了 JobGraph, JobGraph是提交給 JobManager 的數據結構。主要的優化為,將多個符合條件的節點 chain 在一起作為一個節點,這樣可以減少數據在節點之間流動所需要的序列化/反序列化/傳輸消耗。JobGraph是唯一被Flink的數據流引擎所識別的表述作業的數據結構,也正是這一共同的抽象體現了流處理和批處理在運行時的統一。
  • ExecutionGraph:JobManager 根據 JobGraph 生成 ExecutionGraph。ExecutionGraph 是 JobGraph 的并行化版本,是調度層最核心的數據結構。
  • 物理執行圖:JobManager 根據 ExecutionGraph 對 Job 進行調度后,在各個TaskManager 上部署 Task 后形成的“圖”,並不是一個具體的數據結構。

2.2 Task和SubTask

因為某種原因,Flink內部對這兩個概念的使用本身就有些混亂:在Task Manager里這個subtask的概念由一個叫Task的類來實現。Task Manager里談論的Task對象實際上對應的是ExecutionGraph里的一個subtask。

所以這兩個概念需要理清楚。

  • Task(任務) :Task對應JobGraph的一個節點,是一個算子Operator。Task 是一個階段多個功能相同 subTask 的集合,類似於 Spark 中的 TaskSet。
  • subTask(子任務) :subTask 是 Flink 中任務最小執行單元,是一個 Java 類的實例,這個 Java 類中有屬性和方法,完成具體的計算邏輯。在ExecutionGraph里Task被分解為多個并行執行的subtask 。每個subtask作為一個excution分配到Task Manager里執行。
  • Operator Chains(算子鏈) :沒有 shuffle 的多個算子合併在一個 subTask 中,就形成了 Operator Chains,類似於 Spark 中的 Pipeline。Operator subTask 的數量指的就是算子的并行度。同一程序的不同算子也可能具有不同的并行度(因為可以通過 setParallelism() 方法來修改并行度)。

Flink 中的程序本質上是并行的。在執行期間,每一個算子 Operator (Transformation)都有一個或多個算子subTask(Operator SubTask),每個算子的 subTask 之間都是彼此獨立,並在不同的線程中執行,並且可能在不同的機器或容器上執行。

Task( SubTask) 是一個Runnable 對象, Task Manager接受到TDD 後會用它實例化成一個Task對象, 並啟動一個線程執行Task的Run方法。

TaskDeploymentDescriptor(TDD) : 是Task Manager在submitTask是提交給TM的數據結構。 他包含了關於Task的所有描述信息。比如:

  • TaskInfo : 包含該Task 執行的java 類,該類是某個 AbstractInvokable的實現類 , 當然也是某個operator的實現類 (比如DataSourceTask, DataSinkTask, BatchTask,StreamTask 等)。
  • IG描述 :通常包含一個或兩個InputGateDeploymentDescriptor(IGD)。
  • 目標RP的描述: ParitionId, PartitionType, RS個數等等。

2.3 如何劃分 Task 的依據

在以下情況下會重新劃分task

  • 并行度發生變化時
  • keyBy() /window()/apply() 等發生 Rebalance 重新分配;
  • 調用 startNewChain() 方法,開啟一個新的算子鏈;
  • 調用 diableChaining()方法,即:告訴當前算子操作不使用 算子鏈 操作。

比如有如下操作

DataStream<String> text = env.socketTextStream(hostname, port);

DataStream counts = text
    .filter(new FilterClass())
    .map(new LineSplitter())
    .keyBy(0)
    .timeWindow(Time.seconds(10))
    .sum(2)

那麼StreamGraph的轉換流是:

 Source --> Filter --> Map --> Timestamps/Watermarks --> Window(SumAggregator) --> Sink

其task是四個:

  • Source –> Filter –> Map
  • keyBy
  • timeWindow
  • Sink

其中每個task又會被分成分若干subtask。在執行時,一個Task會被并行化成若干個subTask實例進行執行,一個subTask對應一個執行線程。

2.4 JobGraph

以上說了這麼多,就是要說jobGraph和subtask,因為本文中我們在分析源碼和調試時候,主要是從jobGraph這裏開始入手來看subtask

JobGraph是在StreamGraph的基礎之上,對StreamNode進行了關聯合併的操作,比如對於source -> flatMap -> reduce -> sink 這樣一個數據處理鏈,當source和flatMap滿足鏈接的條件時,可以可以將兩個操作符的操作放到一個線程并行執行,這樣可以減少網絡中的數據傳輸,由於在source和flatMap之間的傳輸的數據也不用序列化和反序列化,所以也提高了程序的執行效率。

相比流圖(StreamGraph)以及批處理優化計劃(OptimizedPlan),JobGraph發生了一些變化,已經不完全是“靜態”的數據結構了,因為它加入了中間結果集(IntermediateDataSet)這一“動態”概念。

作業頂點(JobVertex)、中間數據集(IntermediateDataSet)、作業邊(JobEdge)是組成JobGraph的基本元素。這三個對象彼此之間互為依賴:

  • 一個JobVertex關聯着若干個JobEdge作為輸入端以及若干個IntermediateDataSet作為其生產的結果集;每個JobVertex都有諸如并行度和執行代碼等屬性。
  • 一個IntermediateDataSet關聯着一個JobVertex作為生產者以及若干個JobEdge作為消費者;
  • 一個JobEdge關聯着一個IntermediateDataSet可認為是源以及一個JobVertex可認為是目標消費者;

那麼JobGraph是怎麼組織並存儲這些元素的呢?其實JobGraph只以Map的形式存儲了所有的JobVertex,鍵是JobVertexID:

private final Map<JobVertexID, JobVertex> taskVertices = new LinkedHashMap<JobVertexID, JobVertex>();

至於其它的元素,通過JobVertex都可以根據關係找尋到。需要注意的是,用於迭代的反饋邊(feedback edge)當前並不體現在JobGraph中,而是被內嵌在特殊的JobVertex中通過反饋信道(feedback channel)在它們之間建立關係。

2.5 BSP模型和Superstep

BSP模型

BSP模型是并行計算模型的一種。并行計算模型通常指從并行算法的設計和分析出發,將各種并行計算機(至少某一類并行計算機)的基本特徵抽象出來,形成一個抽象的計算模型。

BSP模型是一種異步MIMD-DM模型(DM: distributed memory,SM: shared memory),BSP模型支持消息傳遞系統,塊內異步并行,塊間顯式同步,該模型基於一個master協調,所有的worker同步(lock-step)執行, 數據從輸入的隊列中讀取。

BSP計算模型不僅是一種體繫結構模型,也是設計并行程序的一種方法。BSP程序設計準則是整體同步(bulk synchrony),其獨特之處在於超步(superstep)概念的引入。一個BSP程序同時具有水平和垂直兩個方面的結構。從垂直上看,一個BSP程序由一系列串行的超步(superstep)組成。

BSP模型的實現

BSP模型的實現大概舉例如下:

  • Pregel :Google的大規模圖計算框架,首次提出了將BSP模型應用於圖計算,具體請看Pregel——大規模圖處理系統,不過至今未開源。
  • Apache Giraph :ASF社區的Incubator項目,由Yahoo!貢獻,是BSP的java實現,專註於迭代圖計算(如pagerank,最短連接等),每一個job就是一個沒有reducer過程的hadoop job。
  • Apache Hama :也是ASF社區的Incubator項目,與Giraph不同的是它是一個純粹的BSP模型的java實現,並且不單單是用於圖計算,意在提供一個通用的BSP模型的應用框架。

Flink-Gelly

Flink-Gelly利用Flink的高效迭代算子來支持海量數據的迭代式圖處理。目前,Flink Gelly提供了“Vertex-Centric”,“Scatter-Gather”以及“Gather-Sum-Apply”等計算模型的實現。

“Vertex-Centric”迭代模型也就是我們經常聽到的“Pregel”,是一種從Vertex角度出發的圖計算方式。其中,同步地迭代計算的步驟稱之為“superstep”。在每個“superstep”中,每個頂點都執行一個用戶自定義的函數,且頂點之間通過消息進行通信,當一個頂點知道圖中其他任意頂點的唯一ID時,該頂點就可以向其發送一條消息。

但是實際上,KMeans不是圖處理,Alink也沒有基於Flink-Gelly來構建。也許只是借鑒了其概念。所以我們還需要再探尋。

0x03 Flink的迭代算法(superstep-based)

迭代算法在很多數據分析領域會用到,比如機器學習或者圖計算。為了從大數據中抽取有用信息,這個時候往往會需要在處理的過程中用到迭代計算。

所謂迭代運算,就是給定一個初值,用所給的算法公式計算初值得到一个中間結果,然後將中間結果作為輸入參數進行反覆計算,在滿足一定條件的時候得到計算結果。

大數據處理框架很多,比如spark,mr。實際上這些實現迭代計算都是很困難的。

Flink直接支持迭代計算。Flink實現迭代的思路也是很簡單,就是實現一個step函數,然後將其嵌入到迭代算子中去。有兩種迭代操作算子: Iterate和Delta Iterate。兩個操作算子都是在未收到終止迭代信號之前一直調用step函數。

3.1 Bulk Iterate

這種迭代方式稱為全量迭代,它會將整個數據輸入,經過一定的迭代次數,最終得到你想要的結果。

迭代操作算子包括了簡單的迭代形式:每次迭代,step函數會消費全量數據(本次輸入和上次迭代的結果),然後計算得到下輪迭代的輸出(例如,map,reduce,join等)

迭代過程主要分為以下幾步:

  • Iteration Input(迭代輸入):是初始輸入值或者上一次迭代計算的結果。
  • Step Function(step函數):每次迭代都會執行step函數。它迭代計算DataSet,由一系列的operator組成,比如map,flatMap,join等,取決於具體的業務邏輯。
  • Next Partial Solution(中間結果):每一次迭代計算的結果,被發送到下一次迭代計算中。
  • Iteration Result(迭代結果):最後一次迭代輸出的結果,被輸出到datasink或者發送到下游處理。

它迭代的結束條件是:

  • 達到最大迭代次數
  • 自定義收斂聚合函數

編程的時候,需要調用iterate(int),該函數返回的是一個IterativeDataSet,當然我們可以對它進行一些操作,比如map等。Iterate函數唯一的參數是代表最大迭代次數。

迭代是一個環。我們需要進行閉環操作,那麼這時候就要用到closeWith(Dataset)操作了,參數就是需要循環迭代的dataset。也可以可選的指定一個終止標準,操作closeWith(DataSet, DataSet),可以通過判斷第二個dataset是否為空,來終止迭代。如果不指定終止迭代條件,迭代就會在迭代了最大迭代次數后終止。

3.2 迭代機制

DataSet API引進了獨特的同步迭代機制(superstep-based),僅限於用在有界的流。

我們將迭代操作算子的每個步驟函數的執行稱為單個迭代。在并行設置中,在迭代狀態的不同分區上并行計算step函數的多個實例。在許多設置中,對所有并行實例上的step函數的一次評估形成了所謂的superstep,這也是同步的粒度。因此,迭代的所有并行任務都需要在初始化下一個superstep之前完成superstep。終止準則也將被評估為superstep同步屏障。

下面是Apache原文

We referred to each execution of the step function of an iteration operator as a single iteration. In parallel setups, multiple instances of the step function are evaluated in parallel on different partitions of the iteration state. In many settings, one evaluation of the step function on all parallel instances forms a so called superstep, which is also the granularity of synchronization. Therefore, all parallel tasks of an iteration need to complete the superstep, before a next superstep will be initialized. Termination criteria will also be evaluated at superstep barriers.

下面是apache原圖

概括如下:

每次迭代都是一個superstep
    每次迭代中有若干subtask在不同的partition上分別執行step
      	 每個step有一個HeadTask,若干IntermediateTask,一個TailTask
    每個superstep有一個SynchronizationSinkTask 同步,因為迭代的所有并行任務需要在下一個迭代前完成

由此我們可以知道,superstep這是Flink DataSet API的概念,但是你從這裡能夠看到BSP模型的影子,比如:

  • 在傳統的BSP模型中,一個superstep被分為3步: 本地的計算, 消息的傳遞, 同步的barrier.
  • Barrier Synchronization又叫障礙同步或柵欄同步。每一次同步也是一個超步的完成和下一個超步的開始;
  • Superstep超步 是一次計算迭代,從起始每往前步進一層對應一個超步。
  • 程序該什麼時候結束是程序自己控制

0x04 Alink如何使用迭代

KMeansTrainBatchOp.iterateICQ函數中,生成了一個IterativeComQueue,而IterativeComQueue之中就用到了superstep-based迭代。

return new IterativeComQueue()
   .initWithPartitionedData(TRAIN_DATA, data)
   .initWithBroadcastData(INIT_CENTROID, initCentroid)
   .initWithBroadcastData(KMEANS_STATISTICS, statistics)
   .add(new KMeansPreallocateCentroid())
   .add(new KMeansAssignCluster(distance))
   .add(new AllReduce(CENTROID_ALL_REDUCE))
   .add(new KMeansUpdateCentroids(distance))
   .setCompareCriterionOfNode0(new KMeansIterTermination(distance, tol)) // 終止條件
   .closeWith(new KMeansOutputModel(distanceType, vectorColName, latitudeColName, longitudeColName)) 
   .setMaxIter(maxIter) // 迭代最大次數
   .exec();

而BaseComQueue.exec函數中則有:

public DataSet<Row> exec() {
   IterativeDataSet<byte[]> loop // Flink 迭代API
      = loopStartDataSet(executionEnvironment)
      .iterate(maxIter);
     // 後續操作能看出來,之前添加在queue上的比如KMeansPreallocateCentroid,都是在loop之上運行的。
  		if (null == compareCriterion) {
        loopEnd = loop.closeWith...
     	} else {     
        // compare Criterion.
        DataSet<Boolean> criterion = input ... compareCriterion
        loopEnd = loop.closeWith( ... criterion ... )
      }   
}

再仔細研究代碼,我們可以看出:

superstep包括:

.add(new KMeansPreallocateCentroid())
.add(new KMeansAssignCluster(distance))
.add(new AllReduce(CENTROID_ALL_REDUCE))
.add(new KMeansUpdateCentroids(distance))

終止標準就是

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

擁有後台管理系統的網站,將擁有強大的資料管理與更新功能,幫助您隨時新增網站的內容並節省網站開發的成本。

利用KMeansIterTermination構建了一個RichMapPartitionFunction作為終止標準。最後結束時候調用 KMeansOutputModel完成業務操作。

最大循環就是

.setMaxIter(maxIter)

於是我們可以得出結論,superstep-based Bulk Iterate 迭代算子是用來實現整體KMeans算法,KMeans算法就是一個superstep進行迭代。但是在superstep內容如果需要通訊或者柵欄同步,則採用了MPI的allReduce。

0x05 深入Flink源碼和runtime來驗證

我們需要深入到Flink內部去挖掘驗證,如果大家有興趣,可以參見下面調用棧,自己添加斷點來研究。

execute:56, LocalExecutor (org.apache.flink.client.deployment.executors)
executeAsync:944, ExecutionEnvironment (org.apache.flink.api.java)
execute:860, ExecutionEnvironment (org.apache.flink.api.java)
execute:844, ExecutionEnvironment (org.apache.flink.api.java)
collect:413, DataSet (org.apache.flink.api.java)
sinkFrom:44, PrintBatchOp (com.alibaba.alink.operator.batch.utils)
sinkFrom:20, PrintBatchOp (com.alibaba.alink.operator.batch.utils)
linkFrom:31, BaseSinkBatchOp (com.alibaba.alink.operator.batch.sink)
linkFrom:17, BaseSinkBatchOp (com.alibaba.alink.operator.batch.sink)
link:89, BatchOperator (com.alibaba.alink.operator.batch)
linkTo:239, BatchOperator (com.alibaba.alink.operator.batch)
print:337, BatchOperator (com.alibaba.alink.operator.batch)
main:35, KMeansExample (com.alibaba.alink)

5.1 向Flink提交Job

Alink和Flink構建聯繫,是在print調用中完成的。因為是本地調試,Flink會啟動一個miniCluster,然後會做如下操作。

  • 首先生成執行計劃Plan。Plan以數據流形式來表示批處理程序,但它只是批處理程序最初的表示,然後計劃會被優化以生成更高效的方案OptimizedPlan。
  • 然後,計劃被編譯生成JobGraph。這個圖是要交給flink去生成task的圖。
  • 生成一系列配置。
  • 將JobGraph和配置交給flink集群去運行。如果不是本地運行的話,還會把jar文件通過網絡發給其他節點。
  • 以本地模式運行的話,可以看到啟動過程,如啟動性能度量、web模塊、JobManager、ResourceManager、taskManager等等。

當我們看到了submitJob調用,就知道KMeans代碼已經和Flink構建了聯繫

@Internal
public class LocalExecutor implements PipelineExecutor {

   public static final String NAME = "local";

   @Override
   public CompletableFuture<JobClient> execute(Pipeline pipeline, Configuration configuration) throws Exception {

      // we only support attached execution with the local executor.
      checkState(configuration.getBoolean(DeploymentOptions.ATTACHED));

      final JobGraph jobGraph = getJobGraph(pipeline, configuration);
      final MiniCluster miniCluster = startMiniCluster(jobGraph, configuration);
      final MiniClusterClient clusterClient = new MiniClusterClient(configuration, miniCluster);

      CompletableFuture<JobID> jobIdFuture = clusterClient.submitJob(jobGraph);

      jobIdFuture
            .thenCompose(clusterClient::requestJobResult)
            .thenAccept((jobResult) -> clusterClient.shutDownCluster());

      return jobIdFuture.thenApply(jobID ->
            new ClusterClientJobClientAdapter<>(() -> clusterClient, jobID));
   }

5.2 生成JobGraph

生成jobGraph的具體流程是:

  • IterativeDataSet.closeWith會生成一個BulkIterationResultSet。
  • PrintBatchOp.sinkFrom中會調用到ExecutionEnvironment.executeAsync
  • 調用createProgramPlan構建一個Plan
  • OperatorTranslation.translate函數發現if (dataSet instanceof BulkIterationResultSet),則調用translateBulkIteration(bulkIterationResultSet);
  • 這時候生成了執行計劃Plan
  • ExecutionEnvironment.executeAsync調用LocalExecutor.execute
  • 然後調用FlinkPipelineTranslationUtil.getJobGraph來生成jobGraph
  • GraphCreatingVisitor.preVisit中會判斷 if (c instanceof BulkIterationBase),以生成BulkIterationNode
  • PlanTranslator.translateToJobGraph會調用到JobGraphGenerator.compileJobGraph,最終調用到createBulkIterationHead就生成了迭代處理的Head。
  • 最後將jobGraph提交給Cluster ,jobGraph 變形為 ExceutionGraph在JM和TM上執行。

5.3 迭代對應的Task

前面代碼中,getJobGraph函數作用是生成了job graph。

然後 JobManager 根據 JobGraph 生成 ExecutionGraph。ExecutionGraph 是 JobGraph 的并行化版本,是調度層最核心的數據結構。

最後 JobManager 根據 ExecutionGraph 對 Job 進行調度后,在各個TaskManager 上部署 Task。

所以我們需要看看最終運行時候,迭代API對應着哪些Task。

針對IterativeDataSet,即superstep-based Bulk Iterate,Flink生成了如下的task。

  • IterationHeadTask
  • IterationIntermediateTask
  • IterationTailTask
  • IterationSynchronizationSinkTask

5.3.1 IterationHeadTask

IterationHeadTask主要作用是協調一次迭代。

它會讀取初始輸入,和迭代Tail建立一個BlockingBackChannel。在成功處理輸入之後,它會發送EndOfSuperstep事件給自己的輸出。它在每次superstep之後會聯繫 synchronization task,等到自己收到一個用來同步的AllWorkersDoneEvent。AllWorkersDoneEvent表示所有其他的heads已經完成了自己的迭代。

下一次迭代時候,上一次迭代中tail的輸出就經由backchannel傳輸,形成了head的輸入。何時進入到下一個迭代,是由HeadTask完成的。一旦迭代完成,head將發送TerminationEvent給所有和它關聯的task,告訴他們shutdown。

				barrier.waitForOtherWorkers();

				if (barrier.terminationSignaled()) {
					requestTermination();
					nextStepKickoff.signalTermination();
				} else {
					incrementIterationCounter();
					String[] globalAggregateNames = barrier.getAggregatorNames();
					Value[] globalAggregates = barrier.getAggregates();
					aggregatorRegistry.updateGlobalAggregatesAndReset(globalAggregateNames, globalAggregates);
          // 在這裏發起下一次Superstep。
					nextStepKickoff.triggerNextSuperstep();
				}
			}

IterationHeadTask是在JobGraphGenerator.createBulkIterationHead中構建的。其例子如下:

"PartialSolution (Bulk Iteration) (org.apache.flink.runtime.iterative.task.IterationHeadTask)"

5.3.2 IterationIntermediateTask

IterationIntermediateTask是superstep中間段的task,其將傳輸EndOfSuperstepEvent和TerminationEvent給所有和它關聯的tasks。此外,IterationIntermediateTask能更新the workset或者the solution set的迭代狀態。

如果迭代狀態被更新,本task的輸出將傳送回IterationHeadTask,在這種情況下,本task將作為head再次被安排。

IterationIntermediateTask的例子如下:

 "MapPartition (computation@KMeansUpdateCentroids) (org.apache.flink.runtime.iterative.task.IterationIntermediateTask)"
   
 "Combine (SUM(0), at kMeansPlusPlusInit(KMeansInitCentroids.java:135) (org.apache.flink.runtime.iterative.task.IterationIntermediateTask)"
   
 "MapPartition (AllReduceSend) (org.apache.flink.runtime.iterative.task.IterationIntermediateTask)"
   
"Filter (Filter at kMeansPlusPlusInit(KMeansInitCentroids.java:130)) (org.apache.flink.runtime.iterative.task.IterationIntermediateTask)"
   

5.3.3 IterationTailTask

IterationTailTask是迭代的最末尾。如果迭代狀態被更新,本task的輸出將通過BlockingBackChannel傳送回IterationHeadTask,反饋給迭代頭就意味着一個迭代完整邏輯的完成,那麼就可以關閉這個迭代閉合環了。這種情況下,本task將在head所在的實例上重新被調度。

這裡有幾個關鍵點需要注意:

如何和Head建立聯繫

Flink有一個BlockingQueueBroker類,這是一個阻塞式的隊列代理,它的作用是對迭代併發進行控制。Broker是單例的,迭代頭任務和尾任務會生成同樣的broker ID,所以頭尾在同一個JVM中會基於相同的dataChannel進行通信。dataChannel由迭代頭創建。

IterationHeadTask中會生成BlockingBackChannel,這是一個容量為1的阻塞隊列。

// 生成channel
BlockingBackChannel backChannel = new BlockingBackChannel(new SerializedUpdateBuffer(segments, segmentSize, this.getIOManager())); 

// 然後block在這裏,等待Tail
superstepResult = backChannel.getReadEndAfterSuperstepEnded();

IterationTailTask則是如下:

// 在基類得到channel,因為是單例,所以會得到同一個
worksetBackChannel = BlockingBackChannelBroker.instance().getAndRemove(brokerKey());

// notify iteration head if responsible for workset update 在這裏通知Head
worksetBackChannel.notifyOfEndOfSuperstep();

而兩者都是利用如下辦法來建立聯繫,在同一個subtask中會使用同一個brokerKey,這樣首尾就聯繫起來了。

public String brokerKey() {
    if (this.brokerKey == null) {
        int iterationId = this.config.getIterationId();
        this.brokerKey = this.getEnvironment().getJobID().toString() + '#' + iterationId + '#' + this.getEnvironment().getTaskInfo().getIndexOfThisSubtask();
    }

    return this.brokerKey;
}
如何把用戶返回的數值傳給Head

這是通過output.collect來完成的。

首先,在Tail初始化時候,會生成一個outputCollector,這個outputCollector會被設置為本task的輸出outputCollector。這樣就保證了用戶函數的輸出都會轉流到outputCollector。

而outputCollector的輸出就是worksetBackChannel的輸出,這裏設置為同一個instance。這樣用戶輸出就輸出到backChannel中。

	@Override
	protected void initialize() throws Exception {
		super.initialize();
    
		// set the last output collector of this task to reflect the iteration tail state update:
		// a) workset update,
		// b) solution set update, or
		// c) merged workset and solution set update

		Collector<OT> outputCollector = null;
		if (isWorksetUpdate) {
      // 生成一個outputCollector
			outputCollector = createWorksetUpdateOutputCollector();

			// we need the WorksetUpdateOutputCollector separately to count the collected elements
			if (isWorksetIteration) {
				worksetUpdateOutputCollector = (WorksetUpdateOutputCollector<OT>) outputCollector;
			}
		}
    
    ......
    // 把outputCollector設置為本task的輸出
		setLastOutputCollector(outputCollector);
	}

outputCollector的輸出就是worksetBackChannel的輸出buffer,這裏設置為同一個instance。

	protected Collector<OT> createWorksetUpdateOutputCollector(Collector<OT> delegate) {
		DataOutputView outputView = worksetBackChannel.getWriteEnd();
		TypeSerializer<OT> serializer = getOutputSerializer();
		return new WorksetUpdateOutputCollector<OT>(outputView, serializer, delegate);
	}

運行時候如下:

	@Override
	public void run() throws Exception {

		SuperstepKickoffLatch nextSuperStepLatch = SuperstepKickoffLatchBroker.instance().get(brokerKey());

		while (this.running && !terminationRequested()) {

      // 用戶在這裏輸出,最後會輸出到output.collect,也就是worksetBackChannel的輸出buffer。
			super.run();

      // 這時候以及輸出到channel完畢,只是通知head進行讀取。
			if (isWorksetUpdate) {
				// notify iteration head if responsible for workset update
				worksetBackChannel.notifyOfEndOfSuperstep();
			} else if (isSolutionSetUpdate) {
				// notify iteration head if responsible for solution set update
				solutionSetUpdateBarrier.notifySolutionSetUpdate();
			}

      ...
	}

IterationTailTask例子如下:

"Pipe (org.apache.flink.runtime.iterative.task.IterationTailTask)"

5.3.4 IterationSynchronizationSinkTask

IterationSynchronizationSinkTask作用是同步所有的iteration heads,IterationSynchronizationSinkTask被是實現成一個 output task。其只是用來協調,不處理任何數據。

在每一次superstep,IterationSynchronizationSinkTask只是等待直到它從每一個head都收到一個WorkerDoneEvent。這表示下一次superstep可以開始了。

這裏需要注意的是 SynchronizationSinkTask 如何等待各個并行度的headTask。比如Flink的并行度是5,那麼SynchronizationSinkTask怎麼做到等待這5個headTask。

在IterationSynchronizationSinkTask中,註冊了SyncEventHandler來等待head的WorkerDoneEvent。

this.eventHandler = new SyncEventHandler(numEventsTillEndOfSuperstep, this.aggregators, this.getEnvironment().getUserClassLoader());
this.headEventReader.registerTaskEventListener(this.eventHandler, WorkerDoneEvent.class);

在SyncEventHandler中,我們可以看到,在構建時候,numberOfEventsUntilEndOfSuperstep就被設置為并行度,每次收到一個WorkerDoneEvent,workerDoneEventCounter就遞增,當等於numberOfEventsUntilEndOfSuperstep,即并行度時候,就說明本次superstep中,所有headtask都成功了。

    private void onWorkerDoneEvent(WorkerDoneEvent workerDoneEvent) {
        if (this.endOfSuperstep) {
            throw new RuntimeException("Encountered WorderDoneEvent when still in End-of-Superstep status.");
        } else {
          // 每次遞增
            ++this.workerDoneEventCounter;
            String[] aggNames = workerDoneEvent.getAggregatorNames();
            Value[] aggregates = workerDoneEvent.getAggregates(this.userCodeClassLoader);
            if (aggNames.length != aggregates.length) {
                throw new RuntimeException("Inconsistent WorkerDoneEvent received!");
            } else {
                for(int i = 0; i < aggNames.length; ++i) {
                    Aggregator<Value> aggregator = (Aggregator)this.aggregators.get(aggNames[i]);
                    aggregator.aggregate(aggregates[i]);
                }

              // numberOfEventsUntilEndOfSuperstep就是并行度,等於并行度時候就說明所有head都成功了。
                if (this.workerDoneEventCounter % this.numberOfEventsUntilEndOfSuperstep == 0) {
                    this.endOfSuperstep = true;
                    Thread.currentThread().interrupt();
                }
            }
        }
    }

IterationSynchronizationSinkTask的例子如下:

"Sync (BulkIteration (Bulk Iteration)) (org.apache.flink.runtime.iterative.task.IterationSynchronizationSinkTask)"

5.4 superstep

綜上所述,我們最終得到superstep如下:

***** 文字描述如下 *****
  
每次迭代都是一個superstep
  每次迭代中有若干subtask在不同的partition上分別執行step
     每個step有一個HeadTask,若干IntermediateTask,一個TailTask
  每個superstep有一個SynchronizationSinkTask
  
***** 偽代碼大致如下 *****
  
for maxIter :
  begin superstep
      for maxSubTask :
         begin step
           IterationHeadTask
           IterationIntermediateTask
           IterationIntermediateTask
           ...
           IterationIntermediateTask
           IterationIntermediateTask
           IterationTailTask
         end step
    IterationSynchronizationSinkTask
  end superstep

0x06 結合KMeans代碼看superset

6.1 K-means算法概要

K-means算法的過程,為了盡量不用數學符號,所以描述的不是很嚴謹,大概就是這個意思,“物以類聚、人以群分”:

  1. 首先輸入k的值,即我們希望將數據集經過聚類得到k個分組。
  2. 從數據集中隨機選擇k個數據點作為初始大哥(質心,Centroid)
  3. 對集合中每一個小弟,計算與每一個大哥的距離(距離的含義後面會講),離哪個大哥距離近,就跟定哪個大哥。
  4. 這時每一個大哥手下都聚集了一票小弟,這時候召開人民代表大會,每一群選出新的大哥(其實是通過算法選出新的質心)。
  5. 如果新大哥和老大哥之間的距離小於某一個設置的閾值(表示重新計算的質心的位置變化不大,趨於穩定,或者說收斂),可以認為我們進行的聚類已經達到期望的結果,算法終止。
  6. 如果新大哥和老大哥距離變化很大,需要迭代3~5步驟。

6.2 KMeansPreallocateCentroid

KMeansPreallocateCentroid也是superstep一員,但是只有context.getStepNo() == 1的時候,才會進入實際業務邏輯,預分配Centroid。當superstep為大於1的時候,本task會執行,但不會進入具體業務代碼。

public class KMeansPreallocateCentroid extends ComputeFunction {
    private static final Logger LOG = LoggerFactory.getLogger(KMeansPreallocateCentroid.class);

    @Override
    public void calc(ComContext context) {
        // 每次superstep都會進到這裏
        LOG.info("  KMeansPreallocateCentroid 我每次都會進的呀   ");
        if (context.getStepNo() == 1) {
          // 實際預分配業務只進入一次
        }
    }
}

6.3 KMeansAssignCluster 和 KMeansUpdateCentroids

KMeansAssignCluster 作用是為每個點(point)計算最近的聚類中心,為每個聚類中心的點坐標的計數和求和。

KMeansUpdateCentroids 作用是基於計算出來的點計數和坐標,計算新的聚類中心。

Alink在整個計算過程中維護一個特殊節點來記住待求中心點當前的結果。

這就是為啥迭代時候需要區分奇數次和偶數次的原因了。奇數次就表示老大哥,偶數次就表示新大哥。每次superstep只會計算一批大哥,留下另外一批大哥做距離比對。

另外要注意的一點是:普通的迭代計算,是通過Tail給Head回傳用戶數據,但是KMeans這裏的實現並沒有採用這個辦法,而是把計算出來的中心點都存在共享變量中,在各個intermediate之間互相交互。

public class KMeansAssignCluster extends ComputeFunction {
    public void calc(ComContext context) {
        ......
        if (context.getStepNo() % 2 == 0) {
            stepNumCentroids = context.getObj(KMeansTrainBatchOp.CENTROID1);
        } else {
            stepNumCentroids = context.getObj(KMeansTrainBatchOp.CENTROID2);
        }
      /** 具體業務邏輯代碼
       * Find the closest cluster for every point and calculate the sums of the points belonging to the same cluster.
       */
    }
}

public class KMeansUpdateCentroids extends ComputeFunction {
    public void calc(ComContext context) {
        if (context.getStepNo() % 2 == 0) {
            stepNumCentroids = context.getObj(KMeansTrainBatchOp.CENTROID2);
        } else {
            stepNumCentroids = context.getObj(KMeansTrainBatchOp.CENTROID1);
        }
      /** 具體業務邏輯代碼
       * Update the centroids based on the sum of points and point number belonging to the same cluster.
       */
    }

6.4 KMeansOutputModel

這裏要特殊說明,因為KMeansOutputModel是最終輸出模型,而KMeans算法的實現是:所有subtask都擁有所有中心點,就是說所有subtask都會有相同的模型,就沒有必要全部輸出,所以這裏限定了第一個subtask才能輸出,其他的都不輸出。

	@Override
	public List <Row> calc(ComContext context) {
    // 只有第一個subtask才輸出模型數據。
		if (context.getTaskId() != 0) {
			return null;
		}

    ....
      
		modelData.params = new KMeansTrainModelData.ParamSummary();
		modelData.params.k = k;
		modelData.params.vectorColName = vectorColName;
		modelData.params.distanceType = distanceType;
		modelData.params.vectorSize = vectorSize;
		modelData.params.latitudeColName = latitudeColName;
		modelData.params.longtitudeColName = longtitudeColName;

		RowCollector collector = new RowCollector();
		new KMeansModelDataConverter().save(modelData, collector);
		return collector.getRows();
	}

0x07 參考

幾種并行計算模型的區別(BSP LogP PRAM)

https://ci.apache.org/projects/flink/flink-docs-release-1.10/dev/batch/iterations.html
聚類、K-Means、例子、細節

Flink-Gelly:Iterative Graph Processing

從BSP模型到Apache Hama

Flink DataSet迭代運算

幾種并行計算模型的區別(BSP LogP PRAM)

Flink架構,源碼及debug

Flink 之 Dataflow、Task、subTask、Operator Chains、Slot 介紹

Flink 任務和調度

Flink運行時之生成作業圖

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

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

還在煩惱搬家費用要多少哪?台中大展搬家線上試算搬家費用,從此不再擔心「物品怎麼計費」、「多少車才能裝完」

原型和原型鏈的深入探索_台中搬家公司

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

擁有後台管理系統的網站,將擁有強大的資料管理與更新功能,幫助您隨時新增網站的內容並節省網站開發的成本。

前言

原型和原型鏈這方面的底層原理知識,一直以來都是面試市場上的一塊的肥肉,也是每一位前端開發人員不得不掌握的內功心法。一直以來,我都想要搞懂弄明白的這部分知識,所以,就借這次重學前端將這方面的成果和自己的一些拙見整理一下,分享給大家。現在就從編程思想開始講起吧。

本文篇幅較長,如果只想了解原型和原型鏈的核心知識,建議可以直接從第三部分看起。

一.編程思想

提起編程思想,這個概念在我的腦海中一直是一個非常模糊的概念,我所知道的是作為一名開發人員,每個人都應當具備這種能力,並且要不斷地去探索到底怎麼才能提高編程思想。那什麼是編程思想呢?

我覺得這個問題沒有固定的答案,如果非要給一個定義,那大概就是用計算機來解決人們實際問題的思維方式,即編程思想。目前我所了解的編程思想有面向過程編程,結構化編程以及面向對象編程。

1.面向過程編程

面向過程:POP(Process-oriented programming)就是分析出解決問題所需要的的步驟,然後用函數把這些步驟一步一步實現,使用的時候再一個一個的一次調用就可以了。這裏舉個栗子:將一隻大象裝進冰箱,就可以看做是面向過程的做法。

面向過程:就是按照我們分析好了的步驟,按照這個步驟解決問題。

2.結構化編程

結構化編程(Structured programming):在程序設計的早期,程序採用流程圖和自上而下的方式進行設計。這種設計方法的主要思路是將一個大問題分解為多個小問題進行解決,再針對每個小問題編寫方法。總體上來說,是先構建一個主過程來啟動程序流程,隨後根據程序走向來調用相關的其他過程,這種程序設計思想就是結構化編程。

舉個經典的栗子:需要編寫不同的工資計算方法,社保計算方法以及個人所得稅計算方法。而如果從另一個角度看這個程序,則可以從判斷判斷該程序中的對象入手。該程序中的對象,最明顯的就是“員工”!(小玲至今沒弄懂工資咋算的,所以就隨意畫了一個工資計算的流程圖,大家將就看看吧)

3.面向對象編程

面向對象是把事物分解成一個個對象,然後由對象之間分工合作。

舉個栗子:將大象裝進冰箱,面向對象的做法。(突然有點可憐這隻大象了,老是被裝進冰箱)

先找出對象,並寫出這些對象的功能

(1)大象對象

  • 進去

(2)冰箱對象

  • 打開

  • 關閉

(3)使用大象和冰箱的功能

面向對象是以對象功能來劃分問題,而不是步驟。

4.三大編程思想的對比

面向過程:

優點:性能比面向對象高,適合跟硬件聯繫很緊密的東西,例如單片機就採用的面向過程編程。

缺點:沒有面向對象易維護

面向對象:

優點:易維護,易復用,易擴展,由於面向對象有封裝,繼承,多態性的特性,可以設計出低耦合的系統,使系統更加靈活,更加易於維護。

缺點:性能比面向過程低。

結構化編程:

優點:程序易於閱讀、理解和維護,能將一個複雜的程序分解成若干個子結構,便於控制、降低程序的複雜性;提高了編程工作的效率,降低了軟件開發成本

缺點:

  • 用戶要求難以在系統分析階段準確定義,致使系統在交付使用時產生許多回問題。

  • 用系統開發每個階段的成果來進行控制,不能適應事物變化的要求。

  • 系統的開發周期長。

某大佬總結的:用面向過程的方式寫出來的程序是一份蛋炒飯,而用面向對象寫出來的程序是一份蓋澆飯。(覺得有點意思,拿來用用)

5.深入面向對象

面向對象更貼近我們的實際生活,可以使用面向對象的思想來描述世界事物,但是事物分為具體的事物和抽象的事物。

面向對象編程:

程序中先用對象結構保存現實中一個事物的屬性和功能,然後再按需使用事物的屬性和功能,這種編程方法,就叫面向對象編程

使用面向對象的原因:

便於大量數據的管理和維護

面向對象的思維特點:

(1)抽取(抽象)對象共用的屬性和行為組織(封裝)成一個類(模板)

(2)對類進行實例化,獲取類的對象

面向對象編程我們考慮的是有哪些對象;按照面向對象的思維特點,是不斷的創建對象,使用對象,指揮對象做事情。

面向對象的三大特點:

封裝、繼承和多態

想要對編程思想有進一步了解的話,可以看看一位前輩寫的一位十年軟件工程師告訴你什麼是編程思想,相信你會有新的收穫哦。

二.對象

1.什麼是對象?

廣義來講,都說萬物皆對象。對象是一個具體的事物,看的見摸得着的實物,例如:一本書,一支筆,一個人可以是”對象”,一個數據庫、一張網頁等也可以是“對象”。

在JavaScript中,對象是一組無序的相關屬性和方法的集合,所有的實物都是對象,例如字符串,數值,數組,函數等。

許多人都以為“JavaScript 中萬物都是對象”,這是錯誤的。對象是 6 個(或者是 7 個,取 決於你的觀點)基礎類型之一。對象有包括 function 在內的子類型,不同子類型具有不同 的行為,比如內部標籤 [object Array] 表示這是對象的子類型數組。 對象就是鍵 / 值對的集合。可以通過 .propName 或者 [“propName”] 語法來獲取屬性值。

——選自《你不知道的JavaScript(上卷)》

2.組成

對象是由屬性和方法組成的:

  • 屬性:事物的特徵,在對象中用屬性來表示(常用名詞)

  • 方法:事物的行為,在對象中用方法來表示(常用動詞)

3.何時使用對象?

今後只要使用面向對象的編程方式,都要先創建所需的所有對象,作為備用。

4.創建對象的幾種方式

看到前輩們寫的文章中一共提到了有以下五種方式:

  • 工廠模式(對象字面量):用{}創建一個對象

  • 構造函數模式:用new創建

  • 原型對象模式:利用prototype創建

  • 組合使用構造函數模式和原型模式 :構造函數模式用於定義實例屬性,原型模式用於定義方法和共享的屬性(可以看成是重寫原型對象)。

  • 動態原型模式

可以看看javascript中創建對象的幾種方式 或者 JavaScript創建對象的幾種方式,講解的非常詳細。在這裏我就只簡單介紹其中三種

4.1 工廠模式(對象字面量)

今天上班第一天,先把小玲自己的信息記錄一下~

      
1   var person = {
2             uname:"xiaoling",
3             age:18,
4             intr:function(){
5                  console.log('我是小玲')}
6    };
7    console.log(person);
8    person.intr() //調用方法 

 

現在我們來看一下,假如今天老闆開會說,公司要再來三個新同事,讓你記錄一下她們的信息。

然後你按照這種方式一會就完成了

    
 1         var person1 = {
 2             uname:"小張",
 3             age:20,
 4             intr:function(){
 5                  console.log('我是小張1111')}
 6         };
 7         var person2 = {
 8             uname:"小劉",
 9             age:23,
10             intr:function(){
11                  console.log('我是小劉')}
12         };
13         var person3 = {
14             uname:"小蘇",
15             age:24,
16             intr:function(){
17                  console.log('我是小蘇')}
18         };

 

如果按照這種字面量的方式去創建,不會感覺太傻瓜式了嗎?假如老闆說要增加一個特長 Specialty 的信息,你是否對要這些創建好的每一個對象都要進行添加修改呢?另外,請注意 person1 這個對象,因為小玲粗心了一下,不小心記錄錯誤了。導致兩個地方的屬性 name 和 intr 方法中的打印的名字內容是不一致的。那麼,這種問題能否在以後開發工作中避免呢?

對於第二個問題,我們可以用this.屬性名來代替方法中引用的對象屬性即

1 console.log('我是'+this.uname)
2 //或者
3 console.log(`我是${this.uname}`)

 

關於this,每個函數都會自動攜帶(可以想象成天生就有),可以直接使用,指向正在調用函數的對象(誰調用,this就指向誰),所以person.intr(),intr中的this就指向person。

對於第一個問題,我們可以用下面講的構造函數來解決~

4.2 構造函數模式

構造函數

構造函數式是一種特殊的函數,主要用來初始化對象,及為對象成員變量賦初始值,它總是與new一起使用。我們可以把對象中一些公用的屬性和方法抽取出來,然後封裝到這個函數裏面。

new在執行時做的四件事

①在內存中創建一個新的空對象

②讓this指向這個新的對象

③執行構造函數裏面的代碼,給這個新對象添加屬性和方法

④返回這個新對象(所以構造函數裏面不需要return)

    
 1     //2.構造函數模式
 2         //創建構造函數 Person    uname/age/intr 是實例成員
 3       function Person(name,age){
 4           this.uname = name;
 5           this.age = age;
 6  7           this.intr = function(){
 8               console.log(`我是${this.uname}`);
 9           }
10       }
11         //創建實例
12        var p1 = new Person('xiaoling',18),
13            p2 = new Person('小張',20),
14            p3 = new Person('小劉',23);
15 16           console.log(p1); //Person {name: "xiaoling", age: 18, intr: ƒ}
17           console.log(p2); //Person {name: "小張", age: 20, intr: ƒ}
18           console.log(p3); //Person {name: "小劉", age: 23, intr: ƒ}
19         //調用方法
20           p1.intr(); //我是xiaoling
21           p2.intr(); //我是小張
22           p3.intr(); //我是小劉
23            console.log(Person.intr); //undefined
24 25            Person.sex = '女';   //sex是靜態成員
26            console.log(Person.sex); //
27            console.log(p1.sex);  //undefined

 

這裏提幾點:

  • 構造函數中的屬性和方法我們稱為成員,成員可以添加

  • 實例成員就是構造函數內部通過this添加的成員 ,如name,age,intr就是實例成員;實例成員只能通過實例化的對象來訪問,如p1.uname。不能通過構造函數來訪問實例成員,如Person.uname (這樣是不允許的)

  • 靜態成員是在構造函數本身添加的成員,如sex就是靜態成員。靜態成員只能通過構造函數來訪問,不能通過實例對象來訪問(不可以用p1.sex訪問)

這樣,就能愉快地解決4.1的第一個問題啦!

構造函數存在的問題

現在我們來看一下p1,p2,p3在內存中是怎樣的?

從上圖中我們可以看到,創建多個不同的對象時,會同時在內存中開闢多個空間創建多個相同的函數,這些方法都是一樣的。所以就存在浪費內存的問題。我們希望所有的對象使用同一個函數,這樣會比較節省內存,那麼這個問題改如何解決呢?

利用原型對象的方式創建對象可以解決。

4.3 原型對象模式 -prototype

這節內容比較多,就單獨在第三部分講了~

另:其他創建對象的方式請自行查閱資料了解(可以看看javascript中創建對象的幾種方式 或者 JavaScript創建對象的幾種方式),這裏我就不再講了~

三.原型

先來簡單介紹幾個概念

1.構造函數

前面已經提過了,這裏就再簡單概括一下吧。

構造函數式是一種特殊的函數,return會自動返回一個對象。使用時要搭配new使用,並且new會做4件非常重要的事。

2.原型對象

現在想想什麼是原型呢?

一個對象,我們也稱prototype為原型對象,他具有共享方法的作用。

其實在創建每個構造函數時,都會自動附贈一個空對象,名為原型對象(prototype),屬性名為prototype,這個屬性指向函數的原型對象,同時這個屬性是一個對象類型的值。通過 構造函數.prototype 屬性,可獲得這個構造函數對應的一個原型對象。

構造函數通過原型對象分配的函數是所有對象共享的。JavaScript規定,每一個構造函數都有一個prototype屬性,他指向另一個對象。請再次注意這個prototype就是一個對象,這個對象的所有屬性和方法,都會被構造函數所擁有。

借用一張圖來表示構造函數和實例原型之間的關係:

3.對象原型proto

對象都會有一個屬性proto指向構造函數的prototype原型對象,之所以我們的對象可以使用構造函數prototype原型對象的屬性和方法,就是因為對象對象有proto原型的存在,那麼對象的proto是怎麼存在的呢?

我們來看一下這段話:

JavaScript 中的對象有一個特殊的 [[Prototype]] 內置屬性,其實就是對於其他對象的引用。幾乎所有的對象在創建時[[Prototype]] 屬性都會被賦予一個非空的值。 

注意:很快我們就可以看到,對象的 [[Prototype]] 鏈接可以為空,雖然很少見。

                                                    ——選自《你不知道的JavaScript(上卷)》 第五章

這段話的意思其實就是在告訴我們:每個對象都有“proro”屬性,這個屬性會通過指針的方式指向其他對象,這個“其他對象”就是我們說的原型對象。當然除了頂級Object.prototype.proto為null外,幾乎所有的”proto“都會指向一個非空對象。

當我們用構造函數創建對象時,new的第二步自動為新對象添加”_ proto “屬性,將” _ proto_ _”屬性指向當前構造函數的原型對象。

比如: 如果var p1=new Person(“xiaoling”,18)

則new會自動: p1._ proto _=Person.prototype。

然後會有以下 結果:

① 凡是這個構造函數創建出的新對象,都是原型對象的孩子(子對象)

②放在原型對象中的屬性值或方法,所有子對象無需重複創建,就可直接使用。

看到這裏,是否有點懵圈呢?別急,我們來畫個圖理一理

如上圖所示:構造函數的prorotype屬性 和proto對象原型指向的是同一個對象。

來證明一下吧在控制台打印一下這段代碼:

1 console.log(Person.prototype === p1.__proto__);//true
2 console.log(Person.prototype === p2.__proto__);//true
3 console.log(Person.prototype === p3.__proto__);//true

 

我們發現結果都是true,那麼說明proto對象原型和原型對象prototype是等價的。或者說他們就是同一個對象,即:

構造函數.prototype === 對應實例對象.proto

4.原型對象模式

前面介紹了很多概念,現在來介紹一下原型對象prorotype集體是怎麼實現的,其實很簡單。

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

還在煩惱搬家費用要多少哪?台中大展搬家線上試算搬家費用,從此不再擔心「物品怎麼計費」、「多少車才能裝完」

構造函數.prototype.方法 = function(){ … }

具體看代碼:

 1       //原型對象方式
 2          function Person(name,age){
 3           this.uname = name;
 4           this.age = age;
 5       }
 6       Person.prototype.intr = function(){
 7               console.log(`我是${this.uname}`);
 8           }
 9         //創建實例
10        var p1 = new Person('xiaoling',18),
11            p2 = new Person('小張',20),
12            p3 = new Person('小劉',23);
13            console.log(p1); //Person {name: "xiaoling", age: 18}
14           console.log(p2); //Person {name: "小張", age: 20}
15           console.log(p3); //Person {name: "小劉", age: 23}

 

我們來看控制台打印:

我們會發現,打印結果並沒有出現intr方法,那我們直接來調用一下試試吧

//調用intr方法
p1.intr()
p2.intr()
p3.intr()

 

控制台結果:

我們發現,居然可以直接調用,這是怎麼回事呢?我們現在把p1,p2,p3在控制台打印的結果展開

我們可以看到每個實例對象都有一個對象原型proto,將它展開就能看到intr方法,難道實例對象調用成功的intr()是proto上的嗎?。我們知道實例對象的proto就是構造函數的prototype,然而我們的intr方法就是定義在Person.prototype上的,所以,實例對象p1,p2,p3能夠成功調用intr方法。

我們來看一下,他們在內存中是怎麼樣的(簡單畫了一下)?

我們可以看到,p1,p2,p3的__proto__通過指針指向原型對象。我們知道原型對象是一個對象,所以intr在原型對象中存儲的只是一個地址,這個地址會通過指針指向intr方法存儲的位置。所以p1,p2,p3調用的intr方法,其實就是原型對象上的intr,他們共享同一個方法,這也正是前面提到的原型對象的作用:共享方法。

這樣4.2提到的浪費內存的問題就完美的解決啦。雖然創建了三個實例對象,但是他們用的都是同一個方法(只佔用了一份內存),就算咱們再創建3000,30000個對象,他們用的還是同一個方法,佔用的內存依然只有一份,內存資源得到了大大的改善和節省,perfect!

那我們再來思考一個問題:p1,p2,p3,自己沒有uname,age,intr這些方法和屬性,為什麼可以使用呢?這就涉及到我們後面會講的繼承以及JavaScript的查找機制規則了.我們後面會說。

5.三者關係

在上文中,我們總是提到構造函數,原型(原型對象),實例,那麼這三者之間到底有什麼樣的關係呢?

目前我們已經知道的關係是:

  • 構造函數.prototype 指向的是原型對象

  • 實例對象的proto指向的也是原型對象

我們再來看一下這三個實例對象的打印結果,上圖

有看到什麼額外的東西嗎?請再仔細看一下!

我們可以看到每個實例對象展開,他裏面都有一個proto,這個不是重點,重點是,每個proto屬性展開,他不僅僅有我們自己定義的方法intr,還有一個淺粉色的constructor屬性,重點是我們可以看到這個這個constructor的值正好是我們的構造函數Person.

咱們來驗證一下,在控制台輸出以下代碼:

1 console.log(Person.prototype.constructor === Person); //true
2 console.log(p1.__proto__.constructor === Person);//true
3 console.log(p2.__proto__.constructor === Person);//true console.log(p3.__proto__.constructor === Person);//true

 

我們得到的結果是4個true,說明我們上文的猜想是正確的,也就是說構造函數也有一個屬性constructor,這個constructor屬性指向的是構造函數。既然構造函數能夠出生的時候就有prototype屬性–創建每個構造函數時,都會自動附贈一個空對象,名為原型對象(prototype),那我們是不是也可以把原型對象上憑空多出來的constructor當成是它天生就有的呢(哎!有些人註定了,出生就是不平凡的,自帶光環)

好了,我們現在再來總結一下:

  • 創建構造函數時,構造函數會附贈一個prototype屬性,這個prototype屬性以指針的方式指向一個對象,這個對象我們稱為原型對象。

  • 原型對象存在的同時,也會自動附贈一個constructor屬性,這個屬性以指針的方式指向他的構造函數。

  • 用構造函數實例化對象時,會通過new這個動作做4件事。

    ①在內存中創建一個新的空對象

    ②讓this指向這個新的對象(自動設置新對象的_ proto _指向構造函數的原型對象——繼承)

    ③執行構造函數裏面的代碼,給這個新對象添加屬性和方法

    ④返回這個新對象(所以構造函數裏面不需要return)

  • 實例對象創建時會自帶一個內置屬性proto,這個proto性會通過指針的方式指向原型對象(我們可以稱為父類)

    再來畫一個圖看看他們之間的關係

再來看一張更詳細的圖

看完這張圖,你是不是又懵圈了呢?沒關係,請跟我一樣拿起筆自己也在紙上畫一畫吧,如果你能畫明白,說明你已經動了一大半了。如果還沒有明白,我們一起再來捋一捋:

  1. 構造函數Person生成的時候會自動存在一個prototype屬性,即Person.prototype,我們稱為原型對象。

  2. 原型對象是一個對象,它存在的同時會自動生成一個constrctor屬性,這個屬性會自動指向他的構造函數,即Person。

  3. 用new生成實例對象時,這個實力對象同時會攜帶一個proto屬性,這個屬性會指向構造函數的原型對象,通過這個proto,實例對象繼承了原型對象上的所有方法,就是可以用原型對象上的方法。

其實不僅僅是實例對象,任何一個對象數據類型都存在這樣的關係。

再來看一張圖,或許你會更加明白

(本圖來自《JavaScript 高級程序設計》的圖 6-1)

好了,構造函數,原型對象,實例對象三者之間的關係就講完了,如果還沒有弄明白,建議多看幾遍,看的同時自己動手在紙上畫一畫哦。

四.JavaScript的成員查找機制

先直接講一下規則:

①當訪問一個對象的屬性(包括方法)時,首先查找這個對象有沒有該屬性。

②如果沒有就查找他的原型(也就是proto指向的prototype原型對象)。

③如果還沒有就查找原型對象的原型(Object的原型對象)。

④以此類推一直到Object為止(null).

proto對象原型的意義就在於為對象成員查找機制提供了一個方向,或者說一條路線。

這一部分看不明白沒關係,先大概知道這個規則就行。

五.原型鏈

在JavaScript中萬物都是對象,對象和對象之間也有關係,並不是孤立存在的。對象之間的繼承關係,在JavaScript中是通過prototype對象指向父類對象,直到指向Object對象為止,這樣就形成了一個原型指向的鏈條,專業術語稱之為原型鏈

舉例說明:person → Person → Object ,普通人繼承人類,人類繼承對象類

當我們訪問對象的一個屬性或方法時,它會先在對象自身中尋找,如果有則直接使用,如果沒有則會去原型對象中尋找,如果找到則直接使用。如果沒有則去原型的原型中尋找,直到找到Object對象的原型,Object對象的原型沒有原型,如果在Object原型中依然沒有找到,則返回null。這也是實例對象p1,p2,p3能夠使用intr()方法的最重要的

上文說到“如果在Object原型中依然沒有找到,則返回null”,這個一起來驗證一下。我們先來看一下在瀏覽器執行的這些代碼:

我們知道任何一個原型對象都是一個對象,而每個對象都有proto屬性,所以Object的原型對象也是一個對象(可以看Object.prototype的打印結果就是一個對象),那麼他的proto就是上圖我們打印的結果null.所以我們得到了驗證結果

即:Object.prototype.proto = null

所以在JavaScript中,Object是一等公民!

現在我們把原型鏈畫出來!

現在再看着這張圖,重新讀一遍第四和第五部分,你就會明白原型鏈了。

六.構造器

1.定義

本文沒有構造器的定義(沒有找到對它的準確定義),真的要說它是什麼只能說:構造函數。

構造函數跟普通函數非常相似,我們已經說過構造函數時一種特殊的函數(第四部分4.2中),我可以通過new關鍵字來使用它們。主要有兩種類型的構造函數,native構造函數(Array,Object等)它們可以在執行環境中自動生成,還有自定義的構造函數,你可以定義自己的方法和屬性,比如我們自定義的Person構造函數。

2.native構造函數

JavaScript中有內置(build-in)構造器/對象共計12個(ES5中新加了JSON):

Object、Function、String、Number、Boolean、Array、RegExp、Data、Error、Math、JSON、Global

3.自定義構造函數

就是我們可以根據需要按照構造函數的方式定義自己的方法和屬性就行。

4.一個重要結論

所有構造器/函數的proto都指向Function.prototype(Function.prototype是一個空函數)

怎麼驗證這句話呢?上代碼:

 1         console.log(Boolean.__proto__ === Function.prototype); //true
 2         console.log(Number.__proto__ === Function.prototype); // true
 3         console.log(String.__proto__ === Function.prototype);  // true
 4         console.log( Object.__proto__ === Function.prototype); // true
 5         console.log(Function.__proto__ === Function.prototype);  // true
 6         console.log(Array.__proto__ === Function.prototype); // true
 7         console.log(RegExp.__proto__ === Function.prototype); // true
 8         console.log(Error.__proto__ === Function.prototype);  // true
 9         console.log(Date.__proto__ === Function.prototype);// true
10 11 12         console.log(Function.prototype);

 

結果:

再來個自定義構造器:

 1          //自定義構造器
 2          //原型對象方式
 3          function Person(name,age){
 4           this.uname = name;
 5           this.age = age;
 6         }
 7         Person.prototype.intr = function(){
 8               console.log(`我是${this.uname}`);
 9           }
10         
11         console.log(Person.__proto__ === Function.prototype);  //true

 

這說明什麼呢?

①JavaScript中的內置構造器/對象共計12個(ES5中新加了JSON).上面列舉了可訪問的9個構造器。剩下如Global不能直接訪問,Arguments僅在函數調用時由JS引擎創建,Math,JSON是以對象形式存在的,無需new。它們的proto是Object.prototype。如下

Math.__proto__ === Object.prototype // true
JSON.__proto__ === Object.prototype // true

②所有的構造器都來自於Function.prototype,甚至包括根構造器Object及Function自身。所有構造器都繼承了Function.prototype的屬性及方法。如length、call、apply、bind(ES5)。

③Function.prototype也是唯一一個typeof XXX.prototype為 “function”的prototype。其它的構造器的prototype都是一個對象。

    
 1 console.log(typeof Function.prototype) // function
 2 console.log(typeof Object.prototype)   // object
 3 console.log(typeof Number.prototype)   // object
 4 console.log(typeof Boolean.prototype)  // object
 5 console.log(typeof String.prototype)   // object
 6 console.log(typeof Array.prototype)    // object
 7 console.log(typeof RegExp.prototype)   // object
 8 console.log(typeof Error.prototype)    // object
 9 console.log(typeof Date.prototype)     // object
10 console.log(typeof Object.prototype)   // object

 

④除了Function.prototype,所有構造函數的prototype都是一個對象

5.一等公民

前面原型鏈部分我們知道Objec是一等公民,那其實Function也是。

我們已經知道了所有構造器(含內置及自定義)的proto都是Function.prototype,那Function.prototype的proto是誰呢?

console.log(Function.prototype.__proto__ === Object.prototype) // true

這說明所有的構造器也都是一個普通JS對象,可以給構造器添加/刪除屬性等。同時它也繼承了Object.prototype上的所有方法:toString、valueOf、hasOwnProperty等。

最後再提一次:Object.prototype的proto是誰?

前面我們已經驗證過是null,到頂了

Object.prototype.__proto__ === null // true

講到這裏,是不是又有點懵圈了呢?上圖幫助你消化吧

看圖,一起理一理:

  • 每個構造函數(不管是內置構造函數還是自定義的構造函數),他的proto都指向Function.Prototype,包括Function他自己(Math和JSON不算在裏面,因為他們的proto指向的是Object.prototype)。

  • 每個構造函數都有一個內置屬性即prototype,他們指向自己的原型對象,除了Function的原型對象是一個空函數外,所有構造函數的prototype都是一個對象。

  • 每個原型對象都有一個內置屬性constructor,屬性,constructor屬性指回原型對象的屬性

  • Object是頂級對象,Object.prototype.proto為null

  • 每個實例對象都有一個proto屬性,通過這個proto屬性,這個實例對象可以按照JavaScript的成員查找規則(本文第四部分),去使用原型鏈上的屬性和方法,也就是我們說的繼承父類的屬性和方法的本質。這也是我們隨便創建一個數組或者對象,能夠使用toString()/valueOf() pop()/filter()/sort()等方法的原因。

現在再看這張圖明白了嘛,回國頭再去看一看第四部分:JavaScript的成員查找規則 是不是也頓悟了很多。

七.一張手繪原型鏈

請忽略我醜陋的字跡,能看懂這張圖並且自己可以畫出來,說明你今天的收穫非常大哦~

 

到這裏我們就講完原型和原型鏈相關的內容了,可能還有些地方沒講到,大家就自行下去再研究了。

另外建議紅寶書和你不知道系列多看幾遍(「書讀百遍其義自見」是很有道理的)。

再推薦幾位前輩寫的不錯的文章,值得多讀幾遍:

[
​最詳盡的 JS 原型與原型鏈終極詳解,沒有「可能是」]  https://www.jianshu.com/p/dee9f8b14771  [
​javascript中構造器(函數)的__proto__與prototype初探]  https://www.cnblogs.com/webjoker/p/5319377.html 

 

後記

非常感謝大家能夠認真看完這篇文章。其實在提筆寫這篇文章之前,小玲的內心是非常緊張和忐忑的,因為害怕自己研究的不夠深入和全面,但是一想到可以和大家分享我的收穫,還是非常激動和興奮的。可能有些地方講的還不夠清楚,大家可以自己多思考一下,你自己思考想出來的東西,比別人灌輸給你的更能讓你記憶深刻。如果,若有某個地方存在問題或者不明白的,歡迎大家积極提出寶貴的建議和見解!

 

參考文章:

https://www.cnblogs.com/TRY0929/p/11870385.html

https://www.jianshu.com/p/dee9f8b14771

https://www.cnblogs.com/snandy/archive/2012/09/01/2664134.html

https://www.cnblogs.com/webjoker/p/5319377.html

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

※推薦台中搬家公司優質服務,可到府估價

台中搬鋼琴,台中金庫搬運,中部廢棄物處理,南投縣搬家公司,好幫手搬家,西屯區搬家

PAT 1033 To Fill or Not to Fill (25分) 貪心思想_台中搬家公司

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

擁有後台管理系統的網站,將擁有強大的資料管理與更新功能,幫助您隨時新增網站的內容並節省網站開發的成本。

題目

With highways available, driving a car from Hangzhou to any other city is easy. But since the tank capacity of a car is limited, we have to find gas stations on the way from time to time. Different gas station may give different price. You are asked to carefully design the cheapest route to go.

Input Specification:
Each input file contains one test case. For each case, the first line contains 4 positive numbers: C​max​​ (≤ 100), the maximum capacity of the tank; D (≤30000), the distance between Hangzhou and the destination city; D​avg​​ (≤20), the average distance per unit gas that the car can run; and N (≤ 500), the total number of gas stations. Then N lines follow, each contains a pair of non-negative numbers: P​i​​ , the unit gas price, and D​i​​ (≤D), the distance between this station and Hangzhou, for i=1,⋯,N. All the numbers in a line are separated by a space.

Output Specification:
For each test case, print the cheapest price in a line, accurate up to 2 decimal places. It is assumed that the tank is empty at the beginning. If it is impossible to reach the destination, print The maximum travel distance = X where X is the maximum possible distance the car can run, accurate up to 2 decimal places.

Sample Input 1:
50 1300 12 8
6.00 1250
7.00 600
7.00 150
7.10 0
7.20 200
7.50 400
7.30 1000
6.85 300
Sample Output 1:
749.17
Sample Input 2:
50 1300 12 2
7.10 0
7.00 600
Sample Output 2:
The maximum travel distance = 1200.00

題目解讀

題目大意:汽車從杭州出發可以通過高速公路去任何城市,但是油箱的容量是有限的,路上有很多加油站,每個加油站的價格不同,為汽車設計一個從杭州到終點的最便宜的加油策略。

輸入:第一行:Cmax表示油箱最大容量,D表示杭州到目的地的距離,Davg表示平均每單位的汽油可以讓汽車行駛的距離,N表示途中加油站的數量;接下來 N 行:給出給個加油站的單位油價Pi和杭州(起點)到這個站點的距離Di

※推薦台中搬家公司優質服務,可到府估價

台中搬鋼琴,台中金庫搬運,中部廢棄物處理,南投縣搬家公司,好幫手搬家,西屯區搬家

輸出:求汽車從杭州到終點的最少花費(精確到兩位小數)。如果不能夠到達,就輸出汽車能夠行駛的最大距離(精確到兩位小數)。

思路分析

核心思想:貪心算法(每次尋找局部最優,在最便宜的加油站加最多的油)。

前期準備:

  • 最終輸出無論是距離還是價格都要求精確到兩位小數,雖然從給出的輸入數據來看,距離、郵箱容量等好像都是整數,但為了操作方便,避免運算過程精度丟失,我們全都用double保存。(再說了,它給出的數據說不定就是坑你的呢?)
  • 設置結構體數組保存每個加油站的單價和到杭州的距離。
  • 按照到杭州的距離對結構體數組排序,因為輸入是無序的。
  • 排序完判斷第1個結構體到杭州的距離是否為0,也就是說最近的加油站是不是在起點。因為題目說了假定剛開始郵箱沒有油,那麼如果起點處沒有加油站,就比欸想開車了,直接輸出並返回吧。

貪心核心:怎麼實現每次都做出局部最優的選擇?

對於任意一個站點:如果我們在這個站點加滿油,那麼最多就可以跑cmax*davg的距離,我們對這個距離段中遇到的加油站情況進行分析:

  • 按順序遍歷【當前位置,當前位置+cmax*davg】中的所有加油站,如果某個加油站的收費低於當前站點,那麼我就在當前站點加油,跑到那個站點去,加多少呢?就加能恰好讓我到達那個加油站的油。這樣我去那個站點加油就能更便宜。
  • 如果當前站點後面有不止一個站點更便宜,怎麼選擇?比如我現在在A,價格是10,後面是 B 價格9, 後面是C 價格8, 我先找到的是B,那我就退出本次循環,剛好加油跑到B去,在B處重新繼續分析。為啥不直接加油去C,如果從當前位置直接加油去C,那麼BC之間的花費單價是當前加油站的價格也就是10,但我如果先去了B,那麼從B到C的油價就是B處的價格9,顯然更便宜。這樣才滿足局部最優。
  • 如果當前位置後面沒有更便宜的加油站呢?
    • 如果我在當前位置最多能達到的最遠距離超過了終點,那麼我直接加油跑到終點,因為後面的站點只會更貴。
    • 如果我不能直接到終點,那麼我肯定是需要加油的,那我就找盡可能地找比較便宜的那個加油站,在當前加油站加滿油之後過去。既然沒有比當前更低價格的了,就讓油箱加到最大值,這樣能保證利益最大化,保證最大的距離使用的是便宜的油。
  • 如果當前位置不能直接到達終點,並且後面沒有加油站了呢?那麼肯定不能到達中終點了,只能到達當前位置+cmax*davg,也就是說在當前位置加滿,能跑多遠是多遠。

總結:

  • 當前位置能到達的範圍中如果存在更便宜的加油站,就加合適的油剛好到達那個加油站。
  • 如果不存在更便宜的,但是當前位置能直接到達終點,那就加合適的油到達終點。
  • 不存在更便宜的,並且不能直接到終點,找到可達的加油站的相對而言最便宜那個,在當前位置加滿油,然後去那個站點。
  • 當前位置不能到終點,並且後面沒有加油站了,輸出能到達的最大距離。

代碼

#include <iostream>
#include <algorithm>
using namespace std;

struct Station {
    // 每單位汽油價格
    double price;
    // 從起點到這裏的距離
    double dis;
}stat[500];

// 對加油站 離起點 從近到遠 排序
bool cmp(Station a, Station b) {
    return a.dis < b.dis;
}

int main() {
    // cmax油箱容量 d 距離 davg 每單位油能跑多遠 n個加油站
    double cmax, d, davg;
    int n;
    cin >> cmax >> d >> davg >> n;
    // 每個加油站的 價格 位置
    for (int i = 0; i < n; ++i) 
        cin >> stat[i].price >> stat[i].dis;
    // 根據離起點的距離排序
    sort(stat, stat + n, cmp);
    // 第一個加油站不在起點,無法起步
    if(stat[0].dis != 0) {
        printf("The maximum travel distance = 0.00");
        return 0;
    } 
    // nowpos當前在哪個加油站,
    int nowpos = 0;
    // nowgas 當前剩餘多少油,total_price 總花費
    double nowgas = 0.00, total_price = 0.00;
    // 油箱加滿一次油,可以跑多遠
    double each_max_run_dis = cmax * davg;
    // 是否到達終點
    bool arrived = false;
    while (!arrived) {
        // 遍歷 【從當前加油站到最遠能跑到的位置】 之間的 全部加油站
        bool exist_stat  = false; // 當前位置後面是否存在可達加油站
        bool exist_cheaper_stat = false; // 是否存在比當前位置更便宜的加油站
        // 不存在比當前便宜的,就找到其中價格最低的
        int min_pos = -1; double min_price = 9999.99;
        for (int i = nowpos + 1; i < n; ++i) {
            // 當前位置到不了這個加油站,退出循環
            if (stat[i].dis - stat[nowpos].dis > each_max_run_dis) {
                // 最多還能走 nowgas * davg
                break;
            }
            exist_stat = true; // 存在可達加油站
            // 如果有比當前加油站價格更低的加油站
            // 算一下恰好跑到那個加油站需要多少油,
            // 加油跑到那個加油站
            if (stat[i].price < stat[nowpos].price) {
                // 設置標誌
                exist_cheaper_stat = true;
                double needgas = (stat[i].dis - stat[nowpos].dis) / davg - nowgas;
                // 加這麼多油,剛好跑到那個加油站,算一下花費
                total_price += stat[nowpos].price * needgas;
                // 到達那個位置后剩餘油量歸0
                nowgas = 0;
                // 改變位置
                nowpos = i;
                // 不再遍歷後面的加油站
                // 比如我現在 在A,價格是10,後面是 B 價格9, 後面是C 價格8
                // 我先找到的是B,那我就剛好加油跑到B去,在B處重新考慮
                // 為啥不直接加油去C,如果從當前位置直接加油去C,那麼BC之間的花費單價是當前加油站的價格也就是10
                // 但我如果先去了B,那麼從B到C的油價就是B處的價格9,顯然更便宜
                // 這樣才滿足局部最優
                break;
            }
            // 如果說我能從當前位置跑1000米,但是在此之間的加油站的價格沒有一個比我現在的價格低
            // 那我就盡量找最便宜的那個,然後在當前位置加滿油,跑到相對而言最便宜的那個加油站去加油
            // 這樣才滿足局部最優(在最便宜的位置加更多的油跑最多的距離)
            // 這個if不會和上面的if同時執行(上面執行完就break了),所以不用加else
            if (stat[i].price < min_price) {
                min_pos = i;
                min_price = stat[i].price;
            }
        }
        // 不存在比當前便宜的,但是當前位置最遠能達到終點
        if (!exist_cheaper_stat && (d - stat[nowpos].dis <= each_max_run_dis)) {
            double needgas = (d - stat[nowpos].dis) / davg - nowgas;
            // 加這麼多油,剛好跑到終點,算一下花費
            total_price += stat[nowpos].price * needgas;
            // 到達終點
            arrived = true;
            break;
        }
        // 不存在比當前便宜的,但是找到了其他加油站中相對最便宜那個
        if (!exist_cheaper_stat && exist_stat) {
            // 後面有加油站,但是都比當前位置的加油站貴
            // 那我就盡量找最便宜的那個,然後在當前位置加滿油,跑到相對而言最便宜的那個加油站去加油
            // 這樣才滿足局部最優(在最便宜的位置加更多的油跑最多的距離)
            double needgas = cmax - nowgas;
            // 在當前位置加滿油,算一下花費
            total_price += stat[nowpos].price * needgas;
            // 到達那個位置后的剩餘油量
            nowgas = cmax - (stat[min_pos].dis - stat[nowpos].dis) / davg;
            // 改變位置
            nowpos = min_pos;
        // 當前位置無法抵達下一個加油站
        } else if (!exist_stat){
            // 最多還能走 cmax * davg
            printf("The maximum travel distance = %.2f", stat[nowpos].dis + each_max_run_dis);
            return 0;
        }
    }
    // while正常結束,說明到達終點
    printf("%.2f", total_price);
    return 0;
}

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

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

還在煩惱搬家費用要多少哪?台中大展搬家線上試算搬家費用,從此不再擔心「物品怎麼計費」、「多少車才能裝完」

記一次接口性能優化實踐總結:優化接口性能的八個建議_台中搬家公司

※推薦台中搬家公司優質服務,可到府估價

台中搬鋼琴,台中金庫搬運,中部廢棄物處理,南投縣搬家公司,好幫手搬家,西屯區搬家

前言

最近對外接口偶現504超時問題,原因是代碼執行時間過長,超過nginx配置的15秒,然後真槍實彈搞了一次接口性能優化。在這裏結合優化過程,總結了接口優化的八個要點,希望對大家有幫助呀~

  • 數據量比較大,批量操作數據入庫
  • 耗時操作考慮異步處理
  • 恰當使用緩存
  • 優化程序邏輯、代碼
  • SQL優化
  • 壓縮傳輸內容
  • 考慮使用文件/MQ等其他方式暫存,異步再落地DB
  • 跟產品討論需求最恰當,最舒服的實現方式

嘻嘻,先看一下我們對外轉賬接口的大概流程吧

1.數據量比較大,批量操作數據入庫

優化前:

//for循環單筆入庫
for(TransDetail detail:list){
  insert(detail);  
}

優化后:

// 批量入庫,mybatis demo實現
<insert id="insertBatch" parameterType="java.util.List">
insert into trans_detail( id,amount,payer,payee) values
 <foreach collection="list" item="item" index="index" separator=",">(
    #{item.id},	#{item.amount},
    #{item.payer},#{item.payee}
  )
</foreach>
</insert>

性能對比:

單位(ms) for循環單筆入庫 批量入庫
500條 1432 1153
1000條 1876 1425

解析

  • 批量插入性能更好,更加省時間,為什麼呢?
打個比喻:假如你需要搬一萬塊磚到樓頂,你有一個電梯,電梯一次可以放適量的磚(最多放500),
你可以選擇一次運送一塊磚,也可以一次運送500,你覺得哪種方式更方便,時間消耗更少?

2.耗時操作考慮異步處理

耗時操作,考慮用異步處理,這樣可以降低接口耗時。本次轉賬接口優化,匹配聯行號的操作耗時有點長,所以優化過程把它移到異步處理啦,如下:

優化前:

優化后

匹配聯行號的操作異步處理

性能對比:

假設一個聯行號匹配6ms

同步 異步
500條 3000ms ~
1000條 6000ms ~

解析:

  • 因為聯行號匹配比較耗時,放在異步處理的話,同步聯機返回可以省掉這部分時間,大大提升接口性能,並且不會影響到轉賬主流程功能。
  • 除了這個例子,平時我們類似功能,如用戶註冊成功后,短信郵件通知,也是可以異步處理的,這個優化建議香餑餑的~
  • 所以,太耗時的操作,在不影響主流程功能的情況下,可以考慮開子線程異步處理的啦。

3.恰當使用緩存

在適當的業務場景,恰當地使用緩存,是可以大大提高接口性能的。這裏的緩存包括:Redis,JVM本地緩存,memcached,或者Map等。

這次轉賬接口,使用到緩存啦,舉個簡單例子吧~

優化前

以下是輸入用戶賬號,匹配聯行號的流程圖

優化后:

恰當使用緩存,代替查詢DB表,流程圖如下:

解析:

  • 把熱點數據放到緩存,不用每次查詢都去DB拉取,節省了這部分查SQL的耗時,美滋滋呀~
  • 當然,不是什麼數據都適合放到緩存的哦,訪問比較頻繁的熱點數據才考慮緩存起來呢~

4. 優化程序邏輯、代碼

優化程序邏輯、程序代碼,是可以節省耗時的。

我這裏就本次的轉賬接口優化,舉個例子吧~

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

還在煩惱搬家費用要多少哪?台中大展搬家線上試算搬家費用,從此不再擔心「物品怎麼計費」、「多少車才能裝完」

優化前:

優化前,聯行號查詢了兩次(檢驗參數一次,插入DB前查詢一次),如下偽代碼:


punlic void process(Req req){
  //檢驗參數,包括聯行號(前端傳來的payeeBankNo可以為空,但是如果後端沒匹配到,會拋異常)
   checkTransParams(Req req);
   //Save DB
  saveTransDetail(req); 
}

void checkTransParams(Req req){
    //check Amount,and so on.
    checkAmount(req.getamount);
    //check payeebankNo
    if(Utils.isEmpty(req.getPayeeBankNo())){
        String payeebankNo = getPayeebankNo(req.getPayeeAccountNo);
        if(Utils.isEmpty(payeebankNo){
            throws Exception();
        }
    }
}

int saveTransDetail(req){
    String payeebankNo = getPayeebankNo(req.getPayeeAccountNo);
    req.setPayeeBankNo(payeebankNo);
    insert(req);
    ...
}

優化后:

優化后,只在校驗參數的時候插敘一次,然後設置到對象裏面~ 入庫前就不用再查啦,偽代碼如下:

void checkTransParams(Req req){
    //check Amount,and so on.
    checkAmount(req.getamount);
    //check payeebankNo
    if(Utils.isEmpty(req.getPayeeBankNo())){
        String payeebankNo = getPayeebankNo(req.getPayeeAccountNo);
        if(Utils.isEmpty(payeebankNo){
            throws Exception();
        }
    }
    //查詢到有聯行號,直接設置進去啦,這樣等下入庫不用再插入多一次
    req.setPayeeBankNo(payeebankNo);
}

int saveTransDetail(req){
    insert(req);
    ...
}

解析:

  • 對於優化程序邏輯、代碼,是可以降低接口耗時的。以上demo只是一個很簡單的例子,就是優化前payeeBankNo查詢了兩次,但是其實只查一次就可以了。很多時候,我們都知道這個點,但就是到寫代碼的時候,又忘記了呀~所以,寫代碼的時候,留點心吧,優化你的程序邏輯、代碼哦。
  • 除了以上demo這點,還有其它的點,如優化if複雜的邏輯條件,考慮是否可以調整順序,或者for循環,是否重複實例化對象等等,這些適當優化,都是可以讓你的代碼跑得更快的。

之前我這篇文章,也提了幾個優化點噢,有興趣的朋友可以看一下哈~

寫代碼有這些想法,同事才不會認為你是複製粘貼程序員

5. 優化你的SQL

很多時候,你的接口性能瓶頸就在SQL這裏,慢查詢需要我們重點關注的點呢。

我們可以通過這些方式優化我們的SQL:

  • 加索引
  • 避免返回不必要的數據
  • 優化sql結構
  • 分庫分表
  • 讀寫分離

有興趣的朋友可以看一下我這篇文章呢,很詳細的SQL優化點:

後端程序員必備:書寫高質量SQL的30條建議

6.壓縮傳輸內容

壓縮傳輸內容,文件變得更小,因此傳輸會更快啦。10M帶寬,傳輸10k的報文,一般比傳輸1M的會快呀;打個比喻,一匹千里馬,它馱着一百斤的貨跑得快,還是馱着10斤的貨物跑得快呢?

解析:

  • 如果你的接口性能不好,然後傳輸報文比較大的話,這時候是可以考慮壓縮文件內容傳輸的,最後優化效果可能很不錯哦~

7. 考慮使用文件/MQ等其他方式暫存數據,異步再落地DB

如果數據太大,落地數據庫實在是慢的話,可以考慮先用文件的方式保存,或者考慮MQ,先落地,再異步保存到數據庫~

本次轉賬接口,如果是併發開啟,10個併發度,每個批次1000筆數據,數據庫插入會特別耗時,大概10秒左右,這個跟我們公司的數據庫同步機制有關,併發情況下,因為優先保證同步,所以并行的插入變成串行啦,就很耗時。

優化前:

優化前,1000筆先落地DB數據庫,再異步轉賬,如下:

優化后:

先保存數據到文件,再異步下載下來,插入數據庫,如下:

解析:

  • 如果你的耗時瓶頸就在數據庫插入操作這裏了,那就考慮文件保存或者MQ或者其他方式暫存吧,文件保存數據,對比一下耗時,有時候會有意想不到的效果哦。

8.跟產品討論需求最恰當,最舒服的實現方式

這點個人覺得還是很重要的,有些需求需要好好跟產品溝通的。

比如有個用戶連麥列表展示的需求,產品說要展示所有的連麥信息,如果一個用戶的連麥列表信息好大,你拉取所有連麥數據回來,接口性能就降下來啦。如果產品打樁分析,會發現,一般用戶看連麥列表,也就看前幾頁因此,奸笑,哈哈 其實,那個超大分頁加載問題也是類似的。即limit +一個超大的數,一般會很慢的~~

總結

本文呢,基於一次對外接口耗時優化的實踐,總結了優化接口性能的八個點,希望對大家日常開發有幫助哦~嘻嘻,有興趣可以逛逛我的github哈,本文會收藏到github里滴哈

https://github.com/whx123/JavaHome

公眾號

  • 歡迎關注我個人公眾號,交個朋友,一起學習哈~

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

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

擁有後台管理系統的網站,將擁有強大的資料管理與更新功能,幫助您隨時新增網站的內容並節省網站開發的成本。

廣場舞,俘獲多少人的心_台中搬家公司

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

還在煩惱搬家費用要多少哪?台中大展搬家線上試算搬家費用,從此不再擔心「物品怎麼計費」、「多少車才能裝完」

“我和我的祖國”全國廣場舞成果彙報展演現場 傅德偉 攝

沒有哪個舞蹈像廣場舞有着這樣廣泛的群眾基礎,不管是在四季分明的北方,還是綠樹常青的南方,不管是城市還是鄉村,只要有一塊空地,都可以見到隨着音樂翩翩起舞的人群。簡單易學、沒有門檻、有益身心……廣場舞以親民的姿態走進了大眾的視野。

近些年,隨着國家相關政策的推出,廣場舞擾民等問題已不再突出。健康有序、积極向上的廣場舞正在煥發新的光彩,它將如何俘獲億萬國民的心?

“大媽”標籤漸漸淡去

廣場舞還是大媽們的專利么?如果你還這樣認為,你就out啦。

雖然,廣場舞的潮流前線中,大多是大媽們的身影,但隨着我國經濟水平提高,社會老齡化加劇,基本生活得到保障的老年人退休后,有了大把的空閑時間。基於鍛煉身體的需要,脫胎於健身操的廣場舞越來越受到老年人垂青,不僅有大媽,大爺們也越來越活躍在舞蹈的人群中。廣場舞正在籠絡男性以及其他年齡層的“人心”。

記者在“我和我的祖國”——文化新生活全國廣場舞展演中見到了來自河北張家口的武志剛,他所在的廣場舞隊伍中男性佔據了近三分之一。為什麼會跳廣場舞?武志剛說,以前男性退休后,大多打打牌、遛遛鳥,或者宅在家中。看着跳廣場舞的人每天都朝氣蓬勃的,他們也就加入進來,一可以鍛煉身體,二可以結識朋友,逐漸找到了屬於自己的朋友圈。

“廣場舞=大媽們的運動”這種傳統印象正在改變,全國各地的年輕人也開始對廣場舞趨之若鶩。街舞、拉丁舞、爵士舞等流行舞種被年輕人帶到廣場。江西省贛州市上猶縣文化館青年藝術團的16歲女孩張萍,通過跳舞來傾訴內心。為了提升舞蹈水平,她加入廣場舞團隊,在這裏既體味到長輩們的關愛,也可以學到新的舞蹈技巧。

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

擁有後台管理系統的網站,將擁有強大的資料管理與更新功能,幫助您隨時新增網站的內容並節省網站開發的成本。

近些年,青少年在廣場舞比賽中越來越頻繁地現身。據記者了解,青少年通過參加比賽增加了自我鍛煉和自我展示的機會,“多一個舞台,多一次鍛煉,就可以離自己的舞蹈夢想更近一步。”這是他們共同的想法。

健身需求逐漸多元

對跳廣場舞的老年群體進行調研,你會發現,他們選擇跳廣場舞不僅是為了健身,更重要的是可以“健心”。

退休后的生活單調而孤獨,不是圍著兒孫轉,就是抬腳跑醫院,老年人逐漸在單一的生活中失去了自己的社交圈,這是現代社會中需要關注的問題之一。作為一種集體舞蹈,廣場舞帶給老年人集體的溫暖,在一幫志同道合的舞友中,老年人獲得的不僅是身心愉悅,更是社會歸屬感的重拾。

令人驚喜的是,年輕人除了通過跳舞釋放壓力外,他們走上廣場還與街舞的流行有關:2018年,中國首部原創交響樂街舞作品《黃河》在“荷花獎”當代舞、現代舞評選中榮獲“當代舞獎”;北京舞蹈學院成立了中國街舞文化研究中心;《這就是街舞》《熱血街舞團》等多款街舞綜藝節目大熱;大學校園裡,街舞社團也越來越多……街舞場所與廣場舞場所的重合,是廣場上出現年輕人的重要原因。

從“草台”跳上了舞台

經過多年的發展,由群眾自發組織並在大江南北迅速風靡的廣場舞,已經不只是在公園和廣場上簡單呈現,它們登上了更大的舞台綻放魅力。

成立於2000年的湖北武漢的农民藝術團,是一個平均年齡55歲的群眾廣場舞團隊。最初他們只是在公園裡、廣場上進行活動,如今,由於廣場舞的火熱,他們多次受邀參加活動,足跡遍及北京、上海、香港、澳門等城市。中國舞蹈家協會民族民間舞蹈專業委員會副主任阮蘭玉說,廣場舞不僅體現出基層的文化,更能展現出群眾积極向上、蓬勃活躍的精神面貌。隨着新時代老百姓對生活的新期待和對美的追求的提升,他們更希望走上更大的舞台。

追求藝術性將成未來發展趨勢

對廣場舞未來的發展,舞者與專家各有期許。來自北京的蔣艷芝希望將來的廣場舞有更多專業老師的指導,“草台班子”已經不能滿足她們的需求,她們想要向更高水平邁進。來自江西的朱麗娟認為,越來越多的人加入廣場舞隊伍,需要充足的場地和空間以供使用。中國文化館協會舞蹈委員會主任委員、江蘇省南通市文化館館長曹錦揚則認為,廣場舞是一種表演形式,不是一個舞種。只要動作優美、音樂悅耳、風格濃郁、難易適度就可以歸入廣場舞。集藝術性、審美性、民族性、風格性、娛樂性、科學性為一體,一定是未來廣場舞的發展方向。(記者 杜潔芳 王彬)

本站聲明:網站內容來http://www.societynews.cn/html/wh/fq/,如有侵權,請聯繫我們,我們將及時處理

※推薦台中搬家公司優質服務,可到府估價

台中搬鋼琴,台中金庫搬運,中部廢棄物處理,南投縣搬家公司,好幫手搬家,西屯區搬家

還在花錢加裝導航?教授實測哪家手機導航最好用!_台中搬家公司

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

還在煩惱搬家費用要多少哪?台中大展搬家線上試算搬家費用,從此不再擔心「物品怎麼計費」、「多少車才能裝完」

實際上,這幾款比較熱門的導航軟件,無論是定位的精準性還是導航時的精準性,都是值得肯定的,這幾位选手最大的區別,莫過於在路線、額外服務、以及使用便利性上。搜狗地圖推薦指數:在使用軟件過程中,你能發現搜狗地圖的界面設計十分簡潔,在主界面上所能用到功能主要有4個,分別是身邊服務、路線查詢、導航功能、軟件服務。

前言

認為,導航已經成為了我們生活息息相關的一個軟件,隨着導航軟件不斷地更新、換代,無論是我們駕車、還是坐公共交通工具、還是步行或者打車,一個導航軟件就能幫到你,然而市面上哪一款導航使用最為方便好用?為此特意將市面上使用最多的導航軟件下載到了手機上,進行了為期幾天的日常體驗。

從汽車的角度先來說說導航的發展,首先汽車上也具備GpS導航功能,但是使用效率比較低,而且帶有GpS導航功能的一般都要高配車型甚至是頂配車型。可以這麼說,其實買高配的人並不多,所以路上跑的車在原廠狀況下基本是不具備導航功能。

雖然在馬雲家的商店裡有着格式各樣的導航配件,從幾百到幾千元,讓人挑得眼花繚亂。但是這一行水很深,市場上充斥着大量的三無產品、山寨產品,先不說屏幕的分辨率低的問題,而且系統大多都是卡頓、反應緩慢,提供的導航軟件在實際體驗上更是慘不忍睹,也可以這麼說,為什麼現在導航市場還是這麼火熱?在看來大多數車主都是為了那一塊大屏幕。中控加裝一個大屏幕怎麼也比光禿禿的收音機更好看,更高檔次。

拋開山寨廠導航機的問題,雖然車載導航最大的問題就是不能像手機那樣更新迅速,而且更新還得跑回4S店更新有可能要收錢以外,相比手機導航,車載導航信號更為穩定,就算在部分較為偏僻地方手機沒信號,這時候航依舊會擔演一個重要的角色。

相比那一個功能少、反應遲鈍、更新慢的車載導航,手機導航更為靈活。你只需要用一個手機架,就能把手機固定在一個你喜歡的位置,成為導航的角色。另外對於手機這一種聯網的產物來說,導航是可以進行實時更新,並且能夠讓車主知道前方路況。

現在很多比較年輕的司機朋友都喜歡開車就開着導航,然而對於很大一部分老司機來說,實際上對於這一種軟件是持有一種保留的意見。最關鍵的一點就是他們總是認為導航總是繞遠路。又或者去同一個目的地,導航所指路線跟自己預想的不一樣,於是認為導航不準確,實際上雖然導航有可能帶你走遠了幾公里,卻能準確帶你到達目的地。但如果你對道路不熟悉,你卻有可能要多跑上幾十公里,又浪費更多的時間,這就划算了?

回到正題,為了能夠更好地將使用感受告訴大家,已經將App軟件中心下載了那幾款最實用、使用率最高的幾款導航軟件:搜狗地圖、騰訊地圖、高德地圖、百度地圖、蘋果地圖。

實際上,這幾款比較熱門的導航軟件,無論是定位的精準性還是導航時的精準性,都是值得肯定的,這幾位选手最大的區別,莫過於在路線、額外服務、以及使用便利性上。

搜狗地圖

推薦指數:

在使用軟件過程中,你能發現搜狗地圖的界面設計十分簡潔,在主界面上所能用到功能主要有4個,分別是身邊服務、路線查詢、導航功能、軟件服務;在實際體驗中,在導航界面清晰簡潔,而且提供了導航路段的擁堵信息,實際使用感覺良好;另外在軟件服務中,還提供違章查詢、繳款功能、網約車功能、地鐵路線功能、以及霧霾地圖,在功能完善度方面還算齊全。在最常用的導航功能中,在實際使用上跟普通導航並沒太大區別,

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

擁有後台管理系統的網站,將擁有強大的資料管理與更新功能,幫助您隨時新增網站的內容並節省網站開發的成本。

無論是路口提醒還是測速提醒也都清晰地提示,而且在行駛過程中路面上的路況,譬如塞車,均會提醒。但是在計劃路線的時間上,搜狗地圖所預計的時間都會偏長。

騰訊地圖

推薦指數:

作為騰訊出品的導航軟件在設計方面依舊走簡約風格,但是與上一款導航軟件有別,最常用到的導航地圖功能擺在了最顯眼的位置,使用頻率低的周邊服務等則被放到了發現功能的界面上。另外,在功能服務上,具備違章查詢功能(不具備代繳款功能),網約車服務。在最常使用導航功能,整體布局簡潔、清晰,但是提醒項目卻很多,譬如路上的加油站地點、測速攝像頭都會在路線上表明,另外在攝像頭圖標上還會標明限速標誌,對於我們的行駛時非常有幫助的。另外地圖還提供电子狗功能,實際使用效果一樣好用。

高德地圖

推薦指數:

高德地圖相信是這幾款導航中使用群眾比較多的一款軟件,在實際體驗上,高德地圖無論是實時路況還是時間計算上,都算準確,但是在軟件操作方面,雖然高德地圖都追求簡約,但是在實際操作方面卻發現軟件有種難以下手的感覺,需要一定的時間熟練。但是這款產品的功能非常全面,不但能根據不同交通工具提供了多種路線方案,而且還支持語音導航,能夠即時提示攝像頭、擁堵狀況的路況信息,駕駛時還是比較省心。

百度地圖

推薦指數:

實際上,在體驗各導航軟件時,所使用的導航軟件便是百度地圖,在操作界面方面,認為百度地圖更為簡便,而且每次使用所以提供的路線以及路面上的事實路況均比較準確,在功能性軟件方面也比較齊全,百度地圖也可以說是能媲美高德地圖的一個存在。但是由於使用時間比較長,卻發現百度地圖每月總有幾天的某個時段(譬如上下班時間)是搜索不到衛星信號的,這是扣分項。但是百度地圖卻又一個模式叫做路線雷達模式,在清楚路線的情況下能供更多的路線選擇,並能標出時差。在實用性功能上百度地圖和高德地圖一樣都有語音包選擇,而且語音包豐富度很高,只是語音包並不能完全更換系統聲音,而是在特定的情況譬如超速、前方有測速攝像頭這些特定情況語音包才會起到作用,最後成為一個導航兩種聲音。

蘋果地圖

推薦指數:

作為唯一一款手機原生導航,蘋果地圖可以是做得相當牛逼,整一個界面設計極致簡單,而且操作簡便,而且作為IOS系統下專屬導航,整個界面流暢、功能操作簡便,另外蘋果地圖還提供廣東話語音提示,對於廣東的朋友確實比較體貼。

總結

各款導航都能很好地提供實時交通信息,而提示都會使用綠色、黃色、紅色、深紅色…,只遇過最塞的就是深紅色,至於有沒有更深的紅色,還待探究。而經過多天的使用,得出綠色、黃色路況都是能夠選擇的,而遇到紅色路況也不用左過多的擔心(都是小塞一下),但是碰到深紅色,那就得小心,有可能你會為此塞上一段時間。總結這麼多款導航,其實最好使的,認為還是屬於高德地圖、蘋果地圖兩個,首要原因那就是各大車型都開始普及互聯繫統,先有蘋果的Carplay,再有安卓的AppLink等等一些列的互聯繫統,能夠將特定的導航軟件映射到中控大屏幕上,所以哪一款導航軟件支持汽車中控屏的映射功能,註定受眾更大。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

※推薦台中搬家公司優質服務,可到府估價

台中搬鋼琴,台中金庫搬運,中部廢棄物處理,南投縣搬家公司,好幫手搬家,西屯區搬家

線性結構隊列以及應用(上)_台中搬家公司

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

擁有後台管理系統的網站,將擁有強大的資料管理與更新功能,幫助您隨時新增網站的內容並節省網站開發的成本。

隊列Queue:什麼是隊列?

對列是一種有次序的數據集合,其特徵是新數據項的添加總發生在一端(通常稱為“尾rear”端),而現存數據項的移除總發生在另一端(通常稱為“首front”端)

當數據項加入隊列,首先出現在隊尾,隨着隊首數據項的移除,它逐漸接近隊首。新加入的數據項必須在數據集末尾等待,而等待時間最長的數據項則是隊首。這種次序安排的原則稱為:先進先出。

隊列的例子出現在我們日常生活的方方面面:水管,排隊;隊列僅有一個入口和一個出口。不允許數據項直接插入隊中,也不允許從中間移除數據項。

抽象數據類型Queue

抽象數據類型Queue是一個有次序的數據集合,數據項僅添加到“尾”端,而且僅從”首“端移除,Queue具有先進先出的操作次序。

  • 抽象數據類型Queue由如下操作定義:

    Queue():創建一個空隊列對象,返回值為Queue對象;

    enqueue(item):將數據項item添加到隊尾,無返回值;

    dequeue():從隊首移除數據項,返回值為隊首數據項,隊列被修改;

    isEmpty():測試是否空隊列,返回值為bool;

    size():返回隊列中數據項的個數。

    隊列操作 隊列內容 返回值
    q=Queue() [] Queue object
    q.isEmpty() [] True
    q.enqueue(4) [4]
    q.enqueue(“dog”) [“dog”,4]
    q.enqueue(True) [True,”dog”,4]
    q.size() [True,”dog”,4] 3
    q.isEmpty() [True,”dog”,4] False
    q.enqueue(“dog”) [8.4,True,”dog”,4]
    q.dequeue() [8.4,True,”dog”] 4
    q.dequeue() [8.4,True] “dog”
    q.size() [8.4,True] 2

Python實現Queue

採用List來容納Queue的數據項,將List首端作為隊列尾端,List的末端作為隊列的首端,enqueue()複雜度為O(n),dequeue()複雜度為O(1)

class Queue:
    def __init__(self):
        self.items = []
        
    def isEmpty(self):
        return self.items == []
    
    def enqueue(self, item):
        self.items.insert(0, item)
        
    def dequeue(self):
        return self.items.pop()
    
    def size(self):
        return len(self.items)

注意:首尾倒過來的實現,複雜度也倒過來。

隊列的應用:約瑟夫問題

傳說猶太人反叛羅馬人,落到困境,約瑟夫和39人決定殉難,坐成一圈,報數1-7,報到7的人由旁邊殺死,結果略瑟夫給自己安排了個位置,最後活了下來……

模擬程序採用隊列來存放所有參加遊戲的人名,按照報數的方向從隊首排到隊尾,模擬遊戲開始,只需要將隊首的人出隊,隨機再到對尾入隊,算是一次報數完成,反覆n次后,將隊首的人移除,不再入隊,如此反覆,知道隊列中剩下一人。

def JosephQuestion(namelist,num):
    simqueue = Queue()
    for name in namelist:
        simqueue.enqueue(name)

    while simqueue.size() > 1:
        for i in range(num):
            simqueue.enqueue(simqueue.dequeue())  # 一次傳遞

        simqueue.dequeue()   # 處死隊首
    
    return simqueue.dequeue()

隊列的應用:打印任務

有如下場景:多人共享一台打印機,採取“先到先服務”的策略來執行打印任務,在這種設定下,一個首要問題就是:這種打印作業系統的容量有多大?在能夠接受的等待時間內,系統能夠容納多少用戶以多高頻率提交到少打印任務?

一個具體的實例配置如下:

一個實驗室,在任意的一個小時內,大約有10名學生在場,這一個小時中,每個人發起2次左右的打印,每次1-20頁。打印機的性能是:以草稿模式打印的話,每分鐘10頁,以正常模式打印的話,打印質量好,但是速度下降,每分鐘只能打印五頁。

問題:怎麼設定打印機的模式,讓大家都不會等太久的前提下,盡量提高打印質量?這是一個典型的決策支持問題,但無法通過規則直接計算,我們要用一段程序來模擬這種打印任務場景,然後對程序運行結果進行分析,以支持對打印機模式設定的決策。

如何對問題建模:

首先對問題進行抽象,確定相關的對象和過程。拋棄那些對問題實質沒有關係的學生性別,年齡,打印機型號,打印內容,智障大小等等眾多細節。

對象:打印任務,打印隊列,打印機。

打印任務的屬性:提交事件,打印頁數

打印隊列的屬性:具有“先進先出”性質的打印任務隊列

※推薦台中搬家公司優質服務,可到府估價

台中搬鋼琴,台中金庫搬運,中部廢棄物處理,南投縣搬家公司,好幫手搬家,西屯區搬家

打印機的屬性:打印速度,是否忙

過程:生成和提交打印任務

確定生成概率:實例為每小時會有10個學生提交的20個作業,這樣概率是每180秒會有1個作業生成並提交,概率為每秒1/180。

確定打印頁數:實例是1-20頁,那麼就是1-20頁之間概率相同。

過程:實施打印

當前的打印作業:正在打印的作業

打印結束倒計時:新作業開始打印時開始倒計時,返回0表示打印完畢,可以處理下一個作業。

模擬時間:

統一的時間框架:以最小單位(秒)均勻流逝的時間,設定結束時間

同步所有過程:在一個時間單位里,對生成打印任務和實施打印兩個過程各處理一次

打印任務問題:流程模擬

創建打印隊列對象

時間按照秒的單位流逝

按照概率生成打印作業,加入打印隊列

如果打印機空閑,且隊列不空,則取出隊首作業打印,記錄此作業等待時間

如果打印機忙,則按照打印速度進行1秒打印

如果當前作業打印完成,則打印機進入空閑

時間用盡,開始統計平均等待時間

作業的等待時間

生成作業時,記錄生成的時間戳

開始打印時,當前時間減去生成時間即可

作業的打印時間

生成作業時,記錄作業的頁數

開始打印時,頁數除以打印速度即可

import random

class Printer:
    def __init__(self, ppm):
        self.pagerate = ppm  # 打印速度
        self.currentTask = None  # 打印任務
        self.timeRemaining = 0  # 任務倒計時

    def tick(self):  # 打印1秒
        if self.currentTask != None:
            self.timeRemaining -= 1
            if self.timeRemaining <= 0:
                self.currentTask = None

    def busy(self):  # 打印機忙
        if self.currentTask != None:
            return True
        else:
            return False

    def startNext(self, newtask):  # 打印新作業
        self.currentTask = newtask
        self.timeRemaining = newtask.getPages() * 60 / self.pagerate


class Task:
    def __init__(self, time):
        self.timestamp = time  # 生成時間戳
        self.pages = random.randrange(1, 21)  # 打印頁數

    def getStamp(self):
        return self.timestamp

    def getPages(self):
        return self.pages

    def waitTime(self, currenttime):
        return currenttime - self.timestamp  # 等待時間


def newPrintTask():
    num = random.randrange(1, 181)
    if num == 180:
        return True
    else:
        return False

def simulation(numSeconds,  pagesPerMinute): # 模擬
    labprinter = Printer(pagesPerMinute)
    printQueue = Queue()
    waittingtimes = []

    for currentSecond in range(numSeconds):
        if newPrintTask():
            task = Task(currentSecond)
            printQueue.enqueue(task)

        if not labprinter.busy() and not printQueue.isEmpty():
            nexttask = printQueue.dequeue()
            waittingtimes.append(nexttask.waitTime(currentSecond))
            labprinter.startNext(nexttask)

        labprinter.tick()

    averageWait = sum(waittingtimes) / len(waittingtimes)
    print("Average Wait %6.2f secs %3d tasks remaining" %(averageWait,printQueue.size()))

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

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

還在煩惱搬家費用要多少哪?台中大展搬家線上試算搬家費用,從此不再擔心「物品怎麼計費」、「多少車才能裝完」

一個 json 轉換工具_台中搬家公司

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

還在煩惱搬家費用要多少哪?台中大展搬家線上試算搬家費用,從此不再擔心「物品怎麼計費」、「多少車才能裝完」

      在前後端的數據協議(主要指httpwebsocket)的問題上,如果前期溝通好了,那麼數據協議上問題會很好解決,前後端商議一種都可以接受的格式即可。但是如果接入的是老系統、第三方系統,或者由於某些奇怪的需求(如為了節省流量,json 數據使用單字母作為key值,或者對某一段數據進行了加密),這些情況下就無法商議,需要在前端做數據轉換,如果不轉換,那麼奔放的數據格式可讀性差,也會造成項目難以維護。

 

      這也正是我在項目種遇到的問題,網上也找了一些方案,要麼過於複雜,要麼有些功能不能很好的支持,於是有了這個工具 class-converter。歡迎提 issue 和 star~~https://github.com/zquancai/class-converter

 

下面我們用例子來說明下:

面對如下的Server返回的一個用戶user數據:

{
    "i": 1234,
    "n": "name",
    "a": "1a2b3c4d5e6f7a8b"
}

或者這個樣的: 

{
    "user_id": 1234,
    "user_name": "name",
    "u_avatar": "1a2b3c4d5e6f7a8b"
}

數據里的 avatar 字段在使用時,可能需要拼接成一個 url,例如 https://xxx.cdn.com/1a2b3c4d5e6f7a8b.png

當然可以直接這麼做:

const json = {
    "i": 1234,
    "n": "name",
    "a": "1a2b3c4d5e6f7a8b",
};
const data = {};
const keyMap = {
    i: 'id',
    n: 'name',
    a: 'avatar',
}
Object.entries(json).forEach(([key, value]) => {
    data[keyMap[key]] = value;
});
// data = { id: 1234, name: 'name', avatar: '1a2b3c4d5e6f7a8b' }

然後我們進一步就可以把這個抽象成一個方法,像下面這個樣:

const jsonConverter = (json, keyMap) => {
    const data = {};
    Object.entries(json).forEach(([key, value]) => {
        data[keyMap[key]] = value;
    });
    return data;
}

如果這個數據擴展了,添加了教育信息,user 數據結構看起來這個樣:

{
    "i": 1234,
    "n": "name",
    "a": "1a2b3c4d5e6f7a8b",
    "edu": {
        "u": "South China Normal University",
        "ea": 1
    }
}

此時的 jsonConverter 方法已經無法正確轉換 edu 字段的數據,需要做一些修改:

const json = {
    "i": 1234,
    "n": "name",
    "a": "1a2b3c4d5e6f7a8b",
    "edu": {
        "u": "South China Normal University",
        "ea": 1
    }
};
const data = {};
const keyMap = {
    i: 'id',
    n: 'name',
    a: 'avatar',
    edu: {
        key: 'education',
        keyMap: {
            u: 'universityName',
            ea: 'attainment'
        }
    },
}

隨着數據複雜度的上升,keyMap 數據結構會變成一個臃腫的配置文件,此外 jsonConverter 方法會越來越複雜,以至於後面同樣難以維護。但是轉換后的數據格式,對於項目來說,數據的可讀性是很高的。所以,這個轉換必須做,但是方式可以更優雅一點。

寫這個工具的初衷也是為了更優雅的進行數據轉換。

※推薦台中搬家公司優質服務,可到府估價

台中搬鋼琴,台中金庫搬運,中部廢棄物處理,南投縣搬家公司,好幫手搬家,西屯區搬家

 

工具用法

還是上面的例子(這裏使用typescript寫法):

import { toClass, property } from 'class-converter';
// 待解析的數據
const json = {
    "i": 1234,
    "n": "name",
    "a": "1a2b3c4d5e6f7a8b",
};
class User {
    @property('i')
    id: number;
    
    @property('n')
    name: string;
    
    @property('a')
    avatar: string;
}
const userIns = toClass(json, User);

你可以輕而易舉的獲得下面的數據:

// userIns 是 User 的一個實例
const userIns = {
    id: 1234,
    name: 'name',
    avatar: '1a2b3c4d5e6f7a8b',
}
userIns instanceof User // true

Json 類既是文檔又是類似於上文說的與keyMap類似的配置文件,並且可以反向使用。

import { toPlain } from 'class-converter';
const user = toPlain(userIns, User);
// user 數據結構
{
    i: 1234,
    n: 'name',
    a: '1a2b3c4d5e6f7a8b',
};

  

這是一個最簡單的例子,我們來一個複雜的數據結構:

{
  "i": 10000,
  "n": "name",
  "user": {
    "i": 20000,
    "n": "name1",
    "email": "zqczqc",
    // {"i":1111,"n":"department"}
    "d": "eyJpIjoxMTExLCJuIjoiZGVwYXJ0bWVudCJ9",
    "edu": [
      {
        "i": 1111,
        "sn": "szzx"
      },
      {
        "i": 2222,
        "sn": "scnu"
      },
      {
         "i": 3333
      }
    ]
  }
}

這是後端返回的一個叫package的json對象,字段意義在文檔中這麼解釋:

  • i:package 的 id
  • n:package 的名字
  • user:package 的所有者,一個用戶
    • i:用戶 id
    • n:用戶名稱
    • email:用戶email,但是只有郵箱前綴
    • d:用戶的所在部門,使用了base64編碼了一個json字符串
      • i:部門 id
      • n:部門名稱
    • edu:用戶的教育信息,數組格式
      • i:學校 id
      • sn:學校名稱

我們的期望是將這一段數據解析成,不看文檔也能讀懂的一個json對象,首先我們經過分析得出上面一共有4類實體對象:package、用戶信息、部門信息、教育信息。

下面是代碼實現:

import {
    toClass, property, array, defaultVal,
    beforeDeserialize, deserialize, optional
} from 'class-converter';
// 教育信息
class Education {
    @property('i')
    id: number;
    
    // 提供一個默認值
    @defaultVal('unknow')
    @prperty('sn')
    schoolName: string;
}
// 部門信息
class Department {
    @property('i')
    id: number;
    
    @prperty('n')
    name: string;
}
// 用戶信息
class User {
  @property('i')
  id: number;
  @property('n')
  name: string;
  
  // 保留一份郵箱前綴數據
  @optional()
  @property()
  emailPrefix: string;
  
  @optional()
  // 這裏希望自動把後綴加上去
  @deserialize(val => `${val}@xxx.com`)
  @property()
  email: string;
  
  @beforeDeserialize(val => JSON.parse(atob(val)))
  @typed(Department)
  @property('d')
  department: Department;
  
  @array()
  @typed(Education)
  @property('edu')
  educations: Education[];
}
// package
class Package {
  @property('i')
  id: number;
  
  @property('n')
  name: string;
  
  @property('user', User)
  owner: User;
} 

數據已經定義完畢,這時只要我們執行toClass方法就可以得到我們想要的數據格式:

{
  id: 10000,
  name: 'name',
  owner: {
    id: 20000,
    name: 'name1',
    emailPrefix: 'zqczqc',
    email: "zqczqc@xxx.com",
    department: {
        id: 1111,
        name: 'department'
    },
    educations: [
      {
        id: 1111,
        schoolName: 'szzx'
      },
      {
        id: 2222,
        schoolName: 'scnu'
      },
      {
        id: 3333,
        schoolName: 'unknow'
      }
    ]
  }
}

上面這一份數據,相比後端返回的數據格式,可讀性大大提升。這裏的用法出現了@deserialize@beforeDeserialize@yped的裝飾器,這裏對這幾個裝飾器是管道方式調用的(前一個的輸出一個的輸入),這裏做一個解釋:

  • beforeDeserialize 第一個參數可以最早拿到當前屬性值,這裏可以做一些解碼操作
  • typed這個是轉換的類型,入參是一個類,相當於自動調用toClass,並且調動時的第一個參數是beforeDeserialize的返回值或者當前屬性值(如果沒有@beforeDeserialize裝飾器)。如果使用了@array裝飾器,則會對每一項數組元素都執行這個轉換
  • deserialize這個裝飾器是最後執行的,第一個參數是beforeDeserialize返回值,@typed返回值,或者當前屬性值(如果前面兩個裝飾器都沒設置的話)。在這個裝飾器里可以做一些數據訂正的操作

這三個裝飾器是在執行toClass時才會調用的,同樣的,當調用toPlain時也會有對應的裝飾器@serialize@fterSerialize,結合@typed進行一個相反的過程。下面將這兩個轉換過程的流程繪製出來。

調用 toClass的過程:

調用 toPlain的過程是調用 toClass的逆過程,但是有些許不一樣,有一個注意點就是:在調用 toClass時允許出現一對多的情況,就是一個屬性可以派生出多個屬性,所以調用調用 toPlain時需要使用 @serializeTarget來標記使用哪一個值作為逆過程的原始值,具體用法可以參考文檔。

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

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

還在煩惱搬家費用要多少哪?台中大展搬家線上試算搬家費用,從此不再擔心「物品怎麼計費」、「多少車才能裝完」

Netty學習筆記(番外篇) – ChannelHandler、ChannelPipeline和ChannelHandlerContext的聯繫_台中搬家公司

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

還在煩惱搬家費用要多少哪?台中大展搬家線上試算搬家費用,從此不再擔心「物品怎麼計費」、「多少車才能裝完」

這一篇是 ChannelHandler 和 ChannelPipeline 的番外篇,主要從源碼的角度來學習 ChannelHandler、ChannelHandler 和 ChannelPipeline 相互之間是如何建立聯繫和運行的。

一、添加 ChannelHandler

從上一篇的 demo 中可以看到在初始化 Server 和 Client 的時候,都會通過 ChannelPipeline 的 addLast 方法將 ChannelHandler 添加進去

// Server.java

// 部分代碼片段
ServerBootstrap serverBootstrap = new ServerBootstrap();
NioEventLoopGroup group = new NioEventLoopGroup();
serverBootstrap.group(group)
        .channel(NioServerSocketChannel.class)Channel
        .localAddress(new InetSocketAddress("localhost", 9999))
        .childHandler(new ChannelInitializer<SocketChannel>() {
            protected void initChannel(SocketChannel socketChannel) throws Exception {
                // 添加ChannelHandler
                socketChannel.pipeline().addLast(new OneChannelOutBoundHandler());

                socketChannel.pipeline().addLast(new OneChannelInBoundHandler());
                socketChannel.pipeline().addLast(new TwoChannelInBoundHandler());
            }
        });

在上面的代碼片段中,socketChannel.pipeline()方法返回的是一個類型是 DefaultChannelPipeline 的實例,DefaultChannelPipeline 實現了 ChannelPipeline 接口

DefaultChannelPipeline 的 addLast 方法實現如下:

// DefaultChannelPipeline.java

@Override
public final ChannelPipeline addLast(ChannelHandler... handlers) {
    return addLast(null, handlers);
}

@Override
public final ChannelPipeline addLast(EventExecutorGroup executor, ChannelHandler... handlers) {
    ObjectUtil.checkNotNull(handlers, "handlers");

    for (ChannelHandler h: handlers) {
        if (h == null) {
            break;
        }
        addLast(executor, null, h);
    }

    return this;
}

經過一系列重載方法調用,最終進入到下面的 addLast 方法

// DefaultChannelPipeline.java

@Override
public final ChannelPipeline addLast(EventExecutorGroup group, String name, ChannelHandler handler) {
    final AbstractChannelHandlerContext newCtx;
    synchronized (this) {
        checkMultiplicity(handler);

        newCtx = newContext(group, filterName(name, handler), handler);

        addLast0(newCtx);

        // If the registered is false it means that the channel was not registered on an eventLoop yet.
        // In this case we add the context to the pipeline and add a task that will call
        // ChannelHandler.handlerAdded(...) once the channel is registered.
        if (!registered) {
            newCtx.setAddPending();
            callHandlerCallbackLater(newCtx, true);
            return this;
        }

        EventExecutor executor = newCtx.executor();
        if (!executor.inEventLoop()) {
            callHandlerAddedInEventLoop(newCtx, executor);
            return this;
        }
    }
    callHandlerAdded0(newCtx);
    return this;
}

在這個方法實現中,利用傳進來的 ChannelHandler 在 newContext 創建了一個 AbstractChannelHandlerContext 對象。newContext 方法實現如下:

// DefaultChannelPipeline.java

private AbstractChannelHandlerContext newContext(EventExecutorGroup group, String name, ChannelHandler handler) {
    return new DefaultChannelHandlerContext(this, childExecutor(group), name, handler);
}

這裏創建並返回了一個類型為 DefaultChannelHandlerContext 的對象。從傳入的參數可以看到,在這裏將 ChannelHandlerContext、ChannelPipeline(this)和 ChannelHandler 三者建立了關係。
最後再看看 addLast0 方法實現:

// DefaultChannelPipeline.java

private void addLast0(AbstractChannelHandlerContext newCtx) {
    AbstractChannelHandlerContext prev = tail.prev;
    newCtx.prev = prev;
    newCtx.next = tail;
    prev.next = newCtx;
    tail.prev = newCtx;
}

這裏出現了 AbstractChannelHandlerContext 的兩個屬性 prev 和 next,而 DefaultChannelPipeline 有一個屬性 tail。從實現邏輯上看起來像是建立了一個雙向鏈表的結構。下面的代碼片段是關於 tail 和另一個相關屬性 head:

// DefaultChannelPipeline.java

public class DefaultChannelPipeline implements ChannelPipeline {
    final AbstractChannelHandlerContext head;
    final AbstractChannelHandlerContext tail;

    // ......
    protected DefaultChannelPipeline(Channel channel) {
        // ......

        tail = new TailContext(this);
        head = new HeadContext(this);

        head.next = tail;
        tail.prev = head;
    }

    // ......
}

// HeaderContext.java
final class HeadContext extends AbstractChannelHandlerContext implements ChannelOutboundHandler, ChannelInboundHandler {
    // ......

    @Override
    public ChannelHandler handler() {
        return this;
    }

    //......
}

// TailContext.java
final class TailContext extends AbstractChannelHandlerContext implements ChannelInboundHandler {
    // ......

    @Override
    public ChannelHandler handler() {
        return this;
    }

    // ......
}

DefaultChannelPipeline 內部維護了兩個 AbstractChannelHandlerContext 類型的屬性 head、tail,而這兩個屬性又都實現了 ChannelHandler 的子接口。構造方法里將這兩個屬性維護成了一個雙向鏈表。結合上面的 addLast0 方法實現,可以知道在添加 ChannelHandler 的時候,其實是在對 ChannelPipeline 內部維護的雙向鏈表做插入操作。
下面是 ChannelHandlerContext 相關類的結構

所以,對 ChannelPipeline 做 add 操作添加 ChannelHandler 后,內部結構大體是這樣的:

所有的 ChannelHandlerContext 組成了一個雙向鏈表,頭部是 HeadContext,尾部是 TailContext,因為它們都實現了 ChannelHandler 接口,所以它們內部的 Handler 也是自己。每次添加一個 ChannelHandler,將會新創建一個 DefaultChannelHandler 關聯,並按照一定的順序插入到鏈表中。
在 AbstractChannelHandlerContext 類里有一個屬性 executionMask,在構造方法初始化時會對它進行賦值

// AbstractChannelHandlerContext.java

// 省略部分代碼

AbstractChannelHandlerContext(DefaultChannelPipeline pipeline, EventExecutor executor,
                                String name, Class<? extends ChannelHandler> handlerClass) {
    this.name = ObjectUtil.checkNotNull(name, "name");
    this.pipeline = pipeline;
    this.executor = executor;
    this.executionMask = mask(handlerClass);
    // Its ordered if its driven by the EventLoop or the given Executor is an instanceof OrderedEventExecutor.
    ordered = executor == null || executor instanceof OrderedEventExecutor;
}

// 省略部分代碼

mask 是一個靜態方法,來自於 ChannelHandlerMask 類

// ChannelHandlerMask.java

// 省略部分代碼

/**
* Return the {@code executionMask}.
*/
static int mask(Class<? extends ChannelHandler> clazz) {
    // Try to obtain the mask from the cache first. If this fails calculate it and put it in the cache for fast
    // lookup in the future.
    Map<Class<? extends ChannelHandler>, Integer> cache = MASKS.get();
    Integer mask = cache.get(clazz);
    if (mask == null) {
        mask = mask0(clazz);
        cache.put(clazz, mask);
    }
    return mask;
}

/**
* Calculate the {@code executionMask}.
*/
private static int mask0(Class<? extends ChannelHandler> handlerType) {
    int mask = MASK_EXCEPTION_CAUGHT;
    try {
        if (ChannelInboundHandler.class.isAssignableFrom(handlerType)) {
            mask |= MASK_ALL_INBOUND;

            if (isSkippable(handlerType, "channelRegistered", ChannelHandlerContext.class)) {
                mask &= ~MASK_CHANNEL_REGISTERED;
            }
            if (isSkippable(handlerType, "channelUnregistered", ChannelHandlerContext.class)) {
                mask &= ~MASK_CHANNEL_UNREGISTERED;
            }
            if (isSkippable(handlerType, "channelActive", ChannelHandlerContext.class)) {
                mask &= ~MASK_CHANNEL_ACTIVE;
            }
            if (isSkippable(handlerType, "channelInactive", ChannelHandlerContext.class)) {
                mask &= ~MASK_CHANNEL_INACTIVE;
            }
            if (isSkippable(handlerType, "channelRead", ChannelHandlerContext.class, Object.class)) {
                mask &= ~MASK_CHANNEL_READ;
            }
            if (isSkippable(handlerType, "channelReadComplete", ChannelHandlerContext.class)) {
                mask &= ~MASK_CHANNEL_READ_COMPLETE;
            }
            if (isSkippable(handlerType, "channelWritabilityChanged", ChannelHandlerContext.class)) {
                mask &= ~MASK_CHANNEL_WRITABILITY_CHANGED;
            }
            if (isSkippable(handlerType, "userEventTriggered", ChannelHandlerContext.class, Object.class)) {
                mask &= ~MASK_USER_EVENT_TRIGGERED;
            }
        }

        if (ChannelOutboundHandler.class.isAssignableFrom(handlerType)) {
            mask |= MASK_ALL_OUTBOUND;

            if (isSkippable(handlerType, "bind", ChannelHandlerContext.class,
                    SocketAddress.class, ChannelPromise.class)) {
                mask &= ~MASK_BIND;
            }
            if (isSkippable(handlerType, "connect", ChannelHandlerContext.class, SocketAddress.class,
                    SocketAddress.class, ChannelPromise.class)) {
                mask &= ~MASK_CONNECT;
            }
            if (isSkippable(handlerType, "disconnect", ChannelHandlerContext.class, ChannelPromise.class)) {
                mask &= ~MASK_DISCONNECT;
            }
            if (isSkippable(handlerType, "close", ChannelHandlerContext.class, ChannelPromise.class)) {
                mask &= ~MASK_CLOSE;
            }
            if (isSkippable(handlerType, "deregister", ChannelHandlerContext.class, ChannelPromise.class)) {
                mask &= ~MASK_DEREGISTER;
            }
            if (isSkippable(handlerType, "read", ChannelHandlerContext.class)) {
                mask &= ~MASK_READ;
            }
            if (isSkippable(handlerType, "write", ChannelHandlerContext.class,
                    Object.class, ChannelPromise.class)) {
                mask &= ~MASK_WRITE;
            }
            if (isSkippable(handlerType, "flush", ChannelHandlerContext.class)) {
                mask &= ~MASK_FLUSH;
            }
        }

        if (isSkippable(handlerType, "exceptionCaught", ChannelHandlerContext.class, Throwable.class)) {
            mask &= ~MASK_EXCEPTION_CAUGHT;
        }
    } catch (Exception e) {
        // Should never reach here.
        PlatformDependent.throwException(e);
    }

    return mask;
}

@SuppressWarnings("rawtypes")
private static boolean isSkippable(
        final Class<?> handlerType, final String methodName, final Class<?>... paramTypes) throws Exception {
    return AccessController.doPrivileged(new PrivilegedExceptionAction<Boolean>() {
        @Override
        public Boolean run() throws Exception {
            Method m;
            try {
                m = handlerType.getMethod(methodName, paramTypes);
            } catch (NoSuchMethodException e) {
                if (logger.isDebugEnabled()) {
                    logger.debug(
                        "Class {} missing method {}, assume we can not skip execution", handlerType, methodName, e);
                }
                return false;
            }
            return m != null && m.isAnnotationPresent(Skip.class);
        }
    });
}

// 省略部分代碼

以上代碼實現邏輯是這樣的:當創建一個 ChannelHandlerContext 時,會與一個 ChannelHandler 綁定,同時會將傳遞進來的 ChannelHandler 進行解析,解析當前 ChannelHandler 支持哪些回調方法,並通過位運算得到一個結果保存在 ChannelHandlerContext 的 executionMask 屬性里。注意 m.isAnnotationPresent(Skip.class)這裏,ChannelHandler 的基類 ChannelInboundHandlerAdapter 和 ChannelOutboundHandlerAdapter 里的回調方法上都有@Skip 註解,當繼承了這兩個類並重寫了某個回調方法后,這個方法上的註解就會被覆蓋掉,解析時就會被認為當前 ChannelHandler 支持這個回調方法。
下面是每個回調方法對應的掩碼

※推薦台中搬家公司優質服務,可到府估價

台中搬鋼琴,台中金庫搬運,中部廢棄物處理,南投縣搬家公司,好幫手搬家,西屯區搬家

// ChannelHandlerMask.java

final class ChannelHandlerMask {
    // Using to mask which methods must be called for a ChannelHandler.
    static final int MASK_EXCEPTION_CAUGHT = 1;
    static final int MASK_CHANNEL_REGISTERED = 1 << 1;
    static final int MASK_CHANNEL_UNREGISTERED = 1 << 2;
    static final int MASK_CHANNEL_ACTIVE = 1 << 3;
    static final int MASK_CHANNEL_INACTIVE = 1 << 4;
    static final int MASK_CHANNEL_READ = 1 << 5;
    static final int MASK_CHANNEL_READ_COMPLETE = 1 << 6;
    static final int MASK_USER_EVENT_TRIGGERED = 1 << 7;
    static final int MASK_CHANNEL_WRITABILITY_CHANGED = 1 << 8;
    static final int MASK_BIND = 1 << 9;
    static final int MASK_CONNECT = 1 << 10;
    static final int MASK_DISCONNECT = 1 << 11;
    static final int MASK_CLOSE = 1 << 12;
    static final int MASK_DEREGISTER = 1 << 13;
    static final int MASK_READ = 1 << 14;
    static final int MASK_WRITE = 1 << 15;
    static final int MASK_FLUSH = 1 << 16;

    static final int MASK_ONLY_INBOUND =  MASK_CHANNEL_REGISTERED |
            MASK_CHANNEL_UNREGISTERED | MASK_CHANNEL_ACTIVE | MASK_CHANNEL_INACTIVE | MASK_CHANNEL_READ |
            MASK_CHANNEL_READ_COMPLETE | MASK_USER_EVENT_TRIGGERED | MASK_CHANNEL_WRITABILITY_CHANGED;
    private static final int MASK_ALL_INBOUND = MASK_EXCEPTION_CAUGHT | MASK_ONLY_INBOUND;
    static final int MASK_ONLY_OUTBOUND =  MASK_BIND | MASK_CONNECT | MASK_DISCONNECT |
            MASK_CLOSE | MASK_DEREGISTER | MASK_READ | MASK_WRITE | MASK_FLUSH;
    private static final int MASK_ALL_OUTBOUND = MASK_EXCEPTION_CAUGHT | MASK_ONLY_OUTBOUND;
}

二、ChannelHandler 處理消息

我們以消息讀取和寫入為例,來看看在 ChannelPipeline 里的各個 ChannelHandler 是如何按照順序處理消息和事件的。

讀取消息

當 Channel 讀取到消息后,會在以下地方調用 ChannelPipeline 的 fireChannelRead 方法:

// AbstractNioMessageClient.java

private final class NioMessageUnsafe extends AbstractNioUnsafe {

    // 省略代碼

    @Override
    public void read() {
        // ......

        for (int i = 0; i < size; i ++) {
            readPending = false;
            pipeline.fireChannelRead(readBuf.get(i));
        }

        // ......
    }

    // 省略代碼
}

// DefaultChannelPipeline.java

// 省略代碼

@Override
public final ChannelPipeline fireChannelRead(Object msg) {
    AbstractChannelHandlerContext.invokeChannelRead(head, msg);
    return this;
}

// 省略代碼

可以看到,通過 AbstractChannelHandlerContext 的 invokeChannelRead 方法,傳遞 head,從頭部開始觸發讀取事件。

// AbstractChannelHandlerContext.java

// 省略代碼

static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) {
    final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next);
    EventExecutor executor = next.executor();
    if (executor.inEventLoop()) {
        next.invokeChannelRead(m);
    } else {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                next.invokeChannelRead(m);
            }
        });
    }
}

private void invokeChannelRead(Object msg) {
    if (invokeHandler()) {
        try {
            ((ChannelInboundHandler) handler()).channelRead(this, msg);
        } catch (Throwable t) {
            invokeExceptionCaught(t);
        }
    } else {
        fireChannelRead(msg);
    }
}

/**
    * Makes best possible effort to detect if {@link ChannelHandler#handlerAdded(ChannelHandlerContext)} was called
    * yet. If not return {@code false} and if called or could not detect return {@code true}.
    *
    * If this method returns {@code false} we will not invoke the {@link ChannelHandler} but just forward the event.
    * This is needed as {@link DefaultChannelPipeline} may already put the {@link ChannelHandler} in the linked-list
    * but not called {@link ChannelHandler#handlerAdded(ChannelHandlerContext)}.
    */
private boolean invokeHandler() {
    // Store in local variable to reduce volatile reads.
    int handlerState = this.handlerState;
    return handlerState == ADD_COMPLETE || (!ordered && handlerState == ADD_PENDING);
}

// 省略代碼

在這裏通過 invokeHandler 方法對當前 ChannelHandler 進行狀態檢查,通過了就將調用當前 ChannelHandler 的 channelRead 方法,沒有通過將調用 fireChannelRead 方法將事件傳遞到下一個 ChannelHandler 上。而 head 的類型是 HeadContext,本身也實現了 ChannelInBoundHandler 接口,所以這裏調用的是 HeadContext 的 channelRead 方法。

// DefaultChannelPipeline.java

final class HeadContext extends AbstractChannelHandlerContext implements ChannelOutboundHandler, ChannelInboundHandler {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ctx.fireChannelRead(msg);
    }
}

這裏對消息沒有做任何處理,直接將讀取消息傳遞下去。接下來看看 ChannelHandlerContext 的 fireChannelRead 做了什麼

// AbstractChannelHandlerContext.java

@Override
public ChannelHandlerContext fireChannelRead(final Object msg) {
    invokeChannelRead(findContextInbound(MASK_CHANNEL_READ), msg);
    return this;
}

private AbstractChannelHandlerContext findContextInbound(int mask) {
    AbstractChannelHandlerContext ctx = this;
    EventExecutor currentExecutor = executor();
    do {
        ctx = ctx.next;
    } while (skipContext(ctx, currentExecutor, mask, MASK_ONLY_INBOUND));
    return ctx;
}

private static boolean skipContext(
        AbstractChannelHandlerContext ctx, EventExecutor currentExecutor, int mask, int onlyMask) {
    // Ensure we correctly handle MASK_EXCEPTION_CAUGHT which is not included in the MASK_EXCEPTION_CAUGHT
    return (ctx.executionMask & (onlyMask | mask)) == 0 ||
            // We can only skip if the EventExecutor is the same as otherwise we need to ensure we offload
            // everything to preserve ordering.
            //
            // See https://github.com/netty/netty/issues/10067
            (ctx.executor() == currentExecutor && (ctx.executionMask & mask) == 0);
}

這裏實現的邏輯是這樣的:在雙向鏈表中,從當前 ChannelHandlerContext 節點向後尋找,直到找到匹配 MASK_CHANNEL_READ 這個掩碼的 ChannelHandlerContext。從上面的章節里可以直到 ChannelHandlerContext 的屬性里保存了當前 ChannelHandler 支持(重寫)的所有方法掩碼的位運算值,通過位運算的結果來找到實現了對應方法的最近的 ChannelHandlerContext。
鏈表最後一個節點是 TailContext

// DefaultChannelPipeline.java

final class TailContext extends AbstractChannelHandlerContext implements ChannelInboundHandler {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        onUnhandledInboundMessage(ctx, msg);
    }

    /**
     * Called once a message hit the end of the {@link ChannelPipeline} without been handled by the user
     * in {@link ChannelInboundHandler#channelRead(ChannelHandlerContext, Object)}. This method is responsible
     * to call {@link ReferenceCountUtil#release(Object)} on the given msg at some point.
     */
    protected void onUnhandledInboundMessage(ChannelHandlerContext ctx, Object msg) {
        onUnhandledInboundMessage(msg);
        if (logger.isDebugEnabled()) {
            logger.debug("Discarded message pipeline : {}. Channel : {}.",
                         ctx.pipeline().names(), ctx.channel());
        }
    }

    /**
     * Called once a message hit the end of the {@link ChannelPipeline} without been handled by the user
     * in {@link ChannelInboundHandler#channelRead(ChannelHandlerContext, Object)}. This method is responsible
     * to call {@link ReferenceCountUtil#release(Object)} on the given msg at some point.
     */
    protected void onUnhandledInboundMessage(Object msg) {
        try {
            logger.debug(
                    "Discarded inbound message {} that reached at the tail of the pipeline. " +
                            "Please check your pipeline configuration.", msg);
        } finally {
            ReferenceCountUtil.release(msg);
        }
    }
}

可以看到,tail 節點的 channelRead 方法沒有將事件繼續傳遞下去,只是釋放了 msg。

寫入消息

我們通過 OneChannelInBoundHandler 的 channelReadComplete 方法里的 ctx.write 方法來看

// AbstractChannelHandlerContext.java

// 省略代碼

@Override
public ChannelFuture write(Object msg) {
    return write(msg, newPromise());
}

@Override
public ChannelFuture write(final Object msg, final ChannelPromise promise) {
    write(msg, false, promise);

    return promise;
}

private void write(Object msg, boolean flush, ChannelPromise promise) {
    ObjectUtil.checkNotNull(msg, "msg");
    try {
        if (isNotValidPromise(promise, true)) {
            ReferenceCountUtil.release(msg);
            // cancelled
            return;
        }
    } catch (RuntimeException e) {
        ReferenceCountUtil.release(msg);
        throw e;
    }

    final AbstractChannelHandlerContext next = findContextOutbound(flush ?
            (MASK_WRITE | MASK_FLUSH) : MASK_WRITE);
    final Object m = pipeline.touch(msg, next);
    EventExecutor executor = next.executor();
    if (executor.inEventLoop()) {
        if (flush) {
            next.invokeWriteAndFlush(m, promise);
        } else {
            next.invokeWrite(m, promise);
        }
    } else {
        final WriteTask task = WriteTask.newInstance(next, m, promise, flush);
        if (!safeExecute(executor, task, promise, m, !flush)) {
            // We failed to submit the WriteTask. We need to cancel it so we decrement the pending bytes
            // and put it back in the Recycler for re-use later.
            //
            // See https://github.com/netty/netty/issues/8343.
            task.cancel();
        }
    }
}

private AbstractChannelHandlerContext findContextOutbound(int mask) {
    AbstractChannelHandlerContext ctx = this;
    EventExecutor currentExecutor = executor();
    do {
        ctx = ctx.prev;
    } while (skipContext(ctx, currentExecutor, mask, MASK_ONLY_OUTBOUND));
    return ctx;
}

// 省略代碼

通過調用一系列重載的 write 方法后,通過 findContextOutbound 方法在雙向鏈表裡向前尋找最近的實現了 write 或 writeAndFlush 方法的 ChannelHandlerContext,調用它的 invokeWrite 或 invokeWriteAndFlush 方法。

// AbstractChannelHandlerContext.java

// 省略代碼

void invokeWrite(Object msg, ChannelPromise promise) {
    if (invokeHandler()) {
        invokeWrite0(msg, promise);
    } else {
        write(msg, promise);
    }
}

private void invokeWrite0(Object msg, ChannelPromise promise) {
    try {
        ((ChannelOutboundHandler) handler()).write(this, msg, promise);
    } catch (Throwable t) {
        notifyOutboundHandlerException(t, promise);
    }
}

// 省略代碼

同理於讀取消息,這裏經過 invokeHandler 方法檢查通過後調用找到的 ChannelHandlerContext 的 ChannelHandler,沒有通過檢查,則繼續向前傳遞寫入事件。當寫入消息傳遞到頭部,調用 HeadContext 的 write 方法

// DefaultChannelPipeline.java

final class HeadContext extends AbstractChannelHandlerContext implements ChannelOutboundHandler, ChannelInboundHandler {

    private final Unsafe unsafe;

    // 省略代碼

    @Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
        unsafe.write(msg, promise);
    }

    @Override
    public void flush(ChannelHandlerContext ctx) {
        unsafe.flush();
    }

    // 省略代碼
}

最終通過調用 unsafe 的 write 方法寫入消息。
最後,從上面的實現里可以發現,在將 ChannelHandler 加入到 ChannelPipeline 時,要把 ChannelOutBoundHandler 類型的 ChannelHandler 進來添加在前面,否則在 ChannelInBoundHandler 寫入消息時,在它後面的 ChannelOutBoundHandler 將無法獲取到事件。

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

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

還在煩惱搬家費用要多少哪?台中大展搬家線上試算搬家費用,從此不再擔心「物品怎麼計費」、「多少車才能裝完」

电子郵件協議及GO發送QQ郵件_台中搬家公司

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

還在煩惱搬家費用要多少哪?台中大展搬家線上試算搬家費用,從此不再擔心「物品怎麼計費」、「多少車才能裝完」

目錄

  • 一、电子郵件的工作機制
    • 1.1 SMTP
    • 1.2 POP3
    • 1.3 IMAP
  • 二、郵件地址
  • 三、MIME信息
  • 四、使用golang發送qq郵件

一、电子郵件的工作機制

提供电子郵件服務的協議叫做:SMTP(Simple Mail Transfer Protocol)為了能夠高效安全的進行數據的傳輸,SMTP協議底層使用的TCP實現兩端的連接。

早期的电子郵件收發的工作機制如上圖所示。發送端和接收端之間通過SMTP底層的TCP簡歷連接。通過網絡直接將郵件發送到對方的磁盤上。

但是問題也隨之而來:

如果接收方沒有開機,或者開機了但是沒有連接網絡,那麼就不能通過SMTP協議建立連接,這時發送端只能是個隔一段時間后重試,直到接收端開機了,聯網了,發送端才能成功的將郵件發送給接收方。問題很明顯,接收方只要不開機,發送方的郵件就不能發送出去,如果是東方國家和西方國家之間的兩個人各自在各自的白天才開機,那豈不是他們之間的郵件根本不可能發送出去了?

為了解決這個問題,郵件服務器出現了:

這時收發郵件的工作機制就演變成了上圖那樣。發送方 面向 郵件服務器發送郵件,而不管接收方是否開機,是否聯網,接收方通過上線后使用POP3(Post Office Proto-col)從郵件服務器接收郵件。

整個過程中,郵件服務器是不會斷電的。

1.1 SMTP

通過上圖可以看到,SMTP是發送电子郵件時使用的協議。 它底層使用tcp的25號端口。在這個tcp連接上進行控制,應答,以及數據的傳輸。

※推薦台中搬家公司優質服務,可到府估價

台中搬鋼琴,台中金庫搬運,中部廢棄物處理,南投縣搬家公司,好幫手搬家,西屯區搬家

客戶端以文本的方式發送請求,郵件服務器每次回復3位数字作為應答。比如客戶端首次會發送 HELO<domain>表示請求建立連接。正常的話郵件服務器會回復250,表示完成請求命令。

SMTP協議中規定,以’.’最為郵件正文的結束符。當正文前面有一個’.’或者有兩個’.’ 都要進行特殊處理。

SMTP不會校驗發送者,所以我們經常會收到垃圾郵件,據說也會有“POP before SMTP” 和“SMTP認證”機制,來防止冒充發送人。從而減少垃圾郵件的數量。

1.2 POP3

POP服務器也是一台一直處於充電狀態的服務器。

客戶端通過pop3協議從pop服務器上接收發送方發過來的協議,但是在接收之前是需要進行用戶身份驗證的,也就是說,客戶端得將自己的賬號密碼發送到POP服務器,通過驗證后才能取回屬於自己的郵件

POP與SMTP一樣,都是基於TCP連接完成相應的操作的。

1.3 IMAP

IMAP和POP協議一樣都是接收电子郵件時使用的協議。

如果使用IMAP,即使不用將电子郵件下載到本地也可以閱讀。因為IMAP實現了字啊服務端處理MIME類型的數據,所以他能實現當一封电子郵件有10個附件時,它能直接打開其中的某一個。而且在服務端作出已讀/未讀,等狀體的修改。

二、郵件地址

郵件地址通常都是由兩部分組成: 名稱@地址

常見的像 123@qq.com 這種郵件的地址。

123就是名稱,qq.com就是地址。 电子郵件的地址和域名構造相同,後面的com是頂級域名。

現在的电子郵件地址由DNS統一管理。DNS裏面存儲着各個郵件地址,和這個郵件地址作為發送地址時所對應的郵件服務器的域名信息。我們把這種映射關係稱為MX記錄。因為方才說了,對現在的郵件發送機制來說,發送者是將郵件發送到郵件服務器上。那通過查詢DNS中的MX記錄,就能知道xxx@qq.com. xxx@163.com 這種不同的郵件後綴所對應的郵件服務器的域名,通過域名進一步找到這個機器。

三、MIME信息

最初的很長一段時間里,郵件只能發送文本信息。後台能發送的數據類型已經被拓展到了MIME。可以發送諸如gif, video,png,jpg,jpeg,text/plain 等等類型的數據。具體發送啥樣的信息,通過Content-Type定義。

四、使用golang發送qq郵件

實例代碼如下:

package mail

import (
	"strconv"
)
import "gopkg.in/gomail.v2"

func SendMail(mailTo []string, subject string, body string) error {
  
	mailConn := map[string]string{
		"user": "6464xxxx8@qq.com", // 郵件發送者的地址
		"pass": "trsxxxxxxxxxxcd",  // qq郵箱填授權碼,百度一下獲取方式。
		"host": "smtp.qq.com", // 發送將郵件發送給騰訊的smtp郵件服務器
		"port": "465",   // 發送郵件使用的端口
	}
	port, _ := strconv.Atoi(mailConn["port"])
  
	m := gomail.NewMessage()
	m.SetHeader("From", m.FormatAddress(mailConn["user"], "自動化成績查詢"))
	m.SetHeader("To", mailTo...)    //發送給多個用戶
	m.SetHeader("Subject", subject) //設置郵件主題
	 m.SetBody("text/html", body)    //設置郵件正文
  
	d := gomail.NewDialer(mailConn["host"], port, mailConn["user"], mailConn["pass"])
	err := d.DialAndSend(m)
	return err
}

	/*
	發送郵件
	stuEmail:學生的郵箱
	subject:標題
	body:發送的內容
  */
func DoSendMail(stuEmail , subject, body string) (e error) {
	mailTo := []string{stuEmail}
	err := SendMail(mailTo, subject, body)
	if err != nil {
		e = err
		return e
	}
	return nil
}

//func main() {
//	//定義收件人
//	mailTo := []string{
//		"2693xxxx8@qq.com",
//		"196xxxxx30@qq.com",
//	}
//	//郵件主題為"Hello"
//	subject := "Hi 出成績了"
//	// 郵件正文
//	body := "請查收您的新成績"
//
//	err := SendMail(mailTo, subject, body)
//	if err != nil {
//		log.Println(err)
//		fmt.Println("send fail")
//		return
//	}
//	fmt.Println("send successfully")
//}

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

※推薦台中搬家公司優質服務,可到府估價

台中搬鋼琴,台中金庫搬運,中部廢棄物處理,南投縣搬家公司,好幫手搬家,西屯區搬家