Tensorflow2 自定義數據集圖片完成圖片分類任務

對於自定義數據集的圖片任務,通用流程一般分為以下幾個步驟:

  • Load data

  • Train-Val-Test

  • Build model

  • Transfer Learning

其中大部分精力會花在數據的準備和預處理上,本文用一種較為通用的數據處理手段,並通過手動構建,簡單模型, 層數較深的resnet網絡,和基於VGG19的遷移學習。

你可以通過這個例子,快速搭建網絡,並訓練處一個較為滿意的結果。

1. Load data

數據集來自Pokemon的5分類數據, 每一種的圖片數量為200多張,是一個較小型的數據集。

官方項目鏈接:

Keras and Convolutional Neural Networks (CNNs)

1.1 數據集介紹

Pokemon文件夾中包含5個子文件,其中每個子文件夾名為對應的類別名。文件夾中包含有png, jpeg的圖片文件。

1.2 解題思路

  • 由於文件夾中沒有劃分,訓練集和測試集,所以需要構建一個csv文件讀取所有的文件,及其類別

  • shuffle數據集以後,劃分Train_val_test

  • 對數據進行預處理, 數據標準化,數據增強, 可視化處理

“””python
# 創建数字編碼錶

  import os
  import glob
  import random
  import csv
  import tensorflow as tf
  from tensorflow import keras
  import matplotlib.pyplot as plt
  import time
  
  
  def load_csv(root, filename, name2label):
      """
      將分散在各文件夾中的圖片, 轉換為圖片和label對應的一個dataset文件, 格式為csv
      :param root: 文件路徑(每個子文件夾中的文件屬於一類)
      :param filename: 文件名
      :param name2label: 類名編碼錶  {'類名1':0, '類名2':1..}
      :return: images, labels
      """
      # 判斷是否csv文件已經生成
      if not os.path.exists(os.path.join(root, filename)):  # join-將路徑與文件名何為一個路徑並返回(沒有會生成新路徑)
          images = []  # 存的是文件路徑
          for name in name2label.keys():
              # pokemon\pikachu\00000001.png
              # glob.glob() 利用通配符檢索路徑內的文件,類似於正則表達式
              images += glob.glob(os.path.join(root, name, '*'))  # png, jpg, jpeg
          print(name2label)
          print(len(images), images)
  
          random.shuffle(images)
  
          with open(os.path.join(root, filename), 'w', newline='') as f:
              writer = csv.writer(f)
              for img in images:
                  name = img.split(os.sep)[1]  # os.sep 表示分隔符 window-'\\' , linux-'/'
                  label = name2label[name]  # 0, 1, 2..
                  # 'pokemon\\bulbasaur\\00000000.png', 0
                  writer.writerow([img, label])  # 如果不設定newline='', 2個數據會分為2行寫
              print('write into csv file:', filename)
  
      # 讀取現有文件
      images, labels = [], []
      with open(os.path.join(root, filename)) as f:
          reader = csv.reader(f)
          for row in reader:
              # 'pokemon\\bulbasaur\\00000000.png', 0
              img, label = row
              label = int(label)  # str-> int
              images.append(img)
              labels.append(label)
  
      assert len(images) == len(labels)
  
      return images, labels
  
  
  def load_pokemon(root, mode='train'):
      """
      # 創建数字編碼錶
      :param root: root path
      :param mode: train, valid, test
      :return: images, labels, name2label
      """
  
      name2label = {}  # {'bulbasaur': 0, 'charmander': 1, 'mewtwo': 2, 'pikachu': 3, 'squirtle': 4}
      for name in sorted(os.listdir(os.path.join(root))):
          # sorted() 是為了復現結果的一致性
          # os.listdir - 返迴路徑下的所有文件(文件夾,文件)列表
          if not os.path.isdir(os.path.join(root, name)):  # 是否為文件夾且是否存在
              continue
          # 每個類別編碼一個数字
          name2label[name] = len(name2label)
  
      # 讀取label
      images, labels = load_csv(root, 'images.csv', name2label)
  
      # 劃分數據集 [6:2:2]
      if mode == 'train':
          images = images[:int(0.6 * len(images))]
          labels = labels[:int(0.6 * len(labels))]  # len(images) == len(labels)
  
      elif mode == 'valid':
          images = images[int(0.6 * len(images)):int(0.8 * len(images))]
          labels = labels[int(0.6 * len(labels)):int(0.8 * len(labels))]
  
      else:
          images = images[int(0.8 * len(images)):]
          labels = labels[int(0.8 * len(labels)):]
  
      return images, labels, name2label
  
  
  # imagenet 數據集均值, 方差
  img_mean = tf.constant([0.485, 0.456, 0.406])  # 3 channel
  img_std = tf.constant([0.229, 0.224, 0.225])
  
  def normalization(x, mean=img_mean, std=img_std):
      # [224, 224, 3]
      x = (x - mean) / std
      return x
  
  def denormalization(x, mean=img_mean, std=img_std):
      x = x * std + mean
      return x
  
  
  def preprocess(x, y):
      # x: path, y: label
      x = tf.io.read_file(x)  # 2進制
      # x = tf.image.decode_image(x)
      x = tf.image.decode_jpeg(x, channels=3)  # RGBA
      x = tf.image.resize(x, [244, 244])
  
      # data augmentation
      # x = tf.image.random_flip_up_down(x)
      x = tf.image.random_flip_left_right(x)
      x = tf.image.random_crop(x, [224, 224, 3])  # 模型縮減比例不宜過大,否則會增大訓練難度
  
      x = tf.cast(x, dtype=tf.float32) / 255. # unit8 -> float32
      # U[0,1] -> N(0,1)  # 提高訓練準確度
      x = normalization(x)
  
      y = tf.convert_to_tensor(y)
  
      return x, y
  
  def main():
      images, labels, name2label = load_pokemon('pokemon', 'train')
      print('images:', len(images), images)
      print('labels:', len(labels), labels)
      # print(name2label)
  
      # .map()函數要位於.batch()之前, 否則 x=tf.io.read_file()會一次讀取一個batch的圖片,從而報錯
      db = tf.data.Dataset.from_tensor_slices((images, labels)).map(preprocess).shuffle(1000).batch(32)
  
      # tf.summary()
      # 提供了各類方法(支持各種多種格式)用於保存訓練過程中產生的數據(比如loss_value、accuracy、整個variable),
      # 這些數據以日誌文件的形式保存到指定的文件夾中。
  
      # 數據可視化:而tensorboard可以將tf.summary()
      # 記錄下來的日誌可視化,根據記錄的數據格式,生成折線圖、統計直方圖、圖片列表等多種圖。
      # tf.summary()
      # 通過遞增的方式更新日誌,這讓我們可以邊訓練邊使用tensorboard讀取日誌進行可視化,從而實時監控訓練過程。
      writer = tf.summary.create_file_writer('logs')
      for step, (x, y) in enumerate(db):
          with writer.as_default():
              x = denormalization(x)
              tf.summary.image('img', x, step=step, max_outputs=9)  # STEP:默認選項,指的是橫軸显示的是訓練迭代次數
  
              time.sleep(5)
  
  
  
  if __name__ == '__main__':
      main()

“””

2. 構建模型進行訓練

2.1 自定義小型網絡

由於數據集數量較少,大型網絡的訓練中往往會出現過擬合情況,這裏就定義了一個2層卷積的小型網絡。
引入early_stopping回調函數后,3個epoch沒有較大變化的情況下,模型訓練的準確率為0.8547

“””
# 1. 自定義小型網絡
model = keras.Sequential([
layers.Conv2D(16, 5, 3),
layers.MaxPool2D(3, 3),
layers.ReLU(),
layers.Conv2D(64, 5, 3),
layers.MaxPool2D(2, 2),
layers.ReLU(),
layers.Flatten(),
layers.Dense(64),
layers.ReLU(),
layers.Dense(5)
])

  model.build(input_shape=(None, 224, 224, 3))  
  model.summary()
  
  early_stopping = EarlyStopping(
      monitor='val_loss',
      patience=3,
      min_delta=0.001
  )
  
  
  model.compile(optimizer=optimizers.Adam(lr=1e-3),
                 loss=losses.CategoricalCrossentropy(from_logits=True),
                 metrics=['accuracy'])
  model.fit(db_train, validation_data=db_val, validation_freq=1, epochs=100,
             callbacks=[early_stopping])
  model.evaluate(db_test)

“””

2.2 自定義的Resnet網絡

resnet 網絡對於層次較深的網絡的可訓練型提升很大,主要是通過一個identity layer保證了深層次網絡的訓練效果不會弱於淺層網絡。
其他文章中有詳細介紹resnet的搭建,這裏就不做贅述, 這裏構建了一個resnet18網絡, 準確率0.7607。

“””
import os

  import numpy as np
  import tensorflow as tf
  from tensorflow import keras
  from tensorflow.keras import layers
  
  tf.random.set_seed(22)
  np.random.seed(22)
  os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
  assert tf.__version__.startswith('2.')
  
  
  class ResnetBlock(keras.Model):
  
      def __init__(self, channels, strides=1):
          super(ResnetBlock, self).__init__()
  
          self.channels = channels
          self.strides = strides
  
          self.conv1 = layers.Conv2D(channels, 3, strides=strides,
                                     padding=[[0, 0], [1, 1], [1, 1], [0, 0]])
          self.bn1 = keras.layers.BatchNormalization()
          self.conv2 = layers.Conv2D(channels, 3, strides=1,
                                     padding=[[0, 0], [1, 1], [1, 1], [0, 0]])
          self.bn2 = keras.layers.BatchNormalization()
  
          if strides != 1:
              self.down_conv = layers.Conv2D(channels, 1, strides=strides, padding='valid')
              self.down_bn = tf.keras.layers.BatchNormalization()
  
      def call(self, inputs, training=None):
          residual = inputs
  
          x = self.conv1(inputs)
          x = tf.nn.relu(x)
          x = self.bn1(x, training=training)
          x = self.conv2(x)
          x = tf.nn.relu(x)
          x = self.bn2(x, training=training)
  
          # 殘差連接
          if self.strides != 1:
              residual = self.down_conv(inputs)
              residual = tf.nn.relu(residual)
              residual = self.down_bn(residual, training=training)
  
          x = x + residual
          x = tf.nn.relu(x)
          return x
  
  
  class ResNet(keras.Model):
  
      def __init__(self, num_classes, initial_filters=16, **kwargs):
          super(ResNet, self).__init__(**kwargs)
  
          self.stem = layers.Conv2D(initial_filters, 3, strides=3, padding='valid')
  
          self.blocks = keras.models.Sequential([
              ResnetBlock(initial_filters * 2, strides=3),
              ResnetBlock(initial_filters * 2, strides=1),
              # layers.Dropout(rate=0.5),
  
              ResnetBlock(initial_filters * 4, strides=3),
              ResnetBlock(initial_filters * 4, strides=1),
  
              ResnetBlock(initial_filters * 8, strides=2),
              ResnetBlock(initial_filters * 8, strides=1),
  
              ResnetBlock(initial_filters * 16, strides=2),
              ResnetBlock(initial_filters * 16, strides=1),
          ])
  
          self.final_bn = layers.BatchNormalization()
          self.avg_pool = layers.GlobalMaxPool2D()
          self.fc = layers.Dense(num_classes)
  
      def call(self, inputs, training=None):
          # print('x:',inputs.shape)
          out = self.stem(inputs, training = training)
          out = tf.nn.relu(out)
  
          # print('stem:',out.shape)
  
          out = self.blocks(out, training=training)
          # print('res:',out.shape)
  
          out = self.final_bn(out, training=training)
          # out = tf.nn.relu(out)
  
          out = self.avg_pool(out)
  
          # print('avg_pool:',out.shape)
          out = self.fc(out)
  
          # print('out:',out.shape)
  
          return out
  
  
  def main():
      num_classes = 5
  
      resnet18 = ResNet(5)
      resnet18.build(input_shape=(None, 224, 224, 3))
      resnet18.summary()
  
  
  if __name__ == '__main__':
      main()

“””

“””
# 2.resnet18訓練, 圖片數量較小,訓練結果不是特別好
# resnet = ResNet(5) # 0.7607
# resnet.build(input_shape=(None, 224, 224, 3))
# resnet.summary()
“””

2.3 VGG19遷移學習

遷移學習利用了數據集之間的相似性,對於數據集數量較少的時候,訓練效果會遠優於其他。
在訓練過程中,使用include_top=False, 去掉最後分類的基層Dense, 重新構建並訓練就可以了。準確率0.9316

“””
# 3. VGG19遷移學習,遷移學習利用數據集之間的相似性, 結果遠好於其他2種
# 為了方便,這裏仍然使用resnet命名
net = tf.keras.applications.VGG19(weights=’imagenet’, include_top=False, pooling=’max’ )
net.trainable = False
resnet = keras.Sequential([
net,
layers.Dense(5)
])
resnet.build(input_shape=(None, 224, 224, 3)) # 0.9316
resnet.summary()

  early_stopping = EarlyStopping(
      monitor='val_loss',
      patience=3,
      min_delta=0.001
  )
  
  
  resnet.compile(optimizer=optimizers.Adam(lr=1e-3),
                 loss=losses.CategoricalCrossentropy(from_logits=True),
                 metrics=['accuracy'])
  resnet.fit(db_train, validation_data=db_val, validation_freq=1, epochs=100,
             callbacks=[early_stopping])
  resnet.evaluate(db_test)

“””

附錄:

train_scratch.py 代碼

“””

import os

os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'

import tensorflow as tf
import numpy as np
from tensorflow import keras
from tensorflow.keras import layers, optimizers, losses
from tensorflow.keras.callbacks import EarlyStopping

tf.random.set_seed(22)
np.random.seed(22)
assert tf.__version__.startswith('2.')

# 設置GPU顯存按需分配
# gpus = tf.config.experimental.list_physical_devices('GPU')
# if gpus:
#     try:
#         # Currently, memory growth needs to be the same across GPUs
#         for gpu in gpus:
#             tf.config.experimental.set_memory_growth(gpu, True)
#         logical_gpus = tf.config.experimental.list_logical_devices('GPU')
#         print(len(gpus), "Physical GPUs,", len(logical_gpus), "Logical GPUs")
#     except RuntimeError as e:
#         # Memory growth must be set before GPUs have been initialized
#         print(e)

from pokemon import load_pokemon, normalization
from resnet import ResNet


def preprocess(x, y):
    # x: 圖片的路徑,y:圖片的数字編碼
    x = tf.io.read_file(x)
    x = tf.image.decode_jpeg(x, channels=3)  # RGBA
    # 圖片縮放
    # x = tf.image.resize(x, [244, 244])
    # 圖片旋轉
    # x = tf.image.rot90(x,2)
    # 隨機水平翻轉
    x = tf.image.random_flip_left_right(x)
    # 隨機豎直翻轉
    # x = tf.image.random_flip_up_down(x)

    # 圖片先縮放到稍大尺寸
    x = tf.image.resize(x, [244, 244])
    # 再隨機裁剪到合適尺寸
    x = tf.image.random_crop(x, [224, 224, 3])

    # x: [0,255]=> -1~1
    x = tf.cast(x, dtype=tf.float32) / 255.
    x = normalization(x)
    y = tf.convert_to_tensor(y)
    y = tf.one_hot(y, depth=5)

    return x, y


batchsz = 32

# create train db
images1, labels1, table = load_pokemon('pokemon', 'train')
db_train = tf.data.Dataset.from_tensor_slices((images1, labels1))
db_train = db_train.shuffle(1000).map(preprocess).batch(batchsz)
# create validation db
images2, labels2, table = load_pokemon('pokemon', 'valid')
db_val = tf.data.Dataset.from_tensor_slices((images2, labels2))
db_val = db_val.map(preprocess).batch(batchsz)
# create test db
images3, labels3, table = load_pokemon('pokemon', mode='test')
db_test = tf.data.Dataset.from_tensor_slices((images3, labels3))
db_test = db_test.map(preprocess).batch(batchsz)


# 1. 自定義小型網絡
# resnet = keras.Sequential([
#     layers.Conv2D(16, 5, 3),
#     layers.MaxPool2D(3, 3),
#     layers.ReLU(),
#     layers.Conv2D(64, 5, 3),
#     layers.MaxPool2D(2, 2),
#     layers.ReLU(),
#     layers.Flatten(),
#     layers.Dense(64),
#     layers.ReLU(),
#     layers.Dense(5)
# ])  # 0.8547


# 2.resnet18訓練, 圖片數量較小,訓練結果不是特別好
# resnet = ResNet(5)  # 0.7607
# resnet.build(input_shape=(None, 224, 224, 3))
# resnet.summary()


# 3. VGG19遷移學習,遷移學習利用數據集之間的相似性, 結果遠好於其他2種
net = tf.keras.applications.VGG19(weights='imagenet', include_top=False, pooling='max' )
net.trainable = False
resnet = keras.Sequential([
    net,
    layers.Dense(5)
])
resnet.build(input_shape=(None, 224, 224, 3))   # 0.9316
resnet.summary()

early_stopping = EarlyStopping(
    monitor='val_loss',
    patience=3,
    min_delta=0.001
)


resnet.compile(optimizer=optimizers.Adam(lr=1e-3),
               loss=losses.CategoricalCrossentropy(from_logits=True),
               metrics=['accuracy'])
resnet.fit(db_train, validation_data=db_val, validation_freq=1, epochs=100,
           callbacks=[early_stopping])
resnet.evaluate(db_test)

“””

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

【其他文章推薦】

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

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

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

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

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

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

這篇文章,我們來談一談Spring中的屬性注入

本系列文章:

讀源碼,我們可以從第一行讀起

你知道Spring是怎麼解析配置類的嗎?

配置類為什麼要添加@Configuration註解?

談談Spring中的對象跟Bean,你知道Spring怎麼創建對象的嗎?

推薦閱讀:

Spring官網閱讀 | 總結篇

Spring雜談

本系列文章將會帶你一行行的將Spring的源碼吃透,推薦閱讀的文章是閱讀源碼的基礎!

前言

在前面的文章中已經知道了Spring是如何將一個對象創建出來的,那麼緊接着,Spring就需要將這個對象變成一個真正的Bean了,這個過程主要分為兩步

  1. 屬性注入
  2. 初始化

在這兩個過程中,Bean的後置處理器會穿插執行,其中有些後置處理器是為了幫助完成屬性注入或者初始化的,而有些後置處理器是Spring提供給程序員進行擴展的,當然,這二者並不衝突。整個Spring創建對象並將對象變成Bean的過程就是我們經常提到了Spring中Bean的生命周期。當然,本系列源碼分析的文章不會再對生命周期的概念做過多闡述了,如果大家有這方面的需求的話可以參考我之前的文章,或者關注我的公眾號:程序員DMZ

Spring官網閱讀(九)Spring中Bean的生命周期(上)

Spring官網閱讀(十)Spring中Bean的生命周期(下)

源碼分析

閑話不再多說,我們正式進入源碼分析階段,本文重點要分析的方法就是org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean,其源碼如下:

doCreateBean

	protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
			throws BeanCreationException {

		// 創建對象的過程在上篇文章中我們已經介紹過了,這裏不再贅述
		BeanWrapper instanceWrapper = null;
		if (mbd.isSingleton()) {
			instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
		}
		if (instanceWrapper == null) {
			instanceWrapper = createBeanInstance(beanName, mbd, args);
		}
        
        // 獲取到創建的這個對象
		final Object bean = instanceWrapper.getWrappedInstance();
		Class<?> beanType = instanceWrapper.getWrappedClass();
		if (beanType != NullBean.class) {
			mbd.resolvedTargetType = beanType;
		}

		// Allow post-processors to modify the merged bean definition.
        // 按照官方的註釋來說,這個地方是Spring提供的一個擴展點,對程序員而言,我們可以通過一個實現了MergedBeanDefinitionPostProcessor的後置處理器來修改bd中的屬性,從而影響到後續的Bean的生命周期
        // 不過官方自己實現的後置處理器並沒有去修改bd,而是調用了applyMergedBeanDefinitionPostProcessors方法
        // 這個方法名直譯過來就是-應用合併后的bd,也就是說它這裏只是對bd做了進一步的使用而沒有真正的修改
		synchronized (mbd.postProcessingLock) {
           // bd只允許被處理一次
			if (!mbd.postProcessed) {
				try {
                    // 應用合併后的bd
					applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName);
				}
				catch (Throwable ex) {
					throw new BeanCreationException(mbd.getResourceDescription(), beanName,
							"Post-processing of merged bean definition failed", ex);
				}
                // 標註這個bd已經被MergedBeanDefinitionPostProcessor的後置處理器處理過
                // 那麼在第二次創建Bean的時候,不會再次調用applyMergedBeanDefinitionPostProcessors
				mbd.postProcessed = true;
			}
		}

		// 這裡是用來出來循環依賴的,關於循環以來,在介紹完正常的Bean的創建后,單獨用一篇文章說明
        // 這裏不做過多解釋
		boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
				isSingletonCurrentlyInCreation(beanName));
		if (earlySingletonExposure) {
			if (logger.isTraceEnabled()) {
				logger.trace("Eagerly caching bean '" + beanName +
						"' to allow for resolving potential circular references");
			}
			addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
		}


		Object exposedObject = bean;
		try {
            // 我們這篇文章重點要分析的就是populateBean方法,在這個方法中完成了屬性注入
			populateBean(beanName, mbd, instanceWrapper);
            // 初始化
			exposedObject = initializeBean(beanName, exposedObject, mbd);
		}
		catch (Throwable ex) {
			// 省略異常代碼
		}

		// 後續代碼不在本文探討範圍內了,暫不考慮

		return exposedObject;
	}

applyMergedBeanDefinitionPostProcessors

源碼如下:

// 可以看到這個方法的代碼還是很簡單的,就是調用了MergedBeanDefinitionPostProcessor的postProcessMergedBeanDefinition方法
protected void applyMergedBeanDefinitionPostProcessors(RootBeanDefinition mbd, Class<?> beanType, String beanName) {
    for (BeanPostProcessor bp : getBeanPostProcessors()) {
        if (bp instanceof MergedBeanDefinitionPostProcessor) {
            MergedBeanDefinitionPostProcessor bdp = (MergedBeanDefinitionPostProcessor) bp;
            bdp.postProcessMergedBeanDefinition(mbd, beanType, beanName);
        }
    }
}

這個時候我們就要思考一個問題,容器中現在有哪些後置處理器是MergedBeanDefinitionPostProcessor呢?

查看這個方法的實現類我們會發現總共就這麼幾個類實現了MergedBeanDefinitionPostProcessor接口。實際上除了ApplicationListenerDetector之外,其餘的後置處理器的邏輯都差不多。我們在這裏我們主要就分析兩個後置處理

  1. ApplicationListenerDetector
  2. AutowiredAnnotationBeanPostProcessor

ApplicationListenerDetector

首先,我們來ApplicationListenerDetector,這個類在之前的文章中也多次提到過了,它的作用是用來處理嵌套Bean的情況,主要是保證能將嵌套在Bean標籤中的ApplicationListener也能添加到容器的監聽器集合中去。我們先通過一個例子來感受下這個後置處理器的作用吧

配置文件:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	   xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

	<bean class="com.dmz.source.populate.service.DmzService" id="dmzService">
		<constructor-arg name="orderService">
			<bean class="com.dmz.source.populate.service.OrderService"/>
		</constructor-arg>
	</bean>
</beans>

示例代碼:

// 事件
public class DmzEvent extends ApplicationEvent {
	public DmzEvent(Object source) {
		super(source);
	}
}

public class DmzService {

	OrderService orderService;

	public DmzService(OrderService orderService) {
		this.orderService = orderService;
	}
}
// 實現ApplicationListener接口
public class OrderService implements ApplicationListener<DmzEvent> {
	@Override
	public void onApplicationEvent(DmzEvent event) {
		System.out.println(event.getSource());
	}
}

public class Main {
	public static void main(String[] args) {
		ClassPathXmlApplicationContext cc = new ClassPathXmlApplicationContext("application-populate.xml");
		cc.publishEvent(new DmzEvent("my name is dmz"));
	}
}

// 程序運行結果,控制台打印:my name is dmz

說明OrderService已經被添加到了容器的監聽器集合中。但是請注意,在這種情況下,如果要使OrderService能夠執行監聽的邏輯,必須要滿足下面這兩個條件

  • 外部的Bean要是單例的,對於我們的例子而言就是dmzService
  • 內嵌的Bean也必須是單例的,在上面的例子中也就是orderService必須是單例

另外需要注意的是,這種嵌套的Bean比較特殊,它雖然由Spring創建,但是確不存在於容器中,就是說我們不能將其作為依賴注入到別的Bean中。

AutowiredAnnotationBeanPostProcessor

對應源碼如下:

public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) {
    // 找到注入的元數據,第一次是構建,後續可以直接從緩存中拿
    // 註解元數據其實就是當前這個類中的所有需要進行注入的“點”的集合,
    // 注入點(InjectedElement)包含兩種,字段/方法
    // 對應的就是AutowiredFieldElement/AutowiredMethodElement
    InjectionMetadata metadata = findAutowiringMetadata(beanName, beanType, null);
    // 排除掉被外部管理的注入點
    metadata.checkConfigMembers(beanDefinition);
}

上面代碼的核心邏輯就是

  • 找到所有的注入點,其實就是被@Autowired註解修飾的方法以及字段,同時靜態的方法以及字段也會被排除
  • 排除掉被外部管理的注入點,在後續的源碼分析中我們再細說

findAutowiringMetadata

// 這個方法的核心邏輯就是先從緩存中獲取已經解析好的注入點信息,很明顯,在原型情況下才會使用緩存
// 創建注入點的核心邏輯在buildAutowiringMetadata方法中
private InjectionMetadata findAutowiringMetadata(String beanName, Class<?> clazz, @Nullable PropertyValues pvs) {
    String cacheKey = (StringUtils.hasLength(beanName) ? beanName : clazz.getName());
    InjectionMetadata metadata = this.injectionMetadataCache.get(cacheKey);
    // 可能我們會修改bd中的class屬性,那麼InjectionMetadata中的注入點信息也需要刷新
    if (InjectionMetadata.needsRefresh(metadata, clazz)) {
        synchronized (this.injectionMetadataCache) {
            metadata = this.injectionMetadataCache.get(cacheKey);
            if (InjectionMetadata.needsRefresh(metadata, clazz)) {
                if (metadata != null) {
                    metadata.clear(pvs);
                }
                // 這裏真正創建注入點
                metadata = buildAutowiringMetadata(clazz);
                this.injectionMetadataCache.put(cacheKey, metadata);
            }
        }
    }
    return metadata;
}

buildAutowiringMetadata

// 我們應用中使用@Autowired註解標註在字段上或者setter方法能夠完成屬性注入
// 就是因為這個方法將@Autowired註解標註的方法以及字段封裝成InjectionMetadata
// 在後續階段會調用InjectionMetadata的inject方法進行注入
private InjectionMetadata buildAutowiringMetadata(final Class<?> clazz) {
    List<InjectionMetadata.InjectedElement> elements = new ArrayList<>();
    Class<?> targetClass = clazz;

    do {
        final List<InjectionMetadata.InjectedElement> currElements = new ArrayList<>();
		// 處理所有的被@AutoWired/@Value註解標註的字段
        ReflectionUtils.doWithLocalFields(targetClass, field -> {
            AnnotationAttributes ann = findAutowiredAnnotation(field);
            if (ann != null) {
                // 靜態字段會直接跳過
                if (Modifier.isStatic(field.getModifiers())) {
                    // 省略日誌打印
                    return;
                }
                // 得到@AutoWired註解中的required屬性
                boolean required = determineRequiredStatus(ann);
                currElements.add(new AutowiredFieldElement(field, required));
            }
        });
		// 處理所有的被@AutoWired註解標註的方法,相對於字段而言,這裏需要對橋接方法進行特殊處理
        ReflectionUtils.doWithLocalMethods(targetClass, method -> {
            // 只處理一種特殊的橋接場景,其餘的橋接方法都會被忽略
            Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
            if (!BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) {
                return;
            }
            AnnotationAttributes ann = findAutowiredAnnotation(bridgedMethod);
            // 處理方法時需要注意,當父類中的方法被子類重寫時,如果子父類中的方法都加了@Autowired
            // 那麼此時父類方法不能被處理,即不能被封裝成一個AutowiredMethodElement
            if (ann != null && method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) {
                if (Modifier.isStatic(method.getModifiers())) {
                    // 省略日誌打印
                    return;
                }
                if (method.getParameterCount() == 0) {
                    // 當方法的參數數量為0時,雖然不需要進行注入,但是還是會把這個方法作為注入點使用
                    // 這個方法最終還是會被調用
                    if (logger.isInfoEnabled()) {
                        logger.info("Autowired annotation should only be used on methods with parameters: " +
                                    method);
                    }
                }
                boolean required = determineRequiredStatus(ann);
                // PropertyDescriptor: 屬性描述符
                // 就是通過解析getter/setter方法,例如void getA()會解析得到一個屬性名稱為a
                // readMethod為getA的PropertyDescriptor,
                // 在《Spring官網閱讀(十四)Spring中的BeanWrapper及類型轉換》文中已經做過解釋
                // 這裏不再贅述,這裏之所以來這麼一次查找是因為當XML中對這個屬性進行了配置后,
                // 那麼就不會進行自動注入了,XML中显示指定的屬性優先級高於註解
                PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz);		   // 構造一個對應的AutowiredMethodElement,後續這個方法會被執行
                // 方法的參數會被自動注入,這裏不限於setter方法
                currElements.add(new AutowiredMethodElement(method, required, pd));
            }
        });
		// 會處理父類中字段上及方法上的@AutoWired註解,並且父類的優先級比子類高
        elements.addAll(0, currElements);
        targetClass = targetClass.getSuperclass();
    }
    while (targetClass != null && targetClass != Object.class);

    return new InjectionMetadata(clazz, elements);
}
難點代碼分析

上面的代碼整體來說應該很簡單,就如我們之前所說的,處理帶有@Autowired註解的字段及方法,同時會過濾掉所有的靜態字段及方法。上面複雜的地方在於對橋接方法的處理,可能大部分人都沒辦法理解這幾行代碼:

// 第一行
Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);

// 第二行
if (!BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) {
    return;
}

// 第三行
if (ann != null && method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) {

}

要理解這些代碼,首先你得知道什麼是橋接,為此我已經寫好了一篇文章:

Spring雜談 | 從橋接方法到JVM方法調用

除了在上面的文章中提到的橋接方法外,還有一種特殊的情況

// A類跟B類在同一個包下,A不是public的
class A {
	public void test(){

	}
}

// 在B中會生成一個跟A中的方法描述符(參數+返回值)一模一樣的橋接方法
// 這個橋接方法實際上就是調用父類中的方法
// 具體可以參考:https://bugs.java.com/bugdatabase/view_bug.do?bug_id=63424113
public class B extends A {
}

在理解了什麼是橋接之後,那麼上邊的第一行代碼你應該就能看懂了,就以上面的代碼為例,B中會生成一個橋接方法,對應的被橋接的方法就是A中的test方法。

接着,我們看看第二行代碼

public static boolean isVisibilityBridgeMethodPair(Method bridgeMethod, Method bridgedMethod) {
    // 說明這個方法本身就不是橋接方法,直接返回true
    if (bridgeMethod == bridgedMethod) {
        return true;
    }
    // 說明是橋接方法,並且方法描述符一致
    // 當且僅當是上面例子中描述的這種橋接的時候這個判斷才會滿足
    // 正常來說橋接方法跟被橋接方法的返回值+參數類型肯定不一致
    // 所以這個判斷會過濾掉其餘的所有類型的橋接方法
    // 只會保留本文提及這種特殊情況下產生的橋接方法
    return (bridgeMethod.getReturnType().equals(bridgedMethod.getReturnType()) &&
            Arrays.equals(bridgeMethod.getParameterTypes(), bridgedMethod.getParameterTypes()));
}

最後,再來看看第三行代碼,核心就是這句 method.equals(ClassUtils.getMostSpecificMethod(method, clazz)。這句代碼的主要目的就是為了處理下面這種情況

@Component
public class D extends C {

	@Autowired
	@Override
	public void setDmzService(DmzService dmzService) {
		dmzService.init();
		this.dmzService = dmzService;
	}
}

// C不是Spring中的組件
public class C {
	DmzService dmzService;
    @Autowired
	public void setDmzService(DmzService dmzService) {
		this.dmzService = dmzService;
	}
}

這種情況下,在處理D中的@Autowired註解時,雖然我們要處理父類中的@Autowired註解,但是因為子類中的方法已經複寫了父類中的方法,所以此時應該要跳過父類中的這個被複寫的方法,這就是第三行代碼的作用。

小結

到這裏我們主要分析了applyMergedBeanDefinitionPostProcessors這段代碼的作用,它的執行時機是在創建對象之後,屬性注入之前。按照官方的定義來說,到這裏我們仍然可以使用這個方法來修改bd的定義,那麼相對於通過BeanFactoryPostProcessor的方式修改bd,applyMergedBeanDefinitionPostProcessors這個方法影響的範圍更小,BeanFactoryPostProcessor影響的是整個Bean的生命周期,而applyMergedBeanDefinitionPostProcessors只會影響屬性注入之後的生命周期。

其次,我們分析了Spring中內置的MergedBeanDefinitionPostProcessor,選取了其中兩個特殊的後置處理器進行分析,其中ApplicationListenerDetector主要處理內嵌的事件監聽器,而AutowiredAnnotationBeanPostProcessor主要用於處理@Autowired註解,實際上我們會發現,到這裏還只是完成了@Autowired註解的解析,還沒有真正開始進行注入,真正注入的邏輯在後面我們要分析的populateBean方法中,在這個方法中會使用解析好的注入元信息完成真正的屬性注入,那麼接下來我們就開始分析populateBean這個方法的源碼。

populateBean

循環依賴的代碼我們暫且跳過,後續出一篇專門文章解讀循環依賴,我們直接看看populateBean到底做了什麼。

protected void populateBean(String beanName, RootBeanDefinition mbd, @Nullable BeanWrapper bw) {

    // 處理空實例
    if (bw == null) {
        // 如果創建的對象為空,但是在XML中又配置了需要注入的屬性的話,那麼直接報錯
        if (mbd.hasPropertyValues()) {
            throw new BeanCreationException(
                mbd.getResourceDescription(), beanName, "Cannot apply property values to null instance");
        }
        else {
            // 空對象,不進行屬性注入
            return;
        }
    }

    // 滿足兩個條件,不是合成類 && 存在InstantiationAwareBeanPostProcessor
    // 其中InstantiationAwareBeanPostProcessor主要作用就是作為Bean的實例化前後的鈎子
    // 外加完成屬性注入,對於三個方法就是
    // postProcessBeforeInstantiation  創建對象前調用
    // postProcessAfterInstantiation   對象創建完成,@AutoWired註解解析后調用   
    // postProcessPropertyValues(已過期,被postProcessProperties替代) 進行屬性注入
    // 下面這段代碼的主要作用就是我們可以提供一個InstantiationAwareBeanPostProcessor
    // 提供的這個後置處理如果實現了postProcessAfterInstantiation方法並且返回false
    // 那麼可以跳過Spring默認的屬性注入,但是這也意味着我們要自己去實現屬性注入的邏輯
    // 所以一般情況下,我們也不會這麼去擴展
    if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
        for (BeanPostProcessor bp : getBeanPostProcessors()) {
            if (bp instanceof InstantiationAwareBeanPostProcessor) {
                InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
                if (!ibp.postProcessAfterInstantiation(bw.getWrappedInstance(), beanName)) {
                    return;
                }
            }
        }
    }
	
    // 這裏其實就是判斷XML是否提供了屬性相關配置
    PropertyValues pvs = (mbd.hasPropertyValues() ? mbd.getPropertyValues() : null);
	
    // 確認注入模型
    int resolvedAutowireMode = mbd.getResolvedAutowireMode();
    
    // 主要處理byName跟byType兩種注入模型,byConstructor這種注入模型在創建對象的時候已經處理過了
    // 這裏都是對自動注入進行處理,byName跟byType兩種注入模型均是依賴setter方法
    // byName,根據setter方法的名字來查找對應的依賴,例如setA,那麼就是去容器中查找名字為a的Bean
    // byType,根據setter方法的參數類型來查找對應的依賴,例如setXx(A a),就是去容器中查詢類型為A的bean
    if (resolvedAutowireMode == AUTOWIRE_BY_NAME || resolvedAutowireMode == AUTOWIRE_BY_TYPE) {
        MutablePropertyValues newPvs = new MutablePropertyValues(pvs);
        if (resolvedAutowireMode == AUTOWIRE_BY_NAME) {
            autowireByName(beanName, mbd, bw, newPvs);
        }
        if (resolvedAutowireMode == AUTOWIRE_BY_TYPE) {
            autowireByType(beanName, mbd, bw, newPvs);
        }
        // pvs是XML定義的屬性
        // 自動注入后,bean實際用到的屬性就應該要替換成自動注入后的屬性
        pvs = newPvs;
    }
	// 檢查是否有InstantiationAwareBeanPostProcessor
    // 前面說過了,這個後置處理器就是來完成屬性注入的
    boolean hasInstAwareBpps = hasInstantiationAwareBeanPostProcessors();
    
    //  是否需要依賴檢查,默認是不會進行依賴檢查的
    boolean needsDepCheck = (mbd.getDependencyCheck() != AbstractBeanDefinition.DEPENDENCY_CHECK_NONE);
	
    // 下面這段代碼有點麻煩了,因為涉及到版本問題
    // 其核心代碼就是調用了postProcessProperties完成了屬性注入
   
    PropertyDescriptor[] filteredPds = null;
    
    // 存在InstantiationAwareBeanPostProcessor,我們需要調用這類後置處理器的方法進行注入
		if (hasInstAwareBpps) {
			if (pvs == null) {
				pvs = mbd.getPropertyValues();
			}
			for (BeanPostProcessor bp : getBeanPostProcessors()) {
				if (bp instanceof InstantiationAwareBeanPostProcessor) {
					InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
                    // 這句就是核心
					PropertyValues pvsToUse = ibp.postProcessProperties(pvs, bw.getWrappedInstance(), beanName);
					if (pvsToUse == null) {
						if (filteredPds == null) {
                            // 得到需要進行依賴檢查的屬性的集合
							filteredPds = filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching);
						}
                        //  這個方法已經過時了,放到這裏就是為了兼容老版本
						pvsToUse = ibp.postProcessPropertyValues(pvs, filteredPds, bw.getWrappedInstance(), beanName);
						if (pvsToUse == null) {
							return;
						}
					}
					pvs = pvsToUse;
				}
			}
		}
    // 需要進行依賴檢查
		if (needsDepCheck) {
			if (filteredPds == null) {
                // 得到需要進行依賴檢查的屬性的集合
				filteredPds = filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching);
			}
            // 對需要進行依賴檢查的屬性進行依賴檢查
			checkDependencies(beanName, mbd, filteredPds, pvs);
		}
    // 將XML中的配置屬性應用到Bean上
		if (pvs != null) {
			applyPropertyValues(beanName, mbd, bw, pvs);
		}
}

上面這段代碼主要可以拆分為三個部分

  1. 處理自動注入
  2. 處理屬性注入(主要指處理@Autowired註解),最重要
  3. 處理依賴檢查

處理自動注入

autowireByName

對應源碼如下:

protected void autowireByName(
    String beanName, AbstractBeanDefinition mbd, BeanWrapper bw, MutablePropertyValues pvs) {
    // 得到符合下麵條件的屬性名稱
    // 1.有setter方法
    // 2.需要進行依賴檢查
    // 3.不包含在XML配置中
    // 4.不是簡單類型(基本數據類型,枚舉,日期等)
    // 這裏可以看到XML配置優先級高於自動注入的優先級
    // 不進行依賴檢查的屬性,也不會進行屬性注入
    String[] propertyNames = unsatisfiedNonSimpleProperties(mbd, bw);
    for (String propertyName : propertyNames) {
        if (containsBean(propertyName)) {
            Object bean = getBean(propertyName);
            // 將自動注入的屬性添加到pvs中去
            pvs.add(propertyName, bean);
            // 註冊bean之間的依賴關係
            registerDependentBean(propertyName, beanName);
            // 忽略日誌
        }
        // 忽略日誌
    }
}

看到了嗎?代碼就是這麼的簡單,不是要通過名稱注入嗎?直接通過beanName調用getBean,完事兒

autowireByType

	protected void autowireByType(
			String beanName, AbstractBeanDefinition mbd, BeanWrapper bw, MutablePropertyValues pvs) {
		// 這個類型轉換器,主要是在處理@Value時需要使用
		TypeConverter converter = getCustomTypeConverter();
		if (converter == null) {
			converter = bw;
		}

		Set<String> autowiredBeanNames = new LinkedHashSet<>(4);
		// 得到符合下麵條件的屬性名稱
		// 1.有setter方法
		// 2.需要進行依賴檢查
		// 3.不包含在XML配置中
		// 4.不是簡單類型(基本數據類型,枚舉,日期等)
		// 這裏可以看到XML配置優先級高於自動注入的優先級
		String[] propertyNames = unsatisfiedNonSimpleProperties(mbd, bw);
		for (String propertyName : propertyNames) {
			try {
				PropertyDescriptor pd = bw.getPropertyDescriptor(propertyName);
				if (Object.class != pd.getPropertyType()) {
					// 這裏獲取到的就是setter方法的參數,因為我們需要按照類型進行注入嘛
					MethodParameter methodParam = BeanUtils.getWriteMethodParameter(pd);
					
                    // 如果是PriorityOrdered在進行類型匹配時不會去匹配factoryBean
					// 如果不是PriorityOrdered,那麼在查找對應類型的依賴的時候會會去匹factoryBean
				 	// 這就是Spring的一種設計理念,實現了PriorityOrdered接口的Bean被認為是一種
                    // 最高優先級的Bean,這一類的Bean在進行為了完成裝配而去檢查類型時,
                    // 不去檢查factoryBean
                    // 具體可以參考PriorityOrdered接口上的註釋文檔
					boolean eager = !(bw.getWrappedInstance() instanceof PriorityOrdered);
					// 將參數封裝成為一個依賴描述符
					// 依賴描述符會通過:依賴所在的類,字段名/方法名,依賴的具體類型等來描述這個依賴
					DependencyDescriptor desc = new AutowireByTypeDependencyDescriptor(methodParam, eager);
					// 解析依賴,這裡會處理@Value註解
                    // 另外,通過指定的類型到容器中查找對應的bean
					Object autowiredArgument = resolveDependency(desc, beanName, autowiredBeanNames, converter);
					if (autowiredArgument != null) {
						// 將查找出來的依賴屬性添加到pvs中,後面會將這個pvs應用到bean上
						pvs.add(propertyName, autowiredArgument);
					}
					// 註冊bean直接的依賴關係
					for (String autowiredBeanName : autowiredBeanNames) {
						registerDependentBean(autowiredBeanName, beanName);
						if (logger.isDebugEnabled()) {
							logger.debug("Autowiring by type from bean name '" + beanName + "' via property '" +
									propertyName + "' to bean named '" + autowiredBeanName + "'");
						}
					}
					autowiredBeanNames.clear();
				}
			}
			catch (BeansException ex) {
				throw new UnsatisfiedDependencyException(mbd.getResourceDescription(), beanName, propertyName, ex);
			}
		}
	}

resolveDependency

這個方法在Spring雜談 | 什麼是ObjectFactory?什麼是ObjectProvider?已經做過分析了,本文不再贅述。

可以看到,真正做事的方法是doResolveDependency

@Override
public Object resolveDependency(DependencyDescriptor descriptor, String requestingBeanName, Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException {
	// descriptor代表當前需要注入的那個字段,或者方法的參數,也就是注入點
    // ParameterNameDiscovery用於解析方法參數名稱
    descriptor.initParameterNameDiscovery(getParameterNameDiscoverer());
    // 1. Optional<T>
    if (Optional.class == descriptor.getDependencyType()) {
        return createOptionalDependency(descriptor, requestingBeanName);
    // 2. ObjectFactory<T>、ObjectProvider<T>
    } else if (ObjectFactory.class == descriptor.getDependencyType() ||
             ObjectProvider.class == descriptor.getDependencyType()) {
        return new DependencyObjectProvider(descriptor, requestingBeanName);
    // 3. javax.inject.Provider<T>
    } else if (javaxInjectProviderClass == descriptor.getDependencyType()) {
        return new Jsr330Factory().createDependencyProvider(descriptor, requestingBeanName);
    } else {
        // 4. @Lazy
        Object result = getAutowireCandidateResolver().getLazyResolutionProxyIfNecessary(
            descriptor, requestingBeanName);
        // 5. 正常情況
        if (result == null) {
            result = doResolveDependency(descriptor, requestingBeanName, autowiredBeanNames, typeConverter);
        }
        return result;
    }
}
doResolveDependency
	public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable String beanName,
			@Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException {

		InjectionPoint previousInjectionPoint = ConstructorResolver.setCurrentInjectionPoint(descriptor);
		try {
			Object shortcut = descriptor.resolveShortcut(this);
			if (shortcut != null) {
				return shortcut;
			}
			// 依賴的具體類型
			Class<?> type = descriptor.getDependencyType();
			// 處理@Value註解,這裏得到的時候@Value中的值
			Object value = getAutowireCandidateResolver().getSuggestedValue(descriptor);
			if (value != null) {
				if (value instanceof String) {
					// 解析@Value中的佔位符
					String strVal = resolveEmbeddedValue((String) value);
					// 獲取到對應的bd
					BeanDefinition bd = (beanName != null && containsBean(beanName) ? getMergedBeanDefinition(beanName) : null);
					// 處理EL表達式
					value = evaluateBeanDefinitionString(strVal, bd);
				}
				// 通過解析el表達式可能還需要進行類型轉換
				TypeConverter converter = (typeConverter != null ? typeConverter : getTypeConverter());
				return (descriptor.getField() != null ?
						converter.convertIfNecessary(value, type, descriptor.getField()) :
						converter.convertIfNecessary(value, type, descriptor.getMethodParameter()));
			}
			
            // 對map,collection,數組類型的依賴進行處理
			// 最終會根據集合中的元素類型,調用findAutowireCandidates方法
			Object multipleBeans = resolveMultipleBeans(descriptor, beanName, autowiredBeanNames, typeConverter);
			if (multipleBeans != null) {
				return multipleBeans;
			}
			
            // 根據指定類型可能會找到多個bean
            // 這裏返回的既有可能是對象,也有可能是對象的類型
            // 這是因為到這裏還不能明確的確定當前bean到底依賴的是哪一個bean
            // 所以如果只會返回這個依賴的類型以及對應名稱,最後還需要調用getBean(beanName)
            // 去創建這個Bean
			Map<String, Object> matchingBeans = findAutowireCandidates(beanName, type, descriptor);
			// 一個都沒找到,直接拋出異常
			if (matchingBeans.isEmpty()) {
				if (isRequired(descriptor)) {
					raiseNoMatchingBeanFound(type, descriptor.getResolvableType(), descriptor);
				}
				return null;
			}

			String autowiredBeanName;
			Object instanceCandidate;
			// 通過類型找到了多個
			if (matchingBeans.size() > 1) {
				// 根據是否是主Bean
				// 是否是最高優先級的Bean
				// 是否是名稱匹配的Bean
				// 來確定具體的需要注入的Bean的名稱
                // 到這裏可以知道,Spring在查找依賴的時候遵循先類型再名稱的原則(沒有@Qualifier註解情況下)
				autowiredBeanName = determineAutowireCandidate(matchingBeans, descriptor);
				if (autowiredBeanName == null) {
					// 無法推斷出具體的名稱
					// 如果依賴是必須的,直接拋出異常
					// 如果依賴不是必須的,但是這個依賴類型不是集合或者數組,那麼也拋出異常
					if (isRequired(descriptor) || !indicatesMultipleBeans(type)) {
						return descriptor.resolveNotUnique(type, matchingBeans);
					}
					// 依賴不是必須的,但是依賴類型是集合或者數組,那麼返回一個null
					else {
						return null;
					}
				}
				instanceCandidate = matchingBeans.get(autowiredBeanName);
			}
			else {
				// 直接找到了一個對應的Bean
				Map.Entry<String, Object> entry = matchingBeans.entrySet().iterator().next();
				autowiredBeanName = entry.getKey();
				instanceCandidate = entry.getValue();
			}
			if (autowiredBeanNames != null) {
				autowiredBeanNames.add(autowiredBeanName);
			}
            
            // 前面已經說過了,這裏可能返回的是Bean的類型,所以需要進一步調用getBean
			if (instanceCandidate instanceof Class) {
				instanceCandidate = descriptor.resolveCandidate(autowiredBeanName, type, this);
			}
            
            // 做一些檢查,如果依賴是必須的,查找出來的依賴是一個null,那麼報錯
            // 查詢處理的依賴類型不符合,也報錯
			Object result = instanceCandidate;
			if (result instanceof NullBean) {
				if (isRequired(descriptor)) {
					raiseNoMatchingBeanFound(type, descriptor.getResolvableType(), descriptor);
				}
				result = null;
			}
			if (!ClassUtils.isAssignableValue(type, result)) {
				throw new BeanNotOfRequiredTypeException(autowiredBeanName, type, instanceCandidate.getClass());
			}
			return result;
		}
		finally {
			ConstructorResolver.setCurrentInjectionPoint(previousInjectionPoint);
		}
	}
findAutowireCandidates
protected Map<String, Object> findAutowireCandidates(
    @Nullable String beanName, Class<?> requiredType, DependencyDescriptor descriptor) {
	
    // 簡單來說,這裏就是到容器中查詢requiredType類型的所有bean的名稱的集合
    // 這裡會根據descriptor.isEager()來決定是否要匹配factoryBean類型的Bean
    // 如果isEager()為true,那麼會匹配factoryBean,反之,不會
    String[] candidateNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
        this, requiredType, true, descriptor.isEager());
   
    Map<String, Object> result = new LinkedHashMap<>(candidateNames.length);
   
    // 第一步會到resolvableDependencies這個集合中查詢是否已經存在了解析好的依賴
    // 像我們之所以能夠直接在Bean中注入applicationContext對象
    // 就是因為Spring之前就將這個對象放入了resolvableDependencies集合中
    for (Class<?> autowiringType : this.resolvableDependencies.keySet()) {
        if (autowiringType.isAssignableFrom(requiredType)) {
            Object autowiringValue = this.resolvableDependencies.get(autowiringType);
            
            // 如果resolvableDependencies放入的是一個ObjectFactory類型的依賴
            // 那麼在這裡會生成一個代理對象
            // 例如,我們可以在controller中直接注入request對象
            // 就是因為,容器啟動時就在resolvableDependencies放入了一個鍵值對
            // 其中key為:Request.class,value為:ObjectFactory
            // 在實際注入時放入的是一個代理對象
            autowiringValue = AutowireUtils.resolveAutowiringValue(autowiringValue, requiredType);
            if (requiredType.isInstance(autowiringValue)) {
                // 這裏放入的key不是Bean的名稱
                // value是實際依賴的對象
                result.put(ObjectUtils.identityToString(autowiringValue), autowiringValue);
                break;
            }
        }
    }
    
    // 接下來開始對之前查找出來的類型匹配的所有BeanName進行處理
    for (String candidate : candidateNames) {
        // 不是自引用,什麼是自引用?
        // 1.候選的Bean的名稱跟需要進行注入的Bean名稱相同,意味着,自己注入自己
        // 2.或者候選的Bean對應的factoryBean的名稱跟需要注入的Bean名稱相同,
        // 也就是說A依賴了B但是B的創建又需要依賴A
        // 要符合注入的條件
        if (!isSelfReference(beanName, candidate) && isAutowireCandidate(candidate, descriptor)) {
            // 調用addCandidateEntry,加入到返回集合中,後文有對這個方法的分析
            addCandidateEntry(result, candidate, descriptor, requiredType);
        }
    }
    
    // 排除自引用的情況下,沒有找到一個合適的依賴
    if (result.isEmpty() && !indicatesMultipleBeans(requiredType)) {
        // 1.先走fallback邏輯,Spring提供的一個擴展吧,感覺沒什麼卵用
        // 默認情況下fallback的依賴描述符就是自身
        DependencyDescriptor fallbackDescriptor = descriptor.forFallbackMatch();
        for (String candidate : candidateNames) {
            if (!isSelfReference(beanName, candidate) && isAutowireCandidate(candidate, fallbackDescriptor)) {
                addCandidateEntry(result, candidate, descriptor, requiredType);
            }
        }
        // fallback還是失敗
        if (result.isEmpty()) {
            // 處理自引用
            // 從這裏可以看出,自引用的優先級是很低的,只有在容器中真正的只有這個Bean能作為
            // 候選者的時候,才會去處理,否則自引用是被排除掉的
            for (String candidate : candidateNames) {
                if (isSelfReference(beanName, candidate) &&
                    // 不是一個集合或者
                    // 是一個集合,但是beanName跟candidate的factoryBeanName相同
                    (!(descriptor instanceof MultiElementDescriptor) || !beanName.equals(candidate)) &&
                    isAutowireCandidate(candidate, fallbackDescriptor)) {
                    addCandidateEntry(result, candidate, descriptor, requiredType);
                }
            }
        }
    }
    return result;
}


// candidates:就是findAutowireCandidates方法要返回的候選集合
// candidateName:當前的這個候選Bean的名稱
// descriptor:依賴描述符
// requiredType:依賴的類型
private void addCandidateEntry(Map<String, Object> candidates, String candidateName,
                               DependencyDescriptor descriptor, Class<?> requiredType) {
	
    // 如果依賴是一個集合,或者容器中已經包含這個單例了
    // 那麼直接調用getBean方法創建或者獲取這個Bean
    if (descriptor instanceof MultiElementDescriptor || containsSingleton(candidateName)) {
        Object beanInstance = descriptor.resolveCandidate(candidateName, requiredType, this);
        candidates.put(candidateName, (beanInstance instanceof NullBean ? null : beanInstance));
    }
    // 如果依賴的類型不是一個集合,這個時候還不能確定到底要使用哪個依賴,
    // 所以不能將這些Bean創建出來,所以這個時候,放入candidates是Bean的名稱以及類型
    else {
        candidates.put(candidateName, getType(candidateName));
    }
}

處理屬性注入(@Autowired)

postProcessProperties

// 在applyMergedBeanDefinitionPostProcessors方法執行的時候,
// 已經解析過了@Autowired註解(buildAutowiringMetadata方法)
public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) {
    // 這裏獲取到的是解析過的緩存好的注入元數據
    InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs);
    try {
        // 直接調用inject方法
        // 存在兩種InjectionMetadata
        // 1.AutowiredFieldElement
        // 2.AutowiredMethodElement
        // 分別對應字段的屬性注入以及方法的屬性注入
        metadata.inject(bean, beanName, pvs);
    }
    catch (BeanCreationException ex) {
        throw ex;
    }
    catch (Throwable ex) {
        throw new BeanCreationException(beanName, "Injection of autowired dependencies failed", ex);
    }
    return pvs;
}
字段的屬性注入
// 最終反射調用filed.set方法
protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
    Field field = (Field) this.member;
    Object value;
    if (this.cached) {
        // 第一次注入的時候肯定沒有緩存
        // 這裏也是對原型情況的處理
        value = resolvedCachedArgument(beanName, this.cachedFieldValue);
    } else {
        DependencyDescriptor desc = new DependencyDescriptor(field, this.required);
        desc.setContainingClass(bean.getClass());
        Set<String> autowiredBeanNames = new LinkedHashSet<>(1);
        Assert.state(beanFactory != null, "No BeanFactory available");
        TypeConverter typeConverter = beanFactory.getTypeConverter();
        try {
            // 這裏可以看到,對@Autowired註解在字段上的處理
            // 跟byType下自動注入的處理是一樣的,就是調用resolveDependency方法
            value = beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter);
        } catch (BeansException ex) {
            throw new UnsatisfiedDependencyException(null, beanName, new InjectionPoint(field), ex);
        }
        synchronized (this) {
            // 沒有緩存過的話,這裏需要進行緩存
            if (!this.cached) {
                if (value != null || this.required) {
                    this.cachedFieldValue = desc;
                    // 註冊Bean之間的依賴關係
                    registerDependentBeans(beanName, autowiredBeanNames);
                    // 如果這個類型的依賴只存在一個的話,我們就能確定這個Bean的名稱
                    // 那麼直接將這個名稱緩存到ShortcutDependencyDescriptor中
                    // 第二次進行注入的時候就可以直接調用getBean(beanName)得到這個依賴了
                    // 實際上正常也只有一個,多個就報錯了
                    // 另外這裡會過濾掉@Vlaue得到的依賴
                    if (autowiredBeanNames.size() == 1) {
                        String autowiredBeanName = autowiredBeanNames.iterator().next();
                        // 通過resolvableDependencies這個集合找的依賴不滿足containsBean條件
                        // 不會進行緩存,因為緩存實際還是要調用getBean,而resolvableDependencies
                        // 是沒法通過getBean獲取的
                        if (beanFactory.containsBean(autowiredBeanName) &&
                            beanFactory.isTypeMatch(autowiredBeanName, field.getType())) {							 // 依賴描述符封裝成ShortcutDependencyDescriptor進行緩存
                            this.cachedFieldValue = new ShortcutDependencyDescriptor(
                                desc, autowiredBeanName, field.getType());
                        }
                    }
                } else {
                    this.cachedFieldValue = null;
                }
                this.cached = true;
            }
        }
    }
    if (value != null) {
        // 反射調用Field.set方法
        ReflectionUtils.makeAccessible(field);
        field.set(bean, value);
    }
}
方法的屬性注入
// 代碼看着很長,實際上邏輯跟字段注入基本一樣
protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
    // 判斷XML中是否配置了這個屬性,如果配置了直接跳過
    // 換而言之,XML配置的屬性優先級高於@Autowired註解
    if (checkPropertySkipping(pvs)) {
        return;
    }
    Method method = (Method) this.member;
    Object[] arguments;
    if (this.cached) {
        arguments = resolveCachedArguments(beanName);
    } else {
        // 通過方法參數類型構造依賴描述符
        // 邏輯基本一樣的,最終也是調用beanFactory.resolveDependency方法
        Class<?>[] paramTypes = method.getParameterTypes();
        arguments = new Object[paramTypes.length];
        DependencyDescriptor[] descriptors = new DependencyDescriptor[paramTypes.length];
        Set<String> autowiredBeans = new LinkedHashSet<>(paramTypes.length);
        Assert.state(beanFactory != null, "No BeanFactory available");
        TypeConverter typeConverter = beanFactory.getTypeConverter();
        
        // 遍歷方法的每個參數
        for (int i = 0; i < arguments.length; i++) {
            MethodParameter methodParam = new MethodParameter(method, i);
            DependencyDescriptor currDesc = new DependencyDescriptor(methodParam, this.required);
            currDesc.setContainingClass(bean.getClass());
            descriptors[i] = currDesc;
            try {
                // 還是要調用這個方法
                Object arg = beanFactory.resolveDependency(currDesc, beanName, autowiredBeans, typeConverter);
                if (arg == null && !this.required) {
                    arguments = null;
                    break;
                }
                arguments[i] = arg;
            } catch (BeansException ex) {
                throw new UnsatisfiedDependencyException(null, beanName, new InjectionPoint(methodParam), ex);
            }
        }
        synchronized (this) {
            if (!this.cached) {
                if (arguments != null) {
                    Object[] cachedMethodArguments = new Object[paramTypes.length];
                    System.arraycopy(descriptors, 0, cachedMethodArguments, 0, arguments.length);  
                    // 註冊bean之間的依賴關係
                    registerDependentBeans(beanName, autowiredBeans);
                    
                    // 跟字段注入差不多,存在@Value註解,不進行緩存
                    if (autowiredBeans.size() == paramTypes.length) {
                        Iterator<String> it = autowiredBeans.iterator();
                        for (int i = 0; i < paramTypes.length; i++) {
                            String autowiredBeanName = it.next();
                            if (beanFactory.containsBean(autowiredBeanName) &&
                                beanFactory.isTypeMatch(autowiredBeanName, paramTypes[i])) {
                                cachedMethodArguments[i] = new ShortcutDependencyDescriptor(
                                    descriptors[i], autowiredBeanName, paramTypes[i]);
                            }
                        }
                    }
                    this.cachedMethodArguments = cachedMethodArguments;
                } else {
                    this.cachedMethodArguments = null;
                }
                this.cached = true;
            }
        }
    }
    if (arguments != null) {
        try {
            // 反射調用方法
            // 像我們的setter方法就是在這裏調用的
            ReflectionUtils.makeAccessible(method);
            method.invoke(bean, arguments);
        } catch (InvocationTargetException ex) {
            throw ex.getTargetException();
        }
    }
}

處理依賴檢查

protected void checkDependencies(
    String beanName, AbstractBeanDefinition mbd, PropertyDescriptor[] pds, PropertyValues pvs)
    throws UnsatisfiedDependencyException {

    int dependencyCheck = mbd.getDependencyCheck();
    for (PropertyDescriptor pd : pds) {
        
        // 有set方法但是在pvs中沒有對應屬性,那麼需要判斷這個屬性是否要進行依賴檢查
        // 如果需要進行依賴檢查的話,就需要報錯了
        // pvs中保存的是自動注入以及XML配置的屬性
        if (pd.getWriteMethod() != null && !pvs.contains(pd.getName())) {
           
            // 是否是基本屬性,枚舉/日期等也包括在內
            boolean isSimple = BeanUtils.isSimpleProperty(pd.getPropertyType());
           	
            // 如果DEPENDENCY_CHECK_ALL,對任意屬性都開啟了依賴檢查,報錯
            // DEPENDENCY_CHECK_SIMPLE,對基本屬性開啟了依賴檢查並且是基本屬性,報錯
            // DEPENDENCY_CHECK_OBJECTS,對非基本屬性開啟了依賴檢查並且不是非基本屬性,報錯
            boolean unsatisfied = (dependencyCheck == AbstractBeanDefinition.DEPENDENCY_CHECK_ALL) ||
                (isSimple && dependencyCheck == AbstractBeanDefinition.DEPENDENCY_CHECK_SIMPLE) ||
                (!isSimple && dependencyCheck == AbstractBeanDefinition.DEPENDENCY_CHECK_OBJECTS);
            
            if (unsatisfied) {
                throw new UnsatisfiedDependencyException(mbd.getResourceDescription(), beanName, pd.getName(),
                                                         "Set this property value or disable dependency checking for this bean.");
            }
        }
    }
}

將解析出來的屬性應用到Bean上

到這一步解析出來的屬性主要有三個來源

  1. XML中配置的
  2. 通過byName的方式自動注入的
  3. 通過byType的方式自動注入的

但是在應用到Bean前還需要做一步類型轉換,這一部分代碼實際上跟我們之前在Spring官網閱讀(十四)Spring中的BeanWrapper及類型轉換介紹的差不多,而且因為XML跟自動注入的方式都不常見,正常@Autowired的方式進行注入的話,這個方法沒有什麼用,所以本文就不再贅述。

總結

本文我們主要分析了Spring在屬性注入過程中的相關代碼,整個屬性注入可以分為兩個部分

  1. @Autowired/@Vale的方式完成屬性注入
  2. 自動注入(byType/byName

完成屬性注入的核心方法其實就是doResolveDependencydoResolveDependency這個方法的邏輯簡單來說分為兩步:

  1. 通過依賴類型查詢到所有的類型匹配的bean的名稱
  2. 如果找到了多個的話,再根據依賴的名稱匹配對應的Bean的名稱
  3. 調用getBean得到這個需要被注入的Bean
  4. 最後反射調用字段的set方法完成屬性注入

從上面也可以知道,其實整個屬性注入的邏輯是很簡單的。

如果本文對你有幫助的話,記得點個贊吧!也歡迎關注我的公眾號,微信搜索:程序員DMZ,或者掃描下方二維碼,跟着我一起認認真真學Java,踏踏實實做一個coder。

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

【其他文章推薦】

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

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

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

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

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

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

【離散優化】覆蓋問題

覆蓋問題

我們知道設施選址問題有兩類基礎問題,分別是中值問題和覆蓋問題,下面要介紹的就是覆蓋問題。

什麼是覆蓋問題?

覆蓋問題是以所期望的服務範圍滿足大多數或者所有用戶需求為前提,確定設施的位置。覆蓋模型的思想是離服務設施較近的用戶越多,則服務越好。

覆蓋問題的分類

覆蓋問題主要分為兩類:

  • 集合覆蓋問題(Location Set Covering Problem,LSCP)
  • 最大覆蓋問題(Maximum Covering Location Problem,MCLP)

覆蓋模型常用於哪些場景?

由於 P-中值模型常以總距離或者總時間作為測度指標,使得其並不適用於一些特殊的場景,比如消防中心和救護車等應急設施的區位選址問題,而覆蓋模型則比較適用於這些場景。

如何定義覆蓋?

如果需求點 \(i\) 到備選設施點 \(j\) 的距離或者時間小於臨界值 \(D_c\),那麼稱需求點 \(i\) 被候選設施點 \(j\) 覆蓋。、

下面介紹兩類覆蓋問題的數學模型表達

集合覆蓋問題 (Location Set Covering Problem,LSCP)

目標函數:

\[\min \sum_{j \in J}x_j \]

約束:

\[\sum_{j \in N_i} x_j \geqslant 1 \quad \forall i \in I \tag{c-1} \]

\[x_j \in \{0, 1\} \quad \forall j \in J \tag{c-2} \]

其中,

  • \(N_i = \{j:a_{ij}=1\}\) 是覆蓋需求點 \(i\) 的候選設施點的集合,變量 \(a_{ij}\) 用來判斷需求點 \(i\) 是否被候選設施點 \(j\) 覆蓋,若是,則 \(a_{ij}=1\),否則 \(a_{ij}=0\)
  • 目標函數旨在尋求設施總量最小
  • 約束 \(c-1\) 保證每個需求點至少被一個設施服務範圍所覆蓋
  • 約束 \(c-2\) 是決策變量的取值範圍

在某些場景中,集合覆蓋問題有以下兩個缺點:

  • 為了保證所有需求點均被覆蓋而引入過多的設施,以至於超出預算
  • 模型無法區分需求點的需求強度

現實生活中,常常由於預算或者資源的約束,有限的設施不能保證空間中所有需求點都被覆蓋,此時,優先考慮需求強度大的需求點是十分必要的,下面要介紹的最大覆蓋模型就是為了解決這個問題而被提出。

最大覆蓋問題(Maximum Covering Location Problem,MCLP)

目標函數

\[\max \sum_{i \in N_i} \omega_iz_i \]

約束

\[z_i \leqslant \sum_{j \in N_i}x_j \quad \forall i \in I \tag{c-1} \]

\[\sum_{j\in J}x_j = p \tag{c-2} \]

\[x_j \in \{0,1\} \quad \forall j \in J \tag{c-3} \]

\[z_i = \{0, 1\} \quad \forall i \in I \tag{c-4} \]

其中,

  • \(\omega_i\) 為需求點 \(i\) 的需求強度

  • \(z_i\) 用來判斷需求點 \(i\) 是否被覆蓋,若覆蓋,則為 1,否則為 0

  • 目標函數旨在尋求有限設施(\(p\) 個)覆蓋的需求最多

  • 約束 \(c-1\) 要求除非在備選設施點中已定位一個設施可以覆蓋需求點 \(i\),否則需求點 \(i\) 將不被記作被覆蓋

  • 約束 \(c-2\) 限制設施的總數為 \(p\)

  • 約束 \(c-3, c-4\) 是決策變量的取值範圍

更多種類的選址問題

以上介紹的覆蓋問題的基礎模型框架,然而具體問題一般是較為複雜的設施選址問題,這就需要我們對基礎模型設置不同的條件從而進行擴展,比如:

  • 用於環境污染防治的鄰避型設施選址問題
  • 用於不同服務等級的層次型設置選址問題
  • 用於商業競爭的競爭型設施選址問題
  • 選址問題也開始考慮動態、不確定性等因素

總結

總結以上兩類問題,我們可以發現最大覆蓋模型和集合覆蓋模型的主要區別在於對設施數量和需求強度的關注不同,前者一般適用於建設經費充足或者設施成本相同的情況,後者則適用於有設施成本約束的選址決策。

參考文獻

本文內容主要從論文《設施選址問題中的基礎模型與求解方法比較》總結而來。

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

【其他文章推薦】

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

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

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

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

※超省錢租車方案

記一次uboot升級過程的兩個坑

背景

之前做過一次uboot的升級,當時留下了一些記錄,本文摘錄其中比較有意思的兩個問題。

啟動失敗問題

問題簡述

uboot代碼中用到了一個庫,考慮到庫本身跟uboot版本沒什麼關係,就直接把舊的庫文件拷貝過來使用。結果編譯鏈接是沒問題,啟動卻會卡住。

消失的打印

為了明確卡住的位置,就去修改了庫的源碼,添加一些打印(此時還是在舊版本uboot下編譯的),結果發現卡住的位置或隨着添加打印的變化而變化,且有些打印語句,添加后未打印出來。

我決定先從這些神秘消失的打印入手。

分析下uboot中的printf實現,最底層就是寫寄存器,是一個同步的函數,也沒什麼可疑的地方。

為了確認打印不出來的時候,到底有沒有調用到printf,我決定給printf增加一個計數器,在gd結構體中,增加一個printf_count字段,初始化為0,每次打印時執行printf_count++並打印出值。

設計這個試驗,本意是確認未打印出來時是否確實也調用到了printf,但卻有了別的發現,實驗結果中printf_count值會異常變化,不是按打印順序遞增,而是會突變成很大的異常值。

printf_countgd結構體的成員,那就是gd的問題了。進一步將uboot全局結構體gd的地址打印出來。確認了原因是gd結構體的指針變化了。

這也可以解釋部分打印消失的現象,原因是我們在gd中有另一個字段,用於控制打印等級。當gd被改動了,printf就可能解析出錯,誤以為打印等級為0而提前返回。

gd的實現

那麼好端端的,gd為什麼會被改了呢?這就要先看看gd到底是怎麼實現的了。

uboot中維護了一個全局的結構體gd。在代碼中加入

DECLARE_GLOBAL_DATA_PTR;

即可使用gd指針訪問這個全局結構體,許多地方都會藉助gd來保存傳遞信息。

進一步看看這個宏的定義

舊版本uboot:
#define DECLARE_GLOBAL_DATA_PTR        register volatile gd_t *gd asm ("r8")

新版本uboot:
#define DECLARE_GLOBAL_DATA_PTR        register volatile gd_t *gd asm ("r9")

居然不一樣,一個是將gd的值放到r8寄存器,一個是放在r9寄存器。

那麼就可以猜測到,庫是在舊版本uboot中編譯出來的,可能使用了r9,那麼放到新版本uboot中去,就會破壞r9寄存器中保存的gd值,導致一系列依賴gd的代碼不能正常工作。

驗證改動

為了求證,將庫反彙編出來,發現確實避開了r8寄存器,但使用了r9寄存器。

說明uboot在指定gd寄存器的同時,還有某種方法讓其他代碼不使用這個寄存器。

那是不是把舊uboot中的這個r8改成r9,重新編譯庫就可以了呢?試一下,還是不行。

那麼禁止其他代碼使用r8寄存器肯定就是通過別的方式實現的了。簡單粗暴地在舊版本uboot下搜索r8,去掉.c .h等類型后,很容易發現了

./arch/arm/cpu/armv7/config.mk:24:PLATFORM_RELFLAGS += -fno-common -ffixed-r8 -msoft-floa

-ffixed-r8修改為-ffixed-r9,重新編譯出庫,這回就可以正常工作了,打印正常,啟動正常。反彙編出來也可以看到,新編譯出來的庫用了r8沒有用r9

當然更好的改法,是直接在新版本的uboot中編譯,這是最可靠的。

追本溯源

話說回來,為什麼兩個版本的uboot,會使用不同的寄存器呢?難道有什麼坑?

這就得去翻一下git記錄了。

commit fe1378a961e508b31b1f29a2bb08ba1dac063155
Author: Jeroen Hofstee <jeroen@myspectrum.nl>
Date:   Sat Sep 21 14:04:41 2013 +0200

    ARM: use r9 for gd
    
    To be more EABI compliant and as a preparation for building
    with clang, use the platform-specific r9 register for gd
    instead of r8.
    
    note: The FIQ is not updated since it is not used in u-boot,
    and under discussion for the time being.
    
    The following checkpatch warning is ignored:
    WARNING: Use of volatile is usually wrong: see
    Documentation/volatile-considered-harmful.txt
    
    Signed-off-by: Jeroen Hofstee <jeroen@myspectrum.nl>
    cc: Albert ARIBAUD <albert.u.boot@aribaud.net>

git記錄中,也可以確認完整地將r8切換到r9,都需要做哪些修改

diff --git a/arch/arm/config.mk b/arch/arm/config.mk
index 16c2e3d1e0..d0cf43ff41 100644
--- a/arch/arm/config.mk
+++ b/arch/arm/config.mk
@@ -17,7 +17,7 @@ endif
 
 LDFLAGS_FINAL += --gc-sections
 PLATFORM_RELFLAGS += -ffunction-sections -fdata-sections \
-                     -fno-common -ffixed-r8 -msoft-float
+                     -fno-common -ffixed-r9 -msoft-float
 
 # Support generic board on ARM
 __HAVE_ARCH_GENERIC_BOARD := y
diff --git a/arch/arm/cpu/armv7/lowlevel_init.S b/arch/arm/cpu/armv7/lowlevel_init.S
index 82b2b86520..69e3053a42 100644
--- a/arch/arm/cpu/armv7/lowlevel_init.S
+++ b/arch/arm/cpu/armv7/lowlevel_init.S
@@ -22,11 +22,11 @@ ENTRY(lowlevel_init)
        ldr     sp, =CONFIG_SYS_INIT_SP_ADDR
        bic     sp, sp, #7 /* 8-byte alignment for ABI compliance */
 #ifdef CONFIG_SPL_BUILD
-       ldr     r8, =gdata
+       ldr     r9, =gdata
 #else
        sub     sp, #GD_SIZE
        bic     sp, sp, #7
-       mov     r8, sp
+       mov     r9, sp
 #endif
        /*
         * Save the old lr(passed in ip) and the current lr to stack
diff --git a/arch/arm/include/asm/global_data.h b/arch/arm/include/asm/global_data.h
index 79a9597419..e126436093 100644
--- a/arch/arm/include/asm/global_data.h
+++ b/arch/arm/include/asm/global_data.h
@@ -47,6 +47,6 @@ struct arch_global_data {
 
 #include <asm-generic/global_data.h>
 
-#define DECLARE_GLOBAL_DATA_PTR     register volatile gd_t *gd asm ("r8")
+#define DECLARE_GLOBAL_DATA_PTR     register volatile gd_t *gd asm ("r9")
 
 #endif /* __ASM_GBL_DATA_H */
diff --git a/arch/arm/lib/crt0.S b/arch/arm/lib/crt0.S
index 960d12e732..ac54b9359a 100644
--- a/arch/arm/lib/crt0.S
+++ b/arch/arm/lib/crt0.S
@@ -69,7 +69,7 @@ ENTRY(_main)
        bic     sp, sp, #7      /* 8-byte alignment for ABI compliance */
        sub     sp, #GD_SIZE    /* allocate one GD above SP */
        bic     sp, sp, #7      /* 8-byte alignment for ABI compliance */
-       mov     r8, sp          /* GD is above SP */
+       mov     r9, sp          /* GD is above SP */
        mov     r0, #0
        bl      board_init_f
 
@@ -81,15 +81,15 @@ ENTRY(_main)
  * 'here' but relocated.
  */
 
-       ldr     sp, [r8, #GD_START_ADDR_SP]     /* sp = gd->start_addr_sp */
+       ldr     sp, [r9, #GD_START_ADDR_SP]     /* sp = gd->start_addr_sp */
        bic     sp, sp, #7      /* 8-byte alignment for ABI compliance */
-       ldr     r8, [r8, #GD_BD]                /* r8 = gd->bd */
-       sub     r8, r8, #GD_SIZE                /* new GD is below bd */
+       ldr     r9, [r9, #GD_BD]                /* r9 = gd->bd */
+       sub     r9, r9, #GD_SIZE                /* new GD is below bd */
 
        adr     lr, here
-       ldr     r0, [r8, #GD_RELOC_OFF]         /* r0 = gd->reloc_off */
+       ldr     r0, [r9, #GD_RELOC_OFF]         /* r0 = gd->reloc_off */
        add     lr, lr, r0
-       ldr     r0, [r8, #GD_RELOCADDR]         /* r0 = gd->relocaddr */
+       ldr     r0, [r9, #GD_RELOCADDR]         /* r0 = gd->relocaddr */
        b       relocate_code
 here:
 
@@ -111,8 +111,8 @@ clbss_l:cmp r0, r1                  /* while not at end of BSS */
        bl red_led_on
 
        /* call board_init_r(gd_t *id, ulong dest_addr) */
-       mov     r0, r8                  /* gd_t */
-       ldr     r1, [r8, #GD_RELOCADDR] /* dest_addr */
+       mov     r0, r9                  /* gd_t */
+       ldr     r1, [r9, #GD_RELOCADDR] /* dest_addr */
        /* call board_init_r */
        ldr     pc, =board_init_r       /* this is auto-relocated! */

啟動慢問題

問題簡述

填了幾個坑之後,新的uboot可以啟動到內核了,但發現啟動速度非常慢,內核啟動速度慢了接近10倍!明明是同一個內核,為什麼差異這麼大。

排查寄存器

初步排查了下設備樹配置,以及uboot跳轉內核前的一些關鍵寄存器,確實在兩個版本的uboot中有所不同,但具體去看這些不同,發現都不會影響速度,將一些驅動對齊之後寄存器差異基本就消失了。

差異的分界

那再細看,kernel的速度有差異,uboot呢?在哪個時間點之後,速度開始產生差異?

嘗試在兩個版本的uboot中插入一些操作,對比時間戳,發現兩個uboot在某個節點之後的速度確實有區別。

進一步排查,原來是在打開cache操作之後,舊uboot的速度就會比新uboot快。嘗試將舊ubootcache關掉,則二者基本一致。嘗試將舊uboot操作cache的代碼,移植到新uboot,未發生改變。

此時可確認新uboot的開cache有問題。但覺得這個跟kernel啟動慢沒關係。因為uboot進入kernel之前都會關cache,由kernel自己去重新打開。

也就是不管是用哪份uboot,也不管uboot中是否開了cache,對kernel階段都應該沒有影響才對。

於是記錄下來uboot的這個問題,待後續修復。先繼續找kernel啟動慢的原因。(注:現在看來當時的做法是有問題的,這裏的異常這麼明顯,應該設法追蹤下去找出原因才對)

鎖定uboot

uboot的嫌疑非常大,但還不能完全確認,因為uboot之前還有一級spl。是否會是spl的問題呢?

嘗試改用新spl+舊uboot,啟動速度正常。而新spl+新uboot的啟動速度則很慢,其他因素都不變,說明問題確實出在uboot階段。

多做or少做

當時到這一步就卡住了,直接比較兩份uboot的代碼不太現實,差異太大了。

後來我就給自己提了個問題,到底新uboot是多做了某件事情,還是少做了某件事情?

換個說法,目前已知

spl --> 舊uboot --> kernel(速度快)
spl --> 新uboot --> kernel(速度快)

但到底是以下的情況A還是情況B呢?

A: spl(速度慢) --> 舊uboot(做了某個會提升速度的操作) --> kernel(速度快)
   spl(速度慢) --> 新uboot(少做了某個會提升速度的操作) --> kernel(速度慢)

B: spl(速度快) --> 舊uboot(沒做特殊操作) --> kernel(速度快)
   spl(速度快) --> 新uboot(多做了某個會限制速度的操作) --> kernel(速度慢)

為了驗證,我決定讓spl直接啟動內核,看看內核到底是快是慢。

支持過程碰到了一些小問題

1.spl沒有能力加載這麼大的kernel

解決:此時不需要kernel能完全啟動,只需要能加載啟動一段,足以體現出啟動速度是否正常即可,於是裁剪出一個非常小kernel來輔助實驗。

2.kernel需要dtb

解決:內核有一個CONFIG_BUILD_ARM_APPENDED_DTB_IMAGE選項。選上重新編譯。編譯后再用ddkerneldtb拼接到一起,作為新的kernel。這樣,spl就只需要加載一個文件並跳轉過去即可。

試驗結果,spl啟動的kernel和使用新uboot啟動的kernel速度一致,均比舊uboot啟動的kernel慢。

說明,舊uboot中做了某個關鍵操作,而新uboot沒做。

找出關鍵操作

那接下來的任務就是,找出舊uboot中的這個關鍵操作了。

怎麼找呢?有了上一步的成果,我們可以使用以下方法來排查

  1. spl加載kernel和舊uboot

  2. spl跳轉到舊uboot,此時kernel其實已經在dram中準備好了,隨時可以啟動

  3. 在舊uboot的啟動流程各個階段,嘗試直接跳轉到kernel,觀察啟動速度

  4. 如果在舊ubootA點跳轉kernel啟動慢,B點跳轉啟動快,則說明關鍵操作位於AB點之間。

方法有了,很快就鎖定到start.S,進一步在start.S中揪出了這段代碼

#if defined(CONFIG_ARM_A7)
@set SMP bit
    mrc     p15, 0, r0, c1, c0, 1
    orr        r0, r0, #(1<<6)
    mcr        p15, 0, r0, c1, c0, 1
#endif

ubootstart.S中沒有這段代碼,嘗試在新ubootstart.S中添加此操作,速度立馬恢復正常了。

再全局搜索下,原來這個新版本uboot中,套路是在board_init中進行此項設置的,而這個平台從舊版本移植過來,就沒有設置 SMP bit, 補上即可。

SMP bit是什麼

SMP 是指對稱多處理器,看起來這個 bit 會影響多核的 cache一致性,此處沒有再深入研究。

但可以知道,對於單處理器的情況,也需要設置這個bit才能正常使用cache

貼下arm的圖和描述:

[6]	SMP	

Signals if the Cortex-A9 processor is taking part in coherency or not.

In uniprocessor configurations, if this bit is set, then Inner Cacheable Shared is treated as Cacheable. The reset value is zero.

搜下kernel的代碼,發現也是有地方調用了的。不過這個芯片是單核的,根本就沒配置CONFIG_SMP

#ifdef CONFIG_SMP
	ALT_SMP(mrc	p15, 0, r0, c1, c0, 1)
	ALT_UP(mov	r0, #(1 << 6))		@ fake it for UP
	tst	r0, #(1 << 6)			@ SMP/nAMP mode enabled?
	orreq	r0, r0, #(1 << 6)		@ Enable SMP/nAMP mode
	orreq	r0, r0, r10			@ Enable CPU-specific SMP bits
	mcreq	p15, 0, r0, c1, c0, 1
#endif

總結

整理出來一方面是記錄這兩個bug,另一方面也是想記錄下當時的一些操作。

畢竟同樣的bug可能以後都不會碰到了,但解bug的方法和思路卻是可以積累復用的。

blog: https://www.cnblogs.com/zqb-all/p/13172546.html
公眾號:https://sourl.cn/shT3kz

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

【其他文章推薦】

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

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

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

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

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

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

Python 為什麼不支持 i++ 自增語法,不提供 ++ 操作符?

在 C/C++/Java 等等語言中,整型變量的自增或自減操作是標配,它們又可分為前綴操作(++i 和 –i)與後綴操作(i++ 和 i–),彼此存在着一些細微差別,各有不同的用途。

這些語言的使用者在接觸 Python 時,可能會疑惑為什麼它不提供 ++ 或 — 的操作呢?在我前不久發的《Python的十萬個為什麼?》里,就有不少同學在調查問卷中表示了對此話題感興趣。

Python 中雖然可能出現 ++i 這種前綴形式的寫法,但是它並沒有“++”自增操作符,此處只是兩個“+”(正數符號)的疊加而已,至於後綴形式的“++”,則完全不支持(SyntaxError: invalid syntax)。

本期“Python為什麼 ”欄目,我們將會從兩個主要的角度來回答:Python 為什麼不支持 i++ 自增語法? (PS:此處自增指代“自增和自減”,下同)

首先,Python 當然可以實現自增效果,即寫成i += 1 或者 i = i + 1 ,這在其它語言中也是通用的。

雖然 Python 在底層用了不同的魔術方法(__add__()__iadd__() )來完成計算,但表面上的效果完全相同。

所以,我們的問題可以轉化成:為什麼上面的兩種寫法會勝過 i++,成為 Python 的最終選擇呢?

1、Python 的整數是不可變類型

當我們定義i = 1000 時,不同語言會作出不同的處理:

  • C 之類的語言(寫法 int i = 1000)會申請一塊內存空間,並給它“綁定”一個固定的名稱 i,同時寫入一個可變的值 1000。在這裏,i 的地址以及類型是固定的,而值是可變的(在一定的表示範圍內)
  • Python(寫法i = 1000)也會申請一塊內存空間,但是它會“綁定”給数字 1000,即這個 1000 的地址以及類型是固定的(immutable),至於 i,只是一個名稱標籤貼在 1000 上,自身沒有固定的地址和類型

所以當我們令 i “自增”時(i = i + 1),它們的處理是不同的:

  • C 之類的語言先找到 i 的地址上存的數值,然後令它加 1,操作后新的數值就取代了舊的數值
  • Python 的操作過程是把 i 指向的数字加 1,然後把結果綁定到新申請的一塊內存空間,再把名稱標籤 i “貼”到新的数字上。新舊数字可以同時存在,不是取代關係

打一個不太恰當的比方:C 中的 i 就像一個宿主,数字 1000 寄生在它上面;而 Python 中的 1000 像個宿主,名稱 i 寄生在它上面。C 中的 i 與 Python 中的 1000,它們則寄生在底層的內存空間上……

還可以這樣理解:C 中的變量 i 是一等公民,数字 1000 是它的一個可變的屬性;Python 中的数字 1000 是一等公民,名稱 i 是它的一個可變的屬性。

有了以上的鋪墊,我們再來看看 i++,不難發現:

  • C 之類的語言,i++ 可以表示 i 的数字屬性的增加,它不會開闢新的內存空間,也不會產生新的一等公民
  • Python 之類的語言,i++ 如果是對其名稱屬性的操作,那樣就沒有意義了(總不能按字母表順序,把 i 變成 j 吧);如果理解成對数字本體的操作,那麼情況就會變得複雜:它會產生新的一等公民 1001,因此需要給它分配一個內存地址,此時若佔用 1000 的地址,則涉及舊對象的回收,那原有對於 1000 的引用關係都會受到影響,所以只能開闢新的內存空間給 1001

Python 若支持 i++,其操作過程要比 C 的 i++ 複雜,而且其含義也不再是“令数字增加1”(自增),而是“創建一個新的数字”(新增), 這樣的話,“自增操作符”(increment operator)就名不副實了。

Python 在理論上可以實現 i++ 操作,但它就必須重新定義“自增操作符”,還會令有其它語言經驗的人產生誤解,不如就讓大家直接寫成i += 1 或者 i = i + 1 好了。

2、Python 有可迭代對象

C/C++ 等語言設計出 i++,最主要的目的是為了方便使用三段式的 for 結構:

for(int i = 0; i < 100; i++){
    // 執行 xxx
}

這種程序關心的是数字本身的自增過程,数字做加法與程序體的執行相關聯。

Python 中沒有這種 for 結構的寫法,它提供了更為優雅的方式:

for i in range(100):
    # 執行 xxx

my_list = ["你好", "我是Python貓", "歡迎關注"]
for info in my_list:
    print(info)

這裏體現了不同的思維方式,它關心的是在一個數值範圍內的迭代遍歷,並不關心也不需要人為對数字做加法。

Python 中的可迭代對象/迭代器/生成器提供了非常良好的迭代/遍歷用法,能夠做到對 i++ 的完全替代。

例如,上例中實現了對列表內值的遍歷,Python 還可以用 enumerate() 實現對下標與具體值的同時遍歷:

my_list = ["你好", "我是Python貓", "歡迎關注"]
for i, info in enumerate(my_list):
    print(i, info)

# 打印結果:
0 你好
1 我是Python貓
2 歡迎關注

再例如對於字典的遍歷,Python 提供了 keys()、values()、items() 等遍歷方法,非常好用:

my_dict = {'a': '1', 'b': '2', 'c': '3'}
for key in my_dict.keys():
    print(key)

for key, value in my_dict.items():
    print(key, value)

有了這樣的利器,哪裡還有 i++ 的用武之地呢?

不僅如此,Python 中基本上很少使用i += 1 或者 i = i + 1 ,由於存在着隨處可見的可迭代對象,開發者們很容易實現對一個數值區間的操作,也就很少有對於某個數值作累加的訴求了。

所以,回到我們開頭的問題,其實這兩種“自增”寫法並沒有勝出 i++ 多少,只因為它們是通用型操作,又不需要引入新的操作符,所以 Python 才延續了一種基礎性的支持。真正的贏家其實是各種各樣的可迭代對象!

稍微小結下:Python 不支持自增操作符,一方面是因為它的整數是不可變類型的一等公民,自增操作(++)若要支持,則會帶來歧義;另一方面主要因為它有更合適的實現,即可迭代對象,對遍歷操作有很好的支持。

如果你覺得本文分析得不錯,那你應該會喜歡這些文章:

1、Python為什麼使用縮進來劃分代碼塊?

2、Python 的縮進是不是反人類的設計?

3、Python 為什麼不用分號作語句終止符?

4、Python 為什麼沒有 main 函數?為什麼我不推薦寫 main 函數?

5、Python 為什麼推薦蛇形命名法?

寫在最後:本文屬於“Python為什麼”系列(Python貓出品),該系列主要關注 Python 的語法、設計和發展等話題,以一個個“為什麼”式的問題為切入點,試着展現 Python 的迷人魅力。部分話題會推出視頻版,請在 B 站收看,觀看地址:視頻地址

公眾號【Python貓】, 本號連載優質的系列文章,有Python為什麼系列、喵星哲學貓系列、Python進階系列、好書推薦系列、技術寫作、優質英文推薦與翻譯等等,歡迎關注哦。

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

【其他文章推薦】

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

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

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

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

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

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

Java多線程之內存模型

目錄

  • 多線程需要解決的問題
    • 線程之間的通信
    • 線程之間的同步
  • Java內存模型
    • 內存間的交互操作
    • 指令屏障
    • happens-before規則
  • 指令重排序
    • 從源程序到字節指令的重排序
    • as-if-serial語義
    • 程序順序規則
  • 順序一致性模型
    • 順序一致性模型特性
    • 順序一致性模型特性
    • 當程序未正確同步會發生什麼
  • 參考資料

多線程需要解決的問題

在多線程編程中,線程之間如何通信和同步是一個必須解決的問題:

線程之間的通信:

線程之間有兩種通信的方式:消息傳遞和共享內存

  • 共享內存:線程之間共享程序的公共狀態,通過讀——寫修改公共狀態進行隱式通信。如上面代碼中的numLock可以被理解為公共狀態
  • 消息傳遞:線程之間沒有公共狀態,必須通過發送消息來進行显示通信
    在java中,線程是通過共享內存來完成線程之間的通信

線程之間的同步:

同步指程序中永固空值不同線程間的操作發生的相對順序的機制

  • 共享內存:同步是显示進行的,程序員需要指定某個方法或者某段代碼需要在線程之間互斥執行。如上面代碼中的Lock加鎖和解鎖之間的代碼塊,或者被synchronized包圍的代碼塊
  • 消息傳遞:同步是隱式執行的,因為消息的發送必然發生在消息的接收之前,例如使用Objetc#notify(),喚醒的線程接收信號一定在發送喚醒信號的發送之後。

Java內存模型

在java中,所有的實例域,靜態域、數組都被存儲在堆空間當中,堆內存在線程之間共享。

所有的局部變量,方法定義參數和異常處理器參數不會被線程共享,在每個線程棧中獨享,他們不會存在可見性和線程安全問題。

從Java線程模型(JMM)的角度來看,線程之間的共享變量存儲在主內存當中,每個線程擁有一個私有的本地內存(工作內存)本地內存存儲了該線程讀——寫共享的變量的副本。
JMM只是一個抽象的概念,在現實中並不存在,其中所有的存儲區域都在堆內存當中。JMM的模型圖如下圖所示:

而java線程對於共享變量的操作都是對於本地內存(工作內存)中的副本的操作,並沒有對共享內存中原始的共享變量進行操作;

以線程1和線程2為例,假設線程1修改了共享變量,那麼他們之間需要通信就需要兩個步驟:

  1. 線程1本地內存中修改過的共享變量的副本同步到共享內存中去
  2. 線程2從共享內存中讀取被線程1更新過的共享變量
    這樣才能完成線程1的修改對線程2的可見。

內存間的交互操作

為了完成這一線程之間的通信,JMM為內存間的交互操作定義了8個原子操作,如下錶:

操作 作用域 說明
lock(鎖定) 共享內存中的變量 把一個變量標識為一條線程獨佔的狀態
unlock(解鎖) 共享內存中的變量 把一個處於鎖定的變量釋放出來,釋放后其他線程可以進行訪問
read(讀取) 共享內存中的變量 把一個變量的值從共享內存傳輸到線程的工作內存。供隨後的load操作使用
load(載入) 工作內存 把read操作從共享內存中得到的變量值放入工作內存的變量副本當中
use(使用) 工作內存 把工作內存中的一個變量值傳遞給執行引擎
assign(賦值) 工作內存 把一個從執行引擎接受到的值賦值給工作內存的變量
store(存儲) 作用於工作內存 把一個工作內存中的變量傳遞給共享內存,供後續的write使用
write(寫入) 共享內存中的變量 把store操作從工作內存中得到的變量的值放入主內存

JMM規定JVM四線時必須保證上述8個原子操作是不可再分割的,同時必須滿足以下的規則:

  1. 不允許readloadstorewrite操作之一單獨出現,即不允許只從共享內存讀取但工作內存不接受,或者工作捏村發起回寫但是共享內存不接收
  2. 不允許一個線程捨棄assign操作,即當一個線程修改了變量后必須寫回工作內存和共享內存
  3. 不允許一個線程將未修改的變量值寫回共享內存
  4. 變量只能從共享內存中誕生,不允許線程直接使用未初始化的變量
  5. 一個變量同一時刻只能由一個線程對其執行lock操作,但是一個變量可以被同一個線程重複執行多次lock,但是需要相同次數的unlock
  6. 如果對一個變量執行lock操作,那麼會清空工作內存中此變量的值,在執行引擎使用這個變量之前需要重新執行load和assign
  7. 不允許unlock一個沒有被鎖定的變量,也不允許unlock一個其他線程lock的變量
  8. 對一個變量unlock之前必須把此變量同步回主存當中。

longdouble的特殊操作
在一些32位的處理器上,如果要求對64位的longdouble的寫具有原子性,會有較大的開銷,為了照固這種情況,
java語言規範鼓勵但不要求虛擬機對64位的longdouble型變量的寫操作具有原子性,當JVM在這種處理器上運行時,
可能會把64位的long和double拆分成兩次32位的寫

指令屏障

為了保證內存的可見性,JMM的編譯器會禁止特定類型的編譯器重新排序;對於處理器的重新排序,
JMM會要求編譯器在生成指令序列時插入特定類型的的內存屏障指令,通過內存屏障指令巾紙特定類型的處理器重新排序

JMM規定了四種內存屏障,具體如下:

屏障類型 指令示例 說明
LoadLoad Barriers Load1;LoadLoad;Load2 確保Load1的數據先於Load2以及所有後續裝在指令的裝載
StoreStore Barries Store1;StoreStore;Store2 確保Store1數據對於其他處理器可見(刷新到內存)先於Store2及後續存儲指令的存儲
LoadStore Barriers Load1;LoadStore;Store2 確保Load1的裝載先於Store2及後續所有的存儲指令
StoreLoad Barrier Store1;StoreLoad;Load2 確保Store1的存儲指令先於Load1以及後續所所有的加載指令

StoreLoad是一個“萬能”的內存屏障,他同時具有其他三個內存屏障的效果,現代的處理器大都支持該屏障(其他的內存屏障不一定支持),
但是執行這個內存屏障的開銷很昂貴,因為需要將處理器緩衝區所有的數據刷回內存中。

happens-before規則

在JSR-133種內存模型種引入了happens-before規則來闡述操作之間的內存可見性。在JVM種如果一個操作的結果過需要對另一個操作可見,
那麼兩個操作之間必然要存在happens-bsfore關係:

  • 程序順序規則:一個線程中的個每個操作,happens-before於該線程的後續所有操作
  • 監視器鎖規則:對於一個鎖的解鎖,happens-before於隨後對於這個鎖的加鎖
  • volatitle變量規則:對於一個volatile的寫,happens-before於認意後續對這個volatile域的讀
  • 線程啟動原則:對線程的start()操作先行發生於線程內的任何操作
  • 線程終止原則:線程中的所有操作先行發生於檢測到線程終止,可以通過Thread.join()、Thread.isAlive()的返回值檢測線程是否已經終止
  • 線程終端原則:對線程的interrupt()的調用先行發生於線程的代碼中檢測到中斷事件的發生,可以通過Thread.interrupted()方法檢測是否發生中斷
  • 對象終結原則:一個對象的初始化完成(構造方法執行結束)先行發生於它的finalize()方法的開始。
  • 傳遞性:如果A happens-before B B happends-beforeC,那麼A happends-before C

指令重排序

從源程序到字節指令的重排序

眾所周知,JVM執行的是字節碼,Java源代碼需要先編譯成字節碼程序才能在Java虛擬機中運行,但是考慮下面的程序;

int a = 1;
int b = 1;

在這段代碼中,ab沒有任何的相互依賴關係,因此完全可以先對b初始化賦值,再對a變量初始化賦值;

事實上,為了提高性能,編譯器和處理器通常會對指令做重新排序。重排序分為3種:

  1. 編譯器優化的重排序。編譯器在不改變單線程的程序語義的前提下,可以安排字語句的執行順序。編譯器的對象是語句,不是字節碼,
    但是反應的結果就是編譯后的字節碼和寫的語句順序不一致。
  2. 執行級并行的重排序。現代處理器採用了并行技術,來將多條指令重疊執行。如果不存在數據依賴性,處理器可以改變語句對應機器指令的執行順序。
  3. 內存系統的重排序,由於處理器使用了緩存和讀/寫緩衝區,這使得加載和存儲操作看上去可能是在亂序執行。

數據依賴性:如果兩個操作訪問同一個變量,且兩個操作有一個是寫操作,則這兩個操作存在數據依賴性,改變這兩個操作的執行順序,就會改變執行結果。

儘管指令重排序會提高代碼的執行效率,但是卻為多線程編程帶來了問題,多線程操作共享變量需要一定程度上遵循代碼的編寫順序,
也需要將修改的共享數據存儲到共享內存中,不按照代碼順序執行可能會導致多線程程序出現內存可見性的問題,那又如何實現呢?

as-if-serial語義

as-if-serial語義:不論程序怎樣進行重排序,(單線程)程序的執行結果不能被改變。編譯器、runtime和處理器都必須支持as-if-serial語義。

程序順序規則

假設存在以下happens-before程序規則:

    1) A happens-before B
    2) B happens-before C
    3) A happens-before C

儘管這裏存在A happens-before B這一關係,但是JMM並不要求A一定要在B之前執行,僅僅要求A的執行結果對B可見。
即JMM僅要求前一個操作的結果對於后一個操作可見,並且前一個操作按照順序排在後一個操作之前。
但是若前一個操作放在後一個操作之後執行並不影響執行結果,則JMM認為這並不違法,JMM允許這種重排序。

順序一致性模型

在一個線程中寫一個變量,在另一個線程中同時讀取這個變量,讀和寫沒有通過排序來同步來排序,就會引發數據競爭。

數據競爭的核心原因是程序未正確同步。如果一個多線程程序是正確同步的,這個程序將是一個沒有數據競爭的程序。

順序一致性模型只是一個參考模型。

順序一致性模型特性

  • 一個線程中所有的操作必須按照程序的順序來執行。
  • 不管線程是否同步,所有的線程都只能看到一個單一的執行順序。

在順序一致性模型中每個曹祖都必須原子執行且立刻對所有線程可見。

當程序未正確同步會發生什麼

當線程未正確同步時,JMM只提供最小的安全性,當讀取到一個值時,這個值要麼是之前寫入的值,要麼是默認值。
JMM保證線程的操作不會無中生有。為了保證這一特點,JMM在分配對象時,首先會對內存空間清0,然後才在上面分配對象。

未同步的程序在JMM種執行時,整體上是無序的,執行結果也無法預知。位同步程序子兩個模型中執行特點有如下幾個差異:

  • 順序一致性模型保證單線程內的操作會按照程序的順序執行,而JMM不保證單線程內的操作會按照程序的順序執行
  • 順序一致性模型保證所有線程只能看到一致的操作執行順序,而JMM不保證所有線程能看到一致的操作執行順序
  • JMM不保證對64位的longdouble型變量具有寫操作的原子性,而順序一致性模型保證對所有的內存的讀/寫操作都具有原子性

參考資料

java併發編程的藝術-方騰飛,魏鵬,程曉明著

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

【其他文章推薦】

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

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

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

※超省錢租車方案

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

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

【其他文章推薦】

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

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

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

※超省錢租車方案

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

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

三文搞懂學會Docker容器技術(上)

1,Docker簡介

  1.1 Docker是什麼?

Docker官網: https://www.docker.com/

Docker 是一個開源的應用容器引擎,基於 Go 語言 並遵從Apache2.0協議開源。
Docker 可以讓開發者打包他們的應用以及依賴包到一個輕量級、可移植的容器中,然後發布到任何流行的 Linux 機器上,也可以實現虛擬化。
容器是完全使用沙箱機制,相互之間不會有任何接口(類似 iPhone 的 app),更重要的是容器性能開銷極低。
Docker 從 17.03 版本之後分為 CE(Community Edition: 社區版) 和 EE(Enterprise Edition: 企業版),我們用社區版就可以了。

  1.2 Docker架構原理?

 

Docker三要素,鏡像,容器,倉庫

1.鏡像

Docker 鏡像(Image)就是一個只讀的模板,它可以是一個可運行軟件(tomcat,mysql),也可以是一個系統(centos)。鏡像可以用來創建 Docker 容器,一個鏡像可以創建很多容器。

2.容器

Docker 利用容器(Container)獨立運行的一個或一組應用。容器是用鏡像創建的運行實例。它可以被啟動、開始、停止、刪除。每個容器都是相互隔離的、保證安全的平台。可以把容器看做是一個簡易版的 Linux 環境(包括root用戶權限、進程空間、用戶空間和網絡空間等)和運行在其中的應用程序。容器的定義和鏡像幾乎一模一樣,也是一堆層的統一視角,唯一區別在於容器的最上面那一層是可讀可寫的。

3.倉庫

倉庫(Repository)是集中存放鏡像文件的場所,類似GitHub存放項目代碼一樣,只不過Docker Hub是由來存鏡像(image)的。倉庫(Repository)和倉庫註冊服務器(Registry)是有區別的。倉庫註冊服務器上往往存放着多個倉庫,每個倉庫中又包含了多個鏡像,每個鏡像有不同的標籤(tag,類似版本號)。

倉庫分為公開倉庫(Public)和私有倉庫(Private)兩種形式。

最大的公開倉庫是 Docker Hub(https://hub.docker.com/),存放了數量龐大的鏡像供用戶下載。國內的公開倉庫包括阿里雲 、網易雲 等。

 

容器與鏡像的關係類似於面向對象編程中的對象與類。

Docker 面向對象
容器 對象
鏡像

  1.3 Docker有什麼用?

    1,簡化環境搭建,提高開發生命周期效率;

    2,大大簡化運維工作量;

    3,微服務利器;

  1.4 Docker容器與虛擬機區別?

Docker是一種輕量級的虛擬化技術,比傳統的虛擬機性能更好。

下圖是虛擬機的體繫結構:

 

  • server – 表示真實電腦。
  • Host OS – 真實電腦的操作系統,例如:Windows,Linux
  • Hypervisor – 虛擬機平台,模擬硬件,如VMWare,VirtualBox
  • Guest OS – 虛擬機平台上安裝的操作系統,例如CentOS Linux
  • App – 虛擬機操作系統上的應用,例如nginx

 

下圖是Docker的體繫結構:

  • server – 表示真實電腦。
  • Host OS – 真實電腦的操作系統,例如:Windows,Linux
  • Docker Engine – 新一代虛擬化技術,不需要包含單獨的操作系統。
  • App – 所有的應用程序現在都作為Docker容器運行。

 

這種體繫結構的明顯優勢是,不需要為虛擬機操作系統提供硬件模擬。所有應用程序都作為Docker容器工作,性能更好。

  Docker容器 虛擬機(VM)
操作系統 與宿主機共享OS 宿主機OS上運行宿主機OS
存儲大小 鏡像小,便於存儲與傳輸 鏡像龐大(vmdk等)
運行性能 幾乎無額外性能損失 操作系統額外的cpu、內存消耗
移植性 輕便、靈活、適用於Linux 笨重、與虛擬化技術耦合度高
硬件親和性  面向軟件開發者 面向硬件運維者

 

Docker優點:輕量級,速度快,運行應用隔離,方便維護…

2,Docker安裝

  2.1 Docker版本介紹

Docker從1.13版本之後採用時間線的方式作為版本號,分為社區版CE和企業版EE。

社區版是免費提供給個人開發者和小型團體使用的,企業版會提供額外的收費服務,比如經過官方測試認證過的基礎設施、容器、插件等。

社區版按照stable和edge兩種方式發布,每個季度更新stable版本,如17.06,17.09;每個月份更新edge版本,如17.09,17.10。

我們平時用社區版就足夠了。所以我們安裝社區版;

  2.2 Docker安裝官方文檔

我們主要參考:https://docs.docker.com/install/linux/docker-ce/centos/  來安裝;

  2.3 工具準備

前置課程:Centos課程  http://www.java1234.com/javaxuexiluxiantu.html

打包下載: http://pan.baidu.com/s/1i55jJAt

虛擬機 VMware

centos7安裝下虛擬機VM上;

連接工具 才用 FinalShell  官方地址:http://www.hostbuf.com/

  2.4 Docker安裝步驟

我們切換到root用戶

1、Docker 要求 CentOS 系統的內核版本高於 3.10 ,查看本頁面的前提條件來驗證你的CentOS 版本是否支持 Docker 。

通過 uname -r 命令查看你當前的內核版本

 $ uname -r

2、使用 root 權限登錄 Centos。確保 yum 包更新到最新。

$ yum update

3、卸載舊版本(如果安裝過舊版本的話)

$ yum remove docker  docker-common docker-selinux docker-engine

4、安裝需要的軟件包, yum-util 提供yum-config-manager功能,另外兩個是devicemapper驅動依賴的

$ yum install -y yum-utils device-mapper-persistent-data lvm2

5、設置yum源

$ yum-config-manager –add-repo https://download.docker.com/linux/centos/docker-ce.repo

6,安裝最新版本的Docker

$ yum install docker-ce docker-ce-cli containerd.io

7,啟動Docker並設置開機啟動

$ systemctl start docker

$ systemctl enable docker

8,驗證Docker

$ docker version

 

說明安裝OK;

9,Docker HelloWorld測試;

$ docker run hello-world

 

因為本地沒有這個鏡像,所以從遠程官方倉庫去拉取,下載;

然後我們再執行一次;

 

OK了

  2.5 Docker配置阿里雲鏡像倉庫

Docker默認遠程倉庫是 https://hub.docker.com/

比如我們下載一個大點的東西,龜速

 

由於是國外主機,類似Maven倉庫,慢得一腿,經常延遲,破損;

所以我們一般都是配置國內鏡像,比如阿里雲,網易雲等;推薦阿里雲,穩定點;

配置步驟如下:

1,登錄進入阿里雲鏡像服務中心,獲取鏡像地址

進入阿里雲容器鏡像服務地址:點這裏快速進入

使用你的淘寶賬號密碼登錄

 

這裏我們獲取鏡像地址;

2,在/etc/docker目錄下找到在daemon.json文件(沒有就新建),將下面內容寫入

{

 “registry-mirrors”: [“https://xxxxxxx.mirror.aliyuncs.com”]

}

3,重啟daemon

systemctl daemon-reload

4,重啟docker服務

systemctl restart docker

5,測試

由於速度太快,截圖都難;

 

3,HelloWorld運行原理

運行  docker run hello-world

本地倉庫未能找到該鏡像,然後去遠程倉庫尋找以及下載該鏡像;

然後我們再執行該命令:

出來了 Hellowold。我們具體來分析下 執行原理和過程;

從左到右 client客戶端,Docker運行主機,遠程倉庫;

docker build ,pull,run分別是 構建,拉取,運行命令,後面再細講;

中間Docker主機里有 Docker daemon主運行線程,以及Containers容器,容器里可以運行很多實例,(實例是從右側Images鏡像實例化出來的)Images是存儲再本地的鏡像文件,比如 Redis,Tomat這些鏡像文件;

右側是Registry鏡像倉庫,默認遠程鏡像倉庫 https://hub.docker.com/  不過是國外主機,下載很慢,不穩定,所以我們後面要配置成阿里雲倉庫鏡像地址,穩定快捷;

執行 docker run hello-world的過程看如下圖例:

 

 

 

4,Docker基本命令

   4.1 啟動Docker

           systemctl start docker

  4.2 停止Docker

         systemctl stop docker

  4.3 重啟Docker

       systemctl restart docker

  4.4 開機啟動Docker

     systemctl enable docker

  4.5 查看Docker概要信息

   docker info

  4.6 查看Docker幫助文檔

   docker –help

  4.7 查看Docker版本信息

     docker version

5,Docker鏡像

  5.1 docker images 列出本機所有鏡像

 

REPOSITORY 鏡像的倉庫源
TAG 鏡像的標籤(版本)同一個倉庫有多個TAG的鏡像,多個版本;我們用REPOSITORY:TAG來定義不同的鏡像;
IMAGE ID 鏡像ID,鏡像的唯一標識
CREATE 鏡像創建時間
SIZE 鏡像大小

OPTIONS 可選參數:

-a 显示所有鏡像(包括中間層)
q 只显示鏡像ID
-qa 可以組合
–digests 显示鏡像的摘要信息
–no-trunc 显示完整的鏡像信息 

 

  5.2 docker search 搜索鏡像

和 https://hub.docker.com/ 這裏的搜索效果一樣;

OPTIONS可選參數:

–no-trunc 显示完整的鏡像描述
-s 列出收藏數不小於指定值的鏡像
–automated 只列出Docker Hub自動構建類型的鏡像

 

 

 

  5.3 docker pull 下載鏡像

docker pull 鏡像名稱:[TAG]

注意:不加TAG,默認下載最新版本latest

  5.4 docker rmi 刪除鏡像

1,刪除單個:docker rmi 鏡像名稱:[TAG]

如果不寫TAG,默認刪除最新版本latest

有鏡像生成的容器再運行時候,會報錯,刪除失敗;

我們需要加 -f 強制刪除

2,刪除多個:docker rmi -f 鏡像名稱1:[TAG] 鏡像名稱2:[TAG]

中間空格隔開

3,刪除全部:docker rmi -f $(docker images -qa)

 

 

——————————————————————————————————————————

作者: java1234_小鋒

出處:https://www.cnblogs.com/java688/p/13132444.html

版權:本站使用「CC BY 4.0」創作共享協議,轉載請在文章明顯位置註明作者及出處。

——————————————————————————————————————————

 

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

【其他文章推薦】

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

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

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

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

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

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

Windows7/10實現ICMP(ping命令)

  如果覺得本文如果幫到你或者你想轉載都可以,只需要標註出處即可。謝謝

 利用ICMP數據包、C語言實現Ping命令程序,能實現基本的Ping操作,發送ICMP回顯請求報文,用於測試—個主機到只一個主機之間的連通情況。通過本程序的訓練,熟悉ICMP報文結構,對ICMP有更深的理解,掌握Ping程序的設計方法,掌握網絡編程的方法和技巧,從而編寫出功能更強大的程序。有關traceroute如果有時間我會也寫一篇來進行講解.W

  windows和Linux實現ping的底層思想一樣的,代碼有細微的差別。如文文件不一樣,參數定義不一樣等。所以我們要實現ping功能的時候我們需要注意是在Windows上實現還是Linux上實現。

  如果你不想看關於ping命令實現的原理,則可以直接通過以下目錄跳轉到‘8.實現Ping功能’即可.

  本文目錄

  1.ICMP簡介

  2.ICMP工作原理

  3.ICMP報文格式

  4.ICMPv4類型

    4.1響應請求/應答(ping)

    4.2.目標不可到達、源抑制和超時報文

  5.ICMP應用

  6.ICMP攻擊與防禦方法

  7.IP報文頭和ICMP的聯繫

  8.實現Ping功能

    8.1.ping實現步驟

    8.2.結果及心得

    8.3.完整代碼

1.ICMP簡介

  ICMP(Internet Control Message Protocol)Internet控制報文協議。它是TCP/IP協議簇的一個子協議,用於在IP主機、路由器之間傳遞控制消息。控制消息是指網絡通不通、主機是否可達、路由是否可用等網絡本身的消息。這些控制消息雖然並不傳輸用戶數據,但是對於用戶數據的傳遞起着重要的作用。

 ICMP協議是一種面向無連接的協議,用於傳輸出錯報告控制信息。它是一個非常重要的協議,它對於網絡安全具有極其重要的意義。

  ICMP報文通常是由IP層本身、上層的傳輸協議(TCP或UDP)甚至某些情況下用戶應用除法執行的。

  ICMP報文是在IP數據報內被封裝傳輸的。

  ICMP分為兩大類:有關IP數據報傳遞的ICMP報文(稱為差錯報文(error message)),以及有關信息採集和配置的ICMP報文(稱為查詢(query)或者信息類報文(informational message))。

  注:ICMP並不為IP網絡提供可靠性。相反,它表明了某些類別的故障和配置信息。

2.ICMP工作原理

  ICMP提供一致易懂的出錯報告信息。發送的出錯報文返回到發送原數據的設備,因為只有發送設備才是出錯報文的邏輯接受者。發送設備隨後可根據ICMP報文確定發生錯誤的類型,並確定如何才能更好地重發失敗的數據包。但是ICMP唯一的功能是報告問題而不是糾正錯誤,糾正錯誤的任務由發送方完成。

  我們在網絡中經常會使用到ICMP協議,比如我們經常使用的用於檢查網絡通不通的Ping命令(Linux和Windows中均有),這個“Ping”的過程實際上就是ICMP協議工作的過程。還有其他的網絡命令如跟蹤路由的Tracert命令也是基於ICMP協議的。

3.ICMP報文格式

  ICMP報文包含在IP數據報中,屬於IP的一個用戶,IP頭部就在ICMP報文的前面,所以一個ICMP報文包括IP頭部、ICMP頭部和ICMP報文,IP頭部的Protocol值為1就說明這是一個ICMP報文,ICMP頭部中的類型(Type)域用於說明ICMP報文的作用及格式,此外還有一個代碼(Code)域用於詳細說明某種ICMP報文的類型,所有數據都在ICMP頭部後面。

  ICMPICMP報文格式具體由[RFC777][RFC792]規範。792是1981年9月更新,而777是1981年4月更新的。目前最新的ICMP報文格式RFC是2007年4月更新的[RFC488].

 

4.ICMPv4類型

  已經定義的ICMP消息類型大約有10多種,每種ICMP數據類型都被封裝在一個IP數據包中。主要的ICMP消息類型包括以下幾種。

  對於ICMPv4,信息類報文包括回顯請求和回顯應答(分別為類型8和0),以及路由器通告和路由器請求(分別為類型9和10,統一被稱為路由器發現)。最常見的差錯報文類型包括目的不可達(類型3)、重定向(類型5)、超時(類型11)和參數問題(類型12).下圖為一些類型.更多的信息建議去RFC官方查看,Type和Code在IPv4和IPc6不盡相同,所以其中的差異需要我們自行去查看,本圖為IPv4版本的,IPv6需要我們自己RFC查找。

 

1).響應請求/應答(ping)(ICMPv4類型為0/8,ICMPv6類型129/18)

  我們日常使用最多的ping,就是響應請求(Type=8)和應答(Type=0),一台主機向一個節點發送一個Type=8的ICMP報文,如果途中沒有異常(例如被路由器丟棄、目標不回應ICMP或傳輸失敗),則目標返回Type=0的ICMP報文,說明這台主機存在,更詳細的tracert通過計算ICMP報文通過的節點來確定主機與目標之間的網絡距離。更多的信息我們可以通過RFC文檔了解

 

2).目標不可到達(ICMPv4類型3,ICMPv6類型1)、源抑制和超時報文(ICMPv4類型11,ICMPv6類型4)

  這三種報文的格式是一樣的,目標不可到達報文(Type=3)在路由器或主機不能傳遞數據報時使用,例如我們要連接對方一個不存在的系統端口(端口號小於1024)時,將返回Type=3、Code=3的ICMP報文,它要告訴我們:“嘿,別連接了,我不在家的!”,常見的不可到達類型還有網絡不可到達(Code=0)、主機不可到達(Code=1)、協議不可到達(Code=2)等。源抑制則充當一個控制流量的角色,它通知主機減少數據報流量,由於ICMP沒有恢復傳輸的報文,所以只要停止該報文,主機就會逐漸恢復傳輸速率。最後,無連接方式網絡的問題就是數據報會丟失,或者長時間在網絡遊盪而找不到目標,或者擁塞導致主機在規定時間內無法重組數據報分段,這時就要觸發ICMP超時報文的產生。超時報文的代碼域有兩種取值:Code=0表示傳輸超時,Code=1表示重組分段超時。更多的信息我們可以通過RFC文檔了解

 

5.ICMP應用

1).ping 命令使用 ICMP 回送請求和應答報文在網絡可達性測試中使用的分組網間探測命令 ping 能產生 ICMP 回送請求和應答報文。目的主機收到 ICMP 回送請求報文後立刻回送應答報文,若源主機能收到 ICMP 回送應答報文,則說明到達該主機的網絡正常。

2).路由分析診斷程序 tracert 使用了 ICMP時間超過報文tracert 命令主要用來显示數據包到達目的主機所經過的路徑。通過執行一個 tracert 到對方主機的命令,返回數據包到達目的主機所經歷的路徑詳細信息,並显示每個路徑所消耗的時間。

 

6.ICMP攻擊

  涉及ICMP的攻擊主要分為3類:泛洪(flood)、炸彈(bomb)、信息泄露(information disclosure).針對TCP的ICMP攻擊已經被專門記錄在RFC文檔中[RFC5927]

1).泛洪(flood)

  泛洪將會生成大量流量,導致針對一台或者多台計算機的有效Dos攻擊

2).炸彈(bomb)

  炸彈類型有時也稱為核彈(nuke)類型,指的是發送經過特殊構造的報文,能夠導致IP或ICMP的處理崩潰或者終止。

3).信息泄露(information disclosure)

  信息泄露攻擊本身不會造成危害,但是能夠幫助其他攻擊方法避免浪費時間或者被發現了。

7.IP報文頭和ICMP的聯繫

  ICMP報文是封裝在IP數據報的數據部分中進行傳輸的.

 

  ICMP依靠IP來完成它的任務,它是IP的主要部分。它與傳輸協議(如TCP和UDP)顯著不同:它一般不用於在兩點間傳輸數據。它通常不由網絡程序直接使用,除了 ping 和 traceroute 這兩個特別的例子。 IPv4中的ICMP被稱作ICMPv4,IPv6中的ICMP則被稱作ICMPv6。

 

  總的來說,ICMP是封裝在IP數據報中進行傳輸的.具體更多的聯繫我們通過以下改文章進行詳解,從Wireshark抓包然後分析數據包進行兩者的區別和聯繫.

  參考文檔:https://www.cnblogs.com/CSAH/p/13170860.html

8.實現Ping功能

  首先我們注意,本文只是實現ping的最簡單的功能即響應請求/應答(ping),故只能夠ping IP地址,不能夠ping 域名,因為域名到IP地址我們需要經過DNS解析,本文不實現該功能.關於DNS轉換到IP地址的詳情,有時間有機會我會補上的.

  本程序使用的環境是win10+vc++6.0,如果沒有安裝VC++6.0的或者在Win10安裝了無法使用的請查看’Win10安裝vc6.0教程‘。

  ping功能實現參考了TCP/IP詳解 卷1 和 卷2。

1).實現步驟

  首先,我們需要先定義初始化一些全局變量,接着我們對需要用到的數據類型結構進行聲明定義,我們包含的數據類型結構有IP報頭結構、ICMP數據類型結構、結果集類型結構等;對需要使用到的函數進行頭文件的導入,主要的區別在於使用的是Windows系統還是Linux系統,導入的頭文件也不盡相同。準備工作全都完成了,然後我們就可以定義main函數進行試驗的驗證測試。

  其次,我們需要對每一步的遇到的問題需要寫一份說明報告書,以防下次再進行實驗時遇到同樣的問題時,我們無需再去查找大量資料。

  最後,我們對整個實驗的總結,對每一步。每一個函數進行詳講.做好註釋.

  Ping()函數是本程序的核心部分,它基本是調用其他模塊的函數來實現最終功能,其主要布驟包括:定義及初始化各個全局變量、打開socket動態庫、設置接收和發送超時值、域名地址解析、分配內存、創建及初始化ICMP報文、發送ICMP請求報文、接收ICMP 應答報文以及解讀應答報文和輸出Ping結果。

 

  注意:創建套接字的時候參數的以及在創建套接字之前必須首先使用WSAStartup函數。

(1)輸入時不能輸入目標主機名,不然ping結果為TIMEOUT

 

(2)該模塊並非只有處理還包括判斷及輸出判斷結果的含義

(3)程序沒運行一次就只能輸出四行結果(前提是輸入的地址有效),欲再次PING其他地址接着輸入下一個ip地址即可

 

2).代碼實現

    如果要想實現Windows下ping功能的實現,我們只需要從(1)到(8)複製到任意一個新創建filename.cpp文件中即可執行.或者最簡單的方法就是到本文中最低直接複製’完整代碼’到任意一個新創建filename.cpp文件中即可執行

(1).頭文件、全局變量

#include<stdio.h>
#include<Winsock2.h>
#include<ws2tcpip.h>
#include<stdlib.h>
#include<malloc.h>
#include<string.h>
#pragma comment(lib , "Ws2_32.lib")

#define ICMP_ECHO_REQUEST 8 //定義回顯請求類型
#define DEF_ICMP_DATA_SIZE 20 //定義發送數據長度
#define DEF_ICMP_PACK_SIZE 32 //定義數據包長度
#define MAX_ICMP_PACKET_SIZE 1024 //定義最大數據包長度
#define DEF_ICMP_TIMEOUT 3000  //定義超時為3秒
#define ICMP_TIMEOUT 11 //ICMP超時報文
#define ICMP_ECHO_REPLY 0 //定義回顯應答類型

  

(2).IP報頭據類型

/*
 *IP報頭結構
 */
typedef struct
{
    byte h_len_ver ; //IP版本號
    byte tos ; // 服務類型
    unsigned short total_len ; //IP包總長度
    unsigned short ident ; // 標識
    unsigned short frag_and_flags ; //標誌位
    byte ttl ; //生存時間
    byte proto ; //協議
    unsigned short cksum ; //IP首部校驗和
    unsigned long sourceIP ; //源IP地址
    unsigned long destIP ; //目的IP地址
} IP_HEADER ;

  

(3).ICMP數據類型

/*
 *定義ICMP數據類型
 */
typedef struct _ICMP_HEADER
{
    byte type ; //類型-----8
    byte code ; //代碼-----8
    unsigned short cksum ; //校驗和------16
    unsigned short id ; //標識符-------16
    unsigned short seq ; //序列號------16
    unsigned int choose ; //選項-------32
} ICMP_HEADER ;

  

(4).ping返回結果集數據類型

typedef struct
{
    int usSeqNo ; //記錄序列號
    DWORD dwRoundTripTime ; //記錄當前時間
    byte ttl ; //生存時間
    in_addr dwIPaddr ; //源IP地址
} DECODE_RESULT ;

  

(5).網際校驗和

/*
 *產生網際校驗和
 */
unsigned short GenerateChecksum(unsigned short *pBuf , int iSize)
{
    unsigned long cksum = 0 ; //開始時將網際校驗和初始化為0
    while(iSize > 1)
    {
        cksum += *pBuf++ ; //將待校驗的數據每16位逐位相加保存在cksum中
        iSize -= sizeof(unsigned short) ; //每16位加完則將帶校驗數據量減去16
    }
    //如果待校驗的數據為奇數,則循環完之後需將最後一個字節的內容與之前結果相加
    if(iSize)
    {
        cksum += *(unsigned char*)pBuf ;
    }
        //之前的結果產生了進位,需要把進位也加入最後的結果中
    cksum = (cksum >> 16) + (cksum & 0xffff) ;
    cksum += (cksum >> 16) ;
    return (unsigned short)(~ cksum) ;
}

  

(6).ping信息解析

/*
 *對ping應答信息進行解析
 */
boolean DecodeIcmpResponse_Ping(char *pBuf , int iPacketSize , DECODE_RESULT *stDecodeResult)
{
    IP_HEADER *pIpHrd = (IP_HEADER*)pBuf ;
    int iIphedLen = 20 ;
    if(iPacketSize < (int)(iIphedLen + sizeof(ICMP_HEADER)))
    {
        printf("size error! \n") ;
        return 0 ;
    }
    //指針指向ICMP報文的首地址
    ICMP_HEADER *pIcmpHrd = (ICMP_HEADER*)(pBuf + iIphedLen) ;
    unsigned short usID , usSeqNo ;
    //獲得的數據包的type字段為ICMP_ECHO_REPLY,即收到一個回顯應答ICMP報文
    if(pIcmpHrd->type == ICMP_ECHO_REPLY)
    {
        usID = pIcmpHrd->id ;
        //接收到的是網絡字節順序的seq字段信息 , 需轉化為主機字節順序
        usSeqNo = ntohs(pIcmpHrd->seq) ;
    }
    if(usID != GetCurrentProcessId() || usSeqNo != stDecodeResult->usSeqNo)
    {
        printf("usID error!\n") ;
        return 0 ;
    }
    //記錄對方主機的IP地址以及計算往返的時延RTT
    if(pIcmpHrd->type == ICMP_ECHO_REPLY)
    {
        stDecodeResult->dwIPaddr.s_addr = pIpHrd->sourceIP ;
        stDecodeResult->ttl = pIpHrd->ttl ;
        stDecodeResult->dwRoundTripTime = GetTickCount() - stDecodeResult->dwRoundTripTime ;
        return 1 ;
    }
    return 0 ;
}

  

(7).ping功能實現集成

void Ping(char *IP)
{
   unsigned long ulDestIP = inet_addr(IP) ; //將IP地址轉化為長整形
   if(ulDestIP == INADDR_NONE)
   {
       //轉化不成功時按域名解析
       HOSTENT *pHostent = gethostbyname(IP) ;
       if(pHostent)
       {
           ulDestIP = (*(IN_ADDR*)pHostent->h_addr).s_addr ; //將HOSTENT轉化為長整形
       }
       else
       {
           printf("TIMEOUT\n") ;
           return ;
       }
   }
   //填充目的Socket地址
   SOCKADDR_IN destSockAddr ; //定義目的地址
   ZeroMemory(&destSockAddr , sizeof(SOCKADDR_IN)) ; //將目的地址清空
   destSockAddr.sin_family = AF_INET ;
   destSockAddr.sin_addr.s_addr = ulDestIP ;
   destSockAddr.sin_port = htons(0);
    //初始化WinSock
    WORD wVersionRequested = MAKEWORD(2,2);
    WSADATA wsaData;
    if(WSAStartup(wVersionRequested,&wsaData) != 0)
    {
        printf("初始化WinSock失敗!\n") ;
        return ;
    }
   //使用ICMP協議創建Raw Socket
   SOCKET sockRaw = WSASocket(AF_INET , SOCK_RAW , IPPROTO_ICMP , NULL , 0 , WSA_FLAG_OVERLAPPED) ;
   if(sockRaw == INVALID_SOCKET)
   {
       printf("創建Socket失敗 !\n") ;
       return ;
   }
   //設置端口屬性
   int iTimeout = DEF_ICMP_TIMEOUT ;
   if(setsockopt(sockRaw , SOL_SOCKET , SO_RCVTIMEO , (char*)&iTimeout , sizeof(iTimeout)) == SOCKET_ERROR)
   {
         printf("設置參數失敗!\n") ;
         return ;
   }
   if(setsockopt(sockRaw , SOL_SOCKET , SO_SNDTIMEO , (char*)&iTimeout , sizeof(iTimeout)) == SOCKET_ERROR)
   {
         printf("設置參數失敗!\n") ;
         return ;
   }
   //定義發送的數據段
   char IcmpSendBuf[DEF_ICMP_PACK_SIZE] ;
   //填充ICMP數據包個各字段
   ICMP_HEADER *pIcmpHeader  = (ICMP_HEADER*)IcmpSendBuf;
   pIcmpHeader->type = ICMP_ECHO_REQUEST ;
   pIcmpHeader->code = 0 ;
   pIcmpHeader->id = (unsigned short)GetCurrentProcessId() ;
   memset(IcmpSendBuf + sizeof(ICMP_HEADER) , 'E' , DEF_ICMP_DATA_SIZE) ;
   //循環發送四個請求回顯icmp數據包
   int usSeqNo = 0 ;
   DECODE_RESULT stDecodeResult ;
   while(usSeqNo <= 3)
   {
     pIcmpHeader->seq = htons(usSeqNo) ;
     pIcmpHeader->cksum = 0 ;
     pIcmpHeader->cksum = GenerateChecksum((unsigned short*)IcmpSendBuf , DEF_ICMP_PACK_SIZE) ; //生成校驗位
     //記錄序列號和當前時間
     stDecodeResult.usSeqNo = usSeqNo ;
     stDecodeResult.dwRoundTripTime = GetTickCount() ;
     //發送ICMP的EchoRequest數據包
     if(sendto(sockRaw , IcmpSendBuf , DEF_ICMP_PACK_SIZE , 0 , (SOCKADDR*)&destSockAddr , sizeof(destSockAddr)) == SOCKET_ERROR)
     {
        //如果目的主機不可達則直接退出
        if(WSAGetLastError() == WSAEHOSTUNREACH)
        {
            printf("目的主機不可達!\n") ;
            exit(0) ;
        }
     }
     SOCKADDR_IN from ;
     int iFromLen = sizeof(from) ;
     int iReadLen ;
     //定義接收的數據包
     char IcmpRecvBuf[MAX_ICMP_PACKET_SIZE] ;
     while(1)
     {
         iReadLen = recvfrom(sockRaw , IcmpRecvBuf , MAX_ICMP_PACKET_SIZE , 0 , (SOCKADDR*)&from , &iFromLen) ;
         if(iReadLen != SOCKET_ERROR)
         {
             if(DecodeIcmpResponse_Ping(IcmpRecvBuf , sizeof(IcmpRecvBuf) , &stDecodeResult))
             {
                printf("來自 %s 的回復: 字節 = %d 時間 = %dms TTL = %d\n" , inet_ntoa(stDecodeResult.dwIPaddr) ,
                         iReadLen - 20,stDecodeResult.dwRoundTripTime ,stDecodeResult.ttl) ;
             }
             break ;
         }
         else if(WSAGetLastError() == WSAETIMEDOUT)
         {
             printf("time out !  *****\n") ;
             break ;
         }
         else
         {
             printf("發生未知錯誤!\n") ;
             break ;
         }
     }
     usSeqNo++ ;
   }
   //輸出屏幕信息
   printf("Ping complete...\n") ;
   closesocket(sockRaw) ;
   WSACleanup() ;
}

  

①.inet_addr:可以轉化字符串,主要用來將一個十進制的數轉化為二進制的數,用途多於ipv4的IP轉化。

②.if(IpAddress == INADDR_NONE):INADDR_NONE 是個宏定義,代表IpAddress是否為無效的IP地址。

③.ckaddr_in:定義目的地址信息;

 

④.ZeroMemory:用0來填充一塊內存區域.ZeroMemory只能用於windows平台.

 

⑤.WSASocket:創建一個原始套接字。使用時需要包含winsock2.h 頭文件和鏈接ws2_32.lib庫。

⑥.SOCKET socket==INVALID_SOCKET:如果socket為無效套接字,則結果為true;

⑦.DEF_ICMP_TIMEOUT:報文超時時間.

⑧.setsockopt:選項影響套接口的操作,諸如加急數據是否在普通數據流中接收,廣播數據是否可以從套接口發送等等。

⑨.while(usSeqNo <= 3){}:該部分就是實驗要求我們一次測試的進行發包4次

(8).Test測試

int main(int argc , char* argv[])
{
   char  com[10] , IP[20] ;
   while(1){
   printf("command>>") ;
   scanf("%s %s" , com , IP) ;
   if(strcmp(com , "ping") == 0)
   {
       Ping(IP) ;
   }
   else
   {
       printf("輸入錯誤 ! \n") ;
   }
   }
   return 0 ;
}

  

2).結果及心得

(1).查看本機IP

 

(2).ping網關IP

 

(3).ping本機IP

 

(4).ping局域網內IP

 

(5).問題與解決方案

①.問題:telnet是23端口,ssh是22端口,那麼ping是什麼端口?

答:ping基於ICMP,是在網絡層運行的。而端口號為傳輸層的內容。所以在ICMP中根本就不需要關注端口號這樣的信息。

②.Win7、win10 在VC6.0運行時WSASocket 返回錯誤 10013

  

3).完整代碼

 

#include<stdio.h>
#include<Winsock2.h>
#include<ws2tcpip.h>
#include<stdlib.h>
#include<malloc.h>
#include<string.h>
#pragma comment(lib , "Ws2_32.lib")


#define ICMP_ECHO_REQUEST 8 //定義回顯請求類型
#define DEF_ICMP_DATA_SIZE 20 //定義發送數據長度
#define DEF_ICMP_PACK_SIZE 32 //定義數據包長度
#define MAX_ICMP_PACKET_SIZE 1024 //定義最大數據包長度
#define DEF_ICMP_TIMEOUT 3000  //定義超時為3秒
#define ICMP_TIMEOUT 11 //ICMP超時報文
#define ICMP_ECHO_REPLY 0 //定義回顯應答類型
/*
 *IP報頭結構
 */
typedef struct
{
    byte h_len_ver ; //IP版本號
    byte tos ; // 服務類型
    unsigned short total_len ; //IP包總長度
    unsigned short ident ; // 標識
    unsigned short frag_and_flags ; //標誌位
    byte ttl ; //生存時間
    byte proto ; //協議
    unsigned short cksum ; //IP首部校驗和
    unsigned long sourceIP ; //源IP地址
    unsigned long destIP ; //目的IP地址
} IP_HEADER ;
/*
 *定義ICMP數據類型
 */
typedef struct _ICMP_HEADER
{
    byte type ; //類型-----8
    byte code ; //代碼-----8
    unsigned short cksum ; //校驗和------16
    unsigned short id ; //標識符-------16
    unsigned short seq ; //序列號------16
    unsigned int choose ; //選項-------32
} ICMP_HEADER ;


typedef struct
{
    int usSeqNo ; //記錄序列號
    DWORD dwRoundTripTime ; //記錄當前時間
    byte ttl ; //生存時間
    in_addr dwIPaddr ; //源IP地址
} DECODE_RESULT ;

/*
 *產生網際校驗和
 */
unsigned short GenerateChecksum(unsigned short *pBuf , int iSize)
{
    unsigned long cksum = 0 ; //開始時將網際校驗和初始化為0
    while(iSize > 1)
    {
        cksum += *pBuf++ ; //將待校驗的數據每16位逐位相加保存在cksum中
        iSize -= sizeof(unsigned short) ; //每16位加完則將帶校驗數據量減去16
    }
    //如果待校驗的數據為奇數,則循環完之後需將最後一個字節的內容與之前結果相加
    if(iSize)
    {
        cksum += *(unsigned char*)pBuf ;
    }
        //之前的結果產生了進位,需要把進位也加入最後的結果中
    cksum = (cksum >> 16) + (cksum & 0xffff) ;
    cksum += (cksum >> 16) ;
    return (unsigned short)(~ cksum) ;
}

/*
 *對ping應答信息進行解析
 */
boolean DecodeIcmpResponse_Ping(char *pBuf , int iPacketSize , DECODE_RESULT *stDecodeResult)
{
    IP_HEADER *pIpHrd = (IP_HEADER*)pBuf ;
    int iIphedLen = 20 ;
    if(iPacketSize < (int)(iIphedLen + sizeof(ICMP_HEADER)))
    {
        printf("size error! \n") ;
        return 0 ;
    }
    //指針指向ICMP報文的首地址
    ICMP_HEADER *pIcmpHrd = (ICMP_HEADER*)(pBuf + iIphedLen) ;
    unsigned short usID , usSeqNo ;
    //獲得的數據包的type字段為ICMP_ECHO_REPLY,即收到一個回顯應答ICMP報文
    if(pIcmpHrd->type == ICMP_ECHO_REPLY)
    {
        usID = pIcmpHrd->id ;
        //接收到的是網絡字節順序的seq字段信息 , 需轉化為主機字節順序
        usSeqNo = ntohs(pIcmpHrd->seq) ;
    }
    if(usID != GetCurrentProcessId() || usSeqNo != stDecodeResult->usSeqNo)
    {
        printf("usID error!\n") ;
        return 0 ;
    }
    //記錄對方主機的IP地址以及計算往返的時延RTT
    if(pIcmpHrd->type == ICMP_ECHO_REPLY)
    {
        stDecodeResult->dwIPaddr.s_addr = pIpHrd->sourceIP ;
        stDecodeResult->ttl = pIpHrd->ttl ;
        stDecodeResult->dwRoundTripTime = GetTickCount() - stDecodeResult->dwRoundTripTime ;
        return 1 ;
    }
    return 0 ;
}

void Ping(char *IP)
{
   unsigned long ulDestIP = inet_addr(IP) ; //將IP地址轉化為長整形
   if(ulDestIP == INADDR_NONE)
   {
       //轉化不成功時按域名解析
       HOSTENT *pHostent = gethostbyname(IP) ;
       if(pHostent)
       {
           ulDestIP = (*(IN_ADDR*)pHostent->h_addr).s_addr ; //將HOSTENT轉化為長整形
       }
       else
       {
           printf("TIMEOUT\n") ;
           return ;
       }
   }
   //填充目的Socket地址
   SOCKADDR_IN destSockAddr ; //定義目的地址
   ZeroMemory(&destSockAddr , sizeof(SOCKADDR_IN)) ; //將目的地址清空
   destSockAddr.sin_family = AF_INET ;
   destSockAddr.sin_addr.s_addr = ulDestIP ;
   destSockAddr.sin_port = htons(0);
    //初始化WinSock
    WORD wVersionRequested = MAKEWORD(2,2);
    WSADATA wsaData;
    if(WSAStartup(wVersionRequested,&wsaData) != 0)
    {
        printf("初始化WinSock失敗!\n") ;
        return ;
    }
   //使用ICMP協議創建Raw Socket
   SOCKET sockRaw = WSASocket(AF_INET , SOCK_RAW , IPPROTO_ICMP , NULL , 0 , WSA_FLAG_OVERLAPPED) ;
   if(sockRaw == INVALID_SOCKET)
   {
       printf("創建Socket失敗 !\n") ;
       return ;
   }
   //設置端口屬性
   int iTimeout = DEF_ICMP_TIMEOUT ;
   if(setsockopt(sockRaw , SOL_SOCKET , SO_RCVTIMEO , (char*)&iTimeout , sizeof(iTimeout)) == SOCKET_ERROR)
   {
         printf("設置參數失敗!\n") ;
         return ;
   }
   if(setsockopt(sockRaw , SOL_SOCKET , SO_SNDTIMEO , (char*)&iTimeout , sizeof(iTimeout)) == SOCKET_ERROR)
   {
         printf("設置參數失敗!\n") ;
         return ;
   }
   //定義發送的數據段
   char IcmpSendBuf[DEF_ICMP_PACK_SIZE] ;
   //填充ICMP數據包個各字段
   ICMP_HEADER *pIcmpHeader  = (ICMP_HEADER*)IcmpSendBuf;
   pIcmpHeader->type = ICMP_ECHO_REQUEST ;
   pIcmpHeader->code = 0 ;
   pIcmpHeader->id = (unsigned short)GetCurrentProcessId() ;
   memset(IcmpSendBuf + sizeof(ICMP_HEADER) , 'E' , DEF_ICMP_DATA_SIZE) ;
   //循環發送四個請求回顯icmp數據包
   int usSeqNo = 0 ;
   DECODE_RESULT stDecodeResult ;

   while(usSeqNo <= 3)
   {
     pIcmpHeader->seq = htons(usSeqNo) ;
     pIcmpHeader->cksum = 0 ;
     pIcmpHeader->cksum = GenerateChecksum((unsigned short*)IcmpSendBuf , DEF_ICMP_PACK_SIZE) ; //生成校驗位
     //記錄序列號和當前時間
     stDecodeResult.usSeqNo = usSeqNo ;
     stDecodeResult.dwRoundTripTime = GetTickCount() ;
     //發送ICMP的EchoRequest數據包
     if(sendto(sockRaw , IcmpSendBuf , DEF_ICMP_PACK_SIZE , 0 , (SOCKADDR*)&destSockAddr , sizeof(destSockAddr)) == SOCKET_ERROR)
     {
        //如果目的主機不可達則直接退出
        if(WSAGetLastError() == WSAEHOSTUNREACH)
        {
            printf("目的主機不可達!\n") ;
            exit(0) ;
        }
     }
     SOCKADDR_IN from ;
     int iFromLen = sizeof(from) ;
     int iReadLen ;
     //定義接收的數據包
     char IcmpRecvBuf[MAX_ICMP_PACKET_SIZE] ;
     while(1)
     {
         iReadLen = recvfrom(sockRaw , IcmpRecvBuf , MAX_ICMP_PACKET_SIZE , 0 , (SOCKADDR*)&from , &iFromLen) ;
         if(iReadLen != SOCKET_ERROR)
         {
             if(DecodeIcmpResponse_Ping(IcmpRecvBuf , sizeof(IcmpRecvBuf) , &stDecodeResult))
             {
                printf("來自 %s 的回復: 字節 = %d 時間 = %dms TTL = %d\n" , inet_ntoa(stDecodeResult.dwIPaddr) ,
                         iReadLen - 20,stDecodeResult.dwRoundTripTime ,stDecodeResult.ttl) ;
             }
             break ;
         }
         else if(WSAGetLastError() == WSAETIMEDOUT)
         {
             printf("time out !  *****\n") ;
             break ;
         }
         else
         {
             printf("發生未知錯誤!\n") ;
             break ;
         }
     }
     usSeqNo++ ;
   }
   //輸出屏幕信息
   printf("Ping complete...\n") ;
   closesocket(sockRaw) ;
   WSACleanup() ;
}
int main()
{
   char  com[10] , IP[20] ;
   while(1){
   printf("command>>") ;
   scanf("%s %s" , com , IP) ;
   if(strcmp(com , "ping") == 0)
   {
       Ping(IP) ;
   }
   else
   {
       printf("輸入錯誤 ! \n") ;
   }
   }
   return 0 ;
}

  

參考文檔:https://zhidao.baidu.com/question/1946506262344388308.html

https://docs.microsoft.com/zh-cn/windows/win32/api/winsock2/nf-winsock2-wsasocketa?redirectedfrom=MSDN

https://zhidao.baidu.com/question/541753723.html

TCP/IP網絡原理技術[清華大學出版社 周明天,汪文勇]

互聯網控制消息協議[維基百科]

TCP/IP詳解 卷1:協議

TCP/IP詳解 卷2:實現

 

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

【其他文章推薦】

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

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

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

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

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

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

區塊鏈系列教程之:比特幣中的網絡和區塊鏈

目錄

  • 簡介
  • 比特幣的網絡
    • 網絡發現與同步
  • SPV節點
    • 區塊鏈頭
    • Merkle Tree
  • 比特幣中的區塊鏈
    • 區塊標識符
  • 創世區塊
  • 總結

簡介

比特幣的底層就是區塊鏈技術,區塊鏈也是因為比特幣而廣為人知的。和其他的區塊鏈技術相比,比特幣的區塊鏈有什麼特徵呢?作為去區塊鏈的鼻祖,又有什麼與眾不同的特性呢?快來跟我們一起看看吧。

比特幣的網絡

比特幣使用的是P2P(peer-to-peer)網絡,此P2P非彼P2P,這裡是點對點的網絡架構,而不是人對人的借錢模式。

P2P是指位於同一網絡中的每台計算機都彼此對等,各個節點共同提供網絡服務,不存在任何“特殊”節點。每個網絡節點以“扁平(flat)”的拓撲結構相互連通。在P2P網絡中不存在任何服務端(server)、中央化的服務、以及層級結構。

傳統的網絡結構是client-server的模式,所有的client都是和server交互獲取信息, 只要server掛掉了,client也就沒有用了。

而在P2P網絡中,沒有server的概念,每個節點可以作為一個server。對比起來P2P網絡在穩定性方面要比C-S架構的系統要穩定得多。

網絡發現與同步

既然是P2P網絡,那麼問題來了,這個P2P網絡是怎麼建立起來的呢?節點之間是怎麼發現的呢?

有做過P2P下載的同學應該都聽說過種子的概念,這個種子裏面保存了其他活躍的節點的地址。通過下載種子就可以連接對應的節點。

而每個節點又保存了最近連接或者活躍的節點,這樣就形成了龐大的P2P網絡。

同樣的,比特幣的P2P網絡也是這樣的。

新節點是如何發現網絡中的對等節點的呢?雖然比特幣網絡中沒有特殊節點,但是客戶端會維持一個列表,那裡列出了那些長期穩定運行的節點。這樣的節點被稱為“種子節點(seed nodes)”

節點必須持續進行兩項工作:在失去已有連接時發現新節點,並在其他節點啟動時為其提供幫助。

SPV節點

我們之前介紹了,在比特幣的世界里既沒有賬戶,也沒有餘額,只有分散到區塊鏈里的UTXO(Unspent Transaction Outputs)。

那麼如果想要驗證交易的話,需要從歷史的交易中查找所有的和該交易有關的交易,從而進行完整,全面的驗證。

這樣做的問題就是,如果下載所有的歷史記錄,那麼需要上百G的硬盤空間,這對於手機或者其他輕量級的客戶端是無法想象的。

於是SPV出現了。SPV的全稱是Simplified payment verification,叫做簡單認證支付。

SPV保存的不是整個區塊鏈,而是區塊鏈的頭部,因為每個區塊鏈頭只有80字節,所以即使把所有的區塊頭都下載保存起來也不會很大。

區塊鏈頭

區塊頭由三組區塊元數據組成。首先是一組引用父區塊哈希值的數據,這組元數據用於將該區塊與區塊鏈中前一區塊相連接。

第二組元數據,即難度、時間戳和nonce,與挖礦競爭相關。

第三組元數據是merkle樹根(一種用來有效地總結區塊中所有交易的數據結構)。

Nonce、難度目標和時間戳會用於挖礦過程,Merkle根用來索引和組織該區塊所有的交易信息。

上圖是一個區塊鏈頭組成的鏈。

Merkle Tree

Merkle Tree,是一種樹(數據結構中所說的樹),網上大都稱為Merkle Hash Tree,這是因為 它所構造的Merkle Tree的所有節點都是Hash值。Merkle Tree具有以下特點:

  1. 它是一種樹,可以是二叉樹,也可以多叉樹,無論是幾叉樹,它都具有樹結構的所有特點;

  2. Merkle樹的恭弘=叶 恭弘子節點上的value,是由你指定的,這主要看你的設計了,如Merkle Hash Tree會將數據的Hash值作為恭弘=叶 恭弘子節點的值;

  3. 非恭弘=叶 恭弘子節點的value是根據它下面所有的恭弘=叶 恭弘子節點值,然後按照一定的算法計算而得出的。如Merkle Hash Tree的非恭弘=叶 恭弘子節點value的計算方法是將該節點的所有子節點進行組合,然後對組合結果進行hash計算所得出的hash value。

有了Merkle Tree,我們只需要知道和要驗證的交易相關的其他Merkle Tree中的信息,就可以計算出整個Merkle Tree的值,這樣就可以直接使用頭部信息進行驗證了。這就是SPV的原理。

比特幣中的區塊鏈

區塊鏈是由包含交易信息的區塊從後向前有序鏈接起來的數據結構。它可以被存儲為flat file(一種包含沒有相對關係記錄的文件),或是存儲在一個簡單數據庫中。

比特幣核心客戶端使用Google的LevelDB數據庫存儲區塊鏈元數據。

它由一個包含元數據的區塊頭和緊跟其後的構成區塊主體的一長串交易組成。區塊頭是80字節,而平均每個交易至少是250字節,而且平均每個區塊至少包含超過500個交易。

區塊標識符

那怎麼表示一個區塊呢?我們使用區塊標誌符。

區塊主標識符是它的加密哈希值,一個通過SHA256算法對區塊頭進行二次哈希計算而得到的数字指紋。產生的32字節哈希值被稱為區塊哈希值,但是更準確的名稱是:區塊頭哈希值,因為只有區塊頭被用於計算。

第二種識別區塊的方式是通過該區塊在區塊鏈中的位置,即“區塊高度(block height)”。第一個區塊,其區塊高度為0
和區塊哈希值不同的是,區塊高度並不是唯一的標識符。雖然一個單一的區塊總是會有一個明確的、固定的區塊高度,但反過來卻並不成立,一個區塊高度並不總是識別一個單一的區塊。兩個或兩個以上的區塊可能有相同的區塊高度,在區塊鏈里爭奪同一位置。

創世區塊

區塊鏈里的第一個區塊創建於2009年,被稱為創世區塊。它是區塊鏈裏面所有區塊的共同祖先,這意味着你從任一區塊,循鏈向後回溯,最終都將到達創世區塊。

因為創世區塊被編入到比特幣客戶端軟件里,所以每一個節點都始於至少包含一個區塊的區塊鏈,這能確保創世區塊不會被改變。每一個節點都“知道”創世區塊的哈希值、結構、被創建的時間和裏面的一個交易。因此,每個節點都把該區塊作為區塊鏈的首區塊,從而構建了一個安全的、可信的區塊鏈的根。

創世區塊的哈希值為:
0000000000 19d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f

創世區塊包含一個隱藏的信息。在其Coinbase交易的輸入中包含這樣一句話“The Times 03/Jan/2009 Chancellor on brink of second bailout forbanks.”這句話是泰晤士報當天的頭版文章標題,引用這句話,既是對該區塊產生時間的說明,也可視為半開玩笑地提醒人們一個獨立的貨幣制度的重要性,同時告訴人們隨着比特幣的發展,一場前所未有的世界性貨幣革命將要發生。該消息是由比特幣的創立者中本聰嵌入創世區塊中。

coinbase的值是:04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73

解碼方法如下:

在python shell下:

“04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73”.decode(‘hex’)

輸出:

‘\x04\xff\xff\x00\x1d\x01\x04EThe Times 03/Jan/2009 Chancellor on brink of second bailout for banks’

總結

本文介紹了比特幣的網絡和比特幣中的區塊鏈的相關概念,希望大家能夠喜歡。

本文作者:flydean程序那些事

本文鏈接:http://www.flydean.com/bitcoin-blockchain-network/

本文來源:flydean的博客

歡迎關注我的公眾號:程序那些事,更多精彩等着您!

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

【其他文章推薦】

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

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

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

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

※超省錢租車方案

基於領域驅動設計(DDD)超輕量級快速開發架構,ASP.NET MVC 5 SmartCode Scaffolding for Visual Studio.Net

 

smartadmin.core.urf 這個項目是基於asp.net core 3.1(最新)基礎上參照領域驅動設計(DDD)的理念,並參考目前最為了流行的abp架構開發的一套輕量級的快速開發web application 技術架構,專註業務核心需求,減少重複代碼,開始構建和發布,讓初級程序員也能開發出專業並且漂亮的Web應用程序

域驅動設計(DDD)是一種通過將實現與不斷髮展的模型相連接來滿足複雜需求的軟件開發方法。域驅動設計的前提如下:

  • 將項目的主要重點放在核心領域和領域邏輯上;
  • 將複雜的設計基於領域模型;
  • 啟動技術專家和領域專家之間的創造性合作,以迭代方式完善解決特定領域問題的概念模型。

最終的核心思想還是SOLID,只是實現的方式有所不同,ABP可能目前對DDD設計理念最好的實現方式。但對於小項目我還是更喜歡 URF.Core https://github.com/urfnet/URF.Core 這個超輕量級的實現。

同時這個項目也就是我2年前的一個開源項目 ASP.NET MVC 5 SmartCode Scaffolding for Visual Studio.Net 的升級版,支持.net core.目前沒有把所有功能都遷移到.net core,其中最重要的就是代碼生成這塊。再接下來的時間里主要就是完善代碼生成的插件。當然也要看是否受歡迎,如果反應一般,我可能不會繼續更新。

Demo 網站

 演示站點 
賬號:demo 密碼:123456

GitHub 源代碼 https://github.com/neozhu/smartadmin.core.urf

喜歡請給個 Star 每一顆Star都是鼓勵我繼續更新的動力 謝謝
如果你用於自己公司及盈利性的項目,希望給與金錢上的贊助,並且保留原作者的版權

分層

smartadmin.core.urf遵行DDD設計模式來實現應用程序的四層模型

  • 表示層(Presentation Layer):用戶操作展示界面,使用SmartAdmin – Responsive WebApp模板+Jquery EasyUI
  • 應用層(Application Layer):在表示層與域層之間,實現具體應用程序邏輯,業務用例,Project:StartAdmin.Service.csproj
  • 域層(Domain Layer):包括業務對象(Entity)和核心(域)業務規則,應用程序的核心,使用EntityFrmework Core Code-first + Repository實現
  • 基礎結構層(Infrastructure Layer):提供通用技術功能,這些功能主要有第三方庫來支持,比如日誌:Nlog,服務發現:Swagger UI,事件總線(EventBus):dotnetcore/CAP,認證與授權:Microsoft.AspNetCore.Identity,後面會具體介紹

內容

 

域層(Domain Layer)

  • 實體(Entity,BaseEntity) 通常實體就是映射到關係數據庫中的表,這裏說名一下最佳做法和慣例:
  1. 在域層定義:本項目就是(SmartAdmin.Entity.csproj)
  2. 繼承一個基類 Entity,添加必要審計類比如:創建時間,最後修改時間等
  3. 必須要有一個主鍵最好是GRUID(不推薦複合主鍵),但本項目使用遞增的int類型
  4. 字段不要過多的冗餘,可以通過定義關聯關係
  5. 字段屬性和方法盡量使用virtual關鍵字。有些ORM和動態代理工具需要

 

  • 存儲庫(Repositories) 封裝基本數據操作方法(CRUD),本項目應用 URF.Core實現
  • 域服務
  • 技術指標
  • 應用層

    • 應用服務:用於實現應用程序的用例。它們用於將域邏輯公開給表示層,從表示層(可選)使用DTO(數據傳輸對象)作為參數調用應用程序服務。它使用域對象執行某些特定的業務邏輯,並(可選)將DTO返回到表示層。因此,表示層與域層完全隔離。對應本項目:(SmartAdmin.Service.csproj)
    • 數據傳輸對象(DTO):用於在應用程序層和表示層或其他類型的客戶端之間傳輸數據,通常,使用DTO作為參數從表示層(可選)調用應用程序服務。它使用域對象執行某些特定的業務邏輯,並(可選)將DTO返回到表示層。因此,表示層與域層完全隔離.對應本項目:(SmartAdmin.Dto.csproj)
    • Unit of work:管理和控制應用程序中操作數據庫連接和事務 ,本項目使用 URF.Core實現
  • 基礎服務層

    • UI樣式定義:根據用戶喜好選擇多種頁面显示模式
    • 租戶管理:使用EntityFrmework Core提供的Global Filter實現簡單多租戶應用
    • 賬號管理: 對登錄系統賬號維護,註冊,註銷,鎖定,解鎖,重置密碼,導入、導出等功能
    • 角色管理:使用Microsoft身份庫管理角色,用戶及其權限管理
    • 導航菜單:系統主導航欄配置
    • 角色授權:配置角色显示的菜單
    • 鍵值對配置:常用的數據字典維護,如何正確使用和想法後面會介紹
    • 導入&導出配置:使用Excel導入導出做一個可配置的功能
    • 系統日誌:asp.net core 自帶的日誌+Nlog把所有日誌保存到數據庫方便查詢和分析
    • 消息訂閱:集中訂閱CAP分佈式事件總線的消息
    • WebApi: Swagger UI Api服務發現和在線調試工具
    • CAP: CAP看板查看發布和訂閱的消息

快速上手開發

  • 開發環境
    • Visual Studio .Net 2019
    • .Net Core 3.1
    • Sql Server(LocalDb)
  • 附加數據庫

    使用SQL Server Management Studio 附加.\src\SmartAdmin.Data\db\smartadmindb.mdf 數據庫(如果是localdb,那麼不需要修改數據庫連接配置)

  • 打開解決方案

第一個簡單的需求開始 
新增 Company 企業信息 完成CRUD 導入導出功能

  • 新建實體對象(Entity)

在SmartAdmin.Entity.csproj項目的Models目錄下新增一個Company.cs類

 1 //記住:定義實體對象最佳做法,繼承基類,使用virtual關鍵字,盡可能的定義每個屬性,名稱,類型,長度,校驗規則,索引,默認值等
 2 namespace SmartAdmin.Data.Models
 3 {
 4     public partial class Company : URF.Core.EF.Trackable.Entity
 5     {
 6         [Display(Name = "企業名稱", Description = "歸屬企業名稱")]
 7         [MaxLength(50)]
 8         [Required]
 9         //[Index(IsUnique = true)]
10         public virtual string Name { get; set; }
11         [Display(Name = "組織代碼", Description = "組織代碼")]
12         [MaxLength(12)]
13         //[Index(IsUnique = true)]
14         [Required]
15         public virtual string Code { get; set; }
16         [Display(Name = "地址", Description = "地址")]
17         [MaxLength(128)]
18         [DefaultValue("-")]
19         public virtual string Address { get; set; }
20         [Display(Name = "聯繫人", Description = "聯繫人")]
21         [MaxLength(12)]
22         public virtual string Contect { get; set; }
23         [Display(Name = "聯繫電話", Description = "聯繫電話")]
24         [MaxLength(20)]
25         public virtual string PhoneNumber { get; set; }
26         [Display(Name = "註冊日期", Description = "註冊日期")]
27         [DefaultValue("now")]
28         public virtual  DateTime RegisterDate { get; set; }
29     }
30 }
31 //在 SmartAdmin.Data.csproj 項目 SmartDbContext.cs 添加
32 public virtual DbSet<Company> Companies { get; set; }

View Code

  • 添加服務對象 Service

在項目 SmartAdmin.Service.csproj 中添加ICompanyService.cs,CompanyService.cs 就是用來實現業務需求 用例的地方

  1 //ICompany.cs
  2 //根據實際業務用例來創建方法,默認的CRUD,增刪改查不需要再定義
  3 namespace SmartAdmin.Service
  4 {
  5   // Example: extending IService<TEntity> and/or ITrackableRepository<TEntity>, scope: ICustomerService
  6   public interface ICompanyService : IService<Company>
  7   {
  8     // Example: adding synchronous Single method, scope: ICustomerService
  9     Company Single(Expression<Func<Company, bool>> predicate);
 10     Task ImportDataTableAsync(DataTable datatable);
 11     Task<Stream> ExportExcelAsync(string filterRules = "", string sort = "Id", string order = "asc");
 12   }
 13 }
 14 // 具體實現接口的方法
 15 namespace SmartAdmin.Service
 16 {
 17   public class CompanyService : Service<Company>, ICompanyService
 18   {
 19     private readonly IDataTableImportMappingService mappingservice;
 20     private readonly ILogger<CompanyService> logger;
 21     public CompanyService(
 22       IDataTableImportMappingService mappingservice,
 23       ILogger<CompanyService> logger,
 24       ITrackableRepository<Company> repository) : base(repository)
 25     {
 26       this.mappingservice = mappingservice;
 27       this.logger = logger;
 28     }
 29 
 30     public async Task<Stream> ExportExcelAsync(string filterRules = "", string sort = "Id", string order = "asc")
 31     {
 32       var filters = PredicateBuilder.FromFilter<Company>(filterRules);
 33       var expcolopts = await this.mappingservice.Queryable()
 34              .Where(x => x.EntitySetName == "Company")
 35              .Select(x => new ExpColumnOpts()
 36              {
 37                EntitySetName = x.EntitySetName,
 38                FieldName = x.FieldName,
 39                IgnoredColumn = x.IgnoredColumn,
 40                SourceFieldName = x.SourceFieldName
 41              }).ToArrayAsync();
 42 
 43       var works = (await this.Query(filters).OrderBy(n => n.OrderBy(sort, order)).SelectAsync()).ToList();
 44       var datarows = works.Select(n => new
 45       {
 46         Id = n.Id,
 47         Name = n.Name,
 48         Code = n.Code,
 49         Address = n.Address,
 50         Contect = n.Contect,
 51         PhoneNumber = n.PhoneNumber,
 52         RegisterDate = n.RegisterDate.ToString("yyyy-MM-dd HH:mm:ss")
 53       }).ToList();
 54       return await NPOIHelper.ExportExcelAsync("Company", datarows, expcolopts);
 55     }
 56 
 57     public async Task ImportDataTableAsync(DataTable datatable)
 58     {
 59       var mapping = await this.mappingservice.Queryable()
 60                         .Where(x => x.EntitySetName == "Company" &&
 61                            (x.IsEnabled == true || (x.IsEnabled == false && x.DefaultValue != null))
 62                            ).ToListAsync();
 63       if (mapping.Count == 0)
 64       {
 65         throw new  NullReferenceException("沒有找到Work對象的Excel導入配置信息,請執行[系統管理/Excel導入配置]");
 66       }
 67       foreach (DataRow row in datatable.Rows)
 68       {
 69 
 70         var requiredfield = mapping.Where(x => x.IsRequired == true && x.IsEnabled == true && x.DefaultValue == null).FirstOrDefault()?.SourceFieldName;
 71         if (requiredfield != null || !row.IsNull(requiredfield))
 72         {
 73           var item = new Company();
 74           foreach (var field in mapping)
 75           {
 76             var defval = field.DefaultValue;
 77             var contain = datatable.Columns.Contains(field.SourceFieldName ?? "");
 78             if (contain && !row.IsNull(field.SourceFieldName))
 79             {
 80               var worktype = item.GetType();
 81               var propertyInfo = worktype.GetProperty(field.FieldName);
 82               var safetype = Nullable.GetUnderlyingType(propertyInfo.PropertyType) ?? propertyInfo.PropertyType;
 83               var safeValue = (row[field.SourceFieldName] == null) ? null : Convert.ChangeType(row[field.SourceFieldName], safetype);
 84               propertyInfo.SetValue(item, safeValue, null);
 85             }
 86             else if (!string.IsNullOrEmpty(defval))
 87             {
 88               var worktype = item.GetType();
 89               var propertyInfo = worktype.GetProperty(field.FieldName);
 90               if (string.Equals(defval, "now", StringComparison.OrdinalIgnoreCase) && (propertyInfo.PropertyType == typeof(DateTime) || propertyInfo.PropertyType == typeof(Nullable<DateTime>)))
 91               {
 92                 var safetype = Nullable.GetUnderlyingType(propertyInfo.PropertyType) ?? propertyInfo.PropertyType;
 93                 var safeValue = Convert.ChangeType(DateTime.Now, safetype);
 94                 propertyInfo.SetValue(item, safeValue, null);
 95               }
 96               else if (string.Equals(defval, "guid", StringComparison.OrdinalIgnoreCase))
 97               {
 98                 propertyInfo.SetValue(item, Guid.NewGuid().ToString(), null);
 99               }
100               else if (string.Equals(defval, "user", StringComparison.OrdinalIgnoreCase))
101               {
102                 propertyInfo.SetValue(item, "", null);
103               }
104               else
105               {
106                 var safetype = Nullable.GetUnderlyingType(propertyInfo.PropertyType) ?? propertyInfo.PropertyType;
107                 var safeValue = Convert.ChangeType(defval, safetype);
108                 propertyInfo.SetValue(item, safeValue, null);
109               }
110             }
111           }
112           this.Insert(item);
113         }
114       }
115     }
116 
117     // Example, adding synchronous Single method
118     public Company Single(Expression<Func<Company, bool>> predicate)
119     {
120       
121       return this.Repository.Queryable().Single(predicate);
122 
123     }
124   }
125 }

View Code

  • 添加Controller

MVC Controller

  1 namespace SmartAdmin.WebUI.Controllers
  2 {
  3   public class CompaniesController : Controller
  4   {
  5     private  readonly ICompanyService companyService;
  6     private readonly IUnitOfWork unitOfWork;
  7     private readonly ILogger<CompaniesController> _logger;
  8     private readonly IWebHostEnvironment _webHostEnvironment;
  9     public CompaniesController(ICompanyService companyService,
 10           IUnitOfWork unitOfWork,
 11           IWebHostEnvironment webHostEnvironment,
 12           ILogger<CompaniesController> logger)
 13     {
 14       this.companyService = companyService;
 15       this.unitOfWork = unitOfWork;
 16       this._logger = logger;
 17       this._webHostEnvironment = webHostEnvironment;
 18     }
 19 
 20     // GET: Companies
 21     public IActionResult Index()=> View();
 22     //datagrid 數據源
 23     public async Task<JsonResult> GetData(int page = 1, int rows = 10, string sort = "Id", string order = "asc", string filterRules = "")
 24     {
 25       try
 26       {
 27         var filters = PredicateBuilder.FromFilter<Company>(filterRules);
 28         var total = await this.companyService
 29                              .Query(filters)
 30                              .AsNoTracking()
 31                              .CountAsync()
 32                               ;
 33         var pagerows = (await this.companyService
 34                              .Query(filters)
 35                               .AsNoTracking()
 36                            .OrderBy(n => n.OrderBy(sort, order))
 37                            .Skip(page - 1).Take(rows)
 38                            .SelectAsync())
 39                            .Select(n => new
 40                            {
 41                              Id = n.Id,
 42                              Name = n.Name,
 43                              Code = n.Code,
 44                              Address = n.Address,
 45                              Contect = n.Contect,
 46                              PhoneNumber = n.PhoneNumber,
 47                              RegisterDate = n.RegisterDate.ToString("yyyy-MM-dd HH:mm:ss")
 48                            }).ToList();
 49         var pagelist = new { total = total, rows = pagerows };
 50         return Json(pagelist);
 51       }
 52       catch(Exception e) {
 53         throw e;
 54         }
 55 
 56     }
 57     //編輯 
 58     [HttpPost]
 59     [ValidateAntiForgeryToken]
 60     public async Task<JsonResult> Edit(Company company)
 61     {
 62       if (ModelState.IsValid)
 63       {
 64         try
 65         {
 66           this.companyService.Update(company);
 67 
 68           var result = await this.unitOfWork.SaveChangesAsync();
 69           return Json(new { success = true, result = result });
 70         }
 71          catch (Exception e)
 72         {
 73           return Json(new { success = false, err = e.GetBaseException().Message });
 74         }
 75       }
 76       else
 77       {
 78         var modelStateErrors = string.Join(",", this.ModelState.Keys.SelectMany(key => this.ModelState[key].Errors.Select(n => n.ErrorMessage)));
 79         return Json(new { success = false, err = modelStateErrors });
 80         //DisplayErrorMessage(modelStateErrors);
 81       }
 82       //return View(work);
 83     }
 84     //新建
 85     [HttpPost]
 86     [ValidateAntiForgeryToken]
 87    
 88     public async Task<JsonResult> Create([Bind("Name,Code,Address,Contect,PhoneNumber,RegisterDate")] Company company)
 89     {
 90       if (ModelState.IsValid)
 91       {
 92         try
 93         {
 94           this.companyService.Insert(company);
 95        await this.unitOfWork.SaveChangesAsync();
 96           return Json(new { success = true});
 97         }
 98         catch (Exception e)
 99         {
100           return Json(new { success = false, err = e.GetBaseException().Message });
101         }
102 
103         //DisplaySuccessMessage("Has update a Work record");
104         //return RedirectToAction("Index");
105       }
106       else
107        {
108         var modelStateErrors = string.Join(",", this.ModelState.Keys.SelectMany(key => this.ModelState[key].Errors.Select(n => n.ErrorMessage)));
109         return Json(new { success = false, err = modelStateErrors });
110         //DisplayErrorMessage(modelStateErrors);
111       }
112       //return View(work);
113     }
114     //刪除當前記錄
115     //GET: Companies/Delete/:id
116     [HttpGet]
117     public async Task<JsonResult> Delete(int id)
118     {
119       try
120       {
121         await this.companyService.DeleteAsync(id);
122         await this.unitOfWork.SaveChangesAsync();
123         return Json(new { success = true });
124       }
125      
126       catch (Exception e)
127       {
128         return Json(new { success = false, err = e.GetBaseException().Message });
129       }
130     }
131     //刪除選中的記錄
132     [HttpPost]
133     public async Task<JsonResult> DeleteChecked(int[] id)
134     {
135       try
136       {
137         foreach (var key in id)
138         {
139           await this.companyService.DeleteAsync(key);
140         }
141         await this.unitOfWork.SaveChangesAsync();
142         return Json(new { success = true });
143       }
144       catch (Exception e)
145       {
146         return Json(new { success = false, err = e.GetBaseException().Message });
147       }
148     }
149     //保存datagrid編輯的數據
150     [HttpPost]
151     public async Task<JsonResult> AcceptChanges(Company[] companies)
152     {
153       if (ModelState.IsValid)
154       {
155         try
156         {
157           foreach (var item in companies)
158           {
159             this.companyService.ApplyChanges(item);
160           }
161           var result = await this.unitOfWork.SaveChangesAsync();
162           return Json(new { success = true, result });
163         }
164         catch (Exception e)
165         {
166           return Json(new { success = false, err = e.GetBaseException().Message });
167         }
168       }
169       else
170       {
171         var modelStateErrors = string.Join(",", ModelState.Keys.SelectMany(key => ModelState[key].Errors.Select(n => n.ErrorMessage)));
172         return Json(new { success = false, err = modelStateErrors });
173       }
174 
175     }
176     //導出Excel
177     [HttpPost]
178     public async Task<IActionResult> ExportExcel(string filterRules = "", string sort = "Id", string order = "asc")
179     {
180       var fileName = "compnay" + DateTime.Now.ToString("yyyyMMddHHmmss") + ".xlsx";
181       var stream = await this.companyService.ExportExcelAsync(filterRules, sort, order);
182       return File(stream, "application/vnd.ms-excel", fileName);
183     }
184     //導入excel
185     [HttpPost]
186     public async Task<IActionResult> ImportExcel()
187     {
188       try
189       {
190         var watch = new Stopwatch();
191         watch.Start();
192         var total = 0;
193         if (Request.Form.Files.Count > 0)
194         {
195           for (var i = 0; i < this.Request.Form.Files.Count; i++)
196           {
197             var model = Request.Form["model"].FirstOrDefault() ?? "company";
198             var folder = Request.Form["folder"].FirstOrDefault() ?? "company";
199             var autosave = Convert.ToBoolean(Request.Form["autosave"].FirstOrDefault());
200             var properties = (Request.Form["properties"].FirstOrDefault()?.Split(','));
201             var file = Request.Form.Files[i];
202             var filename = file.FileName;
203             var contenttype = file.ContentType;
204             var size = file.Length;
205             var ext = Path.GetExtension(filename);
206             var path = Path.Combine(this._webHostEnvironment.ContentRootPath, "UploadFiles", folder);
207             if (!Directory.Exists(path))
208             {
209               Directory.CreateDirectory(path);
210             }
211             var datatable = await NPOIHelper.GetDataTableFromExcelAsync(file.OpenReadStream(), ext);
212             await this.companyService.ImportDataTableAsync(datatable);
213             await this.unitOfWork.SaveChangesAsync();
214             total = datatable.Rows.Count;
215             if (autosave)
216             {
217               var filepath = Path.Combine(path, filename);
218               file.OpenReadStream().Position = 0;
219 
220               using (var stream = System.IO.File.Create(filepath))
221               {
222                 await file.CopyToAsync(stream);
223               }
224             }
225 
226           }
227         }
228         watch.Stop();
229         return Json(new { success = true, total = total, elapsedTime = watch.ElapsedMilliseconds });
230       }
231       catch (Exception e) {
232         this._logger.LogError(e, "Excel導入失敗");
233         return this.Json(new { success = false,  err = e.GetBaseException().Message });
234       }
235         }
236     //下載模板
237     public async Task<IActionResult> Download(string file) {
238       
239       this.Response.Cookies.Append("fileDownload", "true");
240       var path = Path.Combine(this._webHostEnvironment.ContentRootPath, file);
241       var downloadFile = new FileInfo(path);
242       if (downloadFile.Exists)
243       {
244        var fileName = downloadFile.Name;
245        var mimeType = MimeTypeConvert.FromExtension(downloadFile.Extension);
246        var fileContent = new byte[Convert.ToInt32(downloadFile.Length)];
247         using (var fs = downloadFile.Open(FileMode.Open, FileAccess.Read, FileShare.Read))
248         {
249           await fs.ReadAsync(fileContent, 0, Convert.ToInt32(downloadFile.Length));
250         }
251         return this.File(fileContent, mimeType, fileName);
252       }
253       else
254       {
255         throw new FileNotFoundException($"文件 {file} 不存在!");
256       }
257     }
258 
259     }
260 }

View Code

  • 新建 View

MVC Views\Companies\Index

  1 @model SmartAdmin.Data.Models.Company
  2 @{
  3   ViewData["Title"] = "企業信息";
  4   ViewData["PageName"] = "Companies_Index";
  5   ViewData["Heading"] = "<i class='fal fa-window text-primary'></i> 企業信息";
  6   ViewData["Category1"] = "組織架構";
  7   ViewData["PageDescription"] = "";
  8 }
  9 <div class="row">
 10   <div class="col-lg-12 col-xl-12">
 11     <div id="panel-1" class="panel">
 12       <div class="panel-hdr">
 13         <h2>
 14           公司信息
 15         </h2>
 16         <div class="panel-toolbar">
 17           <button class="btn btn-panel bg-transparent fs-xl w-auto h-auto rounded-0" data-action="panel-collapse" data-toggle="tooltip" data-offset="0,10" data-original-title="Collapse"><i class="fal fa-window-minimize"></i></button>
 18           <button class="btn btn-panel bg-transparent fs-xl w-auto h-auto rounded-0" data-action="panel-fullscreen" data-toggle="tooltip" data-offset="0,10" data-original-title="Fullscreen"><i class="fal fa-expand"></i></button>
 19         </div>
 20 
 21       </div>
 22       <div class="panel-container show">
 23         <div class="panel-content py-2 rounded-bottom border-faded border-left-0 border-right-0  text-muted bg-subtlelight-fade ">
 24           <div class="row no-gutters align-items-center">
 25             <div class="col">
 26               <!-- 開啟授權控制請參考 @@if (Html.IsAuthorize("Create") -->
 27               <div class="btn-group btn-group-sm">
 28                 <button onclick="append()" class="btn btn-default">
 29                   <span class="fal fa-plus mr-1"></span> 新增
 30                 </button>
 31               </div>
 32               <div class="btn-group btn-group-sm">
 33                 <button name="deletebutton" disabled onclick="removeit()" class="btn btn-default">
 34                   <span class="fal fa-times mr-1"></span> 刪除
 35                 </button>
 36               </div>
 37               <div class="btn-group btn-group-sm">
 38                 <button name="savebutton" disabled onclick="acceptChanges()" class="btn btn-default">
 39                   <span class="fal fa-save mr-1"></span> 保存
 40                 </button>
 41               </div>
 42               <div class="btn-group btn-group-sm">
 43                 <button name="cancelbutton" disabled onclick="rejectChanges()" class="btn btn-default">
 44                   <span class="fal fa-ban mr-1"></span> 取消
 45                 </button>
 46               </div>
 47               <div class="btn-group btn-group-sm">
 48                 <button onclick="reload()" class="btn btn-default"> <span class="fal fa-search mr-1"></span> 查詢 </button>
 49                 <button type="button" class="btn btn-default dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
 50                   <span class="sr-only">Toggle Dropdown</span>
 51                 </button>
 52                 <div class="dropdown-menu dropdown-menu-animated">
 53                   <a class="dropdown-item js-waves-on" href="javascript:void()"> 我的記錄 </a>
 54                   <div class="dropdown-divider"></div>
 55                   <a class="dropdown-item js-waves-on" href="javascript:void()"> 自定義查詢 </a>
 56                 </div>
 57               </div>
 58               <div class="btn-group btn-group-sm hidden-xs">
 59                 <button type="button" onclick="importExcel.upload()" class="btn btn-default"><span class="fal fa-cloud-upload mr-1"></span> 導入 </button>
 60                 <button type="button" class="btn btn-default  dropdown-toggle dropdown-toggle-split waves-effect waves-themed" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
 61                   <span class="sr-only">Toggle Dropdown</span>
 62                 </button>
 63                 <div class="dropdown-menu dropdown-menu-animated">
 64                   <a class="dropdown-item js-waves-on" href="javascript:importExcel.downloadtemplate()">
 65                     <span class="fal fa-download"></span> 下載模板
 66                   </a>
 67                 </div>
 68               </div>
 69               <div class="btn-group btn-group-sm hidden-xs">
 70                 <button onclick="exportexcel()" class="btn btn-default">
 71                   <span class="fal fa-file-export mr-1"></span>  導出
 72                 </button>
 73               </div>
 74 
 75             </div>
 76 
 77           </div>
 78 
 79         </div>
 80         <div class="panel-content">
 81           <div class="table-responsive">
 82             <table id="companies_datagrid">
 83             </table>
 84           </div>
 85         </div>
 86       </div>
 87     </div>
 88   </div>
 89 </div>
 90 <!-- 彈出窗體form表單 -->
 91 <div id="companydetailwindow" class="easyui-window"
 92      title="明細數據"
 93      data-options="modal:true,
 94                     closed:true,
 95                     minimizable:false,
 96                     collapsible:false,
 97                     maximized:false,
 98                     iconCls:'fal fa-window',
 99                     onBeforeClose:function(){
100                       var that = $(this);
101                       if(companyhasmodified()){
102                         $.messager.confirm('確認','你確定要放棄保存修改的記錄?',function(r){
103                         if (r){
104                           var opts = that.panel('options');
105                           var onBeforeClose = opts.onBeforeClose;
106                           opts.onBeforeClose = function(){};
107                           that.panel('close');
108                           opts.onBeforeClose = onBeforeClose;
109                           hook = false;
110                         }
111                         });
112                         return false;
113                       }
114                     },
115                     onOpen:function(){
116                        $(this).window('vcenter');
117                        $(this).window('hcenter');
118                     },
119                     onRestore:function(){
120                     },
121                     onMaximize:function(){
122                     }
123                     " style="width:820px;height:420px;display:none">
124   <!-- toolbar -->
125   <div class="panel-content py-2 rounded-bottom border-faded border-left-0 border-right-0  text-muted bg-subtlelight-fade sticky-top">
126     <div class="d-flex flex-row-reverse pr-4">
127       <div class="btn-group btn-group-sm mr-1">
128         <button name="saveitembutton" onclick="savecompanyitem()" class="btn btn-default">
129           <i class="fal fa-save"></i> 保存
130         </button>
131       </div>
132       <div class="btn-group btn-group-sm mr-1" id="deleteitem-btn-group">
133         <button onclick="deletecompanyitem()" class="btn btn-danger">
134           <i class="fal fa-trash-alt"></i> 刪除
135         </button>
136       </div>
137     </div>
138   </div>
139   <div class="panel-container show">
140     <div class="container">
141       <div class="panel-content">
142         <form id="company_form"
143               class="easyui-form form-horizontal p-1"
144               method="post"
145               data-options="novalidate:true,
146                             onChange: function(target){
147                               hook = true;
148                               $('button[name*=\'saveitembutton\']').prop('disabled', false);
149                              },
150                              onLoadSuccess:function(data){
151                                hook = false;
152                                $('button[name*=\'saveitembutton\']').prop('disabled', true);
153                              }">
154           @Html.AntiForgeryToken()
155           <!--Primary Key-->
156           @Html.HiddenFor(model => model.Id)
157           <fieldset class="form-group">
158             <!-- begin row -->
159             <!--名稱-->
160             <div class="row h-100 justify-content-center align-items-center">
161               <label class="col-md-2 pr-1 form-label text-right text-danger">@Html.DisplayNameFor(model => model.Name)</label>
162               <div class="col-md-4 mb-1 pl-1">
163                 <input id="@Html.IdFor(model => model.Name)"
164                        name="@Html.NameFor(model => model.Name)"
165                        value="@Html.ValueFor(model => model.Name)"
166                        tabindex="0" required
167                        class="easyui-textbox"
168                        style="width:100%"
169                        type="text"
170                        data-options="prompt:'@Html.DescriptionFor(model => model.Name)',
171                                  required:true,
172                                  validType: 'length[0,50]'
173                                  " />
174               </div>
175               <label class="col-md-2 pr-1 form-label text-right text-danger">@Html.DisplayNameFor(model => model.Code)</label>
176               <div class="col-md-4 mb-1 pl-1">
177                 <input id="@Html.IdFor(model => model.Code)"
178                        name="@Html.NameFor(model => model.Code)"
179                        value="@Html.ValueFor(model => model.Code)"
180                        tabindex="1" required
181                        class="easyui-textbox"
182                        style="width:100%"
183                        type="text"
184                        data-options="prompt:'@Html.DescriptionFor(model => model.Code)',
185                                  required:true,
186                                  validType: 'length[0,12]'
187                                  " />
188               </div>
189               <label class="col-md-2 pr-1 form-label text-right">@Html.DisplayNameFor(model => model.Address)</label>
190               <div class="col-md-4 mb-1 pl-1">
191                 <input id="@Html.IdFor(model => model.Address)"
192                        name="@Html.NameFor(model => model.Address)"
193                        value="@Html.ValueFor(model => model.Address)"
194                        tabindex="2"
195                        class="easyui-textbox"
196                        style="width:100%"
197                        type="text"
198                        data-options="prompt:'@Html.DescriptionFor(model => model.Address)',
199                                  required:false,
200                                  validType: 'length[0,50]'
201                                  " />
202               </div>
203               <label class="col-md-2 pr-1 form-label text-right">@Html.DisplayNameFor(model => model.Contect)</label>
204               <div class="col-md-4 mb-1 pl-1">
205                 <input id="@Html.IdFor(model => model.Contect)"
206                        name="@Html.NameFor(model => model.Contect)"
207                        value="@Html.ValueFor(model => model.Contect)"
208                        tabindex="3"
209                        class="easyui-textbox"
210                        style="width:100%"
211                        type="text"
212                        data-options="prompt:'@Html.DescriptionFor(model => model.Contect)',
213                                  required:false,
214                                  validType: 'length[0,12]'
215                                  " />
216               </div>
217               <label class="col-md-2 pr-1 form-label text-right">@Html.DisplayNameFor(model => model.PhoneNumber)</label>
218               <div class="col-md-4 mb-1 pl-1">
219                 <input id="@Html.IdFor(model => model.PhoneNumber)"
220                        name="@Html.NameFor(model => model.PhoneNumber)"
221                        value="@Html.ValueFor(model => model.PhoneNumber)"
222                        tabindex="4"
223                        class="easyui-textbox"
224                        style="width:100%"
225                        type="text"
226                        data-options="prompt:'@Html.DescriptionFor(model => model.PhoneNumber)',
227                                  required:false,
228                                  validType: 'length[0,20]'
229                                  " />
230               </div>
231               <label class="col-md-2 pr-1 form-label text-right text-danger">@Html.DisplayNameFor(model => model.RegisterDate)</label>
232               <div class="col-md-4 mb-1 pl-1">
233                 <input id="@Html.IdFor(model => model.RegisterDate)"
234                        name="@Html.NameFor(model => model.RegisterDate)"
235                        value="@Html.ValueFor(model => model.RegisterDate)"
236                        tabindex="5" required
237                        class="easyui-datebox"
238                        style="width:100%"
239                        type="text"
240                        data-options="prompt:'@Html.DescriptionFor(model => model.RegisterDate)',
241                                  required:true,
242                                  formatter:dateformatter" />
243               </div>
244             </div>
245           </fieldset>
246         </form>
247       </div>
248     </div>
249   </div>
250 </div>
251 
252  
253 @await Component.InvokeAsync("ImportExcel", new ImportExcelOptions { entity="Company",
254   folder="Companies",
255   url="/Companies/ImportExcel",
256   tpl="/Companies/Download"
257 
258 
259 })
260 
261 @section HeadBlock {
262   <link href="~/css/notifications/toastr/toastr.css" rel="stylesheet" asp-append-version="true" />
263   <link href="~/css/formplugins/bootstrap-daterangepicker/bootstrap-daterangepicker.css" rel="stylesheet" asp-append-version="true" />
264   <link href="~/js/easyui/themes/insdep/easyui.css" rel="stylesheet" asp-append-version="true" />
265 }
266 @section ScriptsBlock {
267   <script src="~/js/dependency/moment/moment.js" asp-append-version="true"></script>
268   <script src="~/js/notifications/toastr/toastr.js"></script>
269   <script src="~/js/formplugins/bootstrap-daterangepicker/bootstrap-daterangepicker.js" asp-append-version="true"></script>
270   <script src="~/js/easyui/jquery.easyui.min.js" asp-append-version="true"></script>
271   <script src="~/js/easyui/plugins/datagrid-filter.js" asp-append-version="true"></script>
272   <script src="~/js/easyui/plugins/columns-ext.js" asp-append-version="true"></script>
273   <script src="~/js/easyui/plugins/columns-reset.js" asp-append-version="true"></script>
274   <script src="~/js/easyui/locale/easyui-lang-zh_CN.js" asp-append-version="true"></script>
275   <script src="~/js/easyui/jquery.easyui.component.js" asp-append-version="true"></script>
276   <script src="~/js/plugin/filesaver/FileSaver.js" asp-append-version="true"></script>
277   <script src="~/js/plugin/jquery.serializejson/jquery.serializejson.js" asp-append-version="true"></script>
278   <script src="~/js/jquery.custom.extend.js" asp-append-version="true"></script>
279   <script src="~/js/jquery.extend.formatter.js" asp-append-version="true"></script>
280   <script>
281         var $dg = $('#companies_datagrid');
282         var EDITINLINE = true;
283         var company = null;
284     var editIndex = undefined;
285     //下載Excel導入模板
286 
287     //執行導出下載Excel
288     function exportexcel() {
289       const filterRules = JSON.stringify($dg.datagrid('options').filterRules);
290       console.log(filterRules);
291       $.messager.progress({ title: '請等待',msg:'正在執行導出...' });
292       let formData = new FormData();
293       formData.append('filterRules', filterRules);
294       formData.append('sort', 'Id');
295       formData.append('order', 'asc');
296       $.postDownload('/Companies/ExportExcel', formData).then(res => {
297         $.messager.progress('close');
298         toastr.success('導出成功!');
299       }).catch(err => {
300         //console.log(err);
301         $.messager.progress('close');
302         $.messager.alert('導出失敗', err.statusText, 'error');
303       });
304 
305     }
306             //彈出明細信息
307     function showdetailswindow(id, index) {
308       const company = $dg.datagrid('getRows')[index];
309       opencompanydetailwindow(company, 'Modified');
310     }
311         function reload() {
312                $dg.datagrid('uncheckAll');
313                $dg.datagrid('reload');
314         }
315             //新增記錄
316        function append() {
317                 company = {
318                     Address: '-',
319                     RegisterDate: moment().format('YYYY-MM-DD HH:mm:ss'),
320                 };
321                 if (!EDITINLINE) {
322                     //彈出新增窗口
323                     opencompanydetailwindow(company, 'Added');
324                 } else {
325                     if (endEditing()) {
326                         //對必填字段進行默認值初始化
327                        $dg.datagrid('insertRow',
328                             {
329                                 index: 0,
330                                 row: company
331                             });
332                         editIndex = 0;
333                        $dg.datagrid('selectRow', editIndex)
334                             .datagrid('beginEdit', editIndex);
335                         hook = true;
336                     }
337                 }
338         }
339             //刪除編輯的行
340         function removeit() {
341                 if (this.$dg.datagrid('getChecked').length <= 0 && EDITINLINE) {
342                     if (editIndex !== undefined) {
343                         const delindex = editIndex;
344                        $dg.datagrid('cancelEdit', delindex)
345                             .datagrid('deleteRow', delindex);
346                         hook = true;
347                     } else {
348                         const rows =$dg.datagrid('getChecked');
349                         rows.slice().reverse().forEach(row => {
350                             const rowindex =$dg.datagrid('getRowIndex', row);
351                            $dg.datagrid('deleteRow', rowindex);
352                             hook = true;
353                         });
354                     }
355                 } else {
356                     deletechecked();
357                 }
358         }
359             //刪除該行
360         function deleteRow(id) {
361             $.messager.confirm('確認', '你確定要刪除該記錄?', result => {
362                 if (result) {
363                     dodeletechecked([id]);
364                 }
365             })
366         }
367             //刪除選中的行
368          function deletechecked() {
369                 const id =$dg.datagrid('getChecked').filter(item => item.Id != null && item.Id > 0).map(item => {
370                     return item.Id;
371                 });
372                 if (id.length > 0) {
373                     $.messager.confirm('確認', `你確定要刪除這 <span class='badge badge-icon position-relative'>${id.length} </span> 行記錄?`, result => {
374                         if (result) {
375                             dodeletechecked(id);
376                         }
377                     });
378                 } else {
379                     $.messager.alert('提示', '請先選擇要刪除的記錄!', 'question');
380                 }
381         }
382             //執行刪除
383         function dodeletechecked(id) {
384             $.post('/Companies/DeleteChecked', { id: id })
385                 .done(response => {
386                     if (response.success) {
387                         toastr.error(`成功刪除[${id.length}]行記錄`);
388                         reload();
389                     } else {
390                         $.messager.alert('錯誤', response.err, 'error');
391                     }
392                 })
393                 .fail((jqXHR, textStatus, errorThrown) => {
394                     $.messager.alert('異常', `${jqXHR.status}: ${jqXHR.statusText} `, 'error');
395                 });
396         }
397             //開啟編輯狀態
398     function onClickCell(index, field) {
399 
400       company = $dg.datagrid('getRows')[index];
401       const _actions = ['action', 'ck'];
402       if (!EDITINLINE || $.inArray(field, _actions) >= 0) {
403         return;
404       }
405 
406       if (editIndex !== index) {
407         if (endEditing()) {
408           $dg.datagrid('selectRow', index)
409             .datagrid('beginEdit', index);
410           hook = true;
411           editIndex = index;
412           const ed = $dg.datagrid('getEditor', { index: index, field: field });
413           if (ed) {
414             ($(ed.target).data('textbox') ? $(ed.target).textbox('textbox') : $(ed.target)).focus();
415           }
416         } else {
417           $dg.datagrid('selectRow', editIndex);
418         }
419       }
420     }
421             //關閉編輯狀態
422     function endEditing() {
423 
424             if (editIndex === undefined) {
425                 return true;
426             }
427             if (this.$dg.datagrid('validateRow', editIndex)) {
428                 $dg.datagrid('endEdit', editIndex);
429                 return true;
430             } else {
431                 const invalidinput = $('input.validatebox-invalid', $dg.datagrid('getPanel'));
432                 const fieldnames = invalidinput.map((index, item) => {
433                     return $(item).attr('placeholder') || $(item).attr('id');
434                 });
435                 $.messager.alert('提示', `${Array.from(fieldnames)} 輸入有誤.`, 'error');
436                 return false;
437             }
438         }
439             //提交保存後台數據庫
440         function acceptChanges() {
441             if (endEditing()) {
442                 if ($dg.datagrid('getChanges').length > 0) {
443                     const inserted = $dg.datagrid('getChanges', 'inserted').map(item => {
444                         item.TrackingState = 1;
445                         return item;
446                     });
447                     const updated = $dg.datagrid('getChanges', 'updated').map(item => {
448                         item.TrackingState = 2
449                         return item;
450                     });
451                     const deleted = $dg.datagrid('getChanges', 'deleted').map(item => {
452                         item.TrackingState = 3
453                         return item;
454                     });
455                     //過濾已刪除的重複項
456                     const changed = inserted.concat(updated.filter(item => {
457                         return !deleted.includes(item);
458                     })).concat(deleted);
459                     //$.messager.progress({ title: '請等待', msg: '正在保存數據...', interval: 200 });
460                     $.post('/Companies/AcceptChanges', { companies: changed })
461                         .done(response => {
462                             //$.messager.progress('close');
463                             //console.log(response);
464                             if (response.success) {
465                                 toastr.success('保存成功');
466                                 $dg.datagrid('acceptChanges');
467                                 reload();
468                                 hook = false;
469                             } else {
470                                 $.messager.alert('錯誤', response.err, 'error');
471                             }
472                         })
473                         .fail((jqXHR, textStatus, errorThrown) => {
474                             //$.messager.progress('close');
475                             $.messager.alert('異常', `${jqXHR.status}: ${jqXHR.statusText} `, 'error');
476                         });
477                 }
478             }
479         }
480         function rejectChanges() {
481             $dg.datagrid('rejectChanges');
482             editIndex = undefined;
483             hook = false;
484         }
485     $(document).ready(function () {
486       //定義datagrid結構
487       $dg.datagrid({
488         rownumbers: true,
489         checkOnSelect: false,
490         selectOnCheck: false,
491         idField: 'Id',
492         sortName: 'Id',
493         sortOrder: 'desc',
494         remoteFilter: true,
495         singleSelect: true,
496         method: 'get',
497         onClickCell: onClickCell,
498         clientPaging: false,
499         pagination: true,
500         striped: true,
501         filterRules: [],
502         onHeaderContextMenu: function (e, field) {
503           e.preventDefault();
504           $(this).datagrid('columnMenu').menu('show', {
505             left: e.pageX,
506             top: e.pageY
507           });
508         },
509         onBeforeLoad: function () {
510           const that = $(this);
511           document.addEventListener('panel.onfullscreen', () => {
512             setTimeout(() => {
513               that.datagrid('resize');
514             }, 200)
515           })
516         },
517         onLoadSuccess: function (data) {
518           editIndex = undefined;
519           $("button[name*='deletebutton']").prop('disabled', true);
520           $("button[name*='savebutton']").prop('disabled', true);
521           $("button[name*='cancelbutton']").prop('disabled', true);
522         },
523         onCheck: function () {
524           $("button[name*='deletebutton']").prop('disabled', false);
525         },
526         onUncheck: function () {
527           const checked = $(this).datagrid('getChecked').length > 0;
528           $("button[name*='deletebutton']").prop('disabled', !checked);
529         },
530         onSelect: function (index, row) {
531           company = row;
532         },
533         onBeginEdit: function (index, row) {
534           //const editors = $(this).datagrid('getEditors', index);
535 
536         },
537         onEndEdit: function (index, row) {
538           editIndex = undefined;
539         },
540         onBeforeEdit: function (index, row) {
541           editIndex = index;
542           row.editing = true;
543           $("button[name*='deletebutton']").prop('disabled', false);
544           $("button[name*='cancelbutton']").prop('disabled', false);
545           $("button[name*='savebutton']").prop('disabled', false);
546           $(this).datagrid('refreshRow', index);
547         },
548         onAfterEdit: function (index, row) {
549           row.editing = false;
550           editIndex = undefined;
551           $(this).datagrid('refreshRow', index);
552         },
553         onCancelEdit: function (index, row) {
554           row.editing = false;
555           editIndex = undefined;
556           $("button[name*='deletebutton']").prop('disabled', true);
557           $("button[name*='savebutton']").prop('disabled', true);
558           $("button[name*='cancelbutton']").prop('disabled', true);
559           $(this).datagrid('refreshRow', index);
560         },
561         frozenColumns: [[
562           /*開啟CheckBox選擇功能*/
563           { field: 'ck', checkbox: true },
564           {
565             field: 'action',
566             title: '操作',
567             width: 85,
568             sortable: false,
569             resizable: true,
570             formatter: function showdetailsformatter(value, row, index) {
571               if (!row.editing) {
572                 return `<div class="btn-group">\
573                                                          <button onclick="showdetailswindow('${row.Id}',  ${index})" class="btn btn-primary btn-sm btn-icon waves-effect waves-themed" title="查看明細" ><i class="fal fa-edit"></i> </button>\
574                                                          <button onclick="deleteRow('${row.Id}',${index})" class="btn btn-primary btn-sm btn-icon waves-effect waves-themed" title="刪除記錄" ><i class="fal fa-times"></i> </button>\
575                                                     </div>`;
576               } else {
577                 return `<button class="btn btn-primary btn-sm btn-icon waves-effect waves-themed" disabled title="查看明細"  ><i class="fal fa-edit"></i> </button>`;
578               }
579             }
580           }
581         ]],
582         columns: [[
583 
584           {    /*名稱*/
585             field: 'Name',
586             title: '<span class="required">@Html.DisplayNameFor(model => model.Name)</span>',
587             width: 200,
588             hidden: false,
589             editor: {
590               type: 'textbox',
591               options: { prompt: '@Html.DescriptionFor(model => model.Name)', required: true, validType: 'length[0,50]' }
592             },
593             sortable: true,
594             resizable: true
595           },
596           {    /*組織代碼*/
597             field: 'Code',
598             title: '<span class="required">@Html.DisplayNameFor(model => model.Code)</span>',
599             width: 120,
600             hidden: false,
601             editor: {
602               type: 'textbox',
603               options: { prompt: '@Html.DescriptionFor(model => model.Code)', required: true, validType: 'length[0,12]' }
604             },
605             sortable: true,
606             resizable: true
607           },
608           {    /*地址*/
609             field: 'Address',
610             title: '@Html.DisplayNameFor(model => model.Address)',
611             width: 200,
612             hidden: false,
613             editor: {
614               type: 'textbox',
615               options: { prompt: '@Html.DescriptionFor(model => model.Address)', required: false, validType: 'length[0,50]' }
616             },
617             sortable: true,
618             resizable: true
619           },
620           {    /*聯繫人*/
621             field: 'Contect',
622             title: '@Html.DisplayNameFor(model => model.Contect)',
623             width: 120,
624             hidden: false,
625             editor: {
626               type: 'textbox',
627               options: { prompt: '@Html.DescriptionFor(model => model.Contect)', required: false, validType: 'length[0,12]' }
628             },
629             sortable: true,
630             resizable: true
631           },
632           {    /*聯繫電話*/
633             field: 'PhoneNumber',
634             title: '@Html.DisplayNameFor(model => model.PhoneNumber)',
635             width: 120,
636             hidden: false,
637             editor: {
638               type: 'textbox',
639               options: { prompt: '@Html.DescriptionFor(model => model.PhoneNumber)', required: false, validType: 'length[0,20]' }
640             },
641             sortable: true,
642             resizable: true
643           },
644           {   /*註冊日期*/
645             field: 'RegisterDate',
646             title: '<span class="required">@Html.DisplayNameFor(model => model.RegisterDate)</span>',
647             width: 140,
648             align: 'right',
649             hidden: false,
650             editor: {
651               type: 'datebox',
652               options: { prompt: '@Html.DescriptionFor(model => model.RegisterDate)', required: true }
653             },
654             formatter: dateformatter,
655             sortable: true,
656             resizable: true
657           },
658         ]]
659       }).datagrid('columnMoving')
660         .datagrid('resetColumns')
661         .datagrid('enableFilter', [
662           {   /*Id*/
663             field: 'Id',
664             type: 'numberbox',
665             op: ['equal', 'notequal', 'less', 'lessorequal', 'greater', 'greaterorequal']
666           },
667           {     /*註冊日期*/
668             field: 'RegisterDate',
669             type: 'dateRange',
670             options: {
671               onChange: value => {
672                 $dg.datagrid('addFilterRule', {
673                   field: 'RegisterDate',
674                   op: 'between',
675                   value: value
676                 });
677 
678                 $dg.datagrid('doFilter');
679               }
680             }
681           },
682         ])
683         .datagrid('load', '/Companies/GetData');
684     }
685     );
686 
687   </script>
688   <script type="text/javascript">
689     //判斷新增編輯狀態
690     var MODELSTATE = 'Added';
691     var companyid = null;
692     function opencompanydetailwindow(data, state) {
693       MODELSTATE = state;
694       initcompanydetailview();
695       companyid = (data.Id || 0);
696       $("#companydetailwindow").window("open");
697       $('#company_form').form('reset');
698       $('#company_form').form('load', data);
699     }
700     //刪除當前記錄
701     function deletecompanyitem() {
702       $.messager.confirm('確認', '你確定要刪除該記錄?', result => {
703         if (result) {
704           const url = `/Companies/Delete/${companyid}`;
705           $.get(url).done(res => {
706             if (res.success) {
707               toastr.success("刪除成功");
708               $("#companydetailwindow").window("close");
709               reload();
710             } else {
711               $.messager.alert("錯誤", res.err, "error");
712             }
713           });
714         }
715       });
716     }
717     //async 保存數據
718     async function savecompanyitem() {
719       const $companyform = $('#company_form');
720       if ($companyform.form('enableValidation').form('validate')) {
721         let company = $companyform.serializeJSON();
722         let url = '/Companies/Edit';
723         //判斷是新增或是修改方法
724         if (MODELSTATE === 'Added') {
725           url = '/Companies/Create';
726         }
727         var token = $('input[name="__RequestVerificationToken"]', $companyform).val();
728         //$.messager.progress({ title: '請等待', msg: '正在保存數據...', interval: 200 });
729         $.ajax({
730           type: "POST",
731           url: url,
732           data: {
733             __RequestVerificationToken: token,
734             company: company
735           },
736           dataType: 'json',
737           contentType: 'application/x-www-form-urlencoded; charset=utf-8'
738         })
739           .done(response => {
740             //$.messager.progress('close');
741             if (response.success) {
742               hook = false;
743               $companyform.form('disableValidation');
744               $dg.datagrid('reload');
745               $('#companydetailwindow').window("close");
746               toastr.success("保存成功");
747             } else {
748               $.messager.alert("錯誤", response.err, "error");
749             }
750           })
751           .fail((jqXHR, textStatus, errorThrown) => {
752             //$.messager.progress('close');
753             $.messager.alert('異常', `${jqXHR.status}: ${jqXHR.statusText} `, 'error');
754           });
755       }
756     }
757     //關閉窗口
758     function closecompanydetailwindow() {
759       $('#companydetailwindow').window('close');
760     }
761 
762     //判斷是否有沒有保存的記錄
763     function companyhasmodified() {
764       return hook;
765     }
766 
767 
768     function initcompanydetailview() {
769       //判斷是否显示功能按鈕
770       if (MODELSTATE === 'Added') {
771         $('#deleteitem-btn-group').hide();
772       } else {
773         $('#deleteitem-btn-group').show();
774       }
775 
776       //回車光標移動到下個輸入控件
777       //日期類型 註冊日期
778       $('#RegisterDate').datebox('textbox').bind('keydown', function (e) {
779         if (e.keyCode == 13) {
780           $(e.target).emulateTab();
781         }
782       });
783     }
784   </script>
785 }

View Code

 

上面View層的代碼非常的複雜,但都是固定格式,可以用scaffold快速生成

  • 配置依賴注入(DI),註冊服務

打開 startup.cs 在 public void ConfigureServices(IServiceCollection services) 註冊服務 services.AddScoped<IRepositoryX, RepositoryX>(); 
services.AddScoped<ICustomerService, CustomerService>();

  • 更新數據庫

EF Core Code-First 同步更新數據庫 
在 Visual Studio.Net 
Package Manager Controle 運行 
PM>:add-migration create_Company 
PM>:update-database 
PM>:更新完成

  • Debug 運行項目 

高級應用

CAP 分佈式事務的解決方案及應用場景 
nuget 安裝組件 
PM> Install-Package DotNetCore.CAP 
PM> Install-Package DotNetCore.CAP.RabbitMQ 
PM> Install-Package DotNetCore.CAP.SqlServer \

  • 配置Startup.cs
 1 public void ConfigureServices(IServiceCollection services)
 2     {
 3       services.AddCap(x =>
 4       {
 5         x.UseEntityFramework<SmartDbContext>();
 6         x.UseRabbitMQ("127.0.0.1");
 7         x.UseDashboard();
 8         x.FailedRetryCount = 5;
 9         x.FailedThresholdCallback = failed =>
10         {
11           var logger = failed.ServiceProvider.GetService<ILogger<Startup>>();
12           logger.LogError($@"A message of type {failed.MessageType} failed after executing {x.FailedRetryCount} several times, 
13                         requiring manual troubleshooting. Message name: {failed.Message.GetName()}");
14         };
15       });
16     }

View Code

 

  • 發布消息
  • 訂閱消息

roadmap

  • 完善主要的開發文檔
  • 支持My SQL數據庫
  • 還會繼續重構和完善代碼
  • 開發Scaffold MVC模板,生成定製化的Controller 和 View 減少開發人員重複工作
  • 完善授權訪問策略(policy-based authorization)
  • 開發Visual Sutdio.net代碼生成插件(類似國內做比較好的52abp)

我的聯繫方式,qq群,贊助二維碼

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

【其他文章推薦】

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

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

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

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

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

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